diff --git a/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj b/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj
index 21d8ba6..039f964 100644
--- a/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj
+++ b/BililiveRecorder.Cli/BililiveRecorder.Cli.csproj
@@ -28,8 +28,9 @@
-
+
+
diff --git a/BililiveRecorder.Cli/Program.cs b/BililiveRecorder.Cli/Program.cs
index 689a531..dcec629 100644
--- a/BililiveRecorder.Cli/Program.cs
+++ b/BililiveRecorder.Cli/Program.cs
@@ -2,11 +2,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
-using Autofac;
using BililiveRecorder.Core;
using BililiveRecorder.Core.Config.V2;
-using BililiveRecorder.FlvProcessor;
+using BililiveRecorder.DependencyInjection;
using CommandLine;
+using Microsoft.Extensions.DependencyInjection;
namespace BililiveRecorder.Cli
{
@@ -19,10 +19,10 @@ namespace BililiveRecorder.Cli
private static int RunConfigMode(CmdVerbConfigMode opts)
{
- var container = CreateBuilder().Build();
- var rootScope = container.BeginLifetimeScope("recorder_root");
var semaphore = new SemaphoreSlim(0, 1);
- var recorder = rootScope.Resolve();
+
+ var serviceProvider = BuildServiceProvider();
+ var recorder = serviceProvider.GetRequiredService();
ConsoleCancelEventHandler p = null!;
p = (sender, e) =>
@@ -46,10 +46,11 @@ namespace BililiveRecorder.Cli
private static int RunPortableMode(CmdVerbPortableMode opts)
{
- var container = CreateBuilder().Build();
- var rootScope = container.BeginLifetimeScope("recorder_root");
var semaphore = new SemaphoreSlim(0, 1);
- var recorder = rootScope.Resolve();
+
+ var serviceProvider = BuildServiceProvider();
+ var recorder = serviceProvider.GetRequiredService();
+
var config = new ConfigV2()
{
DisableConfigSave = true,
@@ -85,12 +86,12 @@ namespace BililiveRecorder.Cli
return 0;
}
- private static ContainerBuilder CreateBuilder()
+ private static IServiceProvider BuildServiceProvider()
{
- var builder = new ContainerBuilder();
- builder.RegisterModule();
- builder.RegisterModule();
- return builder;
+ var services = new ServiceCollection();
+ services.AddFlvProcessor();
+ services.AddCore();
+ return services.BuildServiceProvider();
}
}
diff --git a/BililiveRecorder.Core/BililiveRecorder.Core.csproj b/BililiveRecorder.Core/BililiveRecorder.Core.csproj
index 7161adb..9a65e69 100644
--- a/BililiveRecorder.Core/BililiveRecorder.Core.csproj
+++ b/BililiveRecorder.Core/BililiveRecorder.Core.csproj
@@ -19,10 +19,10 @@
-
-
+
+
diff --git a/BililiveRecorder.Core/CoreModule.cs b/BililiveRecorder.Core/CoreModule.cs
deleted file mode 100644
index 7f10082..0000000
--- a/BililiveRecorder.Core/CoreModule.cs
+++ /dev/null
@@ -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().Config).As();
- builder.Register(x => x.Resolve().Global).As();
- builder.RegisterType().AsSelf().InstancePerMatchingLifetimeScope("recorder_root");
- builder.RegisterType().AsSelf().ExternallyOwned();
- builder.RegisterType().As().ExternallyOwned();
- builder.RegisterType().As().ExternallyOwned();
- builder.RegisterType().As().ExternallyOwned();
- builder.RegisterType().As().InstancePerMatchingLifetimeScope("recorder_root");
- }
- }
-}
diff --git a/BililiveRecorder.Core/DependencyInjectionExtensions.cs b/BililiveRecorder.Core/DependencyInjectionExtensions.cs
new file mode 100644
index 0000000..e65cb1f
--- /dev/null
+++ b/BililiveRecorder.Core/DependencyInjectionExtensions.cs
@@ -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();
+#pragma warning disable IDE0001
+ services.AddSingleton(x => x.GetRequiredService().Config);
+ services.AddSingleton(x => x.GetRequiredService().Global);
+#pragma warning restore IDE0001
+ services.AddSingleton();
+ services.AddSingleton();
+ }
+ }
+}
diff --git a/BililiveRecorder.Core/IRecordedRoomFactory.cs b/BililiveRecorder.Core/IRecordedRoomFactory.cs
new file mode 100644
index 0000000..ebb677d
--- /dev/null
+++ b/BililiveRecorder.Core/IRecordedRoomFactory.cs
@@ -0,0 +1,9 @@
+using BililiveRecorder.Core.Config.V2;
+
+namespace BililiveRecorder.Core
+{
+ public interface IRecordedRoomFactory
+ {
+ IRecordedRoom CreateRecordedRoom(RoomConfig roomConfig);
+ }
+}
diff --git a/BililiveRecorder.Core/RecordedRoom.cs b/BililiveRecorder.Core/RecordedRoom.cs
index 600f077..8f2f603 100644
--- a/BililiveRecorder.Core/RecordedRoom.cs
+++ b/BililiveRecorder.Core/RecordedRoom.cs
@@ -113,7 +113,7 @@ namespace BililiveRecorder.Core
public event EventHandler RecordEnded;
private readonly IBasicDanmakuWriter basicDanmakuWriter;
- private readonly Func newIFlvStreamProcessor;
+ private readonly IProcessorFactory processorFactory;
private IFlvStreamProcessor _processor;
public IFlvStreamProcessor Processor
{
@@ -156,9 +156,9 @@ namespace BililiveRecorder.Core
public Guid Guid { get; } = Guid.NewGuid();
// TODO: 重构 DI
- public RecordedRoom(Func newBasicDanmakuWriter,
- Func newIStreamMonitor,
- Func newIFlvStreamProcessor,
+ public RecordedRoom(IBasicDanmakuWriter basicDanmakuWriter,
+ IStreamMonitor streamMonitor,
+ IProcessorFactory processorFactory,
BililiveAPI bililiveAPI,
RoomConfig roomConfig)
{
@@ -167,11 +167,11 @@ namespace BililiveRecorder.Core
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.StreamStarted += this.StreamMonitor_StreamStarted;
this.StreamMonitor.ReceivedDanmaku += this.StreamMonitor_ReceivedDanmaku;
@@ -377,7 +377,7 @@ namespace BililiveRecorder.Core
}
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.ClipLengthPast = this.RoomConfig.ClipLengthPast;
this.Processor.CuttingNumber = this.RoomConfig.CuttingNumber;
diff --git a/BililiveRecorder.Core/RecordedRoomFactory.cs b/BililiveRecorder.Core/RecordedRoomFactory.cs
new file mode 100644
index 0000000..547f72a
--- /dev/null
+++ b/BililiveRecorder.Core/RecordedRoomFactory.cs
@@ -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);
+ }
+ }
+}
diff --git a/BililiveRecorder.Core/Recorder.cs b/BililiveRecorder.Core/Recorder.cs
index 6d33f2e..8bc1a5a 100644
--- a/BililiveRecorder.Core/Recorder.cs
+++ b/BililiveRecorder.Core/Recorder.cs
@@ -9,6 +9,7 @@ using System.Threading;
using BililiveRecorder.Core.Callback;
using BililiveRecorder.Core.Config;
using BililiveRecorder.Core.Config.V2;
+using Microsoft.Extensions.DependencyInjection;
using NLog;
#nullable enable
@@ -18,9 +19,9 @@ namespace BililiveRecorder.Core
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
- private readonly Func newIRecordedRoom;
private readonly CancellationTokenSource tokenSource;
-
+ private readonly IServiceProvider serviceProvider;
+ private IRecordedRoomFactory? recordedRoomFactory;
private bool _valid = false;
private bool disposedValue;
@@ -34,10 +35,9 @@ namespace BililiveRecorder.Core
public bool IsReadOnly => true;
public IRecordedRoom this[int index] => this.Rooms[index];
- public Recorder(Func iRecordedRoom)
+ public Recorder(IServiceProvider serviceProvider)
{
- this.newIRecordedRoom = iRecordedRoom ?? throw new ArgumentNullException(nameof(iRecordedRoom));
-
+ this.serviceProvider = serviceProvider;
this.tokenSource = new CancellationTokenSource();
Repeat.Interval(TimeSpan.FromSeconds(3), this.DownloadWatchdog, this.tokenSource.Token);
@@ -60,6 +60,7 @@ namespace BililiveRecorder.Core
this.Config = config;
this.Config.Global.WorkDirectory = workdir;
this.Webhook = new BasicWebhook(this.Config);
+ this.recordedRoomFactory = this.serviceProvider.GetRequiredService();
this._valid = true;
this.Config.Rooms.ForEach(r => this.AddRoom(r));
ConfigParser.SaveTo(this.Config.Global.WorkDirectory, this.Config);
@@ -83,6 +84,7 @@ namespace BililiveRecorder.Core
logger.Debug("Initialize With Config.");
this.Config = config;
this.Webhook = new BasicWebhook(this.Config);
+ this.recordedRoomFactory = this.serviceProvider.GetRequiredService();
this._valid = true;
this.Config.Rooms.ForEach(r => this.AddRoom(r));
return true;
@@ -136,7 +138,7 @@ namespace BililiveRecorder.Core
if (!this._valid) { throw new InvalidOperationException("Not Initialized"); }
roomConfig.SetParent(this.Config?.Global);
- var rr = this.newIRecordedRoom(roomConfig);
+ var rr = this.recordedRoomFactory!.CreateRecordedRoom(roomConfig);
logger.Debug("AddRoom 添加了 {roomid} 直播间 ", rr.RoomId);
rr.RecordEnded += this.RecordedRoom_RecordEnded;
diff --git a/BililiveRecorder.Core/StreamMonitor.cs b/BililiveRecorder.Core/StreamMonitor.cs
index ba0d5c2..4950128 100644
--- a/BililiveRecorder.Core/StreamMonitor.cs
+++ b/BililiveRecorder.Core/StreamMonitor.cs
@@ -30,7 +30,6 @@ namespace BililiveRecorder.Core
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
- private readonly Func funcTcpClient;
private readonly RoomConfig roomConfig;
private readonly BililiveAPI bililiveAPI;
@@ -51,9 +50,8 @@ namespace BililiveRecorder.Core
public event ReceivedDanmakuEvt ReceivedDanmaku;
public event PropertyChangedEventHandler PropertyChanged;
- public StreamMonitor(RoomConfig roomConfig, Func funcTcpClient, BililiveAPI bililiveAPI)
+ public StreamMonitor(RoomConfig roomConfig, BililiveAPI bililiveAPI)
{
- this.funcTcpClient = funcTcpClient;
this.roomConfig = roomConfig;
this.bililiveAPI = bililiveAPI;
@@ -216,7 +214,7 @@ namespace BililiveRecorder.Core
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);
this.dmNetStream = this.dmClient.GetStream();
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.IsDanmakuConnected)));
diff --git a/BililiveRecorder.Flv/Amf/AmfCollectionDebugView.cs b/BililiveRecorder.Flv/Amf/AmfCollectionDebugView.cs
new file mode 100644
index 0000000..450b338
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/AmfCollectionDebugView.cs
@@ -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 _collection;
+
+ public AmfCollectionDebugView(ICollection collection)
+ {
+ this._collection = collection ?? throw new ArgumentNullException(nameof(collection));
+ }
+
+ [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+ public IScriptDataValue[] Items => this._collection.ToArray();
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/AmfDictionaryDebugView.cs b/BililiveRecorder.Flv/Amf/AmfDictionaryDebugView.cs
new file mode 100644
index 0000000..4157078
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/AmfDictionaryDebugView.cs
@@ -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 _dict;
+
+ public AmfDictionaryDebugView(IDictionary dictionary)
+ {
+ this._dict = dictionary ?? throw new ArgumentNullException(nameof(dictionary));
+ }
+
+ [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+ public KeyValuePairDebugView[] Items
+ => this._dict.Select(x => new KeyValuePairDebugView(x.Key, x.Value)).ToArray();
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/AmfException.cs b/BililiveRecorder.Flv/Amf/AmfException.cs
new file mode 100644
index 0000000..acab0b7
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/AmfException.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace BililiveRecorder.Flv.Amf
+{
+ public class AmfException : Exception
+ {
+ ///
+ public AmfException() { }
+ ///
+ public AmfException(string message) : base(message) { }
+ ///
+ public AmfException(string message, Exception innerException) : base(message, innerException) { }
+ ///
+ protected AmfException(SerializationInfo info, StreamingContext context) : base(info, context) { }
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/IScriptDataValue.cs b/BililiveRecorder.Flv/Amf/IScriptDataValue.cs
new file mode 100644
index 0000000..d857355
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/IScriptDataValue.cs
@@ -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);
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/KeyValuePairDebugView.cs b/BililiveRecorder.Flv/Amf/KeyValuePairDebugView.cs
new file mode 100644
index 0000000..386a231
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/KeyValuePairDebugView.cs
@@ -0,0 +1,17 @@
+using System.Diagnostics;
+
+namespace BililiveRecorder.Flv.Amf
+{
+ [DebuggerDisplay("{Key}: {Value}")]
+ internal sealed class KeyValuePairDebugView
+ {
+ public KeyValuePairDebugView(K key, V value)
+ {
+ this.Key = key;
+ this.Value = value;
+ }
+
+ public K Key { get; }
+ public V Value { get; }
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/ScriptDataBoolean.cs b/BililiveRecorder.Flv/Amf/ScriptDataBoolean.cs
new file mode 100644
index 0000000..0e5b810
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/ScriptDataBoolean.cs
@@ -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.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 };
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/ScriptDataDate.cs b/BililiveRecorder.Flv/Amf/ScriptDataDate.cs
new file mode 100644
index 0000000..bda7efc
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/ScriptDataDate.cs
@@ -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.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);
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/ScriptDataEcmaArray.cs b/BililiveRecorder.Flv/Amf/ScriptDataEcmaArray.cs
new file mode 100644
index 0000000..13bc14b
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/ScriptDataEcmaArray.cs
@@ -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, ICollection>, IEnumerable>, IReadOnlyCollection>, IReadOnlyDictionary
+ {
+ public ScriptDataType Type => ScriptDataType.EcmaArray;
+
+ [JsonProperty]
+ public Dictionary Value { get; set; } = new Dictionary();
+
+ 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)this.Value)[key]; set => ((IDictionary)this.Value)[key] = value; }
+ public ICollection Keys => ((IDictionary)this.Value).Keys;
+ public ICollection Values => ((IDictionary)this.Value).Values;
+ IEnumerable IReadOnlyDictionary.Keys => ((IReadOnlyDictionary)this.Value).Keys;
+ IEnumerable IReadOnlyDictionary.Values => ((IReadOnlyDictionary)this.Value).Values;
+ public int Count => ((IDictionary)this.Value).Count;
+ public bool IsReadOnly => ((IDictionary)this.Value).IsReadOnly;
+ public void Add(string key, IScriptDataValue value) => ((IDictionary)this.Value).Add(key, value);
+ public void Add(KeyValuePair item) => ((IDictionary)this.Value).Add(item);
+ public void Clear() => ((IDictionary)this.Value).Clear();
+ public bool Contains(KeyValuePair item) => ((IDictionary)this.Value).Contains(item);
+ public bool ContainsKey(string key) => ((IDictionary)this.Value).ContainsKey(key);
+ public void CopyTo(KeyValuePair[] array, int arrayIndex) => ((IDictionary)this.Value).CopyTo(array, arrayIndex);
+ public IEnumerator> GetEnumerator() => ((IDictionary)this.Value).GetEnumerator();
+ public bool Remove(string key) => ((IDictionary)this.Value).Remove(key);
+ public bool Remove(KeyValuePair item) => ((IDictionary)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)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)this.Value).GetEnumerator();
+ public static implicit operator Dictionary(ScriptDataEcmaArray ecmaArray) => ecmaArray.Value;
+ public static implicit operator ScriptDataEcmaArray(Dictionary ecmaArray) => new ScriptDataEcmaArray { Value = ecmaArray };
+ public static implicit operator ScriptDataEcmaArray(ScriptDataObject @object) => new ScriptDataEcmaArray { Value = @object };
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/ScriptDataLongString.cs b/BililiveRecorder.Flv/Amf/ScriptDataLongString.cs
new file mode 100644
index 0000000..a40e529
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/ScriptDataLongString.cs
@@ -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.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 };
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/ScriptDataNull.cs b/BililiveRecorder.Flv/Amf/ScriptDataNull.cs
new file mode 100644
index 0000000..58f27aa
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/ScriptDataNull.cs
@@ -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.Default.Equals(left, right);
+ public static bool operator !=(ScriptDataNull left, ScriptDataNull right) => !(left == right);
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/ScriptDataNumber.cs b/BililiveRecorder.Flv/Amf/ScriptDataNumber.cs
new file mode 100644
index 0000000..73f0d83
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/ScriptDataNumber.cs
@@ -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.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 };
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/ScriptDataObject.cs b/BililiveRecorder.Flv/Amf/ScriptDataObject.cs
new file mode 100644
index 0000000..4edd671
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/ScriptDataObject.cs
@@ -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, ICollection>, IEnumerable>, IReadOnlyCollection>, IReadOnlyDictionary
+ {
+ public ScriptDataType Type => ScriptDataType.Object;
+
+ [JsonProperty]
+ public Dictionary Value { get; set; } = new Dictionary();
+
+ 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)this.Value)[key]; set => ((IDictionary)this.Value)[key] = value; }
+ public ICollection Keys => ((IDictionary)this.Value).Keys;
+ public ICollection Values => ((IDictionary)this.Value).Values;
+ IEnumerable IReadOnlyDictionary.Keys => ((IReadOnlyDictionary)this.Value).Keys;
+ IEnumerable IReadOnlyDictionary.Values => ((IReadOnlyDictionary)this.Value).Values;
+ public int Count => ((IDictionary)this.Value).Count;
+ public bool IsReadOnly => ((IDictionary)this.Value).IsReadOnly;
+ public void Add(string key, IScriptDataValue value) => ((IDictionary)this.Value).Add(key, value);
+ public void Add(KeyValuePair item) => ((IDictionary)this.Value).Add(item);
+ public void Clear() => ((IDictionary)this.Value).Clear();
+ public bool Contains(KeyValuePair item) => ((IDictionary)this.Value).Contains(item);
+ public bool ContainsKey(string key) => ((IDictionary)this.Value).ContainsKey(key);
+ public void CopyTo(KeyValuePair[] array, int arrayIndex) => ((IDictionary)this.Value).CopyTo(array, arrayIndex);
+ public IEnumerator> GetEnumerator() => ((IDictionary)this.Value).GetEnumerator();
+ public bool Remove(string key) => ((IDictionary)this.Value).Remove(key);
+ public bool Remove(KeyValuePair item) => ((IDictionary)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)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)this.Value).GetEnumerator();
+
+ public static implicit operator Dictionary(ScriptDataObject @object) => @object.Value;
+ public static implicit operator ScriptDataObject(Dictionary @object) => new ScriptDataObject { Value = @object };
+ public static implicit operator ScriptDataObject(ScriptDataEcmaArray ecmaArray) => new ScriptDataObject { Value = ecmaArray };
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/ScriptDataReference.cs b/BililiveRecorder.Flv/Amf/ScriptDataReference.cs
new file mode 100644
index 0000000..c64cc6a
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/ScriptDataReference.cs
@@ -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.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 };
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/ScriptDataStrictArray.cs b/BililiveRecorder.Flv/Amf/ScriptDataStrictArray.cs
new file mode 100644
index 0000000..eeea380
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/ScriptDataStrictArray.cs
@@ -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, ICollection, IEnumerable, IReadOnlyCollection, IReadOnlyList
+ {
+ public ScriptDataType Type => ScriptDataType.StrictArray;
+
+ [JsonProperty]
+ public List Value { get; set; } = new List();
+
+ 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)this.Value)[index]; set => ((IList)this.Value)[index] = value; }
+ public int Count => ((IList)this.Value).Count;
+ public bool IsReadOnly => ((IList)this.Value).IsReadOnly;
+ public void Add(IScriptDataValue item) => ((IList)this.Value).Add(item);
+ public void Clear() => ((IList)this.Value).Clear();
+ public bool Contains(IScriptDataValue item) => ((IList)this.Value).Contains(item);
+ public void CopyTo(IScriptDataValue[] array, int arrayIndex) => ((IList)this.Value).CopyTo(array, arrayIndex);
+ public IEnumerator GetEnumerator() => ((IList)this.Value).GetEnumerator();
+ public int IndexOf(IScriptDataValue item) => ((IList)this.Value).IndexOf(item);
+ public void Insert(int index, IScriptDataValue item) => ((IList)this.Value).Insert(index, item);
+ public bool Remove(IScriptDataValue item) => ((IList)this.Value).Remove(item);
+ public void RemoveAt(int index) => ((IList)this.Value).RemoveAt(index);
+ IEnumerator IEnumerable.GetEnumerator() => ((IList)this.Value).GetEnumerator();
+ public static implicit operator List(ScriptDataStrictArray strictArray) => strictArray.Value;
+ public static implicit operator ScriptDataStrictArray(List values) => new ScriptDataStrictArray { Value = values };
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/ScriptDataString.cs b/BililiveRecorder.Flv/Amf/ScriptDataString.cs
new file mode 100644
index 0000000..e40bd3f
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/ScriptDataString.cs
@@ -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.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 };
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/ScriptDataType.cs b/BililiveRecorder.Flv/Amf/ScriptDataType.cs
new file mode 100644
index 0000000..1d235cb
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/ScriptDataType.cs
@@ -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,
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/ScriptDataUndefined.cs b/BililiveRecorder.Flv/Amf/ScriptDataUndefined.cs
new file mode 100644
index 0000000..41fee61
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/ScriptDataUndefined.cs
@@ -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.Default.Equals(left, right);
+ public static bool operator !=(ScriptDataUndefined left, ScriptDataUndefined right) => !(left == right);
+ }
+}
diff --git a/BililiveRecorder.Flv/Amf/ScriptTagBody.cs b/BililiveRecorder.Flv/Amf/ScriptTagBody.cs
new file mode 100644
index 0000000..ec23d95
--- /dev/null
+++ b/BililiveRecorder.Flv/Amf/ScriptTagBody.cs
@@ -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(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());
+ }
+}
diff --git a/BililiveRecorder.Flv/BililiveRecorder.Flv.csproj b/BililiveRecorder.Flv/BililiveRecorder.Flv.csproj
new file mode 100644
index 0000000..1d9d0e4
--- /dev/null
+++ b/BililiveRecorder.Flv/BililiveRecorder.Flv.csproj
@@ -0,0 +1,23 @@
+
+
+
+ netstandard2.0
+ 8.0
+ enable
+ true
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
diff --git a/BililiveRecorder.Flv/DefaultMemoryStreamProvider.cs b/BililiveRecorder.Flv/DefaultMemoryStreamProvider.cs
new file mode 100644
index 0000000..ef185ae
--- /dev/null
+++ b/BililiveRecorder.Flv/DefaultMemoryStreamProvider.cs
@@ -0,0 +1,9 @@
+using System.IO;
+
+namespace BililiveRecorder.Flv
+{
+ public class DefaultMemoryStreamProvider : IMemoryStreamProvider
+ {
+ public Stream CreateMemoryStream(string tag) => new MemoryStream();
+ }
+}
diff --git a/BililiveRecorder.Flv/DependencyInjectionExtensions.cs b/BililiveRecorder.Flv/DependencyInjectionExtensions.cs
new file mode 100644
index 0000000..bf304e5
--- /dev/null
+++ b/BililiveRecorder.Flv/DependencyInjectionExtensions.cs
@@ -0,0 +1,13 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace BililiveRecorder.DependencyInjection
+{
+ public static class DependencyInjectionExtensions
+ {
+ public static IServiceCollection AddFlv(this IServiceCollection services)
+ {
+
+ return services;
+ }
+ }
+}
diff --git a/BililiveRecorder.Flv/Grouping/Rules/DataGroupingRule.cs b/BililiveRecorder.Flv/Grouping/Rules/DataGroupingRule.cs
new file mode 100644
index 0000000..71957a0
--- /dev/null
+++ b/BililiveRecorder.Flv/Grouping/Rules/DataGroupingRule.cs
@@ -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 tags) => new PipelineDataAction(tags);
+ }
+}
diff --git a/BililiveRecorder.Flv/Grouping/Rules/HeaderGroupingRule.cs b/BililiveRecorder.Flv/Grouping/Rules/HeaderGroupingRule.cs
new file mode 100644
index 0000000..7bbc2cd
--- /dev/null
+++ b/BililiveRecorder.Flv/Grouping/Rules/HeaderGroupingRule.cs
@@ -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 tags) => new PipelineHeaderAction(tags);
+ }
+}
diff --git a/BililiveRecorder.Flv/Grouping/Rules/ScriptGroupingRule.cs b/BililiveRecorder.Flv/Grouping/Rules/ScriptGroupingRule.cs
new file mode 100644
index 0000000..2f043fb
--- /dev/null
+++ b/BililiveRecorder.Flv/Grouping/Rules/ScriptGroupingRule.cs
@@ -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 tags) => new PipelineScriptAction(tags.First());
+ }
+}
diff --git a/BililiveRecorder.Flv/Grouping/TagGroupReader.cs b/BililiveRecorder.Flv/Grouping/TagGroupReader.cs
new file mode 100644
index 0000000..90fb56d
--- /dev/null
+++ b/BililiveRecorder.Flv/Grouping/TagGroupReader.cs
@@ -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 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
+ {
+ new ScriptGroupingRule(),
+ new HeaderGroupingRule(),
+ new DataGroupingRule()
+ };
+ }
+
+ public async Task ReadGroupAsync()
+ {
+ if (!this.semaphoreSlim.Wait(0))
+ {
+ throw new InvalidOperationException("Concurrent read is not supported.");
+ }
+ try
+ {
+ var tags = new List();
+
+ 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
+ }
+}
diff --git a/BililiveRecorder.Flv/H264Nalu.cs b/BililiveRecorder.Flv/H264Nalu.cs
new file mode 100644
index 0000000..2fdf4ae
--- /dev/null
+++ b/BililiveRecorder.Flv/H264Nalu.cs
@@ -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
+{
+ ///
+ /// H.264 NAL unit
+ ///
+ 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? h264Nalus)
+ {
+ h264Nalus = null;
+ var result = new List();
+ 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;
+ }
+
+ ///
+ /// 一个 nal_unit 的开始位置
+ ///
+ [XmlAttribute]
+ public int StartPosition { get; set; }
+
+ ///
+ /// 一个 nal_unit 的完整长度
+ ///
+ [XmlAttribute]
+ public uint FullSize { get; set; }
+
+ ///
+ /// nal_unit_type
+ ///
+ [XmlAttribute]
+ public H264NaluType Type { get; set; }
+ }
+
+ ///
+ /// nal_unit_type
+ ///
+ 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,
+ }
+}
diff --git a/BililiveRecorder.Flv/IFlvProcessingContextWriter.cs b/BililiveRecorder.Flv/IFlvProcessingContextWriter.cs
new file mode 100644
index 0000000..7a1e35e
--- /dev/null
+++ b/BililiveRecorder.Flv/IFlvProcessingContextWriter.cs
@@ -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);
+ }
+}
diff --git a/BililiveRecorder.Flv/IFlvTagReader.cs b/BililiveRecorder.Flv/IFlvTagReader.cs
new file mode 100644
index 0000000..a677ed7
--- /dev/null
+++ b/BililiveRecorder.Flv/IFlvTagReader.cs
@@ -0,0 +1,23 @@
+using System;
+using System.Threading.Tasks;
+
+namespace BililiveRecorder.Flv
+{
+ ///
+ /// 实现 Flv Tag 的读取
+ ///
+ public interface IFlvTagReader : IDisposable
+ {
+ ///
+ /// Returns the next available Flv Tag but does not consume it.
+ ///
+ ///
+ Task PeekTagAsync();
+
+ ///
+ /// Reads the next Flv Tag.
+ ///
+ ///
+ Task ReadTagAsync();
+ }
+}
diff --git a/BililiveRecorder.Flv/IFlvWriterTargetProvider.cs b/BililiveRecorder.Flv/IFlvWriterTargetProvider.cs
new file mode 100644
index 0000000..85abec5
--- /dev/null
+++ b/BililiveRecorder.Flv/IFlvWriterTargetProvider.cs
@@ -0,0 +1,11 @@
+using System.IO;
+
+namespace BililiveRecorder.Flv
+{
+ public interface IFlvWriterTargetProvider
+ {
+ Stream CreateOutputStream();
+
+ Stream CreateAlternativeHeaderStream();
+ }
+}
diff --git a/BililiveRecorder.Flv/IGroupingRule.cs b/BililiveRecorder.Flv/IGroupingRule.cs
new file mode 100644
index 0000000..4c452df
--- /dev/null
+++ b/BililiveRecorder.Flv/IGroupingRule.cs
@@ -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 tags);
+ }
+}
diff --git a/BililiveRecorder.Flv/IMemoryStreamProvider.cs b/BililiveRecorder.Flv/IMemoryStreamProvider.cs
new file mode 100644
index 0000000..0c6329b
--- /dev/null
+++ b/BililiveRecorder.Flv/IMemoryStreamProvider.cs
@@ -0,0 +1,9 @@
+using System.IO;
+
+namespace BililiveRecorder.Flv
+{
+ public interface IMemoryStreamProvider
+ {
+ Stream CreateMemoryStream(string tag);
+ }
+}
diff --git a/BililiveRecorder.Flv/ITagGroupReader.cs b/BililiveRecorder.Flv/ITagGroupReader.cs
new file mode 100644
index 0000000..f3430bb
--- /dev/null
+++ b/BililiveRecorder.Flv/ITagGroupReader.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Threading.Tasks;
+using BililiveRecorder.Flv.Pipeline;
+
+namespace BililiveRecorder.Flv
+{
+ public interface ITagGroupReader : IDisposable
+ {
+ Task ReadGroupAsync();
+ }
+}
diff --git a/BililiveRecorder.Flv/Parser/BigEndianBinaryReader.cs b/BililiveRecorder.Flv/Parser/BigEndianBinaryReader.cs
new file mode 100644
index 0000000..8b97168
--- /dev/null
+++ b/BililiveRecorder.Flv/Parser/BigEndianBinaryReader.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Buffers.Binary;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace BililiveRecorder.Flv.Parser
+{
+ ///
+ public class BigEndianBinaryReader : BinaryReader
+ {
+ ///
+ public BigEndianBinaryReader(Stream input) : base(input)
+ {
+ }
+
+ ///
+ public BigEndianBinaryReader(Stream input, Encoding encoding) : base(input, encoding)
+ {
+ }
+
+ ///
+ public BigEndianBinaryReader(Stream input, Encoding encoding, bool leaveOpen) : base(input, encoding, leaveOpen)
+ {
+ }
+
+ ///
+ public override Stream BaseStream => base.BaseStream;
+
+ ///
+ public override void Close() => base.Close();
+
+ ///
+ public override bool Equals(object? obj) => base.Equals(obj);
+
+ ///
+ public override int GetHashCode() => base.GetHashCode();
+
+ ///
+ public override int PeekChar() => base.PeekChar();
+
+ ///
+ public override int Read() => base.Read();
+
+ ///
+ public override int Read(byte[] buffer, int index, int count) => base.Read(buffer, index, count);
+
+ ///
+ public override int Read(char[] buffer, int index, int count) => base.Read(buffer, index, count);
+
+ ///
+ public override bool ReadBoolean() => base.ReadBoolean();
+
+ ///
+ public override byte ReadByte() => base.ReadByte();
+
+ ///
+ public override byte[] ReadBytes(int count) => base.ReadBytes(count);
+
+ ///
+ public override char ReadChar() => base.ReadChar();
+
+ ///
+ public override char[] ReadChars(int count) => base.ReadChars(count);
+
+ ///
+ public override decimal ReadDecimal() => BitConverter.IsLittleEndian ? throw new NotSupportedException("not supported") : base.ReadDecimal();
+
+ ///
+ public override double ReadDouble() => BitConverter.IsLittleEndian
+ ? BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64BigEndian(base.ReadBytes(sizeof(double))))
+ : base.ReadDouble();
+
+ ///
+ public override short ReadInt16() => BitConverter.IsLittleEndian ? BinaryPrimitives.ReadInt16BigEndian(base.ReadBytes(sizeof(short))) : base.ReadInt16();
+
+ ///
+ public override int ReadInt32() => BitConverter.IsLittleEndian ? BinaryPrimitives.ReadInt32BigEndian(base.ReadBytes(sizeof(int))) : base.ReadInt32();
+
+ ///
+ public override long ReadInt64() => BitConverter.IsLittleEndian ? BinaryPrimitives.ReadInt64BigEndian(base.ReadBytes(sizeof(long))) : base.ReadInt64();
+
+ ///
+ public override sbyte ReadSByte() => base.ReadSByte();
+
+ ///
+ 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;
+
+ ///
+ public override string ReadString() => base.ReadString();
+
+ ///
+ public override ushort ReadUInt16() => BitConverter.IsLittleEndian ? BinaryPrimitives.ReadUInt16BigEndian(base.ReadBytes(sizeof(ushort))) : base.ReadUInt16();
+
+ ///
+ public override uint ReadUInt32() => BitConverter.IsLittleEndian ? BinaryPrimitives.ReadUInt32BigEndian(base.ReadBytes(sizeof(uint))) : base.ReadUInt32();
+
+ ///
+ public override ulong ReadUInt64() => BitConverter.IsLittleEndian ? BinaryPrimitives.ReadUInt64BigEndian(base.ReadBytes(sizeof(ulong))) : base.ReadUInt64();
+
+ ///
+ public override string? ToString() => base.ToString();
+
+ ///
+ protected override void Dispose(bool disposing) => base.Dispose(disposing);
+
+ ///
+ protected override void FillBuffer(int numBytes) => base.FillBuffer(numBytes);
+ }
+}
diff --git a/BililiveRecorder.Flv/Parser/FlvException.cs b/BililiveRecorder.Flv/Parser/FlvException.cs
new file mode 100644
index 0000000..741b006
--- /dev/null
+++ b/BililiveRecorder.Flv/Parser/FlvException.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace BililiveRecorder.Flv.Parser
+{
+ public class FlvException : Exception
+ {
+ ///
+ public FlvException() { }
+ ///
+ public FlvException(string message) : base(message) { }
+ ///
+ public FlvException(string message, Exception innerException) : base(message, innerException) { }
+ ///
+ protected FlvException(SerializationInfo info, StreamingContext context) : base(info, context) { }
+ }
+}
diff --git a/BililiveRecorder.Flv/Parser/FlvTagPipeReader.cs b/BililiveRecorder.Flv/Parser/FlvTagPipeReader.cs
new file mode 100644
index 0000000..22a10a9
--- /dev/null
+++ b/BililiveRecorder.Flv/Parser/FlvTagPipeReader.cs
@@ -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
+{
+ ///
+ /// 从 读取
+ ///
+ 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();
+ }
+
+ ///
+ /// 实现二进制数据的解析
+ ///
+ /// 解析出的 Flv Tag
+ private async Task 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 buffer)
+ {
+ if (buffer.Length < 9)
+ return false;
+
+ var fileHeaderSlice = buffer.Slice(buffer.Start, 9);
+
+ Span stackSpan = stackalloc byte[9];
+ ReadOnlySpan 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 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 stackTemp = stackalloc byte[4];
+ Span stackHeaderSpan = stackalloc byte[11];
+ ReadOnlySpan 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.Shared.Rent(segment.Length);
+ try
+ {
+ segment.CopyTo(sharedBuffer);
+ tagBodyStream.Write(sharedBuffer, 0, segment.Length);
+ }
+ finally
+ {
+ ArrayPool.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;
+ }
+
+ ///
+ public async Task 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();
+ }
+ }
+
+ ///
+ public async Task 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();
+ }
+ }
+ }
+}
diff --git a/BililiveRecorder.Flv/Pipeline/FlvProcessingContext.cs b/BililiveRecorder.Flv/Pipeline/FlvProcessingContext.cs
new file mode 100644
index 0000000..2d91ba5
--- /dev/null
+++ b/BililiveRecorder.Flv/Pipeline/FlvProcessingContext.cs
@@ -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