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

View File

@ -5,12 +5,12 @@ namespace BililiveRecorder.Flv.Pipeline
{ {
public class PipelineDataAction : PipelineAction public class PipelineDataAction : PipelineAction
{ {
public PipelineDataAction(IList<Tag> tags) public PipelineDataAction(List<Tag> tags)
{ {
this.Tags = tags ?? throw new ArgumentNullException(nameof(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)); 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); await next(localContext).ConfigureAwait(false);
context.Output.AddRange(localContext.Output); context.Output.AddRange(localContext.Output);
context.Comments.AddRange(localContext.Comments); 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; using System.Threading.Tasks;
namespace BililiveRecorder.Flv.Pipeline.Rules namespace BililiveRecorder.Flv.Pipeline.Rules
@ -6,22 +5,14 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
/// <summary> /// <summary>
/// 处理 end tag /// 处理 end tag
/// </summary> /// </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.OriginalInput is PipelineEndAction)
{
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;
context.AddNewFileAtEnd(); context.AddNewFileAtEnd();
return Task.CompletedTask;
} return next(context);
else
return next();
} }
} }
} }

View File

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

View File

@ -7,7 +7,7 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
{ {
public class UpdateTimestampRule : ISimpleProcessingRule 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; private const int JUMP_THRESHOLD = 50;
@ -21,7 +21,7 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
public async Task RunAsync(FlvProcessingContext context, Func<Task> next) 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(); 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; context.SessionItems[TS_STORE_KEY] = ts;
@ -32,6 +32,19 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
{ {
this.SetDataTimestamp(dataAction.Tags, ts, context); 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) else if (action is PipelineNewFileAction)
{ {
ts.Reset(); ts.Reset();
@ -54,79 +67,74 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
private static readonly ProcessingComment SkippingComment = new ProcessingComment(CommentType.TimestampJump, "未检测到音频数据,跳过时间戳修改", skipCounting: true); 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; var diff = tags[0].Timestamp - ts.LastOriginal;
if (diff < 0) if (diff < 0)
{ {
var offsetDiff = this.GetOffsetDiff(tags, ts); context.AddComment(new ProcessingComment(CommentType.TimestampJump, "时间戳问题:变小, Diff: " + diff));
context.AddComment(new ProcessingComment(CommentType.TimestampJump, "时间戳问题:变小, Offset Diff: " + offsetDiff)); ts.CurrentOffset = tags[0].Timestamp - ts.NextTimestampTarget;
ts.CurrentOffset += offsetDiff;
} }
else if (diff > JUMP_THRESHOLD) else if (diff > JUMP_THRESHOLD)
{ {
var offsetDiff = this.GetOffsetDiff(tags, ts); context.AddComment(new ProcessingComment(CommentType.TimestampJump, "时间戳问题:间隔过大, Diff: " + diff));
context.AddComment(new ProcessingComment(CommentType.TimestampJump, "时间戳问题:间隔过大, Offset Diff: " + offsetDiff)); ts.CurrentOffset = tags[0].Timestamp - ts.NextTimestampTarget;
ts.CurrentOffset += offsetDiff;
} }
ts.LastVideoOriginal = tags.Last(x => x.Type == TagType.Video).Timestamp; ts.LastOriginal = tags.Last().Timestamp;
ts.LastAudioOriginal = tags.Last(x => x.Type == TagType.Audio).Timestamp;
ts.LastOriginal = Math.Max(ts.LastVideoOriginal, ts.LastAudioOriginal);
foreach (var tag in tags) foreach (var tag in tags)
tag.Timestamp -= ts.CurrentOffset; 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(), var video = CalculatePerChannel(tags, VIDEO_DURATION_FALLBACK, VIDEO_DURATION_MAX, VIDEO_DURATION_MIN, TagType.Video);
ts.LastVideoOriginal, t => t >= VIDEO_DURATION_MIN && t <= VIDEO_DURATION_MAX, VIDEO_DURATION_FALLBACK);
var audioDiff = this.GetAudioOrVideoOffsetDiff(tags.Where(x => x.Type == TagType.Audio).Take(2).ToArray(), if (tags.Any(x => x.Type == TagType.Audio))
ts.LastAudioOriginal, t => t >= AUDIO_DURATION_MIN && t <= AUDIO_DURATION_MAX, AUDIO_DURATION_FALLBACK); {
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) public int NextTimestampTarget;
return sample[0].Timestamp - lastTimestamp - fallbackDuration;
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 LastOriginal;
public int LastVideoOriginal;
public int LastAudioOriginal;
public int CurrentOffset; public int CurrentOffset;
public void Reset() public void Reset()
{ {
this.NextTimestampTarget = 0;
this.LastOriginal = 0; this.LastOriginal = 0;
this.LastVideoOriginal = 0;
this.LastAudioOriginal = 0;
this.CurrentOffset = 0; this.CurrentOffset = 0;
} }
} }

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BililiveRecorder.Flv.Grouping; using BililiveRecorder.Flv.Grouping;
using BililiveRecorder.Flv.Pipeline; using BililiveRecorder.Flv.Pipeline;
@ -24,26 +25,75 @@ namespace BililiveRecorder.Flv.RuleTests.Integrated
// Assert // Assert
Assert.Empty(comments); 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++) for (var i = 0; i < original.Count; i++)
{ {
var a = original[i]; var a = original[i];
var b = file[i]; var b = file[i];
Assert.NotSame(a, b);
Assert.Equal(a.Type, b.Type); Assert.Equal(a.Type, b.Type);
Assert.Equal(a.Timestamp, a.Timestamp);
Assert.Equal(a.Flag, b.Flag); 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); Assert.Equal(a.Index, b.Index);
} }
} }