Merge branch 'local_feature/new_flv'

This commit is contained in:
Genteure 2021-02-08 16:51:19 +08:00
parent 43379957f4
commit 48c8612f95
94 changed files with 3881 additions and 115 deletions

View File

@ -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>

View File

@ -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();
} }
} }

View File

@ -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>

View File

@ -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");
}
}
}

View 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>();
}
}
}

View File

@ -0,0 +1,9 @@
using BililiveRecorder.Core.Config.V2;
namespace BililiveRecorder.Core
{
public interface IRecordedRoomFactory
{
IRecordedRoom CreateRecordedRoom(RoomConfig roomConfig);
}
}

View File

@ -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;

View 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);
}
}
}

View File

@ -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;

View File

@ -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)));

View 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();
}
}

View 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();
}
}

View 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) { }
}
}

View 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);
}
}

View 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; }
}
}

View 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 };
}
}

View 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);
}
}

View 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 };
}
}

View 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 };
}
}

View 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);
}
}

View 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 };
}
}

View 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 };
}
}

View 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 };
}
}

View 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 };
}
}

View 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 };
}
}

View 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,
}
}

View 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);
}
}

View 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());
}
}

View 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>

View File

@ -0,0 +1,9 @@
using System.IO;
namespace BililiveRecorder.Flv
{
public class DefaultMemoryStreamProvider : IMemoryStreamProvider
{
public Stream CreateMemoryStream(string tag) => new MemoryStream();
}
}

View 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;
}
}
}

View 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);
}
}

View 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);
}
}

View 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());
}
}

View 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
}
}

View 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,
}
}

View 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);
}
}

View 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();
}
}

View File

@ -0,0 +1,11 @@
using System.IO;
namespace BililiveRecorder.Flv
{
public interface IFlvWriterTargetProvider
{
Stream CreateOutputStream();
Stream CreateAlternativeHeaderStream();
}
}

View 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);
}
}

View File

@ -0,0 +1,9 @@
using System.IO;
namespace BililiveRecorder.Flv
{
public interface IMemoryStreamProvider
{
Stream CreateMemoryStream(string tag);
}
}

View 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();
}
}

View 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);
}
}

View 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) { }
}
}

View 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();
}
}
}
}

View 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();
}
}

View File

@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace BililiveRecorder.Flv.Pipeline
{
public interface IFullProcessingRule : IProcessingRule
{
Task RunAsync(FlvProcessingContext context, ProcessingDelegate next);
}
}

View 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();
}
}

View File

@ -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>();
}
}

View File

@ -0,0 +1,7 @@
namespace BililiveRecorder.Flv.Pipeline
{
public interface IProcessingRule
{
}
}

View 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);
}
}

View File

@ -0,0 +1,7 @@
namespace BililiveRecorder.Flv.Pipeline
{
public abstract class PipelineAction
{
public abstract PipelineAction Clone();
}
}

View 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));
}
}

View File

@ -0,0 +1,9 @@
namespace BililiveRecorder.Flv.Pipeline
{
public class PipelineDisconnectAction : PipelineAction
{
public static readonly PipelineDisconnectAction Instance = new PipelineDisconnectAction();
public override PipelineAction Clone() => Instance;
}
}

View 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
};
}
}

View File

@ -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());
}
}

View 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;
}
}

View 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);
}
}

View File

@ -0,0 +1,6 @@
using System.Threading.Tasks;
namespace BililiveRecorder.Flv.Pipeline
{
public delegate Task ProcessingDelegate(FlvProcessingContext context);
}

View 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));
}
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}

View 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;
}
}
}
}

View 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();
}
}
}

View File

@ -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();
}
}
}
}
}

View 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;
}
}
}

View 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;
}
}
}
}

View 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
View 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;
}
}
}
}

View 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();
}
}
}
}

View 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,
}
}

View File

@ -0,0 +1,10 @@
namespace BililiveRecorder.Flv
{
public enum TagType : int
{
Unknown = 0,
Audio = 8,
Video = 9,
Script = 18,
}
}

View 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,
}
}

View 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() { }
}
}

View 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>();
}
}

View File

@ -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>

View File

@ -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>();
}
}
}

View File

@ -0,0 +1,7 @@
namespace BililiveRecorder.FlvProcessor
{
public class FlvMetadataFactory : IFlvMetadataFactory
{
public IFlvMetadata CreateFlvMetadata(byte[] data) => new FlvMetadata(data);
}
}

View File

@ -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>();
}
}
}

View File

@ -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;

View File

@ -0,0 +1,7 @@
namespace BililiveRecorder.FlvProcessor
{
public interface IFlvMetadataFactory
{
IFlvMetadata CreateFlvMetadata(byte[] data);
}
}

View File

@ -0,0 +1,8 @@
namespace BililiveRecorder.FlvProcessor
{
public interface IProcessorFactory
{
IFlvClipProcessor CreateClipProcessor();
IFlvStreamProcessor CreateStreamProcessor();
}
}

View 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);
}
}

View File

@ -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>

View File

@ -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;

View 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>

View File

@ -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

View 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());
}
}
}

View File

@ -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>

View 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);
}
}
}

View 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();
}
}
}
}
}
}
}

View File

@ -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);
}
}

View 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();
}
}
}

View File

@ -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>