Add ConvertAnimatedJob (EJobType.AnimatedConversion) targeting image/gif assets: extracts a static WebP first-frame thumbnail and converts the full animation to mp4/H.264, leaving originals untouched. New ConvertedPath/ ConvertedFormat/FfmpegPath settings, PathUtils extension parameter, FfmpegRunner helper, and Dockerfile ffmpeg install. Thumbnail/preview jobs now skip GIFs (handled by the conversion job) and IntegrityCheck validates converted outputs. Failed conversions are marked broken. Refs #181
54 lines
2.2 KiB
C#
54 lines
2.2 KiB
C#
using System.Diagnostics;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Lactose.Jobs;
|
|
|
|
/// <summary>
|
|
/// Runs ffmpeg/ffprobe invocations for media conversion jobs.
|
|
/// </summary>
|
|
internal static class FfmpegRunner {
|
|
/// <summary>
|
|
/// Executes ffmpeg with the given arguments, capturing output for logging.
|
|
/// </summary>
|
|
/// <param name="ffmpegPath">Path to the ffmpeg executable.</param>
|
|
/// <param name="arguments">Command-line arguments to pass to ffmpeg.</param>
|
|
/// <param name="logger">Logger for ffmpeg output.</param>
|
|
/// <param name="token">Cancellation token; the process is killed if cancelled.</param>
|
|
/// <exception cref="InvalidOperationException">Thrown when ffmpeg exits with a non-zero exit code.</exception>
|
|
internal static async Task RunAsync(string ffmpegPath, string arguments, ILogger logger, CancellationToken token) {
|
|
var startInfo = new ProcessStartInfo {
|
|
FileName = ffmpegPath,
|
|
Arguments = arguments,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
};
|
|
|
|
using var process = new Process { StartInfo = startInfo };
|
|
process.Start();
|
|
|
|
var stdoutTask = process.StandardOutput.ReadToEndAsync();
|
|
var stderrTask = process.StandardError.ReadToEndAsync();
|
|
|
|
try {
|
|
await process.WaitForExitAsync(token);
|
|
} catch (OperationCanceledException) {
|
|
try { process.Kill(entireProcessTree: true); } catch { /* already exited */ }
|
|
throw;
|
|
}
|
|
|
|
var stdout = await stdoutTask;
|
|
var stderr = await stderrTask;
|
|
|
|
if (!string.IsNullOrEmpty(stdout))
|
|
logger.LogTrace("ffmpeg stdout: {Output}", stdout);
|
|
if (!string.IsNullOrEmpty(stderr))
|
|
logger.LogTrace("ffmpeg stderr: {Output}", stderr);
|
|
|
|
if (process.ExitCode != 0) {
|
|
logger.LogError("ffmpeg exited with code {Code}. Stderr: {Error}", process.ExitCode, stderr);
|
|
throw new InvalidOperationException($"ffmpeg exited with code {process.ExitCode}.");
|
|
}
|
|
}
|
|
} |