mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-16 11:42:22 +08:00
Merge branch 'local_feature/config_v2' into feature/config_v2
# Conflicts: # BililiveRecorder.Core/DanmakuModel.cs # BililiveRecorder.Core/RecordedRoom.cs
This commit is contained in:
commit
f041abc837
|
@ -5,7 +5,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Autofac;
|
||||
using BililiveRecorder.Core;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.Core.Config.V1;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
using CommandLine;
|
||||
using Newtonsoft.Json;
|
||||
|
|
3
BililiveRecorder.Core/AssemblyAttribute.cs
Normal file
3
BililiveRecorder.Core/AssemblyAttribute.cs
Normal file
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("BililiveRecorder.UnitTest.Core")]
|
|
@ -1,11 +1,12 @@
|
|||
using BililiveRecorder.Core.Config;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Xml;
|
||||
using BililiveRecorder.Core.Config.V2;
|
||||
|
||||
#nullable enable
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
public class BasicDanmakuWriter : IBasicDanmakuWriter
|
||||
|
@ -22,12 +23,12 @@ namespace BililiveRecorder.Core
|
|||
private static readonly Regex invalidXMLChars = new Regex(@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]", RegexOptions.Compiled);
|
||||
private static string RemoveInvalidXMLChars(string text) => string.IsNullOrEmpty(text) ? string.Empty : invalidXMLChars.Replace(text, string.Empty);
|
||||
|
||||
private XmlWriter xmlWriter = null;
|
||||
private XmlWriter? xmlWriter = null;
|
||||
private DateTimeOffset offset = DateTimeOffset.UtcNow;
|
||||
private uint writeCount = 0;
|
||||
private readonly ConfigV1 config;
|
||||
private readonly RoomConfig config;
|
||||
|
||||
public BasicDanmakuWriter(ConfigV1 config)
|
||||
public BasicDanmakuWriter(RoomConfig config)
|
||||
{
|
||||
this.config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
}
|
||||
|
@ -36,62 +37,63 @@ namespace BililiveRecorder.Core
|
|||
|
||||
public void EnableWithPath(string path, IRecordedRoom recordedRoom)
|
||||
{
|
||||
if (disposedValue) return;
|
||||
if (this.disposedValue) return;
|
||||
|
||||
semaphoreSlim.Wait();
|
||||
this.semaphoreSlim.Wait();
|
||||
try
|
||||
{
|
||||
if (xmlWriter != null)
|
||||
if (this.xmlWriter != null)
|
||||
{
|
||||
xmlWriter.Close();
|
||||
xmlWriter.Dispose();
|
||||
xmlWriter = null;
|
||||
this.xmlWriter.Close();
|
||||
this.xmlWriter.Dispose();
|
||||
this.xmlWriter = null;
|
||||
}
|
||||
|
||||
try { Directory.CreateDirectory(Path.GetDirectoryName(path)); } catch (Exception) { }
|
||||
var stream = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.Read);
|
||||
|
||||
xmlWriter = XmlWriter.Create(stream, xmlWriterSettings);
|
||||
WriteStartDocument(xmlWriter, recordedRoom);
|
||||
offset = DateTimeOffset.UtcNow;
|
||||
writeCount = 0;
|
||||
this.xmlWriter = XmlWriter.Create(stream, xmlWriterSettings);
|
||||
this.WriteStartDocument(this.xmlWriter, recordedRoom);
|
||||
this.offset = DateTimeOffset.UtcNow;
|
||||
this.writeCount = 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphoreSlim.Release();
|
||||
this.semaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
if (disposedValue) return;
|
||||
if (this.disposedValue) return;
|
||||
|
||||
semaphoreSlim.Wait();
|
||||
this.semaphoreSlim.Wait();
|
||||
try
|
||||
{
|
||||
if (xmlWriter != null)
|
||||
if (this.xmlWriter != null)
|
||||
{
|
||||
xmlWriter.Close();
|
||||
xmlWriter.Dispose();
|
||||
xmlWriter = null;
|
||||
this.xmlWriter.Close();
|
||||
this.xmlWriter.Dispose();
|
||||
this.xmlWriter = null;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphoreSlim.Release();
|
||||
this.semaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(DanmakuModel danmakuModel)
|
||||
{
|
||||
if (disposedValue) return;
|
||||
if (this.disposedValue) return;
|
||||
|
||||
semaphoreSlim.Wait();
|
||||
this.semaphoreSlim.Wait();
|
||||
try
|
||||
{
|
||||
if (xmlWriter != null)
|
||||
if (this.xmlWriter != null)
|
||||
{
|
||||
var write = true;
|
||||
var recordDanmakuRaw = this.config.RecordDanmakuRaw;
|
||||
switch (danmakuModel.MsgType)
|
||||
{
|
||||
case MsgTypeEnum.Comment:
|
||||
|
@ -100,58 +102,58 @@ namespace BililiveRecorder.Core
|
|||
var size = danmakuModel.RawObj?["info"]?[0]?[2]?.ToObject<int>() ?? 25;
|
||||
var color = danmakuModel.RawObj?["info"]?[0]?[3]?.ToObject<int>() ?? 0XFFFFFF;
|
||||
var st = danmakuModel.RawObj?["info"]?[0]?[4]?.ToObject<long>() ?? 0L;
|
||||
var ts = Math.Max((DateTimeOffset.FromUnixTimeMilliseconds(st) - offset).TotalSeconds, 0d);
|
||||
var ts = Math.Max((DateTimeOffset.FromUnixTimeMilliseconds(st) - this.offset).TotalSeconds, 0d);
|
||||
|
||||
xmlWriter.WriteStartElement("d");
|
||||
xmlWriter.WriteAttributeString("p", $"{ts},{type},{size},{color},{st},0,{danmakuModel.UserID},0");
|
||||
xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
if (config.RecordDanmakuRaw)
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["info"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteValue(RemoveInvalidXMLChars(danmakuModel.CommentText));
|
||||
xmlWriter.WriteEndElement();
|
||||
this.xmlWriter.WriteStartElement("d");
|
||||
this.xmlWriter.WriteAttributeString("p", $"{ts},{type},{size},{color},{st},0,{danmakuModel.UserID},0");
|
||||
this.xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
if (recordDanmakuRaw)
|
||||
this.xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["info"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
this.xmlWriter.WriteValue(RemoveInvalidXMLChars(danmakuModel.CommentText));
|
||||
this.xmlWriter.WriteEndElement();
|
||||
}
|
||||
break;
|
||||
case MsgTypeEnum.SuperChat:
|
||||
if (config.RecordDanmakuSuperChat)
|
||||
if (this.config.RecordDanmakuSuperChat)
|
||||
{
|
||||
xmlWriter.WriteStartElement("sc");
|
||||
var ts = Math.Max((DateTimeOffset.UtcNow - offset).TotalSeconds, 0d);
|
||||
xmlWriter.WriteAttributeString("ts", ts.ToString());
|
||||
xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
xmlWriter.WriteAttributeString("price", danmakuModel.Price.ToString());
|
||||
xmlWriter.WriteAttributeString("time", danmakuModel.SCKeepTime.ToString());
|
||||
if (config.RecordDanmakuRaw)
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteValue(RemoveInvalidXMLChars(danmakuModel.CommentText));
|
||||
xmlWriter.WriteEndElement();
|
||||
this.xmlWriter.WriteStartElement("sc");
|
||||
var ts = Math.Max((DateTimeOffset.UtcNow - this.offset).TotalSeconds, 0d);
|
||||
this.xmlWriter.WriteAttributeString("ts", ts.ToString());
|
||||
this.xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
this.xmlWriter.WriteAttributeString("price", danmakuModel.Price.ToString());
|
||||
this.xmlWriter.WriteAttributeString("time", danmakuModel.SCKeepTime.ToString());
|
||||
if (recordDanmakuRaw)
|
||||
this.xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
this.xmlWriter.WriteValue(RemoveInvalidXMLChars(danmakuModel.CommentText));
|
||||
this.xmlWriter.WriteEndElement();
|
||||
}
|
||||
break;
|
||||
case MsgTypeEnum.GiftSend:
|
||||
if (config.RecordDanmakuGift)
|
||||
if (this.config.RecordDanmakuGift)
|
||||
{
|
||||
xmlWriter.WriteStartElement("gift");
|
||||
var ts = Math.Max((DateTimeOffset.UtcNow - offset).TotalSeconds, 0d);
|
||||
xmlWriter.WriteAttributeString("ts", ts.ToString());
|
||||
xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
xmlWriter.WriteAttributeString("giftname", danmakuModel.GiftName);
|
||||
xmlWriter.WriteAttributeString("giftcount", danmakuModel.GiftCount.ToString());
|
||||
if (config.RecordDanmakuRaw)
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteEndElement();
|
||||
this.xmlWriter.WriteStartElement("gift");
|
||||
var ts = Math.Max((DateTimeOffset.UtcNow - this.offset).TotalSeconds, 0d);
|
||||
this.xmlWriter.WriteAttributeString("ts", ts.ToString());
|
||||
this.xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
this.xmlWriter.WriteAttributeString("giftname", danmakuModel.GiftName);
|
||||
this.xmlWriter.WriteAttributeString("giftcount", danmakuModel.GiftCount.ToString());
|
||||
if (recordDanmakuRaw)
|
||||
this.xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
this.xmlWriter.WriteEndElement();
|
||||
}
|
||||
break;
|
||||
case MsgTypeEnum.GuardBuy:
|
||||
if (config.RecordDanmakuGuard)
|
||||
if (this.config.RecordDanmakuGuard)
|
||||
{
|
||||
xmlWriter.WriteStartElement("guard");
|
||||
var ts = Math.Max((DateTimeOffset.UtcNow - offset).TotalSeconds, 0d);
|
||||
xmlWriter.WriteAttributeString("ts", ts.ToString());
|
||||
xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
xmlWriter.WriteAttributeString("level", danmakuModel.UserGuardLevel.ToString()); ;
|
||||
xmlWriter.WriteAttributeString("count", danmakuModel.GiftCount.ToString());
|
||||
if (config.RecordDanmakuRaw)
|
||||
xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
xmlWriter.WriteEndElement();
|
||||
this.xmlWriter.WriteStartElement("guard");
|
||||
var ts = Math.Max((DateTimeOffset.UtcNow - this.offset).TotalSeconds, 0d);
|
||||
this.xmlWriter.WriteAttributeString("ts", ts.ToString());
|
||||
this.xmlWriter.WriteAttributeString("user", danmakuModel.UserName);
|
||||
this.xmlWriter.WriteAttributeString("level", danmakuModel.UserGuardLevel.ToString()); ;
|
||||
this.xmlWriter.WriteAttributeString("count", danmakuModel.GiftCount.ToString());
|
||||
if (recordDanmakuRaw)
|
||||
this.xmlWriter.WriteAttributeString("raw", danmakuModel.RawObj?["data"]?.ToString(Newtonsoft.Json.Formatting.None));
|
||||
this.xmlWriter.WriteEndElement();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
@ -159,16 +161,16 @@ namespace BililiveRecorder.Core
|
|||
break;
|
||||
}
|
||||
|
||||
if (write && writeCount++ >= config.RecordDanmakuFlushInterval)
|
||||
if (write && this.writeCount++ >= this.config.RecordDanmakuFlushInterval)
|
||||
{
|
||||
xmlWriter.Flush();
|
||||
writeCount = 0;
|
||||
this.xmlWriter.Flush();
|
||||
this.writeCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
semaphoreSlim.Release();
|
||||
this.semaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -204,26 +206,26 @@ namespace BililiveRecorder.Core
|
|||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
if (!this.disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// dispose managed state (managed objects)
|
||||
semaphoreSlim.Dispose();
|
||||
xmlWriter?.Close();
|
||||
xmlWriter?.Dispose();
|
||||
this.semaphoreSlim.Dispose();
|
||||
this.xmlWriter?.Close();
|
||||
this.xmlWriter?.Dispose();
|
||||
}
|
||||
|
||||
// free unmanaged resources (unmanaged objects) and override finalizer
|
||||
// set large fields to null
|
||||
disposedValue = true;
|
||||
this.disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
this.Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ using System.Linq;
|
|||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.Core.Config.V2;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NLog;
|
||||
|
||||
|
@ -20,26 +20,24 @@ namespace BililiveRecorder.Core
|
|||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly Random random = new Random();
|
||||
|
||||
private readonly ConfigV1 Config;
|
||||
private readonly GlobalConfig globalConfig;
|
||||
private readonly HttpClient danmakuhttpclient;
|
||||
private HttpClient httpclient;
|
||||
|
||||
public BililiveAPI(ConfigV1 config)
|
||||
public BililiveAPI(GlobalConfig globalConfig)
|
||||
{
|
||||
Config = config;
|
||||
Config.PropertyChanged += (sender, e) =>
|
||||
this.globalConfig = globalConfig;
|
||||
this.globalConfig.PropertyChanged += (sender, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(Config.Cookie))
|
||||
{
|
||||
ApplyCookieSettings(Config.Cookie);
|
||||
}
|
||||
if (e.PropertyName == nameof(this.globalConfig.Cookie))
|
||||
this.ApplyCookieSettings(this.globalConfig.Cookie);
|
||||
};
|
||||
ApplyCookieSettings(Config.Cookie);
|
||||
this.ApplyCookieSettings(this.globalConfig.Cookie);
|
||||
|
||||
danmakuhttpclient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
|
||||
danmakuhttpclient.DefaultRequestHeaders.Add("Accept", HTTP_HEADER_ACCEPT);
|
||||
danmakuhttpclient.DefaultRequestHeaders.Add("Referer", HTTP_HEADER_REFERER);
|
||||
danmakuhttpclient.DefaultRequestHeaders.Add("User-Agent", Utils.UserAgent);
|
||||
this.danmakuhttpclient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
|
||||
this.danmakuhttpclient.DefaultRequestHeaders.Add("Accept", HTTP_HEADER_ACCEPT);
|
||||
this.danmakuhttpclient.DefaultRequestHeaders.Add("Referer", HTTP_HEADER_REFERER);
|
||||
this.danmakuhttpclient.DefaultRequestHeaders.Add("User-Agent", Utils.UserAgent);
|
||||
}
|
||||
|
||||
public void ApplyCookieSettings(string cookie_string)
|
||||
|
@ -61,7 +59,7 @@ namespace BililiveRecorder.Core
|
|||
pclient.DefaultRequestHeaders.Add("Referer", HTTP_HEADER_REFERER);
|
||||
pclient.DefaultRequestHeaders.Add("User-Agent", Utils.UserAgent);
|
||||
pclient.DefaultRequestHeaders.Add("Cookie", cookie_string);
|
||||
httpclient = pclient;
|
||||
this.httpclient = pclient;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -69,7 +67,7 @@ namespace BililiveRecorder.Core
|
|||
cleanclient.DefaultRequestHeaders.Add("Accept", HTTP_HEADER_ACCEPT);
|
||||
cleanclient.DefaultRequestHeaders.Add("Referer", HTTP_HEADER_REFERER);
|
||||
cleanclient.DefaultRequestHeaders.Add("User-Agent", Utils.UserAgent);
|
||||
httpclient = cleanclient;
|
||||
this.httpclient = cleanclient;
|
||||
}
|
||||
logger.Debug("设置 Cookie 成功");
|
||||
}
|
||||
|
@ -109,9 +107,9 @@ namespace BililiveRecorder.Core
|
|||
/// <exception cref="Exception"/>
|
||||
public async Task<string> GetPlayUrlAsync(int roomid)
|
||||
{
|
||||
var url = $@"{Config.LiveApiHost}/room/v1/Room/playUrl?cid={roomid}&quality=4&platform=web";
|
||||
var url = $@"{this.globalConfig.LiveApiHost}/room/v1/Room/playUrl?cid={roomid}&quality=4&platform=web";
|
||||
// 随机选择一个 url
|
||||
if ((await HttpGetJsonAsync(httpclient, url))?["data"]?["durl"] is JArray array)
|
||||
if ((await this.HttpGetJsonAsync(this.httpclient, url))?["data"]?["durl"] is JArray array)
|
||||
{
|
||||
var urls = array.Select(t => t?["url"]?.ToObject<string>());
|
||||
var distinct = urls.Distinct().ToArray();
|
||||
|
@ -134,14 +132,14 @@ namespace BililiveRecorder.Core
|
|||
{
|
||||
try
|
||||
{
|
||||
var room = await HttpGetJsonAsync(httpclient, $@"https://api.live.bilibili.com/room/v1/Room/get_info?id={roomid}");
|
||||
var room = await this.HttpGetJsonAsync(this.httpclient, $@"https://api.live.bilibili.com/room/v1/Room/get_info?id={roomid}");
|
||||
if (room?["code"]?.ToObject<int>() != 0)
|
||||
{
|
||||
logger.Warn("不能获取 {roomid} 的信息1: {errormsg}", roomid, room?["message"]?.ToObject<string>() ?? "网络超时");
|
||||
return null;
|
||||
}
|
||||
|
||||
var user = await HttpGetJsonAsync(httpclient, $@"https://api.live.bilibili.com/live_user/v1/UserInfo/get_anchor_in_room?roomid={roomid}");
|
||||
var user = await this.HttpGetJsonAsync(this.httpclient, $@"https://api.live.bilibili.com/live_user/v1/UserInfo/get_anchor_in_room?roomid={roomid}");
|
||||
if (user?["code"]?.ToObject<int>() != 0)
|
||||
{
|
||||
logger.Warn("不能获取 {roomid} 的信息2: {errormsg}", roomid, user?["message"]?.ToObject<string>() ?? "网络超时");
|
||||
|
@ -174,7 +172,7 @@ namespace BililiveRecorder.Core
|
|||
{
|
||||
try
|
||||
{
|
||||
var result = await HttpGetJsonAsync(danmakuhttpclient, $@"https://api.live.bilibili.com/room/v1/Danmu/getConf?room_id={roomid}&platform=pc&player=web");
|
||||
var result = await this.HttpGetJsonAsync(this.danmakuhttpclient, $@"https://api.live.bilibili.com/room/v1/Danmu/getConf?room_id={roomid}&platform=pc&player=web");
|
||||
|
||||
if (result?["code"]?.ToObject<int>() == 0)
|
||||
{
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" Version="4.9.4" />
|
||||
<PackageReference Include="JsonSubTypes" Version="1.8.0" />
|
||||
<PackageReference Include="HierarchicalPropertyDefault" Version="0.1.1-beta-g721d36b97c" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
||||
<PackageReference Include="NLog" Version="4.7.6" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -3,7 +3,7 @@ using System.Linq;
|
|||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.Core.Config.V2;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
|
||||
|
@ -15,7 +15,7 @@ namespace BililiveRecorder.Core.Callback
|
|||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly HttpClient client;
|
||||
|
||||
private readonly ConfigV1 Config;
|
||||
private readonly ConfigV2 Config;
|
||||
|
||||
static BasicWebhook()
|
||||
{
|
||||
|
@ -23,21 +23,21 @@ namespace BililiveRecorder.Core.Callback
|
|||
client.DefaultRequestHeaders.Add("User-Agent", $"BililiveRecorder/{typeof(BasicWebhook).Assembly.GetName().Version}-{BuildInfo.HeadShaShort}");
|
||||
}
|
||||
|
||||
public BasicWebhook(ConfigV1 config)
|
||||
public BasicWebhook(ConfigV2 config)
|
||||
{
|
||||
this.Config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
}
|
||||
|
||||
public async void Send(RecordEndData data)
|
||||
{
|
||||
var urls = this.Config.WebHookUrls;
|
||||
var urls = this.Config.Global.WebHookUrls;
|
||||
if (string.IsNullOrWhiteSpace(urls)) return;
|
||||
|
||||
var dataStr = JsonConvert.SerializeObject(data, Formatting.None);
|
||||
using var body = new ByteArrayContent(Encoding.UTF8.GetBytes(dataStr));
|
||||
body.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
|
||||
|
||||
var tasks = urls
|
||||
var tasks = urls!
|
||||
.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => x.Trim())
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
|
|
15
BililiveRecorder.Core/Config/ConfigBase.cs
Normal file
15
BililiveRecorder.Core/Config/ConfigBase.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
using JsonSubTypes;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace BililiveRecorder.Core.Config
|
||||
{
|
||||
[JsonConverter(typeof(JsonSubtypes), nameof(Version))]
|
||||
[JsonSubtypes.KnownSubType(typeof(V1.ConfigV1Wrapper), 1)]
|
||||
[JsonSubtypes.KnownSubType(typeof(V2.ConfigV2), 2)]
|
||||
public abstract class ConfigBase
|
||||
{
|
||||
[JsonProperty("version")]
|
||||
public virtual int Version { get; internal protected set; }
|
||||
}
|
||||
}
|
68
BililiveRecorder.Core/Config/ConfigMapper.cs
Normal file
68
BililiveRecorder.Core/Config/ConfigMapper.cs
Normal file
|
@ -0,0 +1,68 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
|
||||
#nullable enable
|
||||
#pragma warning disable CS0612 // obsolete
|
||||
namespace BililiveRecorder.Core.Config
|
||||
{
|
||||
internal static class ConfigMapper
|
||||
{
|
||||
public static V2.ConfigV2 Map1To2(V1.ConfigV1 v1)
|
||||
{
|
||||
var map = new Dictionary<PropertyInfo, PropertyInfo>();
|
||||
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, EnabledFeature>(map, x => x.EnabledFeature, x => x.EnabledFeature);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.ClipLengthPast, x => x.ClipLengthPast);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.ClipLengthFuture, x => x.ClipLengthFuture);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.TimingStreamRetry, x => x.TimingStreamRetry);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.TimingStreamConnect, x => x.TimingStreamConnect);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.TimingDanmakuRetry, x => x.TimingDanmakuRetry);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.TimingCheckInterval, x => x.TimingCheckInterval);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.TimingWatchdogTimeout, x => x.TimingWatchdogTimeout);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.RecordDanmakuFlushInterval, x => x.RecordDanmakuFlushInterval);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, string?>(map, x => x.Cookie, x => x.Cookie);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, string?>(map, x => x.WebHookUrls, x => x.WebHookUrls);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, string?>(map, x => x.LiveApiHost, x => x.LiveApiHost);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, string?>(map, x => x.RecordFilenameFormat, x => x.RecordFilenameFormat);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, string?>(map, x => x.ClipFilenameFormat, x => x.ClipFilenameFormat);
|
||||
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, AutoCuttingMode>(map, x => x.CuttingMode, x => x.CuttingMode);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, uint>(map, x => x.CuttingNumber, x => x.CuttingNumber);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, bool>(map, x => x.RecordDanmaku, x => x.RecordDanmaku);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, bool>(map, x => x.RecordDanmakuRaw, x => x.RecordDanmakuRaw);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, bool>(map, x => x.RecordDanmakuSuperChat, x => x.RecordDanmakuSuperChat);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, bool>(map, x => x.RecordDanmakuGift, x => x.RecordDanmakuGift);
|
||||
AddMap<V1.ConfigV1, V2.GlobalConfig, bool>(map, x => x.RecordDanmakuGuard, x => x.RecordDanmakuGuard);
|
||||
|
||||
var def = new V1.ConfigV1(); // old default
|
||||
var v2 = new V2.ConfigV2();
|
||||
|
||||
foreach (var item in map)
|
||||
{
|
||||
var data = item.Key.GetValue(v1);
|
||||
if (!(data?.Equals(item.Key.GetValue(def)) ?? true))
|
||||
item.Value.SetValue(v2.Global, data);
|
||||
}
|
||||
|
||||
v2.Rooms = v1.RoomList.Select(x => new V2.RoomConfig { RoomId = x.Roomid, AutoRecord = x.Enabled }).ToList();
|
||||
|
||||
return v2;
|
||||
}
|
||||
|
||||
private static void AddMap<T1, T2, T3>(Dictionary<PropertyInfo, PropertyInfo> map, Expression<Func<T1, T3>> keyExpr, Expression<Func<T2, T3>> valueExpr)
|
||||
{
|
||||
var key = GetProperty(keyExpr);
|
||||
var value = GetProperty(valueExpr);
|
||||
if ((key is null) || (value is null))
|
||||
return;
|
||||
map.Add(key, value);
|
||||
}
|
||||
|
||||
private static PropertyInfo? GetProperty<TType, TValue>(Expression<Func<TType, TValue>> expression)
|
||||
=> (expression.Body as MemberExpression)?.Member as PropertyInfo;
|
||||
}
|
||||
}
|
|
@ -1,88 +1,111 @@
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
|
||||
#nullable enable
|
||||
namespace BililiveRecorder.Core.Config
|
||||
{
|
||||
public static class ConfigParser
|
||||
{
|
||||
private const string CONFIG_FILE_NAME = "config.json";
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public static bool Load(string directory, ConfigV1 config = null)
|
||||
public static V2.ConfigV2? LoadFrom(string directory)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
try
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!Directory.Exists(directory))
|
||||
return null;
|
||||
|
||||
var filepath = Path.Combine(directory, "config.json");
|
||||
if (File.Exists(filepath))
|
||||
{
|
||||
try
|
||||
var filepath = Path.Combine(directory, CONFIG_FILE_NAME);
|
||||
|
||||
if (!File.Exists(filepath))
|
||||
{
|
||||
var cw = JsonConvert.DeserializeObject<ConfigWrapper>(File.ReadAllText(filepath));
|
||||
switch (cw.Version)
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
var v1 = JsonConvert.DeserializeObject<ConfigV1>(cw.Data);
|
||||
v1.CopyPropertiesTo(config);
|
||||
return true;
|
||||
// (v1.ToV2()).CopyPropertiesTo(config);
|
||||
}
|
||||
/**
|
||||
* case 2:
|
||||
* {
|
||||
* var v2 = JsonConvert.DeserializeObject<ConfigV2>(cw.Data);
|
||||
* v2.CopyPropertiesTo(config);
|
||||
* return true;
|
||||
* }
|
||||
* */
|
||||
default:
|
||||
// version not supported
|
||||
// TODO: return status enum
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Failed to parse config!");
|
||||
return false;
|
||||
logger.Debug("Config file does not exist. \"{path}\"", filepath);
|
||||
return new V2.ConfigV2();
|
||||
}
|
||||
|
||||
logger.Debug("Loading config from path \"{path}\".", filepath);
|
||||
var json = File.ReadAllText(filepath, Encoding.UTF8);
|
||||
return LoadJson(json);
|
||||
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
new ConfigV1().CopyPropertiesTo(config);
|
||||
return true;
|
||||
logger.Error(ex, "从文件加载设置时出错");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Save(string directory, ConfigV1 config = null)
|
||||
public static V2.ConfigV2? LoadJson(string json)
|
||||
{
|
||||
if (config == null) { config = new ConfigV1(); }
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
// User should create the directory
|
||||
// TODO: return enum
|
||||
return false;
|
||||
}
|
||||
var filepath = Path.Combine(directory, "config.json");
|
||||
try
|
||||
{
|
||||
var data = JsonConvert.SerializeObject(config);
|
||||
var cw = JsonConvert.SerializeObject(new ConfigWrapper()
|
||||
logger.Debug("Config json: {config}", json);
|
||||
|
||||
var configBase = JsonConvert.DeserializeObject<ConfigBase>(json);
|
||||
switch (configBase)
|
||||
{
|
||||
Version = 1,
|
||||
Data = data
|
||||
});
|
||||
File.WriteAllText(filepath, cw);
|
||||
case V1.ConfigV1Wrapper v1:
|
||||
{
|
||||
logger.Debug("读取到 config v1");
|
||||
#pragma warning disable CS0612
|
||||
var v1Data = JsonConvert.DeserializeObject<V1.ConfigV1>(v1.Data);
|
||||
#pragma warning restore CS0612
|
||||
var newConfig = ConfigMapper.Map1To2(v1Data);
|
||||
|
||||
return newConfig;
|
||||
}
|
||||
case V2.ConfigV2 v2:
|
||||
logger.Debug("读取到 config v2");
|
||||
return v2;
|
||||
default:
|
||||
logger.Error("读取到不支持的设置版本");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "解析设置时出错");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool SaveTo(string directory, V2.ConfigV2 config)
|
||||
{
|
||||
var json = SaveJson(config);
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
return false;
|
||||
|
||||
var filepath = Path.Combine(directory, CONFIG_FILE_NAME);
|
||||
|
||||
if (json is not null)
|
||||
File.WriteAllText(filepath, json, Encoding.UTF8);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "保存设置时出错(写入文件)");
|
||||
return false;
|
||||
// TODO: Log Exception
|
||||
}
|
||||
}
|
||||
|
||||
public static string? SaveJson(V2.ConfigV2 config)
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(config);
|
||||
return json;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "保存设置时出错(序列化)");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace BililiveRecorder.Core.Config
|
||||
{
|
||||
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
|
||||
internal class ConfigWrapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Config Version
|
||||
/// </summary>
|
||||
[JsonProperty("version")]
|
||||
public int Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Config Data String
|
||||
/// </summary>
|
||||
[JsonProperty("data")]
|
||||
public string Data { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
@ -5,8 +6,9 @@ using BililiveRecorder.FlvProcessor;
|
|||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
|
||||
namespace BililiveRecorder.Core.Config
|
||||
namespace BililiveRecorder.Core.Config.V1
|
||||
{
|
||||
[Obsolete]
|
||||
[JsonObject(memberSerialization: MemberSerialization.OptIn)]
|
||||
public class ConfigV1 : INotifyPropertyChanged
|
||||
{
|
||||
|
@ -17,7 +19,7 @@ namespace BililiveRecorder.Core.Config
|
|||
/// </summary>
|
||||
[JsonIgnore]
|
||||
[Utils.DoNotCopyProperty]
|
||||
public string WorkDirectory { get => _workDirectory; set => SetField(ref _workDirectory, value); }
|
||||
public string WorkDirectory { get => this._workDirectory; set => this.SetField(ref this._workDirectory, value); }
|
||||
|
||||
|
||||
/// <summary>
|
||||
|
@ -30,122 +32,122 @@ namespace BililiveRecorder.Core.Config
|
|||
/// 启用的功能
|
||||
/// </summary>
|
||||
[JsonProperty("feature")]
|
||||
public EnabledFeature EnabledFeature { get => _enabledFeature; set => SetField(ref _enabledFeature, value); }
|
||||
public EnabledFeature EnabledFeature { get => this._enabledFeature; set => this.SetField(ref this._enabledFeature, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 剪辑-过去的时长(秒)
|
||||
/// </summary>
|
||||
[JsonProperty("clip_length_future")]
|
||||
public uint ClipLengthFuture { get => _clipLengthFuture; set => SetField(ref _clipLengthFuture, value); }
|
||||
public uint ClipLengthFuture { get => this._clipLengthFuture; set => this.SetField(ref this._clipLengthFuture, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 剪辑-将来的时长(秒)
|
||||
/// </summary>
|
||||
[JsonProperty("clip_length_past")]
|
||||
public uint ClipLengthPast { get => _clipLengthPast; set => SetField(ref _clipLengthPast, value); }
|
||||
public uint ClipLengthPast { get => this._clipLengthPast; set => this.SetField(ref this._clipLengthPast, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 自动切割模式
|
||||
/// </summary>
|
||||
[JsonProperty("cutting_mode")]
|
||||
public AutoCuttingMode CuttingMode { get => _cuttingMode; set => SetField(ref _cuttingMode, value); }
|
||||
public AutoCuttingMode CuttingMode { get => this._cuttingMode; set => this.SetField(ref this._cuttingMode, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 自动切割数值(分钟/MiB)
|
||||
/// </summary>
|
||||
[JsonProperty("cutting_number")]
|
||||
public uint CuttingNumber { get => _cuttingNumber; set => SetField(ref _cuttingNumber, value); }
|
||||
public uint CuttingNumber { get => this._cuttingNumber; set => this.SetField(ref this._cuttingNumber, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 录制断开重连时间间隔 毫秒
|
||||
/// </summary>
|
||||
[JsonProperty("timing_stream_retry")]
|
||||
public uint TimingStreamRetry { get => _timingStreamRetry; set => SetField(ref _timingStreamRetry, value); }
|
||||
public uint TimingStreamRetry { get => this._timingStreamRetry; set => this.SetField(ref this._timingStreamRetry, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 连接直播服务器超时时间 毫秒
|
||||
/// </summary>
|
||||
[JsonProperty("timing_stream_connect")]
|
||||
public uint TimingStreamConnect { get => _timingStreamConnect; set => SetField(ref _timingStreamConnect, value); }
|
||||
public uint TimingStreamConnect { get => this._timingStreamConnect; set => this.SetField(ref this._timingStreamConnect, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 弹幕服务器重连时间间隔 毫秒
|
||||
/// </summary>
|
||||
[JsonProperty("timing_danmaku_retry")]
|
||||
public uint TimingDanmakuRetry { get => _timingDanmakuRetry; set => SetField(ref _timingDanmakuRetry, value); }
|
||||
public uint TimingDanmakuRetry { get => this._timingDanmakuRetry; set => this.SetField(ref this._timingDanmakuRetry, value); }
|
||||
|
||||
/// <summary>
|
||||
/// HTTP API 检查时间间隔 秒
|
||||
/// </summary>
|
||||
[JsonProperty("timing_check_interval")]
|
||||
public uint TimingCheckInterval { get => _timingCheckInterval; set => SetField(ref _timingCheckInterval, value); }
|
||||
public uint TimingCheckInterval { get => this._timingCheckInterval; set => this.SetField(ref this._timingCheckInterval, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 最大未收到新直播数据时间 毫秒
|
||||
/// </summary>
|
||||
[JsonProperty("timing_watchdog_timeout")]
|
||||
public uint TimingWatchdogTimeout { get => _timingWatchdogTimeout; set => SetField(ref _timingWatchdogTimeout, value); }
|
||||
public uint TimingWatchdogTimeout { get => this._timingWatchdogTimeout; set => this.SetField(ref this._timingWatchdogTimeout, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 请求 API 时使用的 Cookie
|
||||
/// </summary>
|
||||
[JsonProperty("cookie")]
|
||||
public string Cookie { get => _cookie; set => SetField(ref _cookie, value); }
|
||||
public string Cookie { get => this._cookie; set => this.SetField(ref this._cookie, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制弹幕
|
||||
/// </summary>
|
||||
[JsonProperty("record_danmaku")]
|
||||
public bool RecordDanmaku { get => _recordDanmaku; set => SetField(ref _recordDanmaku, value); }
|
||||
public bool RecordDanmaku { get => this._recordDanmaku; set => this.SetField(ref this._recordDanmaku, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否记录弹幕原始数据
|
||||
/// </summary>
|
||||
[JsonProperty("record_danmaku_raw")]
|
||||
public bool RecordDanmakuRaw { get => _recordDanmakuRaw; set => SetField(ref _recordDanmakuRaw, value); }
|
||||
public bool RecordDanmakuRaw { get => this._recordDanmakuRaw; set => this.SetField(ref this._recordDanmakuRaw, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制 SuperChat
|
||||
/// </summary>
|
||||
[JsonProperty("record_danmaku_sc")]
|
||||
public bool RecordDanmakuSuperChat { get => _recordDanmakuSuperChat; set => SetField(ref _recordDanmakuSuperChat, value); }
|
||||
public bool RecordDanmakuSuperChat { get => this._recordDanmakuSuperChat; set => this.SetField(ref this._recordDanmakuSuperChat, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制 礼物
|
||||
/// </summary>
|
||||
[JsonProperty("record_danmaku_gift")]
|
||||
public bool RecordDanmakuGift { get => _recordDanmakuGift; set => SetField(ref _recordDanmakuGift, value); }
|
||||
public bool RecordDanmakuGift { get => this._recordDanmakuGift; set => this.SetField(ref this._recordDanmakuGift, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制 上船
|
||||
/// </summary>
|
||||
[JsonProperty("record_danmaku_guard")]
|
||||
public bool RecordDanmakuGuard { get => _recordDanmakuGuard; set => SetField(ref _recordDanmakuGuard, value); }
|
||||
public bool RecordDanmakuGuard { get => this._recordDanmakuGuard; set => this.SetField(ref this._recordDanmakuGuard, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 触发 <see cref="System.Xml.XmlWriter.Flush"/> 的弹幕个数
|
||||
/// </summary>
|
||||
[JsonProperty("record_danmaku_flush_interval")]
|
||||
public uint RecordDanmakuFlushInterval { get => _recordDanmakuFlushInterval; set => SetField(ref _recordDanmakuFlushInterval, value); }
|
||||
public uint RecordDanmakuFlushInterval { get => this._recordDanmakuFlushInterval; set => this.SetField(ref this._recordDanmakuFlushInterval, value); }
|
||||
|
||||
/// <summary>
|
||||
/// 替换api.live.bilibili.com服务器为其他反代,可以支持在云服务器上录制
|
||||
/// </summary>
|
||||
[JsonProperty("live_api_host")]
|
||||
public string LiveApiHost { get => _liveApiHost; set => SetField(ref _liveApiHost, value); }
|
||||
public string LiveApiHost { get => this._liveApiHost; set => this.SetField(ref this._liveApiHost, value); }
|
||||
|
||||
[JsonProperty("record_filename_format")]
|
||||
public string RecordFilenameFormat
|
||||
{
|
||||
get => _record_filename_format;
|
||||
set => SetField(ref _record_filename_format, value);
|
||||
get => this._record_filename_format;
|
||||
set => this.SetField(ref this._record_filename_format, value);
|
||||
}
|
||||
|
||||
[JsonProperty("clip_filename_format")]
|
||||
public string ClipFilenameFormat
|
||||
{
|
||||
get => _clip_filename_format;
|
||||
set => SetField(ref _clip_filename_format, value);
|
||||
get => this._clip_filename_format;
|
||||
set => this.SetField(ref this._clip_filename_format, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -154,8 +156,8 @@ namespace BililiveRecorder.Core.Config
|
|||
[JsonProperty("webhook_urls")]
|
||||
public string WebHookUrls
|
||||
{
|
||||
get => _webhook_urls;
|
||||
set => SetField(ref _webhook_urls, value);
|
||||
get => this._webhook_urls;
|
||||
set => this.SetField(ref this._webhook_urls, value);
|
||||
}
|
||||
|
||||
#region INotifyPropertyChanged
|
||||
|
@ -163,9 +165,8 @@ namespace BililiveRecorder.Core.Config
|
|||
protected virtual void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) { return false; }
|
||||
logger.Trace("设置 [{0}] 的值已从 [{1}] 修改到 [{2}]", propertyName, field, value);
|
||||
field = value; OnPropertyChanged(propertyName); return true;
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) return false; logger.Trace("设置 [{0}] 的值已从 [{1}] 修改到 [{2}]", propertyName, field, value);
|
||||
field = value; this.OnPropertyChanged(propertyName); return true;
|
||||
}
|
||||
#endregion
|
||||
|
13
BililiveRecorder.Core/Config/V1/ConfigV1Wrapper.cs
Normal file
13
BililiveRecorder.Core/Config/V1/ConfigV1Wrapper.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace BililiveRecorder.Core.Config.V1
|
||||
{
|
||||
internal sealed class ConfigV1Wrapper : ConfigBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Config Data String
|
||||
/// </summary>
|
||||
[JsonProperty("data")]
|
||||
public string Data { get; set; }
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BililiveRecorder.Core.Config
|
||||
namespace BililiveRecorder.Core.Config.V1
|
||||
{
|
||||
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
|
||||
public class RoomV1
|
382
BililiveRecorder.Core/Config/V2/Config.gen.cs
Normal file
382
BililiveRecorder.Core/Config/V2/Config.gen.cs
Normal file
|
@ -0,0 +1,382 @@
|
|||
// ******************************
|
||||
// GENERATED CODE, DO NOT EDIT.
|
||||
// RUN FORMATTER AFTER GENERATE
|
||||
// ******************************
|
||||
using System.ComponentModel;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
using HierarchicalPropertyDefault;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
#nullable enable
|
||||
namespace BililiveRecorder.Core.Config.V2
|
||||
{
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
public sealed partial class RoomConfig : HierarchicalObject<GlobalConfig, RoomConfig>
|
||||
{
|
||||
/// <summary>
|
||||
/// 房间号
|
||||
/// </summary>
|
||||
public int RoomId { get => this.GetPropertyValue<int>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasRoomId { get => this.GetPropertyHasValue(nameof(this.RoomId)); set => this.SetPropertyHasValue<int>(value, nameof(this.RoomId)); }
|
||||
[JsonProperty(nameof(RoomId)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<int> OptionalRoomId { get => this.GetPropertyValueOptional<int>(nameof(this.RoomId)); set => this.SetPropertyValueOptional(value, nameof(this.RoomId)); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用自动录制
|
||||
/// </summary>
|
||||
public bool AutoRecord { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasAutoRecord { get => this.GetPropertyHasValue(nameof(this.AutoRecord)); set => this.SetPropertyHasValue<bool>(value, nameof(this.AutoRecord)); }
|
||||
[JsonProperty(nameof(AutoRecord)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<bool> OptionalAutoRecord { get => this.GetPropertyValueOptional<bool>(nameof(this.AutoRecord)); set => this.SetPropertyValueOptional(value, nameof(this.AutoRecord)); }
|
||||
|
||||
/// <summary>
|
||||
/// 录制文件自动切割模式
|
||||
/// </summary>
|
||||
public AutoCuttingMode CuttingMode { get => this.GetPropertyValue<AutoCuttingMode>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasCuttingMode { get => this.GetPropertyHasValue(nameof(this.CuttingMode)); set => this.SetPropertyHasValue<AutoCuttingMode>(value, nameof(this.CuttingMode)); }
|
||||
[JsonProperty(nameof(CuttingMode)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<AutoCuttingMode> OptionalCuttingMode { get => this.GetPropertyValueOptional<AutoCuttingMode>(nameof(this.CuttingMode)); set => this.SetPropertyValueOptional(value, nameof(this.CuttingMode)); }
|
||||
|
||||
/// <summary>
|
||||
/// 录制文件自动切割数值(分钟/MiB)
|
||||
/// </summary>
|
||||
public uint CuttingNumber { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasCuttingNumber { get => this.GetPropertyHasValue(nameof(this.CuttingNumber)); set => this.SetPropertyHasValue<uint>(value, nameof(this.CuttingNumber)); }
|
||||
[JsonProperty(nameof(CuttingNumber)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<uint> OptionalCuttingNumber { get => this.GetPropertyValueOptional<uint>(nameof(this.CuttingNumber)); set => this.SetPropertyValueOptional(value, nameof(this.CuttingNumber)); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制弹幕
|
||||
/// </summary>
|
||||
public bool RecordDanmaku { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasRecordDanmaku { get => this.GetPropertyHasValue(nameof(this.RecordDanmaku)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmaku)); }
|
||||
[JsonProperty(nameof(RecordDanmaku)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<bool> OptionalRecordDanmaku { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmaku)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmaku)); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否记录弹幕原始数据
|
||||
/// </summary>
|
||||
public bool RecordDanmakuRaw { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasRecordDanmakuRaw { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuRaw)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuRaw)); }
|
||||
[JsonProperty(nameof(RecordDanmakuRaw)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<bool> OptionalRecordDanmakuRaw { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuRaw)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuRaw)); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制 SuperChat
|
||||
/// </summary>
|
||||
public bool RecordDanmakuSuperChat { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasRecordDanmakuSuperChat { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuSuperChat)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuSuperChat)); }
|
||||
[JsonProperty(nameof(RecordDanmakuSuperChat)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<bool> OptionalRecordDanmakuSuperChat { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuSuperChat)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuSuperChat)); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制 礼物
|
||||
/// </summary>
|
||||
public bool RecordDanmakuGift { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasRecordDanmakuGift { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuGift)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuGift)); }
|
||||
[JsonProperty(nameof(RecordDanmakuGift)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<bool> OptionalRecordDanmakuGift { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuGift)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGift)); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制 上船
|
||||
/// </summary>
|
||||
public bool RecordDanmakuGuard { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasRecordDanmakuGuard { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuGuard)); }
|
||||
[JsonProperty(nameof(RecordDanmakuGuard)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<bool> OptionalRecordDanmakuGuard { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGuard)); }
|
||||
|
||||
/// <summary>
|
||||
/// 启用的功能
|
||||
/// </summary>
|
||||
public EnabledFeature EnabledFeature => this.GetPropertyValue<EnabledFeature>();
|
||||
|
||||
/// <summary>
|
||||
/// 剪辑-过去的时长(秒)
|
||||
/// </summary>
|
||||
public uint ClipLengthPast => this.GetPropertyValue<uint>();
|
||||
|
||||
/// <summary>
|
||||
/// 剪辑-将来的时长(秒)
|
||||
/// </summary>
|
||||
public uint ClipLengthFuture => this.GetPropertyValue<uint>();
|
||||
|
||||
/// <summary>
|
||||
/// 录制断开重连时间间隔 毫秒
|
||||
/// </summary>
|
||||
public uint TimingStreamRetry => this.GetPropertyValue<uint>();
|
||||
|
||||
/// <summary>
|
||||
/// 连接直播服务器超时时间 毫秒
|
||||
/// </summary>
|
||||
public uint TimingStreamConnect => this.GetPropertyValue<uint>();
|
||||
|
||||
/// <summary>
|
||||
/// 弹幕服务器重连时间间隔 毫秒
|
||||
/// </summary>
|
||||
public uint TimingDanmakuRetry => this.GetPropertyValue<uint>();
|
||||
|
||||
/// <summary>
|
||||
/// HTTP API 检查时间间隔 秒
|
||||
/// </summary>
|
||||
public uint TimingCheckInterval => this.GetPropertyValue<uint>();
|
||||
|
||||
/// <summary>
|
||||
/// 最大未收到新直播数据时间 毫秒
|
||||
/// </summary>
|
||||
public uint TimingWatchdogTimeout => this.GetPropertyValue<uint>();
|
||||
|
||||
/// <summary>
|
||||
/// 触发 <see cref="System.Xml.XmlWriter.Flush"/> 的弹幕个数
|
||||
/// </summary>
|
||||
public uint RecordDanmakuFlushInterval => this.GetPropertyValue<uint>();
|
||||
|
||||
/// <summary>
|
||||
/// 请求 API 时使用的 Cookie
|
||||
/// </summary>
|
||||
public string? Cookie => this.GetPropertyValue<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 录制文件写入结束 Webhook 地址 每行一个
|
||||
/// </summary>
|
||||
public string? WebHookUrls => this.GetPropertyValue<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 替换 api.live.bilibili.com 服务器为其他反代,可以支持在云服务器上录制
|
||||
/// </summary>
|
||||
public string? LiveApiHost => this.GetPropertyValue<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 录制文件名模板
|
||||
/// </summary>
|
||||
public string? RecordFilenameFormat => this.GetPropertyValue<string>();
|
||||
|
||||
/// <summary>
|
||||
/// 剪辑文件名模板
|
||||
/// </summary>
|
||||
public string? ClipFilenameFormat => this.GetPropertyValue<string>();
|
||||
|
||||
}
|
||||
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
public sealed partial class GlobalConfig : HierarchicalObject<DefaultConfig, GlobalConfig>
|
||||
{
|
||||
/// <summary>
|
||||
/// 启用的功能
|
||||
/// </summary>
|
||||
public EnabledFeature EnabledFeature { get => this.GetPropertyValue<EnabledFeature>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasEnabledFeature { get => this.GetPropertyHasValue(nameof(this.EnabledFeature)); set => this.SetPropertyHasValue<EnabledFeature>(value, nameof(this.EnabledFeature)); }
|
||||
[JsonProperty(nameof(EnabledFeature)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<EnabledFeature> OptionalEnabledFeature { get => this.GetPropertyValueOptional<EnabledFeature>(nameof(this.EnabledFeature)); set => this.SetPropertyValueOptional(value, nameof(this.EnabledFeature)); }
|
||||
|
||||
/// <summary>
|
||||
/// 剪辑-过去的时长(秒)
|
||||
/// </summary>
|
||||
public uint ClipLengthPast { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasClipLengthPast { get => this.GetPropertyHasValue(nameof(this.ClipLengthPast)); set => this.SetPropertyHasValue<uint>(value, nameof(this.ClipLengthPast)); }
|
||||
[JsonProperty(nameof(ClipLengthPast)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<uint> OptionalClipLengthPast { get => this.GetPropertyValueOptional<uint>(nameof(this.ClipLengthPast)); set => this.SetPropertyValueOptional(value, nameof(this.ClipLengthPast)); }
|
||||
|
||||
/// <summary>
|
||||
/// 剪辑-将来的时长(秒)
|
||||
/// </summary>
|
||||
public uint ClipLengthFuture { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasClipLengthFuture { get => this.GetPropertyHasValue(nameof(this.ClipLengthFuture)); set => this.SetPropertyHasValue<uint>(value, nameof(this.ClipLengthFuture)); }
|
||||
[JsonProperty(nameof(ClipLengthFuture)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<uint> OptionalClipLengthFuture { get => this.GetPropertyValueOptional<uint>(nameof(this.ClipLengthFuture)); set => this.SetPropertyValueOptional(value, nameof(this.ClipLengthFuture)); }
|
||||
|
||||
/// <summary>
|
||||
/// 录制断开重连时间间隔 毫秒
|
||||
/// </summary>
|
||||
public uint TimingStreamRetry { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasTimingStreamRetry { get => this.GetPropertyHasValue(nameof(this.TimingStreamRetry)); set => this.SetPropertyHasValue<uint>(value, nameof(this.TimingStreamRetry)); }
|
||||
[JsonProperty(nameof(TimingStreamRetry)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<uint> OptionalTimingStreamRetry { get => this.GetPropertyValueOptional<uint>(nameof(this.TimingStreamRetry)); set => this.SetPropertyValueOptional(value, nameof(this.TimingStreamRetry)); }
|
||||
|
||||
/// <summary>
|
||||
/// 连接直播服务器超时时间 毫秒
|
||||
/// </summary>
|
||||
public uint TimingStreamConnect { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasTimingStreamConnect { get => this.GetPropertyHasValue(nameof(this.TimingStreamConnect)); set => this.SetPropertyHasValue<uint>(value, nameof(this.TimingStreamConnect)); }
|
||||
[JsonProperty(nameof(TimingStreamConnect)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<uint> OptionalTimingStreamConnect { get => this.GetPropertyValueOptional<uint>(nameof(this.TimingStreamConnect)); set => this.SetPropertyValueOptional(value, nameof(this.TimingStreamConnect)); }
|
||||
|
||||
/// <summary>
|
||||
/// 弹幕服务器重连时间间隔 毫秒
|
||||
/// </summary>
|
||||
public uint TimingDanmakuRetry { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasTimingDanmakuRetry { get => this.GetPropertyHasValue(nameof(this.TimingDanmakuRetry)); set => this.SetPropertyHasValue<uint>(value, nameof(this.TimingDanmakuRetry)); }
|
||||
[JsonProperty(nameof(TimingDanmakuRetry)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<uint> OptionalTimingDanmakuRetry { get => this.GetPropertyValueOptional<uint>(nameof(this.TimingDanmakuRetry)); set => this.SetPropertyValueOptional(value, nameof(this.TimingDanmakuRetry)); }
|
||||
|
||||
/// <summary>
|
||||
/// HTTP API 检查时间间隔 秒
|
||||
/// </summary>
|
||||
public uint TimingCheckInterval { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasTimingCheckInterval { get => this.GetPropertyHasValue(nameof(this.TimingCheckInterval)); set => this.SetPropertyHasValue<uint>(value, nameof(this.TimingCheckInterval)); }
|
||||
[JsonProperty(nameof(TimingCheckInterval)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<uint> OptionalTimingCheckInterval { get => this.GetPropertyValueOptional<uint>(nameof(this.TimingCheckInterval)); set => this.SetPropertyValueOptional(value, nameof(this.TimingCheckInterval)); }
|
||||
|
||||
/// <summary>
|
||||
/// 最大未收到新直播数据时间 毫秒
|
||||
/// </summary>
|
||||
public uint TimingWatchdogTimeout { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasTimingWatchdogTimeout { get => this.GetPropertyHasValue(nameof(this.TimingWatchdogTimeout)); set => this.SetPropertyHasValue<uint>(value, nameof(this.TimingWatchdogTimeout)); }
|
||||
[JsonProperty(nameof(TimingWatchdogTimeout)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<uint> OptionalTimingWatchdogTimeout { get => this.GetPropertyValueOptional<uint>(nameof(this.TimingWatchdogTimeout)); set => this.SetPropertyValueOptional(value, nameof(this.TimingWatchdogTimeout)); }
|
||||
|
||||
/// <summary>
|
||||
/// 触发 <see cref="System.Xml.XmlWriter.Flush"/> 的弹幕个数
|
||||
/// </summary>
|
||||
public uint RecordDanmakuFlushInterval { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasRecordDanmakuFlushInterval { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuFlushInterval)); set => this.SetPropertyHasValue<uint>(value, nameof(this.RecordDanmakuFlushInterval)); }
|
||||
[JsonProperty(nameof(RecordDanmakuFlushInterval)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<uint> OptionalRecordDanmakuFlushInterval { get => this.GetPropertyValueOptional<uint>(nameof(this.RecordDanmakuFlushInterval)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuFlushInterval)); }
|
||||
|
||||
/// <summary>
|
||||
/// 请求 API 时使用的 Cookie
|
||||
/// </summary>
|
||||
public string? Cookie { get => this.GetPropertyValue<string>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasCookie { get => this.GetPropertyHasValue(nameof(this.Cookie)); set => this.SetPropertyHasValue<string>(value, nameof(this.Cookie)); }
|
||||
[JsonProperty(nameof(Cookie)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<string?> OptionalCookie { get => this.GetPropertyValueOptional<string>(nameof(this.Cookie)); set => this.SetPropertyValueOptional(value, nameof(this.Cookie)); }
|
||||
|
||||
/// <summary>
|
||||
/// 录制文件写入结束 Webhook 地址 每行一个
|
||||
/// </summary>
|
||||
public string? WebHookUrls { get => this.GetPropertyValue<string>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasWebHookUrls { get => this.GetPropertyHasValue(nameof(this.WebHookUrls)); set => this.SetPropertyHasValue<string>(value, nameof(this.WebHookUrls)); }
|
||||
[JsonProperty(nameof(WebHookUrls)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<string?> OptionalWebHookUrls { get => this.GetPropertyValueOptional<string>(nameof(this.WebHookUrls)); set => this.SetPropertyValueOptional(value, nameof(this.WebHookUrls)); }
|
||||
|
||||
/// <summary>
|
||||
/// 替换 api.live.bilibili.com 服务器为其他反代,可以支持在云服务器上录制
|
||||
/// </summary>
|
||||
public string? LiveApiHost { get => this.GetPropertyValue<string>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasLiveApiHost { get => this.GetPropertyHasValue(nameof(this.LiveApiHost)); set => this.SetPropertyHasValue<string>(value, nameof(this.LiveApiHost)); }
|
||||
[JsonProperty(nameof(LiveApiHost)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<string?> OptionalLiveApiHost { get => this.GetPropertyValueOptional<string>(nameof(this.LiveApiHost)); set => this.SetPropertyValueOptional(value, nameof(this.LiveApiHost)); }
|
||||
|
||||
/// <summary>
|
||||
/// 录制文件名模板
|
||||
/// </summary>
|
||||
public string? RecordFilenameFormat { get => this.GetPropertyValue<string>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasRecordFilenameFormat { get => this.GetPropertyHasValue(nameof(this.RecordFilenameFormat)); set => this.SetPropertyHasValue<string>(value, nameof(this.RecordFilenameFormat)); }
|
||||
[JsonProperty(nameof(RecordFilenameFormat)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<string?> OptionalRecordFilenameFormat { get => this.GetPropertyValueOptional<string>(nameof(this.RecordFilenameFormat)); set => this.SetPropertyValueOptional(value, nameof(this.RecordFilenameFormat)); }
|
||||
|
||||
/// <summary>
|
||||
/// 剪辑文件名模板
|
||||
/// </summary>
|
||||
public string? ClipFilenameFormat { get => this.GetPropertyValue<string>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasClipFilenameFormat { get => this.GetPropertyHasValue(nameof(this.ClipFilenameFormat)); set => this.SetPropertyHasValue<string>(value, nameof(this.ClipFilenameFormat)); }
|
||||
[JsonProperty(nameof(ClipFilenameFormat)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<string?> OptionalClipFilenameFormat { get => this.GetPropertyValueOptional<string>(nameof(this.ClipFilenameFormat)); set => this.SetPropertyValueOptional(value, nameof(this.ClipFilenameFormat)); }
|
||||
|
||||
/// <summary>
|
||||
/// 录制文件自动切割模式
|
||||
/// </summary>
|
||||
public AutoCuttingMode CuttingMode { get => this.GetPropertyValue<AutoCuttingMode>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasCuttingMode { get => this.GetPropertyHasValue(nameof(this.CuttingMode)); set => this.SetPropertyHasValue<AutoCuttingMode>(value, nameof(this.CuttingMode)); }
|
||||
[JsonProperty(nameof(CuttingMode)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<AutoCuttingMode> OptionalCuttingMode { get => this.GetPropertyValueOptional<AutoCuttingMode>(nameof(this.CuttingMode)); set => this.SetPropertyValueOptional(value, nameof(this.CuttingMode)); }
|
||||
|
||||
/// <summary>
|
||||
/// 录制文件自动切割数值(分钟/MiB)
|
||||
/// </summary>
|
||||
public uint CuttingNumber { get => this.GetPropertyValue<uint>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasCuttingNumber { get => this.GetPropertyHasValue(nameof(this.CuttingNumber)); set => this.SetPropertyHasValue<uint>(value, nameof(this.CuttingNumber)); }
|
||||
[JsonProperty(nameof(CuttingNumber)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<uint> OptionalCuttingNumber { get => this.GetPropertyValueOptional<uint>(nameof(this.CuttingNumber)); set => this.SetPropertyValueOptional(value, nameof(this.CuttingNumber)); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制弹幕
|
||||
/// </summary>
|
||||
public bool RecordDanmaku { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasRecordDanmaku { get => this.GetPropertyHasValue(nameof(this.RecordDanmaku)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmaku)); }
|
||||
[JsonProperty(nameof(RecordDanmaku)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<bool> OptionalRecordDanmaku { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmaku)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmaku)); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否记录弹幕原始数据
|
||||
/// </summary>
|
||||
public bool RecordDanmakuRaw { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasRecordDanmakuRaw { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuRaw)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuRaw)); }
|
||||
[JsonProperty(nameof(RecordDanmakuRaw)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<bool> OptionalRecordDanmakuRaw { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuRaw)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuRaw)); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制 SuperChat
|
||||
/// </summary>
|
||||
public bool RecordDanmakuSuperChat { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasRecordDanmakuSuperChat { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuSuperChat)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuSuperChat)); }
|
||||
[JsonProperty(nameof(RecordDanmakuSuperChat)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<bool> OptionalRecordDanmakuSuperChat { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuSuperChat)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuSuperChat)); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制 礼物
|
||||
/// </summary>
|
||||
public bool RecordDanmakuGift { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasRecordDanmakuGift { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuGift)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuGift)); }
|
||||
[JsonProperty(nameof(RecordDanmakuGift)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<bool> OptionalRecordDanmakuGift { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuGift)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGift)); }
|
||||
|
||||
/// <summary>
|
||||
/// 是否同时录制 上船
|
||||
/// </summary>
|
||||
public bool RecordDanmakuGuard { get => this.GetPropertyValue<bool>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasRecordDanmakuGuard { get => this.GetPropertyHasValue(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyHasValue<bool>(value, nameof(this.RecordDanmakuGuard)); }
|
||||
[JsonProperty(nameof(RecordDanmakuGuard)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<bool> OptionalRecordDanmakuGuard { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGuard)); }
|
||||
|
||||
}
|
||||
|
||||
public sealed partial class DefaultConfig
|
||||
{
|
||||
internal static readonly DefaultConfig Instance = new DefaultConfig();
|
||||
private DefaultConfig() { }
|
||||
|
||||
public EnabledFeature EnabledFeature => EnabledFeature.RecordOnly;
|
||||
|
||||
public uint ClipLengthPast => 20;
|
||||
|
||||
public uint ClipLengthFuture => 10;
|
||||
|
||||
public uint TimingStreamRetry => 6 * 1000;
|
||||
|
||||
public uint TimingStreamConnect => 5 * 1000;
|
||||
|
||||
public uint TimingDanmakuRetry => 15 * 1000;
|
||||
|
||||
public uint TimingCheckInterval => 5 * 60;
|
||||
|
||||
public uint TimingWatchdogTimeout => 10 * 1000;
|
||||
|
||||
public uint RecordDanmakuFlushInterval => 20;
|
||||
|
||||
public string Cookie => string.Empty;
|
||||
|
||||
public string WebHookUrls => string.Empty;
|
||||
|
||||
public string LiveApiHost => "https://api.live.bilibili.com";
|
||||
|
||||
public string RecordFilenameFormat => @"{roomid}-{name}/录制-{roomid}-{date}-{time}-{title}.flv";
|
||||
|
||||
public string ClipFilenameFormat => @"{roomid}-{name}/剪辑片段-{roomid}-{date}-{time}-{title}.flv";
|
||||
|
||||
public AutoCuttingMode CuttingMode => AutoCuttingMode.Disabled;
|
||||
|
||||
public uint CuttingNumber => 100;
|
||||
|
||||
public bool RecordDanmaku => false;
|
||||
|
||||
public bool RecordDanmakuRaw => false;
|
||||
|
||||
public bool RecordDanmakuSuperChat => true;
|
||||
|
||||
public bool RecordDanmakuGift => false;
|
||||
|
||||
public bool RecordDanmakuGuard => true;
|
||||
|
||||
}
|
||||
|
||||
}
|
44
BililiveRecorder.Core/Config/V2/ConfigV2.cs
Normal file
44
BililiveRecorder.Core/Config/V2/ConfigV2.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
#nullable enable
|
||||
namespace BililiveRecorder.Core.Config.V2
|
||||
{
|
||||
public class ConfigV2 : ConfigBase
|
||||
{
|
||||
public override int Version => 2;
|
||||
|
||||
[JsonProperty("global")]
|
||||
public GlobalConfig Global { get; set; } = new GlobalConfig();
|
||||
|
||||
[JsonProperty("rooms")]
|
||||
public List<RoomConfig> Rooms { get; set; } = new List<RoomConfig>();
|
||||
}
|
||||
|
||||
public partial class RoomConfig
|
||||
{
|
||||
public RoomConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name }))
|
||||
{ }
|
||||
|
||||
internal void SetParent(GlobalConfig? config) => this.Parent = config;
|
||||
|
||||
public string? WorkDirectory => this.GetPropertyValue<string>();
|
||||
}
|
||||
|
||||
public partial class GlobalConfig
|
||||
{
|
||||
public GlobalConfig() : base(x => x.AutoMap(p => new[] { "Has" + p.Name }))
|
||||
{
|
||||
this.Parent = DefaultConfig.Instance;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前工作目录
|
||||
/// </summary>
|
||||
public string? WorkDirectory
|
||||
{
|
||||
get => this.GetPropertyValue<string>();
|
||||
set => this.SetPropertyValue(value);
|
||||
}
|
||||
}
|
||||
}
|
126
BililiveRecorder.Core/Config/V2/build_config.data.js
Normal file
126
BililiveRecorder.Core/Config/V2/build_config.data.js
Normal file
|
@ -0,0 +1,126 @@
|
|||
module.exports = {
|
||||
"global": [{
|
||||
"name": "EnabledFeature",
|
||||
"type": "EnabledFeature",
|
||||
"desc": "启用的功能",
|
||||
"default": "EnabledFeature.RecordOnly"
|
||||
}, {
|
||||
"name": "ClipLengthPast",
|
||||
"type": "uint",
|
||||
"desc": "剪辑-过去的时长(秒)",
|
||||
"default": "20"
|
||||
}, {
|
||||
"name": "ClipLengthFuture",
|
||||
"type": "uint",
|
||||
"desc": "剪辑-将来的时长(秒)",
|
||||
"default": "10"
|
||||
}, {
|
||||
"name": "TimingStreamRetry",
|
||||
"type": "uint",
|
||||
"desc": "录制断开重连时间间隔 毫秒",
|
||||
"default": "6 * 1000"
|
||||
}, {
|
||||
"name": "TimingStreamConnect",
|
||||
"type": "uint",
|
||||
"desc": "连接直播服务器超时时间 毫秒",
|
||||
"default": "5 * 1000"
|
||||
}, {
|
||||
"name": "TimingDanmakuRetry",
|
||||
"type": "uint",
|
||||
"desc": "弹幕服务器重连时间间隔 毫秒",
|
||||
"default": "15 * 1000"
|
||||
}, {
|
||||
"name": "TimingCheckInterval",
|
||||
"type": "uint",
|
||||
"desc": "HTTP API 检查时间间隔 秒",
|
||||
"default": "5 * 60"
|
||||
}, {
|
||||
"name": "TimingWatchdogTimeout",
|
||||
"type": "uint",
|
||||
"desc": "最大未收到新直播数据时间 毫秒",
|
||||
"default": "10 * 1000"
|
||||
}, {
|
||||
"name": "RecordDanmakuFlushInterval",
|
||||
"type": "uint",
|
||||
"desc": "触发 <see cref=\"System.Xml.XmlWriter.Flush\"/> 的弹幕个数",
|
||||
"default": "20"
|
||||
}, {
|
||||
"name": "Cookie",
|
||||
"type": "string",
|
||||
"desc": "请求 API 时使用的 Cookie",
|
||||
"default": "string.Empty",
|
||||
"nullable": true
|
||||
}, {
|
||||
"name": "WebHookUrls",
|
||||
"type": "string",
|
||||
"desc": "录制文件写入结束 Webhook 地址 每行一个",
|
||||
"default": "string.Empty",
|
||||
"nullable": true
|
||||
}, {
|
||||
"name": "LiveApiHost",
|
||||
"type": "string",
|
||||
"desc": "替换 api.live.bilibili.com 服务器为其他反代,可以支持在云服务器上录制",
|
||||
"default": "\"https://api.live.bilibili.com\"",
|
||||
"nullable": true
|
||||
}, {
|
||||
"name": "RecordFilenameFormat",
|
||||
"type": "string",
|
||||
"desc": "录制文件名模板",
|
||||
"default": "@\"{roomid}-{name}/录制-{roomid}-{date}-{time}-{title}.flv\"",
|
||||
"nullable": true
|
||||
}, {
|
||||
"name": "ClipFilenameFormat",
|
||||
"type": "string",
|
||||
"desc": "剪辑文件名模板",
|
||||
"default": "@\"{roomid}-{name}/剪辑片段-{roomid}-{date}-{time}-{title}.flv\"",
|
||||
"nullable": true
|
||||
}, ],
|
||||
"room": [{
|
||||
"name": "RoomId",
|
||||
"type": "int",
|
||||
"desc": "房间号",
|
||||
"default": "default",
|
||||
"without_global": true
|
||||
}, {
|
||||
"name": "AutoRecord",
|
||||
"type": "bool",
|
||||
"desc": "是否启用自动录制",
|
||||
"default": "default",
|
||||
"without_global": true
|
||||
}, {
|
||||
"name": "CuttingMode",
|
||||
"type": "AutoCuttingMode",
|
||||
"desc": "录制文件自动切割模式",
|
||||
"default": "AutoCuttingMode.Disabled"
|
||||
}, {
|
||||
"name": "CuttingNumber",
|
||||
"type": "uint",
|
||||
"desc": "录制文件自动切割数值(分钟/MiB)",
|
||||
"default": "100"
|
||||
}, {
|
||||
"name": "RecordDanmaku",
|
||||
"type": "bool",
|
||||
"desc": "是否同时录制弹幕",
|
||||
"default": "false"
|
||||
}, {
|
||||
"name": "RecordDanmakuRaw",
|
||||
"type": "bool",
|
||||
"desc": "是否记录弹幕原始数据",
|
||||
"default": "false"
|
||||
}, {
|
||||
"name": "RecordDanmakuSuperChat",
|
||||
"type": "bool",
|
||||
"desc": "是否同时录制 SuperChat",
|
||||
"default": "true"
|
||||
}, {
|
||||
"name": "RecordDanmakuGift",
|
||||
"type": "bool",
|
||||
"desc": "是否同时录制 礼物",
|
||||
"default": "false"
|
||||
}, {
|
||||
"name": "RecordDanmakuGuard",
|
||||
"type": "bool",
|
||||
"desc": "是否同时录制 上船",
|
||||
"default": "true"
|
||||
}, ]
|
||||
}
|
79
BililiveRecorder.Core/Config/V2/build_config.js
Normal file
79
BililiveRecorder.Core/Config/V2/build_config.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
"use strict";
|
||||
const fs = require("fs");
|
||||
const data = require("./build_config.data.js");
|
||||
|
||||
const CODE_HEADER =
|
||||
`// ******************************
|
||||
// GENERATED CODE, DO NOT EDIT.
|
||||
// RUN FORMATTER AFTER GENERATE
|
||||
// ******************************
|
||||
using System.ComponentModel;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
using HierarchicalPropertyDefault;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
#nullable enable
|
||||
namespace BililiveRecorder.Core.Config.V2
|
||||
{
|
||||
`;
|
||||
|
||||
const CODE_FOOTER = `}\n`;
|
||||
|
||||
let result = CODE_HEADER;
|
||||
|
||||
function write_property(r) {
|
||||
result += `/// <summary>\n/// ${r.desc}\n/// </summary>\n`
|
||||
result += `public ${r.type}${!!r.nullable ? "?" : ""} ${r.name} { get => this.GetPropertyValue<${r.type}>(); set => this.SetPropertyValue(value); }\n`
|
||||
result += `public bool Has${r.name} { get => this.GetPropertyHasValue(nameof(this.${r.name})); set => this.SetPropertyHasValue<${r.type}>(value, nameof(this.${r.name})); }\n`
|
||||
result += `[JsonProperty(nameof(${r.name})), EditorBrowsable(EditorBrowsableState.Never)]\n`
|
||||
result += `public Optional<${r.type}${!!r.nullable ? "?" : ""}> Optional${r.name} { get => this.GetPropertyValueOptional<${r.type}>(nameof(this.${r.name})); set => this.SetPropertyValueOptional(value, nameof(this.${r.name})); }\n\n`
|
||||
}
|
||||
|
||||
function write_readonly_property(r) {
|
||||
result += `/// <summary>\n/// ${r.desc}\n/// </summary>\n`
|
||||
result += `public ${r.type}${!!r.nullable ? "?" : ""} ${r.name} => this.GetPropertyValue<${r.type}>();\n\n`
|
||||
}
|
||||
|
||||
{
|
||||
result += "[JsonObject(MemberSerialization.OptIn)]\n"
|
||||
result += "public sealed partial class RoomConfig : HierarchicalObject<GlobalConfig, RoomConfig>\n"
|
||||
result += "{\n";
|
||||
|
||||
data.room.forEach(r => write_property(r))
|
||||
data.global.forEach(r => write_readonly_property(r))
|
||||
|
||||
result += "}\n\n"
|
||||
}
|
||||
|
||||
{
|
||||
result += "[JsonObject(MemberSerialization.OptIn)]\n"
|
||||
result += "public sealed partial class GlobalConfig : HierarchicalObject<DefaultConfig, GlobalConfig>\n"
|
||||
result += "{\n";
|
||||
|
||||
data.global
|
||||
.concat(data.room.filter(x => !x.without_global))
|
||||
.forEach(r => write_property(r))
|
||||
|
||||
result += "}\n\n"
|
||||
}
|
||||
|
||||
{
|
||||
result += `public sealed partial class DefaultConfig
|
||||
{
|
||||
internal static readonly DefaultConfig Instance = new DefaultConfig();
|
||||
private DefaultConfig() {}\n\n`;
|
||||
|
||||
data.global
|
||||
.concat(data.room.filter(x => !x.without_global))
|
||||
.forEach(r => {
|
||||
result += `public ${r.type} ${r.name} => ${r.default};\n\n`
|
||||
})
|
||||
|
||||
result += "}\n\n"
|
||||
}
|
||||
|
||||
result += CODE_FOOTER;
|
||||
|
||||
fs.writeFileSync("./Config.gen.cs", result, {
|
||||
encoding: "utf8"
|
||||
});
|
|
@ -1,22 +1,21 @@
|
|||
using System.Net.Sockets;
|
||||
using Autofac;
|
||||
using BililiveRecorder.Core.Callback;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.Core.Config.V2;
|
||||
|
||||
#nullable enable
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
public class CoreModule : Module
|
||||
{
|
||||
public CoreModule()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override void Load(ContainerBuilder builder)
|
||||
{
|
||||
builder.RegisterType<ConfigV1>().AsSelf().InstancePerMatchingLifetimeScope("recorder_root");
|
||||
builder.Register(x => x.Resolve<IRecorder>().Config).As<ConfigV2>();
|
||||
builder.Register(x => x.Resolve<ConfigV2>().Global).As<GlobalConfig>();
|
||||
builder.RegisterType<BililiveAPI>().AsSelf().InstancePerMatchingLifetimeScope("recorder_root");
|
||||
builder.RegisterType<BasicWebhook>().AsSelf().InstancePerMatchingLifetimeScope("recorder_root");
|
||||
builder.RegisterType<TcpClient>().AsSelf().ExternallyOwned();
|
||||
builder.RegisterType<StreamMonitor>().As<IStreamMonitor>().ExternallyOwned();
|
||||
builder.RegisterType<RecordedRoom>().As<IRecordedRoom>().ExternallyOwned();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using BililiveRecorder.Core.Callback;
|
||||
using BililiveRecorder.Core.Config.V2;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
|
||||
#nullable enable
|
||||
|
@ -10,6 +11,8 @@ namespace BililiveRecorder.Core
|
|||
{
|
||||
Guid Guid { get; }
|
||||
|
||||
RoomConfig RoomConfig { get; }
|
||||
|
||||
int ShortRoomId { get; }
|
||||
int RoomId { get; }
|
||||
string StreamerName { get; }
|
||||
|
|
|
@ -2,13 +2,14 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.Core.Config.V2;
|
||||
|
||||
#nullable enable
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
public interface IRecorder : INotifyPropertyChanged, INotifyCollectionChanged, IEnumerable<IRecordedRoom>, ICollection<IRecordedRoom>, IDisposable
|
||||
{
|
||||
ConfigV1 Config { get; }
|
||||
ConfigV2? Config { get; }
|
||||
|
||||
bool Initialize(string workdir);
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ namespace BililiveRecorder.Core
|
|||
{
|
||||
public interface IStreamMonitor : IDisposable, INotifyPropertyChanged
|
||||
{
|
||||
int Roomid { get; }
|
||||
bool IsMonitoring { get; }
|
||||
bool IsDanmakuConnected { get; }
|
||||
event RoomInfoUpdatedEvent RoomInfoUpdated;
|
||||
|
|
|
@ -9,7 +9,7 @@ using System.Net.Http.Headers;
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.Core.Callback;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.Core.Config.V2;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
using NLog;
|
||||
|
||||
|
@ -21,67 +21,68 @@ namespace BililiveRecorder.Core
|
|||
private static readonly Random random = new Random();
|
||||
private static readonly Version VERSION_1_0 = new Version(1, 0);
|
||||
|
||||
private int _roomid;
|
||||
private int _realRoomid;
|
||||
private int _shortRoomid;
|
||||
private string _streamerName;
|
||||
private string _title;
|
||||
private bool _isStreaming;
|
||||
|
||||
public int ShortRoomId
|
||||
{
|
||||
get => _roomid;
|
||||
get => this._shortRoomid;
|
||||
private set
|
||||
{
|
||||
if (value == _roomid) { return; }
|
||||
_roomid = value;
|
||||
TriggerPropertyChanged(nameof(ShortRoomId));
|
||||
if (value == this._shortRoomid) { return; }
|
||||
this._shortRoomid = value;
|
||||
this.TriggerPropertyChanged(nameof(this.ShortRoomId));
|
||||
}
|
||||
}
|
||||
public int RoomId
|
||||
{
|
||||
get => _realRoomid;
|
||||
get => this.RoomConfig.RoomId;
|
||||
private set
|
||||
{
|
||||
if (value == _realRoomid) { return; }
|
||||
_realRoomid = value;
|
||||
TriggerPropertyChanged(nameof(RoomId));
|
||||
if (value == this.RoomConfig.RoomId) { return; }
|
||||
this.RoomConfig.RoomId = value;
|
||||
this.TriggerPropertyChanged(nameof(this.RoomId));
|
||||
}
|
||||
}
|
||||
public string StreamerName
|
||||
{
|
||||
get => _streamerName;
|
||||
get => this._streamerName;
|
||||
private set
|
||||
{
|
||||
if (value == _streamerName) { return; }
|
||||
_streamerName = value;
|
||||
TriggerPropertyChanged(nameof(StreamerName));
|
||||
if (value == this._streamerName) { return; }
|
||||
this._streamerName = value;
|
||||
this.TriggerPropertyChanged(nameof(this.StreamerName));
|
||||
}
|
||||
}
|
||||
public string Title
|
||||
{
|
||||
get => _title;
|
||||
get => this._title;
|
||||
private set
|
||||
{
|
||||
if (value == _title) { return; }
|
||||
_title = value;
|
||||
TriggerPropertyChanged(nameof(Title));
|
||||
if (value == this._title) { return; }
|
||||
this._title = value;
|
||||
this.TriggerPropertyChanged(nameof(this.Title));
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsMonitoring => StreamMonitor.IsMonitoring;
|
||||
public bool IsRecording => !(StreamDownloadTask?.IsCompleted ?? true);
|
||||
public bool IsDanmakuConnected => StreamMonitor.IsDanmakuConnected;
|
||||
public bool IsMonitoring => this.StreamMonitor.IsMonitoring;
|
||||
public bool IsRecording => !(this.StreamDownloadTask?.IsCompleted ?? true);
|
||||
public bool IsDanmakuConnected => this.StreamMonitor.IsDanmakuConnected;
|
||||
public bool IsStreaming
|
||||
{
|
||||
get => _isStreaming;
|
||||
get => this._isStreaming;
|
||||
private set
|
||||
{
|
||||
if (value == _isStreaming) { return; }
|
||||
_isStreaming = value;
|
||||
TriggerPropertyChanged(nameof(IsStreaming));
|
||||
if (value == this._isStreaming) { return; }
|
||||
this._isStreaming = value;
|
||||
this.TriggerPropertyChanged(nameof(this.IsStreaming));
|
||||
}
|
||||
}
|
||||
|
||||
public RoomConfig RoomConfig { get; }
|
||||
|
||||
private RecordEndData recordEndData;
|
||||
public event EventHandler<RecordEndData> RecordEnded;
|
||||
|
||||
|
@ -90,16 +91,15 @@ namespace BililiveRecorder.Core
|
|||
private IFlvStreamProcessor _processor;
|
||||
public IFlvStreamProcessor Processor
|
||||
{
|
||||
get => _processor;
|
||||
get => this._processor;
|
||||
private set
|
||||
{
|
||||
if (value == _processor) { return; }
|
||||
_processor = value;
|
||||
TriggerPropertyChanged(nameof(Processor));
|
||||
if (value == this._processor) { return; }
|
||||
this._processor = value;
|
||||
this.TriggerPropertyChanged(nameof(this.Processor));
|
||||
}
|
||||
}
|
||||
|
||||
private ConfigV1 _config { get; }
|
||||
private BililiveAPI BililiveAPI { get; }
|
||||
public IStreamMonitor StreamMonitor { get; }
|
||||
|
||||
|
@ -118,41 +118,57 @@ namespace BililiveRecorder.Core
|
|||
public DateTime LastUpdateDateTime { get; private set; } = DateTime.Now;
|
||||
public double DownloadSpeedPersentage
|
||||
{
|
||||
get { return _DownloadSpeedPersentage; }
|
||||
private set { if (value != _DownloadSpeedPersentage) { _DownloadSpeedPersentage = value; TriggerPropertyChanged(nameof(DownloadSpeedPersentage)); } }
|
||||
get { return this._DownloadSpeedPersentage; }
|
||||
private set { if (value != this._DownloadSpeedPersentage) { this._DownloadSpeedPersentage = value; this.TriggerPropertyChanged(nameof(this.DownloadSpeedPersentage)); } }
|
||||
}
|
||||
public double DownloadSpeedMegaBitps
|
||||
{
|
||||
get { return _DownloadSpeedMegaBitps; }
|
||||
private set { if (value != _DownloadSpeedMegaBitps) { _DownloadSpeedMegaBitps = value; TriggerPropertyChanged(nameof(DownloadSpeedMegaBitps)); } }
|
||||
get { return this._DownloadSpeedMegaBitps; }
|
||||
private set { if (value != this._DownloadSpeedMegaBitps) { this._DownloadSpeedMegaBitps = value; this.TriggerPropertyChanged(nameof(this.DownloadSpeedMegaBitps)); } }
|
||||
}
|
||||
|
||||
public Guid Guid { get; } = Guid.NewGuid();
|
||||
|
||||
public RecordedRoom(ConfigV1 config,
|
||||
IBasicDanmakuWriter basicDanmakuWriter,
|
||||
Func<int, IStreamMonitor> newIStreamMonitor,
|
||||
// TODO: 重构 DI
|
||||
public RecordedRoom(Func<RoomConfig, IBasicDanmakuWriter> newBasicDanmakuWriter,
|
||||
Func<RoomConfig, IStreamMonitor> newIStreamMonitor,
|
||||
Func<IFlvStreamProcessor> newIFlvStreamProcessor,
|
||||
BililiveAPI bililiveAPI,
|
||||
int roomid)
|
||||
RoomConfig roomConfig)
|
||||
{
|
||||
this.RoomConfig = roomConfig;
|
||||
this.StreamerName = "获取中...";
|
||||
|
||||
this.BililiveAPI = bililiveAPI;
|
||||
|
||||
this.newIFlvStreamProcessor = newIFlvStreamProcessor;
|
||||
|
||||
_config = config;
|
||||
BililiveAPI = bililiveAPI;
|
||||
this.basicDanmakuWriter = newBasicDanmakuWriter(this.RoomConfig);
|
||||
|
||||
this.basicDanmakuWriter = basicDanmakuWriter;
|
||||
this.StreamMonitor = newIStreamMonitor(this.RoomConfig);
|
||||
this.StreamMonitor.RoomInfoUpdated += this.StreamMonitor_RoomInfoUpdated;
|
||||
this.StreamMonitor.StreamStarted += this.StreamMonitor_StreamStarted;
|
||||
this.StreamMonitor.ReceivedDanmaku += this.StreamMonitor_ReceivedDanmaku;
|
||||
this.StreamMonitor.PropertyChanged += this.StreamMonitor_PropertyChanged;
|
||||
|
||||
RoomId = roomid;
|
||||
StreamerName = "获取中...";
|
||||
this.PropertyChanged += this.RecordedRoom_PropertyChanged;
|
||||
|
||||
StreamMonitor = newIStreamMonitor(RoomId);
|
||||
StreamMonitor.RoomInfoUpdated += StreamMonitor_RoomInfoUpdated;
|
||||
StreamMonitor.StreamStarted += StreamMonitor_StreamStarted;
|
||||
StreamMonitor.ReceivedDanmaku += StreamMonitor_ReceivedDanmaku;
|
||||
StreamMonitor.PropertyChanged += StreamMonitor_PropertyChanged;
|
||||
this.StreamMonitor.FetchRoomInfoAsync();
|
||||
|
||||
StreamMonitor.FetchRoomInfoAsync();
|
||||
if (this.RoomConfig.AutoRecord)
|
||||
this.Start();
|
||||
}
|
||||
|
||||
private void RecordedRoom_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(this.IsMonitoring):
|
||||
this.RoomConfig.AutoRecord = this.IsMonitoring;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void StreamMonitor_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
|
@ -160,7 +176,7 @@ namespace BililiveRecorder.Core
|
|||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(IStreamMonitor.IsDanmakuConnected):
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsDanmakuConnected)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsDanmakuConnected)));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -172,115 +188,116 @@ namespace BililiveRecorder.Core
|
|||
switch (e.Danmaku.MsgType)
|
||||
{
|
||||
case MsgTypeEnum.LiveStart:
|
||||
IsStreaming = true;
|
||||
this.IsStreaming = true;
|
||||
break;
|
||||
case MsgTypeEnum.LiveEnd:
|
||||
IsStreaming = false;
|
||||
break;
|
||||
case MsgTypeEnum.RoomChange:
|
||||
Title = e.Danmaku.Title;
|
||||
this.IsStreaming = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
basicDanmakuWriter.Write(e.Danmaku);
|
||||
this.basicDanmakuWriter.Write(e.Danmaku);
|
||||
}
|
||||
|
||||
private void StreamMonitor_RoomInfoUpdated(object sender, RoomInfoUpdatedArgs e)
|
||||
{
|
||||
RoomId = e.RoomInfo.RoomId;
|
||||
ShortRoomId = e.RoomInfo.ShortRoomId;
|
||||
StreamerName = e.RoomInfo.UserName;
|
||||
Title = e.RoomInfo.Title;
|
||||
IsStreaming = e.RoomInfo.IsStreaming;
|
||||
// TODO: StreamMonitor 里的 RoomInfoUpdated Handler 也会设置一次 RoomId
|
||||
// 暂时保持不变,此处的 RoomId 需要触发 PropertyChanged 事件
|
||||
this.RoomId = e.RoomInfo.RoomId;
|
||||
this.ShortRoomId = e.RoomInfo.ShortRoomId;
|
||||
this.StreamerName = e.RoomInfo.UserName;
|
||||
this.Title = e.RoomInfo.Title;
|
||||
this.IsStreaming = e.RoomInfo.IsStreaming;
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
if (disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
// TODO: 重构: 删除 Start() Stop() 通过 RoomConfig.AutoRecord 控制监控状态和逻辑
|
||||
if (this.disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
|
||||
var r = StreamMonitor.Start();
|
||||
TriggerPropertyChanged(nameof(IsMonitoring));
|
||||
var r = this.StreamMonitor.Start();
|
||||
this.TriggerPropertyChanged(nameof(this.IsMonitoring));
|
||||
return r;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
// TODO: 见 Start()
|
||||
if (this.disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
|
||||
StreamMonitor.Stop();
|
||||
TriggerPropertyChanged(nameof(IsMonitoring));
|
||||
this.StreamMonitor.Stop();
|
||||
this.TriggerPropertyChanged(nameof(this.IsMonitoring));
|
||||
}
|
||||
|
||||
public void RefreshRoomInfo()
|
||||
{
|
||||
if (disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
StreamMonitor.FetchRoomInfoAsync();
|
||||
if (this.disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
this.StreamMonitor.FetchRoomInfoAsync();
|
||||
}
|
||||
|
||||
private void StreamMonitor_StreamStarted(object sender, StreamStartedArgs e)
|
||||
{
|
||||
lock (StartupTaskLock)
|
||||
if (!IsRecording && (StartupTask?.IsCompleted ?? true))
|
||||
StartupTask = _StartRecordAsync();
|
||||
lock (this.StartupTaskLock)
|
||||
if (!this.IsRecording && (this.StartupTask?.IsCompleted ?? true))
|
||||
this.StartupTask = this._StartRecordAsync();
|
||||
}
|
||||
|
||||
public void StartRecord()
|
||||
{
|
||||
if (disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
StreamMonitor.Check(TriggerType.Manual);
|
||||
if (this.disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
this.StreamMonitor.Check(TriggerType.Manual);
|
||||
}
|
||||
|
||||
public void StopRecord()
|
||||
{
|
||||
if (disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
if (this.disposedValue) throw new ObjectDisposedException(nameof(RecordedRoom));
|
||||
|
||||
_retry = false;
|
||||
this._retry = false;
|
||||
try
|
||||
{
|
||||
if (cancellationTokenSource != null)
|
||||
if (this.cancellationTokenSource != null)
|
||||
{
|
||||
cancellationTokenSource.Cancel();
|
||||
if (!(StreamDownloadTask?.Wait(TimeSpan.FromSeconds(2)) ?? true))
|
||||
this.cancellationTokenSource.Cancel();
|
||||
if (!(this.StreamDownloadTask?.Wait(TimeSpan.FromSeconds(2)) ?? true))
|
||||
{
|
||||
logger.Log(RoomId, LogLevel.Warn, "停止录制超时,尝试强制关闭连接,请检查网络连接是否稳定");
|
||||
logger.Log(this.RoomId, LogLevel.Warn, "停止录制超时,尝试强制关闭连接,请检查网络连接是否稳定");
|
||||
|
||||
_stream?.Close();
|
||||
_stream?.Dispose();
|
||||
_response?.Dispose();
|
||||
StreamDownloadTask?.Wait();
|
||||
this._stream?.Close();
|
||||
this._stream?.Dispose();
|
||||
this._response?.Dispose();
|
||||
this.StreamDownloadTask?.Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Log(RoomId, LogLevel.Warn, "在尝试停止录制时发生错误,请检查网络连接是否稳定", ex);
|
||||
logger.Log(this.RoomId, LogLevel.Warn, "在尝试停止录制时发生错误,请检查网络连接是否稳定", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_retry = true;
|
||||
this._retry = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task _StartRecordAsync()
|
||||
{
|
||||
if (IsRecording)
|
||||
if (this.IsRecording)
|
||||
{
|
||||
// TODO: 这里逻辑可能有问题,StartupTask 会变成当前这个已经结束的
|
||||
logger.Log(RoomId, LogLevel.Warn, "已经在录制中了");
|
||||
logger.Log(this.RoomId, LogLevel.Warn, "已经在录制中了");
|
||||
return;
|
||||
}
|
||||
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
var token = cancellationTokenSource.Token;
|
||||
this.cancellationTokenSource = new CancellationTokenSource();
|
||||
var token = this.cancellationTokenSource.Token;
|
||||
try
|
||||
{
|
||||
var flv_path = await BililiveAPI.GetPlayUrlAsync(RoomId);
|
||||
var flv_path = await this.BililiveAPI.GetPlayUrlAsync(this.RoomId);
|
||||
if (string.IsNullOrWhiteSpace(flv_path))
|
||||
{
|
||||
if (_retry)
|
||||
if (this._retry)
|
||||
{
|
||||
StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)_config.TimingStreamRetry);
|
||||
this.StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)this.RoomConfig.TimingStreamRetry);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -294,7 +311,7 @@ namespace BililiveRecorder.Core
|
|||
}))
|
||||
{
|
||||
|
||||
client.Timeout = TimeSpan.FromMilliseconds(_config.TimingStreamConnect);
|
||||
client.Timeout = TimeSpan.FromMilliseconds(this.RoomConfig.TimingStreamConnect);
|
||||
client.DefaultRequestHeaders.Accept.Clear();
|
||||
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
|
||||
client.DefaultRequestHeaders.UserAgent.Clear();
|
||||
|
@ -303,46 +320,46 @@ namespace BililiveRecorder.Core
|
|||
client.DefaultRequestHeaders.Add("Origin", "https://live.bilibili.com");
|
||||
|
||||
|
||||
logger.Log(RoomId, LogLevel.Info, "连接直播服务器 " + new Uri(flv_path).Host);
|
||||
logger.Log(RoomId, LogLevel.Debug, "直播流地址: " + flv_path);
|
||||
logger.Log(this.RoomId, LogLevel.Info, "连接直播服务器 " + new Uri(flv_path).Host);
|
||||
logger.Log(this.RoomId, LogLevel.Debug, "直播流地址: " + flv_path);
|
||||
|
||||
_response = await client.GetAsync(flv_path, HttpCompletionOption.ResponseHeadersRead);
|
||||
this._response = await client.GetAsync(flv_path, HttpCompletionOption.ResponseHeadersRead);
|
||||
}
|
||||
|
||||
if (_response.StatusCode == HttpStatusCode.Redirect || _response.StatusCode == HttpStatusCode.Moved)
|
||||
if (this._response.StatusCode == HttpStatusCode.Redirect || this._response.StatusCode == HttpStatusCode.Moved)
|
||||
{
|
||||
// workaround for missing Referrer
|
||||
flv_path = _response.Headers.Location.OriginalString;
|
||||
_response.Dispose();
|
||||
flv_path = this._response.Headers.Location.OriginalString;
|
||||
this._response.Dispose();
|
||||
goto unwrap_redir;
|
||||
}
|
||||
else if (_response.StatusCode != HttpStatusCode.OK)
|
||||
else if (this._response.StatusCode != HttpStatusCode.OK)
|
||||
{
|
||||
logger.Log(RoomId, LogLevel.Info, string.Format("尝试下载直播流时服务器返回了 ({0}){1}", _response.StatusCode, _response.ReasonPhrase));
|
||||
logger.Log(this.RoomId, LogLevel.Info, string.Format("尝试下载直播流时服务器返回了 ({0}){1}", this._response.StatusCode, this._response.ReasonPhrase));
|
||||
|
||||
StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)_config.TimingStreamRetry);
|
||||
this.StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)this.RoomConfig.TimingStreamRetry);
|
||||
|
||||
_CleanupFlvRequest();
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Processor = newIFlvStreamProcessor().Initialize(GetStreamFilePath, GetClipFilePath, _config.EnabledFeature, _config.CuttingMode);
|
||||
Processor.ClipLengthFuture = _config.ClipLengthFuture;
|
||||
Processor.ClipLengthPast = _config.ClipLengthPast;
|
||||
Processor.CuttingNumber = _config.CuttingNumber;
|
||||
Processor.StreamFinalized += (sender, e) => { basicDanmakuWriter.Disable(); };
|
||||
Processor.FileFinalized += (sender, size) =>
|
||||
this.Processor = this.newIFlvStreamProcessor().Initialize(this.GetStreamFilePath, this.GetClipFilePath, this.RoomConfig.EnabledFeature, this.RoomConfig.CuttingMode);
|
||||
this.Processor.ClipLengthFuture = this.RoomConfig.ClipLengthFuture;
|
||||
this.Processor.ClipLengthPast = this.RoomConfig.ClipLengthPast;
|
||||
this.Processor.CuttingNumber = this.RoomConfig.CuttingNumber;
|
||||
this.Processor.StreamFinalized += (sender, e) => { this.basicDanmakuWriter.Disable(); };
|
||||
this.Processor.FileFinalized += (sender, size) =>
|
||||
{
|
||||
if (recordEndData is null) return;
|
||||
var data = recordEndData;
|
||||
recordEndData = null;
|
||||
if (this.recordEndData is null) return;
|
||||
var data = this.recordEndData;
|
||||
this.recordEndData = null;
|
||||
|
||||
data.EndRecordTime = DateTimeOffset.Now;
|
||||
data.FileSize = size;
|
||||
RecordEnded?.Invoke(this, data);
|
||||
};
|
||||
Processor.OnMetaData += (sender, e) =>
|
||||
this.Processor.OnMetaData += (sender, e) =>
|
||||
{
|
||||
e.Metadata["BililiveRecorder"] = new Dictionary<string, object>()
|
||||
{
|
||||
|
@ -356,26 +373,26 @@ namespace BililiveRecorder.Core
|
|||
},
|
||||
{
|
||||
"roomid",
|
||||
RoomId.ToString()
|
||||
this.RoomId.ToString()
|
||||
},
|
||||
{
|
||||
"streamername",
|
||||
StreamerName
|
||||
this.StreamerName
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
_stream = await _response.Content.ReadAsStreamAsync();
|
||||
this._stream = await this._response.Content.ReadAsStreamAsync();
|
||||
|
||||
try
|
||||
{
|
||||
if (_response.Headers.ConnectionClose == false || (_response.Headers.ConnectionClose is null && _response.Version != VERSION_1_0))
|
||||
_stream.ReadTimeout = 3 * 1000;
|
||||
if (this._response.Headers.ConnectionClose == false || (this._response.Headers.ConnectionClose is null && this._response.Version != VERSION_1_0))
|
||||
this._stream.ReadTimeout = 3 * 1000;
|
||||
}
|
||||
catch (InvalidOperationException) { }
|
||||
|
||||
StreamDownloadTask = Task.Run(_ReadStreamLoop);
|
||||
TriggerPropertyChanged(nameof(IsRecording));
|
||||
this.StreamDownloadTask = Task.Run(_ReadStreamLoop);
|
||||
this.TriggerPropertyChanged(nameof(this.IsRecording));
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
|
@ -384,16 +401,16 @@ namespace BililiveRecorder.Core
|
|||
// useless exception message :/
|
||||
|
||||
_CleanupFlvRequest();
|
||||
logger.Log(RoomId, LogLevel.Warn, "连接直播服务器超时。");
|
||||
StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)_config.TimingStreamRetry);
|
||||
logger.Log(this.RoomId, LogLevel.Warn, "连接直播服务器超时。");
|
||||
this.StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)this.RoomConfig.TimingStreamRetry);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_CleanupFlvRequest();
|
||||
logger.Log(RoomId, LogLevel.Error, "启动直播流下载出错。" + (_retry ? "将重试启动。" : ""), ex);
|
||||
if (_retry)
|
||||
logger.Log(this.RoomId, LogLevel.Error, "启动直播流下载出错。" + (this._retry ? "将重试启动。" : ""), ex);
|
||||
if (this._retry)
|
||||
{
|
||||
StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)_config.TimingStreamRetry);
|
||||
this.StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)this.RoomConfig.TimingStreamRetry);
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
@ -406,17 +423,17 @@ namespace BililiveRecorder.Core
|
|||
byte[] buffer = new byte[BUF_SIZE];
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
int bytesRead = await _stream.ReadAsync(buffer, 0, BUF_SIZE, token);
|
||||
int bytesRead = await this._stream.ReadAsync(buffer, 0, BUF_SIZE, token);
|
||||
_UpdateDownloadSpeed(bytesRead);
|
||||
if (bytesRead != 0)
|
||||
{
|
||||
if (bytesRead != BUF_SIZE)
|
||||
{
|
||||
Processor.AddBytes(buffer.Take(bytesRead).ToArray());
|
||||
this.Processor.AddBytes(buffer.Take(bytesRead).ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
Processor.AddBytes(buffer);
|
||||
this.Processor.AddBytes(buffer);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -425,19 +442,19 @@ namespace BililiveRecorder.Core
|
|||
}
|
||||
}
|
||||
|
||||
logger.Log(RoomId, LogLevel.Info,
|
||||
logger.Log(this.RoomId, LogLevel.Info,
|
||||
(token.IsCancellationRequested ? "本地操作结束当前录制。" : "服务器关闭直播流,可能是直播已结束。")
|
||||
+ (_retry ? "将重试启动。" : ""));
|
||||
if (_retry)
|
||||
+ (this._retry ? "将重试启动。" : ""));
|
||||
if (this._retry)
|
||||
{
|
||||
StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)_config.TimingStreamRetry);
|
||||
this.StreamMonitor.Check(TriggerType.HttpApiRecheck, (int)this.RoomConfig.TimingStreamRetry);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is ObjectDisposedException && token.IsCancellationRequested) { return; }
|
||||
|
||||
logger.Log(RoomId, LogLevel.Warn, "录播发生错误", e);
|
||||
logger.Log(this.RoomId, LogLevel.Warn, "录播发生错误", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
@ -446,55 +463,55 @@ namespace BililiveRecorder.Core
|
|||
}
|
||||
void _CleanupFlvRequest()
|
||||
{
|
||||
if (Processor != null)
|
||||
if (this.Processor != null)
|
||||
{
|
||||
Processor.FinallizeFile();
|
||||
Processor.Dispose();
|
||||
Processor = null;
|
||||
this.Processor.FinallizeFile();
|
||||
this.Processor.Dispose();
|
||||
this.Processor = null;
|
||||
}
|
||||
_stream?.Dispose();
|
||||
_stream = null;
|
||||
_response?.Dispose();
|
||||
_response = null;
|
||||
this._stream?.Dispose();
|
||||
this._stream = null;
|
||||
this._response?.Dispose();
|
||||
this._response = null;
|
||||
|
||||
_lastUpdateTimestamp = 0;
|
||||
DownloadSpeedMegaBitps = 0d;
|
||||
DownloadSpeedPersentage = 0d;
|
||||
TriggerPropertyChanged(nameof(IsRecording));
|
||||
this._lastUpdateTimestamp = 0;
|
||||
this.DownloadSpeedMegaBitps = 0d;
|
||||
this.DownloadSpeedPersentage = 0d;
|
||||
this.TriggerPropertyChanged(nameof(this.IsRecording));
|
||||
}
|
||||
void _UpdateDownloadSpeed(int bytesRead)
|
||||
{
|
||||
DateTime now = DateTime.Now;
|
||||
double passedSeconds = (now - LastUpdateDateTime).TotalSeconds;
|
||||
_lastUpdateSize += bytesRead;
|
||||
double passedSeconds = (now - this.LastUpdateDateTime).TotalSeconds;
|
||||
this._lastUpdateSize += bytesRead;
|
||||
if (passedSeconds > 1.5)
|
||||
{
|
||||
DownloadSpeedMegaBitps = _lastUpdateSize / passedSeconds * 8d / 1_000_000d; // mega bit per second
|
||||
DownloadSpeedPersentage = (DownloadSpeedPersentage / 2) + ((Processor.TotalMaxTimestamp - _lastUpdateTimestamp) / passedSeconds / 1000 / 2); // ((RecordedTime/1000) / RealTime)%
|
||||
_lastUpdateTimestamp = Processor.TotalMaxTimestamp;
|
||||
_lastUpdateSize = 0;
|
||||
LastUpdateDateTime = now;
|
||||
this.DownloadSpeedMegaBitps = this._lastUpdateSize / passedSeconds * 8d / 1_000_000d; // mega bit per second
|
||||
this.DownloadSpeedPersentage = (this.DownloadSpeedPersentage / 2) + ((this.Processor.TotalMaxTimestamp - this._lastUpdateTimestamp) / passedSeconds / 1000 / 2); // ((RecordedTime/1000) / RealTime)%
|
||||
this._lastUpdateTimestamp = this.Processor.TotalMaxTimestamp;
|
||||
this._lastUpdateSize = 0;
|
||||
this.LastUpdateDateTime = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called by API or GUI
|
||||
public void Clip() => Processor?.Clip();
|
||||
public void Clip() => this.Processor?.Clip();
|
||||
|
||||
public void Shutdown() => Dispose(true);
|
||||
public void Shutdown() => this.Dispose(true);
|
||||
|
||||
private (string fullPath, string relativePath) GetStreamFilePath()
|
||||
{
|
||||
var path = FormatFilename(_config.RecordFilenameFormat);
|
||||
var path = this.FormatFilename(this.RoomConfig.RecordFilenameFormat);
|
||||
|
||||
// 有点脏的写法,不过凑合吧
|
||||
if (_config.RecordDanmaku)
|
||||
if (this.RoomConfig.RecordDanmaku)
|
||||
{
|
||||
var xmlpath = Path.ChangeExtension(path.fullPath, "xml");
|
||||
basicDanmakuWriter.EnableWithPath(xmlpath, this);
|
||||
this.basicDanmakuWriter.EnableWithPath(xmlpath, this);
|
||||
}
|
||||
|
||||
recordEndData = new RecordEndData
|
||||
this.recordEndData = new RecordEndData
|
||||
{
|
||||
RoomId = RoomId,
|
||||
Title = Title,
|
||||
|
@ -506,7 +523,7 @@ namespace BililiveRecorder.Core
|
|||
return path;
|
||||
}
|
||||
|
||||
private string GetClipFilePath() => FormatFilename(_config.ClipFilenameFormat).fullPath;
|
||||
private string GetClipFilePath() => this.FormatFilename(this.RoomConfig.ClipFilenameFormat).fullPath;
|
||||
|
||||
private (string fullPath, string relativePath) FormatFilename(string formatString)
|
||||
{
|
||||
|
@ -519,29 +536,30 @@ namespace BililiveRecorder.Core
|
|||
.Replace(@"{date}", date)
|
||||
.Replace(@"{time}", time)
|
||||
.Replace(@"{random}", randomStr)
|
||||
.Replace(@"{roomid}", RoomId.ToString())
|
||||
.Replace(@"{title}", Title.RemoveInvalidFileName())
|
||||
.Replace(@"{name}", StreamerName.RemoveInvalidFileName());
|
||||
.Replace(@"{roomid}", this.RoomId.ToString())
|
||||
.Replace(@"{title}", this.Title.RemoveInvalidFileName())
|
||||
.Replace(@"{name}", this.StreamerName.RemoveInvalidFileName());
|
||||
|
||||
if (!relativePath.EndsWith(".flv", StringComparison.OrdinalIgnoreCase))
|
||||
relativePath += ".flv";
|
||||
|
||||
relativePath = relativePath.RemoveInvalidFileName(ignore_slash: true);
|
||||
var fullPath = Path.Combine(_config.WorkDirectory, relativePath);
|
||||
var workDirectory = this.RoomConfig.WorkDirectory;
|
||||
var fullPath = Path.Combine(workDirectory, relativePath);
|
||||
fullPath = Path.GetFullPath(fullPath);
|
||||
|
||||
if (!CheckPath(_config.WorkDirectory, Path.GetDirectoryName(fullPath)))
|
||||
if (!CheckPath(workDirectory, Path.GetDirectoryName(fullPath)))
|
||||
{
|
||||
logger.Log(RoomId, LogLevel.Warn, "录制文件位置超出允许范围,请检查设置。将写入到默认路径。");
|
||||
relativePath = Path.Combine(RoomId.ToString(), $"{RoomId}-{date}-{time}-{randomStr}.flv");
|
||||
fullPath = Path.Combine(_config.WorkDirectory, relativePath);
|
||||
logger.Log(this.RoomId, LogLevel.Warn, "录制文件位置超出允许范围,请检查设置。将写入到默认路径。");
|
||||
relativePath = Path.Combine(this.RoomId.ToString(), $"{this.RoomId}-{date}-{time}-{randomStr}.flv");
|
||||
fullPath = Path.Combine(workDirectory, relativePath);
|
||||
}
|
||||
|
||||
if (new FileInfo(relativePath).Exists)
|
||||
{
|
||||
logger.Log(RoomId, LogLevel.Warn, "录制文件名冲突,请检查设置。将写入到默认路径。");
|
||||
relativePath = Path.Combine(RoomId.ToString(), $"{RoomId}-{date}-{time}-{randomStr}.flv");
|
||||
fullPath = Path.Combine(_config.WorkDirectory, relativePath);
|
||||
logger.Log(this.RoomId, LogLevel.Warn, "录制文件名冲突,请检查设置。将写入到默认路径。");
|
||||
relativePath = Path.Combine(this.RoomId.ToString(), $"{this.RoomId}-{date}-{time}-{randomStr}.flv");
|
||||
fullPath = Path.Combine(workDirectory, relativePath);
|
||||
}
|
||||
|
||||
return (fullPath, relativePath);
|
||||
|
@ -578,34 +596,34 @@ namespace BililiveRecorder.Core
|
|||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
if (!this.disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Stop();
|
||||
StopRecord();
|
||||
Processor?.FinallizeFile();
|
||||
Processor?.Dispose();
|
||||
StreamMonitor?.Dispose();
|
||||
_response?.Dispose();
|
||||
_stream?.Dispose();
|
||||
cancellationTokenSource?.Dispose();
|
||||
basicDanmakuWriter?.Dispose();
|
||||
this.Stop();
|
||||
this.StopRecord();
|
||||
this.Processor?.FinallizeFile();
|
||||
this.Processor?.Dispose();
|
||||
this.StreamMonitor?.Dispose();
|
||||
this._response?.Dispose();
|
||||
this._stream?.Dispose();
|
||||
this.cancellationTokenSource?.Dispose();
|
||||
this.basicDanmakuWriter?.Dispose();
|
||||
}
|
||||
|
||||
Processor = null;
|
||||
_response = null;
|
||||
_stream = null;
|
||||
cancellationTokenSource = null;
|
||||
this.Processor = null;
|
||||
this._response = null;
|
||||
this._stream = null;
|
||||
this.cancellationTokenSource = null;
|
||||
|
||||
disposedValue = true;
|
||||
this.disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
|
||||
Dispose(true);
|
||||
this.Dispose(true);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
|
|
@ -6,18 +6,19 @@ using System.Collections.Specialized;
|
|||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.Core.Callback;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.Core.Config.V2;
|
||||
using NLog;
|
||||
|
||||
#nullable enable
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
public class Recorder : IRecorder
|
||||
{
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
private readonly Func<int, IRecordedRoom> newIRecordedRoom;
|
||||
private readonly Func<RoomConfig, IRecordedRoom> newIRecordedRoom;
|
||||
private readonly CancellationTokenSource tokenSource;
|
||||
|
||||
private bool _valid = false;
|
||||
|
@ -25,24 +26,22 @@ namespace BililiveRecorder.Core
|
|||
|
||||
private ObservableCollection<IRecordedRoom> Rooms { get; } = new ObservableCollection<IRecordedRoom>();
|
||||
|
||||
public ConfigV1 Config { get; }
|
||||
public ConfigV2? Config { get; private set; }
|
||||
|
||||
private BasicWebhook Webhook { get; }
|
||||
private BasicWebhook? Webhook { get; set; }
|
||||
|
||||
public int Count => Rooms.Count;
|
||||
public int Count => this.Rooms.Count;
|
||||
public bool IsReadOnly => true;
|
||||
public IRecordedRoom this[int index] => Rooms[index];
|
||||
public IRecordedRoom this[int index] => this.Rooms[index];
|
||||
|
||||
public Recorder(ConfigV1 config, BasicWebhook webhook, Func<int, IRecordedRoom> iRecordedRoom)
|
||||
public Recorder(Func<RoomConfig, IRecordedRoom> iRecordedRoom)
|
||||
{
|
||||
newIRecordedRoom = iRecordedRoom ?? throw new ArgumentNullException(nameof(iRecordedRoom));
|
||||
Config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
Webhook = webhook ?? throw new ArgumentNullException(nameof(webhook));
|
||||
this.newIRecordedRoom = iRecordedRoom ?? throw new ArgumentNullException(nameof(iRecordedRoom));
|
||||
|
||||
tokenSource = new CancellationTokenSource();
|
||||
Repeat.Interval(TimeSpan.FromSeconds(3), DownloadWatchdog, tokenSource.Token);
|
||||
this.tokenSource = new CancellationTokenSource();
|
||||
Repeat.Interval(TimeSpan.FromSeconds(3), this.DownloadWatchdog, this.tokenSource.Token);
|
||||
|
||||
Rooms.CollectionChanged += (sender, e) =>
|
||||
this.Rooms.CollectionChanged += (sender, e) =>
|
||||
{
|
||||
logger.Trace($"Rooms.CollectionChanged;{e.Action};" +
|
||||
$"O:{e.OldItems?.Cast<IRecordedRoom>()?.Select(rr => rr.RoomId.ToString())?.Aggregate((current, next) => current + "," + next)};" +
|
||||
|
@ -53,15 +52,15 @@ namespace BililiveRecorder.Core
|
|||
public bool Initialize(string workdir)
|
||||
{
|
||||
logger.Debug("Initialize: " + workdir);
|
||||
if (ConfigParser.Load(directory: workdir, config: Config))
|
||||
var config = ConfigParser.LoadFrom(directory: workdir);
|
||||
if (config is not null)
|
||||
{
|
||||
_valid = true;
|
||||
Config.WorkDirectory = workdir;
|
||||
if ((Config.RoomList?.Count ?? 0) > 0)
|
||||
{
|
||||
Config.RoomList.ForEach((r) => AddRoom(r.Roomid, r.Enabled));
|
||||
}
|
||||
ConfigParser.Save(Config.WorkDirectory, Config);
|
||||
this.Config = config;
|
||||
this.Config.Global.WorkDirectory = workdir;
|
||||
this.Webhook = new BasicWebhook(this.Config);
|
||||
this._valid = true;
|
||||
this.Config.Rooms.ForEach(r => this.AddRoom(r));
|
||||
ConfigParser.SaveTo(this.Config.Global.WorkDirectory, this.Config);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
|
@ -75,7 +74,7 @@ namespace BililiveRecorder.Core
|
|||
/// </summary>
|
||||
/// <param name="roomid">房间号(支持短号)</param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"/>
|
||||
public void AddRoom(int roomid) => AddRoom(roomid, true);
|
||||
public void AddRoom(int roomid) => this.AddRoom(roomid, true);
|
||||
|
||||
/// <summary>
|
||||
/// 添加直播间到录播姬
|
||||
|
@ -87,21 +86,19 @@ namespace BililiveRecorder.Core
|
|||
{
|
||||
try
|
||||
{
|
||||
if (!_valid) { throw new InvalidOperationException("Not Initialized"); }
|
||||
if (!this._valid) { throw new InvalidOperationException("Not Initialized"); }
|
||||
if (roomid <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(roomid), "房间号需要大于0");
|
||||
}
|
||||
|
||||
var rr = newIRecordedRoom(roomid);
|
||||
if (enabled)
|
||||
var config = new RoomConfig
|
||||
{
|
||||
Task.Run(() => rr.Start());
|
||||
}
|
||||
RoomId = roomid,
|
||||
AutoRecord = enabled,
|
||||
};
|
||||
|
||||
logger.Debug("AddRoom 添加了 {roomid} 直播间 ", rr.RoomId);
|
||||
rr.RecordEnded += this.RecordedRoom_RecordEnded;
|
||||
Rooms.Add(rr);
|
||||
this.AddRoom(config);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -109,6 +106,29 @@ namespace BililiveRecorder.Core
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加直播间到录播姬
|
||||
/// </summary>
|
||||
/// <param name="roomConfig">房间设置</param>
|
||||
public void AddRoom(RoomConfig roomConfig)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!this._valid) { throw new InvalidOperationException("Not Initialized"); }
|
||||
|
||||
roomConfig.SetParent(this.Config?.Global);
|
||||
var rr = this.newIRecordedRoom(roomConfig);
|
||||
|
||||
logger.Debug("AddRoom 添加了 {roomid} 直播间 ", rr.RoomId);
|
||||
rr.RecordEnded += this.RecordedRoom_RecordEnded;
|
||||
this.Rooms.Add(rr);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Debug(ex, "AddRoom 添加 {roomid} 直播间错误 ", roomConfig.RoomId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从录播姬移除直播间
|
||||
/// </summary>
|
||||
|
@ -116,56 +136,49 @@ namespace BililiveRecorder.Core
|
|||
public void RemoveRoom(IRecordedRoom rr)
|
||||
{
|
||||
if (rr is null) return;
|
||||
if (!_valid) { throw new InvalidOperationException("Not Initialized"); }
|
||||
if (!this._valid) { throw new InvalidOperationException("Not Initialized"); }
|
||||
rr.Shutdown();
|
||||
rr.RecordEnded -= RecordedRoom_RecordEnded;
|
||||
rr.RecordEnded -= this.RecordedRoom_RecordEnded;
|
||||
logger.Debug("RemoveRoom 移除了直播间 {roomid}", rr.RoomId);
|
||||
Rooms.Remove(rr);
|
||||
this.Rooms.Remove(rr);
|
||||
}
|
||||
|
||||
private void Shutdown()
|
||||
{
|
||||
if (!_valid) { return; }
|
||||
if (!this._valid) { return; }
|
||||
logger.Debug("Shutdown called.");
|
||||
tokenSource.Cancel();
|
||||
this.tokenSource.Cancel();
|
||||
|
||||
SaveConfigToFile();
|
||||
this.SaveConfigToFile();
|
||||
|
||||
Rooms.ToList().ForEach(rr =>
|
||||
this.Rooms.ToList().ForEach(rr =>
|
||||
{
|
||||
rr.Shutdown();
|
||||
});
|
||||
|
||||
Rooms.Clear();
|
||||
this.Rooms.Clear();
|
||||
}
|
||||
|
||||
private void RecordedRoom_RecordEnded(object sender, RecordEndData e) => Webhook.Send(e);
|
||||
private void RecordedRoom_RecordEnded(object sender, RecordEndData e) => this.Webhook?.Send(e);
|
||||
|
||||
public void SaveConfigToFile()
|
||||
{
|
||||
Config.RoomList = new List<RoomV1>();
|
||||
Rooms.ToList().ForEach(rr =>
|
||||
{
|
||||
Config.RoomList.Add(new RoomV1()
|
||||
{
|
||||
Roomid = rr.RoomId,
|
||||
Enabled = rr.IsMonitoring,
|
||||
});
|
||||
});
|
||||
if (this.Config is null) return;
|
||||
|
||||
ConfigParser.Save(Config.WorkDirectory, Config);
|
||||
this.Config.Rooms = this.Rooms.Select(x => x.RoomConfig).ToList();
|
||||
ConfigParser.SaveTo(this.Config.Global.WorkDirectory!, this.Config);
|
||||
}
|
||||
|
||||
private void DownloadWatchdog()
|
||||
{
|
||||
if (!_valid) { return; }
|
||||
if (!this._valid) { return; }
|
||||
try
|
||||
{
|
||||
Rooms.ToList().ForEach(room =>
|
||||
this.Rooms.ToList().ForEach(room =>
|
||||
{
|
||||
if (room.IsRecording)
|
||||
{
|
||||
if (DateTime.Now - room.LastUpdateDateTime > TimeSpan.FromMilliseconds(Config.TimingWatchdogTimeout))
|
||||
if (DateTime.Now - room.LastUpdateDateTime > TimeSpan.FromMilliseconds(this.Config!.Global.TimingWatchdogTimeout))
|
||||
{
|
||||
logger.Warn("服务器未断开连接但停止提供 [{roomid}] 直播间的直播数据,通常是录制侧网络不稳定导致,将会断开重连", room.RoomId);
|
||||
room.StopRecord();
|
||||
|
@ -183,37 +196,37 @@ namespace BililiveRecorder.Core
|
|||
void ICollection<IRecordedRoom>.Add(IRecordedRoom item) => throw new NotSupportedException("Collection is readonly");
|
||||
void ICollection<IRecordedRoom>.Clear() => throw new NotSupportedException("Collection is readonly");
|
||||
bool ICollection<IRecordedRoom>.Remove(IRecordedRoom item) => throw new NotSupportedException("Collection is readonly");
|
||||
bool ICollection<IRecordedRoom>.Contains(IRecordedRoom item) => Rooms.Contains(item);
|
||||
void ICollection<IRecordedRoom>.CopyTo(IRecordedRoom[] array, int arrayIndex) => Rooms.CopyTo(array, arrayIndex);
|
||||
public IEnumerator<IRecordedRoom> GetEnumerator() => Rooms.GetEnumerator();
|
||||
IEnumerator<IRecordedRoom> IEnumerable<IRecordedRoom>.GetEnumerator() => Rooms.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => Rooms.GetEnumerator();
|
||||
bool ICollection<IRecordedRoom>.Contains(IRecordedRoom item) => this.Rooms.Contains(item);
|
||||
void ICollection<IRecordedRoom>.CopyTo(IRecordedRoom[] array, int arrayIndex) => this.Rooms.CopyTo(array, arrayIndex);
|
||||
public IEnumerator<IRecordedRoom> GetEnumerator() => this.Rooms.GetEnumerator();
|
||||
IEnumerator<IRecordedRoom> IEnumerable<IRecordedRoom>.GetEnumerator() => this.Rooms.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.Rooms.GetEnumerator();
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged
|
||||
{
|
||||
add => (Rooms as INotifyPropertyChanged).PropertyChanged += value;
|
||||
remove => (Rooms as INotifyPropertyChanged).PropertyChanged -= value;
|
||||
add => (this.Rooms as INotifyPropertyChanged).PropertyChanged += value;
|
||||
remove => (this.Rooms as INotifyPropertyChanged).PropertyChanged -= value;
|
||||
}
|
||||
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged
|
||||
{
|
||||
add => (Rooms as INotifyCollectionChanged).CollectionChanged += value;
|
||||
remove => (Rooms as INotifyCollectionChanged).CollectionChanged -= value;
|
||||
add => (this.Rooms as INotifyCollectionChanged).CollectionChanged += value;
|
||||
remove => (this.Rooms as INotifyCollectionChanged).CollectionChanged -= value;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
if (!this.disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// dispose managed state (managed objects)
|
||||
Shutdown();
|
||||
this.Shutdown();
|
||||
}
|
||||
|
||||
// free unmanaged resources (unmanaged objects) and override finalizer
|
||||
// set large fields to null
|
||||
disposedValue = true;
|
||||
this.disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -227,7 +240,7 @@ namespace BililiveRecorder.Core
|
|||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
this.Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ namespace BililiveRecorder.Core
|
|||
}
|
||||
}
|
||||
|
||||
static class CancellationTokenExtensions
|
||||
internal static class CancellationTokenExtensions
|
||||
{
|
||||
public static bool WaitCancellationRequested(
|
||||
this CancellationToken token,
|
||||
|
|
|
@ -7,7 +7,7 @@ using System.Net.Sockets;
|
|||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.Core.Config.V2;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
@ -31,7 +31,7 @@ namespace BililiveRecorder.Core
|
|||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
private readonly Func<TcpClient> funcTcpClient;
|
||||
private readonly ConfigV1 config;
|
||||
private readonly RoomConfig roomConfig;
|
||||
private readonly BililiveAPI bililiveAPI;
|
||||
|
||||
private Exception dmError = null;
|
||||
|
@ -42,73 +42,74 @@ namespace BililiveRecorder.Core
|
|||
private bool dmConnectionTriggered = false;
|
||||
private readonly Timer httpTimer;
|
||||
|
||||
public int Roomid { get; private set; } = 0;
|
||||
private int RoomId { get => this.roomConfig.RoomId; set => this.roomConfig.RoomId = value; }
|
||||
|
||||
public bool IsMonitoring { get; private set; } = false;
|
||||
public bool IsDanmakuConnected => dmClient?.Connected ?? false;
|
||||
public bool IsDanmakuConnected => this.dmClient?.Connected ?? false;
|
||||
public event RoomInfoUpdatedEvent RoomInfoUpdated;
|
||||
public event StreamStartedEvent StreamStarted;
|
||||
public event ReceivedDanmakuEvt ReceivedDanmaku;
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public StreamMonitor(int roomid, Func<TcpClient> funcTcpClient, ConfigV1 config, BililiveAPI bililiveAPI)
|
||||
public StreamMonitor(RoomConfig roomConfig, Func<TcpClient> funcTcpClient, BililiveAPI bililiveAPI)
|
||||
{
|
||||
this.funcTcpClient = funcTcpClient;
|
||||
this.config = config;
|
||||
this.roomConfig = roomConfig;
|
||||
this.bililiveAPI = bililiveAPI;
|
||||
|
||||
Roomid = roomid;
|
||||
ReceivedDanmaku += this.Receiver_ReceivedDanmaku;
|
||||
RoomInfoUpdated += this.StreamMonitor_RoomInfoUpdated;
|
||||
|
||||
ReceivedDanmaku += Receiver_ReceivedDanmaku;
|
||||
RoomInfoUpdated += StreamMonitor_RoomInfoUpdated;
|
||||
|
||||
dmTokenSource = new CancellationTokenSource();
|
||||
this.dmTokenSource = new CancellationTokenSource();
|
||||
Repeat.Interval(TimeSpan.FromSeconds(30), () =>
|
||||
{
|
||||
if (dmNetStream != null && dmNetStream.CanWrite)
|
||||
if (this.dmNetStream != null && this.dmNetStream.CanWrite)
|
||||
{
|
||||
try
|
||||
{
|
||||
SendSocketData(2);
|
||||
this.SendSocketData(2);
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
}, dmTokenSource.Token);
|
||||
}, this.dmTokenSource.Token);
|
||||
|
||||
httpTimer = new Timer(config.TimingCheckInterval * 1000)
|
||||
this.httpTimer = new Timer(roomConfig.TimingCheckInterval * 1000)
|
||||
{
|
||||
Enabled = false,
|
||||
AutoReset = true,
|
||||
SynchronizingObject = null,
|
||||
Site = null
|
||||
};
|
||||
httpTimer.Elapsed += (sender, e) =>
|
||||
this.httpTimer.Elapsed += (sender, e) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Check(TriggerType.HttpApi);
|
||||
this.Check(TriggerType.HttpApi);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Log(Roomid, LogLevel.Warn, "获取直播间开播状态出错", ex);
|
||||
logger.Log(this.RoomId, LogLevel.Warn, "获取直播间开播状态出错", ex);
|
||||
}
|
||||
};
|
||||
|
||||
config.PropertyChanged += (sender, e) =>
|
||||
roomConfig.PropertyChanged += (sender, e) =>
|
||||
{
|
||||
if (e.PropertyName.Equals(nameof(config.TimingCheckInterval)))
|
||||
if (e.PropertyName.Equals(nameof(roomConfig.TimingCheckInterval)))
|
||||
{
|
||||
httpTimer.Interval = config.TimingCheckInterval * 1000;
|
||||
this.httpTimer.Interval = roomConfig.TimingCheckInterval * 1000;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void StreamMonitor_RoomInfoUpdated(object sender, RoomInfoUpdatedArgs e)
|
||||
{
|
||||
Roomid = e.RoomInfo.RoomId;
|
||||
if (!dmConnectionTriggered)
|
||||
this.RoomId = e.RoomInfo.RoomId;
|
||||
// TODO: RecordedRoom 里的 RoomInfoUpdated Handler 也会设置一次 RoomId
|
||||
// 暂时保持不变,此处需要使用请求返回的房间号连接弹幕服务器
|
||||
if (!this.dmConnectionTriggered)
|
||||
{
|
||||
dmConnectionTriggered = true;
|
||||
Task.Run(() => ConnectWithRetryAsync());
|
||||
this.dmConnectionTriggered = true;
|
||||
Task.Run(() => this.ConnectWithRetryAsync());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,7 +118,7 @@ namespace BililiveRecorder.Core
|
|||
switch (e.Danmaku.MsgType)
|
||||
{
|
||||
case MsgTypeEnum.LiveStart:
|
||||
if (IsMonitoring)
|
||||
if (this.IsMonitoring)
|
||||
{
|
||||
Task.Run(() => StreamStarted?.Invoke(this, new StreamStartedArgs() { type = TriggerType.Danmaku }));
|
||||
}
|
||||
|
@ -133,31 +134,31 @@ namespace BililiveRecorder.Core
|
|||
|
||||
public bool Start()
|
||||
{
|
||||
if (disposedValue)
|
||||
if (this.disposedValue)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(StreamMonitor));
|
||||
}
|
||||
|
||||
IsMonitoring = true;
|
||||
httpTimer.Start();
|
||||
Check(TriggerType.HttpApi);
|
||||
this.IsMonitoring = true;
|
||||
this.httpTimer.Start();
|
||||
this.Check(TriggerType.HttpApi);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (disposedValue)
|
||||
if (this.disposedValue)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(StreamMonitor));
|
||||
}
|
||||
|
||||
IsMonitoring = false;
|
||||
httpTimer.Stop();
|
||||
this.IsMonitoring = false;
|
||||
this.httpTimer.Stop();
|
||||
}
|
||||
|
||||
public void Check(TriggerType type, int millisecondsDelay = 0)
|
||||
{
|
||||
if (disposedValue)
|
||||
if (this.disposedValue)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(StreamMonitor));
|
||||
}
|
||||
|
@ -170,7 +171,7 @@ namespace BililiveRecorder.Core
|
|||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(millisecondsDelay).ConfigureAwait(false);
|
||||
if ((await FetchRoomInfoAsync().ConfigureAwait(false)).IsStreaming)
|
||||
if ((await this.FetchRoomInfoAsync().ConfigureAwait(false)).IsStreaming)
|
||||
{
|
||||
StreamStarted?.Invoke(this, new StreamStartedArgs() { type = type });
|
||||
}
|
||||
|
@ -179,7 +180,7 @@ namespace BililiveRecorder.Core
|
|||
|
||||
public async Task<RoomInfo> FetchRoomInfoAsync()
|
||||
{
|
||||
RoomInfo roomInfo = await bililiveAPI.GetRoomInfoAsync(Roomid).ConfigureAwait(false);
|
||||
RoomInfo roomInfo = await this.bililiveAPI.GetRoomInfoAsync(this.RoomId).ConfigureAwait(false);
|
||||
if (roomInfo != null)
|
||||
RoomInfoUpdated?.Invoke(this, new RoomInfoUpdatedArgs { RoomInfo = roomInfo });
|
||||
return roomInfo;
|
||||
|
@ -191,46 +192,46 @@ namespace BililiveRecorder.Core
|
|||
private async Task ConnectWithRetryAsync()
|
||||
{
|
||||
bool connect_result = false;
|
||||
while (!IsDanmakuConnected && !dmTokenSource.Token.IsCancellationRequested)
|
||||
while (!this.IsDanmakuConnected && !this.dmTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
logger.Log(Roomid, LogLevel.Info, "连接弹幕服务器...");
|
||||
connect_result = await ConnectAsync().ConfigureAwait(false);
|
||||
logger.Log(this.RoomId, LogLevel.Info, "连接弹幕服务器...");
|
||||
connect_result = await this.ConnectAsync().ConfigureAwait(false);
|
||||
if (!connect_result)
|
||||
await Task.Delay((int)Math.Max(config.TimingDanmakuRetry, 0));
|
||||
await Task.Delay((int)Math.Max(this.roomConfig.TimingDanmakuRetry, 0));
|
||||
}
|
||||
|
||||
if (connect_result)
|
||||
{
|
||||
logger.Log(Roomid, LogLevel.Info, "弹幕服务器连接成功");
|
||||
logger.Log(this.RoomId, LogLevel.Info, "弹幕服务器连接成功");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ConnectAsync()
|
||||
{
|
||||
if (IsDanmakuConnected) { return true; }
|
||||
if (this.IsDanmakuConnected) { return true; }
|
||||
|
||||
try
|
||||
{
|
||||
var (token, host, port) = await bililiveAPI.GetDanmuConf(Roomid);
|
||||
var (token, host, port) = await this.bililiveAPI.GetDanmuConf(this.RoomId);
|
||||
|
||||
logger.Log(Roomid, LogLevel.Debug, $"连接弹幕服务器 {host}:{port} {(string.IsNullOrWhiteSpace(token) ? "无" : "有")} token");
|
||||
logger.Log(this.RoomId, LogLevel.Debug, $"连接弹幕服务器 {host}:{port} {(string.IsNullOrWhiteSpace(token) ? "无" : "有")} token");
|
||||
|
||||
dmClient = funcTcpClient();
|
||||
await dmClient.ConnectAsync(host, port).ConfigureAwait(false);
|
||||
dmNetStream = dmClient.GetStream();
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsDanmakuConnected)));
|
||||
this.dmClient = this.funcTcpClient();
|
||||
await this.dmClient.ConnectAsync(host, port).ConfigureAwait(false);
|
||||
this.dmNetStream = this.dmClient.GetStream();
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsDanmakuConnected)));
|
||||
|
||||
dmReceiveMessageLoopThread = new Thread(ReceiveMessageLoop)
|
||||
this.dmReceiveMessageLoopThread = new Thread(this.ReceiveMessageLoop)
|
||||
{
|
||||
Name = "ReceiveMessageLoop " + Roomid,
|
||||
Name = "ReceiveMessageLoop " + this.RoomId,
|
||||
IsBackground = true
|
||||
};
|
||||
dmReceiveMessageLoopThread.Start();
|
||||
this.dmReceiveMessageLoopThread.Start();
|
||||
|
||||
var hello = JsonConvert.SerializeObject(new
|
||||
{
|
||||
uid = 0,
|
||||
roomid = Roomid,
|
||||
roomid = this.RoomId,
|
||||
protover = 2,
|
||||
platform = "web",
|
||||
clientver = "1.11.0",
|
||||
|
@ -238,30 +239,30 @@ namespace BililiveRecorder.Core
|
|||
key = token,
|
||||
|
||||
}, Formatting.None);
|
||||
SendSocketData(7, hello);
|
||||
SendSocketData(2);
|
||||
this.SendSocketData(7, hello);
|
||||
this.SendSocketData(2);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
dmError = ex;
|
||||
logger.Log(Roomid, LogLevel.Warn, "连接弹幕服务器错误", ex);
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsDanmakuConnected)));
|
||||
this.dmError = ex;
|
||||
logger.Log(this.RoomId, LogLevel.Warn, "连接弹幕服务器错误", ex);
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsDanmakuConnected)));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReceiveMessageLoop()
|
||||
{
|
||||
logger.Log(Roomid, LogLevel.Trace, "ReceiveMessageLoop Started");
|
||||
logger.Log(this.RoomId, LogLevel.Trace, "ReceiveMessageLoop Started");
|
||||
try
|
||||
{
|
||||
var stableBuffer = new byte[16];
|
||||
var buffer = new byte[4096];
|
||||
while (IsDanmakuConnected)
|
||||
while (this.IsDanmakuConnected)
|
||||
{
|
||||
dmNetStream.ReadB(stableBuffer, 0, 16);
|
||||
this.dmNetStream.ReadB(stableBuffer, 0, 16);
|
||||
Parse2Protocol(stableBuffer, out DanmakuProtocol protocol);
|
||||
|
||||
if (protocol.PacketLength < 16)
|
||||
|
@ -280,7 +281,7 @@ namespace BililiveRecorder.Core
|
|||
buffer = new byte[payloadlength];
|
||||
}
|
||||
|
||||
dmNetStream.ReadB(buffer, 0, payloadlength);
|
||||
this.dmNetStream.ReadB(buffer, 0, payloadlength);
|
||||
|
||||
if (protocol.Version == 2 && protocol.Action == 5) // 处理deflate消息
|
||||
{
|
||||
|
@ -324,7 +325,7 @@ namespace BililiveRecorder.Core
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Log(Roomid, LogLevel.Warn, "", ex);
|
||||
logger.Log(this.RoomId, LogLevel.Warn, "", ex);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
@ -335,25 +336,25 @@ namespace BililiveRecorder.Core
|
|||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
dmError = ex;
|
||||
this.dmError = ex;
|
||||
// logger.Error(ex);
|
||||
|
||||
logger.Log(Roomid, LogLevel.Debug, "Disconnected");
|
||||
dmClient?.Close();
|
||||
dmNetStream = null;
|
||||
if (!(dmTokenSource?.IsCancellationRequested ?? true))
|
||||
logger.Log(this.RoomId, LogLevel.Debug, "Disconnected");
|
||||
this.dmClient?.Close();
|
||||
this.dmNetStream = null;
|
||||
if (!(this.dmTokenSource?.IsCancellationRequested ?? true))
|
||||
{
|
||||
logger.Log(Roomid, LogLevel.Warn, "弹幕连接被断开,将尝试重连", ex);
|
||||
logger.Log(this.RoomId, LogLevel.Warn, "弹幕连接被断开,将尝试重连", ex);
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay((int)Math.Max(config.TimingDanmakuRetry, 0));
|
||||
await ConnectWithRetryAsync();
|
||||
await Task.Delay((int)Math.Max(this.roomConfig.TimingDanmakuRetry, 0));
|
||||
await this.ConnectWithRetryAsync();
|
||||
});
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsDanmakuConnected)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsDanmakuConnected)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -382,8 +383,8 @@ namespace BililiveRecorder.Core
|
|||
{
|
||||
ms.Write(playload, 0, playload.Length);
|
||||
}
|
||||
dmNetStream.Write(buffer, 0, buffer.Length);
|
||||
dmNetStream.Flush();
|
||||
this.dmNetStream.Write(buffer, 0, buffer.Length);
|
||||
this.dmNetStream.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -423,11 +424,11 @@ namespace BililiveRecorder.Core
|
|||
/// </summary>
|
||||
public void ChangeEndian()
|
||||
{
|
||||
PacketLength = IPAddress.HostToNetworkOrder(PacketLength);
|
||||
HeaderLength = IPAddress.HostToNetworkOrder(HeaderLength);
|
||||
Version = IPAddress.HostToNetworkOrder(Version);
|
||||
Action = IPAddress.HostToNetworkOrder(Action);
|
||||
Parameter = IPAddress.HostToNetworkOrder(Parameter);
|
||||
this.PacketLength = IPAddress.HostToNetworkOrder(this.PacketLength);
|
||||
this.HeaderLength = IPAddress.HostToNetworkOrder(this.HeaderLength);
|
||||
this.Version = IPAddress.HostToNetworkOrder(this.Version);
|
||||
this.Action = IPAddress.HostToNetworkOrder(this.Action);
|
||||
this.Parameter = IPAddress.HostToNetworkOrder(this.Parameter);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -439,25 +440,25 @@ namespace BililiveRecorder.Core
|
|||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
if (!this.disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
dmTokenSource?.Cancel();
|
||||
dmTokenSource?.Dispose();
|
||||
httpTimer?.Dispose();
|
||||
dmClient?.Close();
|
||||
this.dmTokenSource?.Cancel();
|
||||
this.dmTokenSource?.Dispose();
|
||||
this.httpTimer?.Dispose();
|
||||
this.dmClient?.Close();
|
||||
}
|
||||
|
||||
dmNetStream = null;
|
||||
disposedValue = true;
|
||||
this.dmNetStream = null;
|
||||
this.disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。
|
||||
Dispose(true);
|
||||
this.Dispose(true);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace BililiveRecorder.Core
|
||||
namespace BililiveRecorder.Core
|
||||
{
|
||||
public enum TriggerType
|
||||
{
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
using NLog;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using NLog;
|
||||
|
||||
namespace BililiveRecorder.FlvProcessor
|
||||
{
|
||||
|
@ -25,22 +25,22 @@ namespace BililiveRecorder.FlvProcessor
|
|||
public IFlvClipProcessor Initialize(string path, IFlvMetadata metadata, List<IFlvTag> head, List<IFlvTag> data, uint seconds)
|
||||
{
|
||||
this.path = path;
|
||||
Header = metadata; // TODO: Copy a copy, do not share
|
||||
HTags = head;
|
||||
Tags = data;
|
||||
target = Tags[Tags.Count - 1].TimeStamp + (int)(seconds * FlvStreamProcessor.SEC_TO_MS);
|
||||
this.Header = metadata; // TODO: Copy a copy, do not share
|
||||
this.HTags = head;
|
||||
this.Tags = data;
|
||||
this.target = this.Tags[this.Tags.Count - 1].TimeStamp + (int)(seconds * FlvStreamProcessor.SEC_TO_MS);
|
||||
logger.Debug("Clip 创建 Tags.Count={0} Tags[0].TimeStamp={1} Tags[Tags.Count-1].TimeStamp={2} Tags里秒数={3}",
|
||||
Tags.Count, Tags[0].TimeStamp, Tags[Tags.Count - 1].TimeStamp, (Tags[Tags.Count - 1].TimeStamp - Tags[0].TimeStamp) / 1000d);
|
||||
this.Tags.Count, this.Tags[0].TimeStamp, this.Tags[this.Tags.Count - 1].TimeStamp, (this.Tags[this.Tags.Count - 1].TimeStamp - this.Tags[0].TimeStamp) / 1000d);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void AddTag(IFlvTag tag)
|
||||
{
|
||||
Tags.Add(tag);
|
||||
if (tag.TimeStamp >= target)
|
||||
this.Tags.Add(tag);
|
||||
if (tag.TimeStamp >= this.target)
|
||||
{
|
||||
FinallizeFile();
|
||||
this.FinallizeFile();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,40 +48,40 @@ namespace BililiveRecorder.FlvProcessor
|
|||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(Path.GetDirectoryName(path)))
|
||||
if (!Directory.Exists(Path.GetDirectoryName(this.path)))
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(this.path));
|
||||
}
|
||||
using (var fs = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite))
|
||||
using (var fs = new FileStream(this.path, FileMode.CreateNew, FileAccess.ReadWrite))
|
||||
{
|
||||
fs.Write(FlvStreamProcessor.FLV_HEADER_BYTES, 0, FlvStreamProcessor.FLV_HEADER_BYTES.Length);
|
||||
fs.Write(new byte[] { 0, 0, 0, 0, }, 0, 4);
|
||||
|
||||
double clipDuration = (Tags[Tags.Count - 1].TimeStamp - Tags[0].TimeStamp) / 1000d;
|
||||
Header["duration"] = clipDuration;
|
||||
Header["lasttimestamp"] = (double)(Tags[Tags.Count - 1].TimeStamp - Tags[0].TimeStamp);
|
||||
double clipDuration = (this.Tags[this.Tags.Count - 1].TimeStamp - this.Tags[0].TimeStamp) / 1000d;
|
||||
this.Header["duration"] = clipDuration;
|
||||
this.Header["lasttimestamp"] = (double)(this.Tags[this.Tags.Count - 1].TimeStamp - this.Tags[0].TimeStamp);
|
||||
|
||||
var t = funcFlvTag();
|
||||
var t = this.funcFlvTag();
|
||||
t.TagType = TagType.DATA;
|
||||
|
||||
if (Header.ContainsKey("BililiveRecorder"))
|
||||
if (this.Header.ContainsKey("BililiveRecorder"))
|
||||
{
|
||||
// TODO: 更好的写法
|
||||
(Header["BililiveRecorder"] as Dictionary<string, object>)["starttime"] = DateTime.UtcNow - TimeSpan.FromSeconds(clipDuration);
|
||||
(this.Header["BililiveRecorder"] as Dictionary<string, object>)["starttime"] = DateTime.UtcNow - TimeSpan.FromSeconds(clipDuration);
|
||||
}
|
||||
t.Data = Header.ToBytes();
|
||||
t.Data = this.Header.ToBytes();
|
||||
t.WriteTo(fs);
|
||||
|
||||
int offset = Tags[0].TimeStamp;
|
||||
int offset = this.Tags[0].TimeStamp;
|
||||
|
||||
HTags.ForEach(tag => tag.WriteTo(fs));
|
||||
Tags.ForEach(tag => tag.WriteTo(fs, offset));
|
||||
this.HTags.ForEach(tag => tag.WriteTo(fs));
|
||||
this.Tags.ForEach(tag => tag.WriteTo(fs, offset));
|
||||
|
||||
logger.Info("剪辑已保存:{0}", Path.GetFileName(path));
|
||||
logger.Info("剪辑已保存:{0}", Path.GetFileName(this.path));
|
||||
|
||||
fs.Close();
|
||||
}
|
||||
Tags.Clear();
|
||||
this.Tags.Clear();
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
|
|
|
@ -11,20 +11,20 @@ namespace BililiveRecorder.FlvProcessor
|
|||
{
|
||||
private IDictionary<string, object> Meta { get; set; } = new Dictionary<string, object>();
|
||||
|
||||
public ICollection<string> Keys => Meta.Keys;
|
||||
public ICollection<string> Keys => this.Meta.Keys;
|
||||
|
||||
public ICollection<object> Values => Meta.Values;
|
||||
public ICollection<object> Values => this.Meta.Values;
|
||||
|
||||
public int Count => Meta.Count;
|
||||
public int Count => this.Meta.Count;
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public object this[string key] { get => Meta[key]; set => Meta[key] = value; }
|
||||
public object this[string key] { get => this.Meta[key]; set => this.Meta[key] = value; }
|
||||
|
||||
public FlvMetadata()
|
||||
{
|
||||
Meta["duration"] = 0.0;
|
||||
Meta["lasttimestamp"] = 0.0;
|
||||
this.Meta["duration"] = 0.0;
|
||||
this.Meta["lasttimestamp"] = 0.0;
|
||||
}
|
||||
|
||||
public FlvMetadata(byte[] data)
|
||||
|
@ -37,24 +37,24 @@ namespace BililiveRecorder.FlvProcessor
|
|||
{
|
||||
throw new Exception("Isn't onMetadata");
|
||||
}
|
||||
Meta = DecodeScriptDataValue(data, ref readHead) as Dictionary<string, object>;
|
||||
this.Meta = DecodeScriptDataValue(data, ref readHead) as Dictionary<string, object>;
|
||||
|
||||
if (!Meta.ContainsKey("duration"))
|
||||
if (!this.Meta.ContainsKey("duration"))
|
||||
{
|
||||
Meta["duration"] = 0d;
|
||||
this.Meta["duration"] = 0d;
|
||||
}
|
||||
|
||||
if (!Meta.ContainsKey("lasttimestamp"))
|
||||
if (!this.Meta.ContainsKey("lasttimestamp"))
|
||||
{
|
||||
Meta["lasttimestamp"] = 0d;
|
||||
this.Meta["lasttimestamp"] = 0d;
|
||||
}
|
||||
|
||||
Meta.Remove("");
|
||||
foreach (var item in Meta.ToArray())
|
||||
this.Meta.Remove("");
|
||||
foreach (var item in this.Meta.ToArray())
|
||||
{
|
||||
if (item.Value is string text)
|
||||
{
|
||||
Meta[item.Key] = text.Replace("\0", "");
|
||||
this.Meta[item.Key] = text.Replace("\0", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ namespace BililiveRecorder.FlvProcessor
|
|||
using (var ms = new MemoryStream())
|
||||
{
|
||||
EncodeScriptDataValue(ms, "onMetaData");
|
||||
EncodeScriptDataValue(ms, Meta);
|
||||
EncodeScriptDataValue(ms, this.Meta);
|
||||
return ms.ToArray();
|
||||
}
|
||||
}
|
||||
|
@ -278,57 +278,57 @@ namespace BililiveRecorder.FlvProcessor
|
|||
|
||||
public void Add(string key, object value)
|
||||
{
|
||||
Meta.Add(key, value);
|
||||
this.Meta.Add(key, value);
|
||||
}
|
||||
|
||||
public bool ContainsKey(string key)
|
||||
{
|
||||
return Meta.ContainsKey(key);
|
||||
return this.Meta.ContainsKey(key);
|
||||
}
|
||||
|
||||
public bool Remove(string key)
|
||||
{
|
||||
return Meta.Remove(key);
|
||||
return this.Meta.Remove(key);
|
||||
}
|
||||
|
||||
public bool TryGetValue(string key, out object value)
|
||||
{
|
||||
return Meta.TryGetValue(key, out value);
|
||||
return this.Meta.TryGetValue(key, out value);
|
||||
}
|
||||
|
||||
public void Add(KeyValuePair<string, object> item)
|
||||
{
|
||||
Meta.Add(item);
|
||||
this.Meta.Add(item);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Meta.Clear();
|
||||
this.Meta.Clear();
|
||||
}
|
||||
|
||||
public bool Contains(KeyValuePair<string, object> item)
|
||||
{
|
||||
return Meta.Contains(item);
|
||||
return this.Meta.Contains(item);
|
||||
}
|
||||
|
||||
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
|
||||
{
|
||||
Meta.CopyTo(array, arrayIndex);
|
||||
this.Meta.CopyTo(array, arrayIndex);
|
||||
}
|
||||
|
||||
public bool Remove(KeyValuePair<string, object> item)
|
||||
{
|
||||
return Meta.Remove(item);
|
||||
return this.Meta.Remove(item);
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
|
||||
{
|
||||
return Meta.GetEnumerator();
|
||||
return this.Meta.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return Meta.GetEnumerator();
|
||||
return this.Meta.GetEnumerator();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ namespace BililiveRecorder.FlvProcessor
|
|||
private int _tagAudioCount = 0;
|
||||
|
||||
public int TotalMaxTimestamp { get; private set; } = 0;
|
||||
public int CurrentMaxTimestamp { get => TotalMaxTimestamp - _writeTimeStamp; }
|
||||
public int CurrentMaxTimestamp { get => this.TotalMaxTimestamp - this._writeTimeStamp; }
|
||||
|
||||
private readonly Func<IFlvClipProcessor> funcFlvClipProcessor;
|
||||
private readonly Func<byte[], IFlvMetadata> funcFlvMetadata;
|
||||
|
@ -83,56 +83,56 @@ namespace BililiveRecorder.FlvProcessor
|
|||
|
||||
public IFlvStreamProcessor Initialize(Func<(string fullPath, string relativePath)> getStreamFileName, Func<string> getClipFileName, EnabledFeature enabledFeature, AutoCuttingMode autoCuttingMode)
|
||||
{
|
||||
GetStreamFileName = getStreamFileName;
|
||||
GetClipFileName = getClipFileName;
|
||||
EnabledFeature = enabledFeature;
|
||||
CuttingMode = autoCuttingMode;
|
||||
this.GetStreamFileName = getStreamFileName;
|
||||
this.GetClipFileName = getClipFileName;
|
||||
this.EnabledFeature = enabledFeature;
|
||||
this.CuttingMode = autoCuttingMode;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private void OpenNewRecordFile()
|
||||
{
|
||||
var (fullPath, relativePath) = GetStreamFileName();
|
||||
var (fullPath, relativePath) = this.GetStreamFileName();
|
||||
logger.Debug("打开新录制文件: " + fullPath);
|
||||
try { Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); } catch (Exception) { }
|
||||
_targetFile = new FileStream(fullPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read | FileShare.Delete);
|
||||
this._targetFile = new FileStream(fullPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read | FileShare.Delete);
|
||||
|
||||
if (_headerParsed)
|
||||
if (this._headerParsed)
|
||||
{
|
||||
_targetFile.Write(FlvStreamProcessor.FLV_HEADER_BYTES, 0, FlvStreamProcessor.FLV_HEADER_BYTES.Length);
|
||||
_targetFile.Write(new byte[] { 0, 0, 0, 0, }, 0, 4);
|
||||
this._targetFile.Write(FlvStreamProcessor.FLV_HEADER_BYTES, 0, FlvStreamProcessor.FLV_HEADER_BYTES.Length);
|
||||
this._targetFile.Write(new byte[] { 0, 0, 0, 0, }, 0, 4);
|
||||
|
||||
var script_tag = funcFlvTag();
|
||||
var script_tag = this.funcFlvTag();
|
||||
script_tag.TagType = TagType.DATA;
|
||||
if (Metadata.ContainsKey("BililiveRecorder"))
|
||||
if (this.Metadata.ContainsKey("BililiveRecorder"))
|
||||
{
|
||||
// TODO: 更好的写法
|
||||
(Metadata["BililiveRecorder"] as Dictionary<string, object>)["starttime"] = DateTime.UtcNow;
|
||||
(this.Metadata["BililiveRecorder"] as Dictionary<string, object>)["starttime"] = DateTime.UtcNow;
|
||||
}
|
||||
script_tag.Data = Metadata.ToBytes();
|
||||
script_tag.WriteTo(_targetFile);
|
||||
script_tag.Data = this.Metadata.ToBytes();
|
||||
script_tag.WriteTo(this._targetFile);
|
||||
|
||||
_headerTags.ForEach(tag => tag.WriteTo(_targetFile));
|
||||
this._headerTags.ForEach(tag => tag.WriteTo(this._targetFile));
|
||||
}
|
||||
}
|
||||
|
||||
public void AddBytes(byte[] data)
|
||||
{
|
||||
lock (_writelock)
|
||||
lock (this._writelock)
|
||||
{
|
||||
if (_finalized) { return; /*throw new InvalidOperationException("Processor Already Closed");*/ }
|
||||
if (_leftover != null)
|
||||
if (this._finalized) { return; /*throw new InvalidOperationException("Processor Already Closed");*/ }
|
||||
if (this._leftover != null)
|
||||
{
|
||||
byte[] c = new byte[_leftover.Length + data.Length];
|
||||
_leftover.CopyTo(c, 0);
|
||||
data.CopyTo(c, _leftover.Length);
|
||||
_leftover = null;
|
||||
ParseBytes(c);
|
||||
byte[] c = new byte[this._leftover.Length + data.Length];
|
||||
this._leftover.CopyTo(c, 0);
|
||||
data.CopyTo(c, this._leftover.Length);
|
||||
this._leftover = null;
|
||||
this.ParseBytes(c);
|
||||
}
|
||||
else
|
||||
{
|
||||
ParseBytes(data);
|
||||
this.ParseBytes(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -140,14 +140,14 @@ namespace BililiveRecorder.FlvProcessor
|
|||
private void ParseBytes(byte[] data)
|
||||
{
|
||||
int position = 0;
|
||||
if (!_headerParsed) { ReadFlvHeader(); }
|
||||
if (!this._headerParsed) { ReadFlvHeader(); }
|
||||
while (position < data.Length)
|
||||
{
|
||||
if (_currentTag == null)
|
||||
if (this._currentTag == null)
|
||||
{
|
||||
if (!ParseTagHead())
|
||||
{
|
||||
_leftover = data.Skip(position).ToArray();
|
||||
this._leftover = data.Skip(position).ToArray();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -159,7 +159,7 @@ namespace BililiveRecorder.FlvProcessor
|
|||
if (data.Length - position < 15) { return false; }
|
||||
|
||||
byte[] b = new byte[4];
|
||||
IFlvTag tag = funcFlvTag();
|
||||
IFlvTag tag = this.funcFlvTag();
|
||||
|
||||
// Previous Tag Size UI24
|
||||
position += 4;
|
||||
|
@ -186,21 +186,21 @@ namespace BililiveRecorder.FlvProcessor
|
|||
tag.StreamId[1] = data[position++];
|
||||
tag.StreamId[2] = data[position++];
|
||||
|
||||
_currentTag = tag;
|
||||
this._currentTag = tag;
|
||||
|
||||
return true;
|
||||
}
|
||||
void FillTagData()
|
||||
{
|
||||
int toRead = Math.Min(data.Length - position, _currentTag.TagSize - (int)_data.Position);
|
||||
_data.Write(buffer: data, offset: position, count: toRead);
|
||||
int toRead = Math.Min(data.Length - position, this._currentTag.TagSize - (int)this._data.Position);
|
||||
this._data.Write(buffer: data, offset: position, count: toRead);
|
||||
position += toRead;
|
||||
if ((int)_data.Position == _currentTag.TagSize)
|
||||
if ((int)this._data.Position == this._currentTag.TagSize)
|
||||
{
|
||||
_currentTag.Data = _data.ToArray();
|
||||
_data.SetLength(0); // reset data buffer
|
||||
TagCreated(_currentTag);
|
||||
_currentTag = null;
|
||||
this._currentTag.Data = this._data.ToArray();
|
||||
this._data.SetLength(0); // reset data buffer
|
||||
this.TagCreated(this._currentTag);
|
||||
this._currentTag = null;
|
||||
}
|
||||
}
|
||||
void ReadFlvHeader()
|
||||
|
@ -223,108 +223,108 @@ namespace BililiveRecorder.FlvProcessor
|
|||
throw new NotSupportedException("Not FLV Stream or Not Supported"); // TODO: custom Exception.
|
||||
}
|
||||
|
||||
_headerParsed = true;
|
||||
this._headerParsed = true;
|
||||
position += FLV_HEADER_BYTES.Length;
|
||||
}
|
||||
}
|
||||
|
||||
private void TagCreated(IFlvTag tag)
|
||||
{
|
||||
if (Metadata == null)
|
||||
if (this.Metadata == null)
|
||||
{
|
||||
ParseMetadata();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_hasOffset) { ParseTimestampOffset(); }
|
||||
if (!this._hasOffset) { ParseTimestampOffset(); }
|
||||
SetTimestamp();
|
||||
|
||||
if (EnabledFeature.IsRecordEnabled()) { ProcessRecordLogic(); }
|
||||
if (EnabledFeature.IsClipEnabled()) { ProcessClipLogic(); }
|
||||
if (this.EnabledFeature.IsRecordEnabled()) { ProcessRecordLogic(); }
|
||||
if (this.EnabledFeature.IsClipEnabled()) { ProcessClipLogic(); }
|
||||
|
||||
TagProcessed?.Invoke(this, new TagProcessedArgs() { Tag = tag });
|
||||
}
|
||||
return;
|
||||
void SetTimestamp()
|
||||
{
|
||||
if (_hasOffset)
|
||||
if (this._hasOffset)
|
||||
{
|
||||
tag.SetTimeStamp(tag.TimeStamp - _baseTimeStamp);
|
||||
TotalMaxTimestamp = Math.Max(TotalMaxTimestamp, tag.TimeStamp);
|
||||
tag.SetTimeStamp(tag.TimeStamp - this._baseTimeStamp);
|
||||
this.TotalMaxTimestamp = Math.Max(this.TotalMaxTimestamp, tag.TimeStamp);
|
||||
}
|
||||
else
|
||||
{ tag.SetTimeStamp(0); }
|
||||
}
|
||||
void ProcessRecordLogic()
|
||||
{
|
||||
if (CuttingMode != AutoCuttingMode.Disabled && tag.IsVideoKeyframe)
|
||||
if (this.CuttingMode != AutoCuttingMode.Disabled && tag.IsVideoKeyframe)
|
||||
{
|
||||
bool byTime = (CuttingMode == AutoCuttingMode.ByTime) && (CurrentMaxTimestamp / 1000 >= CuttingNumber * 60);
|
||||
bool bySize = (CuttingMode == AutoCuttingMode.BySize) && ((_targetFile.Length / 1024 / 1024) >= CuttingNumber);
|
||||
bool byTime = (this.CuttingMode == AutoCuttingMode.ByTime) && (this.CurrentMaxTimestamp / 1000 >= this.CuttingNumber * 60);
|
||||
bool bySize = (this.CuttingMode == AutoCuttingMode.BySize) && ((this._targetFile.Length / 1024 / 1024) >= this.CuttingNumber);
|
||||
if (byTime || bySize)
|
||||
{
|
||||
FinallizeCurrentFile();
|
||||
OpenNewRecordFile();
|
||||
_writeTimeStamp = TotalMaxTimestamp;
|
||||
this.FinallizeCurrentFile();
|
||||
this.OpenNewRecordFile();
|
||||
this._writeTimeStamp = this.TotalMaxTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(_targetFile?.CanWrite ?? false))
|
||||
if (!(this._targetFile?.CanWrite ?? false))
|
||||
{
|
||||
OpenNewRecordFile();
|
||||
this.OpenNewRecordFile();
|
||||
}
|
||||
|
||||
tag.WriteTo(_targetFile, _writeTimeStamp);
|
||||
tag.WriteTo(this._targetFile, this._writeTimeStamp);
|
||||
}
|
||||
void ProcessClipLogic()
|
||||
{
|
||||
_tags.Add(tag);
|
||||
this._tags.Add(tag);
|
||||
|
||||
// 移除过旧的数据
|
||||
if (TotalMaxTimestamp - _lasttimeRemovedTimestamp > 800)
|
||||
if (this.TotalMaxTimestamp - this._lasttimeRemovedTimestamp > 800)
|
||||
{
|
||||
_lasttimeRemovedTimestamp = TotalMaxTimestamp;
|
||||
int max_remove_index = _tags.FindLastIndex(x => x.IsVideoKeyframe && ((TotalMaxTimestamp - x.TimeStamp) > (ClipLengthPast * SEC_TO_MS)));
|
||||
this._lasttimeRemovedTimestamp = this.TotalMaxTimestamp;
|
||||
int max_remove_index = this._tags.FindLastIndex(x => x.IsVideoKeyframe && ((this.TotalMaxTimestamp - x.TimeStamp) > (this.ClipLengthPast * SEC_TO_MS)));
|
||||
if (max_remove_index > 0)
|
||||
{
|
||||
_tags.RemoveRange(0, max_remove_index);
|
||||
this._tags.RemoveRange(0, max_remove_index);
|
||||
}
|
||||
// Tags.RemoveRange(0, max_remove_index + 1 - 1);
|
||||
// 给将来的备注:这里是故意 + 1 - 1 的,因为要保留选中的那个关键帧, + 1 就把关键帧删除了
|
||||
}
|
||||
|
||||
Clips.ToList().ForEach(fcp => fcp.AddTag(tag));
|
||||
this.Clips.ToList().ForEach(fcp => fcp.AddTag(tag));
|
||||
}
|
||||
void ParseTimestampOffset()
|
||||
{
|
||||
if (tag.TagType == TagType.VIDEO)
|
||||
{
|
||||
_tagVideoCount++;
|
||||
if (_tagVideoCount < 2)
|
||||
this._tagVideoCount++;
|
||||
if (this._tagVideoCount < 2)
|
||||
{
|
||||
logger.Debug("第一个 Video Tag 时间戳 {0} ms", tag.TimeStamp);
|
||||
_headerTags.Add(tag);
|
||||
this._headerTags.Add(tag);
|
||||
}
|
||||
else
|
||||
{
|
||||
_baseTimeStamp = tag.TimeStamp;
|
||||
_hasOffset = true;
|
||||
logger.Debug("重设时间戳 {0} 毫秒", _baseTimeStamp);
|
||||
this._baseTimeStamp = tag.TimeStamp;
|
||||
this._hasOffset = true;
|
||||
logger.Debug("重设时间戳 {0} 毫秒", this._baseTimeStamp);
|
||||
}
|
||||
}
|
||||
else if (tag.TagType == TagType.AUDIO)
|
||||
{
|
||||
_tagAudioCount++;
|
||||
if (_tagAudioCount < 2)
|
||||
this._tagAudioCount++;
|
||||
if (this._tagAudioCount < 2)
|
||||
{
|
||||
logger.Debug("第一个 Audio Tag 时间戳 {0} ms", tag.TimeStamp);
|
||||
_headerTags.Add(tag);
|
||||
this._headerTags.Add(tag);
|
||||
}
|
||||
else
|
||||
{
|
||||
_baseTimeStamp = tag.TimeStamp;
|
||||
_hasOffset = true;
|
||||
logger.Debug("重设时间戳 {0} 毫秒", _baseTimeStamp);
|
||||
this._baseTimeStamp = tag.TimeStamp;
|
||||
this._hasOffset = true;
|
||||
logger.Debug("重设时间戳 {0} 毫秒", this._baseTimeStamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -332,15 +332,15 @@ namespace BililiveRecorder.FlvProcessor
|
|||
{
|
||||
if (tag.TagType == TagType.DATA)
|
||||
{
|
||||
_targetFile?.Write(FLV_HEADER_BYTES, 0, FLV_HEADER_BYTES.Length);
|
||||
_targetFile?.Write(new byte[] { 0, 0, 0, 0, }, 0, 4);
|
||||
this._targetFile?.Write(FLV_HEADER_BYTES, 0, FLV_HEADER_BYTES.Length);
|
||||
this._targetFile?.Write(new byte[] { 0, 0, 0, 0, }, 0, 4);
|
||||
|
||||
Metadata = funcFlvMetadata(tag.Data);
|
||||
this.Metadata = this.funcFlvMetadata(tag.Data);
|
||||
|
||||
OnMetaData?.Invoke(this, new FlvMetadataArgs() { Metadata = Metadata });
|
||||
|
||||
tag.Data = Metadata.ToBytes();
|
||||
tag.WriteTo(_targetFile);
|
||||
tag.Data = this.Metadata.ToBytes();
|
||||
tag.WriteTo(this._targetFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -351,19 +351,19 @@ namespace BililiveRecorder.FlvProcessor
|
|||
|
||||
public IFlvClipProcessor Clip()
|
||||
{
|
||||
if (!EnabledFeature.IsClipEnabled()) { return null; }
|
||||
lock (_writelock)
|
||||
if (!this.EnabledFeature.IsClipEnabled()) { return null; }
|
||||
lock (this._writelock)
|
||||
{
|
||||
if (_finalized)
|
||||
if (this._finalized)
|
||||
{
|
||||
return null;
|
||||
// throw new InvalidOperationException("Processor Already Closed");
|
||||
}
|
||||
|
||||
logger.Info("剪辑处理中,将会保存过去 {0} 秒和将来 {1} 秒的直播流", (_tags[_tags.Count - 1].TimeStamp - _tags[0].TimeStamp) / 1000d, ClipLengthFuture);
|
||||
IFlvClipProcessor clip = funcFlvClipProcessor().Initialize(GetClipFileName(), Metadata, _headerTags, new List<IFlvTag>(_tags.ToArray()), ClipLengthFuture);
|
||||
clip.ClipFinalized += (sender, e) => { Clips.Remove(e.ClipProcessor); };
|
||||
Clips.Add(clip);
|
||||
logger.Info("剪辑处理中,将会保存过去 {0} 秒和将来 {1} 秒的直播流", (this._tags[this._tags.Count - 1].TimeStamp - this._tags[0].TimeStamp) / 1000d, this.ClipLengthFuture);
|
||||
IFlvClipProcessor clip = this.funcFlvClipProcessor().Initialize(this.GetClipFileName(), this.Metadata, this._headerTags, new List<IFlvTag>(this._tags.ToArray()), this.ClipLengthFuture);
|
||||
clip.ClipFinalized += (sender, e) => { this.Clips.Remove(e.ClipProcessor); };
|
||||
this.Clips.Add(clip);
|
||||
return clip;
|
||||
}
|
||||
}
|
||||
|
@ -372,16 +372,16 @@ namespace BililiveRecorder.FlvProcessor
|
|||
{
|
||||
try
|
||||
{
|
||||
var fileSize = _targetFile?.Length ?? -1;
|
||||
logger.Debug("正在关闭当前录制文件: " + _targetFile?.Name);
|
||||
Metadata["duration"] = CurrentMaxTimestamp / 1000.0;
|
||||
Metadata["lasttimestamp"] = (double)CurrentMaxTimestamp;
|
||||
byte[] metadata = Metadata.ToBytes();
|
||||
var fileSize = this._targetFile?.Length ?? -1;
|
||||
logger.Debug("正在关闭当前录制文件: " + this._targetFile?.Name);
|
||||
this.Metadata["duration"] = this.CurrentMaxTimestamp / 1000.0;
|
||||
this.Metadata["lasttimestamp"] = (double)this.CurrentMaxTimestamp;
|
||||
byte[] metadata = this.Metadata.ToBytes();
|
||||
|
||||
// 13 for FLV header & "0th" tag size
|
||||
// 11 for 1st tag header
|
||||
_targetFile?.Seek(13 + 11, SeekOrigin.Begin);
|
||||
_targetFile?.Write(metadata, 0, metadata.Length);
|
||||
this._targetFile?.Seek(13 + 11, SeekOrigin.Begin);
|
||||
this._targetFile?.Write(metadata, 0, metadata.Length);
|
||||
|
||||
if (fileSize > 0)
|
||||
FileFinalized?.Invoke(this, fileSize);
|
||||
|
@ -396,30 +396,30 @@ namespace BililiveRecorder.FlvProcessor
|
|||
}
|
||||
finally
|
||||
{
|
||||
_targetFile?.Close();
|
||||
_targetFile = null;
|
||||
this._targetFile?.Close();
|
||||
this._targetFile = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void FinallizeFile()
|
||||
{
|
||||
if (!_finalized)
|
||||
if (!this._finalized)
|
||||
{
|
||||
lock (_writelock)
|
||||
lock (this._writelock)
|
||||
{
|
||||
try
|
||||
{
|
||||
FinallizeCurrentFile();
|
||||
this.FinallizeCurrentFile();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_targetFile?.Close();
|
||||
_data.Close();
|
||||
_tags.Clear();
|
||||
this._targetFile?.Close();
|
||||
this._data.Close();
|
||||
this._tags.Clear();
|
||||
|
||||
_finalized = true;
|
||||
this._finalized = true;
|
||||
|
||||
Clips.ToList().ForEach(fcp => fcp.FinallizeFile()); // TODO: check
|
||||
this.Clips.ToList().ForEach(fcp => fcp.FinallizeFile()); // TODO: check
|
||||
StreamFinalized?.Invoke(this, new StreamFinalizedArgs() { StreamProcessor = this });
|
||||
}
|
||||
}
|
||||
|
@ -431,24 +431,24 @@ namespace BililiveRecorder.FlvProcessor
|
|||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
if (!this.disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_data.Dispose();
|
||||
_targetFile?.Dispose();
|
||||
this._data.Dispose();
|
||||
this._targetFile?.Dispose();
|
||||
OnMetaData = null;
|
||||
StreamFinalized = null;
|
||||
TagProcessed = null;
|
||||
}
|
||||
_tags.Clear();
|
||||
disposedValue = true;
|
||||
this._tags.Clear();
|
||||
this.disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
this.Dispose(true);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
|
|
@ -15,10 +15,10 @@ namespace BililiveRecorder.FlvProcessor
|
|||
public int Profile { get; private set; } = -1;
|
||||
public int Level { get; private set; } = -1;
|
||||
|
||||
public byte[] Data { get => _data; set { _data = value; ParseInfo(); } }
|
||||
public byte[] Data { get => this._data; set { this._data = value; this.ParseInfo(); } }
|
||||
private byte[] _data = null;
|
||||
|
||||
public void SetTimeStamp(int timestamp) => TimeStamp = timestamp;
|
||||
public void SetTimeStamp(int timestamp) => this.TimeStamp = timestamp;
|
||||
|
||||
private void ParseInfo()
|
||||
{
|
||||
|
@ -46,42 +46,42 @@ namespace BililiveRecorder.FlvProcessor
|
|||
* AVCLevelIndication
|
||||
* */
|
||||
|
||||
IsVideoKeyframe = false;
|
||||
Profile = -1;
|
||||
Level = -1;
|
||||
this.IsVideoKeyframe = false;
|
||||
this.Profile = -1;
|
||||
this.Level = -1;
|
||||
|
||||
if (TagType != TagType.VIDEO) { return; }
|
||||
if (Data.Length < 9) { return; }
|
||||
if (this.TagType != TagType.VIDEO) { return; }
|
||||
if (this.Data.Length < 9) { return; }
|
||||
|
||||
// Not AVC Keyframe
|
||||
if (Data[0] != 0x17) { return; }
|
||||
if (this.Data[0] != 0x17) { return; }
|
||||
|
||||
IsVideoKeyframe = true;
|
||||
this.IsVideoKeyframe = true;
|
||||
|
||||
// Isn't AVCDecoderConfigurationRecord
|
||||
if (Data[1] != 0x00) { return; }
|
||||
if (this.Data[1] != 0x00) { return; }
|
||||
// version is not 1
|
||||
if (Data[5] != 0x01) { return; }
|
||||
if (this.Data[5] != 0x01) { return; }
|
||||
|
||||
Profile = Data[6];
|
||||
Level = Data[8];
|
||||
this.Profile = this.Data[6];
|
||||
this.Level = this.Data[8];
|
||||
#if DEBUG
|
||||
Debug.WriteLine("Video Profile: " + Profile + ", Level: " + Level);
|
||||
Debug.WriteLine("Video Profile: " + this.Profile + ", Level: " + this.Level);
|
||||
#endif
|
||||
}
|
||||
|
||||
public byte[] ToBytes(bool useDataSize, int offset = 0)
|
||||
{
|
||||
var tag = new byte[11];
|
||||
tag[0] = (byte)TagType;
|
||||
var size = BitConverter.GetBytes(useDataSize ? Data.Length : TagSize).ToBE();
|
||||
tag[0] = (byte)this.TagType;
|
||||
var size = BitConverter.GetBytes(useDataSize ? this.Data.Length : this.TagSize).ToBE();
|
||||
Buffer.BlockCopy(size, 1, tag, 1, 3);
|
||||
|
||||
byte[] timing = BitConverter.GetBytes(TimeStamp - offset).ToBE();
|
||||
byte[] timing = BitConverter.GetBytes(this.TimeStamp - offset).ToBE();
|
||||
Buffer.BlockCopy(timing, 1, tag, 4, 3);
|
||||
Buffer.BlockCopy(timing, 0, tag, 7, 1);
|
||||
|
||||
Buffer.BlockCopy(StreamId, 0, tag, 8, 3);
|
||||
Buffer.BlockCopy(this.StreamId, 0, tag, 8, 3);
|
||||
|
||||
return tag;
|
||||
}
|
||||
|
@ -90,22 +90,22 @@ namespace BililiveRecorder.FlvProcessor
|
|||
{
|
||||
if (stream != null)
|
||||
{
|
||||
var vs = ToBytes(true, offset);
|
||||
var vs = this.ToBytes(true, offset);
|
||||
stream.Write(vs, 0, vs.Length);
|
||||
stream.Write(Data, 0, Data.Length);
|
||||
stream.Write(BitConverter.GetBytes(Data.Length + vs.Length).ToBE(), 0, 4);
|
||||
stream.Write(this.Data, 0, this.Data.Length);
|
||||
stream.Write(BitConverter.GetBytes(this.Data.Length + vs.Length).ToBE(), 0, 4);
|
||||
}
|
||||
}
|
||||
|
||||
private int _ParseIsVideoKeyframe()
|
||||
{
|
||||
if (TagType != TagType.VIDEO) { return 0; }
|
||||
if (Data.Length < 1) { return -1; }
|
||||
if (this.TagType != TagType.VIDEO) { return 0; }
|
||||
if (this.Data.Length < 1) { return -1; }
|
||||
|
||||
const byte mask = 0b00001111;
|
||||
const byte compare = 0b00011111;
|
||||
|
||||
return (Data[0] | mask) == compare ? 1 : 0;
|
||||
return (this.Data[0] | mask) == compare ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ namespace BililiveRecorder.WPF
|
|||
logger.Warn(ex, "检查更新时出错,如持续出错请联系开发者 rec@danmuji.org");
|
||||
}
|
||||
|
||||
_ = Task.Run(async () => { await Task.Delay(TimeSpan.FromDays(1)); await RunCheckUpdate(); });
|
||||
_ = Task.Run(async () => { await Task.Delay(TimeSpan.FromDays(1)); await this.RunCheckUpdate(); });
|
||||
}
|
||||
|
||||
private void Application_SessionEnding(object sender, SessionEndingCancelEventArgs e)
|
||||
|
|
|
@ -96,9 +96,15 @@
|
|||
<Compile Include="Controls\DeleteRoomConfirmDialog.xaml.cs">
|
||||
<DependentUpon>DeleteRoomConfirmDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\PerRoomSettingsDialog.xaml.cs">
|
||||
<DependentUpon>PerRoomSettingsDialog.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\RoomCard.xaml.cs">
|
||||
<DependentUpon>RoomCard.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\SettingWithDefault.xaml.cs">
|
||||
<DependentUpon>SettingWithDefault.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Controls\TaskbarIconControl.xaml.cs">
|
||||
<DependentUpon>TaskbarIconControl.xaml</DependentUpon>
|
||||
</Compile>
|
||||
|
@ -145,10 +151,18 @@
|
|||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Controls\PerRoomSettingsDialog.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Controls\RoomCard.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Controls\SettingWithDefault.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Include="Controls\TaskbarIconControl.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
|
|
@ -14,26 +14,26 @@ namespace BililiveRecorder.WPF.Controls
|
|||
|
||||
public AddRoomCard()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
private void AddRoom()
|
||||
{
|
||||
AddRoomRequested?.Invoke(this, InputTextBox.Text);
|
||||
InputTextBox.Text = string.Empty;
|
||||
InputTextBox.Focus();
|
||||
AddRoomRequested?.Invoke(this, this.InputTextBox.Text);
|
||||
this.InputTextBox.Text = string.Empty;
|
||||
this.InputTextBox.Focus();
|
||||
}
|
||||
|
||||
private void Button_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
AddRoom();
|
||||
this.AddRoom();
|
||||
}
|
||||
|
||||
private void InputTextBox_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
AddRoom();
|
||||
this.AddRoom();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ namespace BililiveRecorder.WPF.Controls
|
|||
{
|
||||
public AddRoomFailedDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ namespace BililiveRecorder.WPF.Controls
|
|||
{
|
||||
public CloseWindowConfirmDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ namespace BililiveRecorder.WPF.Controls
|
|||
{
|
||||
public DeleteRoomConfirmDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
87
BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml
Normal file
87
BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml
Normal file
|
@ -0,0 +1,87 @@
|
|||
<ui:ContentDialog
|
||||
x:Class="BililiveRecorder.WPF.Controls.PerRoomSettingsDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
xmlns:mock="clr-namespace:BililiveRecorder.WPF.MockData"
|
||||
xmlns:flv="clr-namespace:BililiveRecorder.FlvProcessor;assembly=BililiveRecorder.FlvProcessor"
|
||||
DefaultButton="Close"
|
||||
CloseButtonText="关闭"
|
||||
d:DataContext="{d:DesignInstance Type=mock:MockRecordedRoom,IsDesignTimeCreatable=True}"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="1000" d:DesignWidth="500">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel>
|
||||
<TextBlock VerticalAlignment="Center" Style="{DynamicResource SubtitleTextBlockStyle}" FontFamily="Microsoft Yahei" TextWrapping="NoWrap" HorizontalAlignment="Center"
|
||||
TextAlignment="Center" TextTrimming="CharacterEllipsis" Text="{Binding StreamerName,Mode=OneWay}" ToolTip="{Binding StreamerName,Mode=OneWay}"/>
|
||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Orientation="Horizontal" Grid.Row="1" Grid.ColumnSpan="2" Margin="5,0,0,0">
|
||||
<ui:PathIcon Height="10" Style="{StaticResource PathIconDataUpperCaseIdentifier}" />
|
||||
<TextBlock Text="{Binding RoomId, StringFormat=\{0\},Mode=OneWay}" Margin="4,0"/>
|
||||
<ui:PathIcon Height="10" Style="{StaticResource PathIconDataLowerCaseIdentifier}" Margin="3,0"
|
||||
Visibility="{Binding ShortRoomId,Converter={StaticResource ShortRoomIdToVisibilityConverter}}"/>
|
||||
<TextBlock Text="{Binding ShortRoomId, StringFormat=\{0\},Mode=OneWay}"
|
||||
Visibility="{Binding ShortRoomId,Converter={StaticResource ShortRoomIdToVisibilityConverter}}"/>
|
||||
</StackPanel>
|
||||
<Separator/>
|
||||
</StackPanel>
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<ui:SimpleStackPanel Orientation="Vertical" Spacing="5" DataContext="{Binding RoomConfig}">
|
||||
<GroupBox Header="弹幕录制">
|
||||
<StackPanel>
|
||||
<local:SettingWithDefault IsSettingNotUsingDefault="{Binding HasRecordDanmaku}">
|
||||
<ui:ToggleSwitch IsOn="{Binding RecordDanmaku}"
|
||||
OnContent="保存弹幕  (当前为保存)" OffContent="不保存弹幕(当前为不保存)"/>
|
||||
</local:SettingWithDefault>
|
||||
<local:SettingWithDefault IsSettingNotUsingDefault="{Binding HasRecordDanmakuSuperChat}">
|
||||
<ui:ToggleSwitch IsOn="{Binding RecordDanmakuSuperChat}"
|
||||
OnContent="同时保存 SuperChat" OffContent="不保存 SuperChat"/>
|
||||
</local:SettingWithDefault>
|
||||
<local:SettingWithDefault IsSettingNotUsingDefault="{Binding HasRecordDanmakuGuard}">
|
||||
<ui:ToggleSwitch IsOn="{Binding RecordDanmakuGuard}"
|
||||
OnContent="同时保存 舰长购买" OffContent="不保存 舰长购买"/>
|
||||
</local:SettingWithDefault>
|
||||
<local:SettingWithDefault IsSettingNotUsingDefault="{Binding HasRecordDanmakuGift}">
|
||||
<ui:ToggleSwitch IsOn="{Binding RecordDanmakuGift}"
|
||||
OnContent="同时保存 送礼信息" OffContent="不保存 送礼信息"/>
|
||||
</local:SettingWithDefault>
|
||||
<local:SettingWithDefault IsSettingNotUsingDefault="{Binding HasRecordDanmakuRaw}">
|
||||
<ui:ToggleSwitch IsOn="{Binding RecordDanmakuRaw}"
|
||||
OnContent="同时保存 弹幕原始数据" OffContent="不保存 弹幕原始数据"/>
|
||||
</local:SettingWithDefault>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="自动分段">
|
||||
<StackPanel>
|
||||
<local:SettingWithDefault IsSettingNotUsingDefault="{Binding HasCuttingMode}">
|
||||
<StackPanel>
|
||||
<RadioButton GroupName="自动分段" Content="不自动分段"
|
||||
IsChecked="{Binding Path=CuttingMode, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.Disabled}}" />
|
||||
<RadioButton GroupName="自动分段" Content="根据文件大小自动分段"
|
||||
IsChecked="{Binding Path=CuttingMode, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.BySize}}" />
|
||||
<RadioButton GroupName="自动分段" Content="根据视频时间自动分段"
|
||||
IsChecked="{Binding Path=CuttingMode, Converter={StaticResource EnumToBooleanConverter},
|
||||
ConverterParameter={x:Static flv:AutoCuttingMode.ByTime}}" />
|
||||
</StackPanel>
|
||||
</local:SettingWithDefault>
|
||||
<local:SettingWithDefault IsSettingNotUsingDefault="{Binding HasCuttingNumber}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,5,0,0">
|
||||
<TextBlock VerticalAlignment="Center" Text="每"/>
|
||||
<TextBox Margin="5,0" Width="100" Text="{Binding CuttingNumber}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
||||
<TextBlock VerticalAlignment="Center" Text="MiB/分 保存为一个文件"/>
|
||||
</StackPanel>
|
||||
</local:SettingWithDefault>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</ui:SimpleStackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</ui:ContentDialog>
|
28
BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml.cs
Normal file
28
BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace BililiveRecorder.WPF.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for PerRoomSettingsDialog.xaml
|
||||
/// </summary>
|
||||
public partial class PerRoomSettingsDialog
|
||||
{
|
||||
public PerRoomSettingsDialog()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -63,6 +63,12 @@
|
|||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator/>
|
||||
<MenuItem Header="房间设置" Click="MenuItem_ShowSettings_Click">
|
||||
<MenuItem.Icon>
|
||||
<ui:PathIcon Style="{StaticResource PathIconDataCogOutline}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator/>
|
||||
<ui:RadioMenuItem Header="自动录制" GroupName="自动录制" IsChecked="{Binding IsMonitoring,Mode=OneWay}" Click="MenuItem_StartMonitor_Click">
|
||||
<ui:RadioMenuItem.Icon>
|
||||
<ui:PathIcon Foreground="Orange" Style="{StaticResource PathIconDataCctv}"/>
|
||||
|
@ -123,7 +129,7 @@
|
|||
<MultiBinding Converter="{StaticResource MultiBoolToVisibilityCollapsedConverter}" Mode="OneWay">
|
||||
<Binding Path="IsRecording" Mode="OneWay"/>
|
||||
<Binding RelativeSource="{RelativeSource Mode=FindAncestor,AncestorType=pages:RootPage}"
|
||||
Path="DataContext.Recorder.Config.EnabledFeature" Mode="OneWay"
|
||||
Path="DataContext.Recorder.Config.Global.EnabledFeature" Mode="OneWay"
|
||||
Converter="{StaticResource ClipEnabledToBooleanConverter}"/>
|
||||
</MultiBinding>
|
||||
</Button.Visibility>
|
||||
|
|
|
@ -13,49 +13,56 @@ namespace BililiveRecorder.WPF.Controls
|
|||
{
|
||||
public RoomCard()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
public event EventHandler DeleteRequested;
|
||||
|
||||
public event EventHandler ShowSettingsRequested;
|
||||
|
||||
private void MenuItem_StartRecording_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
(DataContext as IRecordedRoom)?.StartRecord();
|
||||
(this.DataContext as IRecordedRoom)?.StartRecord();
|
||||
}
|
||||
|
||||
private void MenuItem_StopRecording_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
(DataContext as IRecordedRoom)?.StopRecord();
|
||||
(this.DataContext as IRecordedRoom)?.StopRecord();
|
||||
}
|
||||
|
||||
private void MenuItem_RefreshInfo_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
(DataContext as IRecordedRoom)?.RefreshRoomInfo();
|
||||
(this.DataContext as IRecordedRoom)?.RefreshRoomInfo();
|
||||
}
|
||||
|
||||
private void MenuItem_StartMonitor_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
(DataContext as IRecordedRoom)?.Start();
|
||||
(this.DataContext as IRecordedRoom)?.Start();
|
||||
}
|
||||
|
||||
private void MenuItem_StopMonitor_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
(DataContext as IRecordedRoom)?.Stop();
|
||||
(this.DataContext as IRecordedRoom)?.Stop();
|
||||
}
|
||||
|
||||
private void MenuItem_DeleteRoom_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DeleteRequested?.Invoke(DataContext, EventArgs.Empty);
|
||||
DeleteRequested?.Invoke(this.DataContext, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void MenuItem_ShowSettings_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ShowSettingsRequested?.Invoke(this.DataContext, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void Button_Clip_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
(DataContext as IRecordedRoom)?.Clip();
|
||||
(this.DataContext as IRecordedRoom)?.Clip();
|
||||
}
|
||||
|
||||
private void MenuItem_OpenInBrowser_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is IRecordedRoom r && r is not null)
|
||||
if (this.DataContext is IRecordedRoom r && r is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
28
BililiveRecorder.WPF/Controls/SettingWithDefault.xaml
Normal file
28
BililiveRecorder.WPF/Controls/SettingWithDefault.xaml
Normal file
|
@ -0,0 +1,28 @@
|
|||
<UserControl
|
||||
Name="SettingWithDefaultUserControl"
|
||||
x:Class="BililiveRecorder.WPF.Controls.SettingWithDefault"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="50" d:DesignWidth="200">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="80"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Text="{Binding Header,ElementName=SettingWithDefaultUserControl}" Visibility="{Binding Header.Length, ElementName=SettingWithDefaultUserControl,Converter={StaticResource ShortRoomIdToVisibilityConverter}}"/>
|
||||
<ContentPresenter Grid.Row="1"
|
||||
Content="{Binding InnerContent,ElementName=SettingWithDefaultUserControl}"
|
||||
IsEnabled="{Binding IsSettingNotUsingDefault,ElementName=SettingWithDefaultUserControl}"/>
|
||||
<CheckBox Content="默认" Margin="10,0,0,0" Grid.Row="1" Grid.Column="1" VerticalAlignment="Top" HorizontalAlignment="Left"
|
||||
IsChecked="{Binding IsSettingNotUsingDefault,ElementName=SettingWithDefaultUserControl,Converter={StaticResource BooleanInverterConverter}}"/>
|
||||
</Grid>
|
||||
</UserControl>
|
49
BililiveRecorder.WPF/Controls/SettingWithDefault.xaml.cs
Normal file
49
BililiveRecorder.WPF/Controls/SettingWithDefault.xaml.cs
Normal file
|
@ -0,0 +1,49 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Markup;
|
||||
|
||||
namespace BililiveRecorder.WPF.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for SettingWithDefault.xaml
|
||||
/// </summary>
|
||||
[ContentProperty("InnerContent")]
|
||||
public partial class SettingWithDefault
|
||||
{
|
||||
public SettingWithDefault()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty HeaderProperty
|
||||
= DependencyProperty.Register("Header",
|
||||
typeof(string),
|
||||
typeof(SettingWithDefault),
|
||||
new FrameworkPropertyMetadata(string.Empty));
|
||||
|
||||
public string Header
|
||||
{
|
||||
get => (string)this.GetValue(HeaderProperty);
|
||||
set => this.SetValue(HeaderProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsSettingNotUsingDefaultProperty
|
||||
= DependencyProperty.Register("IsSettingNotUsingDefault",
|
||||
typeof(bool),
|
||||
typeof(SettingWithDefault),
|
||||
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
|
||||
|
||||
public bool IsSettingNotUsingDefault
|
||||
{
|
||||
get => (bool)this.GetValue(IsSettingNotUsingDefaultProperty);
|
||||
set => this.SetValue(IsSettingNotUsingDefaultProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty InnerContentProperty = DependencyProperty.Register("InnerContent", typeof(object), typeof(SettingWithDefault));
|
||||
|
||||
public object InnerContent
|
||||
{
|
||||
get => this.GetValue(InnerContentProperty);
|
||||
set => this.SetValue(InnerContentProperty, value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,7 +35,7 @@
|
|||
</Grid.RowDefinitions>
|
||||
<StackPanel Margin="10">
|
||||
<TextBlock Style="{DynamicResource SubtitleTextBlockStyle}" Text="B站录播姬" FontFamily="Microsoft Yahei"/>
|
||||
<TextBlock Text="{Binding Recorder.Config.WorkDirectory,Mode=OneWay}" TextWrapping="NoWrap" FontFamily="Microsoft Yahei"/>
|
||||
<TextBlock Text="{Binding Recorder.Config.Global.WorkDirectory,Mode=OneWay}" TextWrapping="NoWrap" FontFamily="Microsoft Yahei"/>
|
||||
</StackPanel>
|
||||
<Separator Grid.Row="1" Grid.ColumnSpan="2" />
|
||||
<ItemsControl Grid.Row="2" Grid.ColumnSpan="2" Margin="10,0,10,10" ItemsSource="{Binding Recorder, Mode=OneWay}">
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
|
||||
namespace BililiveRecorder.WPF.Controls
|
||||
{
|
||||
|
@ -11,14 +10,14 @@ namespace BililiveRecorder.WPF.Controls
|
|||
{
|
||||
public TaskbarIconControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.InitializeComponent();
|
||||
|
||||
// AddHandler(NewMainWindow.ShowBalloonTipEvent, (RoutedEventHandler)UserControl_ShowBalloonTip);
|
||||
if (Application.Current.MainWindow is NewMainWindow nmw)
|
||||
{
|
||||
nmw.ShowBalloonTipCallback = (title, msg, sym) =>
|
||||
{
|
||||
TaskbarIcon.ShowBalloonTip(title, msg, sym);
|
||||
this.TaskbarIcon.ShowBalloonTip(title, msg, sym);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,14 +13,14 @@ namespace BililiveRecorder.WPF.Controls
|
|||
private string error = string.Empty;
|
||||
private string path = string.Empty;
|
||||
|
||||
public string Error { get => error; set => SetField(ref error, value); }
|
||||
public string Error { get => this.error; set => this.SetField(ref this.error, value); }
|
||||
|
||||
public string Path { get => path; set => SetField(ref path, value); }
|
||||
public string Path { get => this.path; set => this.SetField(ref this.path, value); }
|
||||
|
||||
public WorkDirectorySelectorDialog()
|
||||
{
|
||||
DataContext = this;
|
||||
InitializeComponent();
|
||||
this.DataContext = this;
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
@ -28,7 +28,7 @@ namespace BililiveRecorder.WPF.Controls
|
|||
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) { return false; }
|
||||
field = value; OnPropertyChanged(propertyName); return true;
|
||||
field = value; this.OnPropertyChanged(propertyName); return true;
|
||||
}
|
||||
|
||||
private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
|
||||
|
@ -45,7 +45,7 @@ namespace BililiveRecorder.WPF.Controls
|
|||
};
|
||||
if (fileDialog.ShowDialog() == CommonFileDialogResult.Ok)
|
||||
{
|
||||
Path = fileDialog.FileName;
|
||||
this.Path = fileDialog.FileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,24 +10,24 @@ namespace BililiveRecorder.WPF.Converters
|
|||
public static readonly DependencyProperty TrueValueProperty = DependencyProperty.Register(nameof(TrueValue), typeof(object), typeof(BoolToValueConverter), new PropertyMetadata(null));
|
||||
public static readonly DependencyProperty FalseValueProperty = DependencyProperty.Register(nameof(FalseValue), typeof(object), typeof(BoolToValueConverter), new PropertyMetadata(null));
|
||||
|
||||
public object TrueValue { get => GetValue(TrueValueProperty); set => SetValue(TrueValueProperty, value); }
|
||||
public object FalseValue { get => GetValue(FalseValueProperty); set => SetValue(FalseValueProperty, value); }
|
||||
public object TrueValue { get => this.GetValue(TrueValueProperty); set => this.SetValue(TrueValueProperty, value); }
|
||||
public object FalseValue { get => this.GetValue(FalseValueProperty); set => this.SetValue(FalseValueProperty, value); }
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return FalseValue;
|
||||
return this.FalseValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (bool)value ? TrueValue : FalseValue;
|
||||
return (bool)value ? this.TrueValue : this.FalseValue;
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value != null ? value.Equals(TrueValue) : false;
|
||||
return value != null ? value.Equals(this.TrueValue) : false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,10 +15,10 @@ namespace BililiveRecorder.WPF.Converters
|
|||
{
|
||||
if ((value is bool boolean) && boolean == false)
|
||||
{
|
||||
return FalseValue;
|
||||
return this.FalseValue;
|
||||
}
|
||||
}
|
||||
return TrueValue;
|
||||
return this.TrueValue;
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
|
|
|
@ -8,6 +8,6 @@ namespace BililiveRecorder.WPF.Converters
|
|||
public DataTemplate Normal { get; set; }
|
||||
public DataTemplate Null { get; set; }
|
||||
|
||||
public override DataTemplate SelectTemplate(object item, DependencyObject container) => item is null ? Null : Normal;
|
||||
public override DataTemplate SelectTemplate(object item, DependencyObject container) => item is null ? this.Null : this.Normal;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ using System.Collections.Specialized;
|
|||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using BililiveRecorder.Core;
|
||||
using BililiveRecorder.Core.Config;
|
||||
|
||||
namespace BililiveRecorder.WPF.Converters
|
||||
{
|
||||
|
@ -27,7 +26,7 @@ namespace BililiveRecorder.WPF.Converters
|
|||
public RecorderWrapper(IRecorder recorder) : base(recorder)
|
||||
{
|
||||
this.recorder = recorder;
|
||||
Add(null);
|
||||
this.Add(null);
|
||||
|
||||
recorder.CollectionChanged += (sender, e) =>
|
||||
{
|
||||
|
@ -35,19 +34,19 @@ namespace BililiveRecorder.WPF.Converters
|
|||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
if (e.NewItems.Count != 1) throw new NotImplementedException("Wrapper Add Item Count != 1");
|
||||
InsertItem(e.NewStartingIndex, e.NewItems[0] as IRecordedRoom);
|
||||
this.InsertItem(e.NewStartingIndex, e.NewItems[0] as IRecordedRoom);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
if (e.OldItems.Count != 1) throw new NotImplementedException("Wrapper Remove Item Count != 1");
|
||||
if (!Remove(e.OldItems[0] as IRecordedRoom)) throw new NotImplementedException("Wrapper Remove Item Sync Fail");
|
||||
if (!this.Remove(e.OldItems[0] as IRecordedRoom)) throw new NotImplementedException("Wrapper Remove Item Sync Fail");
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
throw new NotImplementedException("Wrapper Replace Item");
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
throw new NotImplementedException("Wrapper Move Item");
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
ClearItems();
|
||||
Add(null);
|
||||
this.ClearItems();
|
||||
this.Add(null);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.ComponentModel;
|
||||
using BililiveRecorder.Core;
|
||||
using BililiveRecorder.Core.Callback;
|
||||
using BililiveRecorder.Core.Config.V2;
|
||||
using BililiveRecorder.FlvProcessor;
|
||||
|
||||
#nullable enable
|
||||
|
@ -14,14 +15,14 @@ namespace BililiveRecorder.WPF.MockData
|
|||
|
||||
public MockRecordedRoom()
|
||||
{
|
||||
RoomId = 123456789;
|
||||
ShortRoomId = 1234;
|
||||
StreamerName = "Mock主播名Mock主播名Mock主播名Mock主播名";
|
||||
IsMonitoring = false;
|
||||
IsRecording = true;
|
||||
IsStreaming = true;
|
||||
DownloadSpeedPersentage = 100d;
|
||||
DownloadSpeedMegaBitps = 2.45d;
|
||||
this.RoomId = 123456789;
|
||||
this.ShortRoomId = 1234;
|
||||
this.StreamerName = "Mock主播名Mock主播名Mock主播名Mock主播名";
|
||||
this.IsMonitoring = false;
|
||||
this.IsRecording = true;
|
||||
this.IsStreaming = true;
|
||||
this.DownloadSpeedPersentage = 100d;
|
||||
this.DownloadSpeedMegaBitps = 2.45d;
|
||||
}
|
||||
|
||||
public int ShortRoomId { get; set; }
|
||||
|
@ -52,6 +53,8 @@ namespace BililiveRecorder.WPF.MockData
|
|||
|
||||
public Guid Guid { get; } = Guid.NewGuid();
|
||||
|
||||
public RoomConfig RoomConfig => new RoomConfig();
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public event EventHandler<RecordEndData>? RecordEnded;
|
||||
|
@ -70,32 +73,32 @@ namespace BililiveRecorder.WPF.MockData
|
|||
|
||||
public bool Start()
|
||||
{
|
||||
IsMonitoring = true;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsMonitoring)));
|
||||
this.IsMonitoring = true;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsMonitoring)));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void StartRecord()
|
||||
{
|
||||
IsRecording = true;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRecording)));
|
||||
this.IsRecording = true;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsRecording)));
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
IsMonitoring = false;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsMonitoring)));
|
||||
this.IsMonitoring = false;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsMonitoring)));
|
||||
}
|
||||
|
||||
public void StopRecord()
|
||||
{
|
||||
IsRecording = false;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRecording)));
|
||||
this.IsRecording = false;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsRecording)));
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
if (!this.disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
|
@ -104,7 +107,7 @@ namespace BililiveRecorder.WPF.MockData
|
|||
|
||||
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
|
||||
// TODO: set large fields to null
|
||||
disposedValue = true;
|
||||
this.disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,7 +121,7 @@ namespace BililiveRecorder.WPF.MockData
|
|||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
this.Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ using System.Collections.ObjectModel;
|
|||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using BililiveRecorder.Core;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.Core.Config.V2;
|
||||
|
||||
namespace BililiveRecorder.WPF.MockData
|
||||
{
|
||||
|
@ -16,47 +16,47 @@ namespace BililiveRecorder.WPF.MockData
|
|||
|
||||
public MockRecorder()
|
||||
{
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
this.Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
IsMonitoring = false,
|
||||
IsRecording = false
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
this.Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
IsMonitoring = true,
|
||||
IsRecording = false
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
this.Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 100,
|
||||
DownloadSpeedMegaBitps = 12.45
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
this.Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 95,
|
||||
DownloadSpeedMegaBitps = 789.45
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
this.Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 90
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
this.Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 85
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
this.Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 80
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
this.Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 75
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
this.Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 70
|
||||
});
|
||||
Rooms.Add(new MockRecordedRoom
|
||||
this.Rooms.Add(new MockRecordedRoom
|
||||
{
|
||||
DownloadSpeedPersentage = 109
|
||||
});
|
||||
|
@ -64,28 +64,28 @@ namespace BililiveRecorder.WPF.MockData
|
|||
|
||||
private ObservableCollection<IRecordedRoom> Rooms { get; } = new ObservableCollection<IRecordedRoom>();
|
||||
|
||||
public ConfigV1 Config { get; } = new ConfigV1();
|
||||
public ConfigV2 Config { get; } = new ConfigV2();
|
||||
|
||||
public int Count => Rooms.Count;
|
||||
public int Count => this.Rooms.Count;
|
||||
|
||||
public bool IsReadOnly => true;
|
||||
|
||||
int ICollection<IRecordedRoom>.Count => Rooms.Count;
|
||||
int ICollection<IRecordedRoom>.Count => this.Rooms.Count;
|
||||
|
||||
bool ICollection<IRecordedRoom>.IsReadOnly => true;
|
||||
|
||||
public IRecordedRoom this[int index] => Rooms[index];
|
||||
public IRecordedRoom this[int index] => this.Rooms[index];
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged
|
||||
{
|
||||
add => (Rooms as INotifyPropertyChanged).PropertyChanged += value;
|
||||
remove => (Rooms as INotifyPropertyChanged).PropertyChanged -= value;
|
||||
add => (this.Rooms as INotifyPropertyChanged).PropertyChanged += value;
|
||||
remove => (this.Rooms as INotifyPropertyChanged).PropertyChanged -= value;
|
||||
}
|
||||
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged
|
||||
{
|
||||
add => (Rooms as INotifyCollectionChanged).CollectionChanged += value;
|
||||
remove => (Rooms as INotifyCollectionChanged).CollectionChanged -= value;
|
||||
add => (this.Rooms as INotifyCollectionChanged).CollectionChanged += value;
|
||||
remove => (this.Rooms as INotifyCollectionChanged).CollectionChanged -= value;
|
||||
}
|
||||
|
||||
void ICollection<IRecordedRoom>.Add(IRecordedRoom item) => throw new NotSupportedException("Collection is readonly");
|
||||
|
@ -94,35 +94,35 @@ namespace BililiveRecorder.WPF.MockData
|
|||
|
||||
bool ICollection<IRecordedRoom>.Remove(IRecordedRoom item) => throw new NotSupportedException("Collection is readonly");
|
||||
|
||||
bool ICollection<IRecordedRoom>.Contains(IRecordedRoom item) => Rooms.Contains(item);
|
||||
bool ICollection<IRecordedRoom>.Contains(IRecordedRoom item) => this.Rooms.Contains(item);
|
||||
|
||||
void ICollection<IRecordedRoom>.CopyTo(IRecordedRoom[] array, int arrayIndex) => Rooms.CopyTo(array, arrayIndex);
|
||||
void ICollection<IRecordedRoom>.CopyTo(IRecordedRoom[] array, int arrayIndex) => this.Rooms.CopyTo(array, arrayIndex);
|
||||
|
||||
public IEnumerator<IRecordedRoom> GetEnumerator() => Rooms.GetEnumerator();
|
||||
public IEnumerator<IRecordedRoom> GetEnumerator() => this.Rooms.GetEnumerator();
|
||||
|
||||
IEnumerator<IRecordedRoom> IEnumerable<IRecordedRoom>.GetEnumerator() => Rooms.GetEnumerator();
|
||||
IEnumerator<IRecordedRoom> IEnumerable<IRecordedRoom>.GetEnumerator() => this.Rooms.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => Rooms.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.Rooms.GetEnumerator();
|
||||
|
||||
public bool Initialize(string workdir)
|
||||
{
|
||||
Config.WorkDirectory = workdir;
|
||||
this.Config.Global.WorkDirectory = workdir;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void AddRoom(int roomid)
|
||||
{
|
||||
AddRoom(roomid, false);
|
||||
this.AddRoom(roomid, false);
|
||||
}
|
||||
|
||||
public void AddRoom(int roomid, bool enabled)
|
||||
{
|
||||
Rooms.Add(new MockRecordedRoom { RoomId = roomid, IsMonitoring = enabled });
|
||||
this.Rooms.Add(new MockRecordedRoom { RoomId = roomid, IsMonitoring = enabled });
|
||||
}
|
||||
|
||||
public void RemoveRoom(IRecordedRoom rr)
|
||||
{
|
||||
Rooms.Remove(rr);
|
||||
this.Rooms.Remove(rr);
|
||||
}
|
||||
|
||||
public void SaveConfigToFile()
|
||||
|
@ -132,17 +132,17 @@ namespace BililiveRecorder.WPF.MockData
|
|||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
if (!this.disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// dispose managed state (managed objects)
|
||||
Rooms.Clear();
|
||||
this.Rooms.Clear();
|
||||
}
|
||||
|
||||
// free unmanaged resources (unmanaged objects) and override finalizer
|
||||
// set large fields to null
|
||||
disposedValue = true;
|
||||
this.disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,7 +156,7 @@ namespace BililiveRecorder.WPF.MockData
|
|||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
this.Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,12 +32,12 @@ namespace BililiveRecorder.WPF.Models
|
|||
|
||||
#region ICommand Members
|
||||
|
||||
public void Execute(object parameter) => ExecuteDelegate?.Invoke(parameter);
|
||||
public void Execute(object parameter) => this.ExecuteDelegate?.Invoke(parameter);
|
||||
|
||||
public bool CanExecute(object parameter) => CanExecuteDelegate switch
|
||||
public bool CanExecute(object parameter) => this.CanExecuteDelegate switch
|
||||
{
|
||||
null => true,
|
||||
_ => CanExecuteDelegate(parameter),
|
||||
_ => this.CanExecuteDelegate(parameter),
|
||||
};
|
||||
|
||||
public event EventHandler CanExecuteChanged
|
||||
|
|
|
@ -16,37 +16,37 @@ namespace BililiveRecorder.WPF.Models
|
|||
|
||||
public LogModel() : base(new[] { "" })
|
||||
{
|
||||
LogReceived += LogModel_LogReceived;
|
||||
LogReceived += this.LogModel_LogReceived;
|
||||
}
|
||||
|
||||
private void LogModel_LogReceived(object sender, string e)
|
||||
{
|
||||
_ = Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.DataBind, (Action<string>)AddLogToCollection, e);
|
||||
_ = Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.DataBind, (Action<string>)this.AddLogToCollection, e);
|
||||
}
|
||||
|
||||
private void AddLogToCollection(string e)
|
||||
{
|
||||
Add(e);
|
||||
while (Count > MAX_LINE)
|
||||
this.Add(e);
|
||||
while (this.Count > MAX_LINE)
|
||||
{
|
||||
RemoveItem(0);
|
||||
this.RemoveItem(0);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
if (!this.disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// dispose managed state (managed objects)
|
||||
LogReceived -= LogModel_LogReceived;
|
||||
ClearItems();
|
||||
LogReceived -= this.LogModel_LogReceived;
|
||||
this.ClearItems();
|
||||
}
|
||||
|
||||
// free unmanaged resources (unmanaged objects) and override finalizer
|
||||
// set large fields to null
|
||||
disposedValue = true;
|
||||
this.disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,7 @@ namespace BililiveRecorder.WPF.Models
|
|||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
this.Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace BililiveRecorder.WPF.Models
|
|||
|
||||
public LogModel Logs { get; } = new LogModel();
|
||||
|
||||
public IRecorder Recorder { get => recorder; internal set => SetField(ref recorder, value); }
|
||||
public IRecorder Recorder { get => this.recorder; internal set => this.SetField(ref this.recorder, value); }
|
||||
|
||||
public RootModel()
|
||||
{
|
||||
|
@ -26,23 +26,23 @@ namespace BililiveRecorder.WPF.Models
|
|||
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) { return false; }
|
||||
field = value; OnPropertyChanged(propertyName); return true;
|
||||
field = value; this.OnPropertyChanged(propertyName); return true;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposedValue)
|
||||
if (!this.disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// dispose managed state (managed objects)
|
||||
Recorder?.Dispose();
|
||||
Logs.Dispose();
|
||||
this.Recorder?.Dispose();
|
||||
this.Logs.Dispose();
|
||||
}
|
||||
|
||||
// free unmanaged resources (unmanaged objects) and override finalizer
|
||||
// set large fields to null
|
||||
disposedValue = true;
|
||||
this.disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,7 +56,7 @@ namespace BililiveRecorder.WPF.Models
|
|||
public void Dispose()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
this.Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,14 +14,14 @@ namespace BililiveRecorder.WPF
|
|||
{
|
||||
public NewMainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.InitializeComponent();
|
||||
|
||||
Title = "B站录播姬 " + BuildInfo.Version + " " + BuildInfo.HeadShaShort;
|
||||
this.Title = "B站录播姬 " + BuildInfo.Version + " " + BuildInfo.HeadShaShort;
|
||||
|
||||
SingleInstance.NotificationReceived += SingleInstance_NotificationReceived;
|
||||
SingleInstance.NotificationReceived += this.SingleInstance_NotificationReceived;
|
||||
}
|
||||
|
||||
private void SingleInstance_NotificationReceived(object sender, EventArgs e) => SuperActivateAction();
|
||||
private void SingleInstance_NotificationReceived(object sender, EventArgs e) => this.SuperActivateAction();
|
||||
|
||||
public event EventHandler NativeBeforeWindowClose;
|
||||
|
||||
|
@ -29,20 +29,20 @@ namespace BililiveRecorder.WPF
|
|||
|
||||
internal void CloseWithoutConfirmAction()
|
||||
{
|
||||
CloseConfirmed = true;
|
||||
Close();
|
||||
this.CloseConfirmed = true;
|
||||
this.Close();
|
||||
}
|
||||
|
||||
internal void SuperActivateAction()
|
||||
{
|
||||
try
|
||||
{
|
||||
Show();
|
||||
WindowState = WindowState.Normal;
|
||||
Topmost = true;
|
||||
Activate();
|
||||
Topmost = false;
|
||||
Focus();
|
||||
this.Show();
|
||||
this.WindowState = WindowState.Normal;
|
||||
this.Topmost = true;
|
||||
this.Activate();
|
||||
this.Topmost = false;
|
||||
this.Focus();
|
||||
}
|
||||
catch (Exception)
|
||||
{ }
|
||||
|
@ -50,10 +50,10 @@ namespace BililiveRecorder.WPF
|
|||
|
||||
private void Window_StateChanged(object sender, EventArgs e)
|
||||
{
|
||||
if (WindowState == WindowState.Minimized)
|
||||
if (this.WindowState == WindowState.Minimized)
|
||||
{
|
||||
Hide();
|
||||
ShowBalloonTipCallback?.Invoke("B站录播姬", "录播姬已最小化到托盘,左键单击图标恢复界面", BalloonIcon.Info);
|
||||
this.Hide();
|
||||
this.ShowBalloonTipCallback?.Invoke("B站录播姬", "录播姬已最小化到托盘,左键单击图标恢复界面", BalloonIcon.Info);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,28 +67,28 @@ namespace BililiveRecorder.WPF
|
|||
|
||||
private async void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
if (PromptCloseConfirm && !CloseConfirmed)
|
||||
if (this.PromptCloseConfirm && !this.CloseConfirmed)
|
||||
{
|
||||
e.Cancel = true;
|
||||
if (CloseWindowSemaphoreSlim.Wait(0))
|
||||
if (this.CloseWindowSemaphoreSlim.Wait(0))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await new CloseWindowConfirmDialog().ShowAsync() == ContentDialogResult.Primary)
|
||||
{
|
||||
CloseConfirmed = true;
|
||||
CloseWindowSemaphoreSlim.Release();
|
||||
Close();
|
||||
this.CloseConfirmed = true;
|
||||
this.CloseWindowSemaphoreSlim.Release();
|
||||
this.Close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception) { }
|
||||
CloseWindowSemaphoreSlim.Release();
|
||||
this.CloseWindowSemaphoreSlim.Release();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SingleInstance.NotificationReceived -= SingleInstance_NotificationReceived;
|
||||
SingleInstance.NotificationReceived -= this.SingleInstance_NotificationReceived;
|
||||
NativeBeforeWindowClose?.Invoke(this, EventArgs.Empty);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -5,78 +5,87 @@
|
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:c="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages"
|
||||
xmlns:config="clr-namespace:BililiveRecorder.Core.Config;assembly=BililiveRecorder.Core"
|
||||
xmlns:config="clr-namespace:BililiveRecorder.Core.Config.V2;assembly=BililiveRecorder.Core"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="1500" d:DesignWidth="500"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:RootPage},Path=DataContext.Recorder.Config}"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:RootPage},Path=DataContext.Recorder.Config.Global}"
|
||||
Title="SettingsPage">
|
||||
<ui:Page.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
<Style TargetType="ui:NumberBox">
|
||||
<Setter Property="Width" Value="250"/>
|
||||
<Style TargetType="c:SettingWithDefault">
|
||||
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||
</Style>
|
||||
<Style TargetType="ui:NumberBox">
|
||||
<Setter Property="Width" Value="220"/>
|
||||
<Setter Property="ValidationMode" Value="InvalidInputOverwritten"/>
|
||||
<Setter Property="SpinButtonPlacementMode" Value="Inline"/>
|
||||
</Style>
|
||||
</ui:Page.Resources>
|
||||
<ScrollViewer d:DataContext="{d:DesignInstance Type=config:ConfigV1}">
|
||||
<ScrollViewer d:DataContext="{d:DesignInstance Type=config:GlobalConfig}">
|
||||
<ui:SimpleStackPanel Orientation="Vertical" Spacing="5" Margin="20">
|
||||
<TextBlock Text="高级设置" Style="{StaticResource TitleTextBlockStyle}" Margin="0,10"/>
|
||||
<TextBlock Text="高级设置" Style="{StaticResource TitleTextBlockStyle}" FontFamily="Microsoft YaHei" Margin="0,10"/>
|
||||
<GroupBox Header="Cookie">
|
||||
<StackPanel>
|
||||
<TextBlock Text="请求API时使用此 Cookie"/>
|
||||
<TextBox Text="{Binding Cookie,UpdateSourceTrigger=PropertyChanged,Delay=1000}" Width="250" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasCookie}" Header="请求API时使用此 Cookie">
|
||||
<TextBox Text="{Binding Cookie,UpdateSourceTrigger=PropertyChanged,Delay=1000}" Width="220" HorizontalAlignment="Left"/>
|
||||
</c:SettingWithDefault>
|
||||
</GroupBox>
|
||||
<GroupBox Header="弹幕录制">
|
||||
<ui:NumberBox Minimum="0" Header="触发写硬盘所需弹幕个数" Description="单位: 个" SmallChange="1"
|
||||
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasRecordDanmakuFlushInterval}" Header="触发写硬盘所需弹幕个数">
|
||||
<ui:NumberBox Minimum="0" Description="单位: 个" SmallChange="1"
|
||||
Text="{Binding RecordDanmakuFlushInterval,UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</c:SettingWithDefault>
|
||||
</GroupBox>
|
||||
<GroupBox Header="Timing">
|
||||
<ui:SimpleStackPanel Spacing="10">
|
||||
<ui:NumberBox Minimum="1000" Header="录制重试间隔" Description="单位: 毫秒" SmallChange="100" Text="{Binding TimingStreamRetry,Delay=500}">
|
||||
<ui:NumberBox.ToolTip>
|
||||
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasTimingStreamRetry}" Header="录制重试间隔">
|
||||
<c:SettingWithDefault.ToolTip>
|
||||
<TextBlock>录制断开后等待多长时间再尝试开始录制</TextBlock>
|
||||
</ui:NumberBox.ToolTip>
|
||||
</ui:NumberBox>
|
||||
<ui:NumberBox Minimum="1000" Header="录制连接超时" Description="单位: 毫秒" SmallChange="100" Text="{Binding TimingStreamConnect,Delay=500}">
|
||||
<ui:NumberBox.ToolTip>
|
||||
</c:SettingWithDefault.ToolTip>
|
||||
<ui:NumberBox Minimum="1000" Description="单位: 毫秒" SmallChange="100" Text="{Binding TimingStreamRetry,Delay=500}"/>
|
||||
</c:SettingWithDefault>
|
||||
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasTimingStreamConnect}" Header="录制连接超时">
|
||||
<c:SettingWithDefault.ToolTip>
|
||||
<TextBlock>
|
||||
发出连接直播服务器的请求后等待多长时间<LineBreak/>
|
||||
防止直播服务器长时间不返回数据导致卡住
|
||||
</TextBlock>
|
||||
</ui:NumberBox.ToolTip>
|
||||
</ui:NumberBox>
|
||||
<ui:NumberBox Minimum="1000" Header="弹幕重连间隔" Description="单位: 毫秒" SmallChange="100" Text="{Binding TimingDanmakuRetry,Delay=500}">
|
||||
<ui:NumberBox.ToolTip>
|
||||
</c:SettingWithDefault.ToolTip>
|
||||
<ui:NumberBox Minimum="1000" Description="单位: 毫秒" SmallChange="100" Text="{Binding TimingStreamConnect,Delay=500}"/>
|
||||
</c:SettingWithDefault>
|
||||
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasTimingDanmakuRetry}" Header="弹幕重连间隔">
|
||||
<c:SettingWithDefault.ToolTip>
|
||||
<TextBlock>
|
||||
弹幕服务器被断开后等待多长时间再尝试连接<LineBreak/>
|
||||
监控开播的主要途径是通过弹幕服务器发送的信息
|
||||
</TextBlock>
|
||||
</ui:NumberBox.ToolTip>
|
||||
</ui:NumberBox>
|
||||
<ui:NumberBox Minimum="1000" Header="接收数据超时" Description="单位: 毫秒" SmallChange="100" Text="{Binding TimingWatchdogTimeout,Delay=500}">
|
||||
<ui:NumberBox.ToolTip>
|
||||
</c:SettingWithDefault.ToolTip>
|
||||
<ui:NumberBox Minimum="1000" Description="单位: 毫秒" SmallChange="100" Text="{Binding TimingDanmakuRetry,Delay=500}"/>
|
||||
</c:SettingWithDefault>
|
||||
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasTimingWatchdogTimeout}" Header="接收数据超时">
|
||||
<c:SettingWithDefault.ToolTip>
|
||||
<TextBlock>
|
||||
在一定时间没有收到直播服务器发送的数据后断开重连<LineBreak/>
|
||||
用于防止因为玄学网络问题导致卡住
|
||||
</TextBlock>
|
||||
</ui:NumberBox.ToolTip>
|
||||
</ui:NumberBox>
|
||||
<ui:NumberBox Minimum="60" Header="开播检查间隔" Description="单位: 秒" SmallChange="10" Text="{Binding TimingCheckInterval,Delay=500}">
|
||||
<ui:NumberBox.ToolTip>
|
||||
</c:SettingWithDefault.ToolTip>
|
||||
<ui:NumberBox Minimum="1000" Description="单位: 毫秒" SmallChange="100" Text="{Binding TimingWatchdogTimeout,Delay=500}"/>
|
||||
</c:SettingWithDefault>
|
||||
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasTimingCheckInterval}" Header="开播检查间隔">
|
||||
<c:SettingWithDefault.ToolTip>
|
||||
<TextBlock>
|
||||
此项影响的时间间隔是定时请求HTTP接口的间隔,
|
||||
主要目的是防止没有从弹幕服务器收到开播消息,
|
||||
所以此项不需要设置太短。<LineBreak/>
|
||||
时间间隔设置太短会被B站服务器屏蔽,导致无法录制。
|
||||
</TextBlock>
|
||||
</ui:NumberBox.ToolTip>
|
||||
</ui:NumberBox>
|
||||
</c:SettingWithDefault.ToolTip>
|
||||
<ui:NumberBox Minimum="60" Description="单位: 秒" SmallChange="10" Text="{Binding TimingCheckInterval,Delay=500}"/>
|
||||
</c:SettingWithDefault>
|
||||
</ui:SimpleStackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="手动崩溃">
|
||||
|
|
|
@ -11,7 +11,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
{
|
||||
public AdvancedSettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.InitializeComponent();
|
||||
}
|
||||
|
||||
private void Crash_Click(object sender, RoutedEventArgs e)
|
||||
|
|
|
@ -28,20 +28,20 @@ namespace BililiveRecorder.WPF.Pages
|
|||
|
||||
public AnnouncementPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(async () => await LoadAnnouncementAsync(ignore_cache: false, show_error: false)));
|
||||
this.InitializeComponent();
|
||||
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(async () => await this.LoadAnnouncementAsync(ignore_cache: false, show_error: false)));
|
||||
}
|
||||
|
||||
private async void Button_Click(object sender, RoutedEventArgs e) => await LoadAnnouncementAsync(ignore_cache: true, show_error: Keyboard.Modifiers.HasFlag(ModifierKeys.Control));
|
||||
private async void Button_Click(object sender, RoutedEventArgs e) => await this.LoadAnnouncementAsync(ignore_cache: true, show_error: Keyboard.Modifiers.HasFlag(ModifierKeys.Control));
|
||||
|
||||
private async Task LoadAnnouncementAsync(bool ignore_cache, bool show_error)
|
||||
{
|
||||
MemoryStream data;
|
||||
bool success;
|
||||
|
||||
Container.Child = null;
|
||||
Error.Visibility = Visibility.Collapsed;
|
||||
Loading.Visibility = Visibility.Visible;
|
||||
this.Container.Child = null;
|
||||
this.Error.Visibility = Visibility.Collapsed;
|
||||
this.Loading.Visibility = Visibility.Visible;
|
||||
|
||||
if (AnnouncementCache is not null && !ignore_cache)
|
||||
{
|
||||
|
@ -83,7 +83,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
using var reader = new XamlXmlReader(stream, System.Windows.Markup.XamlReader.GetWpfSchemaContext());
|
||||
var obj = System.Windows.Markup.XamlReader.Load(reader);
|
||||
if (obj is UIElement elem)
|
||||
Container.Child = elem;
|
||||
this.Container.Child = elem;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -93,16 +93,16 @@ namespace BililiveRecorder.WPF.Pages
|
|||
}
|
||||
}
|
||||
|
||||
Loading.Visibility = Visibility.Collapsed;
|
||||
this.Loading.Visibility = Visibility.Collapsed;
|
||||
if (success)
|
||||
{
|
||||
RefreshButton.ToolTip = "当前公告获取时间: " + AnnouncementCacheTime.ToString("F");
|
||||
this.RefreshButton.ToolTip = "当前公告获取时间: " + AnnouncementCacheTime.ToString("F");
|
||||
AnnouncementCache = data;
|
||||
}
|
||||
else
|
||||
{
|
||||
RefreshButton.ToolTip = null;
|
||||
Error.Visibility = Visibility.Visible;
|
||||
this.RefreshButton.ToolTip = null;
|
||||
this.Error.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,7 +135,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
{
|
||||
MessageBox.Show(ex.ToString(), "加载发生错误");
|
||||
}
|
||||
await LoadAnnouncementAsync(ignore_cache: false, show_error: true);
|
||||
await this.LoadAnnouncementAsync(ignore_cache: false, show_error: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@ namespace BililiveRecorder.WPF.Pages
|
|||
{
|
||||
public LogPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
VersionTextBlock.Text = BuildInfo.Version + " " + BuildInfo.HeadShaShort;
|
||||
this.InitializeComponent();
|
||||
this.VersionTextBlock.Text = BuildInfo.Version + " " + BuildInfo.HeadShaShort;
|
||||
}
|
||||
|
||||
private void TextBlock_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages"
|
||||
xmlns:mock="clr-namespace:BililiveRecorder.WPF.MockData"
|
||||
xmlns:controls="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
xmlns:c="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
xmlns:converters="clr-namespace:BililiveRecorder.WPF.Converters"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="1000" d:DesignWidth="960"
|
||||
|
@ -17,12 +17,12 @@
|
|||
<ui:Page.Resources>
|
||||
<DataTemplate x:Key="NormalRoomCardTemplate">
|
||||
<ui:ThemeShadowChrome IsShadowEnabled="True" Depth="10">
|
||||
<controls:RoomCard DeleteRequested="RoomCard_DeleteRequested" />
|
||||
<c:RoomCard DeleteRequested="RoomCard_DeleteRequested" ShowSettingsRequested="RoomCard_ShowSettingsRequested" />
|
||||
</ui:ThemeShadowChrome>
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="AddRoomCardTemplate">
|
||||
<ui:ThemeShadowChrome IsShadowEnabled="True" Depth="10">
|
||||
<controls:AddRoomCard AddRoomRequested="AddRoomCard_AddRoomRequested"/>
|
||||
<c:AddRoomCard AddRoomRequested="AddRoomCard_AddRoomRequested"/>
|
||||
</ui:ThemeShadowChrome>
|
||||
</DataTemplate>
|
||||
<converters:NullValueTemplateSelector
|
||||
|
|
|
@ -29,15 +29,15 @@ namespace BililiveRecorder.WPF.Pages
|
|||
|
||||
public RoomListPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.InitializeComponent();
|
||||
|
||||
SortedRoomList = new SortedItemsSourceView(DataContext);
|
||||
DataContextChanged += RoomListPage_DataContextChanged;
|
||||
this.SortedRoomList = new SortedItemsSourceView(this.DataContext);
|
||||
DataContextChanged += this.RoomListPage_DataContextChanged;
|
||||
}
|
||||
|
||||
private void RoomListPage_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
(SortedRoomList as SortedItemsSourceView).Data = e.NewValue as ICollection<IRecordedRoom>;
|
||||
(this.SortedRoomList as SortedItemsSourceView).Data = e.NewValue as ICollection<IRecordedRoom>;
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty SortedRoomListProperty =
|
||||
|
@ -49,8 +49,8 @@ namespace BililiveRecorder.WPF.Pages
|
|||
|
||||
public object SortedRoomList
|
||||
{
|
||||
get => GetValue(SortedRoomListProperty);
|
||||
set => SetValue(SortedRoomListProperty, value);
|
||||
get => this.GetValue(SortedRoomListProperty);
|
||||
set => this.SetValue(SortedRoomListProperty, value);
|
||||
}
|
||||
|
||||
private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
|
@ -60,7 +60,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
|
||||
private async void RoomCard_DeleteRequested(object sender, EventArgs e)
|
||||
{
|
||||
if (DataContext is IRecorder rec && sender is IRecordedRoom room)
|
||||
if (this.DataContext is IRecorder rec && sender is IRecordedRoom room)
|
||||
{
|
||||
var dialog = new DeleteRoomConfirmDialog
|
||||
{
|
||||
|
@ -77,10 +77,19 @@ namespace BililiveRecorder.WPF.Pages
|
|||
}
|
||||
}
|
||||
|
||||
private async void RoomCard_ShowSettingsRequested(object sender, EventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
await new PerRoomSettingsDialog { DataContext = sender }.ShowAsync();
|
||||
}
|
||||
catch (Exception) { }
|
||||
}
|
||||
|
||||
private async void AddRoomCard_AddRoomRequested(object sender, string e)
|
||||
{
|
||||
var input = e.Trim();
|
||||
if (string.IsNullOrWhiteSpace(input) || DataContext is not IRecorder rec) return;
|
||||
if (string.IsNullOrWhiteSpace(input) || this.DataContext is not IRecorder rec) return;
|
||||
|
||||
if (!int.TryParse(input, out var roomid))
|
||||
{
|
||||
|
@ -119,7 +128,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
|
||||
private async void MenuItem_EnableAutoRecAll_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!(DataContext is IRecorder rec)) return;
|
||||
if (!(this.DataContext is IRecorder rec)) return;
|
||||
|
||||
await Task.WhenAll(rec.ToList().Select(rr => Task.Run(() => rr.Start())));
|
||||
rec.SaveConfigToFile();
|
||||
|
@ -127,7 +136,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
|
||||
private async void MenuItem_DisableAutoRecAll_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!(DataContext is IRecorder rec)) return;
|
||||
if (!(this.DataContext is IRecorder rec)) return;
|
||||
|
||||
await Task.WhenAll(rec.ToList().Select(rr => Task.Run(() => rr.Stop())));
|
||||
rec.SaveConfigToFile();
|
||||
|
@ -135,23 +144,23 @@ namespace BililiveRecorder.WPF.Pages
|
|||
|
||||
private void MenuItem_SortBy_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
(SortedRoomList as SortedItemsSourceView).SortedBy = (SortedBy)((MenuItem)sender).Tag;
|
||||
(this.SortedRoomList as SortedItemsSourceView).SortedBy = (SortedBy)((MenuItem)sender).Tag;
|
||||
}
|
||||
|
||||
private void MenuItem_ShowLog_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Splitter.Visibility = Visibility.Visible;
|
||||
LogElement.Visibility = Visibility.Visible;
|
||||
RoomListRowDefinition.Height = new GridLength(1, GridUnitType.Star);
|
||||
LogRowDefinition.Height = new GridLength(1, GridUnitType.Star);
|
||||
this.Splitter.Visibility = Visibility.Visible;
|
||||
this.LogElement.Visibility = Visibility.Visible;
|
||||
this.RoomListRowDefinition.Height = new GridLength(1, GridUnitType.Star);
|
||||
this.LogRowDefinition.Height = new GridLength(1, GridUnitType.Star);
|
||||
}
|
||||
|
||||
private void MenuItem_HideLog_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Splitter.Visibility = Visibility.Collapsed;
|
||||
LogElement.Visibility = Visibility.Collapsed;
|
||||
RoomListRowDefinition.Height = new GridLength(1, GridUnitType.Star);
|
||||
LogRowDefinition.Height = new GridLength(0);
|
||||
this.Splitter.Visibility = Visibility.Collapsed;
|
||||
this.LogElement.Visibility = Visibility.Collapsed;
|
||||
this.RoomListRowDefinition.Height = new GridLength(1, GridUnitType.Star);
|
||||
this.LogRowDefinition.Height = new GridLength(0);
|
||||
}
|
||||
|
||||
private void Log_ScrollViewer_Loaded(object sender, RoutedEventArgs e)
|
||||
|
@ -177,8 +186,8 @@ namespace BililiveRecorder.WPF.Pages
|
|||
{
|
||||
try
|
||||
{
|
||||
if (DataContext is IRecorder rec)
|
||||
Process.Start("explorer.exe", rec.Config.WorkDirectory);
|
||||
if (this.DataContext is IRecorder rec)
|
||||
Process.Start("explorer.exe", rec.Config.Global.WorkDirectory);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
|
@ -195,7 +204,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
|
||||
internal class SortedItemsSourceView : IList, IReadOnlyList<IRecordedRoom>, IKeyIndexMapping, INotifyCollectionChanged
|
||||
{
|
||||
private static Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
private ICollection<IRecordedRoom> _data;
|
||||
private SortedBy sortedBy;
|
||||
|
@ -210,73 +219,73 @@ namespace BililiveRecorder.WPF.Pages
|
|||
{
|
||||
if (data is IList<IRecordedRoom> list)
|
||||
{
|
||||
if (list is INotifyCollectionChanged n) n.CollectionChanged += Data_CollectionChanged;
|
||||
_data = list;
|
||||
if (list is INotifyCollectionChanged n) n.CollectionChanged += this.Data_CollectionChanged;
|
||||
this._data = list;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Type not supported.", nameof(data));
|
||||
}
|
||||
}
|
||||
Sort();
|
||||
this.Sort();
|
||||
}
|
||||
|
||||
private void Data_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => Sort();
|
||||
private void Data_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => this.Sort();
|
||||
|
||||
public ICollection<IRecordedRoom> Data
|
||||
{
|
||||
get => _data;
|
||||
get => this._data;
|
||||
set
|
||||
{
|
||||
if (_data is INotifyCollectionChanged n1) n1.CollectionChanged -= Data_CollectionChanged;
|
||||
if (value is INotifyCollectionChanged n2) n2.CollectionChanged += Data_CollectionChanged;
|
||||
_data = value;
|
||||
Sort();
|
||||
if (this._data is INotifyCollectionChanged n1) n1.CollectionChanged -= this.Data_CollectionChanged;
|
||||
if (value is INotifyCollectionChanged n2) n2.CollectionChanged += this.Data_CollectionChanged;
|
||||
this._data = value;
|
||||
this.Sort();
|
||||
}
|
||||
}
|
||||
|
||||
public SortedBy SortedBy { get => sortedBy; set { sortedBy = value; Sort(); } }
|
||||
public SortedBy SortedBy { get => this.sortedBy; set { this.sortedBy = value; this.Sort(); } }
|
||||
|
||||
public List<IRecordedRoom> Sorted { get; private set; }
|
||||
|
||||
private int sortSeboucneCount = int.MinValue;
|
||||
private SemaphoreSlim sortSemaphoreSlim = new SemaphoreSlim(1, 1);
|
||||
private readonly SemaphoreSlim sortSemaphoreSlim = new SemaphoreSlim(1, 1);
|
||||
|
||||
private async void Sort()
|
||||
{
|
||||
// debounce && lock
|
||||
logger.Debug("Sort called.");
|
||||
var callCount = Interlocked.Increment(ref sortSeboucneCount);
|
||||
var callCount = Interlocked.Increment(ref this.sortSeboucneCount);
|
||||
await Task.Delay(200);
|
||||
if (sortSeboucneCount != callCount)
|
||||
if (this.sortSeboucneCount != callCount)
|
||||
{
|
||||
logger.Debug("Sort cancelled by debounce.");
|
||||
return;
|
||||
}
|
||||
|
||||
await sortSemaphoreSlim.WaitAsync();
|
||||
try { SortImpl(); }
|
||||
finally { sortSemaphoreSlim.Release(); }
|
||||
await this.sortSemaphoreSlim.WaitAsync();
|
||||
try { this.SortImpl(); }
|
||||
finally { this.sortSemaphoreSlim.Release(); }
|
||||
}
|
||||
|
||||
private void SortImpl()
|
||||
{
|
||||
logger.Debug("SortImpl called with {sortedBy} and {count} rooms.", SortedBy, Data?.Count ?? -1);
|
||||
logger.Debug("SortImpl called with {sortedBy} and {count} rooms.", this.SortedBy, this.Data?.Count ?? -1);
|
||||
|
||||
if (Data is null)
|
||||
if (this.Data is null)
|
||||
{
|
||||
Sorted = NullRoom.ToList();
|
||||
this.Sorted = this.NullRoom.ToList();
|
||||
logger.Debug("SortImpl returned NullRoom.");
|
||||
}
|
||||
else
|
||||
{
|
||||
IEnumerable<IRecordedRoom> orderedData = SortedBy switch
|
||||
IEnumerable<IRecordedRoom> orderedData = this.SortedBy switch
|
||||
{
|
||||
SortedBy.RoomId => Data.OrderBy(x => x.ShortRoomId == 0 ? x.RoomId : x.ShortRoomId),
|
||||
SortedBy.Status => Data.OrderByDescending(x => x.IsRecording).ThenByDescending(x => x.IsMonitoring),
|
||||
_ => Data,
|
||||
SortedBy.RoomId => this.Data.OrderBy(x => x.ShortRoomId == 0 ? x.RoomId : x.ShortRoomId),
|
||||
SortedBy.Status => this.Data.OrderByDescending(x => x.IsRecording).ThenByDescending(x => x.IsMonitoring),
|
||||
_ => this.Data,
|
||||
};
|
||||
var result = orderedData.Concat(NullRoom).ToList();
|
||||
var result = orderedData.Concat(this.NullRoom).ToList();
|
||||
logger.Debug("SortImpl sorted with {count} items.", result.Count);
|
||||
|
||||
{ // 崩溃问题信息收集。。虽然不觉得是这里的问题
|
||||
|
@ -310,7 +319,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
}
|
||||
}
|
||||
|
||||
Sorted = result;
|
||||
this.Sorted = result;
|
||||
}
|
||||
|
||||
// Instead of tossing out existing elements and re-creating them,
|
||||
|
@ -319,21 +328,21 @@ namespace BililiveRecorder.WPF.Pages
|
|||
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
|
||||
public IRecordedRoom this[int index] => Sorted != null ? Sorted[index] : throw new IndexOutOfRangeException();
|
||||
public int Count => Sorted != null ? Sorted.Count : 0;
|
||||
public IRecordedRoom this[int index] => this.Sorted != null ? this.Sorted[index] : throw new IndexOutOfRangeException();
|
||||
public int Count => this.Sorted != null ? this.Sorted.Count : 0;
|
||||
|
||||
public bool IsReadOnly => ((IList)Sorted).IsReadOnly;
|
||||
public bool IsReadOnly => ((IList)this.Sorted).IsReadOnly;
|
||||
|
||||
public bool IsFixedSize => ((IList)Sorted).IsFixedSize;
|
||||
public bool IsFixedSize => ((IList)this.Sorted).IsFixedSize;
|
||||
|
||||
public object SyncRoot => ((ICollection)Sorted).SyncRoot;
|
||||
public object SyncRoot => ((ICollection)this.Sorted).SyncRoot;
|
||||
|
||||
public bool IsSynchronized => ((ICollection)Sorted).IsSynchronized;
|
||||
public bool IsSynchronized => ((ICollection)this.Sorted).IsSynchronized;
|
||||
|
||||
object IList.this[int index] { get => ((IList)Sorted)[index]; set => ((IList)Sorted)[index] = value; }
|
||||
object IList.this[int index] { get => ((IList)this.Sorted)[index]; set => ((IList)this.Sorted)[index] = value; }
|
||||
|
||||
public IEnumerator<IRecordedRoom> GetEnumerator() => Sorted.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
public IEnumerator<IRecordedRoom> GetEnumerator() => this.Sorted.GetEnumerator();
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
#region IKeyIndexMapping
|
||||
|
||||
|
@ -353,15 +362,15 @@ namespace BililiveRecorder.WPF.Pages
|
|||
{
|
||||
// We'll try to increase our odds of finding a match sooner by starting from the
|
||||
// position that we know was last requested and search forward.
|
||||
var start = lastRequestedIndex;
|
||||
for (var i = start; i < Count; i++)
|
||||
var start = this.lastRequestedIndex;
|
||||
for (var i = start; i < this.Count; i++)
|
||||
{
|
||||
if ((this[i]?.Guid ?? Guid.Empty).Equals(uniqueId))
|
||||
return i;
|
||||
}
|
||||
|
||||
// Then try searching backward.
|
||||
start = Math.Min(Count - 1, lastRequestedIndex);
|
||||
start = Math.Min(this.Count - 1, this.lastRequestedIndex);
|
||||
for (var i = start; i >= 0; i--)
|
||||
{
|
||||
if ((this[i]?.Guid ?? Guid.Empty).Equals(uniqueId))
|
||||
|
@ -374,48 +383,48 @@ namespace BililiveRecorder.WPF.Pages
|
|||
public string KeyFromIndex(int index)
|
||||
{
|
||||
var key = this[index]?.Guid ?? Guid.Empty;
|
||||
lastRequestedIndex = index;
|
||||
this.lastRequestedIndex = index;
|
||||
return key.ToString();
|
||||
}
|
||||
|
||||
public int Add(object value)
|
||||
{
|
||||
return ((IList)Sorted).Add(value);
|
||||
return ((IList)this.Sorted).Add(value);
|
||||
}
|
||||
|
||||
public bool Contains(object value)
|
||||
{
|
||||
return ((IList)Sorted).Contains(value);
|
||||
return ((IList)this.Sorted).Contains(value);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
((IList)Sorted).Clear();
|
||||
((IList)this.Sorted).Clear();
|
||||
}
|
||||
|
||||
public int IndexOf(object value)
|
||||
{
|
||||
return ((IList)Sorted).IndexOf(value);
|
||||
return ((IList)this.Sorted).IndexOf(value);
|
||||
}
|
||||
|
||||
public void Insert(int index, object value)
|
||||
{
|
||||
((IList)Sorted).Insert(index, value);
|
||||
((IList)this.Sorted).Insert(index, value);
|
||||
}
|
||||
|
||||
public void Remove(object value)
|
||||
{
|
||||
((IList)Sorted).Remove(value);
|
||||
((IList)this.Sorted).Remove(value);
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
((IList)Sorted).RemoveAt(index);
|
||||
((IList)this.Sorted).RemoveAt(index);
|
||||
}
|
||||
|
||||
public void CopyTo(Array array, int index)
|
||||
{
|
||||
((ICollection)Sorted).CopyTo(array, index);
|
||||
((ICollection)this.Sorted).CopyTo(array, index);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
@ -40,38 +40,38 @@ namespace BililiveRecorder.WPF.Pages
|
|||
|
||||
public RootPage()
|
||||
{
|
||||
void AddType(Type t) => PageMap.Add(t.Name, t);
|
||||
void AddType(Type t) => this.PageMap.Add(t.Name, t);
|
||||
AddType(typeof(RoomListPage));
|
||||
AddType(typeof(LogPage));
|
||||
AddType(typeof(SettingsPage));
|
||||
AddType(typeof(AdvancedSettingsPage));
|
||||
AddType(typeof(AnnouncementPage));
|
||||
|
||||
Model = new RootModel();
|
||||
DataContext = Model;
|
||||
this.Model = new RootModel();
|
||||
this.DataContext = this.Model;
|
||||
|
||||
var builder = new ContainerBuilder();
|
||||
builder.RegisterModule<FlvProcessorModule>();
|
||||
builder.RegisterModule<CoreModule>();
|
||||
Container = builder.Build();
|
||||
RootScope = Container.BeginLifetimeScope("recorder_root");
|
||||
this.Container = builder.Build();
|
||||
this.RootScope = this.Container.BeginLifetimeScope("recorder_root");
|
||||
|
||||
InitializeComponent();
|
||||
AdvancedSettingsPageItem.Visibility = Visibility.Hidden;
|
||||
this.InitializeComponent();
|
||||
this.AdvancedSettingsPageItem.Visibility = Visibility.Hidden;
|
||||
|
||||
(Application.Current.MainWindow as NewMainWindow).NativeBeforeWindowClose += RootPage_NativeBeforeWindowClose;
|
||||
Loaded += RootPage_Loaded;
|
||||
(Application.Current.MainWindow as NewMainWindow).NativeBeforeWindowClose += this.RootPage_NativeBeforeWindowClose;
|
||||
Loaded += this.RootPage_Loaded;
|
||||
}
|
||||
|
||||
private void RootPage_NativeBeforeWindowClose(object sender, EventArgs e)
|
||||
{
|
||||
Model.Dispose();
|
||||
this.Model.Dispose();
|
||||
SingleInstance.Cleanup();
|
||||
}
|
||||
|
||||
private async void RootPage_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var recorder = RootScope.Resolve<IRecorder>();
|
||||
var recorder = this.RootScope.Resolve<IRecorder>();
|
||||
var first_time = true;
|
||||
var error = string.Empty;
|
||||
string path;
|
||||
|
@ -113,8 +113,8 @@ namespace BililiveRecorder.WPF.Pages
|
|||
var lastdir = string.Empty;
|
||||
try
|
||||
{
|
||||
if (File.Exists(lastdir_path))
|
||||
lastdir = File.ReadAllText(lastdir_path).Replace("\r", "").Replace("\n", "").Trim();
|
||||
if (File.Exists(this.lastdir_path))
|
||||
lastdir = File.ReadAllText(this.lastdir_path).Replace("\r", "").Replace("\n", "").Trim();
|
||||
}
|
||||
catch (Exception) { }
|
||||
|
||||
|
@ -161,7 +161,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
|
||||
// 如果不是从命令行参数传入的路径,写入 lastdir_path 记录
|
||||
try
|
||||
{ if (string.IsNullOrWhiteSpace(commandLineOption?.WorkDirectory)) File.WriteAllText(lastdir_path, path); }
|
||||
{ if (string.IsNullOrWhiteSpace(commandLineOption?.WorkDirectory)) File.WriteAllText(this.lastdir_path, path); }
|
||||
catch (Exception) { }
|
||||
|
||||
// 检查已经在同目录运行的其他进程
|
||||
|
@ -170,14 +170,14 @@ namespace BililiveRecorder.WPF.Pages
|
|||
// 无已经在同目录运行的进程
|
||||
if (recorder.Initialize(path))
|
||||
{
|
||||
Model.Recorder = recorder;
|
||||
this.Model.Recorder = recorder;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100);
|
||||
_ = Dispatcher.BeginInvoke(DispatcherPriority.Normal, method: new Action(() =>
|
||||
_ = this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, method: new Action(() =>
|
||||
{
|
||||
RoomListPageNavigationViewItem.IsSelected = true;
|
||||
this.RoomListPageNavigationViewItem.IsSelected = true;
|
||||
}));
|
||||
});
|
||||
break;
|
||||
|
@ -206,10 +206,10 @@ namespace BililiveRecorder.WPF.Pages
|
|||
|
||||
private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
|
||||
{
|
||||
SettingsClickCount = 0;
|
||||
this.SettingsClickCount = 0;
|
||||
if (args.IsSettingsSelected)
|
||||
{
|
||||
MainFrame.Navigate(typeof(SettingsPage), null, transitionInfo);
|
||||
this.MainFrame.Navigate(typeof(SettingsPage), null, this.transitionInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -219,26 +219,26 @@ namespace BililiveRecorder.WPF.Pages
|
|||
{
|
||||
try
|
||||
{
|
||||
MainFrame.Navigate(new Uri(selectedItemTag), null, transitionInfo);
|
||||
this.MainFrame.Navigate(new Uri(selectedItemTag), null, this.transitionInfo);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
else if (PageMap.ContainsKey(selectedItemTag))
|
||||
else if (this.PageMap.ContainsKey(selectedItemTag))
|
||||
{
|
||||
var pageType = PageMap[selectedItemTag];
|
||||
MainFrame.Navigate(pageType, null, transitionInfo);
|
||||
var pageType = this.PageMap[selectedItemTag];
|
||||
this.MainFrame.Navigate(pageType, null, this.transitionInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void NavigationViewItem_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (++SettingsClickCount > 3)
|
||||
if (++this.SettingsClickCount > 3)
|
||||
{
|
||||
SettingsClickCount = 0;
|
||||
AdvancedSettingsPageItem.Visibility = AdvancedSettingsPageItem.Visibility != Visibility.Visible ? Visibility.Visible : Visibility.Hidden;
|
||||
this.SettingsClickCount = 0;
|
||||
this.AdvancedSettingsPageItem.Visibility = this.AdvancedSettingsPageItem.Visibility != Visibility.Visible ? Visibility.Visible : Visibility.Hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,12 +5,13 @@
|
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:c="clr-namespace:BililiveRecorder.WPF.Controls"
|
||||
xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages"
|
||||
xmlns:config="clr-namespace:BililiveRecorder.Core.Config;assembly=BililiveRecorder.Core"
|
||||
xmlns:config="clr-namespace:BililiveRecorder.Core.Config.V2;assembly=BililiveRecorder.Core"
|
||||
xmlns:flv="clr-namespace:BililiveRecorder.FlvProcessor;assembly=BililiveRecorder.FlvProcessor"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="1500" d:DesignWidth="500"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:RootPage},Path=DataContext.Recorder.Config}"
|
||||
DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=local:RootPage},Path=DataContext.Recorder.Config.Global}"
|
||||
Title="SettingsPage">
|
||||
<ui:Page.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
|
@ -18,7 +19,7 @@
|
|||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</ui:Page.Resources>
|
||||
<ScrollViewer d:DataContext="{d:DesignInstance Type=config:ConfigV1}">
|
||||
<ScrollViewer d:DataContext="{d:DesignInstance Type=config:GlobalConfig}">
|
||||
<ui:SimpleStackPanel Orientation="Vertical" Spacing="5" Margin="20">
|
||||
<TextBlock Text="设置" Style="{StaticResource TitleTextBlockStyle}" FontFamily="Microsoft Yahei" Margin="0,10"/>
|
||||
<GroupBox Header="弹幕录制">
|
||||
|
@ -86,7 +87,7 @@
|
|||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="文件名">
|
||||
<StackPanel MaxWidth="400" HorizontalAlignment="Left">
|
||||
<StackPanel MaxWidth="500" HorizontalAlignment="Left">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||
<StackPanel.ToolTip>
|
||||
<TextBlock FontSize="13">
|
||||
|
@ -106,11 +107,13 @@
|
|||
<TextBlock Text="说明"/>
|
||||
<ui:PathIcon Margin="2,0" VerticalAlignment="Center" Height="15" Style="{StaticResource PathIconDataInformationOutline}"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="录制文件名格式"/>
|
||||
<TextBox Text="{Binding RecordFilenameFormat,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
||||
<TextBlock Text="剪辑文件名格式" Margin="0,5,0,0" Visibility="{Binding ElementName=EnabledFeatureRecordOnlyRadioButton,Path=IsChecked,Converter={StaticResource InvertBooleanToVisibilityCollapsedConverter}}"/>
|
||||
<TextBox Text="{Binding ClipFilenameFormat,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"
|
||||
Visibility="{Binding ElementName=EnabledFeatureRecordOnlyRadioButton,Path=IsChecked,Converter={StaticResource InvertBooleanToVisibilityCollapsedConverter}}"/>
|
||||
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasRecordFilenameFormat}" Header="录制文件名格式">
|
||||
<TextBox Text="{Binding RecordFilenameFormat,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
||||
</c:SettingWithDefault>
|
||||
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasClipFilenameFormat}" Header="剪辑文件名格式" Margin="0,5,0,0"
|
||||
Visibility="{Binding ElementName=EnabledFeatureRecordOnlyRadioButton,Path=IsChecked,Converter={StaticResource InvertBooleanToVisibilityCollapsedConverter}}">
|
||||
<TextBox Text="{Binding ClipFilenameFormat,Delay=500}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
|
||||
</c:SettingWithDefault>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="Webhook">
|
||||
|
|
|
@ -1,18 +1,3 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace BililiveRecorder.WPF.Pages
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -22,7 +7,7 @@ namespace BililiveRecorder.WPF.Pages
|
|||
{
|
||||
public SettingsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
|
||||
|
|
|
@ -19,6 +19,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BililiveRecorder.UnitTest.Core", "test\BililiveRecorder.UnitTest.Core\BililiveRecorder.UnitTest.Core.csproj", "{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -41,10 +45,17 @@ Global
|
|||
{1B626335-283F-4313-9045-B5B96FAAB2DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1B626335-283F-4313-9045-B5B96FAAB2DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1B626335-283F-4313-9045-B5B96FAAB2DF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {F3CB8B14-077A-458F-BD8E-1747ED0F5170}
|
||||
EndGlobalSection
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
||||
<PackageReference Include="xunit" Version="2.4.1" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="1.3.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\BililiveRecorder.Core\BililiveRecorder.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
80
test/BililiveRecorder.UnitTest.Core/ConfigTests.cs
Normal file
80
test/BililiveRecorder.UnitTest.Core/ConfigTests.cs
Normal file
|
@ -0,0 +1,80 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.Core.Config.V1;
|
||||
using BililiveRecorder.Core.Config.V2;
|
||||
using Newtonsoft.Json;
|
||||
using Xunit;
|
||||
|
||||
namespace BililiveRecorder.UnitTest.Core
|
||||
{
|
||||
public class ConfigTests
|
||||
{
|
||||
private const string V2TestString1 = "{\"version\":2,\"global\":{\"EnabledFeature\":{\"HasValue\":false,\"Value\":0},\"ClipLengthPast\":{\"HasValue\":false,\"Value\":0},\"ClipLengthFuture\":{\"HasValue\":false,\"Value\":0},\"TimingStreamRetry\":{\"HasValue\":false,\"Value\":0},\"TimingStreamConnect\":{\"HasValue\":false,\"Value\":0},\"TimingDanmakuRetry\":{\"HasValue\":false,\"Value\":0},\"TimingCheckInterval\":{\"HasValue\":false,\"Value\":0},\"TimingWatchdogTimeout\":{\"HasValue\":false,\"Value\":0},\"RecordDanmakuFlushInterval\":{\"HasValue\":false,\"Value\":0},\"Cookie\":{\"HasValue\":false,\"Value\":null},\"WebHookUrls\":{\"HasValue\":false,\"Value\":null},\"LiveApiHost\":{\"HasValue\":false,\"Value\":null},\"RecordFilenameFormat\":{\"HasValue\":false,\"Value\":null},\"ClipFilenameFormat\":{\"HasValue\":false,\"Value\":null},\"CuttingMode\":{\"HasValue\":false,\"Value\":0},\"CuttingNumber\":{\"HasValue\":false,\"Value\":0},\"RecordDanmaku\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuRaw\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuSuperChat\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuGift\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuGuard\":{\"HasValue\":false,\"Value\":false}},\"rooms\":[{\"RoomId\":{\"HasValue\":true,\"Value\":1},\"AutoRecord\":{\"HasValue\":false,\"Value\":false},\"CuttingMode\":{\"HasValue\":false,\"Value\":0},\"CuttingNumber\":{\"HasValue\":false,\"Value\":0},\"RecordDanmaku\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuRaw\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuSuperChat\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuGift\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuGuard\":{\"HasValue\":false,\"Value\":false}},{\"RoomId\":{\"HasValue\":true,\"Value\":2},\"AutoRecord\":{\"HasValue\":false,\"Value\":false},\"CuttingMode\":{\"HasValue\":false,\"Value\":0},\"CuttingNumber\":{\"HasValue\":false,\"Value\":0},\"RecordDanmaku\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuRaw\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuSuperChat\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuGift\":{\"HasValue\":false,\"Value\":false},\"RecordDanmakuGuard\":{\"HasValue\":false,\"Value\":false}}]}";
|
||||
|
||||
private static readonly List<object[]> V1 = new()
|
||||
{
|
||||
new object[] { "{\"version\":1}", 1 },
|
||||
new object[] { "{\"version\":1,\"data\":\"\"}", 1 },
|
||||
};
|
||||
|
||||
private static readonly List<object[]> V2 = new()
|
||||
{
|
||||
new object[] { "{\"version\":2}", 2 },
|
||||
new object[] { "{\"version\":2,\"data\":{}}", 2 },
|
||||
new object[] { V2TestString1, 2 },
|
||||
};
|
||||
|
||||
public static IEnumerable<object[]> GetTestData(int version)
|
||||
=> version switch
|
||||
{
|
||||
0 => V1.Concat(V2).AsEnumerable(),
|
||||
1 => V1.AsEnumerable(),
|
||||
2 => V2.AsEnumerable(),
|
||||
_ => throw new ArgumentException()
|
||||
};
|
||||
|
||||
[Theory, MemberData(nameof(GetTestData), 0)]
|
||||
public void Parse(string data, int ver)
|
||||
{
|
||||
var result = JsonConvert.DeserializeObject<ConfigBase>(data);
|
||||
|
||||
var type = ver switch
|
||||
{
|
||||
1 => typeof(ConfigV1Wrapper),
|
||||
2 => typeof(ConfigV2),
|
||||
_ => throw new Exception("not supported")
|
||||
};
|
||||
|
||||
Assert.Equal(type, result.GetType());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void V2Test1()
|
||||
{
|
||||
var obj = JsonConvert.DeserializeObject<ConfigBase>(V2TestString1);
|
||||
|
||||
var v2 = Assert.IsType<ConfigV2>(obj);
|
||||
Assert.Equal(2, v2.Rooms.Count);
|
||||
Assert.Equal(1, v2.Rooms[0].RoomId);
|
||||
Assert.Equal(2, v2.Rooms[1].RoomId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Save()
|
||||
{
|
||||
ConfigBase config = new ConfigV2()
|
||||
{
|
||||
Rooms = new List<RoomConfig>
|
||||
{
|
||||
new RoomConfig { RoomId = 1 },
|
||||
new RoomConfig { RoomId = 2 }
|
||||
},
|
||||
Global = new GlobalConfig()
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(config);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user