Add ToolBox

This commit is contained in:
Genteure 2021-04-14 23:46:24 +08:00
parent 14ffd7b700
commit c6eae11f95
16 changed files with 579 additions and 204 deletions

View File

@ -33,6 +33,7 @@
<ItemGroup>
<ProjectReference Include="..\BililiveRecorder.Core\BililiveRecorder.Core.csproj" />
<ProjectReference Include="..\BililiveRecorder.ToolBox\BililiveRecorder.ToolBox.csproj" />
</ItemGroup>
</Project>

View File

@ -9,6 +9,7 @@ using BililiveRecorder.Core;
using BililiveRecorder.Core.Config;
using BililiveRecorder.Core.Config.V2;
using BililiveRecorder.DependencyInjection;
using BililiveRecorder.ToolBox;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
using Serilog.Events;
@ -39,7 +40,8 @@ namespace BililiveRecorder.Cli
var root = new RootCommand("A Stream Recorder For Bilibili Live")
{
cmd_run,
cmd_portable
cmd_portable,
new ToolCommand()
};
return root.Invoke(args);
@ -85,7 +87,8 @@ namespace BililiveRecorder.Cli
var logger = BuildLogger();
Log.Logger = logger;
var config = new ConfigV2(){
var config = new ConfigV2()
{
DisableConfigSave = true,
};

View File

@ -34,7 +34,7 @@ namespace BililiveRecorder.Flv.Writer
public bool CloseCurrentFile()
{
if (this.disposedValue)
if (this.disposedValue)
throw new ObjectDisposedException(nameof(FlvTagFileWriter));
if (this.stream is null)
@ -52,6 +52,7 @@ namespace BililiveRecorder.Flv.Writer
if (this.disposedValue)
throw new ObjectDisposedException(nameof(FlvTagFileWriter));
System.Diagnostics.Debug.Assert(this.stream is null, "stream is not null");
this.stream?.Dispose();
(this.stream, this.State) = this.targetProvider.CreateOutputStream();

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20574.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BililiveRecorder.Flv\BililiveRecorder.Flv.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Threading.Tasks;
using BililiveRecorder.Flv;
using BililiveRecorder.Flv.Amf;
using BililiveRecorder.Flv.Grouping;
using BililiveRecorder.Flv.Parser;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Writer;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
namespace BililiveRecorder.ToolBox.Commands
{
public class AnalyzeRequest : ICommandRequest<AnalyzeResponse>
{
public string Input { get; set; } = string.Empty;
}
public class AnalyzeResponse
{
public string InputPath { get; set; } = string.Empty;
public bool NeedFix { get; set; }
public bool Unrepairable { get; set; }
public int OutputFileCount { get; set; }
public int IssueTypeOther { get; set; }
public int IssueTypeUnrepairable { get; set; }
public int IssueTypeTimestampJump { get; set; }
public int IssueTypeDecodingHeader { get; set; }
public int IssueTypeRepeatingData { get; set; }
}
public class AnalyzeHandler : ICommandHandler<AnalyzeRequest, AnalyzeResponse>
{
private static readonly ILogger logger = Log.ForContext<AnalyzeHandler>();
public Task<AnalyzeResponse> Handle(AnalyzeRequest request) => this.Handle(request, null);
public async Task<AnalyzeResponse> Handle(AnalyzeRequest request, Func<double, Task>? progress)
{
var inputPath = Path.GetFullPath(request.Input);
var memoryStreamProvider = new DefaultMemoryStreamProvider();
var tagWriter = new AnalyzeMockFlvTagWriter();
var comments = new List<ProcessingComment>();
var context = new FlvProcessingContext();
var session = new Dictionary<object, object?>();
{
using var inputStream = File.OpenRead(inputPath);
using var grouping = new TagGroupReader(new FlvTagPipeReader(PipeReader.Create(inputStream), memoryStreamProvider, skipData: false, logger: logger));
using var writer = new FlvProcessingContextWriter(tagWriter);
var pipeline = new ProcessingPipelineBuilder(new ServiceCollection().BuildServiceProvider()).AddDefault().AddRemoveFillerData().Build();
var count = 0;
while (true)
{
var group = await grouping.ReadGroupAsync(default).ConfigureAwait(false);
if (group is null)
break;
context.Reset(group, session);
pipeline(context);
if (context.Comments.Count > 0)
{
comments.AddRange(context.Comments);
logger.Debug("分析逻辑输出 {@Comments}", context.Comments);
}
await writer.WriteAsync(context).ConfigureAwait(false);
foreach (var action in context.Actions)
if (action is PipelineDataAction dataAction)
foreach (var tag in dataAction.Tags)
tag.BinaryData?.Dispose();
if (count++ % 10 == 0)
{
progress?.Invoke((double)inputStream.Position / inputStream.Length);
}
}
}
var countableComments = comments.Where(x => x.T != CommentType.Logging);
var response = new AnalyzeResponse
{
InputPath = inputPath,
NeedFix = tagWriter.OutputFileCount != 1 || countableComments.Any(),
Unrepairable = countableComments.Any(x => x.T == CommentType.Unrepairable),
OutputFileCount = tagWriter.OutputFileCount,
IssueTypeOther = countableComments.Count(x => x.T == CommentType.Other),
IssueTypeUnrepairable = countableComments.Count(x => x.T == CommentType.Unrepairable),
IssueTypeTimestampJump = countableComments.Count(x => x.T == CommentType.TimestampJump),
IssueTypeDecodingHeader = countableComments.Count(x => x.T == CommentType.DecodingHeader),
IssueTypeRepeatingData = countableComments.Count(x => x.T == CommentType.RepeatingData)
};
return response;
}
public void PrintResponse(AnalyzeResponse response)
{
Console.Write("Input: ");
Console.WriteLine(response.InputPath);
Console.WriteLine(response.NeedFix ? "File needs repair" : "File doesn't need repair");
if (response.Unrepairable)
Console.WriteLine("File contains error(s) that are unrepairable (yet), please send sample to the author of this program.");
Console.WriteLine("Will output {0} file(s) if repaired", response.OutputFileCount);
Console.WriteLine("Types of error:");
Console.Write("Other: ");
Console.WriteLine(response.IssueTypeOther);
Console.Write("Unrepairable: ");
Console.WriteLine(response.IssueTypeUnrepairable);
Console.Write("TimestampJump: ");
Console.WriteLine(response.IssueTypeTimestampJump);
Console.Write("DecodingHeader: ");
Console.WriteLine(response.IssueTypeDecodingHeader);
Console.Write("RepeatingData: ");
Console.WriteLine(response.IssueTypeRepeatingData);
}
private class AnalyzeMockFlvTagWriter : IFlvTagWriter
{
public long FileSize => 0;
public object? State => null;
public int OutputFileCount { get; private set; }
public bool CloseCurrentFile() => true;
public Task CreateNewFile()
{
this.OutputFileCount++;
return Task.CompletedTask;
}
public void Dispose() { }
public Task OverwriteMetadata(ScriptTagBody metadata) => Task.CompletedTask;
public Task WriteAlternativeHeaders(IEnumerable<Tag> tags) => Task.CompletedTask;
public Task WriteTag(Tag tag) => Task.CompletedTask;
}
}
}

View File

@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.IO.Pipelines;
using System.Threading.Tasks;
using BililiveRecorder.Flv;
using BililiveRecorder.Flv.Parser;
using BililiveRecorder.Flv.Xml;
using Serilog;
namespace BililiveRecorder.ToolBox.Commands
{
public class ExportRequest : ICommandRequest<ExportResponse>
{
public string Input { get; set; } = string.Empty;
public string Output { get; set; } = string.Empty;
}
public class ExportResponse
{
}
public class ExportHandler : ICommandHandler<ExportRequest, ExportResponse>
{
private static readonly ILogger logger = Log.ForContext<ExportHandler>();
public Task<ExportResponse> Handle(ExportRequest request) => this.Handle(request, null);
public async Task<ExportResponse> Handle(ExportRequest request, Func<double, Task>? progress)
{
using var inputStream = File.OpenRead(request.Input);
using var outputStream = File.OpenWrite(request.Output);
var tags = new List<Tag>();
{
using var reader = new FlvTagPipeReader(PipeReader.Create(inputStream), new DefaultMemoryStreamProvider(), skipData: true, logger: logger);
var count = 0;
while (true)
{
var tag = await reader.ReadTagAsync(default).ConfigureAwait(false);
if (tag is null) break;
tags.Add(tag);
if (count++ % 300 == 0)
progress?.Invoke((double)inputStream.Position / inputStream.Length);
}
}
{
using var writer = new StreamWriter(new GZipStream(outputStream, CompressionLevel.Optimal));
XmlFlvFile.Serializer.Serialize(writer, new XmlFlvFile
{
Tags = tags
});
}
return new ExportResponse();
}
public void PrintResponse(ExportResponse response)
{ }
}
}

View File

@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Threading.Tasks;
using BililiveRecorder.Flv;
using BililiveRecorder.Flv.Grouping;
using BililiveRecorder.Flv.Parser;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Writer;
using Microsoft.Extensions.DependencyInjection;
using Serilog;
namespace BililiveRecorder.ToolBox.Commands
{
public class FixRequest : ICommandRequest<FixResponse>
{
public string Input { get; set; } = string.Empty;
public string OutputBase { get; set; } = string.Empty;
}
public class FixResponse
{
public string InputPath { get; set; } = string.Empty;
public string[] OutputPaths { get; set; } = Array.Empty<string>();
public bool NeedFix { get; set; }
public bool Unrepairable { get; set; }
public int OutputFileCount { get; set; }
public int IssueTypeOther { get; set; }
public int IssueTypeUnrepairable { get; set; }
public int IssueTypeTimestampJump { get; set; }
public int IssueTypeDecodingHeader { get; set; }
public int IssueTypeRepeatingData { get; set; }
}
public class FixHandler : ICommandHandler<FixRequest, FixResponse>
{
private static readonly ILogger logger = Log.ForContext<FixHandler>();
public Task<FixResponse> Handle(FixRequest request) => this.Handle(request, null);
public async Task<FixResponse> Handle(FixRequest request, Func<double, Task>? progress)
{
var inputPath = Path.GetFullPath(request.Input);
var outputPaths = new List<string>();
var targetProvider = new AutoFixFlvWriterTargetProvider(request.OutputBase);
targetProvider.BeforeFileOpen += (sender, path) => outputPaths.Add(path);
var memoryStreamProvider = new DefaultMemoryStreamProvider();
var tagWriter = new FlvTagFileWriter(targetProvider, memoryStreamProvider, logger);
var comments = new List<ProcessingComment>();
var context = new FlvProcessingContext();
var session = new Dictionary<object, object?>();
{
using var inputStream = File.OpenRead(inputPath);
using var grouping = new TagGroupReader(new FlvTagPipeReader(PipeReader.Create(inputStream), memoryStreamProvider, skipData: false, logger: logger));
using var writer = new FlvProcessingContextWriter(tagWriter);
var pipeline = new ProcessingPipelineBuilder(new ServiceCollection().BuildServiceProvider()).AddDefault().AddRemoveFillerData().Build();
var count = 0;
while (true)
{
var group = await grouping.ReadGroupAsync(default).ConfigureAwait(false);
if (group is null)
break;
context.Reset(group, session);
pipeline(context);
if (context.Comments.Count > 0)
{
comments.AddRange(context.Comments);
logger.Debug("修复逻辑输出 {@Comments}", context.Comments);
}
await writer.WriteAsync(context).ConfigureAwait(false);
foreach (var action in context.Actions)
if (action is PipelineDataAction dataAction)
foreach (var tag in dataAction.Tags)
tag.BinaryData?.Dispose();
if (count++ % 10 == 0)
{
progress?.Invoke((double)inputStream.Position / inputStream.Length);
}
}
}
var countableComments = comments.Where(x => x.T != CommentType.Logging);
var response = new FixResponse
{
InputPath = inputPath,
OutputPaths = outputPaths.ToArray(),
OutputFileCount = outputPaths.Count,
NeedFix = outputPaths.Count != 1 || countableComments.Any(),
Unrepairable = countableComments.Any(x => x.T == CommentType.Unrepairable),
IssueTypeOther = countableComments.Count(x => x.T == CommentType.Other),
IssueTypeUnrepairable = countableComments.Count(x => x.T == CommentType.Unrepairable),
IssueTypeTimestampJump = countableComments.Count(x => x.T == CommentType.TimestampJump),
IssueTypeDecodingHeader = countableComments.Count(x => x.T == CommentType.DecodingHeader),
IssueTypeRepeatingData = countableComments.Count(x => x.T == CommentType.RepeatingData)
};
return response;
}
public void PrintResponse(FixResponse response)
{
Console.Write("Input: ");
Console.WriteLine(response.InputPath);
Console.WriteLine(response.NeedFix ? "File needs repair" : "File doesn't need repair");
if (response.Unrepairable)
Console.WriteLine("File contains error(s) that are unrepairable (yet), please send sample to the author of this program.");
Console.WriteLine("{0} file(s) written", response.OutputFileCount);
foreach (var path in response.OutputPaths)
{
Console.Write(" ");
Console.WriteLine(path);
}
Console.WriteLine("Types of error:");
Console.Write("Other: ");
Console.WriteLine(response.IssueTypeOther);
Console.Write("Unrepairable: ");
Console.WriteLine(response.IssueTypeUnrepairable);
Console.Write("TimestampJump: ");
Console.WriteLine(response.IssueTypeTimestampJump);
Console.Write("DecodingHeader: ");
Console.WriteLine(response.IssueTypeDecodingHeader);
Console.Write("RepeatingData: ");
Console.WriteLine(response.IssueTypeRepeatingData);
}
private class AutoFixFlvWriterTargetProvider : IFlvWriterTargetProvider
{
private readonly string pathTemplate;
private int fileIndex = 1;
public event EventHandler<string>? BeforeFileOpen;
public AutoFixFlvWriterTargetProvider(string pathTemplate)
{
this.pathTemplate = pathTemplate;
}
public Stream CreateAlternativeHeaderStream()
{
var path = Path.ChangeExtension(this.pathTemplate, "header.txt");
return File.Open(path, FileMode.Append, FileAccess.Write, FileShare.None);
}
public (Stream stream, object state) CreateOutputStream()
{
var i = this.fileIndex++;
var path = Path.ChangeExtension(this.pathTemplate, $"fix_p{i}.flv");
var fileStream = File.Create(path);
BeforeFileOpen?.Invoke(this, path);
return (fileStream, null!);
}
}
}
}

View File

@ -0,0 +1,10 @@
using System.Threading.Tasks;
namespace BililiveRecorder.ToolBox
{
public interface ICommandHandler<TRequest, TResponse> where TRequest : ICommandRequest<TResponse>
{
Task<TResponse> Handle(TRequest request);
void PrintResponse(TResponse response);
}
}

View File

@ -0,0 +1,4 @@
namespace BililiveRecorder.ToolBox
{
public interface ICommandRequest<TResponse> { }
}

View File

@ -0,0 +1,67 @@
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Threading.Tasks;
using BililiveRecorder.ToolBox.Commands;
using Newtonsoft.Json;
namespace BililiveRecorder.ToolBox
{
public class ToolCommand : Command
{
public ToolCommand() : base("tool", "Run Tools")
{
this.RegisterCommand<AnalyzeHandler, AnalyzeRequest, AnalyzeResponse>("analyze", null, c =>
{
c.Add(new Argument<string>("input", "example: input.flv"));
});
this.RegisterCommand<FixHandler, FixRequest, FixResponse>("fix", null, c =>
{
c.Add(new Argument<string>("input", "example: input.flv"));
c.Add(new Argument<string>("output-base", "example: output.flv"));
});
this.RegisterCommand<ExportHandler, ExportRequest, ExportResponse>("export", null, c =>
{
c.Add(new Argument<string>("input", "example: input.flv"));
c.Add(new Argument<string>("output", "example: output.brec.xml.gz"));
});
}
private void RegisterCommand<IHandler, IRequest, IResponse>(string name, string? description, Action<Command> configure)
where IHandler : ICommandHandler<IRequest, IResponse>
where IRequest : ICommandRequest<IResponse>
{
var cmd = new Command(name, description)
{
new Option<bool>("--json", "print result as json string"),
new Option<bool>("--json-indented", "print result as indented json string")
};
cmd.Handler = CommandHandler.Create((IRequest r, bool json, bool jsonIndented) => RunSubCommand<IHandler, IRequest, IResponse>(r, json, jsonIndented));
configure(cmd);
this.Add(cmd);
}
private static async Task<int> RunSubCommand<IHandler, IRequest, IResponse>(IRequest request, bool json, bool jsonIndented)
where IHandler : ICommandHandler<IRequest, IResponse>
where IRequest : ICommandRequest<IResponse>
{
var handler = Activator.CreateInstance<IHandler>();
var response = await handler.Handle(request).ConfigureAwait(false);
if (json || jsonIndented)
{
var json_str = JsonConvert.SerializeObject(response, jsonIndented ? Formatting.Indented : Formatting.None);
Console.WriteLine(json_str);
}
else
{
handler.PrintResponse(response);
}
return 0;
}
}
}

View File

@ -120,7 +120,6 @@
<Compile Include="Converters\RatioToArrowIconConverter.cs" />
<Compile Include="Converters\ShortRoomIdToVisibilityConverter.cs" />
<Compile Include="Converters\ValueConverterGroup.cs" />
<Compile Include="Models\AnalyzeResultModel.cs" />
<Compile Include="Models\Commands.cs" />
<Compile Include="Models\LogModel.cs" />
<Compile Include="Models\PollyPolicyModel.cs" />
@ -293,6 +292,10 @@
<Project>{7610e19c-d3ab-4cbc-983e-6fda36f4d4b3}</Project>
<Name>BililiveRecorder.Flv</Name>
</ProjectReference>
<ProjectReference Include="..\BililiveRecorder.ToolBox\BililiveRecorder.ToolBox.csproj">
<Project>{4faae8e7-ac4e-4e99-a7d1-53d20ad8a200}</Project>
<Name>BililiveRecorder.ToolBox</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="GitVersion.MsBuild" Version="5.6.6">

View File

@ -1,37 +0,0 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
#nullable enable
namespace BililiveRecorder.WPF.Models
{
internal class AnalyzeResultModel : INotifyPropertyChanged
{
private string file = string.Empty;
private bool needFix;
private bool unrepairable;
private int issueTypeOther;
private int issueTypeUnrepairable;
private int issueTypeTimestampJump;
private int issueTypeDecodingHeader;
private int issueTypeRepeatingData;
public string File { get => this.file; set => this.SetField(ref this.file, value); }
public bool NeedFix { get => this.needFix; set => this.SetField(ref this.needFix, value); }
public bool Unrepairable { get => this.unrepairable; set => this.SetField(ref this.unrepairable, value); }
public int IssueTypeOther { get => this.issueTypeOther; set => this.SetField(ref this.issueTypeOther, value); }
public int IssueTypeUnrepairable { get => this.issueTypeUnrepairable; set => this.SetField(ref this.issueTypeUnrepairable, value); }
public int IssueTypeTimestampJump { get => this.issueTypeTimestampJump; set => this.SetField(ref this.issueTypeTimestampJump, value); }
public int IssueTypeDecodingHeader { get => this.issueTypeDecodingHeader; set => this.SetField(ref this.issueTypeDecodingHeader, value); }
public int IssueTypeRepeatingData { get => this.issueTypeRepeatingData; set => this.SetField(ref this.issueTypeRepeatingData, value); }
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (EqualityComparer<T>.Default.Equals(field, value)) { return false; }
field = value; this.OnPropertyChanged(propertyName); return true;
}
}
}

View File

@ -11,7 +11,8 @@
l:ResxLocalizationProvider.DefaultDictionary="Strings"
xmlns:local="clr-namespace:BililiveRecorder.WPF.Pages"
xmlns:model="clr-namespace:BililiveRecorder.WPF.Models"
xmlns:c="clr-namespace:BililiveRecorder.WPF.Converters"
xmlns:c="clr-namespace:BililiveRecorder.WPF.Converters"
xmlns:tool="clr-namespace:BililiveRecorder.ToolBox.Commands;assembly=BililiveRecorder.ToolBox"
mc:Ignorable="d" DataContext="{x:Null}"
d:DesignHeight="600" d:DesignWidth="900"
Title="ToolboxAutoFixPage">
@ -67,7 +68,7 @@
<TextBlock Text="注:不分析也可以进行修复操作" VerticalAlignment="Center" HorizontalAlignment="Center"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="NormalAnalyzeResult" DataType="{x:Type model:AnalyzeResultModel}">
<DataTemplate x:Key="NormalAnalyzeResult" DataType="{x:Type tool:AnalyzeResponse}">
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
@ -75,7 +76,7 @@
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBox Grid.Row="0" IsReadOnly="True" Text="{Binding File}" ui:ControlHelper.Header="文件:"/>
<TextBox Grid.Row="0" IsReadOnly="True" Text="{Binding InputPath}" ui:ControlHelper.Header="文件:"/>
<TextBlock Grid.Row="1" HorizontalAlignment="Center" FontSize="24" Text="无需修复" Foreground="Green"
Visibility="{Binding NeedFix,Converter={StaticResource InvertBooleanToVisibilityCollapsedConverter}}"/>
<TextBlock Grid.Row="1" HorizontalAlignment="Center" FontSize="24" Text="需要修复" Foreground="Red"
@ -86,6 +87,7 @@
<TextBlock HorizontalAlignment="Center" Text="请点击“修复失败?”按钮并反馈本问题"/>
</StackPanel>
<StackPanel Grid.Row="3" HorizontalAlignment="Center" Margin="10">
<TextBlock Text="{Binding OutputFileCount,StringFormat=修复将会输出 {0} 个文件}" Margin="0,0,0,5"/>
<TextBlock Text="{Binding IssueTypeTimestampJump,StringFormat=时间戳问题 {0} 处}"/>
<TextBlock Text="{Binding IssueTypeDecodingHeader,StringFormat=分辨率、解码问题 {0} 处}"/>
<TextBlock Text="{Binding IssueTypeRepeatingData,StringFormat=重复片段 {0} 处}"/>

View File

@ -12,6 +12,7 @@ using BililiveRecorder.Flv.Parser;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Writer;
using BililiveRecorder.Flv.Xml;
using BililiveRecorder.ToolBox.Commands;
using BililiveRecorder.WPF.Controls;
using BililiveRecorder.WPF.Models;
using Microsoft.Extensions.DependencyInjection;
@ -72,7 +73,7 @@ namespace BililiveRecorder.WPF.Pages
progressDialog = new AutoFixProgressDialog();
var showTask = progressDialog.ShowAsync();
IFlvWriterTargetProvider? targetProvider = null;
string? output_path;
{
var title = "选择保存位置";
var fileDialog = new CommonSaveFileDialog()
@ -87,49 +88,25 @@ namespace BililiveRecorder.WPF.Pages
DefaultFileName = Path.GetFileName(inputPath)
};
if (fileDialog.ShowDialog() == CommonFileDialogResult.Ok)
targetProvider = new AutoFixFlvWriterTargetProvider(fileDialog.FileName);
output_path = fileDialog.FileName;
else
return;
}
using var inputStream = File.OpenRead(inputPath);
var memoryStreamProvider = new DefaultMemoryStreamProvider();
using var grouping = new TagGroupReader(new FlvTagPipeReader(PipeReader.Create(inputStream), memoryStreamProvider, skipData: false, logger: logger));
using var writer = new FlvProcessingContextWriter(new FlvTagFileWriter(targetProvider, memoryStreamProvider, logger));
var context = new FlvProcessingContext();
var session = new Dictionary<object, object?>();
var pipeline = new ProcessingPipelineBuilder(new ServiceCollection().BuildServiceProvider()).AddDefault().AddRemoveFillerData().Build();
await Task.Run(async () =>
var req = new FixRequest
{
var count = 0;
while (true)
Input = inputPath,
OutputBase = output_path,
};
var handler = new FixHandler();
var resp = await handler.Handle(req, async p =>
{
await this.Dispatcher.InvokeAsync(() =>
{
var group = await grouping.ReadGroupAsync(default).ConfigureAwait(false);
if (group is null)
break;
context.Reset(group, session);
pipeline(context);
if (context.Comments.Count > 0)
logger.Debug("修复逻辑输出 {@Comments}", context.Comments);
await writer.WriteAsync(context).ConfigureAwait(false);
foreach (var action in context.Actions)
if (action is PipelineDataAction dataAction)
foreach (var tag in dataAction.Tags)
tag.BinaryData?.Dispose();
if (count++ % 5 == 0)
{
await this.Dispatcher.InvokeAsync(() =>
{
progressDialog.Progress = (int)((double)inputStream.Position / inputStream.Length * 98d);
});
}
}
progressDialog.Progress = (int)(p * 98d);
});
}).ConfigureAwait(true);
progressDialog.Hide();
@ -138,6 +115,7 @@ namespace BililiveRecorder.WPF.Pages
catch (Exception ex)
{
logger.Error(ex, "修复时发生错误");
MessageBox.Show("修复时发生错误\n" + ex.Message);
}
finally
{
@ -163,60 +141,22 @@ namespace BililiveRecorder.WPF.Pages
progressDialog = new AutoFixProgressDialog();
var showTask = progressDialog.ShowAsync();
using var inputStream = File.OpenRead(inputPath);
var memoryStreamProvider = new DefaultMemoryStreamProvider();
using var grouping = new TagGroupReader(new FlvTagPipeReader(PipeReader.Create(inputStream), memoryStreamProvider, skipData: false, logger: logger));
var comments = new List<ProcessingComment>();
var context = new FlvProcessingContext();
var session = new Dictionary<object, object?>();
var pipeline = new ProcessingPipelineBuilder(new ServiceCollection().BuildServiceProvider()).AddDefault().AddRemoveFillerData().Build();
await Task.Run(async () =>
var req = new AnalyzeRequest
{
var count = 0;
while (true)
{
var group = await grouping.ReadGroupAsync(default).ConfigureAwait(false);
if (group is null)
break;
context.Reset(group, session);
pipeline(context);
if (context.Comments.Count > 0)
{
logger.Debug("分析逻辑输出 {@Comments}", context.Comments);
comments.AddRange(context.Comments);
}
foreach (var action in context.Actions)
if (action is PipelineDataAction dataAction)
foreach (var tag in dataAction.Tags)
tag.BinaryData?.Dispose();
if (count++ % 5 == 0)
{
await this.Dispatcher.InvokeAsync(() =>
{
progressDialog.Progress = (int)((double)inputStream.Position / inputStream.Length * 98d);
});
}
}
}).ConfigureAwait(true);
var countableComments = comments.Where(x => x.T != CommentType.Logging);
var model = new AnalyzeResultModel
{
File = inputPath,
NeedFix = countableComments.Any(),
Unrepairable = countableComments.Any(x => x.T == CommentType.Unrepairable),
IssueTypeOther = countableComments.Count(x => x.T == CommentType.Other),
IssueTypeUnrepairable = countableComments.Count(x => x.T == CommentType.Unrepairable),
IssueTypeTimestampJump = countableComments.Count(x => x.T == CommentType.TimestampJump),
IssueTypeDecodingHeader = countableComments.Count(x => x.T == CommentType.DecodingHeader),
IssueTypeRepeatingData = countableComments.Count(x => x.T == CommentType.RepeatingData)
Input = inputPath
};
this.analyzeResultDisplayArea.DataContext = model;
var handler = new AnalyzeHandler();
var resp = await handler.Handle(req, async p =>
{
await this.Dispatcher.InvokeAsync(() =>
{
progressDialog.Progress = (int)(p * 98d);
});
}).ConfigureAwait(true);
this.analyzeResultDisplayArea.DataContext = resp;
progressDialog.Hide();
await showTask.ConfigureAwait(true);
@ -224,6 +164,7 @@ namespace BililiveRecorder.WPF.Pages
catch (Exception ex)
{
logger.Error(ex, "分析时发生错误");
MessageBox.Show("分析时发生错误\n" + ex.Message);
}
finally
{
@ -273,35 +214,19 @@ namespace BililiveRecorder.WPF.Pages
return;
}
using var inputStream = File.OpenRead(inputPath);
var outputStream = File.OpenWrite(outputPath);
var tags = new List<Tag>();
using var reader = new FlvTagPipeReader(PipeReader.Create(inputStream), new DefaultMemoryStreamProvider(), skipData: true, logger: logger);
await Task.Run(async () =>
var req = new ExportRequest
{
var count = 0;
while (true)
{
var tag = await reader.ReadTagAsync(default).ConfigureAwait(false);
if (tag is null) break;
tags.Add(tag);
if (count++ % 300 == 0)
{
await this.Dispatcher.InvokeAsync(() =>
{
progressDialog.Progress = (int)((double)inputStream.Position / inputStream.Length * 95d);
});
}
}
}).ConfigureAwait(true);
Input = inputPath,
Output = outputPath
};
await Task.Run(() =>
var handler = new ExportHandler();
var resp = await handler.Handle(req, async p =>
{
using var writer = new StreamWriter(new GZipStream(outputStream, CompressionLevel.Optimal));
XmlFlvFile.Serializer.Serialize(writer, new XmlFlvFile
await this.Dispatcher.InvokeAsync(() =>
{
Tags = tags
progressDialog.Progress = (int)(p * 95d);
});
}).ConfigureAwait(true);
@ -311,6 +236,7 @@ namespace BililiveRecorder.WPF.Pages
catch (Exception ex)
{
logger.Error(ex, "导出时发生错误");
MessageBox.Show("导出时发生错误\n" + ex.Message);
}
finally
{
@ -321,30 +247,5 @@ namespace BililiveRecorder.WPF.Pages
catch (Exception) { }
}
}
private class AutoFixFlvWriterTargetProvider : IFlvWriterTargetProvider
{
private readonly string pathTemplate;
private int fileIndex = 1;
public AutoFixFlvWriterTargetProvider(string pathTemplate)
{
this.pathTemplate = pathTemplate;
}
public Stream CreateAlternativeHeaderStream()
{
var path = Path.ChangeExtension(this.pathTemplate, "header.txt");
return File.Open(path, FileMode.Append, FileAccess.Write, FileShare.None);
}
public (Stream stream, object state) CreateOutputStream()
{
var i = this.fileIndex++;
var path = Path.ChangeExtension(this.pathTemplate, $"fix_p{i}.flv");
var fileStream = File.Create(path);
return (fileStream, null!);
}
}
}
}

