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" Version="7.2.1" />
<PackageReference Include="Polly.Caching.Memory" Version="3.0.2" /> <PackageReference Include="Polly.Caching.Memory" Version="3.0.2" />
<PackageReference Include="Serilog" Version="2.10.0" /> <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.IO.Pipelines" Version="5.0.1" />
</ItemGroup> </ItemGroup>

View File

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

View File

@ -4,6 +4,8 @@ using System.Linq;
using BililiveRecorder.Core.Event; using BililiveRecorder.Core.Event;
using BililiveRecorder.Flv; using BililiveRecorder.Flv;
using BililiveRecorder.Flv.Pipeline; using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.Core.ProcessingRules namespace BililiveRecorder.Core.ProcessingRules
{ {
@ -32,10 +34,20 @@ namespace BililiveRecorder.Core.ProcessingRules
{ {
var e = new RecordingStatsEventArgs(); 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))); static IEnumerable<PipelineDataAction> FilterDataActions(IEnumerable<PipelineAction> actions)
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))); 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(); next();
@ -81,10 +93,17 @@ namespace BililiveRecorder.Core.ProcessingRules
{ {
if (dataActions.Count > 0) if (dataActions.Count > 0)
{ {
e.TotalOutputVideoFrameCount = this.TotalOutputVideoFrameCount += e.OutputVideoFrameCount = dataActions.Sum(x => x.Tags.Count(x => x.Type == TagType.Video)); e.TotalOutputVideoFrameCount = this.TotalOutputVideoFrameCount += e.OutputVideoFrameCount =
e.TotalOutputAudioFrameCount = this.TotalOutputAudioFrameCount += e.OutputAudioFrameCount = dataActions.Sum(x => x.Tags.Count(x => x.Type == TagType.Audio)); dataActions.ToStructEnumerable().Sum(ref LinqFunctions.CountVideoTags, x => x, x => x);
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.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; e.CurrnetFileSize = this.CurrnetFileSize += e.OutputVideoByteCount + e.OutputAudioByteCount;

View File

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

View File

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

View File

@ -1,36 +1,27 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using BililiveRecorder.Flv.Pipeline; using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.Flv.Grouping.Rules namespace BililiveRecorder.Flv.Grouping.Rules
{ {
public class DataGroupingRule : IGroupingRule public class DataGroupingRule : IGroupingRule
{ {
public bool StartWith(Tag tag) => tag.IsData(); private readonly struct DoesNotContainAudioData : IFunction<Tag, bool>
public bool AppendWith(Tag tag, LinkedList<Tag> tags, out LinkedList<Tag>? leftover)
{ {
var shouldAppend = public static DoesNotContainAudioData Instance;
// Tag 是非关键帧数据 public bool Eval(Tag element) => element.Type != TagType.Audio || element.Flag == TagFlag.Header;
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 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);
} }
} }

View File

@ -1,19 +1,14 @@
using System.Collections.Generic; using System.Collections.Generic;
using BililiveRecorder.Flv.Pipeline; using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv.Grouping.Rules namespace BililiveRecorder.Flv.Grouping.Rules
{ {
public class EndGroupingRule : IGroupingRule 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) public bool CanAppendWith(Tag tag, List<Tag> tags) => false;
{
leftover = new LinkedList<Tag>();
leftover.AddLast(tag);
return 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 System.Collections.Generic;
using BililiveRecorder.Flv.Pipeline; using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv.Grouping.Rules namespace BililiveRecorder.Flv.Grouping.Rules
{ {
public class HeaderGroupingRule : IGroupingRule 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) public bool CanAppendWith(Tag tag, List<Tag> tags) => tag.IsHeader();
{
if (tag.IsHeader())
{
tags.AddLast(tag);
leftover = null;
return true;
}
else
{
leftover = new LinkedList<Tag>();
leftover.AddLast(tag);
return false;
}
}
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 System.Collections.Generic;
using BililiveRecorder.Flv.Pipeline; using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv.Grouping.Rules namespace BililiveRecorder.Flv.Grouping.Rules
{ {
public class ScriptGroupingRule : IGroupingRule 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) public bool CanAppendWith(Tag tag, List<Tag> tags) => false;
{
leftover = new LinkedList<Tag>();
leftover.AddLast(tag);
return 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BililiveRecorder.Flv.Grouping.Rules; using BililiveRecorder.Flv.Grouping.Rules;
using BililiveRecorder.Flv.Pipeline; using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv.Grouping namespace BililiveRecorder.Flv.Grouping
{ {
@ -14,7 +13,7 @@ namespace BililiveRecorder.Flv.Grouping
private readonly bool leaveOpen; private readonly bool leaveOpen;
private bool disposedValue; private bool disposedValue;
private LinkedList<Tag>? leftover; private Tag? leftoverTag;
public IFlvTagReader TagReader { get; } public IFlvTagReader TagReader { get; }
public IList<IGroupingRule> GroupingRules { get; } public IList<IGroupingRule> GroupingRules { get; }
@ -45,16 +44,12 @@ namespace BililiveRecorder.Flv.Grouping
} }
try try
{ {
LinkedList<Tag>? queue = null;
Tag? firstTag; Tag? firstTag;
if (this.leftover is not null && this.leftover.Count > 0) if (this.leftoverTag is not null)
{ {
queue = this.leftover; firstTag = this.leftoverTag;
this.leftover = null; this.leftoverTag = null;
firstTag = queue.First.Value;
queue.RemoveFirst();
} }
else else
{ {
@ -65,38 +60,44 @@ namespace BililiveRecorder.Flv.Grouping
return null; 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) if (rule is null)
throw new Exception("No grouping rule accepting tags: " + firstTag.ToString()); throw new Exception("No grouping rule accepting tags: " + firstTag.ToString());
var tags = new LinkedList<Tag>(); var tags = new List<Tag>(32)
tags.AddLast(firstTag); {
firstTag
};
firstTag = null;
while (!token.IsCancellationRequested) 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; tags.Add(tag);
queue.RemoveFirst();
} }
else else
{ {
tag = await this.TagReader.ReadTagAsync(token).ConfigureAwait(false); // 如果数据已经读完,或当前规则不接受此 Tag
} this.leftoverTag = tag;
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);
}
break; break;
} }
} }

View File

@ -1,5 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using BililiveRecorder.Flv.Pipeline; using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv namespace BililiveRecorder.Flv
{ {
@ -10,17 +10,16 @@ namespace BililiveRecorder.Flv
/// </summary> /// </summary>
/// <param name="tags">Current Tags</param> /// <param name="tags">Current Tags</param>
/// <returns></returns> /// <returns></returns>
bool StartWith(Tag tag); bool CanStartWith(Tag tag);
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <param name="tag">Tag not yet added to the list</param> /// <param name="tag">Tag not yet added to the list</param>
/// <param name="tags">List of tags</param> /// <param name="tags">List of tags</param>
/// <param name="leftover"></param>
/// <returns></returns> /// <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;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using BililiveRecorder.Flv.Pipeline; using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv namespace BililiveRecorder.Flv
{ {

View File

@ -14,18 +14,46 @@ namespace BililiveRecorder.Flv
if (!iterator.MoveNext()) if (!iterator.MoveNext())
return false; return false;
var lastItem = iterator.Current; var previousItem = iterator.Current;
while (iterator.MoveNext()) while (iterator.MoveNext())
{ {
var current = iterator.Current; var currentItem = iterator.Current;
if (predicate(lastItem, current)) if (predicate(previousItem, currentItem))
return true; 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; 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 public abstract class PipelineAction
{ {

View File

@ -1,16 +1,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace BililiveRecorder.Flv.Pipeline namespace BililiveRecorder.Flv.Pipeline.Actions
{ {
public class PipelineDataAction : PipelineAction public class PipelineDataAction : PipelineAction
{ {
public PipelineDataAction(List<Tag> tags) public PipelineDataAction(IReadOnlyList<Tag> tags)
{ {
this.Tags = tags ?? throw new ArgumentNullException(nameof(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)); 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 public class PipelineDisconnectAction : PipelineAction
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,9 +26,6 @@ namespace BililiveRecorder.Flv.Pipeline
builder builder
.Add<HandleEndTagRule>() .Add<HandleEndTagRule>()
.Add<HandleDelayedAudioHeaderRule>() .Add<HandleDelayedAudioHeaderRule>()
// TODO .Add<CheckMissingKeyframeRule>()
// .Add<UpdateDataTagOrderRule>()
// .Add<CheckDiscontinuityRule>()
.Add<UpdateTimestampOffsetRule>() .Add<UpdateTimestampOffsetRule>()
.Add<UpdateTimestampJumpRule>() .Add<UpdateTimestampJumpRule>()
.Add<HandleNewScriptRule>() .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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.Flv.Pipeline.Rules namespace BililiveRecorder.Flv.Pipeline.Rules
{ {
@ -12,7 +14,8 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
/// </remarks> /// </remarks>
public class HandleDelayedAudioHeaderRule : ISimpleProcessingRule 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) public void Run(FlvProcessingContext context, Action next)
{ {
@ -25,25 +28,42 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
if (action is PipelineDataAction data) if (action is PipelineDataAction data)
{ {
var tags = data.Tags; 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 之前出现了音频数据 if (tags[i].Type == TagType.Audio)
yield return PipelineDisconnectAction.Instance; {
yield return null; if (tags[i].Flag != TagFlag.None)
yield break; {
// 发现了 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 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); var newDataAction = new PipelineDataAction(dataTags);
yield return newHeaderAction; yield return newHeaderAction;

View File

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

View File

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

View File

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

View File

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using BililiveRecorder.Flv; using BililiveRecorder.Flv;
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.Flv.Pipeline.Rules 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); context.AddComment(comment);
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using BililiveRecorder.Flv.Pipeline.Actions;
namespace BililiveRecorder.Flv.Pipeline.Rules 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.Flv.Pipeline.Rules namespace BililiveRecorder.Flv.Pipeline.Rules
{ {
@ -28,8 +31,27 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
foreach (var action in context.Actions) foreach (var action in context.Actions)
{ {
if (action is PipelineDataAction dataAction) if (action is PipelineDataAction dataAction)
{ { // SetDataTimestamp
this.SetDataTimestamp(dataAction.Tags, ts, context); 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) 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) [MethodImpl(MethodImplOptions.AggressiveInlining)]
{
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);
}
private int CalculateNewTarget(IReadOnlyList<Tag> tags) private int CalculateNewTarget(IReadOnlyList<Tag> tags)
{ {
// 有可能出现只有音频或只有视频的情况 // 有可能出现只有音频或只有视频的情况
int video = 0, audio = 0; 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); 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); audio = CalculatePerChannel(tags, AUDIO_DURATION_FALLBACK, AUDIO_DURATION_MAX, AUDIO_DURATION_MIN, TagType.Audio);
return Math.Max(video, audio); return Math.Max(video, audio);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static int CalculatePerChannel(IReadOnlyList<Tag> tags, int fallback, int max, int min, TagType type) 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; int durationPerTag;
if (sample.Length != 2) if (sample.Length != 2)
{ {
@ -116,7 +117,7 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
durationPerTag = fallback; 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.Flv.Pipeline.Rules namespace BililiveRecorder.Flv.Pipeline.Rules
{ {
@ -15,20 +18,25 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
next(); 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) private IEnumerable<PipelineAction?> RunPerAction(FlvProcessingContext context, PipelineAction action)
{ {
if (action is PipelineDataAction data) if (action is PipelineDataAction data)
{ {
var isNormal = this.CheckIfNormal(data.Tags); var isNotNormal = data.Tags.Any2(ref IsNotNormal.Instance);
if (isNormal) if (!isNotNormal)
{ {
// 没有问题
yield return data; yield return data;
yield break; 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; yield return PipelineDisconnectAction.Instance;
@ -37,146 +45,118 @@ namespace BililiveRecorder.Flv.Pipeline.Rules
} }
else else
{ {
var oc = new OffsetCalculator(); /*
*
*
* 使
* */
var offset = 0;
foreach (var tag in data.Tags) [MethodImpl(MethodImplOptions.AggressiveInlining)]
oc.AddTag(tag); static void ReduceOffsetRange(ref int maxOffset, ref int minOffset, Tag? leftAudio, Tag? rightAudio, Stack<Tag> tags)
if (oc.Calculate(out var videoOffset))
{ {
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 (leftAudio is not null)
if (tag.Type == TagType.Video) {
tag.Timestamp += videoOffset; 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; Tag? lastAudio = null;
yield break; 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 else
{ { // 范围无效
context.AddComment(comment2); goto invalidOffset;
yield return PipelineDisconnectAction.Instance;
yield break;
} }
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 else
yield return action; 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;
}
}
}
}
} }
} }

View File

@ -15,8 +15,8 @@ namespace BililiveRecorder.Flv
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsHeader(this Tag tag) 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsEnd(this Tag tag) public static bool IsEnd(this Tag tag)
@ -24,7 +24,8 @@ namespace BililiveRecorder.Flv
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsData(this Tag tag) 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsNonKeyframeData(this Tag tag) public static bool IsNonKeyframeData(this Tag tag)

View File

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

View File

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

View File

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

View File

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using BililiveRecorder.Flv; using BililiveRecorder.Flv;
using BililiveRecorder.Flv.Pipeline; using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
using StructLinq;
namespace BililiveRecorder.ToolBox.ProcessingRules namespace BililiveRecorder.ToolBox.ProcessingRules
{ {
@ -40,13 +42,14 @@ namespace BililiveRecorder.ToolBox.ProcessingRules
var stat = new FlvStats var stat = new FlvStats
{ {
FrameCount = timestamps.Count, FrameCount = timestamps.Count,
FrameDurations = timestamps.Select((time, i) => i == 0 ? 0 : time - timestamps[i - 1]) FrameDurations = timestamps
.Skip(1) .Select((time, i) => i == 0 ? 0 : time - timestamps[i - 1])
.GroupBy(x => x) .Skip(1)
.ToDictionary(x => x.Key, x => x.Count()) .GroupBy(x => x)
.OrderByDescending(x => x.Value) .ToDictionary(x => x.Key, x => x.Count())
.ThenByDescending(x => x.Key) .OrderByDescending(x => x.Value)
.ToDictionary(x => x.Key, 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(); stat.FramePerSecond = 1000d / stat.FrameDurations.Select(x => x.Key * ((double)x.Value / timestamps.Count)).Sum();

View File

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