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 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() { Meta["duration"] = 0.0; 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"); } Meta = DecodeScriptDataValue(data, ref readHead) as Dictionary; if (!Meta.ContainsKey("duration")) { Meta["duration"] = 0d; } if (!Meta.ContainsKey("lasttimestamp")) { 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() { using (var ms = new MemoryStream()) { EncodeScriptDataValue(ms, "onMetaData"); EncodeScriptDataValue(ms, 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 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)); 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(); 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"); } } #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(); } } }