FLV: Performance improvements

(I guess)
This commit is contained in:
Genteure 2021-04-29 23:51:06 +08:00
parent 5c9706e827
commit 93781b2a56
41 changed files with 448 additions and 465 deletions

View File

@ -17,6 +17,7 @@
<PackageReference Include="Polly" Version="7.2.1" />
<PackageReference Include="Polly.Caching.Memory" Version="3.0.2" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="StructLinq" Version="0.26.0" />
<PackageReference Include="System.IO.Pipelines" Version="5.0.1" />
</ItemGroup>

View File

@ -1,5 +1,6 @@
using System.Threading;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Core.ProcessingRules
{

View File

@ -4,6 +4,8 @@ using System.Linq;
using BililiveRecorder.Core.Event;
using BililiveRecorder.Flv;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.Core.ProcessingRules
{
@ -32,10 +34,20 @@ namespace BililiveRecorder.Core.ProcessingRules
{
var e = new RecordingStatsEventArgs();
{
static IEnumerable<PipelineDataAction> FilterDataActions(IEnumerable<PipelineAction> actions)
{
foreach (var action in actions)
if (action is PipelineDataAction dataAction)
yield return dataAction;
}
e.TotalInputVideoByteCount = this.TotalInputVideoByteCount += e.InputVideoByteCount =
context.Actions.Where(x => x is PipelineDataAction).Cast<PipelineDataAction>().Sum(data => data.Tags.Where(x => x.Type == TagType.Video).Sum(x => x.Size + (11 + 4)));
FilterDataActions(context.Actions).ToStructEnumerable().Sum(ref LinqFunctions.SumSizeOfVideoData, x => x, x => x);
e.TotalInputAudioByteCount = this.TotalInputAudioByteCount += e.InputAudioByteCount =
context.Actions.Where(x => x is PipelineDataAction).Cast<PipelineDataAction>().Sum(data => data.Tags.Where(x => x.Type == TagType.Audio).Sum(x => x.Size + (11 + 4)));
FilterDataActions(context.Actions).ToStructEnumerable().Sum(ref LinqFunctions.SumSizeOfAudioData, x => x, x => x);
}
next();
@ -81,10 +93,17 @@ namespace BililiveRecorder.Core.ProcessingRules
{
if (dataActions.Count > 0)
{
e.TotalOutputVideoFrameCount = this.TotalOutputVideoFrameCount += e.OutputVideoFrameCount = dataActions.Sum(x => x.Tags.Count(x => x.Type == TagType.Video));
e.TotalOutputAudioFrameCount = this.TotalOutputAudioFrameCount += e.OutputAudioFrameCount = dataActions.Sum(x => x.Tags.Count(x => x.Type == TagType.Audio));
e.TotalOutputVideoByteCount = this.TotalOutputVideoByteCount += e.OutputVideoByteCount = dataActions.Sum(x => x.Tags.Where(x => x.Type == TagType.Video).Sum(x => (x.Nalus == null ? x.Size : (5 + x.Nalus.Sum(n => n.FullSize + 4))) + (11 + 4)));
e.TotalOutputAudioByteCount = this.TotalOutputAudioByteCount += e.OutputAudioByteCount = dataActions.Sum(x => x.Tags.Where(x => x.Type == TagType.Audio).Sum(x => x.Size + (11 + 4)));
e.TotalOutputVideoFrameCount = this.TotalOutputVideoFrameCount += e.OutputVideoFrameCount =
dataActions.ToStructEnumerable().Sum(ref LinqFunctions.CountVideoTags, x => x, x => x);
e.TotalOutputAudioFrameCount = this.TotalOutputAudioFrameCount += e.OutputAudioFrameCount =
dataActions.ToStructEnumerable().Sum(ref LinqFunctions.CountAudioTags, x => x, x => x);
e.TotalOutputVideoByteCount = this.TotalOutputVideoByteCount += e.OutputVideoByteCount =
dataActions.ToStructEnumerable().Sum(ref LinqFunctions.SumSizeOfVideoDataByNalu, x => x, x => x);
e.TotalOutputAudioByteCount = this.TotalOutputAudioByteCount += e.OutputAudioByteCount =
dataActions.ToStructEnumerable().Sum(ref LinqFunctions.SumSizeOfAudioData, x => x, x => x);
e.CurrnetFileSize = this.CurrnetFileSize += e.OutputVideoByteCount + e.OutputAudioByteCount;

View File

@ -11,6 +11,7 @@ using BililiveRecorder.Core.ProcessingRules;
using BililiveRecorder.Flv;
using BililiveRecorder.Flv.Amf;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
using Serilog;
namespace BililiveRecorder.Core.Recording

View File

@ -17,6 +17,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="StructLinq" Version="0.26.0" />
<PackageReference Include="System.IO.Pipelines" Version="5.0.1" />
<PackageReference Include="System.Memory" Version="4.5.4" />
</ItemGroup>

View File

@ -1,36 +1,27 @@
using System.Collections.Generic;
using System.Linq;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.Flv.Grouping.Rules
{
public class DataGroupingRule : IGroupingRule
{
public bool StartWith(Tag tag) => tag.IsData();
public bool AppendWith(Tag tag, LinkedList<Tag> tags, out LinkedList<Tag>? leftover)
private readonly struct DoesNotContainAudioData : IFunction<Tag, bool>
{
var shouldAppend =
public static DoesNotContainAudioData Instance;
public bool Eval(Tag element) => element.Type != TagType.Audio || element.Flag == TagFlag.Header;
}
public bool CanStartWith(Tag tag) => tag.IsData();
public bool CanAppendWith(Tag tag, List<Tag> tags) =>
// Tag 是非关键帧数据
tag.IsNonKeyframeData()
// 或是音频头,并且之前未出现过音频数据
|| (tag.Type == TagType.Audio && tag.Flag == TagFlag.Header && tags.All(x => x.Type != TagType.Audio || x.Flag == TagFlag.Header));
|| (tag.Type == TagType.Audio && tag.Flag == TagFlag.Header && tags.ToStructEnumerable().All(ref DoesNotContainAudioData.Instance, x => x));
// || (tag.IsKeyframeData() && tags.All(x => x.IsNonKeyframeData()))
if (shouldAppend)
{
tags.AddLast(tag);
leftover = null;
return true;
}
else
{
leftover = new LinkedList<Tag>();
leftover.AddLast(tag);
return false;
}
}
public PipelineAction CreatePipelineAction(LinkedList<Tag> tags) => new PipelineDataAction(new List<Tag>(tags));
public PipelineAction CreatePipelineAction(List<Tag> tags) => new PipelineDataAction(tags);
}
}

View File

@ -1,19 +1,14 @@
using System.Collections.Generic;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv.Grouping.Rules
{
public class EndGroupingRule : IGroupingRule
{
public bool StartWith(Tag tag) => tag.IsEnd();
public bool CanStartWith(Tag tag) => tag.IsEnd();
public bool AppendWith(Tag tag, LinkedList<Tag> tags, out LinkedList<Tag>? leftover)
{
leftover = new LinkedList<Tag>();
leftover.AddLast(tag);
return false;
}
public bool CanAppendWith(Tag tag, List<Tag> tags) => false;
public PipelineAction CreatePipelineAction(LinkedList<Tag> tags) => new PipelineEndAction(tags.First.Value);
public PipelineAction CreatePipelineAction(List<Tag> tags) => new PipelineEndAction(tags[0]);
}
}

View File

@ -1,28 +1,14 @@
using System.Collections.Generic;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv.Grouping.Rules
{
public class HeaderGroupingRule : IGroupingRule
{
public bool StartWith(Tag tag) => tag.IsHeader();
public bool CanStartWith(Tag tag) => tag.IsHeader();
public bool AppendWith(Tag tag, LinkedList<Tag> tags, out LinkedList<Tag>? leftover)
{
if (tag.IsHeader())
{
tags.AddLast(tag);
leftover = null;
return true;
}
else
{
leftover = new LinkedList<Tag>();
leftover.AddLast(tag);
return false;
}
}
public bool CanAppendWith(Tag tag, List<Tag> tags) => tag.IsHeader();
public PipelineAction CreatePipelineAction(LinkedList<Tag> tags) => new PipelineHeaderAction(new List<Tag>(tags));
public PipelineAction CreatePipelineAction(List<Tag> tags) => new PipelineHeaderAction(tags);
}
}

View File

@ -1,19 +1,14 @@
using System.Collections.Generic;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv.Grouping.Rules
{
public class ScriptGroupingRule : IGroupingRule
{
public bool StartWith(Tag tag) => tag.IsScript();
public bool CanStartWith(Tag tag) => tag.IsScript();
public bool AppendWith(Tag tag, LinkedList<Tag> tags, out LinkedList<Tag>? leftover)
{
leftover = new LinkedList<Tag>();
leftover.AddLast(tag);
return false;
}
public bool CanAppendWith(Tag tag, List<Tag> tags) => false;
public PipelineAction CreatePipelineAction(LinkedList<Tag> tags) => new PipelineScriptAction(tags.First.Value);
public PipelineAction CreatePipelineAction(List<Tag> tags) => new PipelineScriptAction(tags[0]);
}
}

View File

@ -1,10 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using BililiveRecorder.Flv.Grouping.Rules;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv.Grouping
{
@ -14,7 +13,7 @@ namespace BililiveRecorder.Flv.Grouping
private readonly bool leaveOpen;
private bool disposedValue;
private LinkedList<Tag>? leftover;
private Tag? leftoverTag;
public IFlvTagReader TagReader { get; }
public IList<IGroupingRule> GroupingRules { get; }
@ -45,16 +44,12 @@ namespace BililiveRecorder.Flv.Grouping
}
try
{
LinkedList<Tag>? queue = null;
Tag? firstTag;
if (this.leftover is not null && this.leftover.Count > 0)
if (this.leftoverTag is not null)
{
queue = this.leftover;
this.leftover = null;
firstTag = queue.First.Value;
queue.RemoveFirst();
firstTag = this.leftoverTag;
this.leftoverTag = null;
}
else
{
@ -65,38 +60,44 @@ namespace BililiveRecorder.Flv.Grouping
return null;
}
var rule = this.GroupingRules.FirstOrDefault(x => x.StartWith(firstTag));
// 查找能处理此 Tag 的分组规则
// var rule = this.GroupingRules.FirstOrDefault(x => x.StartWith(firstTag));
IGroupingRule? rule = null;
{
var rules = this.GroupingRules;
for (var i = 0; i < rules.Count; i++)
{
var item = rules[i];
if (item.CanStartWith(firstTag))
{
rule = item;
break;
}
}
}
if (rule is null)
throw new Exception("No grouping rule accepting tags: " + firstTag.ToString());
var tags = new LinkedList<Tag>();
tags.AddLast(firstTag);
var tags = new List<Tag>(32)
{
firstTag
};
firstTag = null;
while (!token.IsCancellationRequested)
{
Tag? tag;
var tag = await this.TagReader.ReadTagAsync(token).ConfigureAwait(false);
if (queue is not null && queue.Count > 0)
if (tag != null && rule.CanAppendWith(tag, tags))
{
tag = queue.First.Value;
queue.RemoveFirst();
tags.Add(tag);
}
else
{
tag = await this.TagReader.ReadTagAsync(token).ConfigureAwait(false);
}
if (tag == null || !rule.AppendWith(tag, tags, out this.leftover))
{
if (queue is not null && queue.Count > 0)
{
if (this.leftover is null)
this.leftover = queue;
else
foreach (var item in queue)
this.leftover.AddLast(item);
}
// 如果数据已经读完,或当前规则不接受此 Tag
this.leftoverTag = tag;
break;
}
}

View File

@ -1,5 +1,5 @@
using System.Collections.Generic;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv
{
@ -10,17 +10,16 @@ namespace BililiveRecorder.Flv
/// </summary>
/// <param name="tags">Current Tags</param>
/// <returns></returns>
bool StartWith(Tag tag);
bool CanStartWith(Tag tag);
/// <summary>
///
/// </summary>
/// <param name="tag">Tag not yet added to the list</param>
/// <param name="tags">List of tags</param>
/// <param name="leftover"></param>
/// <returns></returns>
bool AppendWith(Tag tag, LinkedList<Tag> tags, out LinkedList<Tag>? leftover);
bool CanAppendWith(Tag tag, List<Tag> tags);
PipelineAction CreatePipelineAction(LinkedList<Tag> tags);
PipelineAction CreatePipelineAction(List<Tag> tags);
}
}

View File

@ -1,7 +1,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv
{

View File

@ -14,18 +14,46 @@ namespace BililiveRecorder.Flv
if (!iterator.MoveNext())
return false;
var lastItem = iterator.Current;
var previousItem = iterator.Current;
while (iterator.MoveNext())
{
var current = iterator.Current;
var currentItem = iterator.Current;
if (predicate(lastItem, current))
if (predicate(previousItem, currentItem))
return true;
lastItem = current;
previousItem = currentItem;
}
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Any2<TIn, TFunction>(this IEnumerable<TIn> source, ref TFunction function)
where TFunction : ITwoInputFunction<TIn, bool>
{
using var iterator = source.GetEnumerator();
if (!iterator.MoveNext())
return false;
var previousItem = iterator.Current;
while (iterator.MoveNext())
{
var currentItem = iterator.Current;
if (function.Eval(previousItem, currentItem))
return true;
previousItem = currentItem;
}
return false;
}
}
public interface ITwoInputFunction<in TIn, out TOut>
{
TOut Eval(TIn a, TIn b);
}
}

View File

@ -0,0 +1,92 @@
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.Flv
{
public static class LinqFunctions
{
public static TagIsHeaderStruct TagIsHeader;
public readonly struct TagIsHeaderStruct : IFunction<Tag, bool>, IInFunction<Tag, bool>
{
public bool Eval(Tag element) => element.IsHeader();
public bool Eval(in Tag element) => element.IsHeader();
}
public static TagIsDataStruct TagIsData;
public readonly struct TagIsDataStruct : IFunction<Tag, bool>, IInFunction<Tag, bool>
{
public bool Eval(Tag element) => element.IsData();
public bool Eval(in Tag element) => element.IsData();
}
public static TagIsVideoStruct TagIsVideo;
public readonly struct TagIsVideoStruct : IFunction<Tag, bool>, IInFunction<Tag, bool>
{
public bool Eval(Tag element) => element.Type == TagType.Video;
public bool Eval(in Tag element) => element.Type == TagType.Video;
}
public static TagIsAudioStruct TagIsAudio;
public readonly struct TagIsAudioStruct : IFunction<Tag, bool>, IInFunction<Tag, bool>
{
public bool Eval(Tag element) => element.Type == TagType.Audio;
public bool Eval(in Tag element) => element.Type == TagType.Audio;
}
public static SumSizeOfVideoDataStruct SumSizeOfVideoData;
public readonly struct SumSizeOfVideoDataStruct : IFunction<PipelineDataAction, long>, IInFunction<PipelineDataAction, long>
{
public long Eval(PipelineDataAction element) => element.Tags.ToStructEnumerable().Where(ref TagIsVideo, x => x).Sum(ref SumSizeOfTagByProperty, x => x, x => x);
public long Eval(in PipelineDataAction element) => element.Tags.ToStructEnumerable().Where(ref TagIsVideo, x => x).Sum(ref SumSizeOfTagByProperty, x => x, x => x);
}
public static SumSizeOfVideoDataByNaluStruct SumSizeOfVideoDataByNalu;
public readonly struct SumSizeOfVideoDataByNaluStruct : IFunction<PipelineDataAction, long>, IInFunction<PipelineDataAction, long>
{
public long Eval(PipelineDataAction element) => element.Tags.ToStructEnumerable().Where(ref TagIsVideo, x => x).Sum(ref SumSizeOfTagByPropertyOrNalu, x => x, x => x);
public long Eval(in PipelineDataAction element) => element.Tags.ToStructEnumerable().Where(ref TagIsVideo, x => x).Sum(ref SumSizeOfTagByPropertyOrNalu, x => x, x => x);
}
public static SumSizeOfAudioDataStruct SumSizeOfAudioData;
public readonly struct SumSizeOfAudioDataStruct : IFunction<PipelineDataAction, long>, IInFunction<PipelineDataAction, long>
{
public long Eval(PipelineDataAction element) => element.Tags.ToStructEnumerable().Where(ref TagIsAudio, x => x).Sum(ref SumSizeOfTagByProperty, x => x, x => x);
public long Eval(in PipelineDataAction element) => element.Tags.ToStructEnumerable().Where(ref TagIsAudio, x => x).Sum(ref SumSizeOfTagByProperty, x => x, x => x);
}
public static SumSizeOfTagByPropertyStruct SumSizeOfTagByProperty;
public readonly struct SumSizeOfTagByPropertyStruct : IFunction<Tag, long>, IInFunction<Tag, long>
{
public long Eval(Tag element) => element.Size + (11L + 4L);
public long Eval(in Tag element) => element.Size + (11L + 4L);
}
public static SumSizeOfTagByPropertyOrNaluStruct SumSizeOfTagByPropertyOrNalu;
public readonly struct SumSizeOfTagByPropertyOrNaluStruct : IFunction<Tag, long>, IInFunction<Tag, long>
{
public long Eval(Tag element) => 11 + 4 + (element.Nalus == null ? element.Size : (5 + element.Nalus.ToStructEnumerable().Sum(ref SumSizeOfNalu, x => x, x => x)));
public long Eval(in Tag element) => 11 + 4 + (element.Nalus == null ? element.Size : (5 + element.Nalus.ToStructEnumerable().Sum(ref SumSizeOfNalu, x => x, x => x)));
}
public static SumSizeOfNaluStruct SumSizeOfNalu;
public readonly struct SumSizeOfNaluStruct : IFunction<H264Nalu, long>, IInFunction<H264Nalu, long>
{
public long Eval(H264Nalu element) => element.FullSize + 4;
public long Eval(in H264Nalu element) => element.FullSize + 4;
}
public static CountVideoTagsStruct CountVideoTags;
public readonly struct CountVideoTagsStruct : IFunction<PipelineDataAction, int>, IInFunction<PipelineDataAction, int>
{
public int Eval(PipelineDataAction element) => element.Tags.ToStructEnumerable().Where(ref TagIsVideo, x => x).Count();
public int Eval(in PipelineDataAction element) => element.Tags.ToStructEnumerable().Where(ref TagIsVideo, x => x).Count();
}
public static CountAudioTagsStruct CountAudioTags;
public readonly struct CountAudioTagsStruct : IFunction<PipelineDataAction, int>, IInFunction<PipelineDataAction, int>
{
public int Eval(PipelineDataAction element) => element.Tags.ToStructEnumerable().Where(ref TagIsAudio, x => x).Count();
public int Eval(in PipelineDataAction element) => element.Tags.ToStructEnumerable().Where(ref TagIsAudio, x => x).Count();
}
}
}

View File

@ -1,4 +1,4 @@
namespace BililiveRecorder.Flv.Pipeline
namespace BililiveRecorder.Flv.Pipeline.Actions
{
public abstract class PipelineAction
{

View File

@ -1,16 +1,16 @@
using System;
using System.Collections.Generic;
namespace BililiveRecorder.Flv.Pipeline
namespace BililiveRecorder.Flv.Pipeline.Actions
{
public class PipelineDataAction : PipelineAction
{
public PipelineDataAction(List<Tag> tags)
public PipelineDataAction(IReadOnlyList<Tag> tags)
{
this.Tags = tags ?? throw new ArgumentNullException(nameof(tags));
}
public List<Tag> Tags { get; set; }
public IReadOnlyList<Tag> Tags { get; set; }
public override PipelineAction Clone() => new PipelineDataAction(new List<Tag>(this.Tags));
}

View File

@ -1,4 +1,4 @@
namespace BililiveRecorder.Flv.Pipeline
namespace BililiveRecorder.Flv.Pipeline.Actions
{
public class PipelineDisconnectAction : PipelineAction
{

View File

@ -1,4 +1,4 @@
namespace BililiveRecorder.Flv.Pipeline
namespace BililiveRecorder.Flv.Pipeline.Actions
{
public class PipelineEndAction : PipelineAction
{

View File

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
namespace BililiveRecorder.Flv.Pipeline
namespace BililiveRecorder.Flv.Pipeline.Actions
{
public class PipelineHeaderAction : PipelineAction
{

View File

@ -2,7 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
namespace BililiveRecorder.Flv.Pipeline
namespace BililiveRecorder.Flv.Pipeline.Actions
{
public class PipelineLogAlternativeHeaderAction : PipelineAction
{

View File

@ -1,4 +1,4 @@
namespace BililiveRecorder.Flv.Pipeline
namespace BililiveRecorder.Flv.Pipeline.Actions
{
public class PipelineNewFileAction : PipelineAction
{

View File

@ -1,6 +1,6 @@
using System;
namespace BililiveRecorder.Flv.Pipeline
namespace BililiveRecorder.Flv.Pipeline.Actions
{
public class PipelineScriptAction : PipelineAction
{

View File

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.Flv.Pipeline
{

View File

@ -26,9 +26,6 @@ namespace BililiveRecorder.Flv.Pipeline
builder
.Add<HandleEndTagRule>()
.Add<HandleDelayedAudioHeaderRule>()
// TODO .Add<CheckMissingKeyframeRule>()
// .Add<UpdateDataTagOrderRule>()
// .Add<CheckDiscontinuityRule>()
.Add<UpdateTimestampOffsetRule>()
.Add<UpdateTimestampJumpRule>()
.Add<HandleNewScriptRule>()

View File

@ -1,59 +0,0 @@
using System;
using System.Collections.Generic;
namespace BililiveRecorder.Flv.Pipeline.Rules
{
/// <summary>
/// 检查分块内时间戳问题
/// </summary>
/// <remarks>
/// 到目前为止还未发现有在一个 GOP 内出现时间戳异常问题<br/>
/// 本规则是为了预防实际使用中遇到意外情况<br/>
/// <br/>
/// 本规则应该放在所有规则前面
/// </remarks>
public class CheckDiscontinuityRule : ISimpleProcessingRule
{
private const int MAX_ALLOWED_DIFF = 1000 * 10; // 10 seconds
private static readonly ProcessingComment Comment1 = new ProcessingComment(CommentType.Unrepairable, "Flv Chunk 内出现时间戳跳变(变小)");
private static readonly ProcessingComment Comment2 = new ProcessingComment(CommentType.Unrepairable, "Flv Chunk 内出现时间戳跳变(间隔过大)");
public void Run(FlvProcessingContext context, Action next)
{
context.PerActionRun(this.RunPerAction);
next();
}
private IEnumerable<PipelineAction?> RunPerAction(FlvProcessingContext context, PipelineAction action)
{
if (action is PipelineDataAction data)
{
for (var i = 0; i < data.Tags.Count - 1; i++)
{
var f1 = data.Tags[i];
var f2 = data.Tags[i + 1];
if (f1.Timestamp > f2.Timestamp)
{
context.AddComment(Comment1);
yield return PipelineDisconnectAction.Instance;
yield return null;
yield break;
}
else if ((f2.Timestamp - f1.Timestamp) > MAX_ALLOWED_DIFF)
{
context.AddComment(Comment2);
yield return PipelineDisconnectAction.Instance;
yield return null;
yield break;
}
}
yield return data;
}
else
yield return action;
}
}
}

View File

@ -1,47 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace BililiveRecorder.Flv.Pipeline.Rules
{
/// <summary>
/// 检查缺少关键帧的问题
/// </summary>
/// <remarks>
/// 到目前为止还未发现有出现过此问题<br/>
/// 本规则是为了预防实际使用中遇到意外情况<br/>
/// <br/>
/// 本规则应该放在所有规则前面
/// </remarks>
public class CheckMissingKeyframeRule : ISimpleProcessingRule
{
private static readonly ProcessingComment comment = new ProcessingComment(CommentType.Unrepairable, "Flv Chunk 内缺少关键帧");
public void Run(FlvProcessingContext context, Action next)
{
// context.PerActionRun(this.RunPerAction);
// 暂时禁用此规则,必要性待定
// TODO
next();
}
private IEnumerable<PipelineAction?> RunPerAction(FlvProcessingContext context, PipelineAction action)
{
if (action is PipelineDataAction data)
{
var f = data.Tags.FirstOrDefault(x => x.Type == TagType.Video);
if (f == null || (0 == (f.Flag & TagFlag.Keyframe)))
{
context.AddComment(comment);
yield return PipelineDisconnectAction.Instance;
yield return null;
yield break;
}
else
yield return action;
}
else
yield return action;
}
}
}

View File

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.Flv.Pipeline.Rules
{
@ -12,7 +14,8 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
/// </remarks>
public class HandleDelayedAudioHeaderRule : ISimpleProcessingRule
{
private static readonly ProcessingComment comment = new ProcessingComment(CommentType.DecodingHeader, "检测到延后收到的音频头");
private static readonly ProcessingComment comment1 = new ProcessingComment(CommentType.Unrepairable, "音频数据出现在音频头之前");
private static readonly ProcessingComment comment2 = new ProcessingComment(CommentType.DecodingHeader, "检测到延后收到的音频头");
public void Run(FlvProcessingContext context, Action next)
{
@ -25,25 +28,42 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
if (action is PipelineDataAction data)
{
var tags = data.Tags;
if (tags.Any(x => x.IsHeader()))
// 如果分组内含有 Heaer
if (tags.ToStructEnumerable().Any(ref LinqFunctions.TagIsHeader, x => x))
{
context.AddComment(comment);
var index = tags.IndexOf(tags.Last(x => x.Flag == TagFlag.Header));
for (var i = 0; i < index; i++)
{
var shouldReportError = false;
for (var i = tags.Count - 1; i >= 0; i--)
{
if (tags[i].Type == TagType.Audio)
{
if (tags[i].Flag != TagFlag.None)
{
// 发现了 Audio Header
shouldReportError = true;
}
else
{
// 在一段数据内 Header 之前出现了音频数据
if (shouldReportError)
{
context.AddComment(comment1);
yield return PipelineDisconnectAction.Instance;
yield return PipelineNewFileAction.Instance;
yield return null;
yield break;
}
}
}
}
}
var headerTags = tags.Where(x => x.Flag == TagFlag.Header).ToList();
context.AddComment(comment2);
var headerTags = tags.ToStructEnumerable().Where(ref LinqFunctions.TagIsHeader, x => x).ToArray();
var newHeaderAction = new PipelineHeaderAction(headerTags);
var dataTags = tags.Where(x => x.Flag != TagFlag.Header).ToList();
var dataTags = tags.ToStructEnumerable().Except(headerTags.ToStructEnumerable(), x => x, x => x).ToArray();
var newDataAction = new PipelineDataAction(dataTags);
yield return newHeaderAction;

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv.Pipeline.Rules
{

View File

@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
using StructLinq.Where;
namespace BililiveRecorder.Flv.Pipeline.Rules
{
@ -40,8 +43,8 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
// 音频 视频 分别单独处理
var group = header.AllTags.GroupBy(x => x.Type);
var currentVideoHeader = SelectHeader(ref multiple_header_present, group.FirstOrDefault(x => x.Key == TagType.Video));
var currentAudioHeader = SelectHeader(ref multiple_header_present, group.FirstOrDefault(x => x.Key == TagType.Audio));
var currentVideoHeader = SelectHeader(ref multiple_header_present, header.AllTags.ToStructEnumerable().Where(ref LinqFunctions.TagIsVideo, x => x));
var currentAudioHeader = SelectHeader(ref multiple_header_present, header.AllTags.ToStructEnumerable().Where(ref LinqFunctions.TagIsAudio, x => x));
if (multiple_header_present)
context.AddComment(MultipleHeaderComment);
@ -91,30 +94,29 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
yield return action;
}
private static Tag? SelectHeader(ref bool multiple_header_present, IGrouping<TagType, Tag> tagGroup)
private static Tag? SelectHeader<TEnumerable, TEnumerator, TFunction>(ref bool multiple_header_present, WhereEnumerable<Tag, TEnumerable, TEnumerator, TFunction> tags)
where TEnumerable : struct, IStructEnumerable<Tag, TEnumerator>
where TEnumerator : struct, IStructEnumerator<Tag>
where TFunction : struct, IFunction<Tag, bool>
{
Tag? currentHeader;
if (tagGroup != null)
if (tags.Count(x => x) > 1)
{
// 检查是否存在 **多个** **不同的** Header
if (tagGroup.Count() > 1)
{
var first = tagGroup.First();
var first = tags.First(x => x);
if (tagGroup.Skip(1).All(x => first.BinaryData?.SequenceEqual(x.BinaryData) ?? false))
if (tags.Skip(1, x => x).All(x => first.BinaryData?.SequenceEqual(x.BinaryData) ?? false))
currentHeader = first;
else
{
// 默认最后一个为正确的
currentHeader = tagGroup.Last();
currentHeader = tags.Last(x => x);
multiple_header_present = true;
}
}
else
currentHeader = tagGroup.FirstOrDefault();
}
else
currentHeader = null;
currentHeader = tags.FirstOrDefault(x => x);
return currentHeader;
}

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using BililiveRecorder.Flv.Amf;
using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv.Pipeline.Rules
{

View File

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using BililiveRecorder.Flv;
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.Flv.Pipeline.Rules
{
@ -80,7 +82,7 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
}
// 对比历史特征
if (history.Any(x => x.SequenceEqual(feature)))
if (history.ToStructEnumerable().Any(x => x.SequenceEqual(feature), x => x))
{
context.AddComment(comment);
}

View File

@ -1,4 +1,5 @@
using System;
using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv.Pipeline.Rules
{

View File

@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace BililiveRecorder.Flv.Pipeline.Rules
{
/// <summary>
/// 修复 Tag 错位等时间戳相关问题
/// </summary>
public class UpdateDataTagOrderRule : ISimpleProcessingRule
{
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;
}
// 如果一切正常,直接跳过
if (data.Tags.Any2((t1, t2) => t1.Timestamp > t2.Timestamp))
{
// 排序
data.Tags = data.Tags.OrderBy(x => x.Timestamp).ToList();
}
yield return data;
}
}
}

View File

@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.Flv.Pipeline.Rules
{
@ -28,8 +31,27 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
foreach (var action in context.Actions)
{
if (action is PipelineDataAction dataAction)
{ // SetDataTimestamp
var tags = dataAction.Tags;
var currentTimestamp = tags[0].Timestamp;
var diff = currentTimestamp - ts.LastOriginal;
if (diff < 0)
{
this.SetDataTimestamp(dataAction.Tags, ts, context);
context.AddComment(new ProcessingComment(CommentType.TimestampJump, $"时间戳变小, curr: {currentTimestamp}, diff: {diff}"));
ts.CurrentOffset = currentTimestamp - ts.NextTimestampTarget;
}
else if (diff > JUMP_THRESHOLD)
{
context.AddComment(new ProcessingComment(CommentType.TimestampJump, $"时间戳间隔过大, curr: {currentTimestamp}, diff: {diff}"));
ts.CurrentOffset = currentTimestamp - ts.NextTimestampTarget;
}
ts.LastOriginal = tags[tags.Count - 1].Timestamp;
foreach (var tag in tags)
tag.Timestamp -= ts.CurrentOffset;
ts.NextTimestampTarget = this.CalculateNewTarget(tags);
}
else if (action is PipelineEndAction endAction)
{
@ -64,45 +86,24 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
}
}
private void SetDataTimestamp(IReadOnlyList<Tag> tags, TimestampStore ts, FlvProcessingContext context)
{
var currentTimestamp = tags[0].Timestamp;
var diff = currentTimestamp - ts.LastOriginal;
if (diff < 0)
{
context.AddComment(new ProcessingComment(CommentType.TimestampJump, $"时间戳变小, curr: {currentTimestamp}, diff: {diff}"));
ts.CurrentOffset = currentTimestamp - ts.NextTimestampTarget;
}
else if (diff > JUMP_THRESHOLD)
{
context.AddComment(new ProcessingComment(CommentType.TimestampJump, $"时间戳间隔过大, curr: {currentTimestamp}, diff: {diff}"));
ts.CurrentOffset = currentTimestamp - ts.NextTimestampTarget;
}
ts.LastOriginal = tags.Last().Timestamp;
foreach (var tag in tags)
tag.Timestamp -= ts.CurrentOffset;
ts.NextTimestampTarget = this.CalculateNewTarget(tags);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int CalculateNewTarget(IReadOnlyList<Tag> tags)
{
// 有可能出现只有音频或只有视频的情况
int video = 0, audio = 0;
if (tags.Any(x => x.Type == TagType.Video))
if (tags.ToStructEnumerable().Any(ref LinqFunctions.TagIsVideo, x => x))
video = CalculatePerChannel(tags, VIDEO_DURATION_FALLBACK, VIDEO_DURATION_MAX, VIDEO_DURATION_MIN, TagType.Video);
if (tags.Any(x => x.Type == TagType.Audio))
if (tags.ToStructEnumerable().Any(ref LinqFunctions.TagIsAudio, x => x))
audio = CalculatePerChannel(tags, AUDIO_DURATION_FALLBACK, AUDIO_DURATION_MAX, AUDIO_DURATION_MIN, TagType.Audio);
return Math.Max(video, audio);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
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();
var sample = tags.ToStructEnumerable().Where(x => x.Type == type).Take(2).ToArray();
int durationPerTag;
if (sample.Length != 2)
{
@ -116,7 +117,7 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
durationPerTag = fallback;
}
return durationPerTag + tags.Last(x => x.Type == type).Timestamp;
return durationPerTag + tags.ToStructEnumerable().Last(x => x.Type == type).Timestamp;
}
}

View File

@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.Flv.Pipeline.Rules
{
@ -15,20 +18,25 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
next();
}
private bool CheckIfNormal(IEnumerable<Tag> data) => !data.Any2((a, b) => a.Timestamp > b.Timestamp);
private readonly struct IsNotNormal : ITwoInputFunction<Tag, bool>
{
public static IsNotNormal Instance;
public bool Eval(Tag a, Tag b) => a.Timestamp > b.Timestamp;
}
private IEnumerable<PipelineAction?> RunPerAction(FlvProcessingContext context, PipelineAction action)
{
if (action is PipelineDataAction data)
{
var isNormal = this.CheckIfNormal(data.Tags);
if (isNormal)
var isNotNormal = data.Tags.Any2(ref IsNotNormal.Instance);
if (!isNotNormal)
{
// 没有问题
yield return data;
yield break;
}
if (!(this.CheckIfNormal(data.Tags.Where(x => x.Type == TagType.Audio)) && this.CheckIfNormal(data.Tags.Where(x => x.Type == TagType.Video))))
if (data.Tags.Where(x => x.Type == TagType.Audio).Any2(ref IsNotNormal.Instance) || data.Tags.Where(x => x.Type == TagType.Video).Any2(ref IsNotNormal.Instance))
{
// 音频或视频自身就有问题,没救了
yield return PipelineDisconnectAction.Instance;
@ -37,146 +45,118 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
}
else
{
var oc = new OffsetCalculator();
foreach (var tag in data.Tags)
oc.AddTag(tag);
if (oc.Calculate(out var videoOffset))
{
if (videoOffset != 0)
{
context.AddComment(new ProcessingComment(CommentType.TimestampOffset, $"音视频时间戳偏移, D: {videoOffset}"));
foreach (var tag in data.Tags)
if (tag.Type == TagType.Video)
tag.Timestamp += videoOffset;
}
yield return data;
yield break;
}
else
{
context.AddComment(comment2);
yield return PipelineDisconnectAction.Instance;
yield break;
}
}
}
else
yield return action;
}
/// <summary>
/// 音视频偏差量计算
/// </summary>
private class OffsetCalculator
{
/*
*
/*
*
*
* 使
* */
var offset = 0;
private Tag? lastAudio = null;
private readonly Stack<Tag> tags = new Stack<Tag>();
private int maxOffset = int.MaxValue;
private int minOffset = int.MinValue;
public void AddTag(Tag tag)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void ReduceOffsetRange(ref int maxOffset, ref int minOffset, Tag? leftAudio, Tag? rightAudio, Stack<Tag> tags)
{
if (tag.Type == TagType.Audio)
while (tags.Count > 0)
{
this.ReduceOffsetRange(this.lastAudio, tag);
this.lastAudio = tag;
}
else if (tag.Type == TagType.Video)
{
this.tags.Push(tag);
}
else
throw new ArgumentException("unexpected tag type");
}
public bool Calculate(out int offset)
{
{
var last = this.lastAudio;
this.lastAudio = null;
this.ReduceOffsetRange(last, null);
}
if (this.minOffset == this.maxOffset)
{
// 理想情况允许偏移范围只有一个值
offset = this.minOffset;
return true;
}
else if (this.minOffset < this.maxOffset)
{
// 允许偏移的值是一个范围
if (this.minOffset != int.MinValue)
{
if (this.maxOffset != int.MaxValue)
{
// 有一个有效范围,取平均值
offset = (int)(((long)this.minOffset + this.maxOffset) / 2L);
return true;
}
else
{
// 无效最大偏移,以最小偏移为准
offset = this.minOffset + 1;
return true;
}
}
else
{
if (this.maxOffset != int.MaxValue)
{
// 无效最小偏移,以最大偏移为准
offset = this.maxOffset - 1;
return true;
}
else
{
// 无效结果
offset = 0;
return false;
}
}
}
else
{
// 范围无效
offset = 0;
return false;
}
}
private void ReduceOffsetRange(Tag? leftAudio, Tag? rightAudio)
{
while (this.tags.Count > 0)
{
var video = this.tags.Pop();
var video = tags.Pop();
if (leftAudio is not null)
{
var min = leftAudio.Timestamp - video.Timestamp;
if (this.minOffset < min)
this.minOffset = min;
if (minOffset < min)
minOffset = min;
}
if (rightAudio is not null)
{
var max = rightAudio.Timestamp - video.Timestamp;
if (this.maxOffset > max)
this.maxOffset = max;
if (maxOffset > max)
maxOffset = max;
}
}
}
Tag? lastAudio = null;
var tags = new Stack<Tag>();
var maxOffset = int.MaxValue;
var minOffset = int.MinValue;
for (var i = 0; i < data.Tags.Count; i++)
{
var tag = data.Tags[i];
if (tag.Type == TagType.Audio)
{
ReduceOffsetRange(ref maxOffset, ref minOffset, lastAudio, tag, tags);
lastAudio = tag;
}
else if (tag.Type == TagType.Video)
{
tags.Push(tag);
}
else
throw new ArgumentException("unexpected tag type");
}
ReduceOffsetRange(ref maxOffset, ref minOffset, lastAudio, null, tags);
if (minOffset == maxOffset)
{ // 理想情况允许偏移范围只有一个值
offset = minOffset;
goto validOffset;
}
else if (minOffset < maxOffset)
{ // 允许偏移的值是一个范围
if (minOffset != int.MinValue)
{
if (maxOffset != int.MaxValue)
{ // 有一个有效范围,取平均值
offset = (int)(((long)minOffset + maxOffset) / 2L);
goto validOffset;
}
else
{ // 无效最大偏移,以最小偏移为准
offset = minOffset + 1;
goto validOffset;
}
}
else
{
if (maxOffset != int.MaxValue)
{ // 无效最小偏移,以最大偏移为准
offset = maxOffset - 1;
goto validOffset;
}
else
{ // 无效结果
goto invalidOffset;
}
}
}
else
{ // 范围无效
goto invalidOffset;
}
validOffset:
if (offset != 0)
{
context.AddComment(new ProcessingComment(CommentType.TimestampOffset, $"音视频时间戳偏移, D: {offset}"));
foreach (var tag in data.Tags)
if (tag.Type == TagType.Video)
tag.Timestamp += offset;
}
yield return data;
yield break;
invalidOffset:
context.AddComment(comment2);
yield return PipelineDisconnectAction.Instance;
yield break;
}
}
else
yield return action;
}
}
}

View File

@ -15,8 +15,8 @@ namespace BililiveRecorder.Flv
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsHeader(this Tag tag)
=> (tag.Type == TagType.Video || tag.Type == TagType.Audio)
&& (0 != (tag.Flag & TagFlag.Header));
=> (0 != (tag.Flag & TagFlag.Header))
&& (tag.Type == TagType.Video || tag.Type == TagType.Audio);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsEnd(this Tag tag)
@ -24,7 +24,8 @@ namespace BililiveRecorder.Flv
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsData(this Tag tag)
=> tag.Type != TagType.Script && (0 == (tag.Flag & (TagFlag.Header | TagFlag.End)));
=> (0 == (tag.Flag & (TagFlag.Header | TagFlag.End)))
&& (tag.Type == TagType.Video || tag.Type == TagType.Audio);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsNonKeyframeData(this Tag tag)

View File

@ -3,6 +3,7 @@ using System.Threading;
using System.Threading.Tasks;
using BililiveRecorder.Flv.Amf;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv.Writer
{

View File

@ -10,6 +10,7 @@ using BililiveRecorder.Flv.Amf;
using BililiveRecorder.Flv.Grouping;
using BililiveRecorder.Flv.Parser;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
using BililiveRecorder.Flv.Writer;
using BililiveRecorder.Flv.Xml;
using BililiveRecorder.ToolBox.ProcessingRules;

View File

@ -9,6 +9,7 @@ using BililiveRecorder.Flv;
using BililiveRecorder.Flv.Grouping;
using BililiveRecorder.Flv.Parser;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
using BililiveRecorder.Flv.Writer;
using BililiveRecorder.Flv.Xml;
using BililiveRecorder.ToolBox.ProcessingRules;

View File

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Linq;
using BililiveRecorder.Flv;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.ToolBox.ProcessingRules
{
@ -40,7 +42,8 @@ namespace BililiveRecorder.ToolBox.ProcessingRules
var stat = new FlvStats
{
FrameCount = timestamps.Count,
FrameDurations = timestamps.Select((time, i) => i == 0 ? 0 : time - timestamps[i - 1])
FrameDurations = timestamps
.Select((time, i) => i == 0 ? 0 : time - timestamps[i - 1])
.Skip(1)
.GroupBy(x => x)
.ToDictionary(x => x.Key, x => x.Count())

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using BililiveRecorder.Flv.Grouping;
using BililiveRecorder.Flv.Parser;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
using BililiveRecorder.Flv.Writer;
using Microsoft.Extensions.DependencyInjection;
using Xunit;