From 8b4d467bd5397aa95f32373219a12cc565d18b9e Mon Sep 17 00:00:00 2001 From: genteure Date: Sat, 28 Sep 2024 00:24:36 +0800 Subject: [PATCH] feat: add option to disable spliting when H264 Annex-B is detected --- .../Configure/ConfigInstructions.gen.cs | 6 +- BililiveRecorder.Core/Config/V3/Config.gen.cs | 18 +++++ .../Recording/StandardRecordTask.cs | 3 +- .../Pipeline/ProcessingPipelineSettings.cs | 5 ++ .../Pipeline/Rules/HandleNewHeaderRule.cs | 79 +++++++++++++++++++ BililiveRecorder.Web/Models/Config.gen.cs | 11 +++ configV3.schema.json | 32 ++++++++ config_gen/data.ts | 7 ++ 8 files changed, 159 insertions(+), 2 deletions(-) diff --git a/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs b/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs index e0caee2..1848d0e 100644 --- a/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs +++ b/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs @@ -27,6 +27,7 @@ namespace BililiveRecorder.Cli.Configure RecordingQuality, FileNameRecordTemplate, FlvProcessorSplitOnScriptTag, + FlvProcessorDisableSplitOnH264AnnexB, FlvWriteMetadata, WebHookUrls, WebHookUrlsV2, @@ -65,7 +66,8 @@ namespace BililiveRecorder.Cli.Configure RecordDanmakuGuard, SaveStreamCover, RecordingQuality, - FlvProcessorSplitOnScriptTag + FlvProcessorSplitOnScriptTag, + FlvProcessorDisableSplitOnH264AnnexB } public static class ConfigInstructions { @@ -87,6 +89,7 @@ namespace BililiveRecorder.Cli.Configure GlobalConfig.Add(GlobalConfigProperties.RecordingQuality, new ConfigInstruction(config => config.HasRecordingQuality = false, (config, value) => config.RecordingQuality = value) { Name = "RecordingQuality", CanBeOptional = true }); GlobalConfig.Add(GlobalConfigProperties.FileNameRecordTemplate, new ConfigInstruction(config => config.HasFileNameRecordTemplate = false, (config, value) => config.FileNameRecordTemplate = value) { Name = "FileNameRecordTemplate", CanBeOptional = true }); GlobalConfig.Add(GlobalConfigProperties.FlvProcessorSplitOnScriptTag, new ConfigInstruction(config => config.HasFlvProcessorSplitOnScriptTag = false, (config, value) => config.FlvProcessorSplitOnScriptTag = value) { Name = "FlvProcessorSplitOnScriptTag", CanBeOptional = true }); + GlobalConfig.Add(GlobalConfigProperties.FlvProcessorDisableSplitOnH264AnnexB, new ConfigInstruction(config => config.HasFlvProcessorDisableSplitOnH264AnnexB = false, (config, value) => config.FlvProcessorDisableSplitOnH264AnnexB = value) { Name = "FlvProcessorDisableSplitOnH264AnnexB", CanBeOptional = true }); GlobalConfig.Add(GlobalConfigProperties.FlvWriteMetadata, new ConfigInstruction(config => config.HasFlvWriteMetadata = false, (config, value) => config.FlvWriteMetadata = value) { Name = "FlvWriteMetadata", CanBeOptional = true }); GlobalConfig.Add(GlobalConfigProperties.WebHookUrls, new ConfigInstruction(config => config.HasWebHookUrls = false, (config, value) => config.WebHookUrls = value) { Name = "WebHookUrls", CanBeOptional = true }); GlobalConfig.Add(GlobalConfigProperties.WebHookUrlsV2, new ConfigInstruction(config => config.HasWebHookUrlsV2 = false, (config, value) => config.WebHookUrlsV2 = value) { Name = "WebHookUrlsV2", CanBeOptional = true }); @@ -122,6 +125,7 @@ namespace BililiveRecorder.Cli.Configure RoomConfig.Add(RoomConfigProperties.SaveStreamCover, new ConfigInstruction(config => config.HasSaveStreamCover = false, (config, value) => config.SaveStreamCover = value) { Name = "SaveStreamCover", CanBeOptional = true }); RoomConfig.Add(RoomConfigProperties.RecordingQuality, new ConfigInstruction(config => config.HasRecordingQuality = false, (config, value) => config.RecordingQuality = value) { Name = "RecordingQuality", CanBeOptional = true }); RoomConfig.Add(RoomConfigProperties.FlvProcessorSplitOnScriptTag, new ConfigInstruction(config => config.HasFlvProcessorSplitOnScriptTag = false, (config, value) => config.FlvProcessorSplitOnScriptTag = value) { Name = "FlvProcessorSplitOnScriptTag", CanBeOptional = true }); + RoomConfig.Add(RoomConfigProperties.FlvProcessorDisableSplitOnH264AnnexB, new ConfigInstruction(config => config.HasFlvProcessorDisableSplitOnH264AnnexB = false, (config, value) => config.FlvProcessorDisableSplitOnH264AnnexB = value) { Name = "FlvProcessorDisableSplitOnH264AnnexB", CanBeOptional = true }); } } diff --git a/BililiveRecorder.Core/Config/V3/Config.gen.cs b/BililiveRecorder.Core/Config/V3/Config.gen.cs index 70716bf..0db5dfb 100644 --- a/BililiveRecorder.Core/Config/V3/Config.gen.cs +++ b/BililiveRecorder.Core/Config/V3/Config.gen.cs @@ -125,6 +125,14 @@ namespace BililiveRecorder.Core.Config.V3 [JsonProperty(nameof(FlvProcessorSplitOnScriptTag)), EditorBrowsable(EditorBrowsableState.Never)] public Optional OptionalFlvProcessorSplitOnScriptTag { get => this.GetPropertyValueOptional(nameof(this.FlvProcessorSplitOnScriptTag)); set => this.SetPropertyValueOptional(value, nameof(this.FlvProcessorSplitOnScriptTag)); } + /// + /// FLV修复-检测到 H264 Annex-B 时禁用修复分段 + /// + public bool FlvProcessorDisableSplitOnH264AnnexB { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasFlvProcessorDisableSplitOnH264AnnexB { get => this.GetPropertyHasValue(nameof(this.FlvProcessorDisableSplitOnH264AnnexB)); set => this.SetPropertyHasValue(value, nameof(this.FlvProcessorDisableSplitOnH264AnnexB)); } + [JsonProperty(nameof(FlvProcessorDisableSplitOnH264AnnexB)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalFlvProcessorDisableSplitOnH264AnnexB { get => this.GetPropertyValueOptional(nameof(this.FlvProcessorDisableSplitOnH264AnnexB)); set => this.SetPropertyValueOptional(value, nameof(this.FlvProcessorDisableSplitOnH264AnnexB)); } + /// /// 录制文件名模板 /// @@ -339,6 +347,14 @@ namespace BililiveRecorder.Core.Config.V3 [JsonProperty(nameof(FlvProcessorSplitOnScriptTag)), EditorBrowsable(EditorBrowsableState.Never)] public Optional OptionalFlvProcessorSplitOnScriptTag { get => this.GetPropertyValueOptional(nameof(this.FlvProcessorSplitOnScriptTag)); set => this.SetPropertyValueOptional(value, nameof(this.FlvProcessorSplitOnScriptTag)); } + /// + /// FLV修复-检测到 H264 Annex-B 时禁用修复分段 + /// + public bool FlvProcessorDisableSplitOnH264AnnexB { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasFlvProcessorDisableSplitOnH264AnnexB { get => this.GetPropertyHasValue(nameof(this.FlvProcessorDisableSplitOnH264AnnexB)); set => this.SetPropertyHasValue(value, nameof(this.FlvProcessorDisableSplitOnH264AnnexB)); } + [JsonProperty(nameof(FlvProcessorDisableSplitOnH264AnnexB)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalFlvProcessorDisableSplitOnH264AnnexB { get => this.GetPropertyValueOptional(nameof(this.FlvProcessorDisableSplitOnH264AnnexB)); set => this.SetPropertyValueOptional(value, nameof(this.FlvProcessorDisableSplitOnH264AnnexB)); } + /// /// 是否在视频文件写入直播信息 metadata /// @@ -532,6 +548,8 @@ namespace BililiveRecorder.Core.Config.V3 public bool FlvProcessorSplitOnScriptTag => false; + public bool FlvProcessorDisableSplitOnH264AnnexB => false; + public bool FlvWriteMetadata => true; public string WebHookUrls => @""; diff --git a/BililiveRecorder.Core/Recording/StandardRecordTask.cs b/BililiveRecorder.Core/Recording/StandardRecordTask.cs index 13f5b33..81c745d 100644 --- a/BililiveRecorder.Core/Recording/StandardRecordTask.cs +++ b/BililiveRecorder.Core/Recording/StandardRecordTask.cs @@ -64,7 +64,8 @@ namespace BililiveRecorder.Core.Recording this.pipeline = builder .ConfigureServices(services => services.AddSingleton(new ProcessingPipelineSettings { - SplitOnScriptTag = room.RoomConfig.FlvProcessorSplitOnScriptTag + SplitOnScriptTag = room.RoomConfig.FlvProcessorSplitOnScriptTag, + DisableSplitOnH264AnnexB = room.RoomConfig.FlvProcessorDisableSplitOnH264AnnexB, })) .AddRule(this.statsRule) .AddRule(this.splitFileRule) diff --git a/BililiveRecorder.Flv/Pipeline/ProcessingPipelineSettings.cs b/BililiveRecorder.Flv/Pipeline/ProcessingPipelineSettings.cs index c29e17c..1afebf8 100644 --- a/BililiveRecorder.Flv/Pipeline/ProcessingPipelineSettings.cs +++ b/BililiveRecorder.Flv/Pipeline/ProcessingPipelineSettings.cs @@ -9,5 +9,10 @@ namespace BililiveRecorder.Flv.Pipeline /// 控制收到 onMetaData 时是否分段 /// public bool SplitOnScriptTag { get; set; } = false; + + /// + /// 检测到 H264 Annex-B 时禁用修复分段 + /// + public bool DisableSplitOnH264AnnexB { get; set; } = false; } } diff --git a/BililiveRecorder.Flv/Pipeline/Rules/HandleNewHeaderRule.cs b/BililiveRecorder.Flv/Pipeline/Rules/HandleNewHeaderRule.cs index ef3627e..d7dbd5b 100644 --- a/BililiveRecorder.Flv/Pipeline/Rules/HandleNewHeaderRule.cs +++ b/BililiveRecorder.Flv/Pipeline/Rules/HandleNewHeaderRule.cs @@ -15,9 +15,26 @@ namespace BililiveRecorder.Flv.Pipeline.Rules { private const string VIDEO_HEADER_KEY = "HandleNewHeaderRule_VideoHeader"; private const string AUDIO_HEADER_KEY = "HandleNewHeaderRule_AudioHeader"; + private const string ANNEXB_KEY = "HandleNewHeaderRule_AnnexB"; + + private enum AnnexBState + { + Unknown, + Pending, + IsAnnexB, + } private static readonly ProcessingComment MultipleHeaderComment = new ProcessingComment(CommentType.DecodingHeader, true, "收到了连续多个 Header,新建文件"); private static readonly ProcessingComment SplitFileComment = new ProcessingComment(CommentType.DecodingHeader, true, "因为 Header 问题新建文件"); + private static readonly ProcessingComment AnnexBCommentFirst = new ProcessingComment(CommentType.DecodingHeader, true, "检测到一次 AnnexB 格式"); + private static readonly ProcessingComment AnnexBComment = new ProcessingComment(CommentType.DecodingHeader, true, "检测到 AnnexB 格式,不再切割文件"); + + private readonly bool disableSplitOnH264AnnexB; + + public HandleNewHeaderRule(ProcessingPipelineSettings? processingPipelineSettings) + { + this.disableSplitOnH264AnnexB = processingPipelineSettings?.DisableSplitOnH264AnnexB ?? false; + } public void Run(FlvProcessingContext context, Action next) { @@ -27,8 +44,70 @@ namespace BililiveRecorder.Flv.Pipeline.Rules private IEnumerable RunPerAction(FlvProcessingContext context, PipelineAction action) { + if (this.disableSplitOnH264AnnexB) + { + var state = context.SessionItems.ContainsKey(ANNEXB_KEY) ? (AnnexBState)context.SessionItems[ANNEXB_KEY] : AnnexBState.Unknown; + + if (state == AnnexBState.IsAnnexB) + { + // 如果已经检测到 Annex B 格式则不再切割文件 + yield return action; + yield break; + } + + if (action is PipelineDataAction data) + { + var annexb = false; + + for (var i = 0; i < data.Tags.Count; i++) + { + var tag = data.Tags[i]; + if (tag.IsKeyframeData()) + { + // Check nalu + if (tag.Nalus is { } nalus) + { + bool sps = false, pps = false; + foreach (var nalu in nalus) + { + if (nalu.Type == H264NaluType.Sps) + sps = true; + if (nalu.Type == H264NaluType.Pps) + pps = true; + } + annexb = sps && pps; + } + + break; + } + } + + if (annexb) + { + if (state == AnnexBState.Unknown) + { + context.SessionItems[ANNEXB_KEY] = AnnexBState.Pending; + context.AddComment(AnnexBCommentFirst); + } + else + { + context.SessionItems[ANNEXB_KEY] = AnnexBState.IsAnnexB; + context.AddComment(AnnexBComment); + } + } + + yield return action; + yield break; + } // if (action is PipelineDataAction data) + } // if (this.disableSplitOnH264AnnexB) + if (action is PipelineHeaderAction header) { + if (this.disableSplitOnH264AnnexB) + { + context.SessionItems.Remove(ANNEXB_KEY); + } + // 从 Session Items 里取上次写入的 Header var lastVideoHeader = context.SessionItems.ContainsKey(VIDEO_HEADER_KEY) ? context.SessionItems[VIDEO_HEADER_KEY] as Tag : null; var lastAudioHeader = context.SessionItems.ContainsKey(AUDIO_HEADER_KEY) ? context.SessionItems[AUDIO_HEADER_KEY] as Tag : null; diff --git a/BililiveRecorder.Web/Models/Config.gen.cs b/BililiveRecorder.Web/Models/Config.gen.cs index 83b9fff..d4f810b 100644 --- a/BililiveRecorder.Web/Models/Config.gen.cs +++ b/BililiveRecorder.Web/Models/Config.gen.cs @@ -25,6 +25,7 @@ namespace BililiveRecorder.Web.Models public Optional? OptionalSaveStreamCover { get; set; } public Optional? OptionalRecordingQuality { get; set; } public Optional? OptionalFlvProcessorSplitOnScriptTag { get; set; } + public Optional? OptionalFlvProcessorDisableSplitOnH264AnnexB { get; set; } public void ApplyTo(RoomConfig config) { @@ -41,6 +42,7 @@ namespace BililiveRecorder.Web.Models if (this.OptionalSaveStreamCover.HasValue) config.OptionalSaveStreamCover = this.OptionalSaveStreamCover.Value; if (this.OptionalRecordingQuality.HasValue) config.OptionalRecordingQuality = this.OptionalRecordingQuality.Value; if (this.OptionalFlvProcessorSplitOnScriptTag.HasValue) config.OptionalFlvProcessorSplitOnScriptTag = this.OptionalFlvProcessorSplitOnScriptTag.Value; + if (this.OptionalFlvProcessorDisableSplitOnH264AnnexB.HasValue) config.OptionalFlvProcessorDisableSplitOnH264AnnexB = this.OptionalFlvProcessorDisableSplitOnH264AnnexB.Value; } } @@ -59,6 +61,7 @@ namespace BililiveRecorder.Web.Models public Optional? OptionalRecordingQuality { get; set; } public Optional? OptionalFileNameRecordTemplate { get; set; } public Optional? OptionalFlvProcessorSplitOnScriptTag { get; set; } + public Optional? OptionalFlvProcessorDisableSplitOnH264AnnexB { get; set; } public Optional? OptionalFlvWriteMetadata { get; set; } public Optional? OptionalWebHookUrls { get; set; } public Optional? OptionalWebHookUrlsV2 { get; set; } @@ -95,6 +98,7 @@ namespace BililiveRecorder.Web.Models if (this.OptionalRecordingQuality.HasValue) config.OptionalRecordingQuality = this.OptionalRecordingQuality.Value; if (this.OptionalFileNameRecordTemplate.HasValue) config.OptionalFileNameRecordTemplate = this.OptionalFileNameRecordTemplate.Value; if (this.OptionalFlvProcessorSplitOnScriptTag.HasValue) config.OptionalFlvProcessorSplitOnScriptTag = this.OptionalFlvProcessorSplitOnScriptTag.Value; + if (this.OptionalFlvProcessorDisableSplitOnH264AnnexB.HasValue) config.OptionalFlvProcessorDisableSplitOnH264AnnexB = this.OptionalFlvProcessorDisableSplitOnH264AnnexB.Value; if (this.OptionalFlvWriteMetadata.HasValue) config.OptionalFlvWriteMetadata = this.OptionalFlvWriteMetadata.Value; if (this.OptionalWebHookUrls.HasValue) config.OptionalWebHookUrls = this.OptionalWebHookUrls.Value; if (this.OptionalWebHookUrlsV2.HasValue) config.OptionalWebHookUrlsV2 = this.OptionalWebHookUrlsV2.Value; @@ -137,6 +141,7 @@ namespace BililiveRecorder.Web.Models.Rest public Optional OptionalSaveStreamCover { get; set; } public Optional OptionalRecordingQuality { get; set; } public Optional OptionalFlvProcessorSplitOnScriptTag { get; set; } + public Optional OptionalFlvProcessorDisableSplitOnH264AnnexB { get; set; } } public class GlobalConfigDto @@ -154,6 +159,7 @@ namespace BililiveRecorder.Web.Models.Rest public Optional OptionalRecordingQuality { get; set; } public Optional OptionalFileNameRecordTemplate { get; set; } public Optional OptionalFlvProcessorSplitOnScriptTag { get; set; } + public Optional OptionalFlvProcessorDisableSplitOnH264AnnexB { get; set; } public Optional OptionalFlvWriteMetadata { get; set; } public Optional OptionalWebHookUrls { get; set; } public Optional OptionalWebHookUrlsV2 { get; set; } @@ -198,6 +204,7 @@ namespace BililiveRecorder.Web.Models.Graphql this.Field(x => x.OptionalSaveStreamCover, type: typeof(HierarchicalOptionalType)); this.Field(x => x.OptionalRecordingQuality, type: typeof(HierarchicalOptionalType)); this.Field(x => x.OptionalFlvProcessorSplitOnScriptTag, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalFlvProcessorDisableSplitOnH264AnnexB, type: typeof(HierarchicalOptionalType)); } } @@ -218,6 +225,7 @@ namespace BililiveRecorder.Web.Models.Graphql this.Field(x => x.OptionalRecordingQuality, type: typeof(HierarchicalOptionalType)); this.Field(x => x.OptionalFileNameRecordTemplate, type: typeof(HierarchicalOptionalType)); this.Field(x => x.OptionalFlvProcessorSplitOnScriptTag, type: typeof(HierarchicalOptionalType)); + this.Field(x => x.OptionalFlvProcessorDisableSplitOnH264AnnexB, type: typeof(HierarchicalOptionalType)); this.Field(x => x.OptionalFlvWriteMetadata, type: typeof(HierarchicalOptionalType)); this.Field(x => x.OptionalWebHookUrls, type: typeof(HierarchicalOptionalType)); this.Field(x => x.OptionalWebHookUrlsV2, type: typeof(HierarchicalOptionalType)); @@ -258,6 +266,7 @@ namespace BililiveRecorder.Web.Models.Graphql this.Field(x => x.RecordingQuality); this.Field(x => x.FileNameRecordTemplate); this.Field(x => x.FlvProcessorSplitOnScriptTag); + this.Field(x => x.FlvProcessorDisableSplitOnH264AnnexB); this.Field(x => x.FlvWriteMetadata); this.Field(x => x.WebHookUrls); this.Field(x => x.WebHookUrlsV2); @@ -298,6 +307,7 @@ namespace BililiveRecorder.Web.Models.Graphql this.Field(x => x.OptionalSaveStreamCover, nullable: true, type: typeof(HierarchicalOptionalInputType)); this.Field(x => x.OptionalRecordingQuality, nullable: true, type: typeof(HierarchicalOptionalInputType)); this.Field(x => x.OptionalFlvProcessorSplitOnScriptTag, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalFlvProcessorDisableSplitOnH264AnnexB, nullable: true, type: typeof(HierarchicalOptionalInputType)); } } @@ -318,6 +328,7 @@ namespace BililiveRecorder.Web.Models.Graphql this.Field(x => x.OptionalRecordingQuality, nullable: true, type: typeof(HierarchicalOptionalInputType)); this.Field(x => x.OptionalFileNameRecordTemplate, nullable: true, type: typeof(HierarchicalOptionalInputType)); this.Field(x => x.OptionalFlvProcessorSplitOnScriptTag, nullable: true, type: typeof(HierarchicalOptionalInputType)); + this.Field(x => x.OptionalFlvProcessorDisableSplitOnH264AnnexB, nullable: true, type: typeof(HierarchicalOptionalInputType)); this.Field(x => x.OptionalFlvWriteMetadata, nullable: true, type: typeof(HierarchicalOptionalInputType)); this.Field(x => x.OptionalWebHookUrls, nullable: true, type: typeof(HierarchicalOptionalInputType)); this.Field(x => x.OptionalWebHookUrlsV2, nullable: true, type: typeof(HierarchicalOptionalInputType)); diff --git a/configV3.schema.json b/configV3.schema.json index 25a0b62..4f528e1 100644 --- a/configV3.schema.json +++ b/configV3.schema.json @@ -577,6 +577,22 @@ "default": false } } + }, + "FlvProcessorDisableSplitOnH264AnnexB": { + "description": "FLV修复-检测到 H264 Annex-B 时禁用修复分段\n默认: false", + "markdownDescription": "FLV修复-检测到 H264 Annex-B 时禁用修复分段 \n默认: `false `\n\n", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "boolean", + "default": false + } + } } } }, @@ -822,6 +838,22 @@ "default": false } } + }, + "FlvProcessorDisableSplitOnH264AnnexB": { + "description": "FLV修复-检测到 H264 Annex-B 时禁用修复分段\n默认: false", + "markdownDescription": "FLV修复-检测到 H264 Annex-B 时禁用修复分段 \n默认: `false `\n\n", + "type": "object", + "additionalProperties": false, + "properties": { + "HasValue": { + "type": "boolean", + "default": true + }, + "Value": { + "type": "boolean", + "default": false + } + } } } } diff --git a/config_gen/data.ts b/config_gen/data.ts index cc4b267..2efef9f 100644 --- a/config_gen/data.ts +++ b/config_gen/data.ts @@ -107,6 +107,13 @@ export const data: Array = [ configType: "room", default: false }, + { + id: "FlvProcessorDisableSplitOnH264AnnexB", + name: "FLV修复-检测到 H264 Annex-B 时禁用修复分段", + type: "bool", + configType: "room", + default: false + }, { id: "FlvWriteMetadata", name: "是否在视频文件写入直播信息 metadata",