feat: add transcode workspace and media browser
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
using LiveRecorder.Application.Abstractions.Persistence;
|
||||
using LiveRecorder.Application.Abstractions.Recording;
|
||||
using LiveRecorder.Application.Models.RecordTasks;
|
||||
using LiveRecorder.Domain.Entities;
|
||||
using LiveRecorder.Domain.Enums;
|
||||
|
||||
namespace LiveRecorder.Application.Services;
|
||||
|
||||
public sealed class TranscodeTaskService
|
||||
{
|
||||
private readonly IRecordTaskRepository _recordTaskRepository;
|
||||
private readonly IFfmpegService _ffmpegService;
|
||||
|
||||
public TranscodeTaskService(
|
||||
IRecordTaskRepository recordTaskRepository,
|
||||
IFfmpegService ffmpegService)
|
||||
{
|
||||
_recordTaskRepository = recordTaskRepository;
|
||||
_ffmpegService = ffmpegService;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<TranscodeTaskItemDto>> ListAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tasks = await _recordTaskRepository.ListAsync(null, cancellationToken);
|
||||
if (tasks.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var runtimeStates = _ffmpegService.GetTaskRuntimeStates(tasks.Select(static item => item.Id).ToArray());
|
||||
|
||||
return tasks
|
||||
.Select(task =>
|
||||
{
|
||||
runtimeStates.TryGetValue(task.Id, out var runtimeState);
|
||||
var mappedTask = RecordModelMapper.MapTask(task, runtimeState);
|
||||
var sourceFilePath = ResolveManualTranscodeSourcePath(task);
|
||||
var canManualTranscode = CanManuallyTranscode(task, runtimeState, sourceFilePath);
|
||||
|
||||
return new TranscodeTaskItemDto
|
||||
{
|
||||
Task = mappedTask,
|
||||
Result = task.Result is null ? null : RecordModelMapper.MapResult(task.Result),
|
||||
SourceFilePath = sourceFilePath,
|
||||
CanManualTranscode = canManualTranscode
|
||||
};
|
||||
})
|
||||
.Where(static item => ShouldInclude(item))
|
||||
.OrderByDescending(static item => item.Task.CreatedAt)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static bool ShouldInclude(TranscodeTaskItemDto item)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(item.Task.PostProcessStage))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.CanManualTranscode)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return item.Task.OutputFormat == RecordOutputFormat.Mp4 &&
|
||||
item.Task.Status == RecordTaskStatus.Processing;
|
||||
}
|
||||
|
||||
private static bool CanManuallyTranscode(
|
||||
RecordTask recordTask,
|
||||
RecordTaskRuntimeState? runtimeState,
|
||||
string? sourceFilePath)
|
||||
{
|
||||
if (recordTask.OutputFormat != RecordOutputFormat.Mp4 ||
|
||||
runtimeState is not null ||
|
||||
IsActiveStatus(recordTask.Status))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(sourceFilePath) && File.Exists(sourceFilePath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var resultPath = recordTask.Result?.FilePath?.ToLowerInvariant() ?? string.Empty;
|
||||
var errorText = $"{recordTask.ErrorMessage ?? string.Empty} {recordTask.Result?.ErrorMessage ?? string.Empty}".ToLowerInvariant();
|
||||
return resultPath.EndsWith(".ts", StringComparison.OrdinalIgnoreCase) ||
|
||||
errorText.Contains("finaliz", StringComparison.Ordinal) ||
|
||||
errorText.Contains("intermediate ts", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string? ResolveManualTranscodeSourcePath(RecordTask recordTask)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(recordTask.Result?.FilePath) &&
|
||||
recordTask.Result.FilePath.EndsWith(".ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return recordTask.Result.FilePath;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(recordTask.OutputFilePath))
|
||||
{
|
||||
var normalizedOutput = NormalizeAbsolutePath(recordTask.OutputFilePath);
|
||||
if (normalizedOutput.EndsWith(".ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return normalizedOutput;
|
||||
}
|
||||
|
||||
var candidate = Path.ChangeExtension(normalizedOutput, ".ts");
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
var singleFileCandidate = Path.Combine(
|
||||
Path.GetDirectoryName(normalizedOutput) ?? string.Empty,
|
||||
$"{Path.GetFileNameWithoutExtension(normalizedOutput)}.recording.ts");
|
||||
if (File.Exists(singleFileCandidate))
|
||||
{
|
||||
return singleFileCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string NormalizeAbsolutePath(string path) =>
|
||||
Path.IsPathRooted(path)
|
||||
? path
|
||||
: Path.GetFullPath(path, AppContext.BaseDirectory);
|
||||
|
||||
private static bool IsActiveStatus(RecordTaskStatus status) =>
|
||||
status is RecordTaskStatus.Starting or RecordTaskStatus.Running or RecordTaskStatus.Stopping or RecordTaskStatus.Processing;
|
||||
}
|
||||
Reference in New Issue
Block a user