Remove FlvProcessor

This commit is contained in:
Genteure 2021-02-26 15:59:12 +08:00
parent 58970c217b
commit 534adfba1b
21 changed files with 4 additions and 1343 deletions

View File

@ -1,18 +0,0 @@
namespace BililiveRecorder.FlvProcessor
{
public enum AutoCuttingMode : int
{
/// <summary>
/// 禁用
/// </summary>
Disabled,
/// <summary>
/// 根据时间切割
/// </summary>
ByTime,
/// <summary>
/// 根据文件大小切割
/// </summary>
BySize,
}
}

View File

@ -1,32 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>9.0</LangVersion>
<Version>0.0.0.0</Version>
<Authors>Genteure</Authors>
<Company>Genteure</Company>
<Copyright>Copyright © 2018 - 2021 Genteure</Copyright>
<AssemblyVersion>0.0.0.0</AssemblyVersion>
<FileVersion>0.0.0.0</FileVersion>
<FileUpgradeFlags>
</FileUpgradeFlags>
<UpgradeBackupLocation>
</UpgradeBackupLocation>
<OldToolsVersion>2.0</OldToolsVersion>
</PropertyGroup>
<PropertyGroup>
<DebugType>portable</DebugType>
<DebugSymbols>true</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\TempBuildInfo\BuildInfo.FlvProcessor.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
<PackageReference Include="NLog" Version="4.7.6" />
</ItemGroup>
<PropertyGroup>
<PreBuildEvent>cd $(SolutionDir)
powershell -ExecutionPolicy Bypass -File .\CI\patch_buildinfo.ps1 FlvProcessor</PreBuildEvent>
</PropertyGroup>
</Project>

View File

@ -1,15 +0,0 @@
using System;
using BililiveRecorder.FlvProcessor;
using Microsoft.Extensions.DependencyInjection;
namespace BililiveRecorder.DependencyInjection
{
public static class DependencyInjectionExtensions
{
public static IServiceCollection AddFlvProcessor(this IServiceCollection services) => services
.AddSingleton<Func<IFlvTag>>(() => new FlvTag())
.AddSingleton<IFlvMetadataFactory, FlvMetadataFactory>()
.AddSingleton<IProcessorFactory, ProcessorFactory>()
;
}
}

View File

@ -1,32 +0,0 @@
namespace BililiveRecorder.FlvProcessor
{
public enum EnabledFeature : int
{
/// <summary>
/// 同时启用两个功能
/// </summary>
Both,
/// <summary>
/// 只使用即时回放剪辑功能
/// </summary>
ClipOnly,
/// <summary>
/// 只使用录制功能
/// </summary>
RecordOnly,
}
public static class EnabledFeatureExtenstion
{
public static bool IsClipEnabled(this EnabledFeature enabledFeature)
{
return enabledFeature == EnabledFeature.Both || enabledFeature == EnabledFeature.ClipOnly;
}
public static bool IsRecordEnabled(this EnabledFeature enabledFeature)
{
return enabledFeature == EnabledFeature.Both || enabledFeature == EnabledFeature.RecordOnly;
}
}
}

View File

@ -1,69 +0,0 @@
namespace BililiveRecorder.FlvProcessor
{
public enum TagType : int
{
AUDIO = 8,
VIDEO = 9,
DATA = 18,
}
public enum AMFTypes : byte
{
/// <summary>
/// 非标准类型。在 Decode 过程中作为函数参数使用
/// </summary>
Any = 0xFF,
/// <summary>
/// Double
/// </summary>
Number = 0,
/// <summary>
/// 8 bit unsigned integer
/// </summary>
Boolean = 1,
/// <summary>
/// ScriptDataString
/// </summary>
String = 2,
/// <summary>
/// ScriptDataObject
/// </summary>
Object = 3,
/// <summary>
/// Not Supported
/// </summary>
MovieClip = 4,
/// <summary>
/// Nothing
/// </summary>
Null = 5,
/// <summary>
/// Nothing
/// </summary>
Undefined = 6,
/// <summary>
/// Not Supported
/// </summary>
Reference = 7,
/// <summary>
/// ScriptDataEcmaArray
/// </summary>
ECMAArray = 8,
/// <summary>
/// Nothing
/// </summary>
ObjectEndMarker = 9,
/// <summary>
/// ScriptDataStrictArray
/// </summary>
StrictArray = 10,
/// <summary>
/// ScriptDataDate
/// </summary>
Date = 11,
/// <summary>
/// ScriptDataLongString
/// </summary>
LongString = 12
}
}

View File

@ -1,27 +0,0 @@
namespace BililiveRecorder.FlvProcessor
{
public delegate void FlvMetadataEvent(object sender, FlvMetadataArgs e);
public class FlvMetadataArgs
{
public IFlvMetadata Metadata;
}
public delegate void TagProcessedEvent(object sender, TagProcessedArgs e);
public class TagProcessedArgs
{
public IFlvTag Tag;
}
public delegate void ClipFinalizedEvent(object sender, ClipFinalizedArgs e);
public class ClipFinalizedArgs
{
public IFlvClipProcessor ClipProcessor;
}
public delegate void StreamFinalizedEvent(object sender, StreamFinalizedArgs e);
public class StreamFinalizedArgs
{
public IFlvStreamProcessor StreamProcessor;
}
}

