feat: add support for websocket danmaku protocol

This commit is contained in:
genteure 2022-08-25 18:42:36 +08:00
parent 574c689c3c
commit 9c7e99944a
19 changed files with 287 additions and 58 deletions

View File

@ -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<GlobalConfig, uint>(config => config.HasTimingDanmakuRetry = false, (config, value) => config.TimingDanmakuRetry = value) { Name = "TimingDanmakuRetry", CanBeOptional = true });
GlobalConfig.Add(GlobalConfigProperties.TimingWatchdogTimeout, new ConfigInstruction<GlobalConfig, uint>(config => config.HasTimingWatchdogTimeout = false, (config, value) => config.TimingWatchdogTimeout = value) { Name = "TimingWatchdogTimeout", CanBeOptional = true });
GlobalConfig.Add(GlobalConfigProperties.RecordDanmakuFlushInterval, new ConfigInstruction<GlobalConfig, uint>(config => config.HasRecordDanmakuFlushInterval = false, (config, value) => config.RecordDanmakuFlushInterval = value) { Name = "RecordDanmakuFlushInterval", CanBeOptional = true });
GlobalConfig.Add(GlobalConfigProperties.DanmakuTransport, new ConfigInstruction<GlobalConfig, DanmakuTransportMode>(config => config.HasDanmakuTransport = false, (config, value) => config.DanmakuTransport = value) { Name = "DanmakuTransport", CanBeOptional = true });
GlobalConfig.Add(GlobalConfigProperties.NetworkTransportUseSystemProxy, new ConfigInstruction<GlobalConfig, bool>(config => config.HasNetworkTransportUseSystemProxy = false, (config, value) => config.NetworkTransportUseSystemProxy = value) { Name = "NetworkTransportUseSystemProxy", CanBeOptional = true });
GlobalConfig.Add(GlobalConfigProperties.NetworkTransportAllowedAddressFamily, new ConfigInstruction<GlobalConfig, AllowedAddressFamily>(config => config.HasNetworkTransportAllowedAddressFamily = false, (config, value) => config.NetworkTransportAllowedAddressFamily = value) { Name = "NetworkTransportAllowedAddressFamily", CanBeOptional = true });
GlobalConfig.Add(GlobalConfigProperties.UserScript, new ConfigInstruction<GlobalConfig, string>(config => config.HasUserScript = false, (config, value) => config.UserScript = value) { Name = "UserScript", CanBeOptional = true });

View File

@ -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<StatusChangedEventArgs>? StatusChanged;
public event EventHandler<DanmakuReceivedEventArgs>? 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<byte>();
const int headerLength = 16;
var size = playload.Length + headerLength;
var buffer = ArrayPool<byte>.Shared.Rent(headerLength);
var totalLength = playload.Length + headerLength;
var buffer = ArrayPool<byte>.Shared.Rent(totalLength);
try
{
BinaryPrimitives.WriteUInt32BigEndian(new Span<byte>(buffer, 0, 4), (uint)size);
BinaryPrimitives.WriteUInt32BigEndian(new Span<byte>(buffer, 0, 4), (uint)totalLength);
BinaryPrimitives.WriteUInt16BigEndian(new Span<byte>(buffer, 4, 2), headerLength);
BinaryPrimitives.WriteUInt16BigEndian(new Span<byte>(buffer, 6, 2), 1);
BinaryPrimitives.WriteUInt32BigEndian(new Span<byte>(buffer, 8, 4), (uint)action);
BinaryPrimitives.WriteUInt32BigEndian(new Span<byte>(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<string> callback)
private static async Task ProcessDataAsync(PipeReader reader, Action<string> callback)
{
var reader = PipeReader.Create(stream);
while (true)
{
var result = await reader.ReadAsync();

View File

@ -0,0 +1,7 @@
namespace BililiveRecorder.Core.Api.Danmaku
{
internal class DanmakuTransportSecureWebSocket : DanmakuTransportWebSocket
{
protected override string Scheme => "wss";
}
}

View File

@ -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<PipeReader> 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);
}
}
}

