diff --git a/BililiveRecorder.Core/BililiveRecorder.Core.csproj b/BililiveRecorder.Core/BililiveRecorder.Core.csproj index a5f8668..b5352b7 100644 --- a/BililiveRecorder.Core/BililiveRecorder.Core.csproj +++ b/BililiveRecorder.Core/BililiveRecorder.Core.csproj @@ -11,6 +11,7 @@ 2.0 + true none @@ -18,6 +19,7 @@ + diff --git a/BililiveRecorder.Core/RecordedRoom.cs b/BililiveRecorder.Core/RecordedRoom.cs index ed08aa5..680e702 100644 --- a/BililiveRecorder.Core/RecordedRoom.cs +++ b/BililiveRecorder.Core/RecordedRoom.cs @@ -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() + { + { + "starttime", + DateTime.UtcNow + }, + { + "version", + "TEST" + }, + { + "roomid", + RealRoomid.ToString() + }, + { + "streamername", + StreamerName + }, + }; + }; _stream = await _response.Content.ReadAsStreamAsync(); _stream.ReadTimeout = 3 * 1000; diff --git a/BililiveRecorder.FlvProcessor/Enums.cs b/BililiveRecorder.FlvProcessor/Enums.cs index 08ff0f2..ab30cd0 100644 --- a/BililiveRecorder.FlvProcessor/Enums.cs +++ b/BililiveRecorder.FlvProcessor/Enums.cs @@ -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, + /// + /// 非标准类型。在 Decode 过程中作为函数参数使用 + /// + Any = 0xFF, + /// + /// Double + /// + Number = 0, + /// + /// 8 bit unsigned integer + /// + Boolean = 1, + /// + /// ScriptDataString + /// + String = 2, + /// + /// ScriptDataObject + /// + Object = 3, + /// + /// Not Supported + /// + MovieClip = 4, + /// + /// Nothing + /// + Null = 5, + /// + /// Nothing + /// + Undefined = 6, + /// + /// Not Supported + /// + Reference = 7, + /// + /// ScriptDataEcmaArray + /// + ECMAArray = 8, + /// + /// Nothing + /// + ObjectEndMarker = 9, + /// + /// ScriptDataStrictArray + /// + StrictArray = 10, + /// + /// ScriptDataDate + /// + Date = 11, + /// + /// ScriptDataLongString + /// + LongString = 12 } - - } diff --git a/BililiveRecorder.FlvProcessor/Events.cs b/BililiveRecorder.FlvProcessor/Events.cs index 79a065a..0ed1c1c 100644 --- a/BililiveRecorder.FlvProcessor/Events.cs +++ b/BililiveRecorder.FlvProcessor/Events.cs @@ -1,11 +1,17 @@ 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 { diff --git a/BililiveRecorder.FlvProcessor/FlvClipProcessor.cs b/BililiveRecorder.FlvProcessor/FlvClipProcessor.cs index 38946e1..f633b83 100644 --- a/BililiveRecorder.FlvProcessor/FlvClipProcessor.cs +++ b/BililiveRecorder.FlvProcessor/FlvClipProcessor.cs @@ -9,14 +9,17 @@ namespace BililiveRecorder.FlvProcessor { private static readonly Logger logger = LogManager.GetCurrentClassLogger(); + private readonly Func funcFlvTag; + public IFlvMetadata Header { get; private set; } public List HTags { get; private set; } public List Tags { get; private set; } private int target = -1; private string path; - public FlvClipProcessor() + public FlvClipProcessor(Func funcFlvTag) { + this.funcFlvTag = funcFlvTag; } public IFlvClipProcessor Initialize(string path, IFlvMetadata metadata, List head, List 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)["starttime"] = DateTime.UtcNow; + } + t.Data = Header.ToBytes(); t.WriteTo(fs); int offset = Tags[0].TimeStamp; diff --git a/BililiveRecorder.FlvProcessor/FlvMetadata.cs b/BililiveRecorder.FlvProcessor/FlvMetadata.cs index 7a83141..bfa2e8e 100644 --- a/BililiveRecorder.FlvProcessor/FlvMetadata.cs +++ b/BililiveRecorder.FlvProcessor/FlvMetadata.cs @@ -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 Meta { get; set; } = new Dictionary(); + private IDictionary Meta { get; set; } = new Dictionary(); + + public ICollection Keys => Meta.Keys; + + public ICollection 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; 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 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 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 _Decode(byte[] buff) + + private static object DecodeScriptDataValue(byte[] buff, ref int _readHead, AMFTypes expectType = AMFTypes.Any) { - IDictionary keyval = new Dictionary(); - 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(); + 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(); + 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 item) + { + Meta.Add(item); + } + + public void Clear() + { + Meta.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return Meta.Contains(item); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + Meta.CopyTo(array, arrayIndex); + } + + public bool Remove(KeyValuePair item) + { + return Meta.Remove(item); + } + + public IEnumerator> GetEnumerator() + { + return Meta.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return Meta.GetEnumerator(); + } + + } } diff --git a/BililiveRecorder.FlvProcessor/FlvStreamProcessor.cs b/BililiveRecorder.FlvProcessor/FlvStreamProcessor.cs index 51d2930..6e2dfa8 100644 --- a/BililiveRecorder.FlvProcessor/FlvStreamProcessor.cs +++ b/BililiveRecorder.FlvProcessor/FlvStreamProcessor.cs @@ -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)["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; diff --git a/BililiveRecorder.FlvProcessor/FlvTag.cs b/BililiveRecorder.FlvProcessor/FlvTag.cs index 69cbd3e..99e3691 100644 --- a/BililiveRecorder.FlvProcessor/FlvTag.cs +++ b/BililiveRecorder.FlvProcessor/FlvTag.cs @@ -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]; diff --git a/BililiveRecorder.FlvProcessor/IFlvMetadata.cs b/BililiveRecorder.FlvProcessor/IFlvMetadata.cs index d790de5..c5fbf03 100644 --- a/BililiveRecorder.FlvProcessor/IFlvMetadata.cs +++ b/BililiveRecorder.FlvProcessor/IFlvMetadata.cs @@ -2,9 +2,8 @@ namespace BililiveRecorder.FlvProcessor { - public interface IFlvMetadata + public interface IFlvMetadata : IDictionary { - IDictionary Meta { get; set; } byte[] ToBytes(); } } diff --git a/BililiveRecorder.FlvProcessor/IFlvStreamProcessor.cs b/BililiveRecorder.FlvProcessor/IFlvStreamProcessor.cs index ed28a87..3e95425 100644 --- a/BililiveRecorder.FlvProcessor/IFlvStreamProcessor.cs +++ b/BililiveRecorder.FlvProcessor/IFlvStreamProcessor.cs @@ -7,6 +7,7 @@ namespace BililiveRecorder.FlvProcessor { event TagProcessedEvent TagProcessed; event StreamFinalizedEvent StreamFinalized; + event FlvMetadataEvent OnMetaData; int TotalMaxTimestamp { get; } int CurrentMaxTimestamp { get; } diff --git a/VERSION b/VERSION index 9c1218c..1b87bcd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.3 \ No newline at end of file +1.1.4 \ No newline at end of file