View File

@ -1,99 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using NLog;
namespace BililiveRecorder.FlvProcessor
{
public class FlvClipProcessor : IFlvClipProcessor
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private readonly Func<IFlvTag> funcFlvTag;
public IFlvMetadata Header { get; private set; }
public List<IFlvTag> HTags { get; private set; }
public List<IFlvTag> Tags { get; private set; }
private int target = -1;
private string path;
public FlvClipProcessor(Func<IFlvTag> funcFlvTag)
{
this.funcFlvTag = funcFlvTag;
}
public IFlvClipProcessor Initialize(string path, IFlvMetadata metadata, List<IFlvTag> head, List<IFlvTag> data, uint seconds)
{
this.path = path;
this.Header = metadata; // TODO: Copy a copy, do not share
this.HTags = head;
this.Tags = data;
this.target = this.Tags[this.Tags.Count - 1].TimeStamp + (int)(seconds * FlvStreamProcessor.SEC_TO_MS);
logger.Debug("Clip 创建 Tags.Count={0} Tags[0].TimeStamp={1} Tags[Tags.Count-1].TimeStamp={2} Tags里秒数={3}",
this.Tags.Count, this.Tags[0].TimeStamp, this.Tags[this.Tags.Count - 1].TimeStamp, (this.Tags[this.Tags.Count - 1].TimeStamp - this.Tags[0].TimeStamp) / 1000d);
return this;
}
public void AddTag(IFlvTag tag)
{
this.Tags.Add(tag);
if (tag.TimeStamp >= this.target)
{
this.FinallizeFile();
}
}
public void FinallizeFile()
{
try
{
if (!Directory.Exists(Path.GetDirectoryName(this.path)))
{
Directory.CreateDirectory(Path.GetDirectoryName(this.path));
}
using (var fs = new FileStream(this.path, FileMode.CreateNew, FileAccess.ReadWrite))
{
fs.Write(FlvStreamProcessor.FLV_HEADER_BYTES, 0, FlvStreamProcessor.FLV_HEADER_BYTES.Length);
fs.Write(new byte[] { 0, 0, 0, 0, }, 0, 4);
double clipDuration = (this.Tags[this.Tags.Count - 1].TimeStamp - this.Tags[0].TimeStamp) / 1000d;
this.Header["duration"] = clipDuration;
this.Header["lasttimestamp"] = (double)(this.Tags[this.Tags.Count - 1].TimeStamp - this.Tags[0].TimeStamp);
var t = this.funcFlvTag();
t.TagType = TagType.DATA;
if (this.Header.ContainsKey("BililiveRecorder"))
{
// TODO: 更好的写法
(this.Header["BililiveRecorder"] as Dictionary<string, object>)["starttime"] = DateTime.UtcNow - TimeSpan.FromSeconds(clipDuration);
}
t.Data = this.Header.ToBytes();
t.WriteTo(fs);
int offset = this.Tags[0].TimeStamp;
this.HTags.ForEach(tag => tag.WriteTo(fs));
this.Tags.ForEach(tag => tag.WriteTo(fs, offset));
logger.Info("剪辑已保存:{0}", Path.GetFileName(this.path));
fs.Close();
}
this.Tags.Clear();
}
catch (IOException ex)
{
logger.Warn(ex, "保存剪辑文件时出错");
}
catch (Exception ex)
{
logger.Error(ex, "保存剪辑文件时出错");
}
ClipFinalized?.Invoke(this, new ClipFinalizedArgs() { ClipProcessor = this });
}
public event ClipFinalizedEvent ClipFinalized;
}
}

View File

