mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-16 11:42:22 +08:00
FLV: 重写了时间戳调整规则,并调整了测试
This commit is contained in:
parent
5a069f1985
commit
3580313bd8
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user