Files
blackBarRemover/JellyfinPlugin/CropHelper.cs
T
2026-09-17 19:26:25 +02:00

93 lines
3.7 KiB
C#

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<string> 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<string, int>();
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}";
}
}
}