Preliminary work on calculating composition time offset

This commit is contained in:
genteure 2022-01-14 00:39:31 +08:00
parent 2225ca2dd3
commit 018390a065
13 changed files with 5593 additions and 1118 deletions

View File

@ -0,0 +1,66 @@
using System;
using System.IO;
namespace BililiveRecorder.Flv
{
internal static class BinaryConvertUtilities
{
private static readonly uint[] _lookup32 = CreateLookup32();
private static uint[] CreateLookup32()
{
var result = new uint[256];
for (var i = 0; i < 256; i++)
{
var s = i.ToString("X2");
result[i] = s[0] + ((uint)s[1] << 16);
}
return result;
}
internal static string ByteArrayToHexString(byte[] bytes) => ByteArrayToHexString(bytes, 0, bytes.Length);
internal static string ByteArrayToHexString(byte[] bytes, int start, int length)
{
var lookup32 = _lookup32;
var result = new char[length * 2];
for (var i = start; i < length; i++)
{
var val = lookup32[bytes[i]];
result[2 * i] = (char)val;
result[2 * i + 1] = (char)(val >> 16);
}
return new string(result);
}
internal static byte[] HexStringToByteArray(string hex)
{
var bytes = new byte[hex.Length / 2];
for (var i = 0; i < hex.Length; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
internal static string StreamToHexString(Stream stream)
{
var lookup32 = _lookup32;
stream.Seek(0, SeekOrigin.Begin);
var result = new char[stream.Length * 2];
for (var i = 0; i < stream.Length; i++)
{
var val = lookup32[stream.ReadByte()];
result[2 * i] = (char)val;
result[2 * i + 1] = (char)(val >> 16);
}
return new string(result);
}
internal static MemoryStream HexStringToMemoryStream(string hex)
{
var stream = new MemoryStream(hex.Length / 2);
for (var i = 0; i < hex.Length; i += 2)
stream.WriteByte(Convert.ToByte(hex.Substring(i, 2), 16));
return stream;
}
}
}

View File

@ -0,0 +1,58 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
namespace BililiveRecorder.Flv
{
public class Int24
{
public static int ReadInt24(ReadOnlySpan<byte> source)
{
if (source.Length < 3)
throw new ArgumentException("source must longer than 3 bytes", nameof(source));
const int mask = -16777216;
var buffer = ArrayPool<byte>.Shared.Rent(4);
try
{
buffer[0] = 0;
buffer[1] = source[0];
buffer[2] = source[1];
buffer[3] = source[2];
var value = BinaryPrimitives.ReadInt32BigEndian(buffer);
if ((value & 0x00800000) > 0)
value |= mask;
else
value &= ~mask;
return value;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
public static void WriteInt24(Span<byte> destination, int value)
{
if (value is > 8388607 or < -8388608)
throw new ArgumentOutOfRangeException(nameof(value), "int24 should be between -8388608 and 8388607");
var buffer = ArrayPool<byte>.Shared.Rent(4);
try
{
BinaryPrimitives.WriteInt32BigEndian(buffer, value);
destination[0] = buffer[1];
destination[1] = buffer[2];
destination[2] = buffer[3];
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}
}

View File

@ -57,7 +57,7 @@ namespace BililiveRecorder.Flv.Parser
/// 实现二进制数据的解析
/// </summary>
/// <returns>解析出的 Flv Tag</returns>
private async Task<Tag?> ReadNextTagAsync(CancellationToken cancellationToken = default)
private async Task<Tag?> ReadNextTagAsync(CancellationToken cancellationToken)
{
while (true)
{
@ -291,11 +291,15 @@ namespace BililiveRecorder.Flv.Parser
tag.Nalus = nalus;
}
tag.BinaryData = tagBodyStream;
tag.UpdateExtraData();
// Dispose Stream If Not Needed
if (!this.skipData || tag.ShouldSerializeBinaryDataForSerializationUseOnly())
tag.BinaryData = tagBodyStream;
else
if (this.skipData && !tag.ShouldSerializeBinaryDataForSerializationUseOnly())
{
tag.BinaryData = null;
tagBodyStream.Dispose();
}
return true;
}

View File

@ -26,7 +26,7 @@ namespace BililiveRecorder.Flv.Pipeline
builder
.Add<HandleEndTagRule>()
.Add<HandleDelayedAudioHeaderRule>()
//.Add<UpdateCompositionTimeRule>()
.Add<UpdateCompositionTimeRule>()
.Add<UpdateTimestampOffsetRule>()
.Add<UpdateTimestampJumpRule>()
.Add<HandleNewScriptRule>()

View File

@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv.Pipeline.Rules
{
@ -6,7 +9,55 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
{
public void Run(FlvProcessingContext context, Action next)
{
context.PerActionRun(this.RunPerAction);
next();
}
private IEnumerable<PipelineAction?> RunPerAction(FlvProcessingContext context, PipelineAction action)
{
if (action is not PipelineDataAction data)
{
yield return action;
yield break;
}
var videoTags = data.Tags.Where(x => x.Type == TagType.Video);
if (!videoTags.Any())
{
// skip
yield return data;
yield break;
}
if (videoTags.Any(x => x.ExtraData is null))
{
context.AddComment(new ProcessingComment(CommentType.Unrepairable, "有 Tag 的 ExtraData 为 null请检查文件或联系开发者"));
yield break;
}
var compositionOffset = videoTags.Min(x => x.ExtraData!.CompositionTime);
if (compositionOffset is <= 0 or >= int.MaxValue)
{
// skip
yield return data;
yield break;
}
else
{
foreach (var tag in data.Tags)
{
if (tag.Type != TagType.Video)
continue;
System.Diagnostics.Debug.WriteLine("CompositionOffset: " + compositionOffset);
tag.ExtraData!.CompositionTime -= compositionOffset;
tag.Timestamp += compositionOffset;
}
yield return data;
yield break;
}
}
}
}

View File

@ -1,6 +1,4 @@
using System;
using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
@ -8,7 +6,6 @@ using System.IO;
using System.Runtime.CompilerServices;
using System.Xml.Serialization;
using BililiveRecorder.Flv.Amf;
using FastHashes;
namespace BililiveRecorder.Flv
{
@ -108,79 +105,6 @@ namespace BililiveRecorder.Flv
};
}
private static readonly FarmHash64 farmHash64 = new();
public TagExtraData? UpdateExtraData()
{
if (this.BinaryData is not { } binaryData || binaryData.Length < 5)
{
this.ExtraData = null;
}
else
{
var old_position = binaryData.Position;
var extra = new TagExtraData();
binaryData.Position = 0;
var buffer = ArrayPool<byte>.Shared.Rent(5);
try
{
binaryData.Read(buffer, 0, 5);
extra.FirstBytes = BinaryConvertUtilities.ByteArrayToHexString(buffer, 0, 2);
if (this.Type == TagType.Video)
{
buffer[1] = 0;
const int mask = -16777216;
var value = BinaryPrimitives.ReadInt32BigEndian(buffer.AsSpan(1));
if ((value & 0x00800000) > 0)
value |= mask;
else
value &= ~mask;
extra.CompositionTime = value;
extra.FinalTime = this.Timestamp + value;
}
else
{
extra.CompositionTime = int.MinValue;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
binaryData.Position = old_position;
this.ExtraData = extra;
}
return this.ExtraData;
}
public string? UpdateDataHash()
{
if (this.BinaryData is null)
{
this.DataHash = null;
}
else
{
var buffer = this.BinaryData.GetBuffer();
this.DataHash = BinaryConvertUtilities.ByteArrayToHexString(farmHash64.ComputeHash(buffer, (int)this.BinaryData.Length));
if (this.Nalus?.Count > 0)
{
foreach (var nalu in this.Nalus)
{
nalu.NaluHash = BinaryConvertUtilities.ByteArrayToHexString(farmHash64.ComputeHash(buffer, nalu.StartPosition, (int)nalu.FullSize));
}
}
}
return this.DataHash;
}
private string DebuggerDisplay => string.Format("{0}, {1}{2}{3}, TS={4}, Size={5}",
this.Type switch
{
@ -194,66 +118,5 @@ namespace BililiveRecorder.Flv
this.Flag.HasFlag(TagFlag.End) ? "E" : "-",
this.Timestamp,
this.Size);
private static class BinaryConvertUtilities
{
private static readonly uint[] _lookup32 = CreateLookup32();
private static uint[] CreateLookup32()
{
var result = new uint[256];
for (var i = 0; i < 256; i++)
{
var s = i.ToString("X2");
result[i] = s[0] + ((uint)s[1] << 16);
}
return result;
}
internal static string ByteArrayToHexString(byte[] bytes) => ByteArrayToHexString(bytes, 0, bytes.Length);
internal static string ByteArrayToHexString(byte[] bytes, int start, int length)
{
var lookup32 = _lookup32;
var result = new char[length * 2];
for (var i = start; i < length; i++)
{
var val = lookup32[bytes[i]];
result[2 * i] = (char)val;
result[2 * i + 1] = (char)(val >> 16);
}
return new string(result);
}
internal static byte[] HexStringToByteArray(string hex)
{
var bytes = new byte[hex.Length / 2];
for (var i = 0; i < hex.Length; i += 2)
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
return bytes;
}
internal static string StreamToHexString(Stream stream)
{
var lookup32 = _lookup32;
stream.Seek(0, SeekOrigin.Begin);
var result = new char[stream.Length * 2];
for (var i = 0; i < stream.Length; i++)
{
var val = lookup32[stream.ReadByte()];
result[2 * i] = (char)val;
result[2 * i + 1] = (char)(val >> 16);
}
return new string(result);
}
internal static MemoryStream HexStringToMemoryStream(string hex)
{
var stream = new MemoryStream(hex.Length / 2);
for (var i = 0; i < hex.Length; i += 2)
stream.WriteByte(Convert.ToByte(hex.Substring(i, 2), 16));
return stream;
}
}
}
}

View File

@ -4,6 +4,7 @@ using System.Buffers.Binary;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using FastHashes;
namespace BililiveRecorder.Flv
{
@ -43,24 +44,34 @@ namespace BililiveRecorder.Flv
Stream? data = null;
try
{
// 先准备 Tag 的 body 部分
if (tag.IsScript())
{
// Script Tag 写入时使用 Script Tag 序列化
if (tag.ScriptData is null)
throw new Exception("BinaryData is null");
throw new Exception("ScriptData is null");
data = memoryStreamProvider?.CreateMemoryStream(nameof(TagExtentions) + ":" + nameof(WriteTo) + ":TagBodyTemp") ?? new MemoryStream();
tag.ScriptData.WriteTo(data);
}
else if (tag.Nalus != null)
{
// 如果 Tag 有 Nalu 信息,则按照 Nalus 里面的指示分段复制
// 这个 Tag 一定是 Video
if (tag.BinaryData is null)
throw new Exception("BinaryData is null");
data = memoryStreamProvider?.CreateMemoryStream(nameof(TagExtentions) + ":" + nameof(WriteTo) + ":TagBodyTemp") ?? new MemoryStream();
tag.BinaryData.Seek(0, SeekOrigin.Begin);
tag.BinaryData.Read(buffer, 0, 5);
if (tag.ExtraData is not null)
{
// 如果有 ExtraData 则以这里面的 composition time 为准
Int24.WriteInt24(buffer.AsSpan(2, 3), tag.ExtraData.CompositionTime);
}
data.Write(buffer, 0, 5);
foreach (var nalu in tag.Nalus)
@ -77,12 +88,27 @@ namespace BililiveRecorder.Flv
if (tag.BinaryData is null)
throw new Exception("BinaryData is null");
dispose = false;
data = tag.BinaryData;
if (tag.Type == TagType.Video && tag.ExtraData is not null)
{
// 复制并修改 composition time
data = memoryStreamProvider?.CreateMemoryStream(nameof(TagExtentions) + ":" + nameof(WriteTo) + ":TagBodyTemp") ?? new MemoryStream();
tag.BinaryData.CopyTo(data);
Int24.WriteInt24(buffer.AsSpan(0, 3), tag.ExtraData.CompositionTime);
data.Seek(2, SeekOrigin.Begin);
data.Read(buffer, 0, 3);
}
else
{
// 直接复用原数据
dispose = false;
data = tag.BinaryData;
}
}
data.Seek(0, SeekOrigin.Begin);
// 序列号 Tag 的 header 部分
BinaryPrimitives.WriteUInt32BigEndian(new Span<byte>(buffer, 0, 4), (uint)data.Length);
buffer[0] = (byte)tag.Type;
@ -100,6 +126,7 @@ namespace BililiveRecorder.Flv
buffer[9] = 0;
buffer[10] = 0;
// 写入
await target.WriteAsync(buffer, 0, 11).ConfigureAwait(false);
await data.CopyToAsync(target).ConfigureAwait(false);
@ -113,5 +140,69 @@ namespace BililiveRecorder.Flv
data?.Dispose();
}
}
public static TagExtraData? UpdateExtraData(this Tag tag)
{
if (tag.BinaryData is not { } binaryData || binaryData.Length < 5)
{
tag.ExtraData = null;
}
else
{
var old_position = binaryData.Position;
var extra = new TagExtraData();
binaryData.Position = 0;
var buffer = ArrayPool<byte>.Shared.Rent(5);
try
{
binaryData.Read(buffer, 0, 5);
extra.FirstBytes = BinaryConvertUtilities.ByteArrayToHexString(buffer, 0, 2);
if (tag.Type == TagType.Video)
{
extra.CompositionTime = Int24.ReadInt24(buffer.AsSpan(2, 3));
extra.FinalTime = tag.Timestamp + extra.CompositionTime;
}
else
{
extra.CompositionTime = int.MinValue;
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
binaryData.Position = old_position;
tag.ExtraData = extra;
}
return tag.ExtraData;
}
private static readonly FarmHash64 farmHash64 = new();
public static string? UpdateDataHash(this Tag tag)
{
if (tag.BinaryData is null)
{
tag.DataHash = null;
}
else
{
var buffer = tag.BinaryData.GetBuffer();
tag.DataHash = BinaryConvertUtilities.ByteArrayToHexString(farmHash64.ComputeHash(buffer, (int)tag.BinaryData.Length));
if (tag.Nalus?.Count > 0)
{
foreach (var nalu in tag.Nalus)
{
nalu.NaluHash = BinaryConvertUtilities.ByteArrayToHexString(farmHash64.ComputeHash(buffer, nalu.StartPosition, (int)nalu.FullSize));
}
}
}
return tag.DataHash;
}
}
}

View File

@ -30,8 +30,7 @@ namespace BililiveRecorder.ToolBox.ProcessingRules
if (tag.ExtraData is { } extra)
this.frameComposition.Add(extra.CompositionTime);
else if (tag.UpdateExtraData() is { } extra2)
this.frameComposition.Add(extra2.CompositionTime);
}
else if (tag.Type == TagType.Audio && tag.Flag == TagFlag.None)
{

View File

@ -73,7 +73,6 @@ namespace BililiveRecorder.ToolBox.Tool.Export
if (tag is null)
break;
tag.UpdateExtraData();
tag.UpdateDataHash();
if (!tag.ShouldSerializeBinaryDataForSerializationUseOnly())
tag.BinaryData?.Dispose();

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using Xunit;
namespace BililiveRecorder.Flv.Tests.FlvTests
{
public class Int24Tests
{
private static IEnumerable<object[]> TestData()
{
yield return new object[] { 0, new byte[] { 0, 0, 0 } };
yield return new object[] { 1, new byte[] { 0, 0, 1 } };
yield return new object[] { -1, new byte[] { 0xFF, 0xFF, 0xFF } };
yield return new object[] { -8388608, new byte[] { 0x80, 0, 0 } };
yield return new object[] { 8388607, new byte[] { 0x7F, 0xFF, 0xFF } };
yield return new object[] { -5517841, new byte[] { 0xAB, 0xCD, 0xEF } };
}
[Theory, MemberData(nameof(TestData))]
public void Int24SerializeCorrectly(int number, byte[] bytes)
{
var result = new byte[3];
Int24.WriteInt24(result, number);
Assert.Equal(bytes, result);
}
[Theory, MemberData(nameof(TestData))]
public void Int24DeserializeCorrectly(int number, byte[] bytes)
{
var result = Int24.ReadInt24(bytes);
Assert.Equal(number, result);
}
[Theory]
[InlineData(8388608)]
[InlineData(-8388609)]
public void Int24ThrowOnOutOfRange(int number)
{
Assert.Throws<ArgumentOutOfRangeException>(() =>
{
var result = new byte[3];
Int24.WriteInt24(result, number);
});
}
}
}

View File

@ -11,7 +11,7 @@ namespace BililiveRecorder.Flv.Tests.FlvTests
{
[UsesVerify]
[ExpectationPath("FlvParser")]
public class ParserTest
public class ParserTests
{
[Theory]
[Expectation("XmlOutput")]