@ -1,336 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace BililiveRecorder.FlvProcessor
{
public class FlvMetadata : IFlvMetadata
{
private IDictionary<string, object> Meta { get; set; } = new Dictionary<string, object>();
public ICollection<string> Keys => this.Meta.Keys;
public ICollection<object> Values => this.Meta.Values;
public int Count => this.Meta.Count;
public bool IsReadOnly => false;
public object this[string key] { get => this.Meta[key]; set => this.Meta[key] = value; }
public FlvMetadata()
{
this.Meta["duration"] = 0.0;
this.Meta["lasttimestamp"] = 0.0;
}
public FlvMetadata(byte[] data)
{
// Meta = _Decode(data);
int readHead = 0;
string name = DecodeScriptDataValue(data, ref readHead, AMFTypes.String) as string;
if (name != "onMetaData")
{
throw new Exception("Isn't onMetadata");
}
this.Meta = DecodeScriptDataValue(data, ref readHead) as Dictionary<string, object>;
if (!this.Meta.ContainsKey("duration"))
{
this.Meta["duration"] = 0d;
}
if (!this.Meta.ContainsKey("lasttimestamp"))
{
this.Meta["lasttimestamp"] = 0d;
}
this.Meta.Remove("");
foreach (var item in this.Meta.ToArray())
{
if (item.Value is string text)
{
this.Meta[item.Key] = text.Replace("\0", "");
}
}
}
public byte[] ToBytes()
{
using (var ms = new MemoryStream())
{
EncodeScriptDataValue(ms, "onMetaData");
EncodeScriptDataValue(ms, this.Meta);
return ms.ToArray();
}
}
#region Static
private static void EncodeScriptDataValue(MemoryStream ms, object value)
{
switch (value)
{
case int number:
{
double asDouble = number;
ms.WriteByte((byte)AMFTypes.Number);
ms.Write(BitConverter.GetBytes(asDouble).ToBE(), 0, sizeof(double));
break;
}
case double number:
{
ms.WriteByte((byte)AMFTypes.Number);
ms.Write(BitConverter.GetBytes(number).ToBE(), 0, sizeof(double));
break;
}
case bool boolean:
{
ms.WriteByte((byte)AMFTypes.Boolean);
if (boolean)
{
ms.WriteByte(1);
}
else
{
ms.WriteByte(0);
}
break;
}
case string text:
{
var b = Encoding.UTF8.GetBytes(text);
if (b.Length >= ushort.MaxValue)
{
ms.WriteByte((byte)AMFTypes.LongString);
ms.Write(BitConverter.GetBytes((uint)b.Length).ToBE(), 0, sizeof(uint));
}
else
{
ms.WriteByte((byte)AMFTypes.String);
ms.Write(BitConverter.GetBytes((ushort)b.Length).ToBE(), 0, sizeof(ushort));
}
ms.Write(b, 0, b.Length);
break;
}
case Dictionary<string, object> d:
{
ms.WriteByte((byte)AMFTypes.Object);
foreach (var item in d)
{
var b = Encoding.UTF8.GetBytes(item.Key);
ms.Write(BitConverter.GetBytes((ushort)b.Length).ToBE(), 0, sizeof(ushort));
ms.Write(b, 0, b.Length);
EncodeScriptDataValue(ms, item.Value);
}
ms.WriteByte(0);
ms.WriteByte(0);
ms.WriteByte(9);
break;
}
case List<object> l:
{
ms.WriteByte((byte)AMFTypes.StrictArray);
ms.Write(BitConverter.GetBytes((uint)l.Count), 0, sizeof(uint));
foreach (var item in l)
{
EncodeScriptDataValue(ms, item);
}
break;
}
case DateTime dateTime:
{
ms.WriteByte((byte)AMFTypes.Date);
ms.Write(BitConverter.GetBytes(dateTime.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds).ToBE(), 0, sizeof(double));
ms.Write(BitConverter.GetBytes((short)0).ToBE(), 0, sizeof(short)); // UTC
break;
}
default:
throw new NotSupportedException("Type " + value.GetType().FullName + " is not supported");
}
}
private static object DecodeScriptDataValue(byte[] buff, ref int _readHead, AMFTypes expectType = AMFTypes.Any)
{
AMFTypes type = (AMFTypes)buff[_readHead++];
if (expectType != AMFTypes.Any && expectType != type)
{
throw new Exception("AMF Decode type error");
}
switch (type)
{
case AMFTypes.Number:
{
const int SIZEOF_DOUBLE = sizeof(double);
byte[] bytes = new byte[SIZEOF_DOUBLE];
Buffer.BlockCopy(buff, _readHead, bytes, 0, bytes.Length);
_readHead += SIZEOF_DOUBLE;
return BitConverter.ToDouble(bytes.ToBE(), 0);
}
case AMFTypes.Boolean:
return buff[_readHead++] != 0;
case AMFTypes.String:
{
byte[] bytes = new byte[sizeof(ushort)];
bytes[0] = buff[_readHead++];
bytes[1] = buff[_readHead++];
ushort text_size = BitConverter.ToUInt16(bytes.ToBE(), 0);
string text = Encoding.UTF8.GetString(buff, _readHead, text_size);
_readHead += text_size;
return text;
}
case AMFTypes.Object:
{
var d = new Dictionary<string, object>();
while (!(buff[_readHead] == 0 && buff[_readHead + 1] == 0 && buff[_readHead + 2] == 9))
{
string key;
{
byte[] bytes = new byte[sizeof(ushort)];
bytes[0] = buff[_readHead++];
bytes[1] = buff[_readHead++];
ushort text_size = BitConverter.ToUInt16(bytes.ToBE(), 0);
key = Encoding.UTF8.GetString(buff, _readHead, text_size);
_readHead += text_size;
}
object value = DecodeScriptDataValue(buff, ref _readHead);
// d.Add(key, value);
d[key] = value; // fix duplicates
}
_readHead += 3;
return d;
}
case AMFTypes.Null:
case AMFTypes.Undefined:
return null;
case AMFTypes.ECMAArray:
_readHead += 4;
goto case AMFTypes.Object;
case AMFTypes.StrictArray:
{
byte[] bytes = new byte[sizeof(uint)];
bytes[0] = buff[_readHead++];
bytes[1] = buff[_readHead++];
bytes[2] = buff[_readHead++];
bytes[3] = buff[_readHead++];
uint array_size = BitConverter.ToUInt32(bytes.ToBE(), 0);
var d = new List<object>();
while (d.Count < array_size)
{
d.Add(DecodeScriptDataValue(buff, ref _readHead));
}
return d;
}
case AMFTypes.Date:
{
const int SIZEOF_DOUBLE = sizeof(double);
const int SIZEOF_SI16 = sizeof(short);
byte[] datetime_bytes = new byte[SIZEOF_DOUBLE];
Buffer.BlockCopy(buff, _readHead, datetime_bytes, 0, datetime_bytes.Length);
_readHead += SIZEOF_DOUBLE;
var datetime = BitConverter.ToDouble(datetime_bytes.ToBE(), 0);
byte[] offset_bytes = new byte[SIZEOF_SI16];
Buffer.BlockCopy(buff, _readHead, offset_bytes, 0, offset_bytes.Length);
_readHead += SIZEOF_SI16;
var offset = BitConverter.ToDouble(offset_bytes.ToBE(), 0);
return new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(datetime).AddMinutes(-offset);
}
case AMFTypes.LongString:
unchecked
{
byte[] bytes = new byte[sizeof(uint)];
bytes[0] = buff[_readHead++];
bytes[1] = buff[_readHead++];
bytes[2] = buff[_readHead++];
bytes[3] = buff[_readHead++];
int text_size = (int)BitConverter.ToUInt32(bytes.ToBE(), 0);
if (text_size <= 0)
{
throw new Exception("LongString longer than " + int.MaxValue + " is not supported");
}
string text = Encoding.UTF8.GetString(buff, _readHead, text_size);
_readHead += text_size;
return text;
}
default:
case AMFTypes.MovieClip:
case AMFTypes.Reference:
case AMFTypes.ObjectEndMarker:
throw new NotSupportedException("AMF type not supported");
}
}
#endregion
public void Add(string key, object value)
{
this.Meta.Add(key, value);
}
public bool ContainsKey(string key)
{
return this.Meta.ContainsKey(key);
}
public bool Remove(string key)
{
return this.Meta.Remove(key);
}
public bool TryGetValue(string key, out object value)
{
return this.Meta.TryGetValue(key, out value);
}
public void Add(KeyValuePair<string, object> item)
{
this.Meta.Add(item);
}
public void Clear()
{
this.Meta.Clear();
}
public bool Contains(KeyValuePair<string, object> item)
{
return this.Meta.Contains(item);
}
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
this.Meta.CopyTo(array, arrayIndex);
}
public bool Remove(KeyValuePair<string, object> item)
{
return this.Meta.Remove(item);
}
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
return this.Meta.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.Meta.GetEnumerator();
}
}
}