View File

@ -4,10 +4,12 @@ using System.CommandLine.Invocation;
using System.Diagnostics;
using System.IO;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
using BililiveRecorder.ToolBox;
using Sentry;
using Serilog;
using Serilog.Core;
@ -29,6 +31,7 @@ namespace BililiveRecorder.WPF
static Program()
{
AttachConsole(-1);
levelSwitchGlobal = new LoggingLevelSwitch(Serilog.Events.LogEventLevel.Debug);
if (Debugger.IsAttached)
levelSwitchGlobal.MinimumLevel = Serilog.Events.LogEventLevel.Verbose;
@ -82,7 +85,8 @@ namespace BililiveRecorder.WPF
new Option<bool>("--squirrel-firstrun")
{
IsHidden = true
}
},
new ToolCommand(),
};
root.Handler = CommandHandler.Create((bool squirrelFirstrun) => Commands.RunWpfHandler(null, squirrelFirstrun));
return root;
@ -126,20 +130,6 @@ namespace BililiveRecorder.WPF
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
}
}
internal static int Tool_Fix(string path)
{
levelSwitchConsole.MinimumLevel = Serilog.Events.LogEventLevel.Information;
// run code
return 0;
}
internal static int Tool_Parse(string path)
{
levelSwitchConsole.MinimumLevel = Serilog.Events.LogEventLevel.Information;
// run code
return 0;
}
}
private static Logger BuildLogger() => new LoggerConfiguration()
@ -156,7 +146,7 @@ namespace BililiveRecorder.WPF
#else
.WriteTo.Sink<WpfLogEventSink>(Serilog.Events.LogEventLevel.Information)
#endif
.WriteTo.File(new CompactJsonFormatter(), "./logs/bilirec.txt", shared: true, rollingInterval: RollingInterval.Day)
.WriteTo.File(new CompactJsonFormatter(), "./logs/bilirec.txt", shared: true, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true)
.WriteTo.Sentry(o =>
{
o.Dsn = "https://7c6c5da3140543809661813aaa836207@o210546.ingest.sentry.io/5556540";
@ -175,6 +165,9 @@ namespace BililiveRecorder.WPF
})
.CreateLogger();
[DllImport("kernel32")]
private static extern bool AttachConsole(int pid);
[HandleProcessCorruptedStateExceptions, SecurityCritical]
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{

View File

@ -27,7 +27,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Core.UnitT
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Flv.UnitTests", "test\BililiveRecorder.Flv.UnitTests\BililiveRecorder.Flv.UnitTests.csproj", "{560E8483-9293-410E-81E9-AB36B49F8A7C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BililiveRecorder.Flv.RuleTests", "test\BililiveRecorder.Flv.RuleTests\BililiveRecorder.Flv.RuleTests.csproj", "{75DA0162-DE06-4FA0-B6F8-C82C11AF65BC}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BililiveRecorder.Flv.RuleTests", "test\BililiveRecorder.Flv.RuleTests\BililiveRecorder.Flv.RuleTests.csproj", "{75DA0162-DE06-4FA0-B6F8-C82C11AF65BC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BililiveRecorder.ToolBox", "BililiveRecorder.ToolBox\BililiveRecorder.ToolBox.csproj", "{4FAAE8E7-AC4E-4E99-A7D1-53D20AD8A200}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -63,6 +65,10 @@ Global
{75DA0162-DE06-4FA0-B6F8-C82C11AF65BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75DA0162-DE06-4FA0-B6F8-C82C11AF65BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75DA0162-DE06-4FA0-B6F8-C82C11AF65BC}.Release|Any CPU.Build.0 = Release|Any CPU
{4FAAE8E7-AC4E-4E99-A7D1-53D20AD8A200}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4FAAE8E7-AC4E-4E99-A7D1-53D20AD8A200}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4FAAE8E7-AC4E-4E99-A7D1-53D20AD8A200}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4FAAE8E7-AC4E-4E99-A7D1-53D20AD8A200}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -75,12 +81,13 @@ Global
{521EC763-5694-45A8-B87F-6E6B7F2A3BD4} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}
{560E8483-9293-410E-81E9-AB36B49F8A7C} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}
{75DA0162-DE06-4FA0-B6F8-C82C11AF65BC} = {623A2ACC-DAC6-4E6F-9242-B4B54381AAE1}
{4FAAE8E7-AC4E-4E99-A7D1-53D20AD8A200} = {2D44A59D-E437-4FEE-8A2E-3FF00D53A64D}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
RESX_ShowErrorsInErrorList = False
RESX_SaveFilesImmediatelyUponChange = True
RESX_NeutralResourcesLanguage = zh-Hans
SolutionGuid = {F3CB8B14-077A-458F-BD8E-1747ED0F5170}
RESX_SortFileContentOnSave = True
SolutionGuid = {F3CB8B14-077A-458F-BD8E-1747ED0F5170}
RESX_NeutralResourcesLanguage = zh-Hans
RESX_SaveFilesImmediatelyUponChange = True
RESX_ShowErrorsInErrorList = False
EndGlobalSection
EndGlobal