diff --git a/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs b/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs index 2aecfba..e1d07a2 100644 --- a/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs +++ b/BililiveRecorder.Cli/Configure/ConfigInstructions.gen.cs @@ -24,6 +24,7 @@ namespace BililiveRecorder.Cli.Configure RecordDanmakuGuard, RecordingQuality, FileNameRecordTemplate, + FlvProcessorSplitOnScriptTag, WebHookUrls, WebHookUrlsV2, WpfShowTitleAndArea, @@ -54,7 +55,8 @@ namespace BililiveRecorder.Cli.Configure RecordDanmakuSuperChat, RecordDanmakuGift, RecordDanmakuGuard, - RecordingQuality + RecordingQuality, + FlvProcessorSplitOnScriptTag } public static class ConfigInstructions { @@ -73,6 +75,7 @@ namespace BililiveRecorder.Cli.Configure GlobalConfig.Add(GlobalConfigProperties.RecordDanmakuGuard, new ConfigInstruction(config => config.HasRecordDanmakuGuard = false, (config, value) => config.RecordDanmakuGuard = value) { Name = "RecordDanmakuGuard", CanBeOptional = true }); 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.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 }); GlobalConfig.Add(GlobalConfigProperties.WpfShowTitleAndArea, new ConfigInstruction(config => config.HasWpfShowTitleAndArea = false, (config, value) => config.WpfShowTitleAndArea = value) { Name = "WpfShowTitleAndArea", CanBeOptional = true }); @@ -100,6 +103,7 @@ namespace BililiveRecorder.Cli.Configure RoomConfig.Add(RoomConfigProperties.RecordDanmakuGift, new ConfigInstruction(config => config.HasRecordDanmakuGift = false, (config, value) => config.RecordDanmakuGift = value) { Name = "RecordDanmakuGift", CanBeOptional = true }); RoomConfig.Add(RoomConfigProperties.RecordDanmakuGuard, new ConfigInstruction(config => config.HasRecordDanmakuGuard = false, (config, value) => config.RecordDanmakuGuard = value) { Name = "RecordDanmakuGuard", 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 }); } } diff --git a/BililiveRecorder.Core/Config/V3/Config.gen.cs b/BililiveRecorder.Core/Config/V3/Config.gen.cs index 43454c1..d178df6 100644 --- a/BililiveRecorder.Core/Config/V3/Config.gen.cs +++ b/BililiveRecorder.Core/Config/V3/Config.gen.cs @@ -101,6 +101,14 @@ namespace BililiveRecorder.Core.Config.V3 [JsonProperty(nameof(RecordingQuality)), EditorBrowsable(EditorBrowsableState.Never)] public Optional OptionalRecordingQuality { get => this.GetPropertyValueOptional(nameof(this.RecordingQuality)); set => this.SetPropertyValueOptional(value, nameof(this.RecordingQuality)); } + /// + /// FLV修复-检测到可能缺少数据时分段 + /// + public bool FlvProcessorSplitOnScriptTag { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasFlvProcessorSplitOnScriptTag { get => this.GetPropertyHasValue(nameof(this.FlvProcessorSplitOnScriptTag)); set => this.SetPropertyHasValue(value, nameof(this.FlvProcessorSplitOnScriptTag)); } + [JsonProperty(nameof(FlvProcessorSplitOnScriptTag)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalFlvProcessorSplitOnScriptTag { get => this.GetPropertyValueOptional(nameof(this.FlvProcessorSplitOnScriptTag)); set => this.SetPropertyValueOptional(value, nameof(this.FlvProcessorSplitOnScriptTag)); } + /// /// 录制文件名模板 /// @@ -266,6 +274,14 @@ namespace BililiveRecorder.Core.Config.V3 [JsonProperty(nameof(FileNameRecordTemplate)), EditorBrowsable(EditorBrowsableState.Never)] public Optional OptionalFileNameRecordTemplate { get => this.GetPropertyValueOptional(nameof(this.FileNameRecordTemplate)); set => this.SetPropertyValueOptional(value, nameof(this.FileNameRecordTemplate)); } + /// + /// FLV修复-检测到可能缺少数据时分段 + /// + public bool FlvProcessorSplitOnScriptTag { get => this.GetPropertyValue(); set => this.SetPropertyValue(value); } + public bool HasFlvProcessorSplitOnScriptTag { get => this.GetPropertyHasValue(nameof(this.FlvProcessorSplitOnScriptTag)); set => this.SetPropertyHasValue(value, nameof(this.FlvProcessorSplitOnScriptTag)); } + [JsonProperty(nameof(FlvProcessorSplitOnScriptTag)), EditorBrowsable(EditorBrowsableState.Never)] + public Optional OptionalFlvProcessorSplitOnScriptTag { get => this.GetPropertyValueOptional(nameof(this.FlvProcessorSplitOnScriptTag)); set => this.SetPropertyValueOptional(value, nameof(this.FlvProcessorSplitOnScriptTag)); } + /// /// WebhookV1 /// @@ -413,6 +429,8 @@ namespace BililiveRecorder.Core.Config.V3 public string FileNameRecordTemplate => @"{{ roomId }}-{{ name }}/录制-{{ roomId }}-{{ ""now"" | time_zone: ""Asia/Shanghai"" | format_date: ""yyyyMMdd-HHmmss-fff"" }}-{{ title }}.flv"; + public bool FlvProcessorSplitOnScriptTag => false; + public string WebHookUrls => @""; public string WebHookUrlsV2 => @""; diff --git a/BililiveRecorder.Core/Recording/StandardRecordTask.cs b/BililiveRecorder.Core/Recording/StandardRecordTask.cs index 4daedd0..caef975 100644 --- a/BililiveRecorder.Core/Recording/StandardRecordTask.cs +++ b/BililiveRecorder.Core/Recording/StandardRecordTask.cs @@ -15,6 +15,7 @@ using BililiveRecorder.Flv; using BililiveRecorder.Flv.Amf; using BililiveRecorder.Flv.Pipeline; using BililiveRecorder.Flv.Pipeline.Actions; +using Microsoft.Extensions.DependencyInjection; using Serilog; namespace BililiveRecorder.Core.Recording @@ -63,10 +64,14 @@ namespace BililiveRecorder.Core.Recording this.statsRule.StatsUpdated += this.StatsRule_StatsUpdated; this.pipeline = builder - .Add(this.statsRule) - .Add(this.splitFileRule) - .AddDefault() - .AddRemoveFillerData() + .ConfigureServices(services => services.AddSingleton(new ProcessingPipelineSettings + { + SplitOnScriptTag = room.RoomConfig.FlvProcessorSplitOnScriptTag + })) + .AddRule(this.statsRule) + .AddRule(this.splitFileRule) + .AddDefaultRules() + .AddRemoveFillerDataRule() .Build(); this.targetProvider = new WriterTargetProvider(this, paths => diff --git a/BililiveRecorder.Flv/BililiveRecorder.Flv.csproj b/BililiveRecorder.Flv/BililiveRecorder.Flv.csproj index 06a4e35..f4cb2b1 100644 --- a/BililiveRecorder.Flv/BililiveRecorder.Flv.csproj +++ b/BililiveRecorder.Flv/BililiveRecorder.Flv.csproj @@ -11,6 +11,7 @@ + diff --git a/BililiveRecorder.Flv/Pipeline/IProcessingPipelineBuilder.cs b/BililiveRecorder.Flv/Pipeline/IProcessingPipelineBuilder.cs index 6be0997..a5eb3b7 100644 --- a/BililiveRecorder.Flv/Pipeline/IProcessingPipelineBuilder.cs +++ b/BililiveRecorder.Flv/Pipeline/IProcessingPipelineBuilder.cs @@ -1,12 +1,13 @@ using System; +using Microsoft.Extensions.DependencyInjection; namespace BililiveRecorder.Flv.Pipeline { public interface IProcessingPipelineBuilder { - IServiceProvider ServiceProvider { get; } + IServiceCollection ServiceCollection { get; } - IProcessingPipelineBuilder Add(Func rule); + IProcessingPipelineBuilder AddRule(Func rule); ProcessingDelegate Build(); } diff --git a/BililiveRecorder.Flv/Pipeline/IProcessingPipelineBuilderExtensions.cs b/BililiveRecorder.Flv/Pipeline/IProcessingPipelineBuilderExtensions.cs index e96c1ca..01f6e97 100644 --- a/BililiveRecorder.Flv/Pipeline/IProcessingPipelineBuilderExtensions.cs +++ b/BililiveRecorder.Flv/Pipeline/IProcessingPipelineBuilderExtensions.cs @@ -6,34 +6,40 @@ namespace BililiveRecorder.Flv.Pipeline { public static class IProcessingPipelineBuilderExtensions { - public static IProcessingPipelineBuilder Add(this IProcessingPipelineBuilder builder) where T : IProcessingRule => - builder.Add(next => (ActivatorUtilities.GetServiceOrCreateInstance(builder.ServiceProvider)) switch + public static IProcessingPipelineBuilder ConfigureServices(this IProcessingPipelineBuilder builder, Action configure) + { + configure?.Invoke(builder.ServiceCollection); + return builder; + } + + public static IProcessingPipelineBuilder AddRule(this IProcessingPipelineBuilder builder) where T : IProcessingRule => + builder.AddRule((next, services) => ActivatorUtilities.GetServiceOrCreateInstance(services) switch { ISimpleProcessingRule simple => context => simple.Run(context, () => next(context)), IFullProcessingRule full => context => full.Run(context, next), _ => throw new ArgumentException($"Type ({typeof(T).FullName}) does not ISimpleProcessingRule or IFullProcessingRule") }); - public static IProcessingPipelineBuilder Add(this IProcessingPipelineBuilder builder, T instance) where T : IProcessingRule => + public static IProcessingPipelineBuilder AddRule(this IProcessingPipelineBuilder builder, T instance) where T : IProcessingRule => instance switch { - ISimpleProcessingRule simple => builder.Add(next => context => simple.Run(context, () => next(context))), - IFullProcessingRule full => builder.Add(next => context => full.Run(context, next)), + ISimpleProcessingRule simple => builder.AddRule((next, services) => context => simple.Run(context, () => next(context))), + IFullProcessingRule full => builder.AddRule((next, services) => context => full.Run(context, next)), _ => throw new ArgumentException($"Type ({typeof(T).FullName}) does not ISimpleProcessingRule or IFullProcessingRule") }; - public static IProcessingPipelineBuilder AddDefault(this IProcessingPipelineBuilder builder) => + public static IProcessingPipelineBuilder AddDefaultRules(this IProcessingPipelineBuilder builder) => builder - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() - .Add() + .AddRule() + .AddRule() + .AddRule() + .AddRule() + .AddRule() + .AddRule() + .AddRule() ; - public static IProcessingPipelineBuilder AddRemoveFillerData(this IProcessingPipelineBuilder builder) => - builder.Add(); + public static IProcessingPipelineBuilder AddRemoveFillerDataRule(this IProcessingPipelineBuilder builder) => + builder.AddRule(); } } diff --git a/BililiveRecorder.Flv/Pipeline/ProcessingPipelineBuilder.cs b/BililiveRecorder.Flv/Pipeline/ProcessingPipelineBuilder.cs index bf3cd0b..0798dc2 100644 --- a/BililiveRecorder.Flv/Pipeline/ProcessingPipelineBuilder.cs +++ b/BililiveRecorder.Flv/Pipeline/ProcessingPipelineBuilder.cs @@ -1,27 +1,36 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace BililiveRecorder.Flv.Pipeline { public class ProcessingPipelineBuilder : IProcessingPipelineBuilder { - public IServiceProvider ServiceProvider { get; } + public IServiceCollection ServiceCollection { get; } - private readonly List> rules = new List>(); + private readonly List> rules = new(); - public ProcessingPipelineBuilder(IServiceProvider serviceProvider) + public ProcessingPipelineBuilder() : this(new ServiceCollection()) + { } + + public ProcessingPipelineBuilder(IServiceCollection servicesCollection) { - this.ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + this.ServiceCollection = servicesCollection; } - public IProcessingPipelineBuilder Add(Func rule) + public IProcessingPipelineBuilder AddRule(Func rule) { this.rules.Add(rule); return this; } public ProcessingDelegate Build() - => this.rules.AsEnumerable().Reverse().Aggregate((ProcessingDelegate)(_ => { }), (i, o) => o(i)); + { + this.ServiceCollection.TryAddSingleton(_ => new ProcessingPipelineSettings()); + var provider = this.ServiceCollection.BuildServiceProvider(); + return this.rules.AsEnumerable().Reverse().Aggregate((ProcessingDelegate)(_ => { }), (i, o) => o(i, provider)); + } } } diff --git a/BililiveRecorder.Flv/Pipeline/ProcessingPipelineSettings.cs b/BililiveRecorder.Flv/Pipeline/ProcessingPipelineSettings.cs new file mode 100644 index 0000000..c29e17c --- /dev/null +++ b/BililiveRecorder.Flv/Pipeline/ProcessingPipelineSettings.cs @@ -0,0 +1,13 @@ +namespace BililiveRecorder.Flv.Pipeline +{ + public class ProcessingPipelineSettings + { + public ProcessingPipelineSettings() + { } + + /// + /// 控制收到 onMetaData 时是否分段 + /// + public bool SplitOnScriptTag { get; set; } = false; + } +} diff --git a/BililiveRecorder.Flv/Pipeline/Rules/HandleNewScriptRule.cs b/BililiveRecorder.Flv/Pipeline/Rules/HandleNewScriptRule.cs index 8468950..ff41d1d 100644 --- a/BililiveRecorder.Flv/Pipeline/Rules/HandleNewScriptRule.cs +++ b/BililiveRecorder.Flv/Pipeline/Rules/HandleNewScriptRule.cs @@ -14,6 +14,13 @@ namespace BililiveRecorder.Flv.Pipeline.Rules private const string onMetaData = "onMetaData"; private static readonly ProcessingComment comment_onmetadata = new ProcessingComment(CommentType.OnMetaData, false, "收到了 onMetaData"); + private readonly bool splitOnScriptTag; + + public HandleNewScriptRule(ProcessingPipelineSettings? processingPipelineSettings) + { + this.splitOnScriptTag = processingPipelineSettings?.SplitOnScriptTag ?? false; + } + public void Run(FlvProcessingContext context, Action next) { context.PerActionRun(this.RunPerAction); @@ -93,11 +100,20 @@ namespace BililiveRecorder.Flv.Pipeline.Rules } else { - // 记录信息,不对文件进行分段。 var message = $"收到直播服务器发送的 onMetaData 数据,请检查此位置是否有重复的直播片段或缺少数据。\n造成这个问题的原因可能是录播姬所连接的直播服务器与它的上级服务器的连接断开重连了。\n数据内容: {data?.ToJson() ?? "(null)"}"; context.AddComment(new ProcessingComment(CommentType.OnMetaData, false, message)); - yield return new PipelineLogMessageWithLocationAction(message); + if (this.splitOnScriptTag) + { + // 对文件进行分段 + yield return PipelineNewFileAction.Instance; + } + else + { + // 记录信息,不对文件进行分段。 + yield return new PipelineLogMessageWithLocationAction(message); + } + yield return (new PipelineScriptAction(new Tag { Type = TagType.Script, @@ -107,6 +123,7 @@ namespace BililiveRecorder.Flv.Pipeline.Rules value }) })); + yield break; } notOnMetaData: diff --git a/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeHandler.cs b/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeHandler.cs index cf57104..4f34cca 100644 --- a/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeHandler.cs +++ b/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeHandler.cs @@ -95,7 +95,13 @@ namespace BililiveRecorder.ToolBox.Tool.Analyze using var writer = new FlvProcessingContextWriter(tagWriter: tagWriter, allowMissingHeader: true, disableKeyframes: true, logger: logger); var statsRule = new StatsRule(); var ffmpegDetectionRule = new FfmpegDetectionRule(); - var pipeline = new ProcessingPipelineBuilder(new ServiceCollection().BuildServiceProvider()).Add(statsRule).Add(ffmpegDetectionRule).AddDefault().AddRemoveFillerData().Build(); + var pipeline = new ProcessingPipelineBuilder() + .ConfigureServices(services => services.AddSingleton(request.PipelineSettings ?? new ProcessingPipelineSettings())) + .AddRule(statsRule) + .AddRule(ffmpegDetectionRule) + .AddDefaultRules() + .AddRemoveFillerDataRule() + .Build(); // Run await Task.Run(async () => diff --git a/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeRequest.cs b/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeRequest.cs index 139ad92..5767886 100644 --- a/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeRequest.cs +++ b/BililiveRecorder.ToolBox/Tool/Analyze/AnalyzeRequest.cs @@ -1,7 +1,11 @@ -namespace BililiveRecorder.ToolBox.Tool.Analyze +using BililiveRecorder.Flv.Pipeline; + +namespace BililiveRecorder.ToolBox.Tool.Analyze { public class AnalyzeRequest : ICommandRequest { public string Input { get; set; } = string.Empty; + + public ProcessingPipelineSettings? PipelineSettings { get; set; } } } diff --git a/BililiveRecorder.ToolBox/Tool/Fix/FixHandler.cs b/BililiveRecorder.ToolBox/Tool/Fix/FixHandler.cs index 5b1b4ad..6ad96c5 100644 --- a/BililiveRecorder.ToolBox/Tool/Fix/FixHandler.cs +++ b/BililiveRecorder.ToolBox/Tool/Fix/FixHandler.cs @@ -114,7 +114,13 @@ namespace BililiveRecorder.ToolBox.Tool.Fix using var writer = new FlvProcessingContextWriter(tagWriter: tagWriter, allowMissingHeader: true, disableKeyframes: false, logger: logger); var statsRule = new StatsRule(); var ffmpegDetectionRule = new FfmpegDetectionRule(); - var pipeline = new ProcessingPipelineBuilder(new ServiceCollection().BuildServiceProvider()).Add(statsRule).Add(ffmpegDetectionRule).AddDefault().AddRemoveFillerData().Build(); + var pipeline = new ProcessingPipelineBuilder() + .ConfigureServices(services => services.AddSingleton(request.PipelineSettings ?? new ProcessingPipelineSettings())) + .AddRule(statsRule) + .AddRule(ffmpegDetectionRule) + .AddDefaultRules() + .AddRemoveFillerDataRule() + .Build(); // Run await Task.Run(async () => diff --git a/BililiveRecorder.ToolBox/Tool/Fix/FixRequest.cs b/BililiveRecorder.ToolBox/Tool/Fix/FixRequest.cs index 0ca1fe5..b95444c 100644 --- a/BililiveRecorder.ToolBox/Tool/Fix/FixRequest.cs +++ b/BililiveRecorder.ToolBox/Tool/Fix/FixRequest.cs @@ -1,9 +1,13 @@ -namespace BililiveRecorder.ToolBox.Tool.Fix +using BililiveRecorder.Flv.Pipeline; + +namespace BililiveRecorder.ToolBox.Tool.Fix { public class FixRequest : ICommandRequest { public string Input { get; set; } = string.Empty; public string OutputBase { get; set; } = string.Empty; + + public ProcessingPipelineSettings? PipelineSettings { get; set; } } } diff --git a/BililiveRecorder.ToolBox/ToolCommand.cs b/BililiveRecorder.ToolBox/ToolCommand.cs index e47350f..b99c0dd 100644 --- a/BililiveRecorder.ToolBox/ToolCommand.cs +++ b/BililiveRecorder.ToolBox/ToolCommand.cs @@ -1,7 +1,10 @@ using System; using System.CommandLine; using System.CommandLine.NamingConventionBinder; +using System.CommandLine.Parsing; +using System.Linq; using System.Threading.Tasks; +using BililiveRecorder.Flv.Pipeline; using BililiveRecorder.ToolBox.Tool.Analyze; using BililiveRecorder.ToolBox.Tool.DanmakuMerger; using BililiveRecorder.ToolBox.Tool.DanmakuStartTime; @@ -19,12 +22,14 @@ namespace BililiveRecorder.ToolBox this.RegisterCommand("analyze", null, c => { c.Add(new Argument("input", "example: input.flv")); + c.Add(new Option(name: "pipeline-settings", parseArgument: this.ParseProcessingPipelineSettings)); }); this.RegisterCommand("fix", null, c => { c.Add(new Argument("input", "example: input.flv")); c.Add(new Argument("output-base", "example: output.flv")); + c.Add(new Option(name: "pipeline-settings", parseArgument: this.ParseProcessingPipelineSettings)); }); this.RegisterCommand("export", null, c => @@ -46,6 +51,21 @@ namespace BililiveRecorder.ToolBox }); } + private ProcessingPipelineSettings? ParseProcessingPipelineSettings(ArgumentResult result) + { + if (result.Tokens.Count == 0) return null; + + try + { + return JsonConvert.DeserializeObject(result.Tokens.Single().Value); + } + catch (Exception) + { + result.ErrorMessage = "Pipeline settings must be a valid json string"; + return null; + } + } + private void RegisterCommand(string name, string? description, Action configure) where THandler : ICommandHandler where TRequest : ICommandRequest diff --git a/BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml b/BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml index 0d02f6d..a945b67 100644 --- a/BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml +++ b/BililiveRecorder.WPF/Controls/PerRoomSettingsDialog.xaml @@ -61,6 +61,15 @@ + + + + + + + + + diff --git a/BililiveRecorder.WPF/Pages/SettingsPage.xaml b/BililiveRecorder.WPF/Pages/SettingsPage.xaml index 2951089..594feb2 100644 --- a/BililiveRecorder.WPF/Pages/SettingsPage.xaml +++ b/BililiveRecorder.WPF/Pages/SettingsPage.xaml @@ -52,6 +52,11 @@ ConverterParameter={x:Static config:RecordMode.RawData}}" /> + + + + + +