using System; using System.Collections.Concurrent; using System.Diagnostics; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace JellyfinPlugin { public static class CropHelper { private static string GetFFmpegPath() { if (File.Exists("/usr/lib/jellyfin-ffmpeg/ffmpeg")) return "/usr/lib/jellyfin-ffmpeg/ffmpeg"; if (File.Exists("/usr/bin/ffmpeg")) return "/usr/bin/ffmpeg"; return "ffmpeg"; } public static async Task DetectCropAsync(string filePath, double limit) { // FFmpeg w nowych wersjach przyjmuje limit w formie ułamka, np. 24/255 to ok 0.094 // Używamy domyślnego limitu ffmpeg (ok. 0.094), ewentualnie tego z configu jeśli użytkownik zmienił string limitStr = limit.ToString(System.Globalization.CultureInfo.InvariantCulture); var startInfo = new ProcessStartInfo { FileName = GetFFmpegPath(), // KLUCZOWE: reset=1, żeby oceniał każdą klatkę niezależnie! Arguments = $"-skip_frame nokey -i \"{filePath}\" -vf \"cropdetect=limit={limitStr}:round=16:reset=1\" -f null -", RedirectStandardError = true, RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true }; using var process = new Process { StartInfo = startInfo }; process.Start(); string output = await process.StandardError.ReadToEndAsync(); await process.WaitForExitAsync(); var regex = new Regex(@"crop=([0-9]+):([0-9]+):([0-9]+):([0-9]+)"); var matches = regex.Matches(output); var crops = new ConcurrentDictionary(); int totalFrames = 0; foreach (Match match in matches) { string cropStr = match.Value.Replace("crop=", ""); crops.AddOrUpdate(cropStr, 1, (_, count) => count + 1); totalFrames++; } if (totalFrames == 0) return string.Empty; // Bierzemy pod uwagę tylko wymiary, które występują przez co najmniej 5% czasu filmu // Dzięki temu ignorujemy jednosekundowe loga czy zaszumione klatki var validCrops = crops.Where(kvp => (double)kvp.Value / totalFrames > 0.05).ToList(); if (!validCrops.Any()) { validCrops = crops.OrderByDescending(kvp => kvp.Value).Take(1).ToList(); } // Tworzymy maksymalną obwiednię (Bounding Box) ze wszystkich WAŻNYCH kadrów // Dzięki temu jeśli film to np. Interstellar (30% czasu w 16:9), kadr 16:9 zostanie zachowany // i nie utniemy ważnych scen! int minX = int.MaxValue, minY = int.MaxValue, maxXW = 0, maxYH = 0; foreach (var kvp in validCrops) { var parts = kvp.Key.Split(':'); if (parts.Length == 4 && int.TryParse(parts[0], out int w) && int.TryParse(parts[1], out int h) && int.TryParse(parts[2], out int x) && int.TryParse(parts[3], out int y)) { minX = Math.Min(minX, x); minY = Math.Min(minY, y); maxXW = Math.Max(maxXW, x + w); maxYH = Math.Max(maxYH, y + h); } } int finalW = maxXW - minX; int finalH = maxYH - minY; if (finalW <= 0 || finalH <= 0) return string.Empty; return $"{finalW}:{finalH}:{minX}:{minY}"; } } }