FLV: 重写了时间戳调整规则,并调整了测试

This commit is contained in:
Genteure 2021-03-08 23:31:24 +08:00
parent 5a069f1985
commit 3580313bd8
7 changed files with 132 additions and 73 deletions

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace BililiveRecorder.Flv.Pipeline
{
@ -38,18 +39,23 @@ namespace BililiveRecorder.Flv.Pipeline
public static class FlvProcessingContextExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void AddComment(this FlvProcessingContext context, ProcessingComment comment)
=> context.Comments.Add(comment);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void AddNewFileAtStart(this FlvProcessingContext context)
=> context.Output.Insert(0, PipelineNewFileAction.Instance);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void AddNewFileAtEnd(this FlvProcessingContext context)
=> context.Output.Add(PipelineNewFileAction.Instance);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void AddDisconnectAtStart(this FlvProcessingContext context)
=> context.Output.Insert(0, PipelineDisconnectAction.Instance);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ClearOutput(this FlvProcessingContext context)
=> context.Output.Clear();
}

View File

@ -5,12 +5,12 @@ namespace BililiveRecorder.Flv.Pipeline
{
public class PipelineDataAction : PipelineAction
{
public PipelineDataAction(IList<Tag> tags)
public PipelineDataAction(List<Tag> tags)
{
this.Tags = tags ?? throw new ArgumentNullException(nameof(tags));
}
public IList<Tag> Tags { get; set; }
public List<Tag> Tags { get; set; }
public override PipelineAction Clone() => new PipelineDataAction(new List<Tag>(this.Tags));
}

View File

@ -58,6 +58,10 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
await next(localContext).ConfigureAwait(false);
context.Output.AddRange(localContext.Output);
context.Comments.AddRange(localContext.Comments);
// TODO fix me
//var oi = context.Output.IndexOf(dataAction);
//context.Output.Insert(oi,newHeaderAction);
}
}
}

View File

@ -1,4 +1,3 @@
using System;
using System.Threading.Tasks;
namespace BililiveRecorder.Flv.Pipeline.Rules
@ -6,22 +5,14 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
/// <summary>
/// 处理 end tag
/// </summary>
public class HandleEndTagRule : ISimpleProcessingRule
public class HandleEndTagRule : IFullProcessingRule
{
public Task RunAsync(FlvProcessingContext context, Func<Task> next)
public Task RunAsync(FlvProcessingContext context, ProcessingDelegate next)
{
if (context.OriginalInput is PipelineEndAction end)
{
if (context.SessionItems.TryGetValue(UpdateTimestampRule.TS_STORE_KEY, out var obj) && obj is UpdateTimestampRule.TimestampStore store)
end.Tag.Timestamp -= store.CurrentOffset;
else
end.Tag.Timestamp = 0;
if (context.OriginalInput is PipelineEndAction)
context.AddNewFileAtEnd();
return Task.CompletedTask;
}
else
return next();
return next(context);
}
}
}

View File