View File

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

View File

@ -1,453 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using NLog;
namespace BililiveRecorder.FlvProcessor
{
// TODO: 添加测试
public class FlvStreamProcessor : IFlvStreamProcessor
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
internal const uint SEC_TO_MS = 1000; // 1 second = 1000 ms
internal const int MIN_BUFFER_SIZE = 1024 * 2;
internal static readonly byte[] FLV_HEADER_BYTES = new byte[]
{
0x46, // F
0x4c, // L
0x56, // V
0x01, // Version 1
0x05, // bit 00000 1 0 1 (have video and audio)
0x00, // ---
0x00, // |
0x00, // |
0x09, // total of 9 bytes
// 0x00, // ---
// 0x00, // |
// 0x00, // |
// 0x00, // the "0th" tag has a length of 0
};
private readonly object _writelock = new object();
private readonly List<IFlvTag> _headerTags = new List<IFlvTag>();
private readonly List<IFlvTag> _tags = new List<IFlvTag>();
private readonly MemoryStream _data = new MemoryStream();
private FileStream _targetFile;
private IFlvTag _currentTag = null;
private byte[] _leftover = null;
private bool _finalized = false;
private bool _headerParsed = false;
private bool _hasOffset = false;
private int _lasttimeRemovedTimestamp = 0;
private int _baseTimeStamp = 0;
private int _writeTimeStamp = 0;
private int _tagVideoCount = 0;
private int _tagAudioCount = 0;
public int TotalMaxTimestamp { get; private set; } = 0;
public int CurrentMaxTimestamp { get => this.TotalMaxTimestamp - this._writeTimeStamp; }
private readonly IProcessorFactory processorFactory;
private readonly IFlvMetadataFactory flvMetadataFactory;
private readonly Func<IFlvTag> funcFlvTag;
private Func<(string fullPath, string relativePath)> GetStreamFileName;
private Func<string> GetClipFileName;
public event TagProcessedEvent TagProcessed;
public event EventHandler<long> FileFinalized;
public event StreamFinalizedEvent StreamFinalized;
public event FlvMetadataEvent OnMetaData;
public uint ClipLengthPast { get; set; } = 20;
public uint ClipLengthFuture { get; set; } = 10;
public uint CuttingNumber { get; set; } = 10;
private EnabledFeature EnabledFeature;
private AutoCuttingMode CuttingMode;
public IFlvMetadata Metadata { get; set; } = null;
public ObservableCollection<IFlvClipProcessor> Clips { get; } = new ObservableCollection<IFlvClipProcessor>();
public FlvStreamProcessor(IProcessorFactory processorFactory, IFlvMetadataFactory flvMetadataFactory, Func<IFlvTag> funcFlvTag)
{
this.processorFactory = processorFactory;
this.flvMetadataFactory = flvMetadataFactory;
this.funcFlvTag = funcFlvTag;
}
public IFlvStreamProcessor Initialize(Func<(string fullPath, string relativePath)> getStreamFileName, Func<string> getClipFileName, EnabledFeature enabledFeature, AutoCuttingMode autoCuttingMode)
{
this.GetStreamFileName = getStreamFileName;
this.GetClipFileName = getClipFileName;
this.EnabledFeature = enabledFeature;
this.CuttingMode = autoCuttingMode;
return this;
}
private void OpenNewRecordFile()
{
var (fullPath, relativePath) = this.GetStreamFileName();
logger.Debug("打开新录制文件: " + fullPath);
try { Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); } catch (Exception) { }
this._targetFile = new FileStream(fullPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.Read | FileShare.Delete);
if (this._headerParsed)
{
this._targetFile.Write(FlvStreamProcessor.FLV_HEADER_BYTES, 0, FlvStreamProcessor.FLV_HEADER_BYTES.Length);
this._targetFile.Write(new byte[] { 0, 0, 0, 0, }, 0, 4);
var script_tag = this.funcFlvTag();
script_tag.TagType = TagType.DATA;
if (this.Metadata.ContainsKey("BililiveRecorder"))
{
// TODO: 更好的写法
(this.Metadata["BililiveRecorder"] as Dictionary<string, object>)["starttime"] = DateTime.UtcNow;
}
script_tag.Data = this.Metadata.ToBytes();
script_tag.WriteTo(this._targetFile);
this._headerTags.ForEach(tag => tag.WriteTo(this._targetFile));
}
}
public void AddBytes(byte[] data)
{
lock (this._writelock)
{
if (this._finalized) { return; /*throw new InvalidOperationException("Processor Already Closed");*/ }
if (this._leftover != null)
{
byte[] c = new byte[this._leftover.Length + data.Length];
this._leftover.CopyTo(c, 0);
data.CopyTo(c, this._leftover.Length);
this._leftover = null;
this.ParseBytes(c);
}
else
{
this.ParseBytes(data);
}
}
}
private void ParseBytes(byte[] data)
{
int position = 0;
if (!this._headerParsed) { ReadFlvHeader(); }
while (position < data.Length)
{
if (this._currentTag == null)
{
if (!ParseTagHead())
{
this._leftover = data.Skip(position).ToArray();
break;
}
}
else
{ FillTagData(); }
}
bool ParseTagHead()
{
if (data.Length - position < 15) { return false; }
byte[] b = new byte[4];
IFlvTag tag = this.funcFlvTag();
// Previous Tag Size UI24
position += 4;
// TagType UI8
tag.TagType = (TagType)data[position++];
// DataSize UI24
b[1] = data[position++];
b[2] = data[position++];
b[3] = data[position++];
tag.TagSize = BitConverter.ToInt32(b.ToBE(), 0);
// Timestamp UI24
b[1] = data[position++];
b[2] = data[position++];
b[3] = data[position++];
// TimestampExtended UI8
b[0] = data[position++];
tag.SetTimeStamp(BitConverter.ToInt32(b.ToBE(), 0));
// StreamID UI24
tag.StreamId[0] = data[position++];
tag.StreamId[1] = data[position++];
tag.StreamId[2] = data[position++];
this._currentTag = tag;
return true;
}
void FillTagData()
{
int toRead = Math.Min(data.Length - position, this._currentTag.TagSize - (int)this._data.Position);
this._data.Write(buffer: data, offset: position, count: toRead);
position += toRead;
if ((int)this._data.Position == this._currentTag.TagSize)
{
this._currentTag.Data = this._data.ToArray();
this._data.SetLength(0); // reset data buffer
this.TagCreated(this._currentTag);
this._currentTag = null;
}
}
void ReadFlvHeader()
{
if (data[4] != FLV_HEADER_BYTES[4])
{
// 七牛 直播云 的高端 FLV 头
logger.Debug("FLV头[4]的值是 {0}", data[4]);
data[4] = FLV_HEADER_BYTES[4];
}
var r = new bool[FLV_HEADER_BYTES.Length];
for (int i = 0; i < FLV_HEADER_BYTES.Length; i++)
{
r[i] = data[i] == FLV_HEADER_BYTES[i];
}
bool succ = r.All(x => x);
if (!succ)
{
throw new NotSupportedException("Not FLV Stream or Not Supported"); // TODO: custom Exception.
}
this._headerParsed = true;
position += FLV_HEADER_BYTES.Length;
}
}
private void TagCreated(IFlvTag tag)
{
if (this.Metadata == null)
{
ParseMetadata();
}
else
{
if (!this._hasOffset) { ParseTimestampOffset(); }
SetTimestamp();
if (this.EnabledFeature.IsRecordEnabled()) { ProcessRecordLogic(); }
if (this.EnabledFeature.IsClipEnabled()) { ProcessClipLogic(); }
TagProcessed?.Invoke(this, new TagProcessedArgs() { Tag = tag });
}
return;
void SetTimestamp()
{
if (this._hasOffset)
{
tag.SetTimeStamp(tag.TimeStamp - this._baseTimeStamp);
this.TotalMaxTimestamp = Math.Max(this.TotalMaxTimestamp, tag.TimeStamp);
}
else
{ tag.SetTimeStamp(0); }
}
void ProcessRecordLogic()
{
if (this.CuttingMode != AutoCuttingMode.Disabled && tag.IsVideoKeyframe)
{
bool byTime = (this.CuttingMode == AutoCuttingMode.ByTime) && (this.CurrentMaxTimestamp / 1000 >= this.CuttingNumber * 60);
bool bySize = (this.CuttingMode == AutoCuttingMode.BySize) && (((this._targetFile?.Length ?? 0) / 1024 / 1024) >= this.CuttingNumber);
if (byTime || bySize)
{
this.FinallizeCurrentFile();
this.OpenNewRecordFile();
this._writeTimeStamp = this.TotalMaxTimestamp;
}
}
if (!(this._targetFile?.CanWrite ?? false))
{
this.OpenNewRecordFile();
}
tag.WriteTo(this._targetFile, this._writeTimeStamp);
}
void ProcessClipLogic()
{
this._tags.Add(tag);
// 移除过旧的数据
if (this.TotalMaxTimestamp - this._lasttimeRemovedTimestamp > 800)
{
this._lasttimeRemovedTimestamp = this.TotalMaxTimestamp;
int max_remove_index = this._tags.FindLastIndex(x => x.IsVideoKeyframe && ((this.TotalMaxTimestamp - x.TimeStamp) > (this.ClipLengthPast * SEC_TO_MS)));
if (max_remove_index > 0)
{
this._tags.RemoveRange(0, max_remove_index);
}
// Tags.RemoveRange(0, max_remove_index + 1 - 1);
// 给将来的备注:这里是故意 + 1 - 1 的,因为要保留选中的那个关键帧, + 1 就把关键帧删除了
}
this.Clips.ToList().ForEach(fcp => fcp.AddTag(tag));
}
void ParseTimestampOffset()
{
if (tag.TagType == TagType.VIDEO)
{
this._tagVideoCount++;
if (this._tagVideoCount < 2)
{
logger.Debug("第一个 Video Tag 时间戳 {0} ms", tag.TimeStamp);
this._headerTags.Add(tag);
}
else
{
this._baseTimeStamp = tag.TimeStamp;
this._hasOffset = true;
logger.Debug("重设时间戳 {0} 毫秒", this._baseTimeStamp);
}
}
else if (tag.TagType == TagType.AUDIO)
{
this._tagAudioCount++;
if (this._tagAudioCount < 2)
{
logger.Debug("第一个 Audio Tag 时间戳 {0} ms", tag.TimeStamp);
this._headerTags.Add(tag);
}
else
{
this._baseTimeStamp = tag.TimeStamp;
this._hasOffset = true;
logger.Debug("重设时间戳 {0} 毫秒", this._baseTimeStamp);
}
}
}
void ParseMetadata()
{
if (tag.TagType == TagType.DATA)
{
this._targetFile?.Write(FLV_HEADER_BYTES, 0, FLV_HEADER_BYTES.Length);
this._targetFile?.Write(new byte[] { 0, 0, 0, 0, }, 0, 4);
this.Metadata = this.flvMetadataFactory.CreateFlvMetadata(tag.Data);
OnMetaData?.Invoke(this, new FlvMetadataArgs() { Metadata = Metadata });
tag.Data = this.Metadata.ToBytes();
tag.WriteTo(this._targetFile);
}
else
{
throw new Exception("onMetaData not found");
}
}
}
public IFlvClipProcessor Clip()
{
if (!this.EnabledFeature.IsClipEnabled()) { return null; }
lock (this._writelock)
{
if (this._finalized)
{
return null;
// throw new InvalidOperationException("Processor Already Closed");
}
logger.Info("剪辑处理中,将会保存过去 {0} 秒和将来 {1} 秒的直播流", (this._tags[this._tags.Count - 1].TimeStamp - this._tags[0].TimeStamp) / 1000d, 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); };
this.Clips.Add(clip);
return clip;
}
}
private void FinallizeCurrentFile()
{
try
{
var fileSize = this._targetFile?.Length ?? -1;
logger.Debug("正在关闭当前录制文件: " + this._targetFile?.Name);
this.Metadata["duration"] = this.CurrentMaxTimestamp / 1000.0;
this.Metadata["lasttimestamp"] = (double)this.CurrentMaxTimestamp;
byte[] metadata = this.Metadata.ToBytes();
// 13 for FLV header & "0th" tag size
// 11 for 1st tag header
this._targetFile?.Seek(13 + 11, SeekOrigin.Begin);
this._targetFile?.Write(metadata, 0, metadata.Length);
if (fileSize > 0)
FileFinalized?.Invoke(this, fileSize);
}
catch (IOException ex)
{
logger.Warn(ex, "保存录制文件时出错");
}
catch (Exception ex)
{
logger.Error(ex, "保存录制文件时出错");
}
finally
{
this._targetFile?.Close();
this._targetFile = null;
}
}
public void FinallizeFile()
{
if (!this._finalized)
{
lock (this._writelock)
{
try
{
this.FinallizeCurrentFile();
}
finally
{
this._targetFile?.Close();
this._data.Close();
this._tags.Clear();
this._finalized = true;
this.Clips.ToList().ForEach(fcp => fcp.FinallizeFile()); // TODO: check
StreamFinalized?.Invoke(this, new StreamFinalizedArgs() { StreamProcessor = this });
}
}
}
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!this.disposedValue)
{
if (disposing)
{
this._data.Dispose();
this._targetFile?.Dispose();
OnMetaData = null;
StreamFinalized = null;
TagProcessed = null;
}
this._tags.Clear();
this.disposedValue = true;
}
}
public void Dispose()
{
this.Dispose(true);
}
#endregion
}
}

