mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-15 19:22:19 +08:00
Core: Add scripting support
This commit is contained in:
parent
cc27045fc4
commit
bb06539993
|
@ -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;
|
||||
|
@ -218,6 +221,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, 0);
|
||||
}
|
||||
|
||||
const int DefaultQn = 10000;
|
||||
var selected_qn = DefaultQn;
|
||||
var codecItem = await this.apiClient.GetCodecItemInStreamUrlAsync(roomid: roomid, qn: DefaultQn).ConfigureAwait(false);
|
||||
|
@ -225,12 +241,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 +290,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 +356,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));
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
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;
|
||||
|
@ -9,6 +11,7 @@ using Jint.Native.Object;
|
|||
using Jint.Runtime;
|
||||
using Jint.Runtime.Interop;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace BililiveRecorder.Core.Scripting.Runtime
|
||||
{
|
||||
|
@ -16,9 +19,26 @@ namespace BililiveRecorder.Core.Scripting.Runtime
|
|||
{
|
||||
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));
|
||||
|
@ -30,15 +50,15 @@ namespace BililiveRecorder.Core.Scripting.Runtime
|
|||
Add("clear", this.Clear);
|
||||
Add("count", this.Count);
|
||||
Add("countReset", this.CountReset);
|
||||
Add("debug", this.Debug);
|
||||
Add("error", this.Error);
|
||||
Add("info", this.Info);
|
||||
Add("log", this.Log);
|
||||
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.Trace);
|
||||
Add("warn", this.Warn);
|
||||
Add("trace", this.BuildLogFunc(LogEventLevel.Information));
|
||||
Add("warn", this.BuildLogFunc(LogEventLevel.Warning));
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
void Add(string name, Func<JsValue, JsValue[], JsValue> func)
|
||||
|
@ -71,6 +91,31 @@ namespace BililiveRecorder.Core.Scripting.Runtime
|
|||
// 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(0))
|
||||
|
@ -96,6 +141,7 @@ namespace BililiveRecorder.Core.Scripting.Runtime
|
|||
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++;
|
||||
|
@ -107,7 +153,6 @@ namespace BililiveRecorder.Core.Scripting.Runtime
|
|||
this.counters[name] = count;
|
||||
|
||||
this.logger.Information("[Script] {CounterName}: {Count}", name, count);
|
||||
|
||||
return Undefined;
|
||||
}
|
||||
|
||||
|
@ -119,34 +164,6 @@ namespace BililiveRecorder.Core.Scripting.Runtime
|
|||
return Undefined;
|
||||
}
|
||||
|
||||
private JsValue Debug(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
var messages = this.FormatToString(arguments);
|
||||
this.logger.Debug("[Script] {Messages}", messages);
|
||||
return Undefined;
|
||||
}
|
||||
|
||||
private JsValue Error(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
var messages = this.FormatToString(arguments);
|
||||
this.logger.Error("[Script] {Messages}", messages);
|
||||
return Undefined;
|
||||
}
|
||||
|
||||
private JsValue Info(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
var messages = this.FormatToString(arguments);
|
||||
this.logger.Information("[Script] {Messages}", messages);
|
||||
return Undefined;
|
||||
}
|
||||
|
||||
private JsValue Log(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
var messages = this.FormatToString(arguments);
|
||||
this.logger.Information("[Script] {Messages}", messages);
|
||||
return Undefined;
|
||||
}
|
||||
|
||||
private JsValue Time(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
var name = arguments.Length > 0 ? arguments[0].ToString() : "default";
|
||||
|
@ -190,19 +207,5 @@ namespace BililiveRecorder.Core.Scripting.Runtime
|
|||
}
|
||||
return Undefined;
|
||||
}
|
||||
|
||||
private JsValue Trace(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
var messages = this.FormatToString(arguments);
|
||||
this.logger.Information("[Script] {Messages}", messages);
|
||||
return Undefined;
|
||||
}
|
||||
|
||||
private JsValue Warn(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
var messages = this.FormatToString(arguments);
|
||||
this.logger.Warning("[Script] {Messages}", messages);
|
||||
return Undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using Jint.Native.Function;
|
||||
|
@ -14,7 +16,15 @@ namespace BililiveRecorder.Core.Scripting.Runtime
|
|||
|
||||
protected override JsValue Call(JsValue thisObject, JsValue[] arguments)
|
||||
{
|
||||
return Undefined;
|
||||
var (promise, resolve, reject) = this._engine.RegisterPromise();
|
||||
|
||||
var task = Task.Run(() =>
|
||||
{
|
||||
|
||||
});
|
||||
var req = new HttpRequestMessage(HttpMethod.Get, "https://example.com");
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,14 +5,18 @@ 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;
|
||||
|
@ -27,7 +31,7 @@ namespace BililiveRecorder.Core.Scripting
|
|||
{
|
||||
setupScript = new JavaScriptParser(@"
|
||||
globalThis.recorderEvents = {};
|
||||
").ParseScript();
|
||||
", new ParserOptions(@"internalSetup.js")).ParseScript();
|
||||
}
|
||||
|
||||
public UserScriptRunner(GlobalConfig config)
|
||||
|
@ -69,7 +73,7 @@ globalThis.recorderEvents = {};
|
|||
return null;
|
||||
}
|
||||
|
||||
var parser = new JavaScriptParser(source);
|
||||
var parser = new JavaScriptParser(source, new ParserOptions("userscript.js"));
|
||||
var script = parser.ParseScript();
|
||||
|
||||
this.cachedScript = script;
|
||||
|
@ -89,55 +93,134 @@ globalThis.recorderEvents = {};
|
|||
return engine;
|
||||
}
|
||||
|
||||
public string CallOnStreamUrl(ILogger logger, string originalUrl)
|
||||
{
|
||||
var log = BuildLogger(logger);
|
||||
|
||||
var script = this.GetParsedScript();
|
||||
|
||||
if (script is null)
|
||||
{
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
var engine = this.CreateJintEngine(log);
|
||||
|
||||
engine.Execute(script);
|
||||
|
||||
if (engine.Realm.GlobalObject.Get("recorderEvents") is not ObjectInstance events)
|
||||
{
|
||||
log.Warning("脚本: recorderEvents 被修改为非 object");
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
if (events.Get("onStreamUrl") is not FunctionInstance func)
|
||||
{
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
var result = engine.Call(func, originalUrl);
|
||||
|
||||
if (result.Type == Jint.Runtime.Types.String)
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
else if (result.Type == Jint.Runtime.Types.Object)
|
||||
{
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user