Merge pull request #38 from Bililive/dev

Release 1.1.4
This commit is contained in:
Genteure 2019-01-17 22:42:02 +08:00 committed by GitHub
commit a8a75fefa7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 470 additions and 187 deletions

View File

@ -11,6 +11,7 @@
<UpgradeBackupLocation>
</UpgradeBackupLocation>
<OldToolsVersion>2.0</OldToolsVersion>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DebugType>none</DebugType>
@ -18,6 +19,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="4.8.1" />
<PackageReference Include="DnsClient" Version="1.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="NLog" Version="4.5.10" />
</ItemGroup>

View File

@ -1,7 +1,9 @@
using BililiveRecorder.Core.Config;
using BililiveRecorder.FlvProcessor;
using DnsClient;
using NLog;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
@ -18,6 +20,11 @@ namespace BililiveRecorder.Core
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private static readonly Random random = new Random();
private static readonly LookupClient lookupClient = new LookupClient()
{
ThrowDnsErrors = true,
};
private int _roomid;
private int _realRoomid;
private string _streamerName;
@ -142,7 +149,7 @@ namespace BililiveRecorder.Core
private void StreamMonitor_StreamStatusChanged(object sender, StreamStatusChangedArgs e)
{
// if (StartupTask?.IsCompleted ?? true)
if (!IsRecording)
if (!IsRecording && (StartupTask?.IsCompleted ?? true))
{
StartupTask = _StartRecordAsync();
}
@ -208,7 +215,11 @@ namespace BililiveRecorder.Core
{
using (var client = new HttpClient())
{
var raw_uri = new Uri(BililiveAPI.GetPlayUrl(RealRoomid));
client.Timeout = TimeSpan.FromMilliseconds(_config.TimingStreamConnect);
client.DefaultRequestHeaders.Host = raw_uri.Host;
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*"));
client.DefaultRequestHeaders.UserAgent.Clear();
@ -216,11 +227,13 @@ namespace BililiveRecorder.Core
client.DefaultRequestHeaders.Referrer = new Uri("https://live.bilibili.com");
client.DefaultRequestHeaders.Add("Origin", "https://live.bilibili.com");
string flv_path = BililiveAPI.GetPlayUrl(RealRoomid);
logger.Log(RealRoomid, LogLevel.Info, "连接直播服务器 " + new Uri(flv_path).Host);
logger.Log(RealRoomid, LogLevel.Debug, "直播流地址: " + flv_path);
var ips = lookupClient.Query(raw_uri.DnsSafeHost, QueryType.A).Answers?.ARecords()?.ToArray();
var ip = ips[random.Next(0, ips.Count())].Address;
_response = await client.GetAsync(flv_path, HttpCompletionOption.ResponseHeadersRead);
logger.Log(RealRoomid, LogLevel.Info, "连接直播服务器 " + raw_uri.Host + " (" + ip + ")");
logger.Log(RealRoomid, LogLevel.Debug, "直播流地址: " + raw_uri.ToString());
_response = await client.GetAsync(new UriBuilder(raw_uri) { Host = ip.ToString() }.Uri, HttpCompletionOption.ResponseHeadersRead);
}
if (_response.StatusCode != HttpStatusCode.OK)
@ -238,6 +251,28 @@ namespace BililiveRecorder.Core
Processor.ClipLengthFuture = _config.ClipLengthFuture;
Processor.ClipLengthPast = _config.ClipLengthPast;
Processor.CuttingNumber = _config.CuttingNumber;
Processor.OnMetaData += (sender, e) =>
{
e.Metadata["BililiveRecorder"] = new Dictionary<string, object>()
{
{
"starttime",
DateTime.UtcNow
},
{
"version",
"TEST"
},
{
"roomid",
RealRoomid.ToString()
},
{
"streamername",
StreamerName
},
};
};
_stream = await _response.Content.ReadAsStreamAsync();
_stream.ReadTimeout = 3 * 1000;

View File

@ -7,16 +7,63 @@
DATA = 18,
}
public enum AMFTypes
public enum AMFTypes : byte
{
Number = 0x00, // (Encoded as IEEE 64-bit double-precision floating point number)
Boolean = 0x01, // (Encoded as a single byte of value 0x00 or 0x01)
String = 0x02, //(ASCII encoded)
Object = 0x03, // (Set of key/value pairs)
Null = 0x05,
Array = 0x08,
End = 0x09,
/// <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,5 +1,11 @@
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
{

View File

@ -9,14 +9,17 @@ namespace BililiveRecorder.FlvProcessor
{
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()
public FlvClipProcessor(Func<IFlvTag> funcFlvTag)
{
this.funcFlvTag = funcFlvTag;
}
public IFlvClipProcessor Initialize(string path, IFlvMetadata metadata, List<IFlvTag> head, List<IFlvTag> data, uint seconds)
@ -54,14 +57,18 @@ namespace BililiveRecorder.FlvProcessor
fs.Write(FlvStreamProcessor.FLV_HEADER_BYTES, 0, FlvStreamProcessor.FLV_HEADER_BYTES.Length);
fs.Write(new byte[] { 0, 0, 0, 0, }, 0, 4);
Header.Meta["duration"] = (Tags[Tags.Count - 1].TimeStamp - Tags[0].TimeStamp) / 1000d;
Header.Meta["lasttimestamp"] = (Tags[Tags.Count - 1].TimeStamp - Tags[0].TimeStamp);
Header["duration"] = (Tags[Tags.Count - 1].TimeStamp - Tags[0].TimeStamp) / 1000d;
Header["lasttimestamp"] = (Tags[Tags.Count - 1].TimeStamp - Tags[0].TimeStamp);
var t = new FlvTag
var t = funcFlvTag();
t.TagType = TagType.DATA;
if (Header.ContainsKey("BililiveRecorder"))
{
TagType = TagType.DATA,
Data = Header.ToBytes()
};
// TODO: 更好的写法
(Header["BililiveRecorder"] as Dictionary<string, object>)["starttime"] = DateTime.UtcNow;
}
t.Data = Header.ToBytes();
t.WriteTo(fs);
int offset = Tags[0].TimeStamp;

View File

@ -1,14 +1,25 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
namespace BililiveRecorder.FlvProcessor
{
public class FlvMetadata : IFlvMetadata
{
public IDictionary<string, object> Meta { get; set; } = new Dictionary<string, object>();
private IDictionary<string, object> Meta { get; set; } = new Dictionary<string, object>();
public ICollection<string> Keys => Meta.Keys;
public ICollection<object> Values => Meta.Values;
public int Count => Meta.Count;
public bool IsReadOnly => false;
public object this[string key] { get => Meta[key]; set => Meta[key] = value; }
public FlvMetadata()
{
@ -18,186 +29,299 @@ namespace BililiveRecorder.FlvProcessor
public FlvMetadata(byte[] data)
{
Meta = _Decode(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");
}
Meta = DecodeScriptDataValue(data, ref readHead) as Dictionary<string, object>;
if (!Meta.ContainsKey("duration"))
{
Meta["duration"] = 0.0;
Meta["duration"] = 0d;
}
if (!Meta.ContainsKey("lasttimestamp"))
{
Meta["lasttimestamp"] = 0.0;
Meta["lasttimestamp"] = 0d;
}
Meta.Remove("");
foreach (var item in Meta.ToArray())
{
if (item.Value is string text)
{
Meta[item.Key] = text.Replace("\0", "");
}
}
}
public byte[] ToBytes()
{
return _Encode();
}
#region - Encode -
private byte[] _Encode()
{
using (MemoryStream ms = new MemoryStream())
using (var ms = new MemoryStream())
{
const string onMetaData = "onMetaData";
ms.WriteByte((byte)AMFTypes.String);
UInt16 strSize = (UInt16)onMetaData.Length;
byte[] strSizeb = BitConverter.GetBytes(strSize).ToBE();
ms.Write(strSizeb, 0, strSizeb.Length);
ms.Write(Encoding.ASCII.GetBytes(onMetaData), 0, onMetaData.Length);
ms.WriteByte((byte)AMFTypes.Array);
byte[] asize = BitConverter.GetBytes(Meta.Keys.Count).ToBE();
ms.Write(asize, 0, asize.Length);
foreach (string key in Meta.Keys)
{
object val = Meta[key];
byte[] valBytes = _EncodeVal(val);
if (!string.IsNullOrWhiteSpace(key) && valBytes != null)
{
byte[] keyBytes = _EncodeKey(key);
ms.Write(keyBytes, 0, keyBytes.Length);
ms.Write(valBytes, 0, valBytes.Length);
}
}
/* *
* SCRIPTDATAVARIABLEEND
* Script Data Variable End
* Type: UI24
* Always 9
* */
ms.WriteByte(0x0);
ms.WriteByte(0x0);
ms.WriteByte((byte)AMFTypes.End);
EncodeScriptDataValue(ms, "onMetaData");
EncodeScriptDataValue(ms, Meta);
return ms.ToArray();
}
}
private byte[] _EncodeKey(string key)
{
byte[] ret = new byte[2 + key.Length]; // 2 for the size at the front
UInt16 strSize = (UInt16)key.Length;
byte[] strSizeb = BitConverter.GetBytes(strSize).ToBE();
Buffer.BlockCopy(strSizeb, 0, ret, 0, strSizeb.Length);
Buffer.BlockCopy(Encoding.ASCII.GetBytes(key), 0, ret, 2, key.Length);
return ret;
}
private byte[] _EncodeVal(object val)
{
if (val is double num)
{
byte[] ret = new byte[1 + sizeof(double)];
ret[0] = (byte)AMFTypes.Number;
byte[] numbits = BitConverter.GetBytes(num).ToBE();
Buffer.BlockCopy(numbits, 0, ret, 1, numbits.Length);
return ret;
}
else if (val is string)
{
string str = val as string;
byte[] ret = new byte[3 + str.Length];
ret[0] = (byte)AMFTypes.String;
UInt16 strSize = (UInt16)str.Length;
byte[] strSizeb = BitConverter.GetBytes(strSize).ToBE();
Buffer.BlockCopy(strSizeb, 0, ret, 1, strSizeb.Length);
Buffer.BlockCopy(Encoding.ASCII.GetBytes(str), 0, ret, 3, str.Length);
return ret;
}
else if (val is byte bit)
{
byte[] ret = new byte[2];
ret[0] = (byte)AMFTypes.Boolean;
ret[1] = bit;
return ret;
}
else
{
Debug.Write(string.Format("Unknown Value type: {0}\n", val?.GetType()?.Name));
return null;
}
}
#endregion
#region - Decode -
#region Static
private static string _DecodeKey(byte[] buff, ref int _readHead)
private static void EncodeScriptDataValue(MemoryStream ms, object value)
{
// get length of string name
byte[] flip = new byte[sizeof(short)];
flip[0] = buff[_readHead++];
flip[1] = buff[_readHead++];
ushort klen = BitConverter.ToUInt16(flip.ToBE(), 0);
string name = Encoding.Default.GetString(buff, _readHead, klen);
_readHead += klen;
return name;
}
private static object _DecodeVal(byte[] buff, ref int _readHead)
{
byte type = buff[_readHead++];
AMFTypes amfType = (AMFTypes)Enum.ToObject(typeof(AMFTypes), (int)type);
switch (amfType)
switch (value)
{
case AMFTypes.String:
return _DecodeKey(buff, ref _readHead);
case AMFTypes.Number:
byte[] flip = new byte[sizeof(double)];
Buffer.BlockCopy(buff, _readHead, flip, 0, flip.Length);
double num = BitConverter.ToDouble(flip.ToBE(), 0);
_readHead += sizeof(double);
return num;
case AMFTypes.Boolean:
byte b = buff[_readHead++];
return b;
case AMFTypes.End:
return null;
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));
break;
}
default:
throw new MissingMethodException();
throw new NotSupportedException("Type " + value.GetType().FullName + " is not supported");
}
}
private static IDictionary<string, object> _Decode(byte[] buff)
private static object DecodeScriptDataValue(byte[] buff, ref int _readHead, AMFTypes expectType = AMFTypes.Any)
{
IDictionary<string, object> keyval = new Dictionary<string, object>();
int _readHead = 0;
// get the onMetadata
string onMeta = _DecodeVal(buff, ref _readHead) as string;
// read array type
byte type = buff[_readHead++];
Debug.Assert(type == (byte)AMFTypes.Array || type == (byte)AMFTypes.Object);
if (type == (byte)AMFTypes.Array)
AMFTypes type = (AMFTypes)buff[_readHead++];
if (expectType != AMFTypes.Any && expectType != type)
{
byte[] alen = new byte[sizeof(int)];
Buffer.BlockCopy(buff, _readHead, alen, 0, alen.Length);
_readHead += alen.Length;
int arrayLen = BitConverter.ToInt32(alen.ToBE(), 0);
Debug.Write(string.Format("onMetaData Array Len: {0}\n", arrayLen));
throw new Exception("AMF Decode type error");
}
else if (type == (byte)AMFTypes.Object)
switch (type)
{
Debug.Write("onMetaData isn't an Array but Object!\n");
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);
}
_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");
}
else
{
throw new Exception("Parse Script Tag Error"); // TODO: custom Exception
}
while (_readHead <= buff.Length - 1)
{
string key = _DecodeKey(buff, ref _readHead);
object val = _DecodeVal(buff, ref _readHead);
Debug.Write(string.Format("Parse Script Tag: {0} => {1}\n", key, val));
keyval[key] = val;
}
return keyval;
}
#endregion
public void Add(string key, object value)
{
Meta.Add(key, value);
}
public bool ContainsKey(string key)
{
return Meta.ContainsKey(key);
}
public bool Remove(string key)
{
return Meta.Remove(key);
}
public bool TryGetValue(string key, out object value)
{
return Meta.TryGetValue(key, out value);
}
public void Add(KeyValuePair<string, object> item)
{
Meta.Add(item);
}
public void Clear()
{
Meta.Clear();
}
public bool Contains(KeyValuePair<string, object> item)
{
return Meta.Contains(item);
}
public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
{
Meta.CopyTo(array, arrayIndex);
}
public bool Remove(KeyValuePair<string, object> item)
{
return Meta.Remove(item);
}
public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
{
return Meta.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return Meta.GetEnumerator();
}
}
}