View File

@ -1,111 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
namespace BililiveRecorder.FlvProcessor
{
public class FlvTag : IFlvTag
{
public TagType TagType { get; set; } = 0;
public int TagSize { get; set; } = 0;
public int TimeStamp { get; private set; } = 0;
public byte[] StreamId { get; set; } = new byte[3];
public bool IsVideoKeyframe { get; private set; }// _IsVideoKeyframe != -1 ? _IsVideoKeyframe == 1 : 1 == (_IsVideoKeyframe = _ParseIsVideoKeyframe());
public int Profile { get; private set; } = -1;
public int Level { get; private set; } = -1;
public byte[] Data { get => this._data; set { this._data = value; this.ParseInfo(); } }
private byte[] _data = null;
public void SetTimeStamp(int timestamp) => this.TimeStamp = timestamp;
private void ParseInfo()
{
/**
* VIDEODATA:
* 0x17 (1 byte)
* 1 = AVC Keyframe
* 7 = AVC Codec
* AVCVIDEOPACKET:
* 0x00 (1 byte)
* 0 = AVC Header
* 1 = AVC NALU
* 0x00
* 0x00
* 0x00 (3 bytes)
* if(AVC_HEADER) then always 0
* AVCDecoderConfigurationRecord:
* 0x01 (1 byte)
* configurationVersion must be 1
* 0x00 (1 byte)
* AVCProfileIndication
* 0x00 (1 byte)
* profile_compatibility
* 0x00 (1 byte)
* AVCLevelIndication
* */
this.IsVideoKeyframe = false;
this.Profile = -1;
this.Level = -1;
if (this.TagType != TagType.VIDEO) { return; }
if (this.Data.Length < 9) { return; }
// Not AVC Keyframe
if (this.Data[0] != 0x17) { return; }
this.IsVideoKeyframe = true;
// Isn't AVCDecoderConfigurationRecord
if (this.Data[1] != 0x00) { return; }
// version is not 1
if (this.Data[5] != 0x01) { return; }
this.Profile = this.Data[6];
this.Level = this.Data[8];
#if DEBUG
Debug.WriteLine("Video Profile: " + this.Profile + ", Level: " + this.Level);
#endif
}
public byte[] ToBytes(bool useDataSize, int offset = 0)
{
var tag = new byte[11];
tag[0] = (byte)this.TagType;
var size = BitConverter.GetBytes(useDataSize ? this.Data.Length : this.TagSize).ToBE();
Buffer.BlockCopy(size, 1, tag, 1, 3);
byte[] timing = BitConverter.GetBytes(this.TimeStamp - offset).ToBE();
Buffer.BlockCopy(timing, 1, tag, 4, 3);
Buffer.BlockCopy(timing, 0, tag, 7, 1);
Buffer.BlockCopy(this.StreamId, 0, tag, 8, 3);
return tag;
}
public void WriteTo(Stream stream, int offset = 0)
{
if (stream != null)
{
var vs = this.ToBytes(true, offset);
stream.Write(vs, 0, vs.Length);
stream.Write(this.Data, 0, this.Data.Length);
stream.Write(BitConverter.GetBytes(this.Data.Length + vs.Length).ToBE(), 0, 4);
}
}
private int _ParseIsVideoKeyframe()
{
if (this.TagType != TagType.VIDEO) { return 0; }
if (this.Data.Length < 1) { return -1; }
const byte mask = 0b00001111;
const byte compare = 0b00011111;
return (this.Data[0] | mask) == compare ? 1 : 0;
}
}
}