@ -95,7 +95,7 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
if (lastAudioHeader is null && lastVideoHeader is null)
{
// 本 session 第一次输出 header
context.Output.Clear();
context.ClearOutput();
var output = new PipelineHeaderAction(Array.Empty<Tag>())
{
@ -120,7 +120,7 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
if (split_file)
context.AddComment(SplitFileComment);
context.Output.Clear();
context.ClearOutput();
if (split_file)
{

View File

@ -7,7 +7,7 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
{
public class UpdateTimestampRule : ISimpleProcessingRule
{
public const string TS_STORE_KEY = "Timestamp_Store_Key";
private const string TS_STORE_KEY = "Timestamp_Store_Key";
private const int JUMP_THRESHOLD = 50;
@ -21,7 +21,7 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
public async Task RunAsync(FlvProcessingContext context, Func<Task> next)
{
await next();
await next().ConfigureAwait(false);
var ts = context.SessionItems.ContainsKey(TS_STORE_KEY) ? context.SessionItems[TS_STORE_KEY] as TimestampStore ?? new TimestampStore() : new TimestampStore();
context.SessionItems[TS_STORE_KEY] = ts;
@ -32,6 +32,19 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
{
this.SetDataTimestamp(dataAction.Tags, ts, context);
}
else if (action is PipelineEndAction endAction)
{
var tag = endAction.Tag;
var diff = tag.Timestamp - ts.LastOriginal;
if (diff < 0 || diff > JUMP_THRESHOLD)
{
tag.Timestamp = ts.NextTimestampTarget;
}
else
{
tag.Timestamp -= ts.CurrentOffset;
}
}
else if (action is PipelineNewFileAction)
{
ts.Reset();
@ -54,79 +67,74 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
private static readonly ProcessingComment SkippingComment = new ProcessingComment(CommentType.TimestampJump, "未检测到音频数据,跳过时间戳修改", skipCounting: true);
private void SetDataTimestamp(IList<Tag> tags, TimestampStore ts, FlvProcessingContext context)
private void SetDataTimestamp(IReadOnlyList<Tag> tags, TimestampStore ts, FlvProcessingContext context)
{
// 检查有至少一个音频数据
// 在 CheckMissingKeyframeRule 已经确认了有视频数据不需要重复检查
if (!tags.Any(x => x.Type == TagType.Audio))
{
context.AddComment(SkippingComment);
return;
}
var diff = tags[0].Timestamp - ts.LastOriginal;
if (diff < 0)
{
var offsetDiff = this.GetOffsetDiff(tags, ts);
context.AddComment(new ProcessingComment(CommentType.TimestampJump, "时间戳问题:变小, Offset Diff: " + offsetDiff));
ts.CurrentOffset += offsetDiff;
context.AddComment(new ProcessingComment(CommentType.TimestampJump, "时间戳问题:变小, Diff: " + diff));
ts.CurrentOffset = tags[0].Timestamp - ts.NextTimestampTarget;
}
else if (diff > JUMP_THRESHOLD)
{
var offsetDiff = this.GetOffsetDiff(tags, ts);
context.AddComment(new ProcessingComment(CommentType.TimestampJump, "时间戳问题:间隔过大, Offset Diff: " + offsetDiff));
ts.CurrentOffset += offsetDiff;
context.AddComment(new ProcessingComment(CommentType.TimestampJump, "时间戳问题:间隔过大, Diff: " + diff));
ts.CurrentOffset = tags[0].Timestamp - ts.NextTimestampTarget;
}
ts.LastVideoOriginal = tags.Last(x => x.Type == TagType.Video).Timestamp;
ts.LastAudioOriginal = tags.Last(x => x.Type == TagType.Audio).Timestamp;
ts.LastOriginal = Math.Max(ts.LastVideoOriginal, ts.LastAudioOriginal);
ts.LastOriginal = tags.Last().Timestamp;
foreach (var tag in tags)
tag.Timestamp -= ts.CurrentOffset;
ts.NextTimestampTarget = this.CalculateNewTarget(tags);
}
private int GetOffsetDiff(IList<Tag> tags, TimestampStore ts)
private int CalculateNewTarget(IReadOnlyList<Tag> tags)
{
var videoDiff = this.GetAudioOrVideoOffsetDiff(tags.Where(x => x.Type == TagType.Video).Take(2).ToArray(),
ts.LastVideoOriginal, t => t >= VIDEO_DURATION_MIN && t <= VIDEO_DURATION_MAX, VIDEO_DURATION_FALLBACK);
var video = CalculatePerChannel(tags, VIDEO_DURATION_FALLBACK, VIDEO_DURATION_MAX, VIDEO_DURATION_MIN, TagType.Video);
var audioDiff = this.GetAudioOrVideoOffsetDiff(tags.Where(x => x.Type == TagType.Audio).Take(2).ToArray(),
ts.LastAudioOriginal, t => t >= AUDIO_DURATION_MIN && t <= AUDIO_DURATION_MAX, AUDIO_DURATION_FALLBACK);
if (tags.Any(x => x.Type == TagType.Audio))
{
var audio = CalculatePerChannel(tags, AUDIO_DURATION_FALLBACK, AUDIO_DURATION_MAX, AUDIO_DURATION_MIN, TagType.Audio);
return Math.Max(video, audio);
}
else
{
return video;
}
return Math.Min(videoDiff, audioDiff);
static int CalculatePerChannel(IReadOnlyList<Tag> tags, int fallback, int max, int min, TagType type)
{
var sample = tags.Where(x => x.Type == type).Take(2).ToArray();
int durationPerTag;
if (sample.Length != 2)
{
durationPerTag = fallback;
}
else
{
durationPerTag = sample[1].Timestamp - sample[0].Timestamp;
if (durationPerTag < min || durationPerTag > max)
durationPerTag = fallback;
}
return durationPerTag + tags.Last(x => x.Type == type).Timestamp;
}
}
private int GetAudioOrVideoOffsetDiff(Tag[] sample, int lastTimestamp, Func<int, bool> validFunc, int fallbackDuration)
private class TimestampStore
{
if (sample.Length <= 1)
return sample[0].Timestamp - lastTimestamp - fallbackDuration;
public int NextTimestampTarget;
var duration = sample[1].Timestamp - sample[0].Timestamp;
var valid = validFunc(duration);
if (!valid)
duration = fallbackDuration;
return sample[0].Timestamp - lastTimestamp - duration;
}
public class TimestampStore
{
public int LastOriginal;
public int LastVideoOriginal;
public int LastAudioOriginal;
public int CurrentOffset;
public void Reset()
{
this.NextTimestampTarget = 0;
this.LastOriginal = 0;
this.LastVideoOriginal = 0;
this.LastAudioOriginal = 0;
this.CurrentOffset = 0;
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BililiveRecorder.Flv.Grouping;
using BililiveRecorder.Flv.Pipeline;
@ -24,26 +25,75 @@ namespace BililiveRecorder.Flv.RuleTests.Integrated
// Assert
Assert.Empty(comments);
Assert.Empty(output.AlternativeHeaders);
Assert.Single(output.Files);
Assert.Equal(original.Count, output.Files[0].Count);
var file = output.Files[0];
Assert.Empty(output.AlternativeHeaders);
var file = Assert.Single(output.Files);
Assert.Equal(original.Count, file.Count);
AssertTags(original, file);
}
[Theory]
[SampleFileTestData("samples/good-strict")]
public async Task StrictWithArtificalOffsetTestsAsync(string path)
{
// Arrange
var original = this.LoadFile(path).Tags;
var offset = new System.Random().Next(-9999, 9999);
var inputTags = this.LoadFile(path).Tags;
foreach (var tag in inputTags)
tag.Timestamp += offset;
var reader = new TagGroupReader(new FlvTagListReader(inputTags));
var output = new FlvTagListWriter();
var comments = new List<ProcessingComment>();
// Act
await this.RunPipeline(reader, output, comments).ConfigureAwait(false);
// Assert
Assert.Equal(CommentType.TimestampJump, Assert.Single(comments).CommentType);
Assert.Empty(output.AlternativeHeaders);
var file = Assert.Single(output.Files);
Assert.Equal(original.Count, file.Count);
AssertTags(original, file);
}
private static void AssertTags(List<Tag> original, List<Tag> file)
{
Assert.Single(file.Where(x => x.Type == TagType.Script));
Assert.Single(file.Where(x => x.Type == TagType.Audio && x.Flag == TagFlag.Header));
Assert.Single(file.Where(x => x.Type == TagType.Video && x.Flag == (TagFlag.Header | TagFlag.Keyframe)));
for (var i = 0; i < original.Count; i++)
{
var a = original[i];
var b = file[i];
Assert.NotSame(a, b);
Assert.Equal(a.Type, b.Type);
Assert.Equal(a.Timestamp, a.Timestamp);
Assert.Equal(a.Flag, b.Flag);
if (a.IsHeader())
if (a.IsScript())
{
Assert.Equal(a.BinaryDataForSerializationUseOnly, b.BinaryDataForSerializationUseOnly);
Assert.Equal(0, b.Timestamp);
}
else if (!a.IsScript())
else if (a.IsEnd())
{
}
else if (a.IsHeader())
{
Assert.Equal(0, b.Timestamp);
var binaryDataForSerializationUseOnly = a.BinaryDataForSerializationUseOnly;
Assert.False(string.IsNullOrWhiteSpace(binaryDataForSerializationUseOnly));
Assert.Equal(binaryDataForSerializationUseOnly, b.BinaryDataForSerializationUseOnly);
}
else
{
Assert.Equal(a.Timestamp, b.Timestamp);
Assert.Equal(a.Index, b.Index);
}
}