mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-15 19:22:19 +08:00
~
This commit is contained in:
parent
2459867f8f
commit
0abe31752f
|
@ -26,6 +26,7 @@ namespace BililiveRecorder.Core
|
|||
public RecordedRoom()
|
||||
{
|
||||
Processor.TagProcessed += Processor_TagProcessed;
|
||||
Processor.StreamFinalized += Processor_StreamFinalized;
|
||||
streamMonitor.StreamStatusChanged += StreamMonitor_StreamStatusChanged;
|
||||
|
||||
UpdateRoomInfo();
|
||||
|
@ -153,8 +154,6 @@ namespace BililiveRecorder.Core
|
|||
public void Clip()
|
||||
{
|
||||
var clip = Processor.Clip();
|
||||
// TODO: 多个线程同时运行,这个位置有可能会导致 Clip 丢数据
|
||||
// 考虑在此处加锁, Clip 操作时停止向主 Processor 添加数据
|
||||
clip.ClipFinalized += CallBack_ClipFinalized;
|
||||
Clips.Add(clip);
|
||||
}
|
||||
|
@ -173,7 +172,12 @@ namespace BililiveRecorder.Core
|
|||
|
||||
private void Processor_TagProcessed(object sender, TagProcessedArgs e)
|
||||
{
|
||||
Clips.ToList().ForEach((fcp) => fcp.AddTag(e.Tag));
|
||||
Clips.ToList().ForEach(fcp => fcp.AddTag(e.Tag));
|
||||
}
|
||||
|
||||
private void Processor_StreamFinalized(object sender, StreamFinalizedArgs e)
|
||||
{
|
||||
Clips.ToList().ForEach(fcp => fcp.FinallizeFile());
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -17,4 +17,10 @@ namespace BililiveRecorder.FlvProcessor
|
|||
public FlvClipProcessor ClipProcessor;
|
||||
}
|
||||
|
||||
public delegate void StreamFinalizedEvent(object sender, StreamFinalizedArgs e);
|
||||
public class StreamFinalizedArgs
|
||||
{
|
||||
public FlvStreamProcessor StreamProcessor;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BililiveRecorder.FlvProcessor
|
||||
|
@ -7,17 +8,58 @@ namespace BililiveRecorder.FlvProcessor
|
|||
public class FlvClipProcessor : IDisposable
|
||||
{
|
||||
public readonly FlvMetadata Header;
|
||||
public List<FlvTag> Tags;
|
||||
private int target = -1;
|
||||
|
||||
public FlvClipProcessor(FlvMetadata header)
|
||||
public FlvClipProcessor(FlvMetadata header, List<FlvTag> past, int future)
|
||||
{
|
||||
Header = header;
|
||||
Tags = past;
|
||||
target = Tags[Tags.Count - 1].TimeStamp + future;
|
||||
}
|
||||
|
||||
public void AddTag(FlvTag tag)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
Tags.Add(tag);
|
||||
if (tag.TimeStamp >= target)
|
||||
{
|
||||
FinallizeFile();
|
||||
}
|
||||
}
|
||||
|
||||
public void FinallizeFile()
|
||||
{
|
||||
using (var fs = new FileStream("", FileMode.CreateNew, FileAccess.ReadWrite))
|
||||
{
|
||||
fs.Write(FlvStreamProcessor.FLV_HEADER_BYTES, 0, FlvStreamProcessor.FLV_HEADER_BYTES.Length);
|
||||
|
||||
Header.Meta["duration"] = Tags[Tags.Count - 1].TimeStamp / 1000.0;
|
||||
Header.Meta["lasttimestamp"] = (double)Tags[Tags.Count - 1].TimeStamp;
|
||||
|
||||
var t = new FlvTag();
|
||||
t.TagType = TagType.DATA;
|
||||
t.Data = Header.ToBytes();
|
||||
var b = t.ToBytes();
|
||||
fs.Write(b, 0, b.Length);
|
||||
fs.Write(t.Data, 0, t.Data.Length);
|
||||
fs.Write(BitConverter.GetBytes(t.Data.Length + b.Length).ToBE(), 0, 4);
|
||||
|
||||
int timestamp = Tags[0].TimeStamp;
|
||||
|
||||
Tags.ForEach(tag =>
|
||||
{
|
||||
tag.TimeStamp -= timestamp;
|
||||
var vs = tag.ToBytes();
|
||||
fs.Write(vs, 0, vs.Length);
|
||||
fs.Write(tag.Data, 0, tag.Data.Length);
|
||||
fs.Write(BitConverter.GetBytes(tag.Data.Length + vs.Length).ToBE(), 0, 4);
|
||||
});
|
||||
|
||||
fs.Close();
|
||||
}
|
||||
|
||||
ClipFinalized?.Invoke(this, new ClipFinalizedArgs() { ClipProcessor = this });
|
||||
}
|
||||
|
||||
public event ClipFinalizedEvent ClipFinalized;
|
||||
|
||||
|
|
|
@ -1,14 +1,198 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace BililiveRecorder.FlvProcessor
|
||||
{
|
||||
public class FlvMetadata
|
||||
{
|
||||
public IDictionary<string, object> Meta = new Dictionary<string, object>();
|
||||
|
||||
public static FlvMetadata Parse(byte[] data)
|
||||
{
|
||||
var m = new FlvMetadata();
|
||||
|
||||
m.Meta = _Decode(data);
|
||||
|
||||
if (!m.Meta.ContainsKey("duration"))
|
||||
m.Meta["duration"] = 0.0;
|
||||
if (!m.Meta.ContainsKey("lasttimestamp"))
|
||||
m.Meta["lasttimestamp"] = 0.0;
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
return _Encode();
|
||||
}
|
||||
|
||||
|
||||
#region - Encode -
|
||||
|
||||
private byte[] _Encode()
|
||||
{
|
||||
using (MemoryStream 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);
|
||||
|
||||
|
||||
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)
|
||||
{
|
||||
double num = (double)val;
|
||||
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)
|
||||
{
|
||||
byte bit = (byte)val;
|
||||
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 -
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private static object _DecodeVal(byte[] buff, ref int _readHead)
|
||||
{
|
||||
byte type = buff[_readHead++];
|
||||
AMFTypes amfType = (AMFTypes)Enum.ToObject(typeof(AMFTypes), (int)type);
|
||||
switch (amfType)
|
||||
{
|
||||
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;
|
||||
default:
|
||||
throw new MissingMethodException();
|
||||
}
|
||||
}
|
||||
|
||||
private static IDictionary<string, object> _Decode(byte[] buff)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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));
|
||||
}
|
||||
else if (type == (byte)AMFTypes.Object)
|
||||
{
|
||||
Debug.Write("onMetaData isn't an Array but Object!\n");
|
||||
}
|
||||
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
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,32 +11,77 @@ namespace BililiveRecorder.FlvProcessor
|
|||
public class FlvStreamProcessor : IDisposable
|
||||
{
|
||||
private const int MIN_BUFFER_SIZE = 1024 * 2;
|
||||
internal static readonly byte[] FLV_HEADER_BYTES = new byte[]
|
||||
{
|
||||
0x46, // F
|
||||
0x4c, // L
|
||||
0x56, // V
|
||||
0x01, // Version 1
|
||||
0x05, // bit 00000 1 0 1 (have video and audio)
|
||||
0x00, // ---
|
||||
0x00, // |
|
||||
0x00, // |
|
||||
0x09, // total of 9 bytes
|
||||
0x00, // ---
|
||||
0x00, // |
|
||||
0x00, // |
|
||||
0x00, // the "0th" tag has a length of 0
|
||||
};
|
||||
|
||||
public RecordInfo Info; // not used for now.
|
||||
public readonly FlvMetadata Metadata = new FlvMetadata();
|
||||
public FlvMetadata Metadata = null;
|
||||
public event TagProcessedEvent TagProcessed;
|
||||
|
||||
private byte[] headers;
|
||||
public event StreamFinalizedEvent StreamFinalized;
|
||||
|
||||
private bool _headerParsed = false;
|
||||
private readonly List<FlvTag> Tags = new List<FlvTag>();
|
||||
private readonly MemoryStream _buffer = new MemoryStream();
|
||||
private readonly MemoryStream _data = new MemoryStream();
|
||||
private FlvTag currentTag = null;
|
||||
private object _writelock = new object();
|
||||
private bool Finallized = false;
|
||||
|
||||
public FlvStreamProcessor(RecordInfo info)
|
||||
private readonly FileStream _fs;
|
||||
|
||||
public int MaxTimeStamp { get; private set; }
|
||||
|
||||
public FlvStreamProcessor(RecordInfo info, string path)
|
||||
{
|
||||
Info = info;
|
||||
_fs = new FileStream(path, FileMode.CreateNew, FileAccess.ReadWrite);
|
||||
if (!_fs.CanSeek)
|
||||
{
|
||||
_fs.Dispose();
|
||||
try { File.Delete(path); } catch (Exception) { }
|
||||
throw new NotSupportedException("Target File Cannot Seek");
|
||||
}
|
||||
}
|
||||
|
||||
public void AddBytes(byte[] data)
|
||||
{
|
||||
// lock ( ) { _AddBytes() }
|
||||
lock (_writelock)
|
||||
_AddBytes(data);
|
||||
}
|
||||
|
||||
private void _AddBytes(byte[] data)
|
||||
{
|
||||
if (currentTag == null)
|
||||
if (Finallized)
|
||||
{
|
||||
throw new Exception("Stream File Already Closed");
|
||||
}
|
||||
else if (!_headerParsed)
|
||||
{
|
||||
var r = new bool[FLV_HEADER_BYTES.Length];
|
||||
for (int i = 0; i < FLV_HEADER_BYTES.Length; i++)
|
||||
r[i] = data[i] == FLV_HEADER_BYTES[i];
|
||||
bool succ = r.All(x => x);
|
||||
if (!succ)
|
||||
throw new NotSupportedException("Not FLV Stream or Not Supported"); // TODO: custom Exception.
|
||||
|
||||
_headerParsed = true;
|
||||
_AddBytes(data.Skip(FLV_HEADER_BYTES.Length).ToArray());
|
||||
}
|
||||
else if (currentTag == null)
|
||||
{
|
||||
if (_buffer.Position >= MIN_BUFFER_SIZE)
|
||||
{
|
||||
|
@ -65,17 +110,41 @@ namespace BililiveRecorder.FlvProcessor
|
|||
|
||||
private void _TagCreated(FlvTag tag)
|
||||
{
|
||||
tag.TimeStamp -= 0;//TODO: 修复时间戳
|
||||
Tags.Add(tag);
|
||||
// TODO: remove old tag
|
||||
TagProcessed?.Invoke(this, new TagProcessedArgs() { Tag = tag });
|
||||
if (tag.TagType == TagType.DATA)
|
||||
if (Metadata == null)
|
||||
{
|
||||
// TODO: onMetaData
|
||||
if (tag.TagType == TagType.DATA)
|
||||
{
|
||||
_fs.Write(FLV_HEADER_BYTES, 0, FLV_HEADER_BYTES.Length);
|
||||
Metadata = FlvMetadata.Parse(tag.Data);
|
||||
|
||||
// TODO: 添加录播姬标记、录制信息
|
||||
|
||||
tag.Data = Metadata.ToBytes();
|
||||
var b = tag.ToBytes();
|
||||
_fs.Write(b, 0, b.Length);
|
||||
_fs.Write(tag.Data, 0, tag.Data.Length);
|
||||
_fs.Write(BitConverter.GetBytes(tag.Data.Length + b.Length).ToBE(), 0, 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("onMetaData not found");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
tag.TimeStamp -= 0; // TODO: 修复时间戳
|
||||
Tags.Add(tag);
|
||||
// TODO: remove old tag
|
||||
|
||||
var b = tag.ToBytes();
|
||||
_fs.Write(b, 0, b.Length);
|
||||
_fs.Write(tag.Data, 0, tag.Data.Length);
|
||||
_fs.Write(BitConverter.GetBytes(tag.Data.Length + b.Length).ToBE(), 0, 4);
|
||||
|
||||
TagProcessed?.Invoke(this, new TagProcessedArgs() { Tag = tag });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void _ParseTag(byte[] data)
|
||||
{
|
||||
byte[] b = { 0, 0, 0, 0, };
|
||||
|
@ -108,16 +177,47 @@ namespace BililiveRecorder.FlvProcessor
|
|||
_AddBytes(rest);
|
||||
}
|
||||
|
||||
private void _ParseHeader()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public FlvClipProcessor Clip()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
lock (_writelock)
|
||||
{
|
||||
return new FlvClipProcessor(Metadata, Tags, 30);
|
||||
}
|
||||
}
|
||||
|
||||
public void FinallizeFile()
|
||||
{
|
||||
lock (_writelock)
|
||||
{
|
||||
Metadata.Meta["duration"] = MaxTimeStamp / 1000.0;
|
||||
Metadata.Meta["lasttimestamp"] = (double)MaxTimeStamp;
|
||||
byte[] metadata = Metadata.ToBytes();
|
||||
|
||||
// 13 for FLV header & "0th" tag size
|
||||
// 11 for 1st tag header
|
||||
_fs.Seek(13 + 11, SeekOrigin.Begin);
|
||||
_fs.Write(metadata, 0, metadata.Length);
|
||||
|
||||
_fs.Close();
|
||||
_fs.Dispose();
|
||||
|
||||
_buffer.Close();
|
||||
_buffer.Dispose();
|
||||
|
||||
_data.Close();
|
||||
_data.Dispose();
|
||||
|
||||
Tags.Clear();
|
||||
|
||||
Finallized = true;
|
||||
|
||||
StreamFinalized?.Invoke(this, new StreamFinalizedArgs() { StreamProcessor = this });
|
||||
|
||||
// TODO: 通知 Clip 也进行保存
|
||||
// TODO: 阻止再尝试写入任何数据
|
||||
// TODO: 清空 Tags List
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -16,7 +16,18 @@ namespace BililiveRecorder.FlvProcessor
|
|||
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
var tag = new byte[11];
|
||||
tag[0] = (byte)TagType;
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(Data.Length).ToBE(), 0, tag, 1, 3);
|
||||
|
||||
byte[] timing = new byte[4];
|
||||
Buffer.BlockCopy(BitConverter.GetBytes(this.TimeStamp).ToBE(), 0, timing, 0, timing.Length);
|
||||
Buffer.BlockCopy(timing, 1, tag, 4, 3);
|
||||
Buffer.BlockCopy(timing, 0, tag, 7, 1);
|
||||
|
||||
Buffer.BlockCopy(StreamId, 0, tag, 8, 3);
|
||||
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user