mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-16 19:57:27 +08:00
commit
a8a75fefa7
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -2,9 +2,8 @@
|
|||
|
||||
namespace BililiveRecorder.FlvProcessor
|
||||
{
|
||||
public interface IFlvMetadata
|
||||
public interface IFlvMetadata : IDictionary<string, object>
|
||||
{
|
||||
IDictionary<string, object> Meta { get; set; }
|
||||
byte[] ToBytes();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ namespace BililiveRecorder.FlvProcessor
|
|||
{
|
||||
event TagProcessedEvent TagProcessed;
|
||||
event StreamFinalizedEvent StreamFinalized;
|
||||
event FlvMetadataEvent OnMetaData;
|
||||
|
||||
int TotalMaxTimestamp { get; }
|
||||
int CurrentMaxTimestamp { get; }
|
||||
|
|
Loading…
Reference in New Issue
Block a user