using ExifLibrary; using FFMpegCore; namespace VoidCat.Services.Files; /// /// Service which utilizes ffmpeg to strip metadata from media /// and compress media to reduce storage costs /// public class CompressContent { private readonly ILogger _logger; public CompressContent(ILogger logger) { _logger = logger; } public async Task TryCompressMedia(string input, string output, CancellationToken cts) { try { string? outMime = null; var inExt = Path.GetExtension(input).ToLower(); var isImage = false; switch (inExt) { case ".jpg": case ".jpeg": case ".gif": case ".png": case ".bmp": case ".tiff": case ".heic": { output = Path.ChangeExtension(output, ".webp"); outMime = "image/webp"; isImage = true; break; } } var ffProbe = await TryProbe(input, cts); var probe = isImage ? await ImageFile.FromFileAsync(input) : default; var ffmpeg = FFMpegArguments .FromFileInput(input) .OutputToFile(output, true, o => { o.WithoutMetadata(); if (inExt == ".gif") { o.Loop(0); } if (probe != default) { var orientation = probe.Properties.Get>(ExifTag.Orientation); if (orientation != default && orientation.Value != Orientation.Normal) { if (orientation.Value == Orientation.RotatedRight) { o.WithCustomArgument("-metadata:s:v rotate=\"90\""); } else if (orientation.Value == Orientation.RotatedLeft) { o.WithCustomArgument("-metadata:s:v rotate=\"-90\""); } } } }) .CancellableThrough(cts); _logger.LogInformation("Running: {command}", ffmpeg.Arguments); var result = await ffmpeg.ProcessAsynchronously(); return new(result, output) { MimeType = outMime, Width = ffProbe?.PrimaryVideoStream?.Width, Height = ffProbe?.PrimaryVideoStream?.Height, }; } catch (Exception ex) { _logger.LogError(ex, "Could not strip metadata"); } return new(false, output); } private async Task TryProbe(string path, CancellationToken cts) { try { return await FFProbe.AnalyseAsync(path, cancellationToken: cts); } catch { // ignored } return default; } public record CompressResult(bool Success, string OutPath) { public string? MimeType { get; init; } public int? Width { get; init; } public int? Height { get; init; } } }