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
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:
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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 "";
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
@@ -80,6 +81,12 @@ class MediaFfmpegServiceTest {
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
if [ "$1" = "-hide_banner" ] && [ "$2" = "-muxers" ]; then
|
||||
cat <<'EOF'
|
||||
E avif
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "-still-picture" ]; then
|
||||
@@ -117,4 +124,42 @@ class MediaFfmpegServiceTest {
|
||||
assertTrue(Files.exists(target));
|
||||
assertEquals("ok", Files.readString(target));
|
||||
}
|
||||
|
||||
@Test
|
||||
void canEncode_avifShouldRequireMuxerAvailability() throws Exception {
|
||||
Path fakeFfmpeg = tempDir.resolve("fake-ffmpeg-no-avif-muxer.sh");
|
||||
Files.writeString(
|
||||
fakeFfmpeg,
|
||||
"""
|
||||
#!/bin/sh
|
||||
if [ "$1" = "-hide_banner" ] && [ "$2" = "-encoders" ]; then
|
||||
cat <<'EOF'
|
||||
V..... mjpeg
|
||||
V..... libaom-av1
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
if [ "$1" = "-hide_banner" ] && [ "$2" = "-muxers" ]; then
|
||||
cat <<'EOF'
|
||||
E mp4
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
"""
|
||||
);
|
||||
Files.setPosixFilePermissions(
|
||||
fakeFfmpeg,
|
||||
Set.of(
|
||||
PosixFilePermission.OWNER_READ,
|
||||
PosixFilePermission.OWNER_WRITE,
|
||||
PosixFilePermission.OWNER_EXECUTE
|
||||
)
|
||||
);
|
||||
|
||||
MediaFfmpegService service = new MediaFfmpegService(fakeFfmpeg.toString());
|
||||
|
||||
assertTrue(service.canEncode("JPEG"));
|
||||
assertFalse(service.canEncode("AVIF"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user