mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-16 11:42:22 +08:00
336 lines
12 KiB
C#
336 lines
12 KiB
C#
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 => 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()
|
|
{
|
|
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<string, object>;
|
|
|
|
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<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);
|
|
}
|
|
_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)
|
|
{
|
|
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();
|
|
}
|
|
|
|
|
|
}
|
|
}
|