Update configuration and enhance media processing: add stream token TTL and timeout settings in .env.example, improve error handling in media endpoints, and refactor command handlers for asynchronous operations. Update documentation to reflect current application state and features.
build / backend (push) Successful in 2m19s
build / frontend (push) Successful in 53s
tests / backend-tests (push) Successful in 2m27s

This commit is contained in:
Leonid Pershin
2026-07-25 23:14:55 +03:00
parent a261e261f0
commit 6c18a9da79
42 changed files with 387 additions and 98 deletions
@@ -17,15 +17,23 @@ public sealed class BumperTemplateStorage(MediaPathResolver paths) : IBumperTemp
CancellationToken cancellationToken
) => SaveAsync(templateId, Audio, extension, content, cancellationToken);
public void DeleteAudio(Guid templateId) => DeleteKind(templateId, Audio);
public void DeleteTemplate(Guid templateId)
public Task DeleteAudioAsync(Guid templateId, CancellationToken cancellationToken)
{
var dir = paths.BumperTemplateDir(templateId);
if (Directory.Exists(dir))
Directory.Delete(dir, recursive: true);
DeleteKind(templateId, Audio);
return Task.CompletedTask;
}
public Task DeleteTemplateAsync(Guid templateId, CancellationToken cancellationToken) =>
Task.Run(
() =>
{
var dir = paths.BumperTemplateDir(templateId);
if (Directory.Exists(dir))
Directory.Delete(dir, recursive: true);
},
cancellationToken
);
public string? AudioPath(Guid templateId, string? extension) =>
ResolvePath(templateId, Audio, extension);
@@ -82,6 +82,7 @@ public sealed class FfmpegBumperRenderer(
_media.FfmpegPath,
args,
lowPriority: true,
TimeSpan.FromSeconds(_media.TranscodeTimeoutSeconds),
cancellationToken
);
if (result.ExitCode != 0)
@@ -49,6 +49,7 @@ public sealed class FfmpegMediaProcessor(
_media.FfmpegPath,
args,
lowPriority: true,
TimeSpan.FromSeconds(_media.TranscodeTimeoutSeconds),
cancellationToken
);
if (result.ExitCode != 0)
@@ -169,6 +170,7 @@ public sealed class FfmpegMediaProcessor(
_media.FfprobePath,
["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", path],
lowPriority: false,
TimeSpan.FromSeconds(_media.ProbeTimeoutSeconds),
cancellationToken
);
if (result.ExitCode != 0)
@@ -27,6 +27,7 @@ public sealed class FfprobeAudioProbe(IOptions<MediaOptions> mediaOptions) : IAu
_media.FfprobePath,
["-v", "quiet", "-print_format", "json", "-show_format", absolutePath],
lowPriority: false,
TimeSpan.FromSeconds(_media.ProbeTimeoutSeconds),
cancellationToken
);
if (result.ExitCode != 0)
@@ -43,11 +43,12 @@ public sealed class FileSystemMediaStorage(MediaPathResolver paths) : IMediaStor
return token;
}
public void DeleteUpload(string uploadToken)
public Task DeleteUploadAsync(string uploadToken, CancellationToken cancellationToken)
{
var path = paths.UploadPath(uploadToken);
if (File.Exists(path))
File.Delete(path);
return Task.CompletedTask;
}
public Task PromoteToOriginalAsync(
@@ -72,14 +73,24 @@ public sealed class FileSystemMediaStorage(MediaPathResolver paths) : IMediaStor
return Task.CompletedTask;
}
public void DeleteAssetArtifacts(Guid assetId, string extension)
{
var original = paths.OriginalPath(assetId, extension);
if (File.Exists(original))
File.Delete(original);
public Task DeleteAssetArtifactsAsync(
Guid assetId,
string extension,
CancellationToken cancellationToken
) =>
// Каталог сегментов удаляется рекурсивно (может быть много .ts) — офлоадим с вызывающего потока
// (запрос/фоновый сервис), чтобы не блокировать его на время файлового I/O.
Task.Run(
() =>
{
var original = paths.OriginalPath(assetId, extension);
if (File.Exists(original))
File.Delete(original);
var assetDir = paths.AssetDir(assetId);
if (Directory.Exists(assetDir))
Directory.Delete(assetDir, recursive: true);
}
var assetDir = paths.AssetDir(assetId);
if (Directory.Exists(assetDir))
Directory.Delete(assetDir, recursive: true);
},
cancellationToken
);
}
@@ -30,11 +30,12 @@ public sealed class ImageStore(MediaPathResolver paths) : IImageStore
await File.WriteAllBytesAsync(path, content, cancellationToken);
}
public void Delete(Guid imageId, string extension)
public Task DeleteAsync(Guid imageId, string extension, CancellationToken cancellationToken)
{
var path = paths.ImagePath(imageId, Normalize(extension));
if (File.Exists(path))
File.Delete(path);
return Task.CompletedTask;
}
public string? ResolvePath(Guid imageId, string extension)
@@ -28,4 +28,12 @@ public sealed class MediaOptions
/// <summary>Целевая интегральная громкость, LUFS (−16 — типично для стриминга; тише — уменьшить).</summary>
public int LoudnessTargetLufs { get; init; } = -16;
/// <summary>Таймаут одного вызова ffprobe, секунды (0 — без таймаута). Пробирование метаданных —
/// быстрая операция; страхует от подвисшего ffprobe на битом/сетевом источнике.</summary>
public int ProbeTimeoutSeconds { get; init; } = 120;
/// <summary>Таймаут одного вызова ffmpeg (транскод/рендер заставки), секунды (0 — без таймаута).
/// Не даёт зависшему процессу вечно держать слот параллелизма. Поднять для очень длинных файлов.</summary>
public int TranscodeTimeoutSeconds { get; init; } = 1800;
}
@@ -13,6 +13,7 @@ internal static class ProcessRunner
string fileName,
IEnumerable<string> arguments,
bool lowPriority,
TimeSpan timeout,
CancellationToken cancellationToken
)
{
@@ -58,13 +59,30 @@ internal static class ProcessRunner
}
}
// Таймаут (если задан) — отдельный источник отмены, слитый с внешним: зависший ffmpeg/ffprobe
// (битый источник, -stream_loop и т.п.) не должен держать слот параллелизма/тик планировщика вечно.
using var timeoutCts =
timeout > TimeSpan.Zero ? new CancellationTokenSource(timeout) : null;
using var linked =
timeoutCts is null
? null
: CancellationTokenSource.CreateLinkedTokenSource(
cancellationToken,
timeoutCts.Token
);
var waitToken = linked?.Token ?? cancellationToken;
try
{
await process.WaitForExitAsync(cancellationToken);
await process.WaitForExitAsync(waitToken);
}
catch (OperationCanceledException)
{
TryKill(process);
if (timeoutCts is { IsCancellationRequested: true } && !cancellationToken.IsCancellationRequested)
throw new TimeoutException(
$"Процесс {fileName} превысил таймаут {timeout.TotalSeconds:0}с и был прерван."
);
throw;
}