mirror of
https://github.com/BililiveRecorder/BililiveRecorder.git
synced 2024-11-15 19:22:19 +08:00
FLV: Performance improvements
(I guess)
This commit is contained in:
parent
5c9706e827
commit
93781b2a56
|
@ -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>
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System.Threading;
|
||||
using BililiveRecorder.Flv.Pipeline;
|
||||
using BililiveRecorder.Flv.Pipeline.Actions;
|
||||
|
||||
namespace BililiveRecorder.Core.ProcessingRules
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
||||
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)));
|
||||
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)));
|
||||
{
|
||||
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 =
|
||||
FilterDataActions(context.Actions).ToStructEnumerable().Sum(ref LinqFunctions.SumSizeOfVideoData, x => x, x => x);
|
||||
|
||||
e.TotalInputAudioByteCount = this.TotalInputAudioByteCount += e.InputAudioByteCount =
|
||||
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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 =
|
||||
// Tag 是非关键帧数据
|
||||
tag.IsNonKeyframeData()
|
||||
// 或是音频头,并且之前未出现过音频数据
|
||||
|| (tag.Type == TagType.Audio && tag.Flag == TagFlag.Header && tags.All(x => x.Type != TagType.Audio || x.Flag == TagFlag.Header));
|
||||
// || (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 static DoesNotContainAudioData Instance;
|
||||
public bool Eval(Tag element) => element.Type != TagType.Audio || element.Flag == TagFlag.Header;
|
||||
}
|
||||
|
||||
public PipelineAction CreatePipelineAction(LinkedList<Tag> tags) => new PipelineDataAction(new List<Tag>(tags));
|
||||
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.ToStructEnumerable().All(ref DoesNotContainAudioData.Instance, x => x));
|
||||
// || (tag.IsKeyframeData() && tags.All(x => x.IsNonKeyframeData()))
|
||||
|
||||
public PipelineAction CreatePipelineAction(List<Tag> tags) => new PipelineDataAction(tags);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
92
BililiveRecorder.Flv/LinqFunctions.cs
Normal file
92
BililiveRecorder.Flv/LinqFunctions.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
namespace BililiveRecorder.Flv.Pipeline
|
||||
namespace BililiveRecorder.Flv.Pipeline.Actions
|
||||
{
|
||||
public abstract class PipelineAction
|
||||
{
|
|
@ -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));
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
namespace BililiveRecorder.Flv.Pipeline
|
||||
namespace BililiveRecorder.Flv.Pipeline.Actions
|
||||
{
|
||||
public class PipelineDisconnectAction : PipelineAction
|
||||
{
|
|
@ -1,4 +1,4 @@
|
|||
namespace BililiveRecorder.Flv.Pipeline
|
||||
namespace BililiveRecorder.Flv.Pipeline.Actions
|
||||
{
|
||||
public class PipelineEndAction : PipelineAction
|
||||
{
|
|
@ -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
|
||||
{
|
|
@ -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
|
||||
{
|
|
@ -1,4 +1,4 @@
|
|||
namespace BililiveRecorder.Flv.Pipeline
|
||||
namespace BililiveRecorder.Flv.Pipeline.Actions
|
||||
{
|
||||
public class PipelineNewFileAction : PipelineAction
|
||||
{
|
|
@ -1,6 +1,6 @@
|
|||
using System;
|
||||
|
||||
namespace BililiveRecorder.Flv.Pipeline
|
||||
namespace BililiveRecorder.Flv.Pipeline.Actions
|
||||
{
|
||||
public class PipelineScriptAction : PipelineAction
|
||||
{
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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++)
|
||||
{
|
||||
if (tags[i].Type == TagType.Audio)
|
||||
var shouldReportError = false;
|
||||
for (var i = tags.Count - 1; i >= 0; i--)
|
||||
{
|
||||
// 在一段数据内 Header 之前出现了音频数据
|
||||
yield return PipelineDisconnectAction.Instance;
|
||||
yield return null;
|
||||
yield break;
|
||||
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;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BililiveRecorder.Flv.Pipeline.Actions;
|
||||
|
||||
namespace BililiveRecorder.Flv.Pipeline.Rules
|
||||
{
|
||||
|
|
|
@ -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))
|
||||
currentHeader = first;
|
||||
else
|
||||
{
|
||||
// 默认最后一个为正确的
|
||||
currentHeader = tagGroup.Last();
|
||||
multiple_header_present = true;
|
||||
}
|
||||
}
|
||||
if (tags.Skip(1, x => x).All(x => first.BinaryData?.SequenceEqual(x.BinaryData) ?? false))
|
||||
currentHeader = first;
|
||||
else
|
||||
currentHeader = tagGroup.FirstOrDefault();
|
||||
{
|
||||
// 默认最后一个为正确的
|
||||
currentHeader = tags.Last(x => x);
|
||||
multiple_header_present = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
currentHeader = null;
|
||||
currentHeader = tags.FirstOrDefault(x => x);
|
||||
|
||||
return currentHeader;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BililiveRecorder.Flv.Amf;
|
||||
using BililiveRecorder.Flv.Pipeline.Actions;
|
||||
|
||||
namespace BililiveRecorder.Flv.Pipeline.Rules
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using BililiveRecorder.Flv.Pipeline.Actions;
|
||||
|
||||
namespace BililiveRecorder.Flv.Pipeline.Rules
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
this.SetDataTimestamp(dataAction.Tags, ts, context);
|
||||
{ // SetDataTimestamp
|
||||
var tags = dataAction.Tags;
|
||||
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[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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
/*
|
||||
* 设定做调整的为视频帧,参照每个视频帧左右(左为前、右为后)的音频帧的时间戳
|
||||
* 计算出最多和最少能符合“不小于前面的帧并且不大于后面的帧”的要求的偏移量
|
||||
* 如果当前偏移量比总偏移量要求更严,则使用当前偏移量范围作为总偏移量范围
|
||||
* */
|
||||
var offset = 0;
|
||||
|
||||
foreach (var tag in data.Tags)
|
||||
oc.AddTag(tag);
|
||||
|
||||
if (oc.Calculate(out var videoOffset))
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
static void ReduceOffsetRange(ref int maxOffset, ref int minOffset, Tag? leftAudio, Tag? rightAudio, Stack<Tag> tags)
|
||||
{
|
||||
if (videoOffset != 0)
|
||||
while (tags.Count > 0)
|
||||
{
|
||||
context.AddComment(new ProcessingComment(CommentType.TimestampOffset, $"音视频时间戳偏移, D: {videoOffset}"));
|
||||
var video = tags.Pop();
|
||||
|
||||
foreach (var tag in data.Tags)
|
||||
if (tag.Type == TagType.Video)
|
||||
tag.Timestamp += videoOffset;
|
||||
if (leftAudio is not null)
|
||||
{
|
||||
var min = leftAudio.Timestamp - video.Timestamp;
|
||||
if (minOffset < min)
|
||||
minOffset = min;
|
||||
}
|
||||
|
||||
if (rightAudio is not null)
|
||||
{
|
||||
var max = rightAudio.Timestamp - video.Timestamp;
|
||||
if (maxOffset > max)
|
||||
maxOffset = max;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yield return data;
|
||||
yield break;
|
||||
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
|
||||
{
|
||||
context.AddComment(comment2);
|
||||
yield return PipelineDisconnectAction.Instance;
|
||||
yield break;
|
||||
{ // 范围无效
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 音视频偏差量计算
|
||||
/// </summary>
|
||||
private class OffsetCalculator
|
||||
{
|
||||
/* 算法思路和原理
|
||||
* 设定作调整的为视频帧,参照每个视频帧左右(左为前、右为后)的音频帧的时间戳
|
||||
* 计算出最多和最少能符合“不小于前面的帧并且不大于后面的帧”的要求的偏移量
|
||||
* 如果当前偏移量比总偏移量要求更严,则使用当前偏移量范围作为总偏移量范围
|
||||
* */
|
||||
|
||||
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)
|
||||
{
|
||||
if (tag.Type == TagType.Audio)
|
||||
{
|
||||
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();
|
||||
|
||||
if (leftAudio is not null)
|
||||
{
|
||||
var min = leftAudio.Timestamp - video.Timestamp;
|
||||
if (this.minOffset < min)
|
||||
this.minOffset = min;
|
||||
}
|
||||
|
||||
if (rightAudio is not null)
|
||||
{
|
||||
var max = rightAudio.Timestamp - video.Timestamp;
|
||||
if (this.maxOffset > max)
|
||||
this.maxOffset = max;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,13 +42,14 @@ namespace BililiveRecorder.ToolBox.ProcessingRules
|
|||
var stat = new FlvStats
|
||||
{
|
||||
FrameCount = timestamps.Count,
|
||||
FrameDurations = timestamps.Select((time, i) => i == 0 ? 0 : time - timestamps[i - 1])
|
||||
.Skip(1)
|
||||
.GroupBy(x => x)
|
||||
.ToDictionary(x => x.Key, x => x.Count())
|
||||
.OrderByDescending(x => x.Value)
|
||||
.ThenByDescending(x => x.Key)
|
||||
.ToDictionary(x => x.Key, x => x.Value)
|
||||
FrameDurations = timestamps
|
||||
.Select((time, i) => i == 0 ? 0 : time - timestamps[i - 1])
|
||||
.Skip(1)
|
||||
.GroupBy(x => x)
|
||||
.ToDictionary(x => x.Key, x => x.Count())
|
||||
.OrderByDescending(x => x.Value)
|
||||
.ThenByDescending(x => x.Key)
|
||||
.ToDictionary(x => x.Key, x => x.Value)
|
||||
};
|
||||
stat.FramePerSecond = 1000d / stat.FrameDurations.Select(x => x.Key * ((double)x.Value / timestamps.Count)).Sum();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue
Block a user