mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-15 19:22:19 +08:00
Merge branch 'local_feature/new_flv'
This commit is contained in:
parent
43379957f4
commit
48c8612f95
|
@ -28,8 +28,9 @@
|
||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Autofac" Version="4.9.4" />
|
|
||||||
<PackageReference Include="CommandLineParser" Version="2.4.3" />
|
<PackageReference Include="CommandLineParser" Version="2.4.3" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||||
<PackageReference Include="NLog" Version="4.7.6" />
|
<PackageReference Include="NLog" Version="4.7.6" />
|
||||||
<PackageReference Include="NLog.Config" Version="4.7.6" />
|
<PackageReference Include="NLog.Config" Version="4.7.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -2,11 +2,11 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Autofac;
|
|
||||||
using BililiveRecorder.Core;
|
using BililiveRecorder.Core;
|
||||||
using BililiveRecorder.Core.Config.V2;
|
using BililiveRecorder.Core.Config.V2;
|
||||||
using BililiveRecorder.FlvProcessor;
|
using BililiveRecorder.DependencyInjection;
|
||||||
using CommandLine;
|
using CommandLine;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace BililiveRecorder.Cli
|
namespace BililiveRecorder.Cli
|
||||||
{
|
{
|
||||||
|
@ -19,10 +19,10 @@ namespace BililiveRecorder.Cli
|
||||||
|
|
||||||
private static int RunConfigMode(CmdVerbConfigMode opts)
|
private static int RunConfigMode(CmdVerbConfigMode opts)
|
||||||
{
|
{
|
||||||
var container = CreateBuilder().Build();
|
|
||||||
var rootScope = container.BeginLifetimeScope("recorder_root");
|
|
||||||
var semaphore = new SemaphoreSlim(0, 1);
|
var semaphore = new SemaphoreSlim(0, 1);
|
||||||
var recorder = rootScope.Resolve<IRecorder>();
|
|
||||||
|
var serviceProvider = BuildServiceProvider();
|
||||||
|
var recorder = serviceProvider.GetRequiredService<IRecorder>();
|
||||||
|
|
||||||
ConsoleCancelEventHandler p = null!;
|
ConsoleCancelEventHandler p = null!;
|
||||||
p = (sender, e) =>
|
p = (sender, e) =>
|
||||||
|
@ -46,10 +46,11 @@ namespace BililiveRecorder.Cli
|
||||||
|
|
||||||
private static int RunPortableMode(CmdVerbPortableMode opts)
|
private static int RunPortableMode(CmdVerbPortableMode opts)
|
||||||
{
|
{
|
||||||
var container = CreateBuilder().Build();
|
|
||||||
var rootScope = container.BeginLifetimeScope("recorder_root");
|
|
||||||
var semaphore = new SemaphoreSlim(0, 1);
|
var semaphore = new SemaphoreSlim(0, 1);
|
||||||
var recorder = rootScope.Resolve<IRecorder>();
|
|
||||||
|
var serviceProvider = BuildServiceProvider();
|
||||||
|
var recorder = serviceProvider.GetRequiredService<IRecorder>();
|
||||||
|
|
||||||
var config = new ConfigV2()
|
var config = new ConfigV2()
|
||||||
{
|
{
|
||||||
DisableConfigSave = true,
|
DisableConfigSave = true,
|
||||||
|
@ -85,12 +86,12 @@ namespace BililiveRecorder.Cli
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ContainerBuilder CreateBuilder()
|
private static IServiceProvider BuildServiceProvider()
|
||||||
{
|
{
|
||||||
var builder = new ContainerBuilder();
|
var services = new ServiceCollection();
|
||||||
builder.RegisterModule<FlvProcessorModule>();
|
services.AddFlvProcessor();
|
||||||
builder.RegisterModule<CoreModule>();
|
services.AddCore();
|
||||||
return builder;
|
return services.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,10 @@
|
||||||
<Compile Include="..\TempBuildInfo\BuildInfo.Core.cs" />
|
<Compile Include="..\TempBuildInfo\BuildInfo.Core.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Autofac" Version="4.9.4" />
|
|
||||||
<PackageReference Include="JsonSubTypes" Version="1.8.0" />
|
<PackageReference Include="JsonSubTypes" Version="1.8.0" />
|
||||||
<PackageReference Include="HierarchicalPropertyDefault" Version="0.1.1-beta-g721d36b97c" />
|
<PackageReference Include="HierarchicalPropertyDefault" Version="0.1.1-beta-g721d36b97c" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="NLog" Version="4.7.6" />
|
<PackageReference Include="NLog" Version="4.7.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
using System.Net.Sockets;
|
|
||||||
using Autofac;
|
|
||||||
using BililiveRecorder.Core.Config.V2;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
namespace BililiveRecorder.Core
|
|
||||||
{
|
|
||||||
public class CoreModule : Module
|
|
||||||
{
|
|
||||||
public CoreModule()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Load(ContainerBuilder builder)
|
|
||||||
{
|
|
||||||
builder.Register(x => x.Resolve<IRecorder>().Config).As<ConfigV2>();
|
|
||||||
builder.Register(x => x.Resolve<ConfigV2>().Global).As<GlobalConfig>();
|
|
||||||
builder.RegisterType<BililiveAPI>().AsSelf().InstancePerMatchingLifetimeScope("recorder_root");
|
|
||||||
builder.RegisterType<TcpClient>().AsSelf().ExternallyOwned();
|
|
||||||
builder.RegisterType<StreamMonitor>().As<IStreamMonitor>().ExternallyOwned();
|
|
||||||
builder.RegisterType<RecordedRoom>().As<IRecordedRoom>().ExternallyOwned();
|
|
||||||
builder.RegisterType<BasicDanmakuWriter>().As<IBasicDanmakuWriter>().ExternallyOwned();
|
|
||||||
builder.RegisterType<Recorder>().As<IRecorder>().InstancePerMatchingLifetimeScope("recorder_root");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
20
BililiveRecorder.Core/DependencyInjectionExtensions.cs
Normal file
20
BililiveRecorder.Core/DependencyInjectionExtensions.cs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
using BililiveRecorder.Core;
|
||||||
|
using BililiveRecorder.Core.Config.V2;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.DependencyInjection
|
||||||
|
{
|
||||||
|
public static class DependencyInjectionExtensions
|
||||||
|
{
|
||||||
|
public static void AddCore(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<IRecorder, Recorder>();
|
||||||
|
#pragma warning disable IDE0001
|
||||||
|
services.AddSingleton<ConfigV2>(x => x.GetRequiredService<IRecorder>().Config);
|
||||||
|
services.AddSingleton<GlobalConfig>(x => x.GetRequiredService<ConfigV2>().Global);
|
||||||
|
#pragma warning restore IDE0001
|
||||||
|
services.AddSingleton<BililiveAPI>();
|
||||||
|
services.AddSingleton<IRecordedRoomFactory, RecordedRoomFactory>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
BililiveRecorder.Core/IRecordedRoomFactory.cs
Normal file
9
BililiveRecorder.Core/IRecordedRoomFactory.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
using BililiveRecorder.Core.Config.V2;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Core
|
||||||
|
{
|
||||||
|
public interface IRecordedRoomFactory
|
||||||
|
{
|
||||||
|
IRecordedRoom CreateRecordedRoom(RoomConfig roomConfig);
|
||||||
|
}
|
||||||
|
}
|
|
@ -113,7 +113,7 @@ namespace BililiveRecorder.Core
|
||||||
public event EventHandler<RecordEndData> RecordEnded;
|
public event EventHandler<RecordEndData> RecordEnded;
|
||||||
|
|
||||||
private readonly IBasicDanmakuWriter basicDanmakuWriter;
|
private readonly IBasicDanmakuWriter basicDanmakuWriter;
|
||||||
private readonly Func<IFlvStreamProcessor> newIFlvStreamProcessor;
|
private readonly IProcessorFactory processorFactory;
|
||||||
private IFlvStreamProcessor _processor;
|
private IFlvStreamProcessor _processor;
|
||||||
public IFlvStreamProcessor Processor
|
public IFlvStreamProcessor Processor
|
||||||
{
|
{
|
||||||
|
@ -156,9 +156,9 @@ namespace BililiveRecorder.Core
|
||||||
public Guid Guid { get; } = Guid.NewGuid();
|
public Guid Guid { get; } = Guid.NewGuid();
|
||||||
|
|
||||||
// TODO: 重构 DI
|
// TODO: 重构 DI
|
||||||
public RecordedRoom(Func<RoomConfig, IBasicDanmakuWriter> newBasicDanmakuWriter,
|
public RecordedRoom(IBasicDanmakuWriter basicDanmakuWriter,
|
||||||
Func<RoomConfig, IStreamMonitor> newIStreamMonitor,
|
IStreamMonitor streamMonitor,
|
||||||
Func<IFlvStreamProcessor> newIFlvStreamProcessor,
|
IProcessorFactory processorFactory,
|
||||||
BililiveAPI bililiveAPI,
|
BililiveAPI bililiveAPI,
|
||||||
RoomConfig roomConfig)
|
RoomConfig roomConfig)
|
||||||
{
|
{
|
||||||
|
@ -167,11 +167,11 @@ namespace BililiveRecorder.Core
|
||||||
|
|
||||||
this.BililiveAPI = bililiveAPI;
|
this.BililiveAPI = bililiveAPI;
|
||||||
|
|
||||||
this.newIFlvStreamProcessor = newIFlvStreamProcessor;
|
this.processorFactory = processorFactory;
|
||||||
|
|
||||||
this.basicDanmakuWriter = newBasicDanmakuWriter(this.RoomConfig);
|
this.basicDanmakuWriter = basicDanmakuWriter;
|
||||||
|
|
||||||
this.StreamMonitor = newIStreamMonitor(this.RoomConfig);
|
this.StreamMonitor = streamMonitor;
|
||||||
this.StreamMonitor.RoomInfoUpdated += this.StreamMonitor_RoomInfoUpdated;
|
this.StreamMonitor.RoomInfoUpdated += this.StreamMonitor_RoomInfoUpdated;
|
||||||
this.StreamMonitor.StreamStarted += this.StreamMonitor_StreamStarted;
|
this.StreamMonitor.StreamStarted += this.StreamMonitor_StreamStarted;
|
||||||
this.StreamMonitor.ReceivedDanmaku += this.StreamMonitor_ReceivedDanmaku;
|
this.StreamMonitor.ReceivedDanmaku += this.StreamMonitor_ReceivedDanmaku;
|
||||||
|
@ -377,7 +377,7 @@ namespace BililiveRecorder.Core
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
this.Processor = this.newIFlvStreamProcessor().Initialize(this.GetStreamFilePath, this.GetClipFilePath, this.RoomConfig.EnabledFeature, this.RoomConfig.CuttingMode);
|
this.Processor = this.processorFactory.CreateStreamProcessor().Initialize(this.GetStreamFilePath, this.GetClipFilePath, this.RoomConfig.EnabledFeature, this.RoomConfig.CuttingMode);
|
||||||
this.Processor.ClipLengthFuture = this.RoomConfig.ClipLengthFuture;
|
this.Processor.ClipLengthFuture = this.RoomConfig.ClipLengthFuture;
|
||||||
this.Processor.ClipLengthPast = this.RoomConfig.ClipLengthPast;
|
this.Processor.ClipLengthPast = this.RoomConfig.ClipLengthPast;
|
||||||
this.Processor.CuttingNumber = this.RoomConfig.CuttingNumber;
|
this.Processor.CuttingNumber = this.RoomConfig.CuttingNumber;
|
||||||
|
|
25
BililiveRecorder.Core/RecordedRoomFactory.cs
Normal file
25
BililiveRecorder.Core/RecordedRoomFactory.cs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
using System;
|
||||||
|
using BililiveRecorder.Core.Config.V2;
|
||||||
|
using BililiveRecorder.FlvProcessor;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Core
|
||||||
|
{
|
||||||
|
public class RecordedRoomFactory : IRecordedRoomFactory
|
||||||
|
{
|
||||||
|
private readonly IProcessorFactory processorFactory;
|
||||||
|
private readonly BililiveAPI bililiveAPI;
|
||||||
|
|
||||||
|
public RecordedRoomFactory(IProcessorFactory processorFactory, BililiveAPI bililiveAPI)
|
||||||
|
{
|
||||||
|
this.processorFactory = processorFactory ?? throw new ArgumentNullException(nameof(processorFactory));
|
||||||
|
this.bililiveAPI = bililiveAPI ?? throw new ArgumentNullException(nameof(bililiveAPI));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IRecordedRoom CreateRecordedRoom(RoomConfig roomConfig)
|
||||||
|
{
|
||||||
|
var basicDanmakuWriter = new BasicDanmakuWriter(roomConfig);
|
||||||
|
var streamMonitor = new StreamMonitor(roomConfig, this.bililiveAPI);
|
||||||
|
return new RecordedRoom(basicDanmakuWriter, streamMonitor, this.processorFactory, this.bililiveAPI, roomConfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ using System.Threading;
|
||||||
using BililiveRecorder.Core.Callback;
|
using BililiveRecorder.Core.Callback;
|
||||||
using BililiveRecorder.Core.Config;
|
using BililiveRecorder.Core.Config;
|
||||||
using BililiveRecorder.Core.Config.V2;
|
using BililiveRecorder.Core.Config.V2;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
@ -18,9 +19,9 @@ namespace BililiveRecorder.Core
|
||||||
{
|
{
|
||||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||||
|
|
||||||
private readonly Func<RoomConfig, IRecordedRoom> newIRecordedRoom;
|
|
||||||
private readonly CancellationTokenSource tokenSource;
|
private readonly CancellationTokenSource tokenSource;
|
||||||
|
private readonly IServiceProvider serviceProvider;
|
||||||
|
private IRecordedRoomFactory? recordedRoomFactory;
|
||||||
private bool _valid = false;
|
private bool _valid = false;
|
||||||
private bool disposedValue;
|
private bool disposedValue;
|
||||||
|
|
||||||
|
@ -34,10 +35,9 @@ namespace BililiveRecorder.Core
|
||||||
public bool IsReadOnly => true;
|
public bool IsReadOnly => true;
|
||||||
public IRecordedRoom this[int index] => this.Rooms[index];
|
public IRecordedRoom this[int index] => this.Rooms[index];
|
||||||
|
|
||||||
public Recorder(Func<RoomConfig, IRecordedRoom> iRecordedRoom)
|
public Recorder(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
this.newIRecordedRoom = iRecordedRoom ?? throw new ArgumentNullException(nameof(iRecordedRoom));
|
this.serviceProvider = serviceProvider;
|
||||||
|
|
||||||
this.tokenSource = new CancellationTokenSource();
|
this.tokenSource = new CancellationTokenSource();
|
||||||
Repeat.Interval(TimeSpan.FromSeconds(3), this.DownloadWatchdog, this.tokenSource.Token);
|
Repeat.Interval(TimeSpan.FromSeconds(3), this.DownloadWatchdog, this.tokenSource.Token);
|
||||||
|
|
||||||
|
@ -60,6 +60,7 @@ namespace BililiveRecorder.Core
|
||||||
this.Config = config;
|
this.Config = config;
|
||||||
this.Config.Global.WorkDirectory = workdir;
|
this.Config.Global.WorkDirectory = workdir;
|
||||||
this.Webhook = new BasicWebhook(this.Config);
|
this.Webhook = new BasicWebhook(this.Config);
|
||||||
|
this.recordedRoomFactory = this.serviceProvider.GetRequiredService<IRecordedRoomFactory>();
|
||||||
this._valid = true;
|
this._valid = true;
|
||||||
this.Config.Rooms.ForEach(r => this.AddRoom(r));
|
this.Config.Rooms.ForEach(r => this.AddRoom(r));
|
||||||
ConfigParser.SaveTo(this.Config.Global.WorkDirectory, this.Config);
|
ConfigParser.SaveTo(this.Config.Global.WorkDirectory, this.Config);
|
||||||
|
@ -83,6 +84,7 @@ namespace BililiveRecorder.Core
|
||||||
logger.Debug("Initialize With Config.");
|
logger.Debug("Initialize With Config.");
|
||||||
this.Config = config;
|
this.Config = config;
|
||||||
this.Webhook = new BasicWebhook(this.Config);
|
this.Webhook = new BasicWebhook(this.Config);
|
||||||
|
this.recordedRoomFactory = this.serviceProvider.GetRequiredService<IRecordedRoomFactory>();
|
||||||
this._valid = true;
|
this._valid = true;
|
||||||
this.Config.Rooms.ForEach(r => this.AddRoom(r));
|
this.Config.Rooms.ForEach(r => this.AddRoom(r));
|
||||||
return true;
|
return true;
|
||||||
|
@ -136,7 +138,7 @@ namespace BililiveRecorder.Core
|
||||||
if (!this._valid) { throw new InvalidOperationException("Not Initialized"); }
|
if (!this._valid) { throw new InvalidOperationException("Not Initialized"); }
|
||||||
|
|
||||||
roomConfig.SetParent(this.Config?.Global);
|
roomConfig.SetParent(this.Config?.Global);
|
||||||
var rr = this.newIRecordedRoom(roomConfig);
|
var rr = this.recordedRoomFactory!.CreateRecordedRoom(roomConfig);
|
||||||
|
|
||||||
logger.Debug("AddRoom 添加了 {roomid} 直播间 ", rr.RoomId);
|
logger.Debug("AddRoom 添加了 {roomid} 直播间 ", rr.RoomId);
|
||||||
rr.RecordEnded += this.RecordedRoom_RecordEnded;
|
rr.RecordEnded += this.RecordedRoom_RecordEnded;
|
||||||
|
|
|
@ -30,7 +30,6 @@ namespace BililiveRecorder.Core
|
||||||
{
|
{
|
||||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||||
|
|
||||||
private readonly Func<TcpClient> funcTcpClient;
|
|
||||||
private readonly RoomConfig roomConfig;
|
private readonly RoomConfig roomConfig;
|
||||||
private readonly BililiveAPI bililiveAPI;
|
private readonly BililiveAPI bililiveAPI;
|
||||||
|
|
||||||
|
@ -51,9 +50,8 @@ namespace BililiveRecorder.Core
|
||||||
public event ReceivedDanmakuEvt ReceivedDanmaku;
|
public event ReceivedDanmakuEvt ReceivedDanmaku;
|
||||||
public event PropertyChangedEventHandler PropertyChanged;
|
public event PropertyChangedEventHandler PropertyChanged;
|
||||||
|
|
||||||
public StreamMonitor(RoomConfig roomConfig, Func<TcpClient> funcTcpClient, BililiveAPI bililiveAPI)
|
public StreamMonitor(RoomConfig roomConfig, BililiveAPI bililiveAPI)
|
||||||
{
|
{
|
||||||
this.funcTcpClient = funcTcpClient;
|
|
||||||
this.roomConfig = roomConfig;
|
this.roomConfig = roomConfig;
|
||||||
this.bililiveAPI = bililiveAPI;
|
this.bililiveAPI = bililiveAPI;
|
||||||
|
|
||||||
|
@ -216,7 +214,7 @@ namespace BililiveRecorder.Core
|
||||||
|
|
||||||
logger.Log(this.RoomId, LogLevel.Debug, $"连接弹幕服务器 {host}:{port} {(string.IsNullOrWhiteSpace(token) ? "无" : "有")} token");
|
logger.Log(this.RoomId, LogLevel.Debug, $"连接弹幕服务器 {host}:{port} {(string.IsNullOrWhiteSpace(token) ? "无" : "有")} token");
|
||||||
|
|
||||||
this.dmClient = this.funcTcpClient();
|
this.dmClient = new TcpClient();
|
||||||
await this.dmClient.ConnectAsync(host, port).ConfigureAwait(false);
|
await this.dmClient.ConnectAsync(host, port).ConfigureAwait(false);
|
||||||
this.dmNetStream = this.dmClient.GetStream();
|
this.dmNetStream = this.dmClient.GetStream();
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsDanmakuConnected)));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsDanmakuConnected)));
|
||||||
|
|
20
BililiveRecorder.Flv/Amf/AmfCollectionDebugView.cs
Normal file
20
BililiveRecorder.Flv/Amf/AmfCollectionDebugView.cs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
internal sealed class AmfCollectionDebugView
|
||||||
|
{
|
||||||
|
private readonly ICollection<IScriptDataValue> _collection;
|
||||||
|
|
||||||
|
public AmfCollectionDebugView(ICollection<IScriptDataValue> collection)
|
||||||
|
{
|
||||||
|
this._collection = collection ?? throw new ArgumentNullException(nameof(collection));
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
|
||||||
|
public IScriptDataValue[] Items => this._collection.ToArray();
|
||||||
|
}
|
||||||
|
}
|
21
BililiveRecorder.Flv/Amf/AmfDictionaryDebugView.cs
Normal file
21
BililiveRecorder.Flv/Amf/AmfDictionaryDebugView.cs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
internal sealed class AmfDictionaryDebugView
|
||||||
|
{
|
||||||
|
private readonly IDictionary<string, IScriptDataValue> _dict;
|
||||||
|
|
||||||
|
public AmfDictionaryDebugView(IDictionary<string, IScriptDataValue> dictionary)
|
||||||
|
{
|
||||||
|
this._dict = dictionary ?? throw new ArgumentNullException(nameof(dictionary));
|
||||||
|
}
|
||||||
|
|
||||||
|
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
|
||||||
|
public KeyValuePairDebugView<string, IScriptDataValue>[] Items
|
||||||
|
=> this._dict.Select(x => new KeyValuePairDebugView<string, IScriptDataValue>(x.Key, x.Value)).ToArray();
|
||||||
|
}
|
||||||
|
}
|
17
BililiveRecorder.Flv/Amf/AmfException.cs
Normal file
17
BililiveRecorder.Flv/Amf/AmfException.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
public class AmfException : Exception
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public AmfException() { }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public AmfException(string message) : base(message) { }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public AmfException(string message, Exception innerException) : base(message, innerException) { }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected AmfException(SerializationInfo info, StreamingContext context) : base(info, context) { }
|
||||||
|
}
|
||||||
|
}
|
26
BililiveRecorder.Flv/Amf/IScriptDataValue.cs
Normal file
26
BililiveRecorder.Flv/Amf/IScriptDataValue.cs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
using System.IO;
|
||||||
|
using JsonSubTypes;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
[JsonObject(MemberSerialization = MemberSerialization.OptIn)]
|
||||||
|
[JsonConverter(typeof(JsonSubtypes), nameof(Type))]
|
||||||
|
[JsonSubtypes.KnownSubType(typeof(ScriptDataNumber), ScriptDataType.Number)]
|
||||||
|
[JsonSubtypes.KnownSubType(typeof(ScriptDataBoolean), ScriptDataType.Boolean)]
|
||||||
|
[JsonSubtypes.KnownSubType(typeof(ScriptDataString), ScriptDataType.String)]
|
||||||
|
[JsonSubtypes.KnownSubType(typeof(ScriptDataObject), ScriptDataType.Object)]
|
||||||
|
[JsonSubtypes.KnownSubType(typeof(ScriptDataNull), ScriptDataType.Null)]
|
||||||
|
[JsonSubtypes.KnownSubType(typeof(ScriptDataUndefined), ScriptDataType.Undefined)]
|
||||||
|
[JsonSubtypes.KnownSubType(typeof(ScriptDataReference), ScriptDataType.Reference)]
|
||||||
|
[JsonSubtypes.KnownSubType(typeof(ScriptDataEcmaArray), ScriptDataType.EcmaArray)]
|
||||||
|
[JsonSubtypes.KnownSubType(typeof(ScriptDataStrictArray), ScriptDataType.StrictArray)]
|
||||||
|
[JsonSubtypes.KnownSubType(typeof(ScriptDataDate), ScriptDataType.Date)]
|
||||||
|
[JsonSubtypes.KnownSubType(typeof(ScriptDataLongString), ScriptDataType.LongString)]
|
||||||
|
public interface IScriptDataValue
|
||||||
|
{
|
||||||
|
[JsonProperty]
|
||||||
|
ScriptDataType Type { get; }
|
||||||
|
void WriteTo(Stream stream);
|
||||||
|
}
|
||||||
|
}
|
17
BililiveRecorder.Flv/Amf/KeyValuePairDebugView.cs
Normal file
17
BililiveRecorder.Flv/Amf/KeyValuePairDebugView.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("{Key}: {Value}")]
|
||||||
|
internal sealed class KeyValuePairDebugView<K, V>
|
||||||
|
{
|
||||||
|
public KeyValuePairDebugView(K key, V value)
|
||||||
|
{
|
||||||
|
this.Key = key;
|
||||||
|
this.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public K Key { get; }
|
||||||
|
public V Value { get; }
|
||||||
|
}
|
||||||
|
}
|
30
BililiveRecorder.Flv/Amf/ScriptDataBoolean.cs
Normal file
30
BililiveRecorder.Flv/Amf/ScriptDataBoolean.cs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("AmfBoolean, {Value}")]
|
||||||
|
public class ScriptDataBoolean : IScriptDataValue
|
||||||
|
{
|
||||||
|
public ScriptDataType Type => ScriptDataType.Boolean;
|
||||||
|
|
||||||
|
[JsonProperty]
|
||||||
|
public bool Value { get; set; }
|
||||||
|
|
||||||
|
public void WriteTo(Stream stream)
|
||||||
|
{
|
||||||
|
stream.WriteByte((byte)this.Type);
|
||||||
|
stream.WriteByte((byte)(this.Value ? 1 : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj) => obj is ScriptDataBoolean boolean && this.Value == boolean.Value;
|
||||||
|
public override int GetHashCode() => HashCode.Combine(this.Value);
|
||||||
|
public static bool operator ==(ScriptDataBoolean left, ScriptDataBoolean right) => EqualityComparer<ScriptDataBoolean>.Default.Equals(left, right);
|
||||||
|
public static bool operator !=(ScriptDataBoolean left, ScriptDataBoolean right) => !(left == right);
|
||||||
|
public static implicit operator bool(ScriptDataBoolean boolean) => boolean.Value;
|
||||||
|
public static implicit operator ScriptDataBoolean(bool boolean) => new ScriptDataBoolean { Value = boolean };
|
||||||
|
}
|
||||||
|
}
|
51
BililiveRecorder.Flv/Amf/ScriptDataDate.cs
Normal file
51
BililiveRecorder.Flv/Amf/ScriptDataDate.cs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("AmfDate, {Value}")]
|
||||||
|
public class ScriptDataDate : IScriptDataValue
|
||||||
|
{
|
||||||
|
public ScriptDataDate() { }
|
||||||
|
public ScriptDataDate(DateTimeOffset value)
|
||||||
|
{
|
||||||
|
this.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScriptDataDate(double dateTime, short localDateTimeOffset)
|
||||||
|
{
|
||||||
|
this.Value = DateTimeOffset.FromUnixTimeMilliseconds((long)dateTime).ToOffset(TimeSpan.FromMinutes(localDateTimeOffset));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScriptDataType Type => ScriptDataType.Date;
|
||||||
|
|
||||||
|
[JsonProperty]
|
||||||
|
public DateTimeOffset Value { get; set; }
|
||||||
|
|
||||||
|
public void WriteTo(Stream stream)
|
||||||
|
{
|
||||||
|
var dateTime = (double)this.Value.ToUnixTimeMilliseconds();
|
||||||
|
var localDateTimeOffset = (short)this.Value.Offset.TotalMinutes;
|
||||||
|
var buffer1 = new byte[sizeof(double)];
|
||||||
|
var buffer2 = new byte[sizeof(ushort)];
|
||||||
|
BinaryPrimitives.WriteInt64BigEndian(buffer1, BitConverter.DoubleToInt64Bits(dateTime));
|
||||||
|
BinaryPrimitives.WriteInt16BigEndian(buffer2, localDateTimeOffset);
|
||||||
|
stream.WriteByte((byte)this.Type);
|
||||||
|
stream.Write(buffer1);
|
||||||
|
stream.Write(buffer2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj) => obj is ScriptDataDate date && this.Value.Equals(date.Value);
|
||||||
|
public override int GetHashCode() => HashCode.Combine(this.Value);
|
||||||
|
public static bool operator ==(ScriptDataDate left, ScriptDataDate right) => EqualityComparer<ScriptDataDate>.Default.Equals(left, right);
|
||||||
|
public static bool operator !=(ScriptDataDate left, ScriptDataDate right) => !(left == right);
|
||||||
|
public static implicit operator DateTimeOffset(ScriptDataDate date) => date.Value;
|
||||||
|
public static implicit operator ScriptDataDate(DateTimeOffset date) => new ScriptDataDate(date);
|
||||||
|
public static implicit operator DateTime(ScriptDataDate date) => date.Value.DateTime;
|
||||||
|
public static implicit operator ScriptDataDate(DateTime date) => new ScriptDataDate(date);
|
||||||
|
}
|
||||||
|
}
|
75
BililiveRecorder.Flv/Amf/ScriptDataEcmaArray.cs
Normal file
75
BililiveRecorder.Flv/Amf/ScriptDataEcmaArray.cs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
[DebuggerTypeProxy(typeof(AmfDictionaryDebugView))]
|
||||||
|
[DebuggerDisplay("AmfEcmaArray, Count = {Count}")]
|
||||||
|
public class ScriptDataEcmaArray : IScriptDataValue, IDictionary<string, IScriptDataValue>, ICollection<KeyValuePair<string, IScriptDataValue>>, IEnumerable<KeyValuePair<string, IScriptDataValue>>, IReadOnlyCollection<KeyValuePair<string, IScriptDataValue>>, IReadOnlyDictionary<string, IScriptDataValue>
|
||||||
|
{
|
||||||
|
public ScriptDataType Type => ScriptDataType.EcmaArray;
|
||||||
|
|
||||||
|
[JsonProperty]
|
||||||
|
public Dictionary<string, IScriptDataValue> Value { get; set; } = new Dictionary<string, IScriptDataValue>();
|
||||||
|
|
||||||
|
public void WriteTo(Stream stream)
|
||||||
|
{
|
||||||
|
stream.WriteByte((byte)this.Type);
|
||||||
|
|
||||||
|
{
|
||||||
|
var buffer = new byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)this.Value.Count);
|
||||||
|
stream.Write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in this.Value)
|
||||||
|
{
|
||||||
|
// key
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(item.Key);
|
||||||
|
if (bytes.Length > ushort.MaxValue)
|
||||||
|
throw new AmfException($"Cannot write more than {ushort.MaxValue} into ScriptDataString");
|
||||||
|
|
||||||
|
var buffer = new byte[sizeof(ushort)];
|
||||||
|
BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)bytes.Length);
|
||||||
|
|
||||||
|
stream.Write(buffer);
|
||||||
|
stream.Write(bytes);
|
||||||
|
|
||||||
|
// value
|
||||||
|
item.Value.WriteTo(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.Write(new byte[] { 0, 0, 9 });
|
||||||
|
}
|
||||||
|
|
||||||
|
public IScriptDataValue this[string key] { get => ((IDictionary<string, IScriptDataValue>)this.Value)[key]; set => ((IDictionary<string, IScriptDataValue>)this.Value)[key] = value; }
|
||||||
|
public ICollection<string> Keys => ((IDictionary<string, IScriptDataValue>)this.Value).Keys;
|
||||||
|
public ICollection<IScriptDataValue> Values => ((IDictionary<string, IScriptDataValue>)this.Value).Values;
|
||||||
|
IEnumerable<string> IReadOnlyDictionary<string, IScriptDataValue>.Keys => ((IReadOnlyDictionary<string, IScriptDataValue>)this.Value).Keys;
|
||||||
|
IEnumerable<IScriptDataValue> IReadOnlyDictionary<string, IScriptDataValue>.Values => ((IReadOnlyDictionary<string, IScriptDataValue>)this.Value).Values;
|
||||||
|
public int Count => ((IDictionary<string, IScriptDataValue>)this.Value).Count;
|
||||||
|
public bool IsReadOnly => ((IDictionary<string, IScriptDataValue>)this.Value).IsReadOnly;
|
||||||
|
public void Add(string key, IScriptDataValue value) => ((IDictionary<string, IScriptDataValue>)this.Value).Add(key, value);
|
||||||
|
public void Add(KeyValuePair<string, IScriptDataValue> item) => ((IDictionary<string, IScriptDataValue>)this.Value).Add(item);
|
||||||
|
public void Clear() => ((IDictionary<string, IScriptDataValue>)this.Value).Clear();
|
||||||
|
public bool Contains(KeyValuePair<string, IScriptDataValue> item) => ((IDictionary<string, IScriptDataValue>)this.Value).Contains(item);
|
||||||
|
public bool ContainsKey(string key) => ((IDictionary<string, IScriptDataValue>)this.Value).ContainsKey(key);
|
||||||
|
public void CopyTo(KeyValuePair<string, IScriptDataValue>[] array, int arrayIndex) => ((IDictionary<string, IScriptDataValue>)this.Value).CopyTo(array, arrayIndex);
|
||||||
|
public IEnumerator<KeyValuePair<string, IScriptDataValue>> GetEnumerator() => ((IDictionary<string, IScriptDataValue>)this.Value).GetEnumerator();
|
||||||
|
public bool Remove(string key) => ((IDictionary<string, IScriptDataValue>)this.Value).Remove(key);
|
||||||
|
public bool Remove(KeyValuePair<string, IScriptDataValue> item) => ((IDictionary<string, IScriptDataValue>)this.Value).Remove(item);
|
||||||
|
#pragma warning disable CS8767 // Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes).
|
||||||
|
public bool TryGetValue(string key, [MaybeNullWhen(false)] out IScriptDataValue value) => ((IDictionary<string, IScriptDataValue>)this.Value).TryGetValue(key, out value!);
|
||||||
|
#pragma warning restore CS8767 // Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes).
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => ((IDictionary<string, IScriptDataValue>)this.Value).GetEnumerator();
|
||||||
|
public static implicit operator Dictionary<string, IScriptDataValue>(ScriptDataEcmaArray ecmaArray) => ecmaArray.Value;
|
||||||
|
public static implicit operator ScriptDataEcmaArray(Dictionary<string, IScriptDataValue> ecmaArray) => new ScriptDataEcmaArray { Value = ecmaArray };
|
||||||
|
public static implicit operator ScriptDataEcmaArray(ScriptDataObject @object) => new ScriptDataEcmaArray { Value = @object };
|
||||||
|
}
|
||||||
|
}
|
39
BililiveRecorder.Flv/Amf/ScriptDataLongString.cs
Normal file
39
BililiveRecorder.Flv/Amf/ScriptDataLongString.cs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("AmfLongString, {Value}")]
|
||||||
|
public class ScriptDataLongString : IScriptDataValue
|
||||||
|
{
|
||||||
|
public ScriptDataType Type => ScriptDataType.LongString;
|
||||||
|
|
||||||
|
[JsonProperty(Required = Required.Always)]
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public void WriteTo(Stream stream)
|
||||||
|
{
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(this.Value);
|
||||||
|
|
||||||
|
var buffer = new byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytes.Length);
|
||||||
|
|
||||||
|
stream.WriteByte((byte)this.Type);
|
||||||
|
stream.Write(buffer);
|
||||||
|
stream.Write(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj) => obj is ScriptDataLongString @string && this.Value == @string.Value;
|
||||||
|
public override int GetHashCode() => HashCode.Combine(this.Value);
|
||||||
|
public static bool operator ==(ScriptDataLongString left, ScriptDataLongString right) => EqualityComparer<ScriptDataLongString>.Default.Equals(left, right);
|
||||||
|
public static bool operator !=(ScriptDataLongString left, ScriptDataLongString right) => !(left == right);
|
||||||
|
public static implicit operator string(ScriptDataLongString @string) => @string.Value;
|
||||||
|
public static implicit operator ScriptDataLongString(string @string) => new ScriptDataLongString { Value = @string };
|
||||||
|
public static implicit operator ScriptDataLongString(ScriptDataString @string) => new ScriptDataLongString { Value = @string.Value };
|
||||||
|
}
|
||||||
|
}
|
19
BililiveRecorder.Flv/Amf/ScriptDataNull.cs
Normal file
19
BililiveRecorder.Flv/Amf/ScriptDataNull.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("AmfNull")]
|
||||||
|
public class ScriptDataNull : IScriptDataValue
|
||||||
|
{
|
||||||
|
public ScriptDataType Type => ScriptDataType.Null;
|
||||||
|
|
||||||
|
public void WriteTo(Stream stream) => stream.WriteByte((byte)this.Type);
|
||||||
|
|
||||||
|
public override bool Equals(object? obj) => obj is ScriptDataNull;
|
||||||
|
public override int GetHashCode() => 0;
|
||||||
|
public static bool operator ==(ScriptDataNull left, ScriptDataNull right) => EqualityComparer<ScriptDataNull>.Default.Equals(left, right);
|
||||||
|
public static bool operator !=(ScriptDataNull left, ScriptDataNull right) => !(left == right);
|
||||||
|
}
|
||||||
|
}
|
35
BililiveRecorder.Flv/Amf/ScriptDataNumber.cs
Normal file
35
BililiveRecorder.Flv/Amf/ScriptDataNumber.cs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("AmfNumber, {Value}")]
|
||||||
|
public class ScriptDataNumber : IScriptDataValue
|
||||||
|
{
|
||||||
|
public ScriptDataType Type => ScriptDataType.Number;
|
||||||
|
|
||||||
|
[JsonProperty]
|
||||||
|
public double Value { get; set; }
|
||||||
|
|
||||||
|
public void WriteTo(Stream stream)
|
||||||
|
{
|
||||||
|
stream.WriteByte((byte)this.Type);
|
||||||
|
var buffer = new byte[sizeof(double)];
|
||||||
|
BinaryPrimitives.WriteInt64BigEndian(buffer, BitConverter.DoubleToInt64Bits(this.Value));
|
||||||
|
stream.Write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator ==(ScriptDataNumber left, ScriptDataNumber right) => EqualityComparer<ScriptDataNumber>.Default.Equals(left, right);
|
||||||
|
public static bool operator !=(ScriptDataNumber left, ScriptDataNumber right) => !(left == right);
|
||||||
|
public override bool Equals(object? obj) => obj is ScriptDataNumber number && this.Value == number.Value;
|
||||||
|
public override int GetHashCode() => HashCode.Combine(this.Value);
|
||||||
|
public static implicit operator double(ScriptDataNumber number) => number.Value;
|
||||||
|
public static explicit operator int(ScriptDataNumber number) => (int)number.Value;
|
||||||
|
public static implicit operator ScriptDataNumber(double number) => new ScriptDataNumber { Value = number };
|
||||||
|
public static explicit operator ScriptDataNumber(int number) => new ScriptDataNumber { Value = number };
|
||||||
|
}
|
||||||
|
}
|
70
BililiveRecorder.Flv/Amf/ScriptDataObject.cs
Normal file
70
BililiveRecorder.Flv/Amf/ScriptDataObject.cs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
[DebuggerTypeProxy(typeof(AmfDictionaryDebugView))]
|
||||||
|
[DebuggerDisplay("AmfObject, Count = {Count}")]
|
||||||
|
public class ScriptDataObject : IScriptDataValue, IDictionary<string, IScriptDataValue>, ICollection<KeyValuePair<string, IScriptDataValue>>, IEnumerable<KeyValuePair<string, IScriptDataValue>>, IReadOnlyCollection<KeyValuePair<string, IScriptDataValue>>, IReadOnlyDictionary<string, IScriptDataValue>
|
||||||
|
{
|
||||||
|
public ScriptDataType Type => ScriptDataType.Object;
|
||||||
|
|
||||||
|
[JsonProperty]
|
||||||
|
public Dictionary<string, IScriptDataValue> Value { get; set; } = new Dictionary<string, IScriptDataValue>();
|
||||||
|
|
||||||
|
public void WriteTo(Stream stream)
|
||||||
|
{
|
||||||
|
stream.WriteByte((byte)this.Type);
|
||||||
|
|
||||||
|
foreach (var item in this.Value)
|
||||||
|
{
|
||||||
|
// key
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(item.Key);
|
||||||
|
if (bytes.Length > ushort.MaxValue)
|
||||||
|
throw new AmfException($"Cannot write more than {ushort.MaxValue} into ScriptDataString");
|
||||||
|
|
||||||
|
var buffer = new byte[sizeof(ushort)];
|
||||||
|
BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)bytes.Length);
|
||||||
|
|
||||||
|
stream.Write(buffer);
|
||||||
|
stream.Write(bytes);
|
||||||
|
|
||||||
|
// value
|
||||||
|
item.Value.WriteTo(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.Write(new byte[] { 0, 0, 9 });
|
||||||
|
}
|
||||||
|
|
||||||
|
public IScriptDataValue this[string key] { get => ((IDictionary<string, IScriptDataValue>)this.Value)[key]; set => ((IDictionary<string, IScriptDataValue>)this.Value)[key] = value; }
|
||||||
|
public ICollection<string> Keys => ((IDictionary<string, IScriptDataValue>)this.Value).Keys;
|
||||||
|
public ICollection<IScriptDataValue> Values => ((IDictionary<string, IScriptDataValue>)this.Value).Values;
|
||||||
|
IEnumerable<string> IReadOnlyDictionary<string, IScriptDataValue>.Keys => ((IReadOnlyDictionary<string, IScriptDataValue>)this.Value).Keys;
|
||||||
|
IEnumerable<IScriptDataValue> IReadOnlyDictionary<string, IScriptDataValue>.Values => ((IReadOnlyDictionary<string, IScriptDataValue>)this.Value).Values;
|
||||||
|
public int Count => ((IDictionary<string, IScriptDataValue>)this.Value).Count;
|
||||||
|
public bool IsReadOnly => ((IDictionary<string, IScriptDataValue>)this.Value).IsReadOnly;
|
||||||
|
public void Add(string key, IScriptDataValue value) => ((IDictionary<string, IScriptDataValue>)this.Value).Add(key, value);
|
||||||
|
public void Add(KeyValuePair<string, IScriptDataValue> item) => ((IDictionary<string, IScriptDataValue>)this.Value).Add(item);
|
||||||
|
public void Clear() => ((IDictionary<string, IScriptDataValue>)this.Value).Clear();
|
||||||
|
public bool Contains(KeyValuePair<string, IScriptDataValue> item) => ((IDictionary<string, IScriptDataValue>)this.Value).Contains(item);
|
||||||
|
public bool ContainsKey(string key) => ((IDictionary<string, IScriptDataValue>)this.Value).ContainsKey(key);
|
||||||
|
public void CopyTo(KeyValuePair<string, IScriptDataValue>[] array, int arrayIndex) => ((IDictionary<string, IScriptDataValue>)this.Value).CopyTo(array, arrayIndex);
|
||||||
|
public IEnumerator<KeyValuePair<string, IScriptDataValue>> GetEnumerator() => ((IDictionary<string, IScriptDataValue>)this.Value).GetEnumerator();
|
||||||
|
public bool Remove(string key) => ((IDictionary<string, IScriptDataValue>)this.Value).Remove(key);
|
||||||
|
public bool Remove(KeyValuePair<string, IScriptDataValue> item) => ((IDictionary<string, IScriptDataValue>)this.Value).Remove(item);
|
||||||
|
#pragma warning disable CS8767 // Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes).
|
||||||
|
public bool TryGetValue(string key, [MaybeNullWhen(false)] out IScriptDataValue value) => ((IDictionary<string, IScriptDataValue>)this.Value).TryGetValue(key, out value!);
|
||||||
|
#pragma warning restore CS8767 // Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes).
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => ((IDictionary<string, IScriptDataValue>)this.Value).GetEnumerator();
|
||||||
|
|
||||||
|
public static implicit operator Dictionary<string, IScriptDataValue>(ScriptDataObject @object) => @object.Value;
|
||||||
|
public static implicit operator ScriptDataObject(Dictionary<string, IScriptDataValue> @object) => new ScriptDataObject { Value = @object };
|
||||||
|
public static implicit operator ScriptDataObject(ScriptDataEcmaArray ecmaArray) => new ScriptDataObject { Value = ecmaArray };
|
||||||
|
}
|
||||||
|
}
|
34
BililiveRecorder.Flv/Amf/ScriptDataReference.cs
Normal file
34
BililiveRecorder.Flv/Amf/ScriptDataReference.cs
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("AmfReference, {Value}")]
|
||||||
|
public class ScriptDataReference : IScriptDataValue
|
||||||
|
{
|
||||||
|
public ScriptDataType Type => ScriptDataType.Reference;
|
||||||
|
|
||||||
|
[JsonProperty]
|
||||||
|
public ushort Value { get; set; }
|
||||||
|
|
||||||
|
public void WriteTo(Stream stream)
|
||||||
|
{
|
||||||
|
stream.WriteByte((byte)this.Type);
|
||||||
|
|
||||||
|
var buffer = new byte[sizeof(ushort)];
|
||||||
|
BinaryPrimitives.WriteUInt16BigEndian(buffer, this.Value);
|
||||||
|
stream.Write(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj) => obj is ScriptDataReference reference && this.Value == reference.Value;
|
||||||
|
public override int GetHashCode() => HashCode.Combine(this.Value);
|
||||||
|
public static bool operator ==(ScriptDataReference left, ScriptDataReference right) => EqualityComparer<ScriptDataReference>.Default.Equals(left, right);
|
||||||
|
public static bool operator !=(ScriptDataReference left, ScriptDataReference right) => !(left == right);
|
||||||
|
public static implicit operator ushort(ScriptDataReference reference) => reference.Value;
|
||||||
|
public static implicit operator ScriptDataReference(ushort number) => new ScriptDataReference { Value = number };
|
||||||
|
}
|
||||||
|
}
|
47
BililiveRecorder.Flv/Amf/ScriptDataStrictArray.cs
Normal file
47
BililiveRecorder.Flv/Amf/ScriptDataStrictArray.cs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
[DebuggerTypeProxy(typeof(AmfCollectionDebugView))]
|
||||||
|
[DebuggerDisplay("AmfStrictArray, Count = {Count}")]
|
||||||
|
public class ScriptDataStrictArray : IScriptDataValue, IList<IScriptDataValue>, ICollection<IScriptDataValue>, IEnumerable<IScriptDataValue>, IReadOnlyCollection<IScriptDataValue>, IReadOnlyList<IScriptDataValue>
|
||||||
|
{
|
||||||
|
public ScriptDataType Type => ScriptDataType.StrictArray;
|
||||||
|
|
||||||
|
[JsonProperty]
|
||||||
|
public List<IScriptDataValue> Value { get; set; } = new List<IScriptDataValue>();
|
||||||
|
|
||||||
|
public void WriteTo(Stream stream)
|
||||||
|
{
|
||||||
|
stream.WriteByte((byte)this.Type);
|
||||||
|
|
||||||
|
var buffer = new byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)this.Value.Count);
|
||||||
|
stream.Write(buffer);
|
||||||
|
|
||||||
|
foreach (var item in this.Value)
|
||||||
|
item.WriteTo(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IScriptDataValue this[int index] { get => ((IList<IScriptDataValue>)this.Value)[index]; set => ((IList<IScriptDataValue>)this.Value)[index] = value; }
|
||||||
|
public int Count => ((IList<IScriptDataValue>)this.Value).Count;
|
||||||
|
public bool IsReadOnly => ((IList<IScriptDataValue>)this.Value).IsReadOnly;
|
||||||
|
public void Add(IScriptDataValue item) => ((IList<IScriptDataValue>)this.Value).Add(item);
|
||||||
|
public void Clear() => ((IList<IScriptDataValue>)this.Value).Clear();
|
||||||
|
public bool Contains(IScriptDataValue item) => ((IList<IScriptDataValue>)this.Value).Contains(item);
|
||||||
|
public void CopyTo(IScriptDataValue[] array, int arrayIndex) => ((IList<IScriptDataValue>)this.Value).CopyTo(array, arrayIndex);
|
||||||
|
public IEnumerator<IScriptDataValue> GetEnumerator() => ((IList<IScriptDataValue>)this.Value).GetEnumerator();
|
||||||
|
public int IndexOf(IScriptDataValue item) => ((IList<IScriptDataValue>)this.Value).IndexOf(item);
|
||||||
|
public void Insert(int index, IScriptDataValue item) => ((IList<IScriptDataValue>)this.Value).Insert(index, item);
|
||||||
|
public bool Remove(IScriptDataValue item) => ((IList<IScriptDataValue>)this.Value).Remove(item);
|
||||||
|
public void RemoveAt(int index) => ((IList<IScriptDataValue>)this.Value).RemoveAt(index);
|
||||||
|
IEnumerator IEnumerable.GetEnumerator() => ((IList<IScriptDataValue>)this.Value).GetEnumerator();
|
||||||
|
public static implicit operator List<IScriptDataValue>(ScriptDataStrictArray strictArray) => strictArray.Value;
|
||||||
|
public static implicit operator ScriptDataStrictArray(List<IScriptDataValue> values) => new ScriptDataStrictArray { Value = values };
|
||||||
|
}
|
||||||
|
}
|
41
BililiveRecorder.Flv/Amf/ScriptDataString.cs
Normal file
41
BililiveRecorder.Flv/Amf/ScriptDataString.cs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("AmfString, {Value}")]
|
||||||
|
public class ScriptDataString : IScriptDataValue
|
||||||
|
{
|
||||||
|
public ScriptDataType Type => ScriptDataType.String;
|
||||||
|
|
||||||
|
[JsonProperty(Required = Required.Always)]
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public void WriteTo(Stream stream)
|
||||||
|
{
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(this.Value);
|
||||||
|
if (bytes.Length > ushort.MaxValue)
|
||||||
|
throw new AmfException($"Cannot write more than {ushort.MaxValue} into ScriptDataString");
|
||||||
|
|
||||||
|
var buffer = new byte[sizeof(ushort)];
|
||||||
|
BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)bytes.Length);
|
||||||
|
|
||||||
|
stream.WriteByte((byte)this.Type);
|
||||||
|
stream.Write(buffer);
|
||||||
|
stream.Write(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Equals(object? obj) => obj is ScriptDataString @string && this.Value == @string.Value;
|
||||||
|
public override int GetHashCode() => HashCode.Combine(this.Value);
|
||||||
|
public static bool operator ==(ScriptDataString left, ScriptDataString right) => EqualityComparer<ScriptDataString>.Default.Equals(left, right);
|
||||||
|
public static bool operator !=(ScriptDataString left, ScriptDataString right) => !(left == right);
|
||||||
|
public static implicit operator string(ScriptDataString @string) => @string.Value;
|
||||||
|
public static implicit operator ScriptDataString(string @string) => new ScriptDataString { Value = @string };
|
||||||
|
public static implicit operator ScriptDataString(ScriptDataLongString @string) => new ScriptDataString { Value = @string.Value };
|
||||||
|
}
|
||||||
|
}
|
23
BililiveRecorder.Flv/Amf/ScriptDataType.cs
Normal file
23
BililiveRecorder.Flv/Amf/ScriptDataType.cs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public enum ScriptDataType : byte
|
||||||
|
{
|
||||||
|
Number = 0,
|
||||||
|
Boolean = 1,
|
||||||
|
String = 2,
|
||||||
|
Object = 3,
|
||||||
|
MovieClip = 4,
|
||||||
|
Null = 5,
|
||||||
|
Undefined = 6,
|
||||||
|
Reference = 7,
|
||||||
|
EcmaArray = 8,
|
||||||
|
ObjectEndMarker = 9,
|
||||||
|
StrictArray = 10,
|
||||||
|
Date = 11,
|
||||||
|
LongString = 12,
|
||||||
|
}
|
||||||
|
}
|
19
BililiveRecorder.Flv/Amf/ScriptDataUndefined.cs
Normal file
19
BililiveRecorder.Flv/Amf/ScriptDataUndefined.cs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("AmfUndefined")]
|
||||||
|
public class ScriptDataUndefined : IScriptDataValue
|
||||||
|
{
|
||||||
|
public ScriptDataType Type => ScriptDataType.Undefined;
|
||||||
|
|
||||||
|
public void WriteTo(Stream stream) => stream.WriteByte((byte)this.Type);
|
||||||
|
|
||||||
|
public override bool Equals(object? obj) => obj is ScriptDataUndefined;
|
||||||
|
public override int GetHashCode() => 0;
|
||||||
|
public static bool operator ==(ScriptDataUndefined left, ScriptDataUndefined right) => EqualityComparer<ScriptDataUndefined>.Default.Equals(left, right);
|
||||||
|
public static bool operator !=(ScriptDataUndefined left, ScriptDataUndefined right) => !(left == right);
|
||||||
|
}
|
||||||
|
}
|
177
BililiveRecorder.Flv/Amf/ScriptTagBody.cs
Normal file
177
BililiveRecorder.Flv/Amf/ScriptTagBody.cs
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Xml;
|
||||||
|
using System.Xml.Schema;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
using BililiveRecorder.Flv.Parser;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Amf
|
||||||
|
{
|
||||||
|
public class ScriptTagBody : IXmlSerializable
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
DateParseHandling = DateParseHandling.DateTimeOffset,
|
||||||
|
DateFormatHandling = DateFormatHandling.IsoDateFormat,
|
||||||
|
DateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind,
|
||||||
|
};
|
||||||
|
|
||||||
|
public ScriptDataString Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public ScriptDataObject Value { get; set; } = new ScriptDataObject();
|
||||||
|
|
||||||
|
public static ScriptTagBody Parse(string json) => JsonConvert.DeserializeObject<ScriptTagBody>(json, jsonSerializerSettings)!;
|
||||||
|
|
||||||
|
public string ToJson() => JsonConvert.SerializeObject(this, jsonSerializerSettings);
|
||||||
|
|
||||||
|
public static ScriptTagBody Parse(byte[] bytes)
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream(bytes);
|
||||||
|
return Parse(ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ScriptTagBody Parse(Stream stream)
|
||||||
|
{
|
||||||
|
return Parse(new BigEndianBinaryReader(stream, Encoding.UTF8, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ScriptTagBody Parse(BigEndianBinaryReader binaryReader)
|
||||||
|
{
|
||||||
|
if (ParseValue(binaryReader) is ScriptDataString stringName)
|
||||||
|
return new ScriptTagBody
|
||||||
|
{
|
||||||
|
Name = stringName,
|
||||||
|
Value = ((ParseValue(binaryReader)) switch
|
||||||
|
{
|
||||||
|
ScriptDataEcmaArray value => value,
|
||||||
|
ScriptDataObject value => value,
|
||||||
|
_ => throw new AmfException("type of ScriptTagBody.Value is not supported"),
|
||||||
|
})
|
||||||
|
};
|
||||||
|
else
|
||||||
|
throw new AmfException("ScriptTagBody.Name is not String");
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] ToBytes()
|
||||||
|
{
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
this.WriteTo(ms);
|
||||||
|
return ms.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteTo(Stream stream)
|
||||||
|
{
|
||||||
|
this.Name.WriteTo(stream);
|
||||||
|
this.Value.WriteTo(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IScriptDataValue ParseValue(BigEndianBinaryReader binaryReader)
|
||||||
|
{
|
||||||
|
var type = (ScriptDataType)binaryReader.ReadByte();
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case ScriptDataType.Number:
|
||||||
|
return (ScriptDataNumber)binaryReader.ReadDouble();
|
||||||
|
case ScriptDataType.Boolean:
|
||||||
|
return (ScriptDataBoolean)binaryReader.ReadBoolean();
|
||||||
|
case ScriptDataType.String:
|
||||||
|
return ReadScriptDataString(binaryReader, false) ?? string.Empty;
|
||||||
|
case ScriptDataType.Object:
|
||||||
|
{
|
||||||
|
var result = new ScriptDataObject();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var propertyName = ReadScriptDataString(binaryReader, true);
|
||||||
|
if (propertyName is null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var propertyData = ParseValue(binaryReader);
|
||||||
|
|
||||||
|
result[propertyName] = propertyData;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
case ScriptDataType.MovieClip:
|
||||||
|
throw new AmfException("MovieClip is not supported");
|
||||||
|
case ScriptDataType.Null:
|
||||||
|
return new ScriptDataNull();
|
||||||
|
case ScriptDataType.Undefined:
|
||||||
|
return new ScriptDataUndefined();
|
||||||
|
case ScriptDataType.Reference:
|
||||||
|
return (ScriptDataReference)binaryReader.ReadUInt16();
|
||||||
|
case ScriptDataType.EcmaArray:
|
||||||
|
{
|
||||||
|
binaryReader.ReadUInt32();
|
||||||
|
var result = new ScriptDataEcmaArray();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var propertyName = ReadScriptDataString(binaryReader, true);
|
||||||
|
if (propertyName is null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var propertyData = ParseValue(binaryReader);
|
||||||
|
|
||||||
|
result[propertyName] = propertyData;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
case ScriptDataType.ObjectEndMarker:
|
||||||
|
throw new AmfException("Read ObjectEndMarker");
|
||||||
|
case ScriptDataType.StrictArray:
|
||||||
|
{
|
||||||
|
var length = binaryReader.ReadUInt32();
|
||||||
|
var result = new ScriptDataStrictArray();
|
||||||
|
for (var i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
var value = ParseValue(binaryReader);
|
||||||
|
result.Add(value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
case ScriptDataType.Date:
|
||||||
|
{
|
||||||
|
var dateTime = binaryReader.ReadDouble();
|
||||||
|
var offset = binaryReader.ReadInt16();
|
||||||
|
return new ScriptDataDate(dateTime, offset);
|
||||||
|
}
|
||||||
|
case ScriptDataType.LongString:
|
||||||
|
{
|
||||||
|
var length = binaryReader.ReadUInt32();
|
||||||
|
if (length > int.MaxValue)
|
||||||
|
throw new AmfException($"LongString larger than {int.MaxValue} is not supported.");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var bytes = binaryReader.ReadBytes((int)length);
|
||||||
|
var str = Encoding.UTF8.GetString(bytes);
|
||||||
|
return (ScriptDataLongString)str;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new AmfException("Unknown ScriptDataValueType");
|
||||||
|
}
|
||||||
|
|
||||||
|
static ScriptDataString? ReadScriptDataString(BigEndianBinaryReader binaryReader, bool expectObjectEndMarker)
|
||||||
|
{
|
||||||
|
var length = binaryReader.ReadUInt16();
|
||||||
|
if (length == 0)
|
||||||
|
{
|
||||||
|
if (expectObjectEndMarker && binaryReader.ReadByte() != 9)
|
||||||
|
throw new AmfException("ObjectEndMarker not matched.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Encoding.UTF8.GetString(binaryReader.ReadBytes(length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
XmlSchema IXmlSerializable.GetSchema() => null!;
|
||||||
|
void IXmlSerializable.ReadXml(XmlReader reader)
|
||||||
|
{
|
||||||
|
var str = reader.ReadElementContentAsString();
|
||||||
|
var obj = Parse(str);
|
||||||
|
this.Name = obj.Name;
|
||||||
|
this.Value = obj.Value;
|
||||||
|
}
|
||||||
|
void IXmlSerializable.WriteXml(XmlWriter writer) => writer.WriteString(this.ToJson());
|
||||||
|
}
|
||||||
|
}
|
23
BililiveRecorder.Flv/BililiveRecorder.Flv.csproj
Normal file
23
BililiveRecorder.Flv/BililiveRecorder.Flv.csproj
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<LangVersion>8.0</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="JsonSubTypes" Version="1.8.0" />
|
||||||
|
<PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
|
<PackageReference Include="Nullable" Version="1.3.0">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="System.IO.Pipelines" Version="5.0.1" />
|
||||||
|
<PackageReference Include="System.Memory" Version="4.5.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
9
BililiveRecorder.Flv/DefaultMemoryStreamProvider.cs
Normal file
9
BililiveRecorder.Flv/DefaultMemoryStreamProvider.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv
|
||||||
|
{
|
||||||
|
public class DefaultMemoryStreamProvider : IMemoryStreamProvider
|
||||||
|
{
|
||||||
|
public Stream CreateMemoryStream(string tag) => new MemoryStream();
|
||||||
|
}
|
||||||
|
}
|
13
BililiveRecorder.Flv/DependencyInjectionExtensions.cs
Normal file
13
BililiveRecorder.Flv/DependencyInjectionExtensions.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.DependencyInjection
|
||||||
|
{
|
||||||
|
public static class DependencyInjectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddFlv(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
BililiveRecorder.Flv/Grouping/Rules/DataGroupingRule.cs
Normal file
14
BililiveRecorder.Flv/Grouping/Rules/DataGroupingRule.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using BililiveRecorder.Flv.Pipeline;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Grouping.Rules
|
||||||
|
{
|
||||||
|
public class DataGroupingRule : IGroupingRule
|
||||||
|
{
|
||||||
|
public bool StartWith(Tag tag) => tag.IsData();
|
||||||
|
|
||||||
|
public bool AppendWith(Tag tag) => tag.IsNonKeyframeData();
|
||||||
|
|
||||||
|
public PipelineAction CreatePipelineAction(List<Tag> tags) => new PipelineDataAction(tags);
|
||||||
|
}
|
||||||
|
}
|
14
BililiveRecorder.Flv/Grouping/Rules/HeaderGroupingRule.cs
Normal file
14
BililiveRecorder.Flv/Grouping/Rules/HeaderGroupingRule.cs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using BililiveRecorder.Flv.Pipeline;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Grouping.Rules
|
||||||
|
{
|
||||||
|
public class HeaderGroupingRule : IGroupingRule
|
||||||
|
{
|
||||||
|
public bool StartWith(Tag tag) => tag.IsHeader();
|
||||||
|
|
||||||
|
public bool AppendWith(Tag tag) => tag.IsHeader();
|
||||||
|
|
||||||
|
public PipelineAction CreatePipelineAction(List<Tag> tags) => new PipelineHeaderAction(tags);
|
||||||
|
}
|
||||||
|
}
|
15
BililiveRecorder.Flv/Grouping/Rules/ScriptGroupingRule.cs
Normal file
15
BililiveRecorder.Flv/Grouping/Rules/ScriptGroupingRule.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using BililiveRecorder.Flv.Pipeline;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Grouping.Rules
|
||||||
|
{
|
||||||
|
public class ScriptGroupingRule : IGroupingRule
|
||||||
|
{
|
||||||
|
public bool StartWith(Tag tag) => tag.IsScript();
|
||||||
|
|
||||||
|
public bool AppendWith(Tag tag) => false;
|
||||||
|
|
||||||
|
public PipelineAction CreatePipelineAction(List<Tag> tags) => new PipelineScriptAction(tags.First());
|
||||||
|
}
|
||||||
|
}
|
118
BililiveRecorder.Flv/Grouping/TagGroupReader.cs
Normal file
118
BililiveRecorder.Flv/Grouping/TagGroupReader.cs
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BililiveRecorder.Flv.Grouping.Rules;
|
||||||
|
using BililiveRecorder.Flv.Pipeline;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Grouping
|
||||||
|
{
|
||||||
|
public class TagGroupReader : ITagGroupReader
|
||||||
|
{
|
||||||
|
private readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
|
||||||
|
private readonly bool leaveOpen;
|
||||||
|
private bool disposedValue;
|
||||||
|
|
||||||
|
public IFlvTagReader TagReader { get; }
|
||||||
|
public IList<IGroupingRule> GroupingRules { get; }
|
||||||
|
|
||||||
|
public TagGroupReader(IFlvTagReader tagReader)
|
||||||
|
: this(tagReader, false)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public TagGroupReader(IFlvTagReader flvTagReader, bool leaveOpen = false)
|
||||||
|
{
|
||||||
|
this.TagReader = flvTagReader ?? throw new ArgumentNullException(nameof(flvTagReader));
|
||||||
|
this.leaveOpen = leaveOpen;
|
||||||
|
|
||||||
|
this.GroupingRules = new List<IGroupingRule>
|
||||||
|
{
|
||||||
|
new ScriptGroupingRule(),
|
||||||
|
new HeaderGroupingRule(),
|
||||||
|
new DataGroupingRule()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PipelineAction?> ReadGroupAsync()
|
||||||
|
{
|
||||||
|
if (!this.semaphoreSlim.Wait(0))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Concurrent read is not supported.");
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tags = new List<Tag>();
|
||||||
|
|
||||||
|
var firstTag = await this.TagReader.ReadTagAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
// 数据已经全部读完
|
||||||
|
if (firstTag is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var rule = this.GroupingRules.FirstOrDefault(x => x.StartWith(firstTag));
|
||||||
|
|
||||||
|
if (rule is null)
|
||||||
|
throw new Exception("No grouping rule accepting the tag:" + firstTag.ToString());
|
||||||
|
|
||||||
|
tags.Add(firstTag);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var tag = await this.TagReader.PeekTagAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (tag != null && rule.AppendWith(tag))
|
||||||
|
{
|
||||||
|
await this.TagReader.ReadTagAsync().ConfigureAwait(false);
|
||||||
|
tags.Add(tag);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rule.CreatePipelineAction(tags);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.semaphoreSlim.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Dispose
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!this.disposedValue)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
// dispose managed state (managed objects)
|
||||||
|
if (!this.leaveOpen)
|
||||||
|
this.TagReader.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
|
||||||
|
// TODO: set large fields to null
|
||||||
|
this.disposedValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
|
||||||
|
// ~TagGroupReader()
|
||||||
|
// {
|
||||||
|
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||||
|
// Dispose(disposing: false);
|
||||||
|
// }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||||
|
this.Dispose(disposing: true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
127
BililiveRecorder.Flv/H264Nalu.cs
Normal file
127
BililiveRecorder.Flv/H264Nalu.cs
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// H.264 NAL unit
|
||||||
|
/// </summary>
|
||||||
|
public sealed class H264Nalu
|
||||||
|
{
|
||||||
|
private H264Nalu()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public H264Nalu(int startPosition, uint fullSize, H264NaluType type)
|
||||||
|
{
|
||||||
|
this.StartPosition = startPosition;
|
||||||
|
this.FullSize = fullSize;
|
||||||
|
this.Type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParseNalu(Stream data, [NotNullWhen(true)] out List<H264Nalu>? h264Nalus)
|
||||||
|
{
|
||||||
|
h264Nalus = null;
|
||||||
|
var result = new List<H264Nalu>();
|
||||||
|
var b = new byte[4];
|
||||||
|
|
||||||
|
data.Seek(5, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (data.Position < data.Length)
|
||||||
|
{
|
||||||
|
data.Read(b, 0, 4);
|
||||||
|
var size = BinaryPrimitives.ReadUInt32BigEndian(b);
|
||||||
|
if (TryParseNaluType((byte)data.ReadByte(), out var h264NaluType))
|
||||||
|
{
|
||||||
|
var nalu = new H264Nalu((int)(data.Position - 1), size, h264NaluType);
|
||||||
|
data.Seek(size - 1, SeekOrigin.Current);
|
||||||
|
result.Add(nalu);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
h264Nalus = result;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryParseNaluType(byte firstByte, out H264NaluType h264NaluType)
|
||||||
|
{
|
||||||
|
if ((firstByte & 0b10000000) != 0)
|
||||||
|
{
|
||||||
|
h264NaluType = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
h264NaluType = (H264NaluType)(firstByte & 0b00011111);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 一个 nal_unit 的开始位置
|
||||||
|
/// </summary>
|
||||||
|
[XmlAttribute]
|
||||||
|
public int StartPosition { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 一个 nal_unit 的完整长度
|
||||||
|
/// </summary>
|
||||||
|
[XmlAttribute]
|
||||||
|
public uint FullSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// nal_unit_type
|
||||||
|
/// </summary>
|
||||||
|
[XmlAttribute]
|
||||||
|
public H264NaluType Type { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// nal_unit_type
|
||||||
|
/// </summary>
|
||||||
|
public enum H264NaluType : byte
|
||||||
|
{
|
||||||
|
Unspecified0 = 0,
|
||||||
|
CodedSliceOfANonIdrPicture = 1,
|
||||||
|
CodedSliceDataPartitionA = 2,
|
||||||
|
CodedSliceDataPartitionB = 3,
|
||||||
|
CodedSliceDataPartitionC = 4,
|
||||||
|
CodedSliceOfAnIdrPicture = 5,
|
||||||
|
Sei = 6,
|
||||||
|
Sps = 7,
|
||||||
|
Pps = 8,
|
||||||
|
AccessUnitDelimiter = 9,
|
||||||
|
EndOfSequence = 10,
|
||||||
|
EndOfStream = 11,
|
||||||
|
FillerData = 12,
|
||||||
|
SpsExtension = 13,
|
||||||
|
PrefixNalUnit = 14,
|
||||||
|
SubsetSps = 15,
|
||||||
|
DepthParameterSet = 16,
|
||||||
|
Reserved17 = 17,
|
||||||
|
Reserved18 = 18,
|
||||||
|
SliceLayerWithoutPartitioning = 19,
|
||||||
|
SliceLayerExtension20 = 20,
|
||||||
|
SliceLayerExtension21 = 21,
|
||||||
|
Reserved22 = 22,
|
||||||
|
Reserved23 = 23,
|
||||||
|
Unspecified24 = 24,
|
||||||
|
Unspecified25 = 25,
|
||||||
|
Unspecified23 = 23,
|
||||||
|
Unspecified27 = 27,
|
||||||
|
Unspecified28 = 28,
|
||||||
|
Unspecified29 = 29,
|
||||||
|
Unspecified30 = 30,
|
||||||
|
Unspecified31 = 31,
|
||||||
|
}
|
||||||
|
}
|
11
BililiveRecorder.Flv/IFlvProcessingContextWriter.cs
Normal file
11
BililiveRecorder.Flv/IFlvProcessingContextWriter.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BililiveRecorder.Flv.Pipeline;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv
|
||||||
|
{
|
||||||
|
public interface IFlvProcessingContextWriter : IDisposable
|
||||||
|
{
|
||||||
|
Task WriteAsync(FlvProcessingContext context);
|
||||||
|
}
|
||||||
|
}
|
23
BililiveRecorder.Flv/IFlvTagReader.cs
Normal file
23
BililiveRecorder.Flv/IFlvTagReader.cs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 实现 Flv Tag 的读取
|
||||||
|
/// </summary>
|
||||||
|
public interface IFlvTagReader : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the next available Flv Tag but does not consume it.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<Tag?> PeekTagAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the next Flv Tag.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<Tag?> ReadTagAsync();
|
||||||
|
}
|
||||||
|
}
|
11
BililiveRecorder.Flv/IFlvWriterTargetProvider.cs
Normal file
11
BililiveRecorder.Flv/IFlvWriterTargetProvider.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv
|
||||||
|
{
|
||||||
|
public interface IFlvWriterTargetProvider
|
||||||
|
{
|
||||||
|
Stream CreateOutputStream();
|
||||||
|
|
||||||
|
Stream CreateAlternativeHeaderStream();
|
||||||
|
}
|
||||||
|
}
|
12
BililiveRecorder.Flv/IGroupingRule.cs
Normal file
12
BililiveRecorder.Flv/IGroupingRule.cs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using BililiveRecorder.Flv.Pipeline;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv
|
||||||
|
{
|
||||||
|
public interface IGroupingRule
|
||||||
|
{
|
||||||
|
bool StartWith(Tag tag);
|
||||||
|
bool AppendWith(Tag tag);
|
||||||
|
PipelineAction CreatePipelineAction(List<Tag> tags);
|
||||||
|
}
|
||||||
|
}
|
9
BililiveRecorder.Flv/IMemoryStreamProvider.cs
Normal file
9
BililiveRecorder.Flv/IMemoryStreamProvider.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv
|
||||||
|
{
|
||||||
|
public interface IMemoryStreamProvider
|
||||||
|
{
|
||||||
|
Stream CreateMemoryStream(string tag);
|
||||||
|
}
|
||||||
|
}
|
11
BililiveRecorder.Flv/ITagGroupReader.cs
Normal file
11
BililiveRecorder.Flv/ITagGroupReader.cs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BililiveRecorder.Flv.Pipeline;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv
|
||||||
|
{
|
||||||
|
public interface ITagGroupReader : IDisposable
|
||||||
|
{
|
||||||
|
Task<PipelineAction?> ReadGroupAsync();
|
||||||
|
}
|
||||||
|
}
|
115
BililiveRecorder.Flv/Parser/BigEndianBinaryReader.cs
Normal file
115
BililiveRecorder.Flv/Parser/BigEndianBinaryReader.cs
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Parser
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public class BigEndianBinaryReader : BinaryReader
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public BigEndianBinaryReader(Stream input) : base(input)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public BigEndianBinaryReader(Stream input, Encoding encoding) : base(input, encoding)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public BigEndianBinaryReader(Stream input, Encoding encoding, bool leaveOpen) : base(input, encoding, leaveOpen)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override Stream BaseStream => base.BaseStream;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override void Close() => base.Close();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool Equals(object? obj) => base.Equals(obj);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override int GetHashCode() => base.GetHashCode();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override int PeekChar() => base.PeekChar();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override int Read() => base.Read();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override int Read(byte[] buffer, int index, int count) => base.Read(buffer, index, count);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override int Read(char[] buffer, int index, int count) => base.Read(buffer, index, count);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override bool ReadBoolean() => base.ReadBoolean();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override byte ReadByte() => base.ReadByte();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override byte[] ReadBytes(int count) => base.ReadBytes(count);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override char ReadChar() => base.ReadChar();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override char[] ReadChars(int count) => base.ReadChars(count);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override decimal ReadDecimal() => BitConverter.IsLittleEndian ? throw new NotSupportedException("not supported") : base.ReadDecimal();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override double ReadDouble() => BitConverter.IsLittleEndian
|
||||||
|
? BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64BigEndian(base.ReadBytes(sizeof(double))))
|
||||||
|
: base.ReadDouble();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override short ReadInt16() => BitConverter.IsLittleEndian ? BinaryPrimitives.ReadInt16BigEndian(base.ReadBytes(sizeof(short))) : base.ReadInt16();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override int ReadInt32() => BitConverter.IsLittleEndian ? BinaryPrimitives.ReadInt32BigEndian(base.ReadBytes(sizeof(int))) : base.ReadInt32();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override long ReadInt64() => BitConverter.IsLittleEndian ? BinaryPrimitives.ReadInt64BigEndian(base.ReadBytes(sizeof(long))) : base.ReadInt64();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override sbyte ReadSByte() => base.ReadSByte();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override float ReadSingle() => BitConverter.IsLittleEndian
|
||||||
|
? Int32BitsToSingle(BinaryPrimitives.ReadInt32BigEndian(base.ReadBytes(sizeof(float))))
|
||||||
|
: base.ReadSingle();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static unsafe float Int32BitsToSingle(int value) => *(float*)&value;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string ReadString() => base.ReadString();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override ushort ReadUInt16() => BitConverter.IsLittleEndian ? BinaryPrimitives.ReadUInt16BigEndian(base.ReadBytes(sizeof(ushort))) : base.ReadUInt16();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override uint ReadUInt32() => BitConverter.IsLittleEndian ? BinaryPrimitives.ReadUInt32BigEndian(base.ReadBytes(sizeof(uint))) : base.ReadUInt32();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override ulong ReadUInt64() => BitConverter.IsLittleEndian ? BinaryPrimitives.ReadUInt64BigEndian(base.ReadBytes(sizeof(ulong))) : base.ReadUInt64();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public override string? ToString() => base.ToString();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override void Dispose(bool disposing) => base.Dispose(disposing);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected override void FillBuffer(int numBytes) => base.FillBuffer(numBytes);
|
||||||
|
}
|
||||||
|
}
|
17
BililiveRecorder.Flv/Parser/FlvException.cs
Normal file
17
BililiveRecorder.Flv/Parser/FlvException.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Parser
|
||||||
|
{
|
||||||
|
public class FlvException : Exception
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public FlvException() { }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public FlvException(string message) : base(message) { }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public FlvException(string message, Exception innerException) : base(message, innerException) { }
|
||||||
|
/// <inheritdoc/>
|
||||||
|
protected FlvException(SerializationInfo info, StreamingContext context) : base(info, context) { }
|
||||||
|
}
|
||||||
|
}
|
338
BililiveRecorder.Flv/Parser/FlvTagPipeReader.cs
Normal file
338
BililiveRecorder.Flv/Parser/FlvTagPipeReader.cs
Normal file
|
@ -0,0 +1,338 @@
|
||||||
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Parser
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 从 <see cref="PipeReader"/> 读取 <see cref="FlvDataTag"/>
|
||||||
|
/// </summary>
|
||||||
|
public class FlvTagPipeReader : IFlvTagReader, IDisposable
|
||||||
|
{
|
||||||
|
private static int memoryCreateCounter = 0;
|
||||||
|
|
||||||
|
private readonly IMemoryStreamProvider memoryStreamProvider;
|
||||||
|
private readonly bool skipData;
|
||||||
|
private readonly bool leaveOpen;
|
||||||
|
|
||||||
|
private bool peek = false;
|
||||||
|
private Tag? peekTag = null;
|
||||||
|
private readonly SemaphoreSlim peekSemaphoreSlim = new SemaphoreSlim(1, 1);
|
||||||
|
|
||||||
|
private bool fileHeader = false;
|
||||||
|
|
||||||
|
public PipeReader Reader { get; }
|
||||||
|
|
||||||
|
public FlvTagPipeReader(PipeReader reader, IMemoryStreamProvider memoryStreamProvider) : this(reader, memoryStreamProvider, false) { }
|
||||||
|
|
||||||
|
public FlvTagPipeReader(PipeReader reader, IMemoryStreamProvider memoryStreamProvider, bool skipData = false) : this(reader, memoryStreamProvider, skipData, false) { }
|
||||||
|
|
||||||
|
public FlvTagPipeReader(PipeReader reader, IMemoryStreamProvider memoryStreamProvider, bool skipData = false, bool leaveOpen = false)
|
||||||
|
{
|
||||||
|
this.Reader = reader ?? throw new ArgumentNullException(nameof(reader));
|
||||||
|
|
||||||
|
this.memoryStreamProvider = memoryStreamProvider ?? throw new ArgumentNullException(nameof(memoryStreamProvider));
|
||||||
|
this.skipData = skipData;
|
||||||
|
this.leaveOpen = leaveOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!this.leaveOpen)
|
||||||
|
this.Reader.Complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 实现二进制数据的解析
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>解析出的 Flv Tag</returns>
|
||||||
|
private async Task<Tag?> ReadNextTagAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var result = await this.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
var buffer = result.Buffer;
|
||||||
|
|
||||||
|
// In the event that no message is parsed successfully, mark consumed
|
||||||
|
// as nothing and examined as the entire buffer.
|
||||||
|
var consumed = buffer.Start;
|
||||||
|
var examined = buffer.End;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!this.fileHeader)
|
||||||
|
{
|
||||||
|
if (this.ParseFileHeader(ref buffer))
|
||||||
|
{
|
||||||
|
this.fileHeader = true;
|
||||||
|
consumed = buffer.Start;
|
||||||
|
examined = consumed;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.ParseTagData(ref buffer, out var tag))
|
||||||
|
{
|
||||||
|
// A single message was successfully parsed so mark the start as the
|
||||||
|
// parsed buffer as consumed. TryParseMessage trims the buffer to
|
||||||
|
// point to the data after the message was parsed.
|
||||||
|
consumed = buffer.Start;
|
||||||
|
|
||||||
|
// Examined is marked the same as consumed here, so the next call
|
||||||
|
// to ReadSingleMessageAsync will process the next message if there's
|
||||||
|
// one.
|
||||||
|
examined = consumed;
|
||||||
|
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
examined = buffer.End;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.IsCompleted)
|
||||||
|
{
|
||||||
|
if (buffer.Length > 0)
|
||||||
|
{
|
||||||
|
// The message is incomplete and there's no more data to process.
|
||||||
|
// throw new FlvException("Incomplete message.");
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.Reader.AdvanceTo(consumed, examined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool ParseFileHeader(ref ReadOnlySequence<byte> buffer)
|
||||||
|
{
|
||||||
|
if (buffer.Length < 9)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var fileHeaderSlice = buffer.Slice(buffer.Start, 9);
|
||||||
|
|
||||||
|
Span<byte> stackSpan = stackalloc byte[9];
|
||||||
|
ReadOnlySpan<byte> data = stackSpan;
|
||||||
|
|
||||||
|
if (fileHeaderSlice.IsSingleSegment)
|
||||||
|
data = fileHeaderSlice.First.Span;
|
||||||
|
else
|
||||||
|
fileHeaderSlice.CopyTo(stackSpan);
|
||||||
|
|
||||||
|
if (data[0] != 'F' || data[1] != 'L' || data[2] != 'V' || data[3] != 1)
|
||||||
|
throw new FlvException("Data is not FLV.");
|
||||||
|
|
||||||
|
if (data[5] != 0 || data[6] != 0 || data[7] != 0 || data[8] != 9)
|
||||||
|
throw new FlvException("Not Supported FLV format.");
|
||||||
|
|
||||||
|
buffer = buffer.Slice(fileHeaderSlice.End);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool ParseTagData(ref ReadOnlySequence<byte> buffer, [NotNullWhen(true)] out Tag? tag)
|
||||||
|
{
|
||||||
|
tag = default;
|
||||||
|
|
||||||
|
if (buffer.Length < 11 + 4)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Slice Tag Header
|
||||||
|
var tagHeaderSlice = buffer.Slice(4, 11);
|
||||||
|
buffer = buffer.Slice(tagHeaderSlice.End);
|
||||||
|
|
||||||
|
Span<byte> stackTemp = stackalloc byte[4];
|
||||||
|
Span<byte> stackHeaderSpan = stackalloc byte[11];
|
||||||
|
ReadOnlySpan<byte> header = stackHeaderSpan;
|
||||||
|
|
||||||
|
if (tagHeaderSlice.IsSingleSegment)
|
||||||
|
header = tagHeaderSlice.First.Span;
|
||||||
|
else
|
||||||
|
tagHeaderSlice.CopyTo(stackHeaderSpan);
|
||||||
|
|
||||||
|
Debug.Assert(header.Length == 11, "Tag header length is not 11.");
|
||||||
|
|
||||||
|
// Read Tag Type
|
||||||
|
var tagType = (TagType)header[0];
|
||||||
|
|
||||||
|
switch (tagType)
|
||||||
|
{
|
||||||
|
case TagType.Audio:
|
||||||
|
case TagType.Video:
|
||||||
|
case TagType.Script:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new FlvException("Unexpected Tag Type: " + header[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read Tag Size
|
||||||
|
stackTemp[0] = 0;
|
||||||
|
stackTemp[1] = header[1];
|
||||||
|
stackTemp[2] = header[2];
|
||||||
|
stackTemp[3] = header[3];
|
||||||
|
var tagSize = BinaryPrimitives.ReadUInt32BigEndian(stackTemp);
|
||||||
|
|
||||||
|
// if not enough data are available
|
||||||
|
if (buffer.Length < tagSize)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Read Tag Timestamp
|
||||||
|
stackTemp[1] = header[4];
|
||||||
|
stackTemp[2] = header[5];
|
||||||
|
stackTemp[3] = header[6];
|
||||||
|
stackTemp[0] = header[7];
|
||||||
|
var tagTimestamp = BinaryPrimitives.ReadInt32BigEndian(stackTemp);
|
||||||
|
|
||||||
|
// Slice Tag Data
|
||||||
|
var tagDataSlice = buffer.Slice(buffer.Start, tagSize);
|
||||||
|
buffer = buffer.Slice(tagDataSlice.End);
|
||||||
|
|
||||||
|
// Copy Tag Data If Required
|
||||||
|
var tagBodyStream = this.memoryStreamProvider.CreateMemoryStream(nameof(FlvTagPipeReader) + ":TagBody:" + Interlocked.Increment(ref memoryCreateCounter));
|
||||||
|
|
||||||
|
foreach (var segment in tagDataSlice)
|
||||||
|
{
|
||||||
|
var sharedBuffer = ArrayPool<byte>.Shared.Rent(segment.Length);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
segment.CopyTo(sharedBuffer);
|
||||||
|
tagBodyStream.Write(sharedBuffer, 0, segment.Length);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(sharedBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Tag Flag
|
||||||
|
var tagFlag = TagFlag.None;
|
||||||
|
|
||||||
|
if (tagBodyStream.Length > 2)
|
||||||
|
{
|
||||||
|
tagBodyStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
switch (tagType)
|
||||||
|
{
|
||||||
|
case TagType.Audio:
|
||||||
|
{
|
||||||
|
var format = tagBodyStream.ReadByte() >> 4;
|
||||||
|
if (format != 10) // AAC
|
||||||
|
break;
|
||||||
|
var packet = tagBodyStream.ReadByte();
|
||||||
|
if (packet == 0)
|
||||||
|
tagFlag = TagFlag.Header;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TagType.Video:
|
||||||
|
{
|
||||||
|
var frame = tagBodyStream.ReadByte();
|
||||||
|
if ((frame & 0x0F) != 7) // AVC
|
||||||
|
break;
|
||||||
|
if (frame == 0x17)
|
||||||
|
tagFlag |= TagFlag.Keyframe;
|
||||||
|
var packet = tagBodyStream.ReadByte();
|
||||||
|
tagFlag |= packet switch
|
||||||
|
{
|
||||||
|
0 => TagFlag.Header,
|
||||||
|
2 => TagFlag.End,
|
||||||
|
_ => TagFlag.None,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Tag Object
|
||||||
|
tag = new Tag
|
||||||
|
{
|
||||||
|
Type = tagType,
|
||||||
|
Flag = tagFlag,
|
||||||
|
Size = tagSize,
|
||||||
|
Timestamp = tagTimestamp,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read Tag Type Specific Data
|
||||||
|
tagBodyStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
if (tag.Type == TagType.Script)
|
||||||
|
{
|
||||||
|
tag.ScriptData = Amf.ScriptTagBody.Parse(tagBodyStream);
|
||||||
|
}
|
||||||
|
else if (tag.Type == TagType.Video && !tag.Flag.HasFlag(TagFlag.Header))
|
||||||
|
{
|
||||||
|
if (H264Nalu.TryParseNalu(tagBodyStream, out var nalus))
|
||||||
|
tag.Nalus = nalus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispose Stream If Not Needed
|
||||||
|
if (!this.skipData || tag.ShouldSerializeBinaryDataForSerializationUseOnly())
|
||||||
|
tag.BinaryData = tagBodyStream;
|
||||||
|
else
|
||||||
|
tagBodyStream.Dispose();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<Tag?> PeekTagAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.peekSemaphoreSlim.Wait();
|
||||||
|
|
||||||
|
if (this.peek)
|
||||||
|
{
|
||||||
|
return this.peekTag;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.peekTag = await this.ReadNextTagAsync();
|
||||||
|
this.peek = true;
|
||||||
|
return this.peekTag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.peekSemaphoreSlim.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public async Task<Tag?> ReadTagAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.peekSemaphoreSlim.Wait();
|
||||||
|
if (this.peek)
|
||||||
|
{
|
||||||
|
var tag = this.peekTag;
|
||||||
|
this.peekTag = null;
|
||||||
|
this.peek = false;
|
||||||
|
return tag;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return await this.ReadNextTagAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.peekSemaphoreSlim.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
BililiveRecorder.Flv/Pipeline/FlvProcessingContext.cs
Normal file
56
BililiveRecorder.Flv/Pipeline/FlvProcessingContext.cs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline
|
||||||
|
{
|
||||||
|
public class FlvProcessingContext
|
||||||
|
{
|
||||||
|
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
|
public FlvProcessingContext()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public FlvProcessingContext(PipelineAction data, IDictionary<object, object?> sessionItems)
|
||||||
|
{
|
||||||
|
this.Reset(data, sessionItems);
|
||||||
|
}
|
||||||
|
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
|
|
||||||
|
public PipelineAction OriginalInput { get; private set; }
|
||||||
|
|
||||||
|
public List<PipelineAction> Output { get; set; }
|
||||||
|
|
||||||
|
public IDictionary<object, object?> SessionItems { get; private set; }
|
||||||
|
|
||||||
|
public IDictionary<object, object?> LocalItems { get; private set; }
|
||||||
|
|
||||||
|
public List<string> Comments { get; private set; }
|
||||||
|
|
||||||
|
public void Reset(PipelineAction data, IDictionary<object, object?> sessionItems)
|
||||||
|
{
|
||||||
|
this.OriginalInput = data ?? throw new ArgumentNullException(nameof(data));
|
||||||
|
this.SessionItems = sessionItems ?? throw new ArgumentNullException(nameof(sessionItems));
|
||||||
|
this.Output = new List<PipelineAction> { this.OriginalInput.Clone() };
|
||||||
|
this.LocalItems = new Dictionary<object, object?>();
|
||||||
|
this.Comments = new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FlvProcessingContextExtensions
|
||||||
|
{
|
||||||
|
public static void AddComment(this FlvProcessingContext context, string comment)
|
||||||
|
=> context.Comments.Add(comment);
|
||||||
|
|
||||||
|
public static void AddNewFileAtStart(this FlvProcessingContext context)
|
||||||
|
=> context.Output.Insert(0, PipelineNewFileAction.Instance);
|
||||||
|
|
||||||
|
public static void AddNewFileAtEnd(this FlvProcessingContext context)
|
||||||
|
=> context.Output.Add(PipelineNewFileAction.Instance);
|
||||||
|
|
||||||
|
public static void AddDisconnectAtStart(this FlvProcessingContext context)
|
||||||
|
=> context.Output.Insert(0, PipelineDisconnectAction.Instance);
|
||||||
|
|
||||||
|
public static void ClearOutput(this FlvProcessingContext context)
|
||||||
|
=> context.Output.Clear();
|
||||||
|
}
|
||||||
|
}
|
9
BililiveRecorder.Flv/Pipeline/IFullProcessingRule.cs
Normal file
9
BililiveRecorder.Flv/Pipeline/IFullProcessingRule.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline
|
||||||
|
{
|
||||||
|
public interface IFullProcessingRule : IProcessingRule
|
||||||
|
{
|
||||||
|
Task RunAsync(FlvProcessingContext context, ProcessingDelegate next);
|
||||||
|
}
|
||||||
|
}
|
13
BililiveRecorder.Flv/Pipeline/IProcessingPipelineBuilder.cs
Normal file
13
BililiveRecorder.Flv/Pipeline/IProcessingPipelineBuilder.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline
|
||||||
|
{
|
||||||
|
public interface IProcessingPipelineBuilder
|
||||||
|
{
|
||||||
|
IServiceProvider ServiceProvider { get; }
|
||||||
|
|
||||||
|
IProcessingPipelineBuilder Add(Func<ProcessingDelegate, ProcessingDelegate> rule);
|
||||||
|
|
||||||
|
ProcessingDelegate Build();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
using System;
|
||||||
|
using BililiveRecorder.Flv.Pipeline.Rules;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline
|
||||||
|
{
|
||||||
|
public static class IProcessingPipelineBuilderExtensions
|
||||||
|
{
|
||||||
|
public static IProcessingPipelineBuilder Add<T>(this IProcessingPipelineBuilder builder) where T : IProcessingRule =>
|
||||||
|
builder.Add(next => (ActivatorUtilities.GetServiceOrCreateInstance<T>(builder.ServiceProvider)) switch
|
||||||
|
{
|
||||||
|
ISimpleProcessingRule simple => context => simple.RunAsync(context, () => next(context)),
|
||||||
|
IFullProcessingRule full => context => full.RunAsync(context, next),
|
||||||
|
_ => throw new ArgumentException($"Type ({typeof(T).FullName}) does not ISimpleProcessingRule or IFullProcessingRule")
|
||||||
|
});
|
||||||
|
|
||||||
|
public static IProcessingPipelineBuilder Add<T>(this IProcessingPipelineBuilder builder, T instance) where T : IProcessingRule =>
|
||||||
|
instance switch
|
||||||
|
{
|
||||||
|
ISimpleProcessingRule simple => builder.Add(next => context => simple.RunAsync(context, () => next(context))),
|
||||||
|
IFullProcessingRule full => builder.Add(next => context => full.RunAsync(context, next)),
|
||||||
|
_ => throw new ArgumentException($"Type ({typeof(T).FullName}) does not ISimpleProcessingRule or IFullProcessingRule")
|
||||||
|
};
|
||||||
|
|
||||||
|
public static IProcessingPipelineBuilder AddDefault(this IProcessingPipelineBuilder builder) =>
|
||||||
|
builder
|
||||||
|
.Add<CheckMissingKeyframeRule>()
|
||||||
|
.Add<CheckDiscontinuityRule>()
|
||||||
|
.Add<UpdateTimestampRule>()
|
||||||
|
.Add<HandleNewScriptRule>()
|
||||||
|
.Add<HandleNewHeaderRule>()
|
||||||
|
.Add<RemoveDuplicatedChunkRule>()
|
||||||
|
;
|
||||||
|
|
||||||
|
public static IProcessingPipelineBuilder AddRemoveFillerData(this IProcessingPipelineBuilder builder) =>
|
||||||
|
builder.Add<RemoveFillerDataRule>();
|
||||||
|
}
|
||||||
|
}
|
7
BililiveRecorder.Flv/Pipeline/IProcessingRule.cs
Normal file
7
BililiveRecorder.Flv/Pipeline/IProcessingRule.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline
|
||||||
|
{
|
||||||
|
public interface IProcessingRule
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
10
BililiveRecorder.Flv/Pipeline/ISimpleProcessingRule.cs
Normal file
10
BililiveRecorder.Flv/Pipeline/ISimpleProcessingRule.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline
|
||||||
|
{
|
||||||
|
public interface ISimpleProcessingRule : IProcessingRule
|
||||||
|
{
|
||||||
|
Task RunAsync(FlvProcessingContext context, Func<Task> next);
|
||||||
|
}
|
||||||
|
}
|
7
BililiveRecorder.Flv/Pipeline/PipelineAction.cs
Normal file
7
BililiveRecorder.Flv/Pipeline/PipelineAction.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline
|
||||||
|
{
|
||||||
|
public abstract class PipelineAction
|
||||||
|
{
|
||||||
|
public abstract PipelineAction Clone();
|
||||||
|
}
|
||||||
|
}
|
17
BililiveRecorder.Flv/Pipeline/PipelineDataAction.cs
Normal file
17
BililiveRecorder.Flv/Pipeline/PipelineDataAction.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline
|
||||||
|
{
|
||||||
|
public class PipelineDataAction : PipelineAction
|
||||||
|
{
|
||||||
|
public PipelineDataAction(IList<Tag> tags)
|
||||||
|
{
|
||||||
|
this.Tags = tags ?? throw new ArgumentNullException(nameof(tags));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IList<Tag> Tags { get; set; }
|
||||||
|
|
||||||
|
public override PipelineAction Clone() => new PipelineDataAction(new List<Tag>(this.Tags));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline
|
||||||
|
{
|
||||||
|
public class PipelineDisconnectAction : PipelineAction
|
||||||
|
{
|
||||||
|
public static readonly PipelineDisconnectAction Instance = new PipelineDisconnectAction();
|
||||||
|
|
||||||
|
public override PipelineAction Clone() => Instance;
|
||||||
|
}
|
||||||
|
}
|
26
BililiveRecorder.Flv/Pipeline/PipelineHeaderAction.cs
Normal file
26
BililiveRecorder.Flv/Pipeline/PipelineHeaderAction.cs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline
|
||||||
|
{
|
||||||
|
public class PipelineHeaderAction : PipelineAction
|
||||||
|
{
|
||||||
|
public PipelineHeaderAction(IReadOnlyList<Tag> allTags)
|
||||||
|
{
|
||||||
|
this.AllTags = allTags ?? throw new ArgumentNullException(nameof(allTags));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Tag? VideoHeader { get; set; }
|
||||||
|
|
||||||
|
public Tag? AudioHeader { get; set; }
|
||||||
|
|
||||||
|
public IReadOnlyList<Tag> AllTags { get; set; }
|
||||||
|
|
||||||
|
public override PipelineAction Clone() => new PipelineHeaderAction(this.AllTags.ToArray())
|
||||||
|
{
|
||||||
|
VideoHeader = VideoHeader,
|
||||||
|
AudioHeader = AudioHeader
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline
|
||||||
|
{
|
||||||
|
public class PipelineLogAlternativeHeaderAction : PipelineAction
|
||||||
|
{
|
||||||
|
public IReadOnlyList<Tag> Tags { get; set; }
|
||||||
|
|
||||||
|
public PipelineLogAlternativeHeaderAction(IReadOnlyList<Tag> tags)
|
||||||
|
{
|
||||||
|
this.Tags = tags ?? throw new ArgumentNullException(nameof(tags));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override PipelineAction Clone() => new PipelineLogAlternativeHeaderAction(this.Tags.ToArray());
|
||||||
|
}
|
||||||
|
}
|
9
BililiveRecorder.Flv/Pipeline/PipelineNewFileAction.cs
Normal file
9
BililiveRecorder.Flv/Pipeline/PipelineNewFileAction.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline
|
||||||
|
{
|
||||||
|
public class PipelineNewFileAction : PipelineAction
|
||||||
|
{
|
||||||
|
public static readonly PipelineNewFileAction Instance = new PipelineNewFileAction();
|
||||||
|
|
||||||
|
public override PipelineAction Clone() => Instance;
|
||||||
|
}
|
||||||
|
}
|
16
BililiveRecorder.Flv/Pipeline/PipelineScriptAction.cs
Normal file
16
BililiveRecorder.Flv/Pipeline/PipelineScriptAction.cs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline
|
||||||
|
{
|
||||||
|
public class PipelineScriptAction : PipelineAction
|
||||||
|
{
|
||||||
|
public PipelineScriptAction(Tag tag)
|
||||||
|
{
|
||||||
|
this.Tag = tag ?? throw new ArgumentNullException(nameof(tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Tag Tag { get; set; }
|
||||||
|
|
||||||
|
public override PipelineAction Clone() => new PipelineScriptAction(this.Tag);
|
||||||
|
}
|
||||||
|
}
|
6
BililiveRecorder.Flv/Pipeline/ProcessingDelegate.cs
Normal file
6
BililiveRecorder.Flv/Pipeline/ProcessingDelegate.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline
|
||||||
|
{
|
||||||
|
public delegate Task ProcessingDelegate(FlvProcessingContext context);
|
||||||
|
}
|
28
BililiveRecorder.Flv/Pipeline/ProcessingPipelineBuilder.cs
Normal file
28
BililiveRecorder.Flv/Pipeline/ProcessingPipelineBuilder.cs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline
|
||||||
|
{
|
||||||
|
public class ProcessingPipelineBuilder : IProcessingPipelineBuilder
|
||||||
|
{
|
||||||
|
public IServiceProvider ServiceProvider { get; }
|
||||||
|
|
||||||
|
private readonly List<Func<ProcessingDelegate, ProcessingDelegate>> rules = new List<Func<ProcessingDelegate, ProcessingDelegate>>();
|
||||||
|
|
||||||
|
public ProcessingPipelineBuilder(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
this.ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IProcessingPipelineBuilder Add(Func<ProcessingDelegate, ProcessingDelegate> rule)
|
||||||
|
{
|
||||||
|
this.rules.Add(rule);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProcessingDelegate Build()
|
||||||
|
=> this.rules.AsEnumerable().Reverse().Aggregate((ProcessingDelegate)(_ => Task.CompletedTask), (i, o) => o(i));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline.Rules
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 检查分块内时间戳问题
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 到目前为止还未发现有在一个 GOP 内出现时间戳异常问题<br/>
|
||||||
|
/// 本规则是为了预防实际使用中遇到意外情况<br/>
|
||||||
|
/// <br/>
|
||||||
|
/// 本规则应该放在所有规则前面
|
||||||
|
/// </remarks>
|
||||||
|
public class CheckDiscontinuityRule : ISimpleProcessingRule
|
||||||
|
{
|
||||||
|
private const int MAX_ALLOWED_DIFF = 1000 * 10; // 10 seconds
|
||||||
|
|
||||||
|
public Task RunAsync(FlvProcessingContext context, Func<Task> next)
|
||||||
|
{
|
||||||
|
if (context.OriginalInput is PipelineDataAction data)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < data.Tags.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var f1 = data.Tags[i];
|
||||||
|
var f2 = data.Tags[i + 1];
|
||||||
|
|
||||||
|
if (f1.Timestamp > f2.Timestamp)
|
||||||
|
{
|
||||||
|
context.ClearOutput();
|
||||||
|
context.AddDisconnectAtStart();
|
||||||
|
context.AddComment("Flv Chunk 内出现时间戳跳变(变小)");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
else if ((f2.Timestamp - f1.Timestamp) > MAX_ALLOWED_DIFF)
|
||||||
|
{
|
||||||
|
context.ClearOutput();
|
||||||
|
context.AddDisconnectAtStart();
|
||||||
|
context.AddComment("Flv Chunk 内出现时间戳跳变(间隔过大)");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
else return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline.Rules
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 检查缺少关键帧的问题
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 到目前为止还未发现有出现过此问题<br/>
|
||||||
|
/// 本规则是为了预防实际使用中遇到意外情况<br/>
|
||||||
|
/// <br/>
|
||||||
|
/// 本规则应该放在所有规则前面
|
||||||
|
/// </remarks>
|
||||||
|
public class CheckMissingKeyframeRule : ISimpleProcessingRule
|
||||||
|
{
|
||||||
|
public Task RunAsync(FlvProcessingContext context, Func<Task> next)
|
||||||
|
{
|
||||||
|
if (context.OriginalInput is PipelineDataAction data)
|
||||||
|
{
|
||||||
|
var f = data.Tags.FirstOrDefault(x => x.Type == TagType.Video);
|
||||||
|
if (f != null && !f.Flag.HasFlag(TagFlag.Keyframe))
|
||||||
|
{
|
||||||
|
context.AddComment("Flv Chunk 内缺少关键帧");
|
||||||
|
context.AddDisconnectAtStart();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
else return next();
|
||||||
|
}
|
||||||
|
else return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
144
BililiveRecorder.Flv/Pipeline/Rules/HandleNewHeaderRule.cs
Normal file
144
BililiveRecorder.Flv/Pipeline/Rules/HandleNewHeaderRule.cs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline.Rules
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 处理收到音视频 Header 的情况
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 当收到音视频 Header 时检查与上一组是否相同<br/>
|
||||||
|
/// 并根据情况删除重复的 Header 或新建文件写入<br/>
|
||||||
|
/// <br/>
|
||||||
|
/// 本规则为一般规则
|
||||||
|
/// </remarks>
|
||||||
|
public class HandleNewHeaderRule : ISimpleProcessingRule
|
||||||
|
{
|
||||||
|
private const string VIDEO_HEADER_KEY = "HandleNewHeaderRule_VideoHeader";
|
||||||
|
private const string AUDIO_HEADER_KEY = "HandleNewHeaderRule_AudioHeader";
|
||||||
|
|
||||||
|
public Task RunAsync(FlvProcessingContext context, Func<Task> next)
|
||||||
|
{
|
||||||
|
if (!(context.OriginalInput is PipelineHeaderAction header))
|
||||||
|
return next();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Tag? lastVideoHeader, lastAudioHeader;
|
||||||
|
Tag? currentVideoHeader, currentAudioHeader;
|
||||||
|
var multiple_header_present = false;
|
||||||
|
|
||||||
|
// 从 Session Items 里取上次写入的 Header
|
||||||
|
lastVideoHeader = context.SessionItems.ContainsKey(VIDEO_HEADER_KEY) ? context.SessionItems[VIDEO_HEADER_KEY] as Tag : null;
|
||||||
|
lastAudioHeader = context.SessionItems.ContainsKey(AUDIO_HEADER_KEY) ? context.SessionItems[AUDIO_HEADER_KEY] as Tag : null;
|
||||||
|
|
||||||
|
// 音频 视频 分别单独处理
|
||||||
|
var group = header.AllTags.GroupBy(x => x.Type);
|
||||||
|
|
||||||
|
{ // 音频
|
||||||
|
var group_audio = group.FirstOrDefault(x => x.Key == TagType.Audio);
|
||||||
|
if (group_audio != null)
|
||||||
|
{
|
||||||
|
// 检查是否存在 **多个** **不同的** Header
|
||||||
|
if (group_audio.Count() > 1)
|
||||||
|
{
|
||||||
|
var first = group_audio.First();
|
||||||
|
|
||||||
|
if (group_audio.Skip(1).All(x => first.BinaryData?.SequenceEqual(x.BinaryData) ?? false))
|
||||||
|
currentAudioHeader = first;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 默认最后一个为正确的
|
||||||
|
currentAudioHeader = group_audio.Last();
|
||||||
|
multiple_header_present = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else currentAudioHeader = group_audio.FirstOrDefault();
|
||||||
|
}
|
||||||
|
else currentAudioHeader = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
{ // 视频
|
||||||
|
var group_video = group.FirstOrDefault(x => x.Key == TagType.Video);
|
||||||
|
if (group_video != null)
|
||||||
|
{
|
||||||
|
// 检查是否存在 **多个** **不同的** Header
|
||||||
|
if (group_video.Count() > 1)
|
||||||
|
{
|
||||||
|
var first = group_video.First();
|
||||||
|
|
||||||
|
if (group_video.Skip(1).All(x => first.BinaryData?.SequenceEqual(x.BinaryData) ?? false))
|
||||||
|
currentVideoHeader = first;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 默认最后一个为正确的
|
||||||
|
currentVideoHeader = group_video.Last();
|
||||||
|
multiple_header_present = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else currentVideoHeader = group_video.FirstOrDefault();
|
||||||
|
}
|
||||||
|
else currentVideoHeader = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (multiple_header_present)
|
||||||
|
context.AddComment("收到了连续多个 Header");
|
||||||
|
|
||||||
|
if (currentVideoHeader != null)
|
||||||
|
context.SessionItems[VIDEO_HEADER_KEY] = currentVideoHeader.Clone(); // TODO use memory provider
|
||||||
|
if (currentAudioHeader != null)
|
||||||
|
context.SessionItems[AUDIO_HEADER_KEY] = currentAudioHeader.Clone();
|
||||||
|
|
||||||
|
if (lastAudioHeader is null && lastVideoHeader is null)
|
||||||
|
{
|
||||||
|
// 本 session 第一次输出 header
|
||||||
|
context.Output.Clear();
|
||||||
|
|
||||||
|
var output = new PipelineHeaderAction(Array.Empty<Tag>())
|
||||||
|
{
|
||||||
|
AudioHeader = currentAudioHeader?.Clone(),
|
||||||
|
VideoHeader = currentVideoHeader?.Clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Output.Add(output);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 是否需要创建新文件
|
||||||
|
// 如果存在多个不同 Header 则必定创建新文件
|
||||||
|
var split_file = multiple_header_present;
|
||||||
|
|
||||||
|
// 如果最终选中的 Header 不等于上次写入的 Header
|
||||||
|
if (lastAudioHeader != null && !(currentAudioHeader?.BinaryData?.SequenceEqual(lastAudioHeader.BinaryData) ?? false))
|
||||||
|
split_file = true;
|
||||||
|
if (lastVideoHeader != null && !(currentVideoHeader?.BinaryData?.SequenceEqual(lastVideoHeader.BinaryData) ?? false))
|
||||||
|
split_file = true;
|
||||||
|
|
||||||
|
if (split_file)
|
||||||
|
context.AddComment("因为 Header 问题新建文件");
|
||||||
|
|
||||||
|
context.Output.Clear();
|
||||||
|
|
||||||
|
if (split_file)
|
||||||
|
{
|
||||||
|
context.AddNewFileAtStart();
|
||||||
|
|
||||||
|
var output = new PipelineHeaderAction(Array.Empty<Tag>())
|
||||||
|
{
|
||||||
|
AudioHeader = currentAudioHeader?.Clone(),
|
||||||
|
VideoHeader = currentVideoHeader?.Clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
context.Output.Add(output);
|
||||||
|
|
||||||
|
// 输出所有 Header 到一个独立的文件,以防出现无法播放的问题
|
||||||
|
// 如果不能正常播放,后期可以使用这里保存的 Header 配合 FlvInteractiveRebase 工具手动修复
|
||||||
|
if (multiple_header_present)
|
||||||
|
context.Output.Add(new PipelineLogAlternativeHeaderAction(header.AllTags));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
BililiveRecorder.Flv/Pipeline/Rules/HandleNewScriptRule.cs
Normal file
24
BililiveRecorder.Flv/Pipeline/Rules/HandleNewScriptRule.cs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline.Rules
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 处理收到 Script Tag 的情况
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 本规则为一般规则
|
||||||
|
/// </remarks>
|
||||||
|
public class HandleNewScriptRule : ISimpleProcessingRule
|
||||||
|
{
|
||||||
|
public Task RunAsync(FlvProcessingContext context, Func<Task> next)
|
||||||
|
{
|
||||||
|
if (context.OriginalInput is PipelineScriptAction)
|
||||||
|
{
|
||||||
|
context.AddNewFileAtStart();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
else return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BililiveRecorder.Flv;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline.Rules
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 删除重复的直播数据
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 通过对比直播数据的特征,删除重复的直播数据<br/>
|
||||||
|
/// <br/>
|
||||||
|
/// 本规则为一般规则
|
||||||
|
/// </remarks>
|
||||||
|
public class RemoveDuplicatedChunkRule : ISimpleProcessingRule
|
||||||
|
{
|
||||||
|
private const int MAX_HISTORY = 8;
|
||||||
|
private const string QUEUE_KEY = "DeDuplicationQueue";
|
||||||
|
|
||||||
|
public Task RunAsync(FlvProcessingContext context, Func<Task> next)
|
||||||
|
{
|
||||||
|
if (!(context.OriginalInput is PipelineDataAction data))
|
||||||
|
return next();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var feature = new List<long>(data.Tags.Count * 2 + 1)
|
||||||
|
{
|
||||||
|
data.Tags.Count
|
||||||
|
};
|
||||||
|
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
// 计算一个特征码
|
||||||
|
// 此处并没有遵循什么特定的算法,只是随便取了一些代表性比较强的值,用简单又尽量可靠的方式糅合到一起而已
|
||||||
|
foreach (var tag in data.Tags)
|
||||||
|
{
|
||||||
|
var f = 0L;
|
||||||
|
f ^= tag.Type switch
|
||||||
|
{
|
||||||
|
TagType.Audio => 0b01,
|
||||||
|
TagType.Video => 0b10,
|
||||||
|
TagType.Script => 0b11,
|
||||||
|
_ => 0b00,
|
||||||
|
};
|
||||||
|
f <<= 3;
|
||||||
|
f ^= (int)tag.Flag & ((1 << 3) - 1);
|
||||||
|
f <<= 32;
|
||||||
|
f ^= tag.Timestamp;
|
||||||
|
f <<= 32 - 5;
|
||||||
|
f ^= tag.Size & ((1 << (32 - 5)) - 1);
|
||||||
|
feature.Add(f);
|
||||||
|
|
||||||
|
if (tag.Nalus == null)
|
||||||
|
feature.Add(long.MinValue);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
long n = tag.Nalus.Count << 32;
|
||||||
|
foreach (var nalu in tag.Nalus)
|
||||||
|
n ^= (((int)nalu.Type) << 16) ^ ((int)nalu.FullSize);
|
||||||
|
feature.Add(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储最近 MAX_HISTORY 个 Data Chunk 的特征的 Queue
|
||||||
|
Queue<List<long>> history;
|
||||||
|
if (context.SessionItems.TryGetValue(QUEUE_KEY, out var obj) && obj is Queue<List<long>> q)
|
||||||
|
history = q;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
history = new Queue<List<long>>(MAX_HISTORY + 1);
|
||||||
|
context.SessionItems[QUEUE_KEY] = history;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对比历史特征
|
||||||
|
if (history.Any(x => x.SequenceEqual(feature)))
|
||||||
|
{
|
||||||
|
context.ClearOutput();
|
||||||
|
context.AddComment("发现了重复的 Flv Chunk");
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
history.Enqueue(feature);
|
||||||
|
|
||||||
|
while (history.Count > MAX_HISTORY)
|
||||||
|
history.Dequeue();
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
BililiveRecorder.Flv/Pipeline/Rules/RemoveFillerDataRule.cs
Normal file
36
BililiveRecorder.Flv/Pipeline/Rules/RemoveFillerDataRule.cs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline.Rules
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 删除 H.264 Filler Data
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 部分直播码率瞎填的主播的直播数据中存在大量无用的 Filler Data<br/>
|
||||||
|
/// 录制这些主播时删除这些数据可以节省硬盘空间<br/>
|
||||||
|
/// <br/>
|
||||||
|
/// 本规则应该放在一般规则前面
|
||||||
|
/// </remarks>
|
||||||
|
public class RemoveFillerDataRule : ISimpleProcessingRule
|
||||||
|
{
|
||||||
|
public async Task RunAsync(FlvProcessingContext context, Func<Task> next)
|
||||||
|
{
|
||||||
|
// 先运行下层规则
|
||||||
|
await next().ConfigureAwait(false);
|
||||||
|
|
||||||
|
// 处理下层规则返回的数据
|
||||||
|
context.Output.ForEach(action =>
|
||||||
|
{
|
||||||
|
if (action is PipelineDataAction dataAction)
|
||||||
|
foreach (var tag in dataAction.Tags)
|
||||||
|
if (tag.Nalus != null)
|
||||||
|
// 虽然这里处理的是 Output 但是因为与 Input 共享同一个 object 所以会把 Input 一起改掉
|
||||||
|
// tag.Nalus = tag.Nalus.Where(x => x.Type != H264NaluType.FillerData).ToList();
|
||||||
|
tag.Nalus.RemoveAll(x => x.Type == H264NaluType.FillerData);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
132
BililiveRecorder.Flv/Pipeline/Rules/UpdateTimestampRule.cs
Normal file
132
BililiveRecorder.Flv/Pipeline/Rules/UpdateTimestampRule.cs
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Pipeline.Rules
|
||||||
|
{
|
||||||
|
public class UpdateTimestampRule : ISimpleProcessingRule
|
||||||
|
{
|
||||||
|
private const string TS_STORE_KEY = "Timestamp_Store_Key";
|
||||||
|
|
||||||
|
private const int JUMP_THRESHOLD = 50;
|
||||||
|
|
||||||
|
private const int AUDIO_DURATION_FALLBACK = 22;
|
||||||
|
private const int AUDIO_DURATION_MIN = 20;
|
||||||
|
private const int AUDIO_DURATION_MAX = 24;
|
||||||
|
|
||||||
|
private const int VIDEO_DURATION_FALLBACK = 33;
|
||||||
|
private const int VIDEO_DURATION_MIN = 15;
|
||||||
|
private const int VIDEO_DURATION_MAX = 50;
|
||||||
|
|
||||||
|
public async Task RunAsync(FlvProcessingContext context, Func<Task> next)
|
||||||
|
{
|
||||||
|
await next();
|
||||||
|
|
||||||
|
var ts = context.SessionItems.ContainsKey(TS_STORE_KEY) ? context.SessionItems[TS_STORE_KEY] as TimestampStore ?? new TimestampStore() : new TimestampStore();
|
||||||
|
context.SessionItems[TS_STORE_KEY] = ts;
|
||||||
|
|
||||||
|
foreach (var action in context.Output)
|
||||||
|
{
|
||||||
|
if (action is PipelineDataAction dataAction)
|
||||||
|
{
|
||||||
|
this.SetDataTimestamp(dataAction.Tags, ts, context);
|
||||||
|
}
|
||||||
|
else if (action is PipelineNewFileAction)
|
||||||
|
{
|
||||||
|
ts.Reset();
|
||||||
|
}
|
||||||
|
else if (action is PipelineScriptAction s)
|
||||||
|
{
|
||||||
|
s.Tag.Timestamp = 0;
|
||||||
|
ts.Reset();
|
||||||
|
}
|
||||||
|
else if (action is PipelineHeaderAction h)
|
||||||
|
{
|
||||||
|
if (h.VideoHeader != null)
|
||||||
|
h.VideoHeader.Timestamp = 0;
|
||||||
|
if (h.AudioHeader != null)
|
||||||
|
h.AudioHeader.Timestamp = 0;
|
||||||
|
ts.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetDataTimestamp(IList<Tag> tags, TimestampStore ts, FlvProcessingContext context)
|
||||||
|
{
|
||||||
|
// 检查有至少一个音频数据
|
||||||
|
// 在 CheckMissingKeyframeRule 已经确认了有视频数据不需要重复检查
|
||||||
|
if (!tags.Any(x => x.Type == TagType.Audio))
|
||||||
|
{
|
||||||
|
context.AddComment("未检测到音频数据,跳过时间戳修改");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var diff = tags[0].Timestamp - ts.LastOriginal;
|
||||||
|
if (diff < 0)
|
||||||
|
{
|
||||||
|
var offsetDiff = this.GetOffsetDiff(tags, ts);
|
||||||
|
context.AddComment("时间戳问题:变小, Offset Diff: " + offsetDiff);
|
||||||
|
ts.CurrentOffset += offsetDiff;
|
||||||
|
}
|
||||||
|
else if (diff > JUMP_THRESHOLD)
|
||||||
|
{
|
||||||
|
var offsetDiff = this.GetOffsetDiff(tags, ts);
|
||||||
|
context.AddComment("时间戳问题:间隔过大, Offset Diff: " + offsetDiff);
|
||||||
|
ts.CurrentOffset += offsetDiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
ts.LastVideoOriginal = tags.Last(x => x.Type == TagType.Video).Timestamp;
|
||||||
|
ts.LastAudioOriginal = tags.Last(x => x.Type == TagType.Audio).Timestamp;
|
||||||
|
ts.LastOriginal = Math.Max(ts.LastVideoOriginal, ts.LastAudioOriginal);
|
||||||
|
|
||||||
|
foreach (var tag in tags)
|
||||||
|
tag.Timestamp -= ts.CurrentOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetOffsetDiff(IList<Tag> tags, TimestampStore ts)
|
||||||
|
{
|
||||||
|
var videoDiff = this.GetAudioOrVideoOffsetDiff(tags.Where(x => x.Type == TagType.Video).Take(2).ToArray(),
|
||||||
|
ts.LastVideoOriginal, t => t >= VIDEO_DURATION_MIN && t <= VIDEO_DURATION_MAX, VIDEO_DURATION_FALLBACK);
|
||||||
|
|
||||||
|
var audioDiff = this.GetAudioOrVideoOffsetDiff(tags.Where(x => x.Type == TagType.Audio).Take(2).ToArray(),
|
||||||
|
ts.LastAudioOriginal, t => t >= AUDIO_DURATION_MIN && t <= AUDIO_DURATION_MAX, AUDIO_DURATION_FALLBACK);
|
||||||
|
|
||||||
|
return Math.Min(videoDiff, audioDiff);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetAudioOrVideoOffsetDiff(Tag[] sample, int lastTimestamp, Func<int, bool> validFunc, int fallbackDuration)
|
||||||
|
{
|
||||||
|
if (sample.Length <= 1)
|
||||||
|
return sample[0].Timestamp - lastTimestamp - fallbackDuration;
|
||||||
|
|
||||||
|
var duration = sample[1].Timestamp - sample[0].Timestamp;
|
||||||
|
|
||||||
|
var valid = validFunc(duration);
|
||||||
|
|
||||||
|
if (!valid)
|
||||||
|
duration = fallbackDuration;
|
||||||
|
|
||||||
|
return sample[0].Timestamp - lastTimestamp - duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TimestampStore
|
||||||
|
{
|
||||||
|
public int LastOriginal;
|
||||||
|
|
||||||
|
public int LastVideoOriginal;
|
||||||
|
|
||||||
|
public int LastAudioOriginal;
|
||||||
|
|
||||||
|
public int CurrentOffset;
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
this.LastOriginal = 0;
|
||||||
|
this.LastVideoOriginal = 0;
|
||||||
|
this.LastAudioOriginal = 0;
|
||||||
|
this.CurrentOffset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
128
BililiveRecorder.Flv/StreamExtensions.cs
Normal file
128
BililiveRecorder.Flv/StreamExtensions.cs
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv
|
||||||
|
{
|
||||||
|
internal static class StreamExtensions
|
||||||
|
{
|
||||||
|
private const int BUFFER_SIZE = 4 * 1024;
|
||||||
|
private static readonly ThreadLocal<byte[]> t_buffer = new ThreadLocal<byte[]>(() => new byte[BUFFER_SIZE]);
|
||||||
|
|
||||||
|
internal static async Task<int> SkipBytesAsync(this Stream stream, int length)
|
||||||
|
{
|
||||||
|
if (length < 0) { throw new ArgumentOutOfRangeException("length must be non-negative"); }
|
||||||
|
if (length == 0) { return 0; }
|
||||||
|
if (null == stream) { throw new ArgumentNullException(nameof(stream)); }
|
||||||
|
if (!stream.CanRead) { throw new ArgumentException("cannot read stream", nameof(stream)); }
|
||||||
|
|
||||||
|
if (stream.CanSeek)
|
||||||
|
{
|
||||||
|
if (stream.Length - stream.Position >= length)
|
||||||
|
{
|
||||||
|
stream.Position += length;
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = t_buffer.Value!;
|
||||||
|
var total = 0;
|
||||||
|
|
||||||
|
while (length > BUFFER_SIZE)
|
||||||
|
{
|
||||||
|
var read = await stream.ReadAsync(buffer, 0, BUFFER_SIZE);
|
||||||
|
total += read;
|
||||||
|
if (read != BUFFER_SIZE) { return total; }
|
||||||
|
length -= BUFFER_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
total += await stream.ReadAsync(buffer, 0, length);
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<bool> CopyBytesAsync(this Stream from, Stream to, long length)
|
||||||
|
{
|
||||||
|
if (null == from) { throw new ArgumentNullException(nameof(from)); }
|
||||||
|
if (null == to) { throw new ArgumentNullException(nameof(to)); }
|
||||||
|
if (length < 0) { throw new ArgumentOutOfRangeException("length must be non-negative"); }
|
||||||
|
if (length == 0) { return true; }
|
||||||
|
if (!from.CanRead) { throw new ArgumentException("cannot read stream", nameof(from)); }
|
||||||
|
if (!to.CanWrite) { throw new ArgumentException("cannot write stream", nameof(to)); }
|
||||||
|
|
||||||
|
var buffer = t_buffer.Value!;
|
||||||
|
|
||||||
|
while (length > BUFFER_SIZE)
|
||||||
|
{
|
||||||
|
if (BUFFER_SIZE != await from.ReadAsync(buffer, 0, BUFFER_SIZE))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await to.WriteAsync(buffer, 0, BUFFER_SIZE);
|
||||||
|
length -= BUFFER_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length != await from.ReadAsync(buffer, 0, (int)length))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await to.WriteAsync(buffer, 0, (int)length);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<bool> CopyBytesAsync(this Stream stream, byte[] target)
|
||||||
|
{
|
||||||
|
if (null == stream) { throw new ArgumentNullException(nameof(stream)); }
|
||||||
|
if (null == target) { throw new ArgumentNullException(nameof(target)); }
|
||||||
|
if (!stream.CanRead) { throw new ArgumentException("cannot read stream", nameof(stream)); }
|
||||||
|
if (target.Length < 1) return true;
|
||||||
|
|
||||||
|
var head = 0;
|
||||||
|
while (head < target.Length)
|
||||||
|
{
|
||||||
|
var read = await stream.ReadAsync(target, head, target.Length - head);
|
||||||
|
head += read;
|
||||||
|
if (read == 0)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static byte[] ToBE(this byte[] b)
|
||||||
|
{
|
||||||
|
if (BitConverter.IsLittleEndian)
|
||||||
|
{
|
||||||
|
Array.Reverse(b);
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Write(this Stream stream, byte[] bytes) => stream.Write(bytes, 0, bytes.Length);
|
||||||
|
|
||||||
|
internal static bool SequenceEqual(this Stream self, Stream? other)
|
||||||
|
{
|
||||||
|
if (self is null)
|
||||||
|
throw new ArgumentNullException(nameof(self));
|
||||||
|
|
||||||
|
if (self == other)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (self.Length != (other?.Length ?? -1))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
self.Seek(0, SeekOrigin.Begin);
|
||||||
|
other!.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
int b;
|
||||||
|
while ((b = self.ReadByte()) != -1)
|
||||||
|
if (b != other!.ReadByte())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
165
BililiveRecorder.Flv/Tag.cs
Normal file
165
BililiveRecorder.Flv/Tag.cs
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
using BililiveRecorder.Flv.Amf;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv
|
||||||
|
{
|
||||||
|
[DebuggerDisplay("{DebuggerDisplay,nq}")]
|
||||||
|
public sealed class Tag : ICloneable
|
||||||
|
{
|
||||||
|
[XmlAttribute]
|
||||||
|
public TagType Type { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute]
|
||||||
|
public TagFlag Flag { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute]
|
||||||
|
public uint Size { get; set; }
|
||||||
|
|
||||||
|
[XmlAttribute]
|
||||||
|
public int Timestamp { get; set; }
|
||||||
|
|
||||||
|
[XmlIgnore]
|
||||||
|
public Stream? BinaryData { get; set; }
|
||||||
|
|
||||||
|
[XmlElement]
|
||||||
|
public ScriptTagBody? ScriptData { get; set; }
|
||||||
|
|
||||||
|
[XmlElement]
|
||||||
|
public List<H264Nalu>? Nalus { get; set; }
|
||||||
|
|
||||||
|
[XmlElement(nameof(BinaryData))]
|
||||||
|
public string? BinaryDataForSerializationUseOnly
|
||||||
|
{
|
||||||
|
get => this.BinaryData == null
|
||||||
|
? null
|
||||||
|
: BinaryConvertUtilities.StreamToHexString(this.BinaryData);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
Stream? new_stream = null;
|
||||||
|
if (value != null)
|
||||||
|
new_stream = BinaryConvertUtilities.HexStringToMemoryStream(value);
|
||||||
|
|
||||||
|
this.BinaryData?.Dispose();
|
||||||
|
this.BinaryData = new_stream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ShouldSerializeBinaryDataForSerializationUseOnly() => this.Flag.HasFlag(TagFlag.Header);
|
||||||
|
|
||||||
|
public bool ShouldSerializeScriptData() => this.Type == TagType.Script;
|
||||||
|
|
||||||
|
public bool ShouldSerializeNalus() => this.Type == TagType.Video && !this.Flag.HasFlag(TagFlag.Header);
|
||||||
|
|
||||||
|
public override string ToString() => this.DebuggerDisplay;
|
||||||
|
|
||||||
|
object ICloneable.Clone() => this.Clone(null);
|
||||||
|
public Tag Clone() => this.Clone(null);
|
||||||
|
public Tag Clone(IMemoryStreamProvider? provider = null)
|
||||||
|
{
|
||||||
|
Stream? binaryData = null;
|
||||||
|
if (this.BinaryData != null)
|
||||||
|
{
|
||||||
|
binaryData = provider?.CreateMemoryStream(nameof(Tag) + ":" + nameof(Clone)) ?? new MemoryStream();
|
||||||
|
this.BinaryData.Seek(0, SeekOrigin.Begin);
|
||||||
|
this.BinaryData.CopyToAsync(binaryData);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScriptTagBody? scriptData = null;
|
||||||
|
if (this.ScriptData != null)
|
||||||
|
{
|
||||||
|
using var stream = provider?.CreateMemoryStream(nameof(Tag) + ":" + nameof(Clone) + ":Temp") ?? new MemoryStream();
|
||||||
|
this.ScriptData.WriteTo(stream);
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
scriptData = ScriptTagBody.Parse(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Tag
|
||||||
|
{
|
||||||
|
Type = this.Type,
|
||||||
|
Flag = this.Flag,
|
||||||
|
Size = this.Size,
|
||||||
|
Timestamp = this.Timestamp,
|
||||||
|
BinaryData = binaryData,
|
||||||
|
ScriptData = scriptData,
|
||||||
|
Nalus = this.Nalus is null ? null : new List<H264Nalu>(this.Nalus),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string DebuggerDisplay => string.Format("{0}, {1}{2}{3}, TS={4}, Size={5}",
|
||||||
|
this.Type switch
|
||||||
|
{
|
||||||
|
TagType.Audio => "A",
|
||||||
|
TagType.Video => "V",
|
||||||
|
TagType.Script => "S",
|
||||||
|
_ => "?",
|
||||||
|
},
|
||||||
|
this.Flag.HasFlag(TagFlag.Keyframe) ? "K" : "-",
|
||||||
|
this.Flag.HasFlag(TagFlag.Header) ? "H" : "-",
|
||||||
|
this.Flag.HasFlag(TagFlag.End) ? "E" : "-",
|
||||||
|
this.Timestamp,
|
||||||
|
this.Size);
|
||||||
|
|
||||||
|
private static class BinaryConvertUtilities
|
||||||
|
{
|
||||||
|
private static readonly uint[] _lookup32 = CreateLookup32();
|
||||||
|
|
||||||
|
private static uint[] CreateLookup32()
|
||||||
|
{
|
||||||
|
var result = new uint[256];
|
||||||
|
for (var i = 0; i < 256; i++)
|
||||||
|
{
|
||||||
|
var s = i.ToString("X2");
|
||||||
|
result[i] = s[0] + ((uint)s[1] << 16);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string ByteArrayToHexString(byte[] bytes)
|
||||||
|
{
|
||||||
|
var lookup32 = _lookup32;
|
||||||
|
var result = new char[bytes.Length * 2];
|
||||||
|
for (var i = 0; i < bytes.Length; i++)
|
||||||
|
{
|
||||||
|
var val = lookup32[bytes[i]];
|
||||||
|
result[2 * i] = (char)val;
|
||||||
|
result[2 * i + 1] = (char)(val >> 16);
|
||||||
|
}
|
||||||
|
return new string(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static byte[] HexStringToByteArray(string hex)
|
||||||
|
{
|
||||||
|
var bytes = new byte[hex.Length / 2];
|
||||||
|
for (var i = 0; i < hex.Length; i += 2)
|
||||||
|
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string StreamToHexString(Stream stream)
|
||||||
|
{
|
||||||
|
var lookup32 = _lookup32;
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
var result = new char[stream.Length * 2];
|
||||||
|
for (var i = 0; i < stream.Length; i++)
|
||||||
|
{
|
||||||
|
var val = lookup32[stream.ReadByte()];
|
||||||
|
result[2 * i] = (char)val;
|
||||||
|
result[2 * i + 1] = (char)(val >> 16);
|
||||||
|
}
|
||||||
|
return new string(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Stream HexStringToMemoryStream(string hex)
|
||||||
|
{
|
||||||
|
var stream = new MemoryStream(hex.Length / 2);
|
||||||
|
for (var i = 0; i < hex.Length; i += 2)
|
||||||
|
stream.WriteByte(Convert.ToByte(hex.Substring(i, 2), 16));
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
112
BililiveRecorder.Flv/TagExtentions.cs
Normal file
112
BililiveRecorder.Flv/TagExtentions.cs
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv
|
||||||
|
{
|
||||||
|
public static class TagExtentions
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static bool IsScript(this Tag tag)
|
||||||
|
=> tag.Type == TagType.Script;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static bool IsHeader(this Tag tag)
|
||||||
|
=> (tag.Type == TagType.Video || tag.Type == TagType.Audio)
|
||||||
|
&& tag.Flag.HasFlag(TagFlag.Header);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static bool IsData(this Tag tag)
|
||||||
|
=> tag.Type != TagType.Script;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static bool IsNonKeyframeData(this Tag tag)
|
||||||
|
=> (tag.Type == TagType.Video || tag.Type == TagType.Audio)
|
||||||
|
&& tag.Flag == TagFlag.None;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static bool IsKeyframeData(this Tag tag)
|
||||||
|
=> tag.Type == TagType.Video && tag.Flag == TagFlag.Keyframe;
|
||||||
|
|
||||||
|
public static async Task WriteTo(this Tag tag, Stream target, int timestamp, IMemoryStreamProvider? memoryStreamProvider = null)
|
||||||
|
{
|
||||||
|
var buffer = ArrayPool<byte>.Shared.Rent(11);
|
||||||
|
var dispose = true;
|
||||||
|
Stream? data = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (tag.IsScript())
|
||||||
|
{
|
||||||
|
if (tag.ScriptData is null)
|
||||||
|
throw new Exception("BinaryData is null");
|
||||||
|
|
||||||
|
data = memoryStreamProvider?.CreateMemoryStream(nameof(TagExtentions) + ":" + nameof(WriteTo) + ":TagBodyTemp") ?? new MemoryStream();
|
||||||
|
tag.ScriptData.WriteTo(data);
|
||||||
|
}
|
||||||
|
else if (tag.Nalus != null)
|
||||||
|
{
|
||||||
|
if (tag.BinaryData is null)
|
||||||
|
throw new Exception("BinaryData is null");
|
||||||
|
|
||||||
|
data = memoryStreamProvider?.CreateMemoryStream(nameof(TagExtentions) + ":" + nameof(WriteTo) + ":TagBodyTemp") ?? new MemoryStream();
|
||||||
|
|
||||||
|
tag.BinaryData.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
tag.BinaryData.Read(buffer, 0, 5);
|
||||||
|
data.Write(buffer, 0, 5);
|
||||||
|
|
||||||
|
foreach (var nalu in tag.Nalus)
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteUInt32BigEndian(new Span<byte>(buffer, 0, 4), nalu.FullSize);
|
||||||
|
data.Write(buffer, 0, 4);
|
||||||
|
|
||||||
|
tag.BinaryData.Seek(nalu.StartPosition, SeekOrigin.Begin);
|
||||||
|
await tag.BinaryData.CopyBytesAsync(data, nalu.FullSize).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (tag.BinaryData is null)
|
||||||
|
throw new Exception("BinaryData is null");
|
||||||
|
|
||||||
|
dispose = false;
|
||||||
|
data = tag.BinaryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
BinaryPrimitives.WriteUInt32BigEndian(new Span<byte>(buffer, 0, 4), (uint)data.Length);
|
||||||
|
buffer[0] = (byte)tag.Type;
|
||||||
|
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
var stackTemp = stackalloc byte[4];
|
||||||
|
BinaryPrimitives.WriteInt32BigEndian(new Span<byte>(stackTemp, 4), timestamp);
|
||||||
|
buffer[4] = stackTemp[1];
|
||||||
|
buffer[5] = stackTemp[2];
|
||||||
|
buffer[6] = stackTemp[3];
|
||||||
|
buffer[7] = stackTemp[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer[8] = 0;
|
||||||
|
buffer[9] = 0;
|
||||||
|
buffer[10] = 0;
|
||||||
|
|
||||||
|
await target.WriteAsync(buffer, 0, 11).ConfigureAwait(false);
|
||||||
|
await data.CopyToAsync(target).ConfigureAwait(false);
|
||||||
|
|
||||||
|
BinaryPrimitives.WriteUInt32BigEndian(new Span<byte>(buffer, 0, 4), (uint)data.Length + 11);
|
||||||
|
await target.WriteAsync(buffer, 0, 4).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
if (dispose)
|
||||||
|
data?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
BililiveRecorder.Flv/TagFlag.cs
Normal file
13
BililiveRecorder.Flv/TagFlag.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv
|
||||||
|
{
|
||||||
|
[Flags]
|
||||||
|
public enum TagFlag : int
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Header = 1 << 0,
|
||||||
|
Keyframe = 1 << 1,
|
||||||
|
End = 1 << 2,
|
||||||
|
}
|
||||||
|
}
|
10
BililiveRecorder.Flv/TagType.cs
Normal file
10
BililiveRecorder.Flv/TagType.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace BililiveRecorder.Flv
|
||||||
|
{
|
||||||
|
public enum TagType : int
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Audio = 8,
|
||||||
|
Video = 9,
|
||||||
|
Script = 18,
|
||||||
|
}
|
||||||
|
}
|
332
BililiveRecorder.Flv/Writer/FlvProcessingContextWriter.cs
Normal file
332
BililiveRecorder.Flv/Writer/FlvProcessingContextWriter.cs
Normal file
|
@ -0,0 +1,332 @@
|
||||||
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BililiveRecorder.Flv.Amf;
|
||||||
|
using BililiveRecorder.Flv.Pipeline;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Writer
|
||||||
|
{
|
||||||
|
public class FlvProcessingContextWriter : IFlvProcessingContextWriter, IDisposable
|
||||||
|
{
|
||||||
|
private static readonly byte[] FLV_FILE_HEADER = new byte[] { (byte)'F', (byte)'L', (byte)'V', 1, 0b0000_0101, 0, 0, 0, 9, 0, 0, 0, 0 };
|
||||||
|
|
||||||
|
private readonly SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
|
||||||
|
private readonly IFlvWriterTargetProvider targetProvider;
|
||||||
|
private readonly IMemoryStreamProvider memoryStreamProvider;
|
||||||
|
|
||||||
|
private WriterState state = WriterState.EmptyFileOrNotOpen;
|
||||||
|
|
||||||
|
private Stream? stream = null;
|
||||||
|
|
||||||
|
private Tag? nextScriptTag = null;
|
||||||
|
private Tag? nextAudioHeaderTag = null;
|
||||||
|
private Tag? nextVideoHeaderTag = null;
|
||||||
|
|
||||||
|
private ScriptTagBody? lastScriptBody = null;
|
||||||
|
private uint lastScriptBodyLength = 0;
|
||||||
|
|
||||||
|
public Action<ScriptTagBody>? BeforeScriptTagWrite { get; set; }
|
||||||
|
public Action<ScriptTagBody>? BeforeScriptTagRewrite { get; set; }
|
||||||
|
|
||||||
|
public FlvProcessingContextWriter(IFlvWriterTargetProvider targetProvider, IMemoryStreamProvider memoryStreamProvider)
|
||||||
|
{
|
||||||
|
this.targetProvider = targetProvider ?? throw new ArgumentNullException(nameof(targetProvider));
|
||||||
|
this.memoryStreamProvider = memoryStreamProvider ?? throw new ArgumentNullException(nameof(memoryStreamProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task WriteAsync(FlvProcessingContext context)
|
||||||
|
{
|
||||||
|
if (this.state == WriterState.Invalid)
|
||||||
|
throw new InvalidOperationException("FlvProcessingContextWriter is in a invalid state.");
|
||||||
|
|
||||||
|
// TODO disk speed detection
|
||||||
|
//if (!await this.semaphoreSlim.WaitAsync(1000 * 5).ConfigureAwait(false))
|
||||||
|
//{
|
||||||
|
// this.state = WriterState.Invalid;
|
||||||
|
// throw new InvalidOperationException("WriteAsync Wait timed out.");
|
||||||
|
//}
|
||||||
|
|
||||||
|
await this.semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var item in context.Output)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await this.WriteSingleActionAsync(item).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
this.state = WriterState.Invalid;
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
this.semaphoreSlim.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Flv Writer Implementation
|
||||||
|
|
||||||
|
private Task WriteSingleActionAsync(PipelineAction action) => action switch
|
||||||
|
{
|
||||||
|
PipelineNewFileAction _ => this.OpenNewFile(),
|
||||||
|
PipelineScriptAction scriptAction => this.WriteScriptTag(scriptAction),
|
||||||
|
PipelineHeaderAction headerAction => this.WriteHeaderTags(headerAction),
|
||||||
|
PipelineDataAction dataAction => this.WriteDataTags(dataAction),
|
||||||
|
PipelineLogAlternativeHeaderAction logAlternativeHeaderAction => this.WriteAlternativeHeader(logAlternativeHeaderAction),
|
||||||
|
_ => Task.CompletedTask,
|
||||||
|
};
|
||||||
|
|
||||||
|
private Task WriteAlternativeHeader(PipelineLogAlternativeHeaderAction logAlternativeHeaderAction)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenNewFile()
|
||||||
|
{
|
||||||
|
await this.CloseCurrentFileImpl().ConfigureAwait(false);
|
||||||
|
// delay open until write
|
||||||
|
this.state = WriterState.EmptyFileOrNotOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task WriteScriptTag(PipelineScriptAction scriptAction)
|
||||||
|
{
|
||||||
|
if (scriptAction.Tag != null)
|
||||||
|
this.nextScriptTag = scriptAction.Tag;
|
||||||
|
|
||||||
|
// delay writing
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task WriteHeaderTags(PipelineHeaderAction headerAction)
|
||||||
|
{
|
||||||
|
if (headerAction.AudioHeader != null)
|
||||||
|
this.nextAudioHeaderTag = headerAction.AudioHeader;
|
||||||
|
|
||||||
|
if (headerAction.VideoHeader != null)
|
||||||
|
this.nextVideoHeaderTag = headerAction.VideoHeader;
|
||||||
|
|
||||||
|
// delay writing
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CloseCurrentFileImpl()
|
||||||
|
{
|
||||||
|
if (this.stream is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await this.RewriteScriptTagImpl(0).ConfigureAwait(false);
|
||||||
|
await this.stream.FlushAsync().ConfigureAwait(false);
|
||||||
|
this.stream.Close();
|
||||||
|
this.stream.Dispose();
|
||||||
|
this.stream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenNewFileImpl()
|
||||||
|
{
|
||||||
|
await this.CloseCurrentFileImpl().ConfigureAwait(false);
|
||||||
|
|
||||||
|
Debug.Assert(this.stream is null, "stream is null");
|
||||||
|
|
||||||
|
this.stream = this.targetProvider.CreateOutputStream();
|
||||||
|
await this.stream.WriteAsync(FLV_FILE_HEADER, 0, FLV_FILE_HEADER.Length).ConfigureAwait(false);
|
||||||
|
|
||||||
|
this.state = WriterState.BeforeScript;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RewriteScriptTagImpl(double duration)
|
||||||
|
{
|
||||||
|
if (this.stream is null || this.lastScriptBody is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.lastScriptBody.Value["duration"] = (ScriptDataNumber)duration;
|
||||||
|
this.BeforeScriptTagRewrite?.Invoke(this.lastScriptBody);
|
||||||
|
|
||||||
|
this.stream.Seek(9 + 4 + 11, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
using (var buf = this.memoryStreamProvider.CreateMemoryStream(nameof(FlvProcessingContextWriter) + ":" + nameof(RewriteScriptTagImpl) + ":Temp"))
|
||||||
|
{
|
||||||
|
this.lastScriptBody.WriteTo(buf);
|
||||||
|
if (buf.Length == this.lastScriptBodyLength)
|
||||||
|
{
|
||||||
|
buf.Seek(0, SeekOrigin.Begin);
|
||||||
|
await buf.CopyToAsync(this.stream);
|
||||||
|
await this.stream.FlushAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO logging
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stream.Seek(0, SeekOrigin.End);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WriteScriptTagImpl()
|
||||||
|
{
|
||||||
|
if (this.nextScriptTag is null)
|
||||||
|
throw new InvalidOperationException("No script tag availible");
|
||||||
|
|
||||||
|
if (this.nextScriptTag.ScriptData is null)
|
||||||
|
throw new InvalidOperationException("ScriptData is null");
|
||||||
|
|
||||||
|
if (this.stream is null)
|
||||||
|
throw new Exception("stream is null");
|
||||||
|
|
||||||
|
this.lastScriptBody = this.nextScriptTag.ScriptData;
|
||||||
|
|
||||||
|
this.lastScriptBody.Value["duration"] = (ScriptDataNumber)0;
|
||||||
|
this.BeforeScriptTagWrite?.Invoke(this.lastScriptBody);
|
||||||
|
|
||||||
|
var bytes = ArrayPool<byte>.Shared.Rent(11);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var bodyStream = this.memoryStreamProvider.CreateMemoryStream(nameof(FlvProcessingContextWriter) + ":" + nameof(WriteScriptTagImpl) + ":Temp");
|
||||||
|
this.lastScriptBody.WriteTo(bodyStream);
|
||||||
|
this.lastScriptBodyLength = (uint)bodyStream.Length;
|
||||||
|
bodyStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
BinaryPrimitives.WriteUInt32BigEndian(new Span<byte>(bytes, 0, 4), this.lastScriptBodyLength);
|
||||||
|
bytes[0] = (byte)TagType.Script;
|
||||||
|
|
||||||
|
bytes[4] = 0;
|
||||||
|
bytes[5] = 0;
|
||||||
|
bytes[6] = 0;
|
||||||
|
bytes[7] = 0;
|
||||||
|
bytes[8] = 0;
|
||||||
|
bytes[9] = 0;
|
||||||
|
bytes[10] = 0;
|
||||||
|
|
||||||
|
await this.stream.WriteAsync(bytes, 0, 11).ConfigureAwait(false);
|
||||||
|
await bodyStream.CopyToAsync(this.stream);
|
||||||
|
|
||||||
|
BinaryPrimitives.WriteUInt32BigEndian(new Span<byte>(bytes, 0, 4), this.lastScriptBodyLength + 11);
|
||||||
|
await this.stream.WriteAsync(bytes, 0, 4).ConfigureAwait(false);
|
||||||
|
|
||||||
|
await this.stream.FlushAsync();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(bytes);
|
||||||
|
}
|
||||||
|
this.state = WriterState.BeforeHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WriteHeaderTagsImpl()
|
||||||
|
{
|
||||||
|
if (this.stream is null)
|
||||||
|
throw new Exception("stream is null");
|
||||||
|
|
||||||
|
if (this.nextVideoHeaderTag is null)
|
||||||
|
throw new InvalidOperationException("No video header tag availible");
|
||||||
|
|
||||||
|
if (this.nextAudioHeaderTag is null)
|
||||||
|
throw new InvalidOperationException("No audio header tag availible");
|
||||||
|
|
||||||
|
await this.nextVideoHeaderTag.WriteTo(this.stream, 0, this.memoryStreamProvider).ConfigureAwait(false);
|
||||||
|
await this.nextAudioHeaderTag.WriteTo(this.stream, 0, this.memoryStreamProvider).ConfigureAwait(false);
|
||||||
|
|
||||||
|
this.state = WriterState.Writing;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task WriteDataTags(PipelineDataAction dataAction)
|
||||||
|
{
|
||||||
|
switch (this.state)
|
||||||
|
{
|
||||||
|
case WriterState.EmptyFileOrNotOpen:
|
||||||
|
await this.OpenNewFileImpl().ConfigureAwait(false);
|
||||||
|
await this.WriteScriptTagImpl().ConfigureAwait(false);
|
||||||
|
await this.WriteHeaderTagsImpl().ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
case WriterState.BeforeScript:
|
||||||
|
await this.WriteScriptTagImpl().ConfigureAwait(false);
|
||||||
|
await this.WriteHeaderTagsImpl().ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
case WriterState.BeforeHeader:
|
||||||
|
await this.WriteHeaderTagsImpl().ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
case WriterState.Writing:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException($"Can't write data tag with current state ({this.state})");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.stream is null)
|
||||||
|
throw new Exception("stream is null");
|
||||||
|
|
||||||
|
foreach (var tag in dataAction.Tags)
|
||||||
|
await tag.WriteTo(this.stream, tag.Timestamp, this.memoryStreamProvider).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var duration = dataAction.Tags[dataAction.Tags.Count - 1].Timestamp / 1000d;
|
||||||
|
await this.RewriteScriptTagImpl(duration).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region IDisposable
|
||||||
|
|
||||||
|
private bool disposedValue;
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!this.disposedValue)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
// TODO: dispose managed state (managed objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: free unmanaged resources (unmanaged objects) and override finalizer
|
||||||
|
// TODO: set large fields to null
|
||||||
|
this.disposedValue = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
|
||||||
|
// ~FlvProcessingContextWriter()
|
||||||
|
// {
|
||||||
|
// // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||||
|
// Dispose(disposing: false);
|
||||||
|
// }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||||
|
this.Dispose(disposing: true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum WriterState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invalid
|
||||||
|
/// </summary>
|
||||||
|
Invalid,
|
||||||
|
/// <summary>
|
||||||
|
/// 未开文件、空文件、还未写入 FLV Header
|
||||||
|
/// </summary>
|
||||||
|
EmptyFileOrNotOpen,
|
||||||
|
/// <summary>
|
||||||
|
/// 已写入 FLV Header、还未写入 Script Tag
|
||||||
|
/// </summary>
|
||||||
|
BeforeScript,
|
||||||
|
/// <summary>
|
||||||
|
/// 已写入 Script Tag、还未写入 音视频 Header
|
||||||
|
/// </summary>
|
||||||
|
BeforeHeader,
|
||||||
|
/// <summary>
|
||||||
|
/// 已写入音视频 Header、正常写入数据
|
||||||
|
/// </summary>
|
||||||
|
Writing,
|
||||||
|
}
|
||||||
|
}
|
29
BililiveRecorder.Flv/Xml/FlvTagListReader.cs
Normal file
29
BililiveRecorder.Flv/Xml/FlvTagListReader.cs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Xml
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 从 <see cref="IReadOnlyList{}"/> 读取 Flv Tag
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 主要在调试修复算法和测试时使用
|
||||||
|
/// </remarks>
|
||||||
|
public class FlvTagListReader : IFlvTagReader
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyList<Tag> tags;
|
||||||
|
private int index;
|
||||||
|
|
||||||
|
public FlvTagListReader(IReadOnlyList<Tag> tags)
|
||||||
|
{
|
||||||
|
this.tags = tags ?? throw new ArgumentNullException(nameof(tags));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Tag?> PeekTagAsync() => Task.FromResult(this.index < this.tags.Count ? this.tags[this.index] : null)!;
|
||||||
|
|
||||||
|
public Task<Tag?> ReadTagAsync() => Task.FromResult(this.index < this.tags.Count ? this.tags[this.index++] : null);
|
||||||
|
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
}
|
13
BililiveRecorder.Flv/Xml/XmlFlvFile.cs
Normal file
13
BililiveRecorder.Flv/Xml/XmlFlvFile.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.Xml
|
||||||
|
{
|
||||||
|
[XmlRoot("BililiveRecorderFlv")]
|
||||||
|
public class XmlFlvFile
|
||||||
|
{
|
||||||
|
public static XmlSerializer Serializer { get; } = new XmlSerializer(typeof(XmlFlvFile));
|
||||||
|
|
||||||
|
public List<Tag> Tags { get; set; } = new List<Tag>();
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@
|
||||||
<Compile Include="..\TempBuildInfo\BuildInfo.FlvProcessor.cs" />
|
<Compile Include="..\TempBuildInfo\BuildInfo.FlvProcessor.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Autofac" Version="4.9.4" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||||
<PackageReference Include="NLog" Version="4.7.6" />
|
<PackageReference Include="NLog" Version="4.7.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
using System;
|
||||||
|
using BililiveRecorder.FlvProcessor;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.DependencyInjection
|
||||||
|
{
|
||||||
|
public static class DependencyInjectionExtensions
|
||||||
|
{
|
||||||
|
public static void AddFlvProcessor(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton<Func<IFlvTag>>(() => new FlvTag());
|
||||||
|
services.AddSingleton<IFlvMetadataFactory, FlvMetadataFactory>();
|
||||||
|
services.AddSingleton<IProcessorFactory, ProcessorFactory>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
BililiveRecorder.FlvProcessor/FlvMetadataFactory.cs
Normal file
7
BililiveRecorder.FlvProcessor/FlvMetadataFactory.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace BililiveRecorder.FlvProcessor
|
||||||
|
{
|
||||||
|
public class FlvMetadataFactory : IFlvMetadataFactory
|
||||||
|
{
|
||||||
|
public IFlvMetadata CreateFlvMetadata(byte[] data) => new FlvMetadata(data);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
using Autofac;
|
|
||||||
|
|
||||||
namespace BililiveRecorder.FlvProcessor
|
|
||||||
{
|
|
||||||
public class FlvProcessorModule : Module
|
|
||||||
{
|
|
||||||
public FlvProcessorModule()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Load(ContainerBuilder builder)
|
|
||||||
{
|
|
||||||
builder.Register(c => new FlvTag()).As<IFlvTag>();
|
|
||||||
builder.RegisterType<FlvMetadata>().As<IFlvMetadata>();
|
|
||||||
builder.RegisterType<FlvClipProcessor>().As<IFlvClipProcessor>();
|
|
||||||
builder.RegisterType<FlvStreamProcessor>().As<IFlvStreamProcessor>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -51,8 +51,8 @@ namespace BililiveRecorder.FlvProcessor
|
||||||
public int TotalMaxTimestamp { get; private set; } = 0;
|
public int TotalMaxTimestamp { get; private set; } = 0;
|
||||||
public int CurrentMaxTimestamp { get => this.TotalMaxTimestamp - this._writeTimeStamp; }
|
public int CurrentMaxTimestamp { get => this.TotalMaxTimestamp - this._writeTimeStamp; }
|
||||||
|
|
||||||
private readonly Func<IFlvClipProcessor> funcFlvClipProcessor;
|
private readonly IProcessorFactory processorFactory;
|
||||||
private readonly Func<byte[], IFlvMetadata> funcFlvMetadata;
|
private readonly IFlvMetadataFactory flvMetadataFactory;
|
||||||
private readonly Func<IFlvTag> funcFlvTag;
|
private readonly Func<IFlvTag> funcFlvTag;
|
||||||
|
|
||||||
private Func<(string fullPath, string relativePath)> GetStreamFileName;
|
private Func<(string fullPath, string relativePath)> GetStreamFileName;
|
||||||
|
@ -72,13 +72,11 @@ namespace BililiveRecorder.FlvProcessor
|
||||||
public IFlvMetadata Metadata { get; set; } = null;
|
public IFlvMetadata Metadata { get; set; } = null;
|
||||||
public ObservableCollection<IFlvClipProcessor> Clips { get; } = new ObservableCollection<IFlvClipProcessor>();
|
public ObservableCollection<IFlvClipProcessor> Clips { get; } = new ObservableCollection<IFlvClipProcessor>();
|
||||||
|
|
||||||
public FlvStreamProcessor(Func<IFlvClipProcessor> funcFlvClipProcessor, Func<byte[], IFlvMetadata> funcFlvMetadata, Func<IFlvTag> funcFlvTag)
|
public FlvStreamProcessor(IProcessorFactory processorFactory, IFlvMetadataFactory flvMetadataFactory, Func<IFlvTag> funcFlvTag)
|
||||||
{
|
{
|
||||||
this.funcFlvClipProcessor = funcFlvClipProcessor;
|
this.processorFactory = processorFactory;
|
||||||
this.funcFlvMetadata = funcFlvMetadata;
|
this.flvMetadataFactory = flvMetadataFactory;
|
||||||
this.funcFlvTag = funcFlvTag;
|
this.funcFlvTag = funcFlvTag;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IFlvStreamProcessor Initialize(Func<(string fullPath, string relativePath)> getStreamFileName, Func<string> getClipFileName, EnabledFeature enabledFeature, AutoCuttingMode autoCuttingMode)
|
public IFlvStreamProcessor Initialize(Func<(string fullPath, string relativePath)> getStreamFileName, Func<string> getClipFileName, EnabledFeature enabledFeature, AutoCuttingMode autoCuttingMode)
|
||||||
|
@ -335,7 +333,7 @@ namespace BililiveRecorder.FlvProcessor
|
||||||
this._targetFile?.Write(FLV_HEADER_BYTES, 0, FLV_HEADER_BYTES.Length);
|
this._targetFile?.Write(FLV_HEADER_BYTES, 0, FLV_HEADER_BYTES.Length);
|
||||||
this._targetFile?.Write(new byte[] { 0, 0, 0, 0, }, 0, 4);
|
this._targetFile?.Write(new byte[] { 0, 0, 0, 0, }, 0, 4);
|
||||||
|
|
||||||
this.Metadata = this.funcFlvMetadata(tag.Data);
|
this.Metadata = this.flvMetadataFactory.CreateFlvMetadata(tag.Data);
|
||||||
|
|
||||||
OnMetaData?.Invoke(this, new FlvMetadataArgs() { Metadata = Metadata });
|
OnMetaData?.Invoke(this, new FlvMetadataArgs() { Metadata = Metadata });
|
||||||
|
|
||||||
|
@ -361,7 +359,7 @@ namespace BililiveRecorder.FlvProcessor
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("剪辑处理中,将会保存过去 {0} 秒和将来 {1} 秒的直播流", (this._tags[this._tags.Count - 1].TimeStamp - this._tags[0].TimeStamp) / 1000d, this.ClipLengthFuture);
|
logger.Info("剪辑处理中,将会保存过去 {0} 秒和将来 {1} 秒的直播流", (this._tags[this._tags.Count - 1].TimeStamp - this._tags[0].TimeStamp) / 1000d, this.ClipLengthFuture);
|
||||||
IFlvClipProcessor clip = this.funcFlvClipProcessor().Initialize(this.GetClipFileName(), this.Metadata, this._headerTags, new List<IFlvTag>(this._tags.ToArray()), this.ClipLengthFuture);
|
IFlvClipProcessor clip = this.processorFactory.CreateClipProcessor().Initialize(this.GetClipFileName(), this.Metadata, this._headerTags, new List<IFlvTag>(this._tags.ToArray()), this.ClipLengthFuture);
|
||||||
clip.ClipFinalized += (sender, e) => { this.Clips.Remove(e.ClipProcessor); };
|
clip.ClipFinalized += (sender, e) => { this.Clips.Remove(e.ClipProcessor); };
|
||||||
this.Clips.Add(clip);
|
this.Clips.Add(clip);
|
||||||
return clip;
|
return clip;
|
||||||
|
|
7
BililiveRecorder.FlvProcessor/IFlvMetadataFactory.cs
Normal file
7
BililiveRecorder.FlvProcessor/IFlvMetadataFactory.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace BililiveRecorder.FlvProcessor
|
||||||
|
{
|
||||||
|
public interface IFlvMetadataFactory
|
||||||
|
{
|
||||||
|
IFlvMetadata CreateFlvMetadata(byte[] data);
|
||||||
|
}
|
||||||
|
}
|
8
BililiveRecorder.FlvProcessor/IProcessorFactory.cs
Normal file
8
BililiveRecorder.FlvProcessor/IProcessorFactory.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
namespace BililiveRecorder.FlvProcessor
|
||||||
|
{
|
||||||
|
public interface IProcessorFactory
|
||||||
|
{
|
||||||
|
IFlvClipProcessor CreateClipProcessor();
|
||||||
|
IFlvStreamProcessor CreateStreamProcessor();
|
||||||
|
}
|
||||||
|
}
|
20
BililiveRecorder.FlvProcessor/ProcessorFactory.cs
Normal file
20
BililiveRecorder.FlvProcessor/ProcessorFactory.cs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.FlvProcessor
|
||||||
|
{
|
||||||
|
public class ProcessorFactory : IProcessorFactory
|
||||||
|
{
|
||||||
|
private readonly Func<IFlvTag> flvTagFactory;
|
||||||
|
private readonly IFlvMetadataFactory flvMetadataFactory;
|
||||||
|
|
||||||
|
public ProcessorFactory(Func<IFlvTag> flvTagFactory, IFlvMetadataFactory flvMetadataFactory)
|
||||||
|
{
|
||||||
|
this.flvTagFactory = flvTagFactory ?? throw new ArgumentNullException(nameof(flvTagFactory));
|
||||||
|
this.flvMetadataFactory = flvMetadataFactory ?? throw new ArgumentNullException(nameof(flvMetadataFactory));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IFlvStreamProcessor CreateStreamProcessor() => new FlvStreamProcessor(this, this.flvMetadataFactory, this.flvTagFactory);
|
||||||
|
|
||||||
|
public IFlvClipProcessor CreateClipProcessor() => new FlvClipProcessor(this.flvTagFactory);
|
||||||
|
}
|
||||||
|
}
|
|
@ -280,15 +280,18 @@
|
||||||
</ProjectReference>
|
</ProjectReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Autofac">
|
|
||||||
<Version>4.9.4</Version>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="CommandLineParser">
|
<PackageReference Include="CommandLineParser">
|
||||||
<Version>2.4.3</Version>
|
<Version>2.4.3</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Hardcodet.NotifyIcon.Wpf">
|
<PackageReference Include="Hardcodet.NotifyIcon.Wpf">
|
||||||
<Version>1.0.8</Version>
|
<Version>1.0.8</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection">
|
||||||
|
<Version>5.0.1</Version>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions">
|
||||||
|
<Version>5.0.0</Version>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="ModernWpfUI">
|
<PackageReference Include="ModernWpfUI">
|
||||||
<Version>0.9.2</Version>
|
<Version>0.9.2</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|
|
@ -7,12 +7,12 @@ using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
using Autofac;
|
|
||||||
using BililiveRecorder.Core;
|
using BililiveRecorder.Core;
|
||||||
using BililiveRecorder.FlvProcessor;
|
using BililiveRecorder.DependencyInjection;
|
||||||
using BililiveRecorder.WPF.Controls;
|
using BililiveRecorder.WPF.Controls;
|
||||||
using BililiveRecorder.WPF.Models;
|
using BililiveRecorder.WPF.Models;
|
||||||
using CommandLine;
|
using CommandLine;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using ModernWpf.Controls;
|
using ModernWpf.Controls;
|
||||||
using ModernWpf.Media.Animation;
|
using ModernWpf.Media.Animation;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
@ -31,8 +31,7 @@ namespace BililiveRecorder.WPF.Pages
|
||||||
private readonly string lastdir_path = Path.Combine(Path.GetDirectoryName(typeof(RootPage).Assembly.Location), "lastdir.txt");
|
private readonly string lastdir_path = Path.Combine(Path.GetDirectoryName(typeof(RootPage).Assembly.Location), "lastdir.txt");
|
||||||
private readonly NavigationTransitionInfo transitionInfo = new DrillInNavigationTransitionInfo();
|
private readonly NavigationTransitionInfo transitionInfo = new DrillInNavigationTransitionInfo();
|
||||||
|
|
||||||
private IContainer Container { get; set; }
|
private ServiceProvider ServiceProvider { get; }
|
||||||
private ILifetimeScope RootScope { get; set; }
|
|
||||||
|
|
||||||
private int SettingsClickCount = 0;
|
private int SettingsClickCount = 0;
|
||||||
|
|
||||||
|
@ -50,11 +49,12 @@ namespace BililiveRecorder.WPF.Pages
|
||||||
this.Model = new RootModel();
|
this.Model = new RootModel();
|
||||||
this.DataContext = this.Model;
|
this.DataContext = this.Model;
|
||||||
|
|
||||||
var builder = new ContainerBuilder();
|
{
|
||||||
builder.RegisterModule<FlvProcessorModule>();
|
var services = new ServiceCollection();
|
||||||
builder.RegisterModule<CoreModule>();
|
services.AddFlvProcessor();
|
||||||
this.Container = builder.Build();
|
services.AddCore();
|
||||||
this.RootScope = this.Container.BeginLifetimeScope("recorder_root");
|
this.ServiceProvider = services.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
this.InitializeComponent();
|
this.InitializeComponent();
|
||||||
this.AdvancedSettingsPageItem.Visibility = Visibility.Hidden;
|
this.AdvancedSettingsPageItem.Visibility = Visibility.Hidden;
|
||||||
|
@ -63,7 +63,7 @@ namespace BililiveRecorder.WPF.Pages
|
||||||
if (mw is not null)
|
if (mw is not null)
|
||||||
mw.NativeBeforeWindowClose += this.RootPage_NativeBeforeWindowClose;
|
mw.NativeBeforeWindowClose += this.RootPage_NativeBeforeWindowClose;
|
||||||
|
|
||||||
Loaded += RootPage_Loaded;
|
Loaded += this.RootPage_Loaded;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RootPage_NativeBeforeWindowClose(object sender, EventArgs e)
|
private void RootPage_NativeBeforeWindowClose(object sender, EventArgs e)
|
||||||
|
@ -74,7 +74,7 @@ namespace BililiveRecorder.WPF.Pages
|
||||||
|
|
||||||
private async void RootPage_Loaded(object sender, RoutedEventArgs e)
|
private async void RootPage_Loaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var recorder = this.RootScope.Resolve<IRecorder>();
|
var recorder = this.ServiceProvider.GetRequiredService<IRecorder>();
|
||||||
var first_time = true;
|
var first_time = true;
|
||||||
var error = WorkDirectorySelectorDialog.WorkDirectorySelectorDialogError.None;
|
var error = WorkDirectorySelectorDialog.WorkDirectorySelectorDialogError.None;
|
||||||
string path;
|
string path;
|
||||||
|
|
18
BililiveRecorder.WPF/packages.config
Normal file
18
BililiveRecorder.WPF/packages.config
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="Autofac" version="4.8.1" targetFramework="net462" />
|
||||||
|
<package id="CommandLineParser" version="2.4.3" targetFramework="net462" />
|
||||||
|
<package id="DeltaCompressionDotNet" version="1.1.0" targetFramework="net462" />
|
||||||
|
<package id="Hardcodet.NotifyIcon.Wpf" version="1.0.8" targetFramework="net462" />
|
||||||
|
<package id="Mono.Cecil" version="0.9.6.1" targetFramework="net462" />
|
||||||
|
<package id="Newtonsoft.Json" version="12.0.3" targetFramework="net462" />
|
||||||
|
<package id="NLog" version="4.5.10" targetFramework="net462" />
|
||||||
|
<package id="NLog.Config" version="4.5.10" targetFramework="net462" />
|
||||||
|
<package id="NLog.Schema" version="4.5.10" targetFramework="net462" />
|
||||||
|
<package id="NuGet.CommandLine" version="4.7.1" targetFramework="net462" developmentDependency="true" />
|
||||||
|
<package id="SharpCompress" version="0.17.1" targetFramework="net462" />
|
||||||
|
<package id="Splat" version="1.6.2" targetFramework="net462" />
|
||||||
|
<package id="squirrel.windows" version="1.9.0" targetFramework="net462" />
|
||||||
|
<package id="WindowsAPICodePack-Core" version="1.1.2" targetFramework="net462" />
|
||||||
|
<package id="WindowsAPICodePack-Shell" version="1.1.1" targetFramework="net462" />
|
||||||
|
</packages>
|
|
@ -3,6 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 16
|
# Visual Studio Version 16
|
||||||
VisualStudioVersion = 16.0.29924.181
|
VisualStudioVersion = 16.0.29924.181
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FD13D9F6-94CD-4CBA-AEA8-EF71002EAC6B}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
.editorconfig = .editorconfig
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2D44A59D-E437-4FEE-8A2E-3FF00D53A64D}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}"
|
||||||
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BililiveRecorder.WPF", "BililiveRecorder.WPF\BililiveRecorder.WPF.csproj", "{0C7D4236-BF43-4944-81FE-E07E05A3F31D}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BililiveRecorder.WPF", "BililiveRecorder.WPF\BililiveRecorder.WPF.csproj", "{0C7D4236-BF43-4944-81FE-E07E05A3F31D}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Core", "BililiveRecorder.Core\BililiveRecorder.Core.csproj", "{CB9F2D58-181D-49F7-9560-D35A9B9C1D8C}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Core", "BililiveRecorder.Core\BililiveRecorder.Core.csproj", "{CB9F2D58-181D-49F7-9560-D35A9B9C1D8C}"
|
||||||
|
@ -14,15 +23,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.FlvProcess
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Cli", "BililiveRecorder.Cli\BililiveRecorder.Cli.csproj", "{1B626335-283F-4313-9045-B5B96FAAB2DF}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Cli", "BililiveRecorder.Cli\BililiveRecorder.Cli.csproj", "{1B626335-283F-4313-9045-B5B96FAAB2DF}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FD13D9F6-94CD-4CBA-AEA8-EF71002EAC6B}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Flv", "BililiveRecorder.Flv\BililiveRecorder.Flv.csproj", "{7610E19C-D3AB-4CBC-983E-6FDA36F4D4B3}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
|
||||||
.editorconfig = .editorconfig
|
|
||||||
EndProjectSection
|
|
||||||
EndProject
|
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}"
|
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.UnitTest.Core", "test\BililiveRecorder.UnitTest.Core\BililiveRecorder.UnitTest.Core.csproj", "{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.UnitTest.Core", "test\BililiveRecorder.UnitTest.Core\BililiveRecorder.UnitTest.Core.csproj", "{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Flv.UnitTests", "test\BililiveRecorder.Flv.UnitTests\BililiveRecorder.Flv.UnitTests.csproj", "{560E8483-9293-410E-81E9-AB36B49F8A7C}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -45,21 +51,36 @@ Global
|
||||||
{1B626335-283F-4313-9045-B5B96FAAB2DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{1B626335-283F-4313-9045-B5B96FAAB2DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{1B626335-283F-4313-9045-B5B96FAAB2DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{1B626335-283F-4313-9045-B5B96FAAB2DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{1B626335-283F-4313-9045-B5B96FAAB2DF}.Release|Any CPU.Build.0 = Release|Any CPU
|
{1B626335-283F-4313-9045-B5B96FAAB2DF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7610E19C-D3AB-4CBC-983E-6FDA36F4D4B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7610E19C-D3AB-4CBC-983E-6FDA36F4D4B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7610E19C-D3AB-4CBC-983E-6FDA36F4D4B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7610E19C-D3AB-4CBC-983E-6FDA36F4D4B3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Release|Any CPU.Build.0 = Release|Any CPU
|
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{560E8483-9293-410E-81E9-AB36B49F8A7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{560E8483-9293-410E-81E9-AB36B49F8A7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{560E8483-9293-410E-81E9-AB36B49F8A7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{560E8483-9293-410E-81E9-AB36B49F8A7C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{0C7D4236-BF43-4944-81FE-E07E05A3F31D} = {2D44A59D-E437-4FEE-8A2E-3FF00D53A64D}
|
||||||
|
{CB9F2D58-181D-49F7-9560-D35A9B9C1D8C} = {2D44A59D-E437-4FEE-8A2E-3FF00D53A64D}
|
||||||
|
{51748048-1949-4218-8DED-94014ABE7633} = {2D44A59D-E437-4FEE-8A2E-3FF00D53A64D}
|
||||||
|
{1B626335-283F-4313-9045-B5B96FAAB2DF} = {2D44A59D-E437-4FEE-8A2E-3FF00D53A64D}
|
||||||
|
{7610E19C-D3AB-4CBC-983E-6FDA36F4D4B3} = {2D44A59D-E437-4FEE-8A2E-3FF00D53A64D}
|
||||||
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}
|
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}
|
||||||
|
{560E8483-9293-410E-81E9-AB36B49F8A7C} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
RESX_SortFileContentOnSave = True
|
RESX_ShowErrorsInErrorList = False
|
||||||
SolutionGuid = {F3CB8B14-077A-458F-BD8E-1747ED0F5170}
|
|
||||||
RESX_NeutralResourcesLanguage = zh-Hans
|
|
||||||
RESX_SaveFilesImmediatelyUponChange = True
|
RESX_SaveFilesImmediatelyUponChange = True
|
||||||
|
RESX_NeutralResourcesLanguage = zh-Hans
|
||||||
|
SolutionGuid = {F3CB8B14-077A-458F-BD8E-1747ED0F5170}
|
||||||
|
RESX_SortFileContentOnSave = True
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|
89
test/BililiveRecorder.Flv.UnitTests/Amf/AmfTests.cs
Normal file
89
test/BililiveRecorder.Flv.UnitTests/Amf/AmfTests.cs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
using System;
|
||||||
|
using BililiveRecorder.Flv.Amf;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.UnitTests.Amf
|
||||||
|
{
|
||||||
|
public class AmfTests
|
||||||
|
{
|
||||||
|
private static ScriptTagBody CreateTestObject() => new ScriptTagBody
|
||||||
|
{
|
||||||
|
Name = "test",
|
||||||
|
Value = new ScriptDataObject
|
||||||
|
{
|
||||||
|
["bool_true"] = (ScriptDataBoolean)true,
|
||||||
|
["bool_false"] = (ScriptDataBoolean)false,
|
||||||
|
["date1"] = (ScriptDataDate)DateTimeOffset.FromUnixTimeMilliseconds(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()), // remove extra precision
|
||||||
|
["date2"] = (ScriptDataDate)new DateTimeOffset(2345, 3, 14, 7, 8, 9, 12, TimeSpan.FromHours(4)),
|
||||||
|
["ecmaarray"] = new ScriptDataEcmaArray
|
||||||
|
{
|
||||||
|
["element1"] = (ScriptDataString)"element1",
|
||||||
|
["element2"] = (ScriptDataString)"element2",
|
||||||
|
["element3"] = (ScriptDataString)"element3",
|
||||||
|
},
|
||||||
|
["longstring1"] = (ScriptDataLongString)"longstring1",
|
||||||
|
["longstring2"] = (ScriptDataLongString)"longstring2",
|
||||||
|
["null"] = new ScriptDataNull(),
|
||||||
|
["number1"] = (ScriptDataNumber)0,
|
||||||
|
["number2"] = (ScriptDataNumber)197653.845,
|
||||||
|
["number3"] = (ScriptDataNumber)(-95.7),
|
||||||
|
["number4"] = (ScriptDataNumber)double.Epsilon,
|
||||||
|
["strictarray"] = new ScriptDataStrictArray
|
||||||
|
{
|
||||||
|
(ScriptDataString)"element1",
|
||||||
|
(ScriptDataString)"element2",
|
||||||
|
(ScriptDataString)"element3",
|
||||||
|
},
|
||||||
|
["string1"] = (ScriptDataString)"string1",
|
||||||
|
["string2"] = (ScriptDataString)"string2",
|
||||||
|
["undefined"] = new ScriptDataUndefined(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EqualAfterJsonSerialization()
|
||||||
|
{
|
||||||
|
var body = CreateTestObject();
|
||||||
|
var json = body.ToJson();
|
||||||
|
var body2 = ScriptTagBody.Parse(json);
|
||||||
|
var json2 = body2.ToJson();
|
||||||
|
|
||||||
|
body2.Should().BeEquivalentTo(body, options => options.RespectingRuntimeTypes());
|
||||||
|
json2.Should().Be(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EqualAfterBinarySerialization()
|
||||||
|
{
|
||||||
|
var body = CreateTestObject();
|
||||||
|
var bytes = body.ToBytes();
|
||||||
|
var body2 = ScriptTagBody.Parse(bytes);
|
||||||
|
var bytes2 = body2.ToBytes();
|
||||||
|
|
||||||
|
body2.Should().BeEquivalentTo(body, options => options.RespectingRuntimeTypes());
|
||||||
|
bytes2.Should().BeEquivalentTo(bytes2, options => options.RespectingRuntimeTypes());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EqualAfterMixedSerialization()
|
||||||
|
{
|
||||||
|
var original = CreateTestObject();
|
||||||
|
|
||||||
|
var a_json = original.ToJson();
|
||||||
|
var a_body = ScriptTagBody.Parse(a_json);
|
||||||
|
var a_byte = a_body.ToBytes();
|
||||||
|
|
||||||
|
var b_byte = original.ToBytes();
|
||||||
|
var b_body = ScriptTagBody.Parse(b_byte);
|
||||||
|
var b_json = b_body.ToJson();
|
||||||
|
|
||||||
|
b_json.Should().Be(a_json);
|
||||||
|
a_byte.Should().BeEquivalentTo(b_byte);
|
||||||
|
|
||||||
|
a_body.Should().BeEquivalentTo(original, options => options.RespectingRuntimeTypes());
|
||||||
|
b_body.Should().BeEquivalentTo(original, options => options.RespectingRuntimeTypes());
|
||||||
|
a_body.Should().BeEquivalentTo(b_body, options => options.RespectingRuntimeTypes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
||||||
|
<PackageReference Include="JetBrains.DotMemoryUnit" Version="3.1.20200127.214830" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="1.4.0" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||||
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="3.0.2">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\BililiveRecorder.Flv\BililiveRecorder.Flv.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
57
test/BililiveRecorder.Flv.UnitTests/Flv/ParsingTest.cs
Normal file
57
test/BililiveRecorder.Flv.UnitTests/Flv/ParsingTest.cs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BililiveRecorder.Flv.Parser;
|
||||||
|
using BililiveRecorder.Flv.Xml;
|
||||||
|
using JetBrains.dotMemoryUnit;
|
||||||
|
using Xunit;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.UnitTests.Flv
|
||||||
|
{
|
||||||
|
public class ParsingTest
|
||||||
|
{
|
||||||
|
private readonly ITestOutputHelper _output;
|
||||||
|
|
||||||
|
public ParsingTest(ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
this._output = output;
|
||||||
|
DotMemoryUnitTestOutput.SetOutputMethod(this._output.WriteLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AssertTraffic(AllocatedSizeInBytes = 1000)]
|
||||||
|
[Fact(Skip = "Not ready")]
|
||||||
|
public async Task Run()
|
||||||
|
{
|
||||||
|
var path = @"";
|
||||||
|
|
||||||
|
var tags = new List<Tag>();
|
||||||
|
|
||||||
|
var reader = new FlvTagPipeReader(PipeReader.Create(File.OpenRead(path)), new TestRecyclableMemoryStreamProvider(), skipData: true);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var tag = await reader.ReadTagAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (tag is null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
tags.Add(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
var xmlObj = new XmlFlvFile
|
||||||
|
{
|
||||||
|
Tags = tags
|
||||||
|
};
|
||||||
|
|
||||||
|
var writer = new StringWriter();
|
||||||
|
XmlFlvFile.Serializer.Serialize(writer, xmlObj);
|
||||||
|
|
||||||
|
var xmlStr = writer.ToString();
|
||||||
|
|
||||||
|
//var peakWorkingSet = Process.GetCurrentProcess().PeakWorkingSet64;
|
||||||
|
//throw new System.Exception("PeakWorkingSet64: " + peakWorkingSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
114
test/BililiveRecorder.Flv.UnitTests/Grouping/GroupingTest.cs
Normal file
114
test/BililiveRecorder.Flv.UnitTests/Grouping/GroupingTest.cs
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BililiveRecorder.Flv.Grouping;
|
||||||
|
using BililiveRecorder.Flv.Parser;
|
||||||
|
using BililiveRecorder.Flv.Pipeline;
|
||||||
|
using BililiveRecorder.Flv.Writer;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.UnitTests.Grouping
|
||||||
|
{
|
||||||
|
public class GroupingTest
|
||||||
|
{
|
||||||
|
private const string TEST_OUTPUT_PATH = @"";
|
||||||
|
|
||||||
|
public class TestOutputProvider : IFlvWriterTargetProvider
|
||||||
|
{
|
||||||
|
public Stream CreateAlternativeHeaderStream() => throw new NotImplementedException();
|
||||||
|
public Stream CreateOutputStream() => File.Open(Path.Combine(TEST_OUTPUT_PATH, DateTimeOffset.Now.ToString("s").Replace(':', '-') + ".flv"), FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact(Skip = "Not ready")]
|
||||||
|
public async Task Test1Async()
|
||||||
|
{
|
||||||
|
var path = @"";
|
||||||
|
|
||||||
|
var results = new List<PipelineAction>();
|
||||||
|
|
||||||
|
var grouping = new TagGroupReader(new FlvTagPipeReader(PipeReader.Create(File.OpenRead(path)), new TestRecyclableMemoryStreamProvider(), skipData: true));
|
||||||
|
|
||||||
|
var context = new FlvProcessingContext();
|
||||||
|
var session = new Dictionary<object, object>();
|
||||||
|
|
||||||
|
var sp = new ServiceCollection().BuildServiceProvider();
|
||||||
|
var pipeline = new ProcessingPipelineBuilder(sp).AddDefault().AddRemoveFillerData().Build();
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var g = await grouping.ReadGroupAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (g is null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
context.Reset(g, session);
|
||||||
|
|
||||||
|
await pipeline(context);
|
||||||
|
|
||||||
|
foreach (var item in context.Output)
|
||||||
|
{
|
||||||
|
results.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sizes = results.Select(a => a switch
|
||||||
|
{
|
||||||
|
PipelineDataAction x => x.Tags.Sum(b => b.Size),
|
||||||
|
PipelineHeaderAction x => x.AllTags.Sum(b => b.Size),
|
||||||
|
PipelineScriptAction x => x.Tag.Size,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact(Skip = "Not ready")]
|
||||||
|
public async Task Test2Async()
|
||||||
|
{
|
||||||
|
const string PATH = @"";
|
||||||
|
|
||||||
|
using var grouping = new TagGroupReader(new FlvTagPipeReader(PipeReader.Create(File.OpenRead(PATH)), new TestRecyclableMemoryStreamProvider(), skipData: false));
|
||||||
|
|
||||||
|
var comments = new List<string>();
|
||||||
|
|
||||||
|
var context = new FlvProcessingContext();
|
||||||
|
var session = new Dictionary<object, object>();
|
||||||
|
|
||||||
|
var sp = new ServiceCollection().BuildServiceProvider();
|
||||||
|
var pipeline = new ProcessingPipelineBuilder(sp).AddDefault().AddRemoveFillerData().Build();
|
||||||
|
|
||||||
|
using var writer = new FlvProcessingContextWriter(new TestOutputProvider(), new TestRecyclableMemoryStreamProvider());
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var g = await grouping.ReadGroupAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (g is null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
context.Reset(g, session);
|
||||||
|
|
||||||
|
await pipeline(context);
|
||||||
|
|
||||||
|
comments.AddRange(context.Comments);
|
||||||
|
await writer.WriteAsync(context).ConfigureAwait(false);
|
||||||
|
|
||||||
|
foreach (var action in context.Output)
|
||||||
|
{
|
||||||
|
// TODO action.Dispose();
|
||||||
|
|
||||||
|
if (action is PipelineDataAction dataAction)
|
||||||
|
{
|
||||||
|
foreach (var tag in dataAction.Tags)
|
||||||
|
{
|
||||||
|
tag.BinaryData?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.IO;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.UnitTests
|
||||||
|
{
|
||||||
|
public class TestRecyclableMemoryStreamProvider : IMemoryStreamProvider
|
||||||
|
{
|
||||||
|
private static readonly RecyclableMemoryStreamManager manager
|
||||||
|
= new RecyclableMemoryStreamManager(32 * 1024, 64 * 1024, 64 * 1024 * 32)
|
||||||
|
{
|
||||||
|
MaximumFreeSmallPoolBytes = 64 * 1024 * 1024,
|
||||||
|
MaximumFreeLargePoolBytes = 64 * 1024 * 32,
|
||||||
|
};
|
||||||
|
|
||||||
|
public Stream CreateMemoryStream(string tag) => manager.GetStream(tag);
|
||||||
|
}
|
||||||
|
}
|
125
test/BililiveRecorder.Flv.UnitTests/Xml/XmlTests.cs
Normal file
125
test/BililiveRecorder.Flv.UnitTests/Xml/XmlTests.cs
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
using BililiveRecorder.Flv.Amf;
|
||||||
|
using BililiveRecorder.Flv.Parser;
|
||||||
|
using BililiveRecorder.Flv.Xml;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace BililiveRecorder.Flv.UnitTests.Xml
|
||||||
|
{
|
||||||
|
public class XmlTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Test1()
|
||||||
|
{
|
||||||
|
// TODO cleanup
|
||||||
|
|
||||||
|
var source = new XmlFlvFile
|
||||||
|
{
|
||||||
|
Tags = new List<Tag>
|
||||||
|
{
|
||||||
|
new Tag
|
||||||
|
{
|
||||||
|
Type = TagType.Script,
|
||||||
|
Size=4321,
|
||||||
|
ScriptData = new ScriptTagBody
|
||||||
|
{
|
||||||
|
Name = "test1",
|
||||||
|
Value = new ScriptDataObject
|
||||||
|
{
|
||||||
|
["key1"] = (ScriptDataNumber)5,
|
||||||
|
["key2"] = (ScriptDataString)"testTest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Tag
|
||||||
|
{
|
||||||
|
Type = TagType.Audio,
|
||||||
|
ScriptData = new ScriptTagBody
|
||||||
|
{
|
||||||
|
Name = "test2",
|
||||||
|
Value = new ScriptDataObject
|
||||||
|
{
|
||||||
|
["key1"] = (ScriptDataNumber)5,
|
||||||
|
["key2"] = (ScriptDataString)"testTest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Tag
|
||||||
|
{
|
||||||
|
Type = TagType.Audio,
|
||||||
|
Flag = TagFlag.Header,
|
||||||
|
BinaryData = new MemoryStream(new byte[]{0,1,2,3,4,5,6,7})
|
||||||
|
},
|
||||||
|
new Tag
|
||||||
|
{
|
||||||
|
Type = TagType.Video,
|
||||||
|
Flag = TagFlag.Header | TagFlag.Keyframe
|
||||||
|
},
|
||||||
|
new Tag
|
||||||
|
{
|
||||||
|
Type = TagType.Video,
|
||||||
|
Nalus = new List<H264Nalu>
|
||||||
|
{
|
||||||
|
new H264Nalu(0,156, H264NaluType.CodedSliceDataPartitionB),
|
||||||
|
new H264Nalu(198,13216, H264NaluType.Pps),
|
||||||
|
new H264Nalu(432154,432156, H264NaluType.FillerData),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var serializer = new XmlSerializer(typeof(XmlFlvFile));
|
||||||
|
|
||||||
|
var writer1 = new StringWriter();
|
||||||
|
serializer.Serialize(writer1, source);
|
||||||
|
var str1 = writer1.ToString();
|
||||||
|
|
||||||
|
var obj1 = serializer.Deserialize(new StringReader(str1));
|
||||||
|
|
||||||
|
var writer2 = new StringWriter();
|
||||||
|
serializer.Serialize(writer2, obj1);
|
||||||
|
var str2 = writer2.ToString();
|
||||||
|
|
||||||
|
var obj2 = serializer.Deserialize(new StringReader(str1));
|
||||||
|
|
||||||
|
var writer3 = new StringWriter();
|
||||||
|
serializer.Serialize(writer3, obj2);
|
||||||
|
var str3 = writer3.ToString();
|
||||||
|
|
||||||
|
Assert.Equal(str1, str2);
|
||||||
|
Assert.Equal(str2, str3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact(Skip = "Not ready")]
|
||||||
|
public async Task Test2Async()
|
||||||
|
{
|
||||||
|
var PATH = @"";
|
||||||
|
|
||||||
|
var reader = new FlvTagPipeReader(PipeReader.Create(File.OpenRead(PATH)), new TestRecyclableMemoryStreamProvider(), skipData: true);
|
||||||
|
|
||||||
|
var source = new XmlFlvFile
|
||||||
|
{
|
||||||
|
Tags = new List<Tag>()
|
||||||
|
};
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var tag = await reader.ReadTagAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (tag is null)
|
||||||
|
break;
|
||||||
|
|
||||||
|
source.Tags.Add(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
var writer1 = new StringWriter();
|
||||||
|
XmlFlvFile.Serializer.Serialize(writer1, source);
|
||||||
|
var str1 = writer1.ToString();
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,13 +8,13 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
|
||||||
<PackageReference Include="xunit" Version="2.4.1" />
|
<PackageReference Include="xunit" Version="2.4.1" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="coverlet.collector" Version="1.3.0">
|
<PackageReference Include="coverlet.collector" Version="3.0.2">
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user