View File

@ -1,17 +0,0 @@
using System.Collections.Generic;
namespace BililiveRecorder.FlvProcessor
{
public interface IFlvClipProcessor
{
IFlvMetadata Header { get; }
List<IFlvTag> HTags { get; }
List<IFlvTag> Tags { get; }
IFlvClipProcessor Initialize(string path, IFlvMetadata metadata, List<IFlvTag> head, List<IFlvTag> data, uint seconds);
void AddTag(IFlvTag tag);
void FinallizeFile();
event ClipFinalizedEvent ClipFinalized;
}
}

View File

@ -1,9 +0,0 @@
using System.Collections.Generic;
namespace BililiveRecorder.FlvProcessor
{
public interface IFlvMetadata : IDictionary<string, object>
{
byte[] ToBytes();
}
}

View File

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

View File

@ -1,28 +0,0 @@
using System;
using System.Collections.ObjectModel;
#nullable enable
namespace BililiveRecorder.FlvProcessor
{
public interface IFlvStreamProcessor : IDisposable
{
event TagProcessedEvent TagProcessed;
event EventHandler<long> FileFinalized;
event StreamFinalizedEvent StreamFinalized;
event FlvMetadataEvent OnMetaData;
int TotalMaxTimestamp { get; }
int CurrentMaxTimestamp { get; }
IFlvMetadata Metadata { get; set; }
ObservableCollection<IFlvClipProcessor> Clips { get; }
uint ClipLengthPast { get; set; }
uint ClipLengthFuture { get; set; }
uint CuttingNumber { get; set; }
IFlvStreamProcessor Initialize(Func<(string fullPath, string relativePath)> getStreamFileName, Func<string> getClipFileName, EnabledFeature enabledFeature, AutoCuttingMode autoCuttingMode);
IFlvClipProcessor Clip();
void AddBytes(byte[] data);
void FinallizeFile();
}
}

