mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-16 03:32:20 +08:00
feat: add support for websocket danmaku protocol
This commit is contained in:
parent
574c689c3c
commit
9c7e99944a
|
@ -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 });
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
namespace BililiveRecorder.Core.Api.Danmaku
|
||||
{
|
||||
internal class DanmakuTransportSecureWebSocket : DanmakuTransportWebSocket
|
||||
{
|
||||
protected override string Scheme => "wss";
|
||||
}
|
||||
}
|
43
BililiveRecorder.Core/Api/Danmaku/DanmakuTransportTcp.cs
Normal file
43
BililiveRecorder.Core/Api/Danmaku/DanmakuTransportTcp.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
13
BililiveRecorder.Core/Api/Danmaku/IDanmakuTransport.cs
Normal file
13
BililiveRecorder.Core/Api/Danmaku/IDanmakuTransport.cs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
10
BililiveRecorder.Core/Config/DanmakuTransportMode.cs
Normal file
10
BililiveRecorder.Core/Config/DanmakuTransportMode.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace BililiveRecorder.Core.Config
|
||||
{
|
||||
public enum DanmakuTransportMode
|
||||
{
|
||||
Random = 0,
|
||||
Tcp = 1,
|
||||
Ws = 2,
|
||||
Wss = 3,
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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="使用系统代理"/>
|
||||
|
|
|
@ -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>));
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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: "是否使用系统代理",
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -15,6 +15,7 @@ export type ConfigValueType =
|
|||
| "RecordMode"
|
||||
| "CuttingMode"
|
||||
| "AllowedAddressFamily"
|
||||
| "DanmakuTransportMode"
|
||||
|
||||
export interface ConfigEntry {
|
||||
/** 名字 */
|
||||
|
|
Loading…
Reference in New Issue
Block a user