feat(front-end): update media ffmpeg
Some checks failed
Build and Deploy / test-backend (push) Successful in 29s
PR Checks / prettier-autofix (pull_request) Successful in 13s
Build and Deploy / test-frontend (push) Successful in 1m3s
PR Checks / security-sast (pull_request) Successful in 32s
PR Checks / test-backend (pull_request) Successful in 29s
PR Checks / test-frontend (pull_request) Successful in 1m6s
Build and Deploy / build-and-push (push) Failing after 1m0s
Build and Deploy / deploy (push) Has been skipped

This commit is contained in:
2026-03-10 17:20:45 +01:00
parent ed8fd89217
commit a4facf74d7
7 changed files with 186 additions and 12 deletions

View File

@@ -402,7 +402,7 @@ public class AdminMediaControllerService {
if (!skippedFormats.isEmpty()) {
logger.warn(
"Skipping media formats for asset {} because FFmpeg encoders are unavailable: {}",
"Skipping media formats for asset {} because FFmpeg support is unavailable: {}",
asset.getId(),
String.join(", ", skippedFormats)
);

View File

@@ -29,13 +29,18 @@ public class MediaFfmpegService {
"WEBP", List.of("libwebp", "webp"),
"AVIF", List.of("libaom-av1", "librav1e", "libsvtav1")
);
private static final Map<String, List<String>> REQUIRED_MUXERS = Map.of(
"AVIF", List.of("avif")
);
private final String ffmpegExecutable;
private final Set<String> availableEncoders;
private final Set<String> availableMuxers;
public MediaFfmpegService(@Value("${media.ffmpeg.path:ffmpeg}") String ffmpegPath) {
this.ffmpegExecutable = resolveExecutable(ffmpegPath);
this.availableEncoders = Collections.unmodifiableSet(loadAvailableEncoders());
this.availableMuxers = Collections.unmodifiableSet(loadAvailableMuxers());
}
public void generateVariant(Path source, Path target, int widthPx, int heightPx, String format) throws IOException {
@@ -47,9 +52,13 @@ public class MediaFfmpegService {
Path targetPath = sanitizeMediaPath(target, "target", false);
Files.createDirectories(targetPath.getParent());
String encoder = resolveEncoder(format);
String normalizedFormat = normalizeFormat(format);
String encoder = resolveEncoder(normalizedFormat);
if (encoder == null) {
throw new IOException("FFmpeg encoder not available for media format " + format + ".");
throw new IOException("FFmpeg encoder not available for media format " + normalizedFormat + ".");
}
if (!hasRequiredMuxer(normalizedFormat)) {
throw new IOException("FFmpeg muxer not available for media format " + normalizedFormat + ".");
}
List<String> command = new ArrayList<>();
@@ -66,7 +75,7 @@ public class MediaFfmpegService {
command.add("1");
command.add("-an");
switch (format) {
switch (normalizedFormat) {
case "JPEG" -> {
command.add("-c:v");
command.add(encoder);
@@ -86,8 +95,10 @@ public class MediaFfmpegService {
command.add("30");
command.add("-b:v");
command.add("0");
command.add("-f");
command.add("avif");
}
default -> throw new IllegalArgumentException("Unsupported media format: " + format);
default -> throw new IllegalArgumentException("Unsupported media format: " + normalizedFormat);
}
command.add(targetPath.toString());
@@ -112,14 +123,16 @@ public class MediaFfmpegService {
}
public boolean canEncode(String format) {
return resolveEncoder(format) != null;
String normalizedFormat = normalizeFormat(format);
return resolveEncoder(normalizedFormat) != null && hasRequiredMuxer(normalizedFormat);
}
private String resolveEncoder(String format) {
if (format == null) {
String normalizedFormat = normalizeFormat(format);
if (normalizedFormat == null) {
return null;
}
List<String> candidates = ENCODER_CANDIDATES.get(format.trim().toUpperCase(Locale.ROOT));
List<String> candidates = ENCODER_CANDIDATES.get(normalizedFormat);
if (candidates == null) {
return null;
}
@@ -129,6 +142,21 @@ public class MediaFfmpegService {
.orElse(null);
}
private boolean hasRequiredMuxer(String format) {
List<String> requiredMuxers = REQUIRED_MUXERS.get(format);
if (requiredMuxers == null || requiredMuxers.isEmpty()) {
return true;
}
return requiredMuxers.stream().anyMatch(availableMuxers::contains);
}
private String normalizeFormat(String format) {
if (format == null) {
return null;
}
return format.trim().toUpperCase(Locale.ROOT);
}
private Set<String> loadAvailableEncoders() {
List<String> command = List.of(ffmpegExecutable, "-hide_banner", "-encoders");
try {
@@ -154,6 +182,31 @@ public class MediaFfmpegService {
}
}
private Set<String> loadAvailableMuxers() {
List<String> command = List.of(ffmpegExecutable, "-hide_banner", "-muxers");
try {
Process process = startValidatedProcess(command);
String output;
try (InputStream processStream = process.getInputStream()) {
output = new String(processStream.readAllBytes(), StandardCharsets.UTF_8);
}
int exitCode = process.waitFor();
if (exitCode != 0) {
logger.warn("Unable to inspect FFmpeg muxers. Falling back to empty muxer list.");
return Set.of();
}
return parseAvailableMuxers(output);
} catch (Exception e) {
logger.warn(
"Unable to inspect FFmpeg muxers for executable '{}'. Falling back to empty muxer list. {}",
ffmpegExecutable,
e.getMessage()
);
return Set.of();
}
}
private Process startValidatedProcess(List<String> command) throws IOException {
// nosemgrep: java.lang.security.audit.command-injection-process-builder.command-injection-process-builder
return new ProcessBuilder(List.copyOf(command))
@@ -264,6 +317,26 @@ public class MediaFfmpegService {
return encoders;
}
private Set<String> parseAvailableMuxers(String output) {
if (output == null || output.isBlank()) {
return Set.of();
}
Set<String> muxers = new LinkedHashSet<>();
for (String line : output.split("\\R")) {
String trimmed = line.trim();
if (trimmed.isBlank() || trimmed.startsWith("--") || trimmed.startsWith("Muxers:")) {
continue;
}
String[] parts = trimmed.split("\\s+", 3);
if (parts.length < 2) {
continue;
}
muxers.add(parts[1]);
}
return muxers;
}
private String truncate(String output) {
if (output == null || output.isBlank()) {
return "";