View File

@ -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<PipeReader> 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<byte>(buffer, offset, count), WebSocketMessageType.Binary, true, default).ConfigureAwait(false);
public void Dispose() => this.socket.Dispose();
}
}

View File

@ -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<PipeReader> ConnectAsync(string host, int port, CancellationToken cancellationToken);
Task SendAsync(byte[] buffer, int offset, int count);
}
}

View File

@ -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;

View File

@ -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<StatusChangedEventArgs>? StatusChanged;
event EventHandler<DanmakuReceivedEventArgs>? DanmakuReceived;
Task ConnectAsync(int roomid, CancellationToken cancellationToken);
Task ConnectAsync(int roomid, DanmakuTransportMode transportMode, CancellationToken cancellationToken);
Task DisconnectAsync();
}
}

View File

@ -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; }
}
}
}

View File

@ -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<DanmakuServerInfo> 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;
}
}
}

View File

@ -0,0 +1,10 @@
namespace BililiveRecorder.Core.Config
{
public enum DanmakuTransportMode
{
Random = 0,
Tcp = 1,
Ws = 2,
Wss = 3,
}
}

View File

@ -174,6 +174,11 @@ namespace BililiveRecorder.Core.Config.V3
/// </summary>
public uint RecordDanmakuFlushInterval => this.GetPropertyValue<uint>();
/// <summary>
/// 使用的弹幕服务器传输协议
/// </summary>
public DanmakuTransportMode DanmakuTransport => this.GetPropertyValue<DanmakuTransportMode>();
/// <summary>
/// 是否使用系统代理
/// </summary>
@ -378,6 +383,14 @@ namespace BililiveRecorder.Core.Config.V3
[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>
/// 使用的弹幕服务器传输协议
/// </summary>
public DanmakuTransportMode DanmakuTransport { get => this.GetPropertyValue<DanmakuTransportMode>(); set => this.SetPropertyValue(value); }
public bool HasDanmakuTransport { get => this.GetPropertyHasValue(nameof(this.DanmakuTransport)); set => this.SetPropertyHasValue<DanmakuTransportMode>(value, nameof(this.DanmakuTransport)); }
[JsonProperty(nameof(DanmakuTransport)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<DanmakuTransportMode> OptionalDanmakuTransport { get => this.GetPropertyValueOptional<DanmakuTransportMode>(nameof(this.DanmakuTransport)); set => this.SetPropertyValueOptional(value, nameof(this.DanmakuTransport)); }
/// <summary>
/// 是否使用系统代理
/// </summary>
@ -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;

View File

@ -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);
}
});

View File

@ -57,6 +57,24 @@
</c:SettingWithDefault>
</StackPanel>
</GroupBox>
<GroupBox Header="弹幕服务器">
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasDanmakuTransport}" Header="弹幕服务器传输协议">
<StackPanel>
<RadioButton GroupName="DanmakuTransport" Content="随机"
IsChecked="{Binding Path=DanmakuTransport, Converter={StaticResource EnumToBooleanConverter},
ConverterParameter={x:Static config:DanmakuTransportMode.Random}}" />
<RadioButton GroupName="DanmakuTransport" Content="TCP"
IsChecked="{Binding Path=DanmakuTransport, Converter={StaticResource EnumToBooleanConverter},
ConverterParameter={x:Static config:DanmakuTransportMode.Tcp}}" />
<RadioButton GroupName="DanmakuTransport" Content="WS"
IsChecked="{Binding Path=DanmakuTransport, Converter={StaticResource EnumToBooleanConverter},
ConverterParameter={x:Static config:DanmakuTransportMode.Ws}}" />
<RadioButton GroupName="DanmakuTransport" Content="WSS"
IsChecked="{Binding Path=DanmakuTransport, Converter={StaticResource EnumToBooleanConverter},
ConverterParameter={x:Static config:DanmakuTransportMode.Wss}}" />
</StackPanel>
</c:SettingWithDefault>
</GroupBox>
<GroupBox Header="Network">
<StackPanel>
<CheckBox IsChecked="{Binding Path=NetworkTransportUseSystemProxy}" Content="使用系统代理"/>

