mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-16 11:42:22 +08:00
Remove FlvProcessor
This commit is contained in:
parent
58970c217b
commit
534adfba1b
|
@ -1,18 +0,0 @@
|
|||
namespace BililiveRecorder.FlvProcessor
|
||||
{
|
||||
public enum AutoCuttingMode : int
|
||||
{
|
||||
/// <summary>
|
||||
/// 禁用
|
||||
/// </summary>
|
||||
Disabled,
|
||||
/// <summary>
|
||||
/// 根据时间切割
|
||||
/// </summary>
|
||||
ByTime,
|
||||
/// <summary>
|
||||
/// 根据文件大小切割
|
||||
/// </summary>
|
||||
BySize,
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>()
|
||||
;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
namespace BililiveRecorder.FlvProcessor
|
||||
{
|
||||
public class FlvMetadataFactory : IFlvMetadataFactory
|
||||
{
|
||||
public IFlvMetadata CreateFlvMetadata(byte[] data) => new FlvMetadata(data);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace BililiveRecorder.FlvProcessor
|
||||
{
|
||||
public interface IFlvMetadata : IDictionary<string, object>
|
||||
{
|
||||
byte[] ToBytes();
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
namespace BililiveRecorder.FlvProcessor
|
||||
{
|
||||
public interface IFlvMetadataFactory
|
||||
{
|
||||
IFlvMetadata CreateFlvMetadata(byte[] data);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
namespace BililiveRecorder.FlvProcessor
|
||||
{
|
||||
public interface IProcessorFactory
|
||||
{
|
||||
IFlvClipProcessor CreateClipProcessor();
|
||||
IFlvStreamProcessor CreateStreamProcessor();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
# BililiveRecorder.FlvProcessor
|
||||
|
||||
TODO
|
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user