View File

@ -1,18 +0,0 @@
using System.IO;
namespace BililiveRecorder.FlvProcessor
{
public interface IFlvTag
{
TagType TagType { get; set; }
int TagSize { get; set; }
int TimeStamp { get; }
byte[] StreamId { get; set; }
bool IsVideoKeyframe { get; }
byte[] Data { get; set; }
void SetTimeStamp(int timestamp);
byte[] ToBytes(bool useDataSize, int offset = 0);
void WriteTo(Stream stream, int offset = 0);
}
}

View File

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

View File

@ -1,20 +0,0 @@
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

@ -1,3 +0,0 @@
# BililiveRecorder.FlvProcessor
TODO

View File

@ -1,22 +0,0 @@
using System;
using System.Linq;
namespace BililiveRecorder.FlvProcessor
{
internal static class Utils
{
/// <summary>
/// 转换字节序。实际上通常是把 BE 转成 LE
/// </summary>
/// <param name="b"></param>
/// <returns></returns>
internal static byte[] ToBE(this byte[] b)
{
if (BitConverter.IsLittleEndian)
return b.Reverse().ToArray();
else
return b;
}
}
}

View File

@ -16,12 +16,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BililiveRecorder.WPF", "Bil
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Core", "BililiveRecorder.Core\BililiveRecorder.Core.csproj", "{CB9F2D58-181D-49F7-9560-D35A9B9C1D8C}"
ProjectSection(ProjectDependencies) = postProject
{51748048-1949-4218-8DED-94014ABE7633} = {51748048-1949-4218-8DED-94014ABE7633}
{7610E19C-D3AB-4CBC-983E-6FDA36F4D4B3} = {7610E19C-D3AB-4CBC-983E-6FDA36F4D4B3}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.FlvProcessor", "BililiveRecorder.FlvProcessor\BililiveRecorder.FlvProcessor.csproj", "{51748048-1949-4218-8DED-94014ABE7633}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Cli", "BililiveRecorder.Cli\BililiveRecorder.Cli.csproj", "{1B626335-283F-4313-9045-B5B96FAAB2DF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Flv", "BililiveRecorder.Flv\BililiveRecorder.Flv.csproj", "{7610E19C-D3AB-4CBC-983E-6FDA36F4D4B3}"
@ -44,10 +41,6 @@ Global
{CB9F2D58-181D-49F7-9560-D35A9B9C1D8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CB9F2D58-181D-49F7-9560-D35A9B9C1D8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CB9F2D58-181D-49F7-9560-D35A9B9C1D8C}.Release|Any CPU.Build.0 = Release|Any CPU
{51748048-1949-4218-8DED-94014ABE7633}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{51748048-1949-4218-8DED-94014ABE7633}.Debug|Any CPU.Build.0 = Debug|Any CPU
{51748048-1949-4218-8DED-94014ABE7633}.Release|Any CPU.ActiveCfg = Release|Any CPU
{51748048-1949-4218-8DED-94014ABE7633}.Release|Any CPU.Build.0 = Release|Any CPU
{1B626335-283F-4313-9045-B5B96FAAB2DF}.Debug|Any CPU.ActiveCfg = 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
@ -71,17 +64,16 @@ Global
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}
{560E8483-9293-410E-81E9-AB36B49F8A7C} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
RESX_ShowErrorsInErrorList = False
RESX_SaveFilesImmediatelyUponChange = True
RESX_NeutralResourcesLanguage = zh-Hans
SolutionGuid = {F3CB8B14-077A-458F-BD8E-1747ED0F5170}
RESX_SortFileContentOnSave = True
SolutionGuid = {F3CB8B14-077A-458F-BD8E-1747ED0F5170}
RESX_NeutralResourcesLanguage = zh-Hans
RESX_SaveFilesImmediatelyUponChange = True
RESX_ShowErrorsInErrorList = False
EndGlobalSection
EndGlobal