BililiveRecorder/BililiveRecorder.ToolBox/Tool/Fix/FixHandler.cs

286 lines
13 KiB
C#
Raw Normal View History

2021-04-14 23:46:24 +08:00
using System;
using System.Collections.Generic;
using System.IO;
2021-04-20 20:41:26 +08:00
using System.IO.Compression;
2021-04-14 23:46:24 +08:00
using System.IO.Pipelines;
using System.Linq;
2022-04-02 18:49:23 +08:00
using System.Text;
2021-05-02 21:34:27 +08:00
using System.Threading;
2021-04-14 23:46:24 +08:00
using System.Threading.Tasks;
2022-04-02 18:49:23 +08:00
using System.Xml;
2021-04-14 23:46:24 +08:00
using BililiveRecorder.Flv;
using BililiveRecorder.Flv.Grouping;
using BililiveRecorder.Flv.Parser;
using BililiveRecorder.Flv.Pipeline;
using BililiveRecorder.Flv.Pipeline.Actions;
2021-04-14 23:46:24 +08:00
using BililiveRecorder.Flv.Writer;
2021-04-20 20:41:26 +08:00
using BililiveRecorder.Flv.Xml;
2021-04-23 18:51:27 +08:00
using BililiveRecorder.ToolBox.ProcessingRules;
2021-04-14 23:46:24 +08:00
using Microsoft.Extensions.DependencyInjection;
using Serilog;
2021-07-15 12:58:50 +08:00
namespace BililiveRecorder.ToolBox.Tool.Fix
2021-04-14 23:46:24 +08:00
{
public class FixHandler : ICommandHandler<FixRequest, FixResponse>
{
private static readonly ILogger logger = Log.ForContext<FixHandler>();
public string Name => "Fix";
2021-04-14 23:46:24 +08:00
public async Task<CommandResponse<FixResponse>> Handle(FixRequest request, CancellationToken cancellationToken, ProgressCallback? progress)
2021-04-14 23:46:24 +08:00
{
2021-04-20 20:41:26 +08:00
FileStream? flvFileStream = null;
2021-04-19 18:20:14 +08:00
try
2021-04-14 23:46:24 +08:00
{
2021-05-02 22:24:57 +08:00
XmlFlvFile.XmlFlvFileMeta? meta = null;
var memoryStreamProvider = new RecyclableMemoryStreamProvider();
2021-04-19 18:20:14 +08:00
var comments = new List<ProcessingComment>();
var context = new FlvProcessingContext();
var session = new Dictionary<object, object?>();
2021-04-14 23:46:24 +08:00
2021-04-20 20:41:26 +08:00
// Input
string? inputPath;
IFlvTagReader tagReader;
var xmlMode = false;
try
2021-04-19 18:20:14 +08:00
{
2021-04-20 20:41:26 +08:00
inputPath = Path.GetFullPath(request.Input);
if (inputPath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
2021-04-14 23:46:24 +08:00
{
2021-04-20 20:41:26 +08:00
xmlMode = true;
tagReader = await Task.Run(() =>
{
using var stream = new GZipStream(File.Open(inputPath, FileMode.Open, FileAccess.Read, FileShare.Read), CompressionMode.Decompress);
var xmlFlvFile = (XmlFlvFile)XmlFlvFile.Serializer.Deserialize(stream);
2021-05-02 22:24:57 +08:00
meta = xmlFlvFile.Meta;
2021-04-20 20:41:26 +08:00
return new FlvTagListReader(xmlFlvFile.Tags);
});
2021-04-19 18:20:14 +08:00
}
2021-04-20 20:41:26 +08:00
else if (inputPath.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
2021-04-19 18:20:14 +08:00
{
2021-04-20 20:41:26 +08:00
xmlMode = true;
tagReader = await Task.Run(() =>
2021-04-19 18:20:14 +08:00
{
2021-04-20 20:41:26 +08:00
using var stream = File.Open(inputPath, FileMode.Open, FileAccess.Read, FileShare.Read);
var xmlFlvFile = (XmlFlvFile)XmlFlvFile.Serializer.Deserialize(stream);
2021-05-02 22:24:57 +08:00
meta = xmlFlvFile.Meta;
return new FlvTagListReader(xmlFlvFile.Tags);
});
}
else if (inputPath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
xmlMode = true;
tagReader = await Task.Run(() =>
{
using var zip = new ZipArchive(File.Open(inputPath, FileMode.Open, FileAccess.Read, FileShare.Read), ZipArchiveMode.Read, false, Encoding.UTF8);
var entry = zip.Entries.First(x => x.Name.EndsWith(".xml", StringComparison.OrdinalIgnoreCase));
var xmlFlvFile = (XmlFlvFile)XmlFlvFile.Serializer.Deserialize(entry.Open());
meta = xmlFlvFile.Meta;
2021-04-20 20:41:26 +08:00
return new FlvTagListReader(xmlFlvFile.Tags);
});
}
else
{
flvFileStream = new FileStream(inputPath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan);
tagReader = new FlvTagPipeReader(PipeReader.Create(flvFileStream), memoryStreamProvider, skipData: false, logger: logger);
2021-04-14 23:46:24 +08:00
}
2021-04-20 20:41:26 +08:00
}
catch (Exception ex) when (ex is not FlvException)
{
return new CommandResponse<FixResponse>
{
Status = ResponseStatus.InputIOError,
Exception = ex,
ErrorMessage = ex.Message
};
}
// Output
var outputPaths = new List<string>();
IFlvTagWriter tagWriter;
if (xmlMode)
tagWriter = new FlvTagListWriter();
else
{
var targetProvider = new AutoFixFlvWriterTargetProvider(request.OutputBase);
targetProvider.BeforeFileOpen += (sender, path) => outputPaths.Add(path);
tagWriter = new FlvTagFileWriter(targetProvider, memoryStreamProvider, logger);
}
2021-04-14 23:46:24 +08:00
2021-04-20 20:41:26 +08:00
// Pipeline
using var grouping = new TagGroupReader(tagReader);
using var writer = new FlvProcessingContextWriter(tagWriter: tagWriter, allowMissingHeader: true, disableKeyframes: false, logger: logger);
2021-04-23 18:51:27 +08:00
var statsRule = new StatsRule();
2022-06-22 23:57:48 +08:00
var ffmpegDetectionRule = new FfmpegDetectionRule();
var pipeline = new ProcessingPipelineBuilder(new ServiceCollection().BuildServiceProvider()).Add(statsRule).Add(ffmpegDetectionRule).AddDefault().AddRemoveFillerData().Build();
2021-04-14 23:46:24 +08:00
2021-04-20 20:41:26 +08:00
// Run
await Task.Run(async () =>
{
2021-04-19 18:20:14 +08:00
var count = 0;
2021-05-02 21:34:27 +08:00
while (!cancellationToken.IsCancellationRequested)
2021-04-14 23:46:24 +08:00
{
2021-05-02 21:34:27 +08:00
var group = await grouping.ReadGroupAsync(cancellationToken).ConfigureAwait(false);
2021-04-19 18:20:14 +08:00
if (group is null)
break;
2021-04-14 23:46:24 +08:00
2021-04-19 18:20:14 +08:00
context.Reset(group, session);
pipeline(context);
2021-04-14 23:46:24 +08:00
2021-04-19 18:20:14 +08:00
if (context.Comments.Count > 0)
{
comments.AddRange(context.Comments);
logger.Debug("修复逻辑输出 {@Comments}", context.Comments);
}
await writer.WriteAsync(context).ConfigureAwait(false);
2021-04-14 23:46:24 +08:00
2021-04-19 18:20:14 +08:00
foreach (var action in context.Actions)
if (action is PipelineDataAction dataAction)
foreach (var tag in dataAction.Tags)
tag.BinaryData?.Dispose();
2021-04-14 23:46:24 +08:00
2021-04-20 20:41:26 +08:00
if (count++ % 10 == 0 && progress is not null && flvFileStream is not null)
await progress((double)flvFileStream.Position / flvFileStream.Length);
2021-04-19 18:20:14 +08:00
}
2021-04-20 20:41:26 +08:00
}).ConfigureAwait(false);
2021-05-02 21:34:27 +08:00
if (cancellationToken.IsCancellationRequested)
return new CommandResponse<FixResponse> { Status = ResponseStatus.Cancelled };
2021-04-20 20:41:26 +08:00
// Post Run
2021-05-02 22:24:57 +08:00
if (meta is not null)
logger.Information("Xml meta: {@Meta}", meta);
2021-04-20 20:41:26 +08:00
if (xmlMode)
await Task.Run(() =>
{
var w = (FlvTagListWriter)tagWriter;
for (var i = 0; i < w.Files.Count; i++)
{
var path = Path.ChangeExtension(request.OutputBase, $"fix_p{i + 1:D3}.brec.xml");
2021-04-20 20:41:26 +08:00
outputPaths.Add(path);
2022-04-02 18:49:23 +08:00
using var file = XmlWriter.Create(File.Create(path), new()
{
Encoding = Encoding.UTF8,
Indent = true
});
2021-04-20 20:41:26 +08:00
XmlFlvFile.Serializer.Serialize(file, new XmlFlvFile { Tags = w.Files[i] });
}
if (w.AccompanyingTextLogs.Count > 0)
2021-04-20 20:41:26 +08:00
{
var path = Path.ChangeExtension(request.OutputBase, "txt");
2021-04-20 20:41:26 +08:00
using var writer = new StreamWriter(File.Open(path, FileMode.Append, FileAccess.Write, FileShare.None));
foreach (var (lastTagDuration, message) in w.AccompanyingTextLogs)
2021-04-20 20:41:26 +08:00
{
writer.WriteLine();
writer.WriteLine(lastTagDuration);
writer.WriteLine(message);
2021-04-20 20:41:26 +08:00
}
}
});
2021-04-14 23:46:24 +08:00
2021-05-02 21:34:27 +08:00
if (cancellationToken.IsCancellationRequested)
return new CommandResponse<FixResponse> { Status = ResponseStatus.Cancelled };
2021-04-20 20:41:26 +08:00
// Result
2021-04-19 18:20:14 +08:00
var response = await Task.Run(() =>
{
2021-04-23 18:51:27 +08:00
var (videoStats, audioStats) = statsRule.GetStats();
var countableComments = comments.Where(x => x.ActionRequired).ToArray();
2021-04-19 18:20:14 +08:00
return new FixResponse
{
InputPath = inputPath,
OutputPaths = outputPaths.ToArray(),
OutputFileCount = outputPaths.Count,
NeedFix = outputPaths.Count != 1 || countableComments.Any(),
2022-06-17 17:42:50 +08:00
Unrepairable = countableComments.Any(x => x.Type == CommentType.Unrepairable),
2022-06-22 23:57:48 +08:00
FfmpegDetected = ffmpegDetectionRule.LavfEncoderDetected && ffmpegDetectionRule.EndTagDetected,
2021-04-19 18:20:14 +08:00
2021-04-23 18:51:27 +08:00
VideoStats = videoStats,
AudioStats = audioStats,
2022-06-17 17:42:50 +08:00
IssueTypeOther = countableComments.Count(x => x.Type == CommentType.Other),
IssueTypeUnrepairable = countableComments.Count(x => x.Type == CommentType.Unrepairable),
IssueTypeTimestampJump = countableComments.Count(x => x.Type == CommentType.TimestampJump),
IssueTypeTimestampOffset = countableComments.Count(x => x.Type == CommentType.TimestampOffset),
IssueTypeDecodingHeader = countableComments.Count(x => x.Type == CommentType.DecodingHeader),
IssueTypeRepeatingData = countableComments.Count(x => x.Type == CommentType.RepeatingData)
2021-04-19 18:20:14 +08:00
};
});
return new CommandResponse<FixResponse> { Status = ResponseStatus.OK, Data = response };
2021-04-19 18:20:14 +08:00
}
2021-05-02 21:34:27 +08:00
catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested)
{
return new CommandResponse<FixResponse> { Status = ResponseStatus.Cancelled };
}
2021-04-19 18:20:14 +08:00
catch (NotFlvFileException ex)
{
return new CommandResponse<FixResponse>
{
Status = ResponseStatus.NotFlvFile,
Exception = ex,
ErrorMessage = ex.Message
};
}
catch (UnknownFlvTagTypeException ex)
{
return new CommandResponse<FixResponse>
{
Status = ResponseStatus.UnknownFlvTagType,
Exception = ex,
ErrorMessage = ex.Message
};
}
catch (Exception ex)
{
return new CommandResponse<FixResponse>
{
Status = ResponseStatus.Error,
Exception = ex,
ErrorMessage = ex.Message
};
}
finally
{
2021-04-20 20:41:26 +08:00
flvFileStream?.Dispose();
2021-04-19 18:20:14 +08:00
}
2021-04-14 23:46:24 +08:00
}
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 CreateAccompanyingTextLogStream()
2021-04-14 23:46:24 +08:00
{
var path = Path.ChangeExtension(this.pathTemplate, "txt");
2021-05-21 21:04:37 +08:00
return new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read);
2021-04-14 23:46:24 +08:00
}
2021-05-21 21:04:37 +08:00
public (Stream stream, object? state) CreateOutputStream()
2021-04-14 23:46:24 +08:00
{
var i = this.fileIndex++;
var path = Path.ChangeExtension(this.pathTemplate, $"fix_p{i:D3}.flv");
2021-04-20 20:41:26 +08:00
var fileStream = File.Open(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
2021-04-14 23:46:24 +08:00
BeforeFileOpen?.Invoke(this, path);
2021-05-21 21:04:37 +08:00
return (fileStream, null);
2021-04-14 23:46:24 +08:00
}
}
}
}