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>
|
||||||
</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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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];
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
Loading…
Reference in New Issue
Block a user