View File

@ -61,6 +61,7 @@ namespace BililiveRecorder.FlvProcessor
public event TagProcessedEvent TagProcessed;
public event StreamFinalizedEvent StreamFinalized;
public event FlvMetadataEvent OnMetaData;
public uint ClipLengthPast { get; set; } = 20;
public uint ClipLengthFuture { get; set; } = 10;
@ -104,11 +105,15 @@ namespace BililiveRecorder.FlvProcessor
_targetFile.Write(FlvStreamProcessor.FLV_HEADER_BYTES, 0, FlvStreamProcessor.FLV_HEADER_BYTES.Length);
_targetFile.Write(new byte[] { 0, 0, 0, 0, }, 0, 4);
new FlvTag
var script_tag = funcFlvTag();
script_tag.TagType = TagType.DATA;
if (Metadata.ContainsKey("BililiveRecorder"))
{
TagType = TagType.DATA,
Data = Metadata.ToBytes()
}.WriteTo(_targetFile);
// TODO: 更好的写法
(Metadata["BililiveRecorder"] as Dictionary<string, object>)["starttime"] = DateTime.UtcNow;
}
script_tag.Data = Metadata.ToBytes();
script_tag.WriteTo(_targetFile);
_headerTags.ForEach(tag => tag.WriteTo(_targetFile));
}
@ -331,7 +336,7 @@ namespace BililiveRecorder.FlvProcessor
Metadata = funcFlvMetadata(tag.Data);
// TODO: 添加录播姬标记、录制信息
OnMetaData?.Invoke(this, new FlvMetadataArgs() { Metadata = Metadata });
tag.Data = Metadata.ToBytes();
tag.WriteTo(_targetFile);
@ -362,9 +367,9 @@ namespace BililiveRecorder.FlvProcessor
{
try
{
logger.Debug("正在关闭当前录制文件: " + _targetFile.Name);
Metadata.Meta["duration"] = CurrentMaxTimestamp / 1000.0;
Metadata.Meta["lasttimestamp"] = (double)CurrentMaxTimestamp;
logger.Debug("正在关闭当前录制文件: " + _targetFile?.Name);
Metadata["duration"] = CurrentMaxTimestamp / 1000.0;
Metadata["lasttimestamp"] = (double)CurrentMaxTimestamp;
byte[] metadata = Metadata.ToBytes();
// 13 for FLV header & "0th" tag size
@ -419,6 +424,9 @@ namespace BililiveRecorder.FlvProcessor
{
_data.Dispose();
_targetFile?.Dispose();
OnMetaData = null;
StreamFinalized = null;
TagProcessed = null;
}
_tags.Clear();
disposedValue = true;

View File

@ -1,21 +1,75 @@
using System;
using System.Diagnostics;
using System.IO;
namespace BililiveRecorder.FlvProcessor
{
public class FlvTag : IFlvTag
{
private int _IsVideoKeyframe = -1;
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 => _IsVideoKeyframe != -1 ? _IsVideoKeyframe == 1 : 1 == (_IsVideoKeyframe = _ParseIsVideoKeyframe());
public byte[] Data { get; set; } = null;
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 => _data; set { _data = value; ParseInfo(); } }
private byte[] _data = null;
public void SetTimeStamp(int timestamp) => 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
* */
IsVideoKeyframe = false;
Profile = -1;
Level = -1;
if (TagType != TagType.VIDEO) { return; }
if (Data.Length < 9) { return; }
// Not AVC Keyframe
if (Data[0] != 0x17) { return; }
IsVideoKeyframe = true;
// Isn't AVCDecoderConfigurationRecord
if (Data[1] != 0x00) { return; }
// version is not 1
if (Data[5] != 0x01) { return; }
Profile = Data[6];
Level = Data[8];
#if DEBUG
Debug.WriteLine("Video Profile: " + Profile + ", Level: " + Level);
#endif
}
public byte[] ToBytes(bool useDataSize, int offset = 0)
{
var tag = new byte[11];

View File

@ -2,9 +2,8 @@
namespace BililiveRecorder.FlvProcessor
{
public interface IFlvMetadata
public interface IFlvMetadata : IDictionary<string, object>
{
IDictionary<string, object> Meta { get; set; }
byte[] ToBytes();
}
}

View File

@ -7,6 +7,7 @@ namespace BililiveRecorder.FlvProcessor
{
event TagProcessedEvent TagProcessed;
event StreamFinalizedEvent StreamFinalized;
event FlvMetadataEvent OnMetaData;
int TotalMaxTimestamp { get; }
int CurrentMaxTimestamp { get; }

View File

@ -1 +1 @@
1.1.3
1.1.4