From 9c7e99944ad7d9230532497d578ffd499838484f Mon Sep 17 00:00:00 2001 From: genteure Date: Thu, 25 Aug 2022 18:42:36 +0800 Subject: [PATCH] feat: add support for websocket danmaku protocol --- .../Configure/ConfigInstructions.gen.cs | 2 + .../Api/Danmaku/DanmakuClient.cs | 85 ++++++++++--------- .../DanmakuTransportSecureWebSocket.cs | 7 ++ .../Api/Danmaku/DanmakuTransportTcp.cs | 43 ++++++++++ .../Api/Danmaku/DanmakuTransportWebSocket.cs | 46 ++++++++++ .../Api/Danmaku/IDanmakuTransport.cs | 13 +++ .../Api/Http/HttpApiClient.cs | 8 +- BililiveRecorder.Core/Api/IDanmakuClient.cs | 3 +- BililiveRecorder.Core/Api/Model/DanmuInfo.cs | 6 ++ BililiveRecorder.Core/Api/ModelExtensions.cs | 43 +++++++--- .../Config/DanmakuTransportMode.cs | 10 +++ BililiveRecorder.Core/Config/V3/Config.gen.cs | 15 ++++ BililiveRecorder.Core/Room.cs | 6 +- .../Pages/AdvancedSettingsPage.xaml | 18 ++++ BililiveRecorder.Web/Models/Config.gen.cs | 6 ++ configV3.schema.json | 23 +++++ config_gen/data.ts | 8 ++ config_gen/generators/codeSchema.ts | 2 + config_gen/types.ts | 1 + 19 files changed, 287 insertions(+), 58 deletions(-) create mode 100644 BililiveRecorder.Core/Api/Danmaku/DanmakuTransportSecureWebSocket.cs create mode 100644 BililiveRecorder.Core/Api/Danmaku/DanmakuTransportTcp.cs create mode 100644 BililiveRecorder.Core/Api/Danmaku/DanmakuTransportWebSocket.cs create mode 100644 BililiveRecorder.Core/Api/Danmaku/IDanmakuTransport.cs create mode 100644 BililiveRecorder.Core/Config/DanmakuTransportMode.cs diff --git a/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs b/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs index e1d07a2..adbb680 100644 --- a/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs +++ b/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs @@ -37,6 +37,7 @@ namespace BililiveRecorder.Cli.Configure TimingDanmakuRetry, TimingWatchdogTimeout, RecordDanmakuFlushInterval, + DanmakuTransport, NetworkTransportUseSystemProxy, NetworkTransportAllowedAddressFamily, UserScript @@ -88,6 +89,7 @@ namespace BililiveRecorder.Cli.Configure GlobalConfig.Add(GlobalConfigProperties.TimingDanmakuRetry, new ConfigInstruction(config => config.HasTimingDanmakuRetry = false, (config, value) => config.TimingDanmakuRetry = value) { Name = "TimingDanmakuRetry", CanBeOptional = true }); GlobalConfig.Add(GlobalConfigProperties.TimingWatchdogTimeout, new ConfigInstruction(config => config.HasTimingWatchdogTimeout = false, (config, value) => config.TimingWatchdogTimeout = value) { Name = "TimingWatchdogTimeout", CanBeOptional = true }); GlobalConfig.Add(GlobalConfigProperties.RecordDanmakuFlushInterval, new ConfigInstruction(config => config.HasRecordDanmakuFlushInterval = false, (config, value) => config.RecordDanmakuFlushInterval = value) { Name = "RecordDanmakuFlushInterval", CanBeOptional = true }); + GlobalConfig.Add(GlobalConfigProperties.DanmakuTransport, new ConfigInstruction(config => config.HasDanmakuTransport = false, (config, value) => config.DanmakuTransport = value) { Name = "DanmakuTransport", CanBeOptional = true }); GlobalConfig.Add(GlobalConfigProperties.NetworkTransportUseSystemProxy, new ConfigInstruction(config => config.HasNetworkTransportUseSystemProxy = false, (config, value) => config.NetworkTransportUseSystemProxy = value) { Name = "NetworkTransportUseSystemProxy", CanBeOptional = true }); GlobalConfig.Add(GlobalConfigProperties.NetworkTransportAllowedAddressFamily, new ConfigInstruction(config => config.HasNetworkTransportAllowedAddressFamily = false, (config, value) => config.NetworkTransportAllowedAddressFamily = value) { Name = "NetworkTransportAllowedAddressFamily", CanBeOptional = true }); GlobalConfig.Add(GlobalConfigProperties.UserScript, new ConfigInstruction(config => config.HasUserScript = false, (config, value) => config.UserScript = value) { Name = "UserScript", CanBeOptional = true }); diff --git a/BililiveRecorder.Core/Api/Danmaku/DanmakuClient.cs b/BililiveRecorder.Core/Api/Danmaku/DanmakuClient.cs index 718278a..4d403f3 100644 --- a/BililiveRecorder.Core/Api/Danmaku/DanmakuClient.cs +++ b/BililiveRecorder.Core/Api/Danmaku/DanmakuClient.cs @@ -1,15 +1,14 @@ using System; using System.Buffers; using System.Buffers.Binary; -using System.IO; using System.IO.Compression; using System.IO.Pipelines; using System.Net; -using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Timers; +using BililiveRecorder.Core.Config; using Nerdbank.Streams; using Newtonsoft.Json; using Serilog; @@ -24,10 +23,10 @@ namespace BililiveRecorder.Core.Api.Danmaku private readonly Timer timer; private readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1); - private Stream? danmakuStream; + private IDanmakuTransport? danmakuTransport; private bool disposedValue; - public bool Connected => this.danmakuStream != null; + public bool Connected => this.danmakuTransport != null; public event EventHandler? StatusChanged; public event EventHandler? DanmakuReceived; @@ -50,8 +49,8 @@ namespace BililiveRecorder.Core.Api.Danmaku await this.semaphoreSlim.WaitAsync().ConfigureAwait(false); try { - this.danmakuStream?.Dispose(); - this.danmakuStream = null; + this.danmakuTransport?.Dispose(); + this.danmakuTransport = null; this.timer.Stop(); } @@ -63,40 +62,47 @@ namespace BililiveRecorder.Core.Api.Danmaku StatusChanged?.Invoke(this, StatusChangedEventArgs.False); } - public async Task ConnectAsync(int roomid, CancellationToken cancellationToken) + public async Task ConnectAsync(int roomid, DanmakuTransportMode transportMode, CancellationToken cancellationToken) { if (this.disposedValue) throw new ObjectDisposedException(nameof(DanmakuClient)); + if (!Enum.IsDefined(typeof(DanmakuTransportMode), transportMode)) + throw new ArgumentOutOfRangeException(nameof(transportMode), transportMode, "Invalid danmaku transport mode."); + await this.semaphoreSlim.WaitAsync().ConfigureAwait(false); try { - if (this.danmakuStream != null) + if (this.danmakuTransport != null) return; var serverInfo = await this.apiClient.GetDanmakuServerAsync(roomid).ConfigureAwait(false); if (serverInfo.Data is null) return; - serverInfo.Data.ChooseOne(out var host, out var port, out var token); - this.logger.Debug("Connecting to {Host}:{Port} with a {TokenLength} char long token for room {RoomId}", host, port, token?.Length, roomid); + var danmakuServerInfo = serverInfo.Data.SelectDanmakuServer(transportMode); - if (cancellationToken.IsCancellationRequested) - return; + this.logger.Debug("连接弹幕服务器 {Mode} {Host}:{Port} 房间: {RoomId} TokenLength: {TokenLength}", danmakuServerInfo.TransportMode, danmakuServerInfo.Host, danmakuServerInfo.Port, roomid, danmakuServerInfo.Token?.Length); - var tcp = new TcpClient(); - await tcp.ConnectAsync(host, port).ConfigureAwait(false); + IDanmakuTransport transport = danmakuServerInfo.TransportMode switch + { + DanmakuTransportMode.Tcp => new DanmakuTransportTcp(), + DanmakuTransportMode.Ws => new DanmakuTransportWebSocket(), + DanmakuTransportMode.Wss => new DanmakuTransportSecureWebSocket(), + _ => throw new ArgumentOutOfRangeException(nameof(transportMode), transportMode, "Invalid danmaku transport mode."), + }; - this.danmakuStream = tcp.GetStream(); + var reader = await transport.ConnectAsync(danmakuServerInfo.Host, danmakuServerInfo.Port, cancellationToken).ConfigureAwait(false); - await SendHelloAsync(this.danmakuStream, roomid, token!).ConfigureAwait(false); - await SendPingAsync(this.danmakuStream); + this.danmakuTransport = transport; + + await this.SendHelloAsync(roomid, danmakuServerInfo.Token ?? string.Empty).ConfigureAwait(false); + await this.SendPingAsync().ConfigureAwait(false); if (cancellationToken.IsCancellationRequested) { - tcp.Dispose(); - this.danmakuStream.Dispose(); - this.danmakuStream = null; + this.danmakuTransport.Dispose(); + this.danmakuTransport = null; return; } @@ -106,7 +112,7 @@ namespace BililiveRecorder.Core.Api.Danmaku { try { - await ProcessDataAsync(this.danmakuStream, this.ProcessCommand).ConfigureAwait(false); + await ProcessDataAsync(reader, this.ProcessCommand).ConfigureAwait(false); } catch (ObjectDisposedException) { } catch (Exception ex) @@ -151,10 +157,10 @@ namespace BililiveRecorder.Core.Api.Danmaku await this.semaphoreSlim.WaitAsync().ConfigureAwait(false); try { - if (this.danmakuStream is null) + if (this.danmakuTransport is null) return; - await SendPingAsync(this.danmakuStream).ConfigureAwait(false); + await this.SendPingAsync().ConfigureAwait(false); } finally { @@ -177,7 +183,7 @@ namespace BililiveRecorder.Core.Api.Danmaku { // dispose managed state (managed objects) this.timer.Dispose(); - this.danmakuStream?.Dispose(); + this.danmakuTransport?.Dispose(); this.semaphoreSlim.Dispose(); } @@ -205,42 +211,41 @@ namespace BililiveRecorder.Core.Api.Danmaku #region Send - private static Task SendHelloAsync(Stream stream, int roomid, string token) => - SendMessageAsync(stream, 7, JsonConvert.SerializeObject(new + private Task SendHelloAsync(int roomid, string token) => + this.SendMessageAsync(7, JsonConvert.SerializeObject(new { uid = 0, roomid = roomid, protover = 0, platform = "web", - clientver = "2.6.25", type = 2, key = token, }, Formatting.None)); - private static Task SendPingAsync(Stream stream) => - SendMessageAsync(stream, 2); + private Task SendPingAsync() => this.SendMessageAsync(2); - private static async Task SendMessageAsync(Stream stream, int action, string body = "") + private async Task SendMessageAsync(int action, string body = "") { - if (stream is null) - throw new ArgumentNullException(nameof(stream)); + if (this.danmakuTransport is not { } transport) + return; var playload = ((body?.Length ?? 0) > 0) ? Encoding.UTF8.GetBytes(body) : Array.Empty(); const int headerLength = 16; - var size = playload.Length + headerLength; - var buffer = ArrayPool.Shared.Rent(headerLength); + var totalLength = playload.Length + headerLength; + + var buffer = ArrayPool.Shared.Rent(totalLength); try { - BinaryPrimitives.WriteUInt32BigEndian(new Span(buffer, 0, 4), (uint)size); + BinaryPrimitives.WriteUInt32BigEndian(new Span(buffer, 0, 4), (uint)totalLength); BinaryPrimitives.WriteUInt16BigEndian(new Span(buffer, 4, 2), headerLength); BinaryPrimitives.WriteUInt16BigEndian(new Span(buffer, 6, 2), 1); BinaryPrimitives.WriteUInt32BigEndian(new Span(buffer, 8, 4), (uint)action); BinaryPrimitives.WriteUInt32BigEndian(new Span(buffer, 12, 4), 1); - await stream.WriteAsync(buffer, 0, headerLength).ConfigureAwait(false); if (playload.Length > 0) - await stream.WriteAsync(playload, 0, playload.Length).ConfigureAwait(false); - await stream.FlushAsync().ConfigureAwait(false); + Array.Copy(playload, 0, buffer, headerLength, playload.Length); + + await transport.SendAsync(buffer, 0, totalLength).ConfigureAwait(false); } finally { @@ -252,10 +257,8 @@ namespace BililiveRecorder.Core.Api.Danmaku #region Receive - private static async Task ProcessDataAsync(Stream stream, Action callback) + private static async Task ProcessDataAsync(PipeReader reader, Action callback) { - var reader = PipeReader.Create(stream); - while (true) { var result = await reader.ReadAsync(); diff --git a/BililiveRecorder.Core/Api/Danmaku/DanmakuTransportSecureWebSocket.cs b/BililiveRecorder.Core/Api/Danmaku/DanmakuTransportSecureWebSocket.cs new file mode 100644 index 0000000..5711b3d --- /dev/null +++ b/BililiveRecorder.Core/Api/Danmaku/DanmakuTransportSecureWebSocket.cs @@ -0,0 +1,7 @@ +namespace BililiveRecorder.Core.Api.Danmaku +{ + internal class DanmakuTransportSecureWebSocket : DanmakuTransportWebSocket + { + protected override string Scheme => "wss"; + } +} diff --git a/BililiveRecorder.Core/Api/Danmaku/DanmakuTransportTcp.cs b/BililiveRecorder.Core/Api/Danmaku/DanmakuTransportTcp.cs new file mode 100644 index 0000000..d942dfe --- /dev/null +++ b/BililiveRecorder.Core/Api/Danmaku/DanmakuTransportTcp.cs @@ -0,0 +1,43 @@ +using System; +using System.IO; +using System.IO.Pipelines; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Nerdbank.Streams; + +namespace BililiveRecorder.Core.Api.Danmaku +{ + internal class DanmakuTransportTcp : IDanmakuTransport + { + private Stream? stream; + + public DanmakuTransportTcp() + { + } + + public async Task ConnectAsync(string host, int port, CancellationToken cancellationToken) + { + if (this.stream is not null) + throw new InvalidOperationException("Tcp socket is connected."); + + var tcp = new TcpClient(); + await tcp.ConnectAsync(host, port).ConfigureAwait(false); + + var networkStream = tcp.GetStream(); + this.stream = networkStream; + return networkStream.UsePipeReader(); + } + + public void Dispose() => this.stream?.Dispose(); + + public async Task SendAsync(byte[] buffer, int offset, int count) + { + if (this.stream is not { } s) + return; + + await s.WriteAsync(buffer, offset, count).ConfigureAwait(false); + await s.FlushAsync().ConfigureAwait(false); + } + } +} diff --git a/BililiveRecorder.Core/Api/Danmaku/DanmakuTransportWebSocket.cs b/BililiveRecorder.Core/Api/Danmaku/DanmakuTransportWebSocket.cs new file mode 100644 index 0000000..b27dff2 --- /dev/null +++ b/BililiveRecorder.Core/Api/Danmaku/DanmakuTransportWebSocket.cs @@ -0,0 +1,46 @@ +using System; +using System.IO.Pipelines; +using System.Net.WebSockets; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using BililiveRecorder.Core.Api.Http; +using Nerdbank.Streams; + +namespace BililiveRecorder.Core.Api.Danmaku +{ + internal class DanmakuTransportWebSocket : IDanmakuTransport + { + private static readonly bool isDotNetFramework = RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework", StringComparison.Ordinal); + + private readonly ClientWebSocket socket; + + protected virtual string Scheme => "ws"; + + public DanmakuTransportWebSocket() + { + this.socket = new ClientWebSocket(); + this.socket.Options.UseDefaultCredentials = false; + this.socket.Options.Credentials = null; + this.socket.Options.Proxy = null; + this.socket.Options.Cookies = null; + + this.socket.Options.SetRequestHeader("Origin", HttpApiClient.HttpHeaderOrigin); + + if (!isDotNetFramework) + this.socket.Options.SetRequestHeader("User-Agent", HttpApiClient.HttpHeaderUserAgent); + } + + public async Task ConnectAsync(string host, int port, CancellationToken cancellationToken) + { + var b = new UriBuilder(this.Scheme, host, port, "/sub"); + await this.socket.ConnectAsync(b.Uri, cancellationToken).ConfigureAwait(false); + return this.socket.UsePipeReader(); + } + + public async Task SendAsync(byte[] buffer, int offset, int count) + => await this.socket.SendAsync(new ArraySegment(buffer, offset, count), WebSocketMessageType.Binary, true, default).ConfigureAwait(false); + + public void Dispose() => this.socket.Dispose(); + } +} diff --git a/BililiveRecorder.Core/Api/Danmaku/IDanmakuTransport.cs b/BililiveRecorder.Core/Api/Danmaku/IDanmakuTransport.cs new file mode 100644 index 0000000..e0c7ff1 --- /dev/null +++ b/BililiveRecorder.Core/Api/Danmaku/IDanmakuTransport.cs @@ -0,0 +1,13 @@ +using System; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; + +namespace BililiveRecorder.Core.Api.Danmaku +{ + internal interface IDanmakuTransport : IDisposable + { + Task ConnectAsync(string host, int port, CancellationToken cancellationToken); + Task SendAsync(byte[] buffer, int offset, int count); + } +} diff --git a/BililiveRecorder.Core/Api/Http/HttpApiClient.cs b/BililiveRecorder.Core/Api/Http/HttpApiClient.cs index e49de9a..c56847f 100644 --- a/BililiveRecorder.Core/Api/Http/HttpApiClient.cs +++ b/BililiveRecorder.Core/Api/Http/HttpApiClient.cs @@ -13,10 +13,10 @@ namespace BililiveRecorder.Core.Api.Http { internal class HttpApiClient : IApiClient, IDanmakuServerApiClient, IHttpClientAccessor { - private const string HttpHeaderAccept = "application/json, text/javascript, */*; q=0.01"; - private const string HttpHeaderOrigin = "https://live.bilibili.com"; - private const string HttpHeaderReferer = "https://live.bilibili.com/"; - private const string HttpHeaderUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36"; + internal const string HttpHeaderAccept = "application/json, text/javascript, */*; q=0.01"; + internal const string HttpHeaderReferer = "https://live.bilibili.com/"; + internal const string HttpHeaderOrigin = "https://live.bilibili.com"; + internal const string HttpHeaderUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"; private static readonly TimeSpan TimeOutTimeSpan = TimeSpan.FromSeconds(15); private readonly GlobalConfig config; diff --git a/BililiveRecorder.Core/Api/IDanmakuClient.cs b/BililiveRecorder.Core/Api/IDanmakuClient.cs index 31546f2..e5e9020 100644 --- a/BililiveRecorder.Core/Api/IDanmakuClient.cs +++ b/BililiveRecorder.Core/Api/IDanmakuClient.cs @@ -2,6 +2,7 @@ using System; using System.Threading; using System.Threading.Tasks; using BililiveRecorder.Core.Api.Danmaku; +using BililiveRecorder.Core.Config; namespace BililiveRecorder.Core.Api { @@ -12,7 +13,7 @@ namespace BililiveRecorder.Core.Api event EventHandler? StatusChanged; event EventHandler? DanmakuReceived; - Task ConnectAsync(int roomid, CancellationToken cancellationToken); + Task ConnectAsync(int roomid, DanmakuTransportMode transportMode, CancellationToken cancellationToken); Task DisconnectAsync(); } } diff --git a/BililiveRecorder.Core/Api/Model/DanmuInfo.cs b/BililiveRecorder.Core/Api/Model/DanmuInfo.cs index 25b0c79..85dca35 100644 --- a/BililiveRecorder.Core/Api/Model/DanmuInfo.cs +++ b/BililiveRecorder.Core/Api/Model/DanmuInfo.cs @@ -18,6 +18,12 @@ namespace BililiveRecorder.Core.Api.Model [JsonProperty("port")] public int Port { get; set; } + + [JsonProperty("ws_port")] + public int WsPort { get; set; } + + [JsonProperty("wss_port")] + public int WssPort { get; set; } } } } diff --git a/BililiveRecorder.Core/Api/ModelExtensions.cs b/BililiveRecorder.Core/Api/ModelExtensions.cs index 2f6bd4e..764422c 100644 --- a/BililiveRecorder.Core/Api/ModelExtensions.cs +++ b/BililiveRecorder.Core/Api/ModelExtensions.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Linq; using BililiveRecorder.Core.Api.Model; +using BililiveRecorder.Core.Config; namespace BililiveRecorder.Core.Api { @@ -8,25 +10,46 @@ namespace BililiveRecorder.Core.Api { private static readonly Random random = new Random(); - public static void ChooseOne(this DanmuInfo danmuInfo, out string host, out int port, out string token) + private const string DefaultServerHost = "broadcastlv.chat.bilibili.com"; + + private static readonly DanmakuServerInfo[] DefaultServers = new[] { + new DanmakuServerInfo { TransportMode = DanmakuTransportMode.Tcp, Host = DefaultServerHost, Port = 2243 }, + new DanmakuServerInfo { TransportMode = DanmakuTransportMode.Ws, Host = DefaultServerHost, Port = 2244 }, + new DanmakuServerInfo { TransportMode = DanmakuTransportMode.Wss, Host = DefaultServerHost, Port = 443 }, + }; + + public static DanmakuServerInfo SelectDanmakuServer(this DanmuInfo danmuInfo, DanmakuTransportMode transportMode) { - const string DefaultServerHost = "broadcastlv.chat.bilibili.com"; - const int DefaultServerPort = 2243; + static IEnumerable SelectServerInfo(DanmuInfo.HostListItem x) + { + yield return new DanmakuServerInfo { TransportMode = DanmakuTransportMode.Tcp, Host = x.Host, Port = x.Port }; + yield return new DanmakuServerInfo { TransportMode = DanmakuTransportMode.Ws, Host = x.Host, Port = x.WsPort }; + yield return new DanmakuServerInfo { TransportMode = DanmakuTransportMode.Wss, Host = x.Host, Port = x.WssPort }; + } - token = danmuInfo.Token; - - var list = danmuInfo.HostList.Where(x => !string.IsNullOrWhiteSpace(x.Host) && x.Host != DefaultServerHost && x.Port > 0).ToArray(); + var list = danmuInfo.HostList.Where(x => !string.IsNullOrWhiteSpace(x.Host) && x.Host != DefaultServerHost) + .SelectMany(SelectServerInfo) + .Where(x => x.Port > 0) + .Where(x => transportMode == DanmakuTransportMode.Random || transportMode == x.TransportMode) + .ToArray(); if (list.Length > 0) { var result = list[random.Next(list.Length)]; - host = result.Host; - port = result.Port; + result.Token = danmuInfo.Token; + return result; } else { - host = DefaultServerHost; - port = DefaultServerPort; + return DefaultServers[random.Next(DefaultServers.Length)]; } } + + internal struct DanmakuServerInfo + { + internal DanmakuTransportMode TransportMode; + internal string Host; + internal int Port; + internal string Token; + } } } diff --git a/BililiveRecorder.Core/Config/DanmakuTransportMode.cs b/BililiveRecorder.Core/Config/DanmakuTransportMode.cs new file mode 100644 index 0000000..04c6276 --- /dev/null +++ b/BililiveRecorder.Core/Config/DanmakuTransportMode.cs @@ -0,0 +1,10 @@ +namespace BililiveRecorder.Core.Config +{ + public enum DanmakuTransportMode + { + Random = 0, + Tcp = 1, + Ws = 2, + Wss = 3, + } +} diff --git a/BililiveRecorder.Core/Config/V3/Config.gen.cs b/BililiveRecorder.Core/Config/V3/Config.gen.cs index d178df6..91629e2 100644 --- a/BililiveRecorder.Core/Config/V3/Config.gen.cs +++ b/BililiveRecorder.Core/Config/V3/Config.gen.cs @@ -174,6 +174,11 @@ namespace BililiveRecorder.Core.Config.V3 /// public uint RecordDanmakuFlushInterval => this.GetPropertyValue(); + /// + /// 使用的弹幕服务器传输协议 + /// + public DanmakuTransportMode DanmakuTransport => this.GetPropertyValue(); + /// /// 是否使用系统代理 /// @@ -378,6 +383,14 @@ namespace BililiveRecorder.Core.Config.V3 [JsonProperty(nameof(RecordDanmakuFlushInterval)), EditorBrowsable(EditorBrowsableState.Never)] public Optional OptionalRecordDanmakuFlushInterval { get => this.GetPropertyValueOptional(nameof(this.RecordDanmakuFlushInterval)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuFlushInterval)); } + /// + /// 使用的弹幕服务器传输协议 + /// + public DanmakuTransportMode DanmakuTransport { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasDanmakuTransport { get => this.GetPropertyHasValue(nameof(this.DanmakuTransport)); set => this.SetPropertyHasValue(value, nameof(this.DanmakuTransport)); } + [JsonProperty(nameof(DanmakuTransport)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalDanmakuTransport { get => this.GetPropertyValueOptional(nameof(this.DanmakuTransport)); set => this.SetPropertyValueOptional(value, nameof(this.DanmakuTransport)); } + /// /// 是否使用系统代理 /// @@ -455,6 +468,8 @@ namespace BililiveRecorder.Core.Config.V3 public uint RecordDanmakuFlushInterval => 20; + public DanmakuTransportMode DanmakuTransport => DanmakuTransportMode.Random; + public bool NetworkTransportUseSystemProxy => false; public AllowedAddressFamily NetworkTransportAllowedAddressFamily => AllowedAddressFamily.Any; diff --git a/BililiveRecorder.Core/Room.cs b/BililiveRecorder.Core/Room.cs index 7654f11..46a8a0a 100644 --- a/BililiveRecorder.Core/Room.cs +++ b/BililiveRecorder.Core/Room.cs @@ -325,6 +325,7 @@ namespace BililiveRecorder.Core try { if (delay) + { try { await Task.Delay((int)this.RoomConfig.TimingDanmakuRetry, this.ct).ConfigureAwait(false); @@ -334,15 +335,16 @@ namespace BililiveRecorder.Core // 房间已被删除 return; } + } - await this.danmakuClient.ConnectAsync(this.RoomConfig.RoomId, this.ct).ConfigureAwait(false); + await this.danmakuClient.ConnectAsync(this.RoomConfig.RoomId, this.RoomConfig.DanmakuTransport, this.ct).ConfigureAwait(false); } catch (Exception ex) { this.logger.Write(ex is ExecutionRejectedException ? LogEventLevel.Verbose : LogEventLevel.Warning, ex, "连接弹幕服务器时出错"); if (!this.ct.IsCancellationRequested) - this.StartDamakuConnection(); + this.StartDamakuConnection(delay: true); } }); diff --git a/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml b/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml index 6ee1814..a4f30b1 100644 --- a/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml +++ b/BililiveRecorder.WPF/Pages/AdvancedSettingsPage.xaml @@ -57,6 +57,24 @@ + + + + + + + + + + diff --git a/BililiveRecorder.Web/Models/Config.gen.cs b/BililiveRecorder.Web/Models/Config.gen.cs index 1e8f174..fb94a71 100644 --- a/BililiveRecorder.Web/Models/Config.gen.cs +++ b/BililiveRecorder.Web/Models/Config.gen.cs @@ -65,6 +65,7 @@ namespace BililiveRecorder.Web.Models public Optional? OptionalTimingDanmakuRetry { get; set; } public Optional? OptionalTimingWatchdogTimeout { get; set; } public Optional? OptionalRecordDanmakuFlushInterval { get; set; } + public Optional? OptionalDanmakuTransport { get; set; } public Optional? OptionalNetworkTransportUseSystemProxy { get; set; } public Optional? OptionalNetworkTransportAllowedAddressFamily { get; set; } public Optional? OptionalUserScript { get; set; } @@ -94,6 +95,7 @@ namespace BililiveRecorder.Web.Models if (this.OptionalTimingDanmakuRetry.HasValue) config.OptionalTimingDanmakuRetry = this.OptionalTimingDanmakuRetry.Value; if (this.OptionalTimingWatchdogTimeout.HasValue) config.OptionalTimingWatchdogTimeout = this.OptionalTimingWatchdogTimeout.Value; if (this.OptionalRecordDanmakuFlushInterval.HasValue) config.OptionalRecordDanmakuFlushInterval = this.OptionalRecordDanmakuFlushInterval.Value; + if (this.OptionalDanmakuTransport.HasValue) config.OptionalDanmakuTransport = this.OptionalDanmakuTransport.Value; if (this.OptionalNetworkTransportUseSystemProxy.HasValue) config.OptionalNetworkTransportUseSystemProxy = this.OptionalNetworkTransportUseSystemProxy.Value; if (this.OptionalNetworkTransportAllowedAddressFamily.HasValue) config.OptionalNetworkTransportAllowedAddressFamily = this.OptionalNetworkTransportAllowedAddressFamily.Value; if (this.OptionalUserScript.HasValue) config.OptionalUserScript = this.OptionalUserScript.Value; @@ -144,6 +146,7 @@ namespace BililiveRecorder.Web.Models.Rest public Optional OptionalTimingDanmakuRetry { get; set; } public Optional OptionalTimingWatchdogTimeout { get; set; } public Optional OptionalRecordDanmakuFlushInterval { get; set; } + public Optional OptionalDanmakuTransport { get; set; } public Optional OptionalNetworkTransportUseSystemProxy { get; set; } public Optional OptionalNetworkTransportAllowedAddressFamily { get; set; } public Optional OptionalUserScript { get; set; } @@ -199,6 +202,7 @@ namespace BililiveRecorder.Web.Models.Graphql this.Field(x => x.OptionalTimingDanmakuRetry, type: typeof(HierarchicalOptionalType)); this.Field(x => x.OptionalTimingWatchdogTimeout, type: typeof(HierarchicalOptionalType)); this.Field(x => x.OptionalRecordDanmakuFlushInterval, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalDanmakuTransport, type: typeof(HierarchicalOptionalType)); this.Field(x => x.OptionalNetworkTransportUseSystemProxy, type: typeof(HierarchicalOptionalType)); this.Field(x => x.OptionalNetworkTransportAllowedAddressFamily, type: typeof(HierarchicalOptionalType)); this.Field(x => x.OptionalUserScript, type: typeof(HierarchicalOptionalType)); @@ -232,6 +236,7 @@ namespace BililiveRecorder.Web.Models.Graphql this.Field(x => x.TimingDanmakuRetry); this.Field(x => x.TimingWatchdogTimeout); this.Field(x => x.RecordDanmakuFlushInterval); + this.Field(x => x.DanmakuTransport); this.Field(x => x.NetworkTransportUseSystemProxy); this.Field(x => x.NetworkTransportAllowedAddressFamily); this.Field(x => x.UserScript); @@ -283,6 +288,7 @@ namespace BililiveRecorder.Web.Models.Graphql this.Field(x => x.OptionalTimingDanmakuRetry, nullable: true, type: typeof(HierarchicalOptionalInputType)); this.Field(x => x.OptionalTimingWatchdogTimeout, nullable: true, type: typeof(HierarchicalOptionalInputType)); this.Field(x => x.OptionalRecordDanmakuFlushInterval, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalDanmakuTransport, nullable: true, type: typeof(HierarchicalOptionalInputType)); this.Field(x => x.OptionalNetworkTransportUseSystemProxy, nullable: true, type: typeof(HierarchicalOptionalInputType)); this.Field(x => x.OptionalNetworkTransportAllowedAddressFamily, nullable: true, type: typeof(HierarchicalOptionalInputType)); this.Field(x => x.OptionalUserScript, nullable: true, type: typeof(HierarchicalOptionalInputType)); diff --git a/configV3.schema.json b/configV3.schema.json index 0720612..e93c559 100644 --- a/configV3.schema.json +++ b/configV3.schema.json @@ -229,6 +229,29 @@ } } }, + "DanmakuTransport": { + "description": "使用的弹幕服务器传输协议\n默认: DanmakuTransportMode.Random", + "markdownDescription": "使用的弹幕服务器传输协议 \n默认: `DanmakuTransportMode.Random `\n\n", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "integer", + "default": 0, + "enum": [ + 0, + 1, + 2, + 3 + ], + "description": "0: 随机\n1: TCP\n2: WebSocket (HTTP)\n3: WebSocket (HTTPS)" + } + } + }, "NetworkTransportUseSystemProxy": { "description": "是否使用系统代理\n默认: false", "markdownDescription": "是否使用系统代理 \n默认: `false `\n\n", diff --git a/config_gen/data.ts b/config_gen/data.ts index 3475ea4..b83c473 100644 --- a/config_gen/data.ts +++ b/config_gen/data.ts @@ -186,6 +186,14 @@ export const data: Array = [ advancedConfig: true, default: 20, }, + { + id: "DanmakuTransport", + name: "使用的弹幕服务器传输协议", + type: "DanmakuTransportMode", + configType: "globalOnly", + advancedConfig: true, + default: "DanmakuTransportMode.Random", + }, { id: "NetworkTransportUseSystemProxy", name: "是否使用系统代理", diff --git a/config_gen/generators/codeSchema.ts b/config_gen/generators/codeSchema.ts index 5f41457..c8321a3 100644 --- a/config_gen/generators/codeSchema.ts +++ b/config_gen/generators/codeSchema.ts @@ -8,6 +8,8 @@ function mapTypeToJsonSchema(id: string, type: string, defaultValue: any) { return { type: "integer", default: 0, enum: [0, 1, 2], "description": "0: 禁用\n1: 根据时间切割\n2: 根据文件大小切割" }; case "AllowedAddressFamily": return { type: "integer", default: 0, enum: [-1, 0, 1, 2], "description": "-1: 由系统决定\n0: 任意 IPv4 或 IPv6\n1: 仅 IPv4\n2: IPv6" }; + case "DanmakuTransportMode": + return { type: "integer", default: 0, enum: [0, 1, 2, 3], "description": "0: 随机\n1: TCP\n2: WebSocket (HTTP)\n3: WebSocket (HTTPS)" }; case "uint": return { type: "integer", minimum: 0, maximum: 4294967295, default: defaultValue }; case "int": diff --git a/config_gen/types.ts b/config_gen/types.ts index 1249a1b..196066f 100644 --- a/config_gen/types.ts +++ b/config_gen/types.ts @@ -15,6 +15,7 @@ export type ConfigValueType = | "RecordMode" | "CuttingMode" | "AllowedAddressFamily" + | "DanmakuTransportMode" export interface ConfigEntry { /** 名字 */