mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-16 11:42:22 +08:00
Merge branch 'feature/scripting' into dev
This commit is contained in:
commit
effa694a65
|
@ -37,7 +37,8 @@ namespace BililiveRecorder.Cli.Configure
|
|||
TimingWatchdogTimeout,
|
||||
RecordDanmakuFlushInterval,
|
||||
NetworkTransportUseSystemProxy,
|
||||
NetworkTransportAllowedAddressFamily
|
||||
NetworkTransportAllowedAddressFamily,
|
||||
UserScript
|
||||
}
|
||||
public enum RoomConfigProperties
|
||||
{
|
||||
|
@ -86,6 +87,7 @@ namespace BililiveRecorder.Cli.Configure
|
|||
GlobalConfig.Add(GlobalConfigProperties.RecordDanmakuFlushInterval, new ConfigInstruction<GlobalConfig, uint>(config => config.HasRecordDanmakuFlushInterval = false, (config, value) => config.RecordDanmakuFlushInterval = value) { Name = "RecordDanmakuFlushInterval", 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 });
|
||||
|
||||
RoomConfig.Add(RoomConfigProperties.RoomId, new ConfigInstruction<RoomConfig, int>(config => config.HasRoomId = false, (config, value) => config.RoomId = value) { Name = "RoomId", CanBeOptional = false });
|
||||
RoomConfig.Add(RoomConfigProperties.AutoRecord, new ConfigInstruction<RoomConfig, bool>(config => config.HasAutoRecord = false, (config, value) => config.AutoRecord = value) { Name = "AutoRecord", CanBeOptional = false });
|
||||
|
|
|
@ -5,10 +5,15 @@
|
|||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<DebugType>portable</DebugType>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<RestoreAdditionalProjectSources>
|
||||
https://api.nuget.org/v3/index.json;
|
||||
https://www.myget.org/F/jint/api/v3/index.json;
|
||||
</RestoreAdditionalProjectSources>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fluid.Core" Version="2.2.8" />
|
||||
<PackageReference Include="Jint" Version="3.0.0-preview-275" />
|
||||
<PackageReference Include="JsonSubTypes" Version="1.8.0" />
|
||||
<PackageReference Include="HierarchicalPropertyDefault" Version="0.1.4-beta-g75fdf624b1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
|
||||
|
|
|
@ -176,6 +176,11 @@ namespace BililiveRecorder.Core.Config.V3
|
|||
/// </summary>
|
||||
public AllowedAddressFamily NetworkTransportAllowedAddressFamily => this.GetPropertyValue<AllowedAddressFamily>();
|
||||
|
||||
/// <summary>
|
||||
/// 自定义脚本
|
||||
/// </summary>
|
||||
public string UserScript => this.GetPropertyValue<string>();
|
||||
|
||||
}
|
||||
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
|
@ -373,6 +378,14 @@ namespace BililiveRecorder.Core.Config.V3
|
|||
[JsonProperty(nameof(NetworkTransportAllowedAddressFamily)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<AllowedAddressFamily> OptionalNetworkTransportAllowedAddressFamily { get => this.GetPropertyValueOptional<AllowedAddressFamily>(nameof(this.NetworkTransportAllowedAddressFamily)); set => this.SetPropertyValueOptional(value, nameof(this.NetworkTransportAllowedAddressFamily)); }
|
||||
|
||||
/// <summary>
|
||||
/// 自定义脚本
|
||||
/// </summary>
|
||||
public string UserScript { get => this.GetPropertyValue<string>(); set => this.SetPropertyValue(value); }
|
||||
public bool HasUserScript { get => this.GetPropertyHasValue(nameof(this.UserScript)); set => this.SetPropertyHasValue<string>(value, nameof(this.UserScript)); }
|
||||
[JsonProperty(nameof(UserScript)), EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public Optional<string> OptionalUserScript { get => this.GetPropertyValueOptional<string>(nameof(this.UserScript)); set => this.SetPropertyValueOptional(value, nameof(this.UserScript)); }
|
||||
|
||||
}
|
||||
|
||||
public sealed partial class DefaultConfig
|
||||
|
@ -428,6 +441,8 @@ namespace BililiveRecorder.Core.Config.V3
|
|||
|
||||
public AllowedAddressFamily NetworkTransportAllowedAddressFamily => AllowedAddressFamily.Any;
|
||||
|
||||
public string UserScript => string.Empty;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ using BililiveRecorder.Core.Api.Http;
|
|||
using BililiveRecorder.Core.Config.V3;
|
||||
using BililiveRecorder.Core.Danmaku;
|
||||
using BililiveRecorder.Core.Recording;
|
||||
using BililiveRecorder.Core.Scripting;
|
||||
using BililiveRecorder.Core.Templating;
|
||||
using BililiveRecorder.Flv;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
@ -27,6 +28,7 @@ namespace BililiveRecorder.DependencyInjection
|
|||
.AddSingleton<IRecorder, Recorder>()
|
||||
.AddSingleton<IRoomFactory, RoomFactory>()
|
||||
.AddScoped<IBasicDanmakuWriter, BasicDanmakuWriter>()
|
||||
.AddSingleton<UserScriptRunner>()
|
||||
;
|
||||
|
||||
private static IServiceCollection AddRecorderPollyPolicy(this IServiceCollection services) => services
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using BililiveRecorder.Core.Api;
|
||||
using BililiveRecorder.Core.Event;
|
||||
using BililiveRecorder.Core.Scripting;
|
||||
using BililiveRecorder.Core.Templating;
|
||||
using Serilog;
|
||||
|
||||
|
@ -16,11 +17,13 @@ namespace BililiveRecorder.Core.Recording
|
|||
public RawDataRecordTask(IRoom room,
|
||||
ILogger logger,
|
||||
IApiClient apiClient,
|
||||
FileNameGenerator fileNameGenerator)
|
||||
FileNameGenerator fileNameGenerator,
|
||||
UserScriptRunner userScriptRunner)
|
||||
: base(room: room,
|
||||
logger: logger?.ForContext<RawDataRecordTask>().ForContext(LoggingContext.RoomId, room.RoomConfig.RoomId)!,
|
||||
apiClient: apiClient,
|
||||
fileNameGenerator: fileNameGenerator)
|
||||
fileNameGenerator: fileNameGenerator,
|
||||
userScriptRunner: userScriptRunner)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ using System.Timers;
|
|||
using BililiveRecorder.Core.Api;
|
||||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.Core.Event;
|
||||
using BililiveRecorder.Core.Scripting;
|
||||
using BililiveRecorder.Core.Templating;
|
||||
using Serilog;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
@ -33,6 +34,7 @@ namespace BililiveRecorder.Core.Recording
|
|||
protected readonly ILogger logger;
|
||||
protected readonly IApiClient apiClient;
|
||||
private readonly FileNameGenerator fileNameGenerator;
|
||||
private readonly UserScriptRunner userScriptRunner;
|
||||
|
||||
protected string? streamHost;
|
||||
protected bool started = false;
|
||||
|
@ -51,12 +53,13 @@ namespace BililiveRecorder.Core.Recording
|
|||
private DateTimeOffset ioStatsLastTrigger;
|
||||
private TimeSpan durationSinceNoDataReceived;
|
||||
|
||||
protected RecordTaskBase(IRoom room, ILogger logger, IApiClient apiClient, FileNameGenerator fileNameGenerator)
|
||||
protected RecordTaskBase(IRoom room, ILogger logger, IApiClient apiClient, FileNameGenerator fileNameGenerator, UserScriptRunner userScriptRunner)
|
||||
{
|
||||
this.room = room ?? throw new ArgumentNullException(nameof(room));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
|
||||
this.fileNameGenerator = fileNameGenerator ?? throw new ArgumentNullException(nameof(fileNameGenerator));
|
||||
this.userScriptRunner = userScriptRunner ?? throw new ArgumentNullException(nameof(userScriptRunner));
|
||||
this.ct = this.cts.Token;
|
||||
|
||||
this.timer.Elapsed += this.Timer_Elapsed_TriggerIOStats;
|
||||
|
@ -95,6 +98,7 @@ namespace BililiveRecorder.Core.Recording
|
|||
this.streamHost = new Uri(fullUrl).Host;
|
||||
var qnDesc = qn switch
|
||||
{
|
||||
30000 => "杜比",
|
||||
20000 => "4K",
|
||||
10000 => "原画",
|
||||
401 => "蓝光(杜比)",
|
||||
|
@ -102,6 +106,7 @@ namespace BililiveRecorder.Core.Recording
|
|||
250 => "超清",
|
||||
150 => "高清",
|
||||
80 => "流畅",
|
||||
-1 => "录播姬用户脚本",
|
||||
_ => "未知"
|
||||
};
|
||||
this.logger.Information("连接直播服务器 {Host} 录制画质 {Qn} ({QnDescription})", this.streamHost, qn, qnDesc);
|
||||
|
@ -218,6 +223,19 @@ namespace BililiveRecorder.Core.Recording
|
|||
|
||||
protected async Task<(string url, int qn)> FetchStreamUrlAsync(int roomid)
|
||||
{
|
||||
var qns = this.room.RoomConfig.RecordingQuality?.Split(new[] { ',', ',', '、', ' ' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => int.TryParse(x, out var num) ? num : -1)
|
||||
.Where(x => x > 0)
|
||||
.ToArray()
|
||||
?? Array.Empty<int>();
|
||||
|
||||
// 优先使用用户脚本获取直播流地址
|
||||
if (this.userScriptRunner.CallOnFetchStreamUrl(this.logger, roomid, qns) is { } urlFromScript)
|
||||
{
|
||||
this.logger.Information("使用用户脚本返回的直播流地址 {Url}", urlFromScript);
|
||||
return (urlFromScript, -1);
|
||||
}
|
||||
|
||||
const int DefaultQn = 10000;
|
||||
var selected_qn = DefaultQn;
|
||||
var codecItem = await this.apiClient.GetCodecItemInStreamUrlAsync(roomid: roomid, qn: DefaultQn).ConfigureAwait(false);
|
||||
|
@ -225,12 +243,6 @@ namespace BililiveRecorder.Core.Recording
|
|||
if (codecItem is null)
|
||||
throw new Exception("no supported stream url, qn: " + DefaultQn);
|
||||
|
||||
var qns = this.room.RoomConfig.RecordingQuality?.Split(new[] { ',', ',', '、', ' ' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => int.TryParse(x, out var num) ? num : -1)
|
||||
.Where(x => x > 0)
|
||||
.ToArray()
|
||||
?? Array.Empty<int>();
|
||||
|
||||
// Select first avaiable qn
|
||||
foreach (var qn in qns)
|
||||
{
|
||||
|
@ -280,10 +292,37 @@ namespace BililiveRecorder.Core.Recording
|
|||
|
||||
while (true)
|
||||
{
|
||||
var originalUri = new Uri(fullUrl);
|
||||
var allowedAddressFamily = this.room.RoomConfig.NetworkTransportAllowedAddressFamily;
|
||||
HttpRequestMessage request;
|
||||
|
||||
if (this.userScriptRunner.CallOnTransformStreamUrl(this.logger, fullUrl) is { } scriptResult)
|
||||
{
|
||||
var (scriptUrl, scriptIp) = scriptResult;
|
||||
|
||||
this.logger.Debug("用户脚本重定向了直播流地址 {NewUrl}, 旧地址 {OldUrl}", scriptUrl, fullUrl);
|
||||
|
||||
fullUrl = scriptUrl;
|
||||
|
||||
if (scriptIp is not null)
|
||||
{
|
||||
this.logger.Debug("用户脚本指定了服务器 IP {IP}", scriptIp);
|
||||
|
||||
request = new HttpRequestMessage(HttpMethod.Get, fullUrl);
|
||||
|
||||
var uri = new Uri(fullUrl);
|
||||
var builder = new UriBuilder(uri)
|
||||
{
|
||||
Host = scriptIp
|
||||
};
|
||||
|
||||
request = new HttpRequestMessage(HttpMethod.Get, builder.Uri);
|
||||
request.Headers.Host = uri.IsDefaultPort ? uri.Host : uri.Host + ":" + uri.Port;
|
||||
|
||||
goto sendRequest;
|
||||
}
|
||||
}
|
||||
|
||||
var originalUri = new Uri(fullUrl);
|
||||
if (allowedAddressFamily == AllowedAddressFamily.System)
|
||||
{
|
||||
this.logger.Debug("NetworkTransportAllowedAddressFamily is System");
|
||||
|
@ -319,6 +358,8 @@ namespace BililiveRecorder.Core.Recording
|
|||
request.Headers.Host = originalUri.IsDefaultPort ? originalUri.Host : originalUri.Host + ":" + originalUri.Port;
|
||||
}
|
||||
|
||||
sendRequest:
|
||||
|
||||
var resp = await client.SendAsync(request,
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
new CancellationTokenSource(timeout).Token)
|
||||
|
|
|
@ -9,6 +9,7 @@ using BililiveRecorder.Core.Api;
|
|||
using BililiveRecorder.Core.Config;
|
||||
using BililiveRecorder.Core.Event;
|
||||
using BililiveRecorder.Core.ProcessingRules;
|
||||
using BililiveRecorder.Core.Scripting;
|
||||
using BililiveRecorder.Core.Templating;
|
||||
using BililiveRecorder.Flv;
|
||||
using BililiveRecorder.Flv.Amf;
|
||||
|
@ -42,11 +43,13 @@ namespace BililiveRecorder.Core.Recording
|
|||
IFlvTagReaderFactory flvTagReaderFactory,
|
||||
ITagGroupReaderFactory tagGroupReaderFactory,
|
||||
IFlvProcessingContextWriterFactory writerFactory,
|
||||
FileNameGenerator fileNameGenerator)
|
||||
FileNameGenerator fileNameGenerator,
|
||||
UserScriptRunner userScriptRunner)
|
||||
: base(room: room,
|
||||
logger: logger?.ForContext<StandardRecordTask>().ForContext(LoggingContext.RoomId, room.RoomConfig.RoomId)!,
|
||||
apiClient: apiClient,
|
||||
fileNameGenerator: fileNameGenerator)
|
||||
fileNameGenerator: fileNameGenerator,
|
||||
userScriptRunner: userScriptRunner)
|
||||
{
|
||||
this.flvTagReaderFactory = flvTagReaderFactory ?? throw new ArgumentNullException(nameof(flvTagReaderFactory));
|
||||
this.tagGroupReaderFactory = tagGroupReaderFactory ?? throw new ArgumentNullException(nameof(tagGroupReaderFactory));
|
||||
|
|
211
BililiveRecorder.Core/Scripting/Runtime/JintConsole.cs
Normal file
211
BililiveRecorder.Core/Scripting/Runtime/JintConsole.cs
Normal file
|
@ -0,0 +1,211 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using Jint.Native.Json;
|
||||
using Jint.Native.Object;
|
||||
using Jint.Runtime;
|
||||
using Jint.Runtime.Interop;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace BililiveRecorder.Core.Scripting.Runtime
|
||||
{
|
||||
public class JintConsole : ObjectInstance
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
|
||||
private static readonly IReadOnlyList<string> templateMessageMap;
|
||||
private const int MaxTemplateSlotCount = 8;
|
||||
|
||||
private readonly Dictionary<string, int> counters = new();
|
||||
private readonly Dictionary<string, Stopwatch> timers = new();
|
||||
|
||||
static JintConsole()
|
||||
{
|
||||
var map = new List<string>();
|
||||
templateMessageMap = map;
|
||||
var b = new StringBuilder("[Script]");
|
||||
for (var i = 1; i <= MaxTemplateSlotCount; i++)
|
||||
{
|
||||
b.Append(" {Message");
|
||||
b.Append(i);
|
||||
b.Append("}");
|
||||
map.Add(b.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public JintConsole(Engine engine, ILogger logger) : base(engine)
|
||||
{
|
||||
this.logger = logger?.ForContext<JintConsole>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
Add("assert", this.Assert);
|
||||
Add("clear", this.Clear);
|
||||
Add("count", this.Count);
|
||||
Add("countReset", this.CountReset);
|
||||
Add("debug", this.BuildLogFunc(LogEventLevel.Debug));
|
||||
Add("error", this.BuildLogFunc(LogEventLevel.Error));
|
||||
Add("info", this.BuildLogFunc(LogEventLevel.Information));
|
||||
Add("log", this.BuildLogFunc(LogEventLevel.Information));
|
||||
Add("time", this.Time);
|
||||
Add("timeEnd", this.TimeEnd);
|
||||
Add("timeLog", this.TimeLog);
|
||||
Add("trace", this.BuildLogFunc(LogEventLevel.Information));
|
||||
Add("warn", this.BuildLogFunc(LogEventLevel.Warning));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
void Add(string name, Func<JsValue, JsValue[], JsValue> func)
|
||||
{
|
||||
this.FastAddProperty(name, new ClrFunctionInstance(this._engine, name, func), false, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
private string[] FormatToString(ReadOnlySpan<JsValue> values)
|
||||
{
|
||||
var result = new string[values.Length];
|
||||
var jsonSerializer = new Lazy<JsonSerializer>(() => new JsonSerializer(this._engine));
|
||||
|
||||
for (var i = 0; i < values.Length; i++)
|
||||
{
|
||||
var value = values[i];
|
||||
var text = value switch
|
||||
{
|
||||
JsString jsString => jsString.ToString(),
|
||||
JsBoolean or JsNumber or JsBigInt or JsNull or JsUndefined => value.ToString(),
|
||||
_ => jsonSerializer.Value.Serialize(values[i], Undefined, Undefined).ToString()
|
||||
};
|
||||
result[i] = text;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// TODO: Add call stack support
|
||||
// Workaround: use `new Error().stack` in js side
|
||||
// ref: https://github.com/sebastienros/jint/discussions/1115
|
||||
|
||||
private Func<JsValue, JsValue[], JsValue> BuildLogFunc(LogEventLevel level)
|
||||
{
|
||||
return Log;
|
||||
JsValue Log(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
var messages = this.FormatToString(arguments);
|
||||
if (messages.Length > 0 && messages.Length <= MaxTemplateSlotCount)
|
||||
{
|
||||
// Serilog quote "Catch a common pitfall when a single non-object array is cast to object[]"
|
||||
// ref: https://github.com/serilog/serilog/blob/fabc2cbe637c9ddfa2d1ddc9f502df120f444acd/src/Serilog/Core/Logger.cs#L368
|
||||
var values = messages.Cast<object>().ToArray();
|
||||
|
||||
// Note: this is the non-generic, `params object[]` version
|
||||
// void Write(LogEventLevel level, string messageTemplate, params object[] propertyValues);
|
||||
this.logger.Write(level: level, messageTemplate: templateMessageMap[messages.Length - 1], propertyValues: values);
|
||||
}
|
||||
else
|
||||
{
|
||||
// void Write<T>(LogEventLevel level, string messageTemplate, T propertyValue);
|
||||
this.logger.Write(level: level, messageTemplate: "[Script] {Messages}", propertyValue: messages);
|
||||
}
|
||||
return Undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private JsValue Assert(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
if (!arguments.At(0).IsLooselyEqual(true))
|
||||
{
|
||||
string[] messages;
|
||||
|
||||
if (arguments.Length < 2)
|
||||
{
|
||||
messages = Array.Empty<string>();
|
||||
}
|
||||
else
|
||||
{
|
||||
messages = this.FormatToString(arguments.AsSpan(1));
|
||||
}
|
||||
|
||||
this.logger.Error("[Script] Assertion failed: {Messages}", messages);
|
||||
}
|
||||
return Undefined;
|
||||
}
|
||||
|
||||
private JsValue Clear(JsValue thisObject, JsValue[] arguments) => Undefined; // noop
|
||||
|
||||
private JsValue Count(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
var name = arguments.Length > 0 ? arguments[0].ToString() : "default";
|
||||
|
||||
if (this.counters.TryGetValue(name, out var count))
|
||||
{
|
||||
count++;
|
||||
}
|
||||
else
|
||||
{
|
||||
count = 1;
|
||||
}
|
||||
this.counters[name] = count;
|
||||
|
||||
this.logger.Information("[Script] {CounterName}: {Count}", name, count);
|
||||
return Undefined;
|
||||
}
|
||||
|
||||
private JsValue CountReset(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
var name = arguments.Length > 0 ? arguments[0].ToString() : "default";
|
||||
this.counters.Remove(name);
|
||||
this.logger.Information("[Script] {CounterName}: {Count}", name, 0);
|
||||
return Undefined;
|
||||
}
|
||||
|
||||
private JsValue Time(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
var name = arguments.Length > 0 ? arguments[0].ToString() : "default";
|
||||
if (this.timers.ContainsKey(name))
|
||||
{
|
||||
this.logger.Warning("[Script] Timer {TimerName} already exists", name);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.timers[name] = Stopwatch.StartNew();
|
||||
}
|
||||
return Undefined;
|
||||
}
|
||||
|
||||
private JsValue TimeEnd(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
var name = arguments.Length > 0 ? arguments[0].ToString() : "default";
|
||||
if (this.timers.TryGetValue(name, out var timer))
|
||||
{
|
||||
timer.Stop();
|
||||
this.timers.Remove(name);
|
||||
this.logger.Information("[Script] {TimerName}: {ElapsedMilliseconds} ms", name, timer.ElapsedMilliseconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.logger.Warning("[Script] Timer {TimerName} does not exist", name);
|
||||
}
|
||||
return Undefined;
|
||||
}
|
||||
|
||||
private JsValue TimeLog(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
var name = arguments.Length > 0 ? arguments[0].ToString() : "default";
|
||||
if (this.timers.TryGetValue(name, out var timer))
|
||||
{
|
||||
this.logger.Information("[Script] {TimerName}: {ElapsedMilliseconds} ms", name, timer.ElapsedMilliseconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.logger.Warning("[Script] Timer {TimerName} does not exist", name);
|
||||
}
|
||||
return Undefined;
|
||||
}
|
||||
}
|
||||
}
|
45
BililiveRecorder.Core/Scripting/Runtime/JintDns.cs
Normal file
45
BililiveRecorder.Core/Scripting/Runtime/JintDns.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using Jint.Native.Object;
|
||||
using Jint.Runtime;
|
||||
using Jint.Runtime.Interop;
|
||||
|
||||
namespace BililiveRecorder.Core.Scripting.Runtime
|
||||
{
|
||||
public class JintDns : ObjectInstance
|
||||
{
|
||||
public JintDns(Engine engine) : base(engine)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
Add("lookup", this.Lookup);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
void Add(string name, Func<JsValue, JsValue[], JsValue> func)
|
||||
{
|
||||
this.FastAddProperty(name, new ClrFunctionInstance(this._engine, name, func), false, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
private JsValue Lookup(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
string[] result;
|
||||
try
|
||||
{
|
||||
result = Dns.GetHostAddresses(arguments.At(0).AsString()).Select(x => x.ToString()).ToArray();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
result = Array.Empty<string>();
|
||||
}
|
||||
|
||||
return FromObject(this._engine, result);
|
||||
}
|
||||
}
|
||||
}
|
39
BililiveRecorder.Core/Scripting/Runtime/JintDotnet.cs
Normal file
39
BililiveRecorder.Core/Scripting/Runtime/JintDotnet.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Web;
|
||||
using Jint;
|
||||
using Jint.Native.Object;
|
||||
using Jint.Runtime.Interop;
|
||||
|
||||
namespace BililiveRecorder.Core.Scripting.Runtime
|
||||
{
|
||||
public class JintDotnet : ObjectInstance
|
||||
{
|
||||
public JintDotnet(Engine engine) : base(engine)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
this.FastAddProperty("Dns", TypeReference.CreateTypeReference(this._engine, typeof(Dns)), false, false, false);
|
||||
|
||||
AddTypeAsProperty<Uri>();
|
||||
AddTypeAsProperty<UriBuilder>();
|
||||
AddTypeAsProperty<HttpUtility>();
|
||||
AddTypeAsProperty<NameValueCollection>();
|
||||
|
||||
AddTypeAsProperty<HttpClient>();
|
||||
AddTypeAsProperty<HttpClientHandler>();
|
||||
AddTypeAsProperty<HttpCompletionOption>();
|
||||
AddTypeAsProperty<HttpRequestMessage>();
|
||||
AddTypeAsProperty<HttpMethod>();
|
||||
AddTypeAsProperty<ByteArrayContent>();
|
||||
AddTypeAsProperty<StringContent>();
|
||||
AddTypeAsProperty<FormUrlEncodedContent>();
|
||||
|
||||
void AddTypeAsProperty<T>() => this.FastAddProperty(typeof(T).Name, TypeReference.CreateTypeReference<T>(this._engine), false, false, false);
|
||||
}
|
||||
}
|
||||
}
|
195
BililiveRecorder.Core/Scripting/Runtime/JintFetchSync.cs
Normal file
195
BililiveRecorder.Core/Scripting/Runtime/JintFetchSync.cs
Normal file
|
@ -0,0 +1,195 @@
|
|||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using Jint.Native.Array;
|
||||
using Jint.Native.Function;
|
||||
using Jint.Native.Object;
|
||||
using Jint.Runtime;
|
||||
|
||||
namespace BililiveRecorder.Core.Scripting.Runtime
|
||||
{
|
||||
public class JintFetchSync : FunctionInstance
|
||||
{
|
||||
private static readonly JsString functionName = new JsString("fetchSync");
|
||||
|
||||
public JintFetchSync(Engine engine) : base(engine, engine.Realm, functionName)
|
||||
{
|
||||
}
|
||||
|
||||
protected override JsValue Call(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
if (arguments.Length == 0)
|
||||
throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, "1 argument required, but only 0 present.");
|
||||
|
||||
if (arguments[0] is not JsString urlString)
|
||||
throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, "Only url string is supported as the 1st argument.");
|
||||
|
||||
ObjectInstance? initObject = null;
|
||||
if (arguments.Length > 1)
|
||||
initObject = arguments[1] is not ObjectInstance arg1
|
||||
? throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, "The provided value is not of type 'RequestInit'.")
|
||||
: arg1;
|
||||
|
||||
HttpClientHandler handler = new HttpClientHandler();
|
||||
HttpClient? httpClient = new HttpClient(handler);
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Get, urlString.ToString());
|
||||
bool throwOnRedirect = false;
|
||||
|
||||
if (initObject is not null)
|
||||
{
|
||||
foreach (var kv in initObject.GetOwnProperties())
|
||||
{
|
||||
var key = kv.Key;
|
||||
var value = kv.Value;
|
||||
|
||||
if (!key.IsString())
|
||||
continue;
|
||||
|
||||
switch (key.AsString())
|
||||
{
|
||||
case "body":
|
||||
this.SetRequestBody(requestMessage, value.Value);
|
||||
break;
|
||||
case "headers":
|
||||
this.SetRequestHeader(requestMessage, value.Value);
|
||||
break;
|
||||
case "method":
|
||||
this.SetRequestMethod(requestMessage, value.Value);
|
||||
break;
|
||||
case "redirect":
|
||||
{
|
||||
var redirect = value.Value;
|
||||
if (redirect is JsNull or JsUndefined)
|
||||
break;
|
||||
switch (redirect.ToString())
|
||||
{
|
||||
case "follow":
|
||||
handler.AllowAutoRedirect = true;
|
||||
break;
|
||||
case "manual":
|
||||
handler.AllowAutoRedirect = false;
|
||||
break;
|
||||
case "error":
|
||||
handler.AllowAutoRedirect = false;
|
||||
throwOnRedirect = true;
|
||||
break;
|
||||
default:
|
||||
throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, $"'{redirect}' is not a valid value for 'redirect'.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "referrer":
|
||||
{
|
||||
var referrer = value.Value;
|
||||
if (referrer is JsNull or JsUndefined)
|
||||
break;
|
||||
requestMessage.Headers.Referrer = new System.Uri(referrer.ToString());
|
||||
break;
|
||||
}
|
||||
case "cache":
|
||||
case "credentials":
|
||||
case "integrity":
|
||||
case "keepalive":
|
||||
case "mode":
|
||||
case "referrerPolicy":
|
||||
case "signal":
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
|
||||
#pragma warning disable VSTHRD104 // Offer async methods
|
||||
var resp = httpClient.SendAsync(requestMessage).Result;
|
||||
|
||||
if (throwOnRedirect && (resp.StatusCode is (HttpStatusCode)301 or (HttpStatusCode)302 or (HttpStatusCode)303 or (HttpStatusCode)307 or (HttpStatusCode)308))
|
||||
{
|
||||
throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, $"'Failed to fetch, Status code: {(int)resp.StatusCode}.");
|
||||
}
|
||||
|
||||
var respString = resp.Content.ReadAsStringAsync().Result;
|
||||
#pragma warning restore VSTHRD104 // Offer async methods
|
||||
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
|
||||
|
||||
var respHeaders = new ObjectInstance(this._engine);
|
||||
foreach (var respHeader in resp.Headers)
|
||||
respHeaders.Set(respHeader.Key, string.Join(", ", respHeader.Value));
|
||||
|
||||
var result = new ObjectInstance(this._engine);
|
||||
result.Set("body", respString);
|
||||
result.Set("headers", respHeaders);
|
||||
result.Set("ok", resp.IsSuccessStatusCode);
|
||||
result.Set("status", (int)resp.StatusCode);
|
||||
result.Set("statusText", resp.ReasonPhrase);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void SetRequestMethod(HttpRequestMessage requestMessage, JsValue value)
|
||||
{
|
||||
if (value is JsNull or JsUndefined)
|
||||
return;
|
||||
|
||||
var method = value.ToString();
|
||||
requestMessage.Method = method.ToUpperInvariant() switch
|
||||
{
|
||||
"HEAD" => HttpMethod.Head,
|
||||
"GET" => HttpMethod.Get,
|
||||
"POST" => HttpMethod.Post,
|
||||
"PUT" => HttpMethod.Put,
|
||||
"DELETE" => HttpMethod.Delete,
|
||||
"OPTIONS" => HttpMethod.Options,
|
||||
"TRACE" => HttpMethod.Trace,
|
||||
_ => new HttpMethod(method),
|
||||
};
|
||||
}
|
||||
|
||||
private void SetRequestHeader(HttpRequestMessage requestMessage, JsValue value)
|
||||
{
|
||||
if (value is JsNull or JsUndefined)
|
||||
return;
|
||||
|
||||
if (value is ObjectInstance objectInstance)
|
||||
{
|
||||
foreach (var header in objectInstance.GetOwnProperties())
|
||||
{
|
||||
var headerName = header.Key.ToString();
|
||||
var headerValue = header.Value.Value.ToString();
|
||||
|
||||
requestMessage.Headers.Remove(headerName);
|
||||
requestMessage.Headers.TryAddWithoutValidation(headerName, headerValue);
|
||||
}
|
||||
}
|
||||
else if (value is ArrayInstance arrayInstance)
|
||||
{
|
||||
foreach (ArrayInstance header in arrayInstance)
|
||||
{
|
||||
if (header.Length != 2)
|
||||
throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, "The header object must contain exactly two elements.");
|
||||
|
||||
var headerName = header[0].ToString();
|
||||
var headerValue = header[1].ToString();
|
||||
|
||||
requestMessage.Headers.Remove(headerName);
|
||||
requestMessage.Headers.TryAddWithoutValidation(headerName, headerValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, "Only object or array is supported for 'header'.");
|
||||
}
|
||||
}
|
||||
|
||||
private void SetRequestBody(HttpRequestMessage requestMessage, JsValue value)
|
||||
{
|
||||
if (value is JsNull or JsUndefined)
|
||||
return;
|
||||
|
||||
if (value is not JsString jsString)
|
||||
throw new JavaScriptException(this._engine.Realm.Intrinsics.Error, "Only string is supported for 'body'.");
|
||||
|
||||
requestMessage.Content = new StringContent(jsString.ToString());
|
||||
}
|
||||
}
|
||||
}
|
226
BililiveRecorder.Core/Scripting/UserScriptRunner.cs
Normal file
226
BililiveRecorder.Core/Scripting/UserScriptRunner.cs
Normal file
|
@ -0,0 +1,226 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using BililiveRecorder.Core.Config.V3;
|
||||
using BililiveRecorder.Core.Scripting.Runtime;
|
||||
using Esprima;
|
||||
using Esprima.Ast;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using Jint.Native.Function;
|
||||
using Jint.Native.Object;
|
||||
using Jint.Runtime.Interop;
|
||||
using Serilog;
|
||||
|
||||
namespace BililiveRecorder.Core.Scripting
|
||||
{
|
||||
public class UserScriptRunner
|
||||
{
|
||||
private const string RecorderEvents = "recorderEvents";
|
||||
private static readonly JsValue RecorderEventsString = RecorderEvents;
|
||||
private static int ExecutionId = 0;
|
||||
|
||||
private readonly GlobalConfig config;
|
||||
private readonly Options jintOptions;
|
||||
|
||||
private static readonly Script setupScript;
|
||||
|
||||
private string? cachedScriptSource;
|
||||
private Script? cachedScript;
|
||||
|
||||
static UserScriptRunner()
|
||||
{
|
||||
setupScript = new JavaScriptParser(@"
|
||||
globalThis.recorderEvents = {};
|
||||
", new ParserOptions(@"internalSetup.js")).ParseScript();
|
||||
}
|
||||
|
||||
public UserScriptRunner(GlobalConfig config)
|
||||
{
|
||||
this.config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
|
||||
this.jintOptions = new Options()
|
||||
.CatchClrExceptions()
|
||||
.LimitRecursion(100)
|
||||
.RegexTimeoutInterval(TimeSpan.FromSeconds(2))
|
||||
.Configure(engine =>
|
||||
{
|
||||
engine.Realm.GlobalObject.FastAddProperty("dns", new JintDns(engine), writable: false, enumerable: false, configurable: false);
|
||||
engine.Realm.GlobalObject.FastAddProperty("dotnet", new JintDotnet(engine), writable: false, enumerable: false, configurable: false);
|
||||
engine.Realm.GlobalObject.FastAddProperty("fetchSync", new JintFetchSync(engine), writable: false, enumerable: false, configurable: false);
|
||||
});
|
||||
}
|
||||
|
||||
private Script? GetParsedScript()
|
||||
{
|
||||
var source = this.config.UserScript;
|
||||
|
||||
if (this.cachedScript is not null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
{
|
||||
this.cachedScript = null;
|
||||
this.cachedScriptSource = null;
|
||||
return null;
|
||||
}
|
||||
else if (this.cachedScriptSource == source)
|
||||
{
|
||||
return this.cachedScript;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var parser = new JavaScriptParser(source, new ParserOptions("userscript.js"));
|
||||
var script = parser.ParseScript();
|
||||
|
||||
this.cachedScript = script;
|
||||
this.cachedScriptSource = source;
|
||||
|
||||
return script;
|
||||
}
|
||||
|
||||
private Engine CreateJintEngine(ILogger logger)
|
||||
{
|
||||
var engine = new Engine(this.jintOptions);
|
||||
|
||||
engine.Realm.GlobalObject.FastAddProperty("console", new JintConsole(engine, logger), writable: false, enumerable: false, configurable: false);
|
||||
|
||||
engine.Execute(setupScript);
|
||||
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static ILogger BuildLogger(ILogger logger)
|
||||
{
|
||||
var id = Interlocked.Increment(ref ExecutionId);
|
||||
return logger.ForContext<UserScriptRunner>().ForContext(nameof(ExecutionId), id);
|
||||
}
|
||||
|
||||
private FunctionInstance? ExecuteScriptThenGetEventHandler(ILogger logger, string functionName)
|
||||
{
|
||||
var script = this.GetParsedScript();
|
||||
if (script is null)
|
||||
return null;
|
||||
|
||||
var engine = this.CreateJintEngine(logger);
|
||||
engine.Execute(script);
|
||||
|
||||
if (engine.Realm.GlobalObject.Get(RecorderEventsString) is not ObjectInstance events)
|
||||
{
|
||||
logger.Warning("[Script] recorderEvents 被修改为非 object");
|
||||
return null;
|
||||
}
|
||||
|
||||
return events.Get(functionName) as FunctionInstance;
|
||||
}
|
||||
|
||||
public void CallOnTest(ILogger logger, Action<string>? alert)
|
||||
{
|
||||
const string callbackName = "onTest";
|
||||
var log = BuildLogger(logger);
|
||||
try
|
||||
{
|
||||
var func = this.ExecuteScriptThenGetEventHandler(log, callbackName);
|
||||
if (func is null) return;
|
||||
|
||||
_ = func.Engine.Call(func, new DelegateWrapper(func.Engine, alert ?? delegate { }));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Error(ex, $"执行脚本 {callbackName} 时发生错误");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取直播流 URL
|
||||
/// </summary>
|
||||
/// <param name="logger">logger</param>
|
||||
/// <param name="roomid">房间号</param>
|
||||
/// <returns>直播流 URL</returns>
|
||||
public string? CallOnFetchStreamUrl(ILogger logger, int roomid, int[] qnSetting)
|
||||
{
|
||||
const string callbackName = "onFetchStreamUrl";
|
||||
var log = BuildLogger(logger);
|
||||
try
|
||||
{
|
||||
var func = this.ExecuteScriptThenGetEventHandler(log, callbackName);
|
||||
if (func is null) return null;
|
||||
|
||||
var input = new ObjectInstance(func.Engine);
|
||||
input.Set("roomid", roomid);
|
||||
input.Set("qn", JsValue.FromObject(func.Engine, qnSetting));
|
||||
|
||||
var result = func.Engine.Call(func, input);
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case JsString jsString:
|
||||
return jsString.ToString();
|
||||
case JsUndefined or JsNull:
|
||||
return null;
|
||||
default:
|
||||
log.Warning($"{RecorderEvents}.{callbackName}() 返回了不支持的类型: {{ValueType}}", result.Type);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Error(ex, $"执行脚本 {callbackName} 时发生错误");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在发送请求之前修改直播流 URL
|
||||
/// </summary>
|
||||
/// <param name="logger">logger</param>
|
||||
/// <param name="originalUrl">原直播流地址</param>
|
||||
/// <returns>url: 新直播流地址<br/>ip: 可选的强制使用的 IP 地址</returns>
|
||||
public (string url, string? ip)? CallOnTransformStreamUrl(ILogger logger, string originalUrl)
|
||||
{
|
||||
const string callbackName = "onTransformStreamUrl";
|
||||
var log = BuildLogger(logger);
|
||||
try
|
||||
{
|
||||
var func = this.ExecuteScriptThenGetEventHandler(log, callbackName);
|
||||
if (func is null) return null;
|
||||
|
||||
var result = func.Engine.Call(func, originalUrl);
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case JsString jsString:
|
||||
return (jsString.ToString(), null);
|
||||
case ObjectInstance obj:
|
||||
{
|
||||
var url = obj.Get("url");
|
||||
|
||||
if (url is not JsString urlString)
|
||||
{
|
||||
log.Warning($"{RecorderEvents}.{callbackName}() 返回的 object 缺少 url 属性");
|
||||
return null;
|
||||
}
|
||||
|
||||
var ip = obj.Get("ip") as JsString;
|
||||
|
||||
return (urlString.ToString(), ip?.ToString());
|
||||
}
|
||||
case JsUndefined or JsNull:
|
||||
return null;
|
||||
default:
|
||||
log.Warning($"{RecorderEvents}.{callbackName}() 返回了不支持的类型: {{ValueType}}", result.Type);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Error(ex, $"执行脚本 {callbackName} 时发生错误");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -48,6 +48,14 @@
|
|||
<ui:NumberBox Minimum="0" SmallChange="1" Text="{Binding RecordDanmakuFlushInterval,UpdateSourceTrigger=PropertyChanged}"/>
|
||||
</c:SettingWithDefault>
|
||||
</GroupBox>
|
||||
<GroupBox Header="Scripting">
|
||||
<StackPanel>
|
||||
<Button Click="TestScript_Click" Content="Test"/>
|
||||
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasUserScript}" Header="请自由发挥((">
|
||||
<TextBox Text="{Binding UserScript, UpdateSourceTrigger=LostFocus}" AcceptsReturn="True" MinHeight="70" MaxHeight="130"/>
|
||||
</c:SettingWithDefault>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox Header="Timing">
|
||||
<ui:SimpleStackPanel Spacing="10">
|
||||
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasTimingStreamRetry}" Header="录制重试间隔">
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Runtime.Serialization;
|
|||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using BililiveRecorder.Core.Api.Http;
|
||||
using BililiveRecorder.Core.Scripting;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog;
|
||||
|
||||
|
@ -16,16 +17,21 @@ namespace BililiveRecorder.WPF.Pages
|
|||
{
|
||||
private static readonly ILogger logger = Log.ForContext<AdvancedSettingsPage>();
|
||||
private readonly HttpApiClient? httpApiClient;
|
||||
private readonly UserScriptRunner? userScriptRunner;
|
||||
|
||||
public AdvancedSettingsPage(HttpApiClient? httpApiClient)
|
||||
public AdvancedSettingsPage(HttpApiClient? httpApiClient, UserScriptRunner? userScriptRunner)
|
||||
{
|
||||
this.InitializeComponent();
|
||||
this.httpApiClient = httpApiClient;
|
||||
this.userScriptRunner = userScriptRunner;
|
||||
}
|
||||
|
||||
public AdvancedSettingsPage() : this((HttpApiClient?)(RootPage.ServiceProvider?.GetService(typeof(HttpApiClient))))
|
||||
{
|
||||
}
|
||||
public AdvancedSettingsPage()
|
||||
: this(
|
||||
(HttpApiClient?)(RootPage.ServiceProvider?.GetService(typeof(HttpApiClient))),
|
||||
(UserScriptRunner?)(RootPage.ServiceProvider?.GetService(typeof(UserScriptRunner)))
|
||||
)
|
||||
{ }
|
||||
|
||||
private void Crash_Click(object sender, RoutedEventArgs e) => throw new TestException("test crash triggered");
|
||||
|
||||
|
@ -75,5 +81,10 @@ namespace BililiveRecorder.WPF.Pages
|
|||
|
||||
MessageBox.Show("User: " + jo["data"]?["uname"]?.ToObject<string>(), "Cookie Test - Successed", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
private void TestScript_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = Task.Run(() => this.userScriptRunner?.CallOnTest(Log.Logger, str => MessageBox.Show(str)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,16 +62,6 @@ namespace BililiveRecorder.WPF.Pages
|
|||
this.Model = new RootModel();
|
||||
this.DataContext = this.Model;
|
||||
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services
|
||||
.AddFlv()
|
||||
.AddRecorder()
|
||||
;
|
||||
|
||||
this.serviceProvider = services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
this.InitializeComponent();
|
||||
this.AdvancedSettingsPageItem.Visibility = Visibility.Hidden;
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ using System.CommandLine.Invocation;
|
|||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
|
@ -11,7 +12,10 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using System.Windows.Threading;
|
||||
using BililiveRecorder.ToolBox;
|
||||
using Esprima;
|
||||
using Jint.Runtime;
|
||||
using Sentry;
|
||||
using Sentry.Extensibility;
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
using Serilog.Exceptions;
|
||||
|
@ -237,7 +241,11 @@ namespace BililiveRecorder.WPF
|
|||
o.IsGlobalModeEnabled = true;
|
||||
o.DisableAppDomainUnhandledExceptionCapture();
|
||||
o.DisableTaskUnobservedTaskExceptionCapture();
|
||||
o.AddExceptionFilterForType<System.Net.Http.HttpRequestException>();
|
||||
o.AddExceptionFilterForType<HttpRequestException>();
|
||||
o.AddExceptionFilterForType<OutOfMemoryException>();
|
||||
o.AddExceptionFilterForType<JintException>();
|
||||
o.AddExceptionFilterForType<ParserException>();
|
||||
o.AddEventProcessor(new SentryEventProcessor());
|
||||
|
||||
o.TextFormatter = new MessageTemplateTextFormatter("[{RoomId}] {Message}{NewLine}{Exception}{@ExceptionDetail:j}");
|
||||
|
||||
|
@ -269,5 +277,12 @@ namespace BililiveRecorder.WPF
|
|||
[HandleProcessCorruptedStateExceptions, SecurityCritical]
|
||||
private static void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) =>
|
||||
logger.Fatal(e.Exception, "Unhandled exception from Application.DispatcherUnhandledException");
|
||||
|
||||
private class SentryEventProcessor : ISentryEventProcessor
|
||||
{
|
||||
private static readonly string JintConsole = typeof(Core.Scripting.Runtime.JintConsole).FullName;
|
||||
private static readonly string UserScriptRunner = typeof(Core.Scripting.UserScriptRunner).FullName;
|
||||
public SentryEvent? Process(SentryEvent e) => (e?.Logger == JintConsole || e?.Logger == UserScriptRunner) ? null : e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ namespace BililiveRecorder.Web.Models
|
|||
public Optional<uint>? OptionalRecordDanmakuFlushInterval { get; set; }
|
||||
public Optional<bool>? OptionalNetworkTransportUseSystemProxy { get; set; }
|
||||
public Optional<AllowedAddressFamily>? OptionalNetworkTransportAllowedAddressFamily { get; set; }
|
||||
public Optional<string>? OptionalUserScript { get; set; }
|
||||
|
||||
public void ApplyTo(GlobalConfig config)
|
||||
{
|
||||
|
@ -91,6 +92,7 @@ namespace BililiveRecorder.Web.Models
|
|||
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;
|
||||
if (this.OptionalUserScript.HasValue) config.OptionalUserScript = this.OptionalUserScript.Value;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,6 +140,7 @@ namespace BililiveRecorder.Web.Models.Rest
|
|||
public Optional<uint> OptionalRecordDanmakuFlushInterval { get; set; }
|
||||
public Optional<bool> OptionalNetworkTransportUseSystemProxy { get; set; }
|
||||
public Optional<AllowedAddressFamily> OptionalNetworkTransportAllowedAddressFamily { get; set; }
|
||||
public Optional<string> OptionalUserScript { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -190,6 +193,7 @@ namespace BililiveRecorder.Web.Models.Graphql
|
|||
this.Field(x => x.OptionalRecordDanmakuFlushInterval, type: typeof(HierarchicalOptionalType<uint>));
|
||||
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>));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -221,6 +225,7 @@ namespace BililiveRecorder.Web.Models.Graphql
|
|||
this.Field(x => x.RecordDanmakuFlushInterval);
|
||||
this.Field(x => x.NetworkTransportUseSystemProxy);
|
||||
this.Field(x => x.NetworkTransportAllowedAddressFamily);
|
||||
this.Field(x => x.UserScript);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -269,6 +274,7 @@ namespace BililiveRecorder.Web.Models.Graphql
|
|||
this.Field(x => x.OptionalRecordDanmakuFlushInterval, nullable: true, type: typeof(HierarchicalOptionalInputType<uint>));
|
||||
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>));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -261,6 +261,22 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"UserScript": {
|
||||
"description": "自定义脚本\n默认: ",
|
||||
"markdownDescription": "自定义脚本 \n默认: ` `\n\n",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"HasValue": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"Value": {
|
||||
"type": "string",
|
||||
"default": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"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/)",
|
||||
|
|
|
@ -234,4 +234,13 @@ export const data: Array<ConfigEntry> = [
|
|||
advancedConfig: true,
|
||||
markdown: ""
|
||||
},
|
||||
{
|
||||
name: "UserScript",
|
||||
description: "自定义脚本",
|
||||
type: "string",
|
||||
defaultValue: "string.Empty",
|
||||
configType: "globalOnly",
|
||||
advancedConfig: true,
|
||||
markdown: ""
|
||||
},
|
||||
];
|
||||
|
|
7
nuget.config
Normal file
7
nuget.config
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="NuGet" value="https://api.nuget.org/v3/index.json" />
|
||||
<add key="Jint" value="https://www.myget.org/F/jint/api/v3/index.json" />
|
||||
</packageSources>
|
||||
</configuration>
|
Loading…
Reference in New Issue
Block a user