View File

@ -65,6 +65,7 @@ namespace BililiveRecorder.Web.Models
public Optional<uint>? OptionalTimingDanmakuRetry { get; set; }
public Optional<uint>? OptionalTimingWatchdogTimeout { get; set; }
public Optional<uint>? OptionalRecordDanmakuFlushInterval { get; set; }
public Optional<DanmakuTransportMode>? OptionalDanmakuTransport { get; set; }
public Optional<bool>? OptionalNetworkTransportUseSystemProxy { get; set; }
public Optional<AllowedAddressFamily>? OptionalNetworkTransportAllowedAddressFamily { get; set; }
public Optional<string?>? 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<uint> OptionalTimingDanmakuRetry { get; set; }
public Optional<uint> OptionalTimingWatchdogTimeout { get; set; }
public Optional<uint> OptionalRecordDanmakuFlushInterval { get; set; }
public Optional<DanmakuTransportMode> OptionalDanmakuTransport { get; set; }
public Optional<bool> OptionalNetworkTransportUseSystemProxy { get; set; }
public Optional<AllowedAddressFamily> OptionalNetworkTransportAllowedAddressFamily { get; set; }
public Optional<string?> OptionalUserScript { get; set; }
@ -199,6 +202,7 @@ namespace BililiveRecorder.Web.Models.Graphql
this.Field(x => x.OptionalTimingDanmakuRetry, type: typeof(HierarchicalOptionalType<uint>));
this.Field(x => x.OptionalTimingWatchdogTimeout, type: typeof(HierarchicalOptionalType<uint>));
this.Field(x => x.OptionalRecordDanmakuFlushInterval, type: typeof(HierarchicalOptionalType<uint>));
this.Field(x => x.OptionalDanmakuTransport, type: typeof(HierarchicalOptionalType<DanmakuTransportMode>));
this.Field(x => x.OptionalNetworkTransportUseSystemProxy, type: typeof(HierarchicalOptionalType<bool>));
this.Field(x => x.OptionalNetworkTransportAllowedAddressFamily, type: typeof(HierarchicalOptionalType<AllowedAddressFamily>));
this.Field(x => x.OptionalUserScript, type: typeof(HierarchicalOptionalType<string>));
@ -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<uint>));
this.Field(x => x.OptionalTimingWatchdogTimeout, nullable: true, type: typeof(HierarchicalOptionalInputType<uint>));
this.Field(x => x.OptionalRecordDanmakuFlushInterval, nullable: true, type: typeof(HierarchicalOptionalInputType<uint>));
this.Field(x => x.OptionalDanmakuTransport, nullable: true, type: typeof(HierarchicalOptionalInputType<DanmakuTransportMode>));
this.Field(x => x.OptionalNetworkTransportUseSystemProxy, nullable: true, type: typeof(HierarchicalOptionalInputType<bool>));
this.Field(x => x.OptionalNetworkTransportAllowedAddressFamily, nullable: true, type: typeof(HierarchicalOptionalInputType<AllowedAddressFamily>));
this.Field(x => x.OptionalUserScript, nullable: true, type: typeof(HierarchicalOptionalInputType<string>));

View File

@ -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",

View File

@ -186,6 +186,14 @@ export const data: Array<ConfigEntry> = [
advancedConfig: true,
default: 20,
},
{
id: "DanmakuTransport",
name: "使用的弹幕服务器传输协议",
type: "DanmakuTransportMode",
configType: "globalOnly",
advancedConfig: true,
default: "DanmakuTransportMode.Random",
},
{
id: "NetworkTransportUseSystemProxy",
name: "是否使用系统代理",

View File

@ -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":

View File

@ -15,6 +15,7 @@ export type ConfigValueType =
| "RecordMode"
| "CuttingMode"
| "AllowedAddressFamily"
| "DanmakuTransportMode"
export interface ConfigEntry {
/** 名字 */