diff --git a/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs b/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs index 4ec8170..f2a0215 100644 --- a/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs +++ b/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs @@ -35,7 +35,9 @@ namespace BililiveRecorder.Cli.Configure TimingStreamConnect, TimingDanmakuRetry, TimingWatchdogTimeout, - RecordDanmakuFlushInterval + RecordDanmakuFlushInterval, + NetworkTransportUseSystemProxy, + NetworkTransportAllowedAddressFamily } public enum RoomConfigProperties { @@ -82,6 +84,8 @@ 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.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 }); RoomConfig.Add(RoomConfigProperties.RoomId, new ConfigInstruction(config => config.HasRoomId = false, (config, value) => config.RoomId = value) { Name = "RoomId", CanBeOptional = false }); RoomConfig.Add(RoomConfigProperties.AutoRecord, new ConfigInstruction(config => config.HasAutoRecord = false, (config, value) => config.AutoRecord = value) { Name = "AutoRecord", CanBeOptional = false }); diff --git a/BililiveRecorder.Cli/Program.cs b/BililiveRecorder.Cli/Program.cs index f59390c..d9ab23d 100644 --- a/BililiveRecorder.Cli/Program.cs +++ b/BililiveRecorder.Cli/Program.cs @@ -4,9 +4,10 @@ using System.CommandLine; using System.CommandLine.Invocation; using System.IO; using System.Linq; +using System.Net; using System.Threading; -using BililiveRecorder.Cli.Configure; using System.Threading.Tasks; +using BililiveRecorder.Cli.Configure; using BililiveRecorder.Core; using BililiveRecorder.Core.Config; using BililiveRecorder.Core.Config.V3; @@ -217,6 +218,7 @@ namespace BililiveRecorder.Cli .Enrich.WithThreadName() .Enrich.FromLogContext() .Enrich.WithExceptionDetails() + .Destructure.AsScalar() .Destructure.ByTransforming(x => new { x.Version, diff --git a/BililiveRecorder.Core/Config/AllowedAddressFamily.cs b/BililiveRecorder.Core/Config/AllowedAddressFamily.cs new file mode 100644 index 0000000..3d1bd5a --- /dev/null +++ b/BililiveRecorder.Core/Config/AllowedAddressFamily.cs @@ -0,0 +1,10 @@ +namespace BililiveRecorder.Core.Config +{ + public enum AllowedAddressFamily + { + System = -1, + Any = 0, + Ipv4 = 1, + Ipv6 = 2, + } +} diff --git a/BililiveRecorder.Core/Config/V3/Config.gen.cs b/BililiveRecorder.Core/Config/V3/Config.gen.cs index 9600441..6b5d168 100644 --- a/BililiveRecorder.Core/Config/V3/Config.gen.cs +++ b/BililiveRecorder.Core/Config/V3/Config.gen.cs @@ -166,6 +166,16 @@ namespace BililiveRecorder.Core.Config.V3 /// public uint RecordDanmakuFlushInterval => this.GetPropertyValue(); + /// + /// 是否使用系统代理 + /// + public bool NetworkTransportUseSystemProxy => this.GetPropertyValue(); + + /// + /// 允许使用的 IP 网络类型 + /// + public AllowedAddressFamily NetworkTransportAllowedAddressFamily => this.GetPropertyValue(); + } [JsonObject(MemberSerialization.OptIn)] @@ -347,6 +357,22 @@ 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 bool NetworkTransportUseSystemProxy { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasNetworkTransportUseSystemProxy { get => this.GetPropertyHasValue(nameof(this.NetworkTransportUseSystemProxy)); set => this.SetPropertyHasValue(value, nameof(this.NetworkTransportUseSystemProxy)); } + [JsonProperty(nameof(NetworkTransportUseSystemProxy)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalNetworkTransportUseSystemProxy { get => this.GetPropertyValueOptional(nameof(this.NetworkTransportUseSystemProxy)); set => this.SetPropertyValueOptional(value, nameof(this.NetworkTransportUseSystemProxy)); } + + /// + /// 允许使用的 IP 网络类型 + /// + public AllowedAddressFamily NetworkTransportAllowedAddressFamily { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasNetworkTransportAllowedAddressFamily { get => this.GetPropertyHasValue(nameof(this.NetworkTransportAllowedAddressFamily)); set => this.SetPropertyHasValue(value, nameof(this.NetworkTransportAllowedAddressFamily)); } + [JsonProperty(nameof(NetworkTransportAllowedAddressFamily)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalNetworkTransportAllowedAddressFamily { get => this.GetPropertyValueOptional(nameof(this.NetworkTransportAllowedAddressFamily)); set => this.SetPropertyValueOptional(value, nameof(this.NetworkTransportAllowedAddressFamily)); } + } public sealed partial class DefaultConfig @@ -398,6 +424,10 @@ namespace BililiveRecorder.Core.Config.V3 public uint RecordDanmakuFlushInterval => 20; + public bool NetworkTransportUseSystemProxy => false; + + public AllowedAddressFamily NetworkTransportAllowedAddressFamily => AllowedAddressFamily.Any; + } } diff --git a/BililiveRecorder.Core/Recording/RecordTaskBase.cs b/BililiveRecorder.Core/Recording/RecordTaskBase.cs index fc4969d..fe78da6 100644 --- a/BililiveRecorder.Core/Recording/RecordTaskBase.cs +++ b/BililiveRecorder.Core/Recording/RecordTaskBase.cs @@ -2,11 +2,13 @@ using System; using System.Diagnostics; using System.IO; using System.Linq; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using System.Timers; using BililiveRecorder.Core.Api; +using BililiveRecorder.Core.Config; using BililiveRecorder.Core.Event; using BililiveRecorder.Core.Templating; using Serilog; @@ -19,7 +21,7 @@ namespace BililiveRecorder.Core.Recording private const string HttpHeaderAccept = "*/*"; 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"; + private const string HttpHeaderUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36"; private const int timer_inverval = 2; protected readonly Timer timer = new Timer(1000 * timer_inverval); @@ -199,11 +201,12 @@ namespace BililiveRecorder.Core.Recording #region Api Requests - private static HttpClient CreateHttpClient() + private HttpClient CreateHttpClient() { var httpClient = new HttpClient(new HttpClientHandler { - AllowAutoRedirect = false + AllowAutoRedirect = false, + UseProxy = this.room.RoomConfig.NetworkTransportUseSystemProxy, }); var headers = httpClient.DefaultRequestHeaders; headers.Add("Accept", HttpHeaderAccept); @@ -273,25 +276,64 @@ namespace BililiveRecorder.Core.Recording protected async Task GetStreamAsync(string fullUrl, int timeout) { - var client = CreateHttpClient(); + var client = this.CreateHttpClient(); while (true) { - var resp = await client.GetAsync(fullUrl, + var originalUri = new Uri(fullUrl); + var allowedAddressFamily = this.room.RoomConfig.NetworkTransportAllowedAddressFamily; + HttpRequestMessage request; + + if (allowedAddressFamily == AllowedAddressFamily.System) + { + this.logger.Debug("NetworkTransportAllowedAddressFamily is System"); + request = new HttpRequestMessage(HttpMethod.Get, originalUri); + } + else + { + var ips = await Dns.GetHostAddressesAsync(originalUri.DnsSafeHost); + + var filtered = ips.Where(x => allowedAddressFamily switch + { + AllowedAddressFamily.Ipv4 => x.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork, + AllowedAddressFamily.Ipv6 => x.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6, + AllowedAddressFamily.Any => true, + _ => false + }).ToArray(); + + var selected = filtered[this.random.Next(filtered.Length)]; + + this.logger.Debug("指定直播服务器地址 {DnsHost}: {SelectedIp}, Allowed: {AllowedAddressFamily}, {IPAddresses}", originalUri.DnsSafeHost, selected, allowedAddressFamily, ips); + + if (selected is null) + { + throw new Exception("DNS 没有返回符合要求的 IP 地址"); + } + + var builder = new UriBuilder(originalUri) + { + Host = selected.ToString() + }; + + request = new HttpRequestMessage(HttpMethod.Get, builder.Uri); + request.Headers.Host = originalUri.IsDefaultPort ? originalUri.Host : originalUri.Host + ":" + originalUri.Port; + } + + var resp = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, new CancellationTokenSource(timeout).Token) .ConfigureAwait(false); switch (resp.StatusCode) { - case System.Net.HttpStatusCode.OK: + case HttpStatusCode.OK: { this.logger.Information("开始接收直播流"); var stream = await resp.Content.ReadAsStreamAsync().ConfigureAwait(false); return stream; } - case System.Net.HttpStatusCode.Moved: - case System.Net.HttpStatusCode.Redirect: + case HttpStatusCode.Moved: + case HttpStatusCode.Redirect: { fullUrl = resp.Headers.Location.OriginalString; this.logger.Debug("跳转到 {Url}", fullUrl); @@ -303,7 +345,6 @@ namespace BililiveRecorder.Core.Recording } } } - #endregion } } diff --git a/BililiveRecorder.WPF/Program.cs b/BililiveRecorder.WPF/Program.cs index 2e9ed91..1a00412 100644 --- a/BililiveRecorder.WPF/Program.cs +++ b/BililiveRecorder.WPF/Program.cs @@ -213,6 +213,7 @@ namespace BililiveRecorder.WPF .Enrich.WithThreadName() .Enrich.FromLogContext() .Enrich.WithExceptionDetails() + .Destructure.AsScalar() .Destructure.ByTransforming(x => new { x.Version, @@ -221,7 +222,6 @@ namespace BililiveRecorder.WPF x.FileCreationTime, x.FileModificationTime, }) - .Destructure.AsScalar() .WriteTo.Console(levelSwitch: levelSwitchConsole) #if DEBUG .WriteTo.Debug() diff --git a/BililiveRecorder.Web/Models/Config.gen.cs b/BililiveRecorder.Web/Models/Config.gen.cs index 72ef347..be8e486 100644 --- a/BililiveRecorder.Web/Models/Config.gen.cs +++ b/BililiveRecorder.Web/Models/Config.gen.cs @@ -62,6 +62,8 @@ namespace BililiveRecorder.Web.Models public Optional? OptionalTimingDanmakuRetry { get; set; } public Optional? OptionalTimingWatchdogTimeout { get; set; } public Optional? OptionalRecordDanmakuFlushInterval { get; set; } + public Optional? OptionalNetworkTransportUseSystemProxy { get; set; } + public Optional? OptionalNetworkTransportAllowedAddressFamily { get; set; } public void ApplyTo(GlobalConfig config) { @@ -87,6 +89,8 @@ 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.OptionalNetworkTransportUseSystemProxy.HasValue) config.OptionalNetworkTransportUseSystemProxy = this.OptionalNetworkTransportUseSystemProxy.Value; + if (this.OptionalNetworkTransportAllowedAddressFamily.HasValue) config.OptionalNetworkTransportAllowedAddressFamily = this.OptionalNetworkTransportAllowedAddressFamily.Value; } } @@ -132,6 +136,8 @@ namespace BililiveRecorder.Web.Models.Rest public Optional OptionalTimingDanmakuRetry { get; set; } public Optional OptionalTimingWatchdogTimeout { get; set; } public Optional OptionalRecordDanmakuFlushInterval { get; set; } + public Optional OptionalNetworkTransportUseSystemProxy { get; set; } + public Optional OptionalNetworkTransportAllowedAddressFamily { get; set; } } } @@ -182,6 +188,8 @@ 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.OptionalNetworkTransportUseSystemProxy, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalNetworkTransportAllowedAddressFamily, type: typeof(HierarchicalOptionalType)); } } @@ -211,6 +219,8 @@ namespace BililiveRecorder.Web.Models.Graphql this.Field(x => x.TimingDanmakuRetry); this.Field(x => x.TimingWatchdogTimeout); this.Field(x => x.RecordDanmakuFlushInterval); + this.Field(x => x.NetworkTransportUseSystemProxy); + this.Field(x => x.NetworkTransportAllowedAddressFamily); } } @@ -257,6 +267,8 @@ 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.OptionalNetworkTransportUseSystemProxy, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalNetworkTransportAllowedAddressFamily, nullable: true, type: typeof(HierarchicalOptionalInputType)); } } diff --git a/configV3.schema.json b/configV3.schema.json index 901fe48..fc01f37 100644 --- a/configV3.schema.json +++ b/configV3.schema.json @@ -229,6 +229,38 @@ } } }, + "NetworkTransportUseSystemProxy": { + "description": "是否使用系统代理\n默认: false", + "markdownDescription": "是否使用系统代理 \n默认: `false `\n\n", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "boolean", + "default": false + } + } + }, + "NetworkTransportAllowedAddressFamily": { + "description": "允许使用的 IP 网络类型\n默认: AllowedAddressFamily.Any", + "markdownDescription": "允许使用的 IP 网络类型 \n默认: `AllowedAddressFamily.Any `\n\n", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "AllowedAddressFamily", + "default": "AllowedAddressFamily.Any" + } + } + }, "RecordMode": { "description": "录制模式\n默认: RecordMode.Standard", "markdownDescription": "录制模式 \n默认: `RecordMode.Standard `\n\n本设置项是一个 enum,键值对应如下:\n\n| 键 | 值 |\n|:--:|:--:|\n| RecordMode.Standard | 0 |\n| RecordMode.RawData | 1 |\n\n关于录制模式的说明见 [录制模式](/docs/basic/record_mode/)", diff --git a/config_gen/data.ts b/config_gen/data.ts index 78d6840..0f614ba 100644 --- a/config_gen/data.ts +++ b/config_gen/data.ts @@ -216,4 +216,22 @@ export const data: Array = [ xmlComment: "触发 的弹幕个数", markdown: "" }, + { + name: "NetworkTransportUseSystemProxy", + description: "是否使用系统代理", + type: "bool", + defaultValue: "false", + configType: "globalOnly", + advancedConfig: true, + markdown: "" + }, + { + name: "NetworkTransportAllowedAddressFamily", + description: "允许使用的 IP 网络类型", + type: "AllowedAddressFamily", + defaultValue: "AllowedAddressFamily.Any", + configType: "globalOnly", + advancedConfig: true, + markdown: "" + }, ];