feat: add transcode workspace and media browser

This commit is contained in:
2026-05-05 02:14:52 +08:00
parent f0f9ed3456
commit 56432a9ece
26 changed files with 2013 additions and 267 deletions
@@ -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;
}