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;
|
2021-05-02 21:34:27 +08:00
|
|
|
using System.Threading;
|
2021-04-14 23:46:24 +08:00
|
|
|
using System.Threading.Tasks;
|
|
|
|
using BililiveRecorder.Flv;
|
|
|
|
using BililiveRecorder.Flv.Grouping;
|
|
|
|
using BililiveRecorder.Flv.Parser;
|
|
|
|
using BililiveRecorder.Flv.Pipeline;
|
2021-04-29 23:51:06 +08:00
|
|
|
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;
|
|
|
|
|
|
|
|
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; }
|
|
|
|
|
2021-04-23 18:51:27 +08:00
|
|
|
public FlvStats? VideoStats { get; set; }
|
|
|
|
public FlvStats? AudioStats { get; set; }
|
|
|
|
|
2021-04-14 23:46:24 +08:00
|
|
|
public int IssueTypeOther { get; set; }
|
|
|
|
public int IssueTypeUnrepairable { get; set; }
|
|
|
|
public int IssueTypeTimestampJump { get; set; }
|
2021-04-20 20:41:26 +08:00
|
|
|
public int IssueTypeTimestampOffset { get; set; }
|
2021-04-14 23:46:24 +08:00
|
|
|
public int IssueTypeDecodingHeader { get; set; }
|
|
|
|
public int IssueTypeRepeatingData { get; set; }
|
|
|
|
}
|
|
|
|
|
|
|
|
public class FixHandler : ICommandHandler<FixRequest, FixResponse>
|
|
|
|
{
|
|
|
|
private static readonly ILogger logger = Log.ForContext<FixHandler>();
|
|
|
|
|
2021-05-02 21:34:27 +08:00
|
|
|
public Task<CommandResponse<FixResponse>> Handle(FixRequest request) => this.Handle(request, default, null);
|
2021-04-14 23:46:24 +08:00
|
|
|
|
2021-05-02 21:34:27 +08:00
|
|
|
public async Task<CommandResponse<FixResponse>> Handle(FixRequest request, CancellationToken cancellationToken, Func<double, Task>? 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;
|
|
|
|
|
2021-04-22 22:40:40 +08:00
|
|
|
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;
|
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);
|
2021-04-23 18:51:27 +08:00
|
|
|
var statsRule = new StatsRule();
|
|
|
|
var pipeline = new ProcessingPipelineBuilder(new ServiceCollection().BuildServiceProvider()).Add(statsRule).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++)
|
|
|
|
{
|
2021-05-02 22:28:52 +08:00
|
|
|
var path = Path.ChangeExtension(request.OutputBase, $"fix_p{i + 1:D3}.brec.xml");
|
2021-04-20 20:41:26 +08:00
|
|
|
outputPaths.Add(path);
|
|
|
|
using var file = new StreamWriter(File.Create(path));
|
|
|
|
XmlFlvFile.Serializer.Serialize(file, new XmlFlvFile { Tags = w.Files[i] });
|
|
|
|
}
|
|
|
|
|
|
|
|
if (w.AlternativeHeaders.Count > 0)
|
|
|
|
{
|
|
|
|
var path = Path.ChangeExtension(request.OutputBase, $"headers.txt");
|
|
|
|
using var writer = new StreamWriter(File.Open(path, FileMode.Append, FileAccess.Write, FileShare.None));
|
|
|
|
foreach (var tag in w.AlternativeHeaders)
|
|
|
|
{
|
|
|
|
writer.WriteLine();
|
|
|
|
writer.WriteLine(tag.ToString());
|
|
|
|
writer.WriteLine(tag.BinaryDataForSerializationUseOnly);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2021-04-19 18:20:14 +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();
|
|
|
|
|
2021-04-19 18:20:14 +08:00
|
|
|
var countableComments = comments.Where(x => x.T != CommentType.Logging).ToArray();
|
|
|
|
return new FixResponse
|
|
|
|
{
|
|
|
|
InputPath = inputPath,
|
|
|
|
OutputPaths = outputPaths.ToArray(),
|
|
|
|
OutputFileCount = outputPaths.Count,
|
|
|
|
|
|
|
|
NeedFix = outputPaths.Count != 1 || countableComments.Any(),
|
|
|
|
Unrepairable = countableComments.Any(x => x.T == CommentType.Unrepairable),
|
|
|
|
|
2021-04-23 18:51:27 +08:00
|
|
|
VideoStats = videoStats,
|
|
|
|
AudioStats = audioStats,
|
|
|
|
|
2021-04-19 18:20:14 +08:00
|
|
|
IssueTypeOther = countableComments.Count(x => x.T == CommentType.Other),
|
|
|
|
IssueTypeUnrepairable = countableComments.Count(x => x.T == CommentType.Unrepairable),
|
|
|
|
IssueTypeTimestampJump = countableComments.Count(x => x.T == CommentType.TimestampJump),
|
2021-04-20 20:41:26 +08:00
|
|
|
IssueTypeTimestampOffset = countableComments.Count(x => x.T == CommentType.TimestampOffset),
|
2021-04-19 18:20:14 +08:00
|
|
|
IssueTypeDecodingHeader = countableComments.Count(x => x.T == CommentType.DecodingHeader),
|
|
|
|
IssueTypeRepeatingData = countableComments.Count(x => x.T == CommentType.RepeatingData)
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
return new CommandResponse<FixResponse> { Status = ResponseStatus.OK, Result = response };
|
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
|
|
|
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++;
|
2021-05-02 22:28:52 +08:00
|
|
|
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);
|
|
|
|
return (fileStream, null!);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|