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

View File

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

View File

@ -7,16 +7,63 @@
DATA = 18, DATA = 18,
} }
public enum AMFTypes public enum AMFTypes : byte
{ {
Number = 0x00, // (Encoded as IEEE 64-bit double-precision floating point number) /// <summary>
Boolean = 0x01, // (Encoded as a single byte of value 0x00 or 0x01) /// 非标准类型。在 Decode 过程中作为函数参数使用
String = 0x02, //(ASCII encoded) /// </summary>
Object = 0x03, // (Set of key/value pairs) Any = 0xFF,
Null = 0x05, /// <summary>
Array = 0x08, /// Double
End = 0x09, /// </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 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 delegate void TagProcessedEvent(object sender, TagProcessedArgs e);
public class TagProcessedArgs public class TagProcessedArgs
{ {

View File

@ -9,14 +9,17 @@ namespace BililiveRecorder.FlvProcessor
{ {
private static readonly Logger logger = LogManager.GetCurrentClassLogger(); private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private readonly Func<IFlvTag> funcFlvTag;
public IFlvMetadata Header { get; private set; } public IFlvMetadata Header { get; private set; }
public List<IFlvTag> HTags { get; private set; } public List<IFlvTag> HTags { get; private set; }
public List<IFlvTag> Tags { get; private set; } public List<IFlvTag> Tags { get; private set; }
private int target = -1; private int target = -1;
private string path; 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) 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(FlvStreamProcessor.FLV_HEADER_BYTES, 0, FlvStreamProcessor.FLV_HEADER_BYTES.Length);
fs.Write(new byte[] { 0, 0, 0, 0, }, 0, 4); fs.Write(new byte[] { 0, 0, 0, 0, }, 0, 4);
Header.Meta["duration"] = (Tags[Tags.Count - 1].TimeStamp - Tags[0].TimeStamp) / 1000d; Header["duration"] = (Tags[Tags.Count - 1].TimeStamp - Tags[0].TimeStamp) / 1000d;
Header.Meta["lasttimestamp"] = (Tags[Tags.Count - 1].TimeStamp - Tags[0].TimeStamp); 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, // TODO: 更好的写法
Data = Header.ToBytes() (Header["BililiveRecorder"] as Dictionary<string, object>)["starttime"] = DateTime.UtcNow;
}; }
t.Data = Header.ToBytes();
t.WriteTo(fs); t.WriteTo(fs);
int offset = Tags[0].TimeStamp; int offset = Tags[0].TimeStamp;

View File

@ -1,14 +1,25 @@
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
namespace BililiveRecorder.FlvProcessor namespace BililiveRecorder.FlvProcessor
{ {
public class FlvMetadata : IFlvMetadata 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() public FlvMetadata()
{ {
@ -18,186 +29,299 @@ namespace BililiveRecorder.FlvProcessor
public FlvMetadata(byte[] data) 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")) if (!Meta.ContainsKey("duration"))
{ {
Meta["duration"] = 0.0; Meta["duration"] = 0d;
} }
if (!Meta.ContainsKey("lasttimestamp")) 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() public byte[] ToBytes()
{ {
return _Encode(); using (var ms = new MemoryStream())
}
#region - Encode -
private byte[] _Encode()
{ {
using (MemoryStream ms = new MemoryStream()) EncodeScriptDataValue(ms, "onMetaData");
{ EncodeScriptDataValue(ms, Meta);
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);
return ms.ToArray(); return ms.ToArray();
} }
} }
private byte[] _EncodeKey(string key)
#region Static
private static void EncodeScriptDataValue(MemoryStream ms, object value)
{ {
byte[] ret = new byte[2 + key.Length]; // 2 for the size at the front switch (value)
UInt16 strSize = (UInt16)key.Length; {
byte[] strSizeb = BitConverter.GetBytes(strSize).ToBE(); case double number:
Buffer.BlockCopy(strSizeb, 0, ret, 0, strSizeb.Length); {
Buffer.BlockCopy(Encoding.ASCII.GetBytes(key), 0, ret, 2, key.Length); ms.WriteByte((byte)AMFTypes.Number);
return ret; ms.Write(BitConverter.GetBytes(number).ToBE(), 0, sizeof(double));
break;
} }
private byte[] _EncodeVal(object val) case bool boolean:
{ {
if (val is double num) ms.WriteByte((byte)AMFTypes.Boolean);
if (boolean)
{ {
byte[] ret = new byte[1 + sizeof(double)]; ms.WriteByte(1);
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 else
{ {
Debug.Write(string.Format("Unknown Value type: {0}\n", val?.GetType()?.Name)); ms.WriteByte(0);
return null;
} }
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);
} }
#endregion ms.WriteByte(0);
ms.WriteByte(0);
#region - Decode - ms.WriteByte(9);
break;
private static string _DecodeKey(byte[] buff, ref int _readHead)
{
// 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;
} }
case List<object> l:
private static object _DecodeVal(byte[] buff, ref int _readHead)
{ {
byte type = buff[_readHead++]; ms.WriteByte((byte)AMFTypes.StrictArray);
AMFTypes amfType = (AMFTypes)Enum.ToObject(typeof(AMFTypes), (int)type); ms.Write(BitConverter.GetBytes((uint)l.Count), 0, sizeof(uint));
switch (amfType) foreach (var item in l)
{ {
case AMFTypes.String: EncodeScriptDataValue(ms, item);
return _DecodeKey(buff, ref _readHead); }
case AMFTypes.Number: break;
byte[] flip = new byte[sizeof(double)]; }
Buffer.BlockCopy(buff, _readHead, flip, 0, flip.Length); case DateTime dateTime:
double num = BitConverter.ToDouble(flip.ToBE(), 0); {
_readHead += sizeof(double); ms.WriteByte((byte)AMFTypes.Date);
return num; ms.Write(BitConverter.GetBytes(dateTime.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds).ToBE(), 0, sizeof(double));
case AMFTypes.Boolean: break;
byte b = buff[_readHead++]; }
return b;
case AMFTypes.End:
return null;
default: 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>(); AMFTypes type = (AMFTypes)buff[_readHead++];
int _readHead = 0; if (expectType != AMFTypes.Any && expectType != type)
// 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)
{ {
byte[] alen = new byte[sizeof(int)]; throw new Exception("AMF Decode type error");
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));
} }
else if (type == (byte)AMFTypes.Object)
switch (type)
{ {
Debug.Write("onMetaData isn't an Array but Object!\n"); case AMFTypes.Number:
}
else
{ {
throw new Exception("Parse Script Tag Error"); // TODO: custom Exception 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);
} }
while (_readHead <= buff.Length - 1) case AMFTypes.Boolean:
return buff[_readHead++] != 0;
case AMFTypes.String:
{ {
string key = _DecodeKey(buff, ref _readHead); byte[] bytes = new byte[sizeof(ushort)];
object val = _DecodeVal(buff, ref _readHead); bytes[0] = buff[_readHead++];
Debug.Write(string.Format("Parse Script Tag: {0} => {1}\n", key, val)); bytes[1] = buff[_readHead++];
keyval[key] = val; 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");
} }
return keyval;
} }
#endregion #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 TagProcessedEvent TagProcessed;
public event StreamFinalizedEvent StreamFinalized; public event StreamFinalizedEvent StreamFinalized;
public event FlvMetadataEvent OnMetaData;
public uint ClipLengthPast { get; set; } = 20; public uint ClipLengthPast { get; set; } = 20;
public uint ClipLengthFuture { get; set; } = 10; 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(FlvStreamProcessor.FLV_HEADER_BYTES, 0, FlvStreamProcessor.FLV_HEADER_BYTES.Length);
_targetFile.Write(new byte[] { 0, 0, 0, 0, }, 0, 4); _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, // TODO: 更好的写法
Data = Metadata.ToBytes() (Metadata["BililiveRecorder"] as Dictionary<string, object>)["starttime"] = DateTime.UtcNow;
}.WriteTo(_targetFile); }
script_tag.Data = Metadata.ToBytes();
script_tag.WriteTo(_targetFile);
_headerTags.ForEach(tag => tag.WriteTo(_targetFile)); _headerTags.ForEach(tag => tag.WriteTo(_targetFile));
} }
@ -331,7 +336,7 @@ namespace BililiveRecorder.FlvProcessor
Metadata = funcFlvMetadata(tag.Data); Metadata = funcFlvMetadata(tag.Data);
// TODO: 添加录播姬标记、录制信息 OnMetaData?.Invoke(this, new FlvMetadataArgs() { Metadata = Metadata });
tag.Data = Metadata.ToBytes(); tag.Data = Metadata.ToBytes();
tag.WriteTo(_targetFile); tag.WriteTo(_targetFile);
@ -362,9 +367,9 @@ namespace BililiveRecorder.FlvProcessor
{ {
try try
{ {
logger.Debug("正在关闭当前录制文件: " + _targetFile.Name); logger.Debug("正在关闭当前录制文件: " + _targetFile?.Name);
Metadata.Meta["duration"] = CurrentMaxTimestamp / 1000.0; Metadata["duration"] = CurrentMaxTimestamp / 1000.0;
Metadata.Meta["lasttimestamp"] = (double)CurrentMaxTimestamp; Metadata["lasttimestamp"] = (double)CurrentMaxTimestamp;
byte[] metadata = Metadata.ToBytes(); byte[] metadata = Metadata.ToBytes();
// 13 for FLV header & "0th" tag size // 13 for FLV header & "0th" tag size
@ -419,6 +424,9 @@ namespace BililiveRecorder.FlvProcessor
{ {
_data.Dispose(); _data.Dispose();
_targetFile?.Dispose(); _targetFile?.Dispose();
OnMetaData = null;
StreamFinalized = null;
TagProcessed = null;
} }
_tags.Clear(); _tags.Clear();
disposedValue = true; disposedValue = true;

View File

@ -1,21 +1,75 @@
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
namespace BililiveRecorder.FlvProcessor namespace BililiveRecorder.FlvProcessor
{ {
public class FlvTag : IFlvTag public class FlvTag : IFlvTag
{ {
private int _IsVideoKeyframe = -1;
public TagType TagType { get; set; } = 0; public TagType TagType { get; set; } = 0;
public int TagSize { get; set; } = 0; public int TagSize { get; set; } = 0;
public int TimeStamp { get; private set; } = 0; public int TimeStamp { get; private set; } = 0;
public byte[] StreamId { get; set; } = new byte[3]; 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; 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) public byte[] ToBytes(bool useDataSize, int offset = 0)
{ {
var tag = new byte[11]; var tag = new byte[11];

View File

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

View File

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

View File

@ -1 +1 @@
1.1.3 1.1.4