dev #37
@@ -33,7 +33,7 @@ Configura il percorso di OrcaSlicer in `backend/src/main/resources/application.p
|
||||
|
||||
- `MEDIA_STORAGE_ROOT` per la root `storage_media` usata dal backend (`original/`, `public/`, `private/`)
|
||||
- `SHOP_STORAGE_ROOT` per la root `storage_shop` usata dal backend per i modelli dei prodotti shop
|
||||
- `MEDIA_FFMPEG_PATH` per il binario `ffmpeg`
|
||||
- `MEDIA_FFMPEG_PATH` per il binario `ffmpeg` (nel deploy Docker default: `/usr/bin/ffmpeg`)
|
||||
- `MEDIA_UPLOAD_MAX_FILE_SIZE_BYTES` per il limite per asset immagine
|
||||
|
||||
```bash
|
||||
@@ -107,7 +107,7 @@ Operativamente:
|
||||
Assicurati che `slicer.path` punti al binario corretto. Su macOS è solitamente `/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer`. Su Linux è il percorso all'AppImage (estratta o meno).
|
||||
|
||||
### FFmpeg e media pubblici
|
||||
Verifica che `MEDIA_FFMPEG_PATH` punti a un `ffmpeg` con supporto JPEG, WebP e AVIF. Se gli URL media restituiti dalle API admin non sono raggiungibili, controlla che `APP_FRONTEND_BASE_URL` punti al dominio corretto, che `location /media/` sia esposto da Nginx e che il volume `storage_media` sia montato correttamente.
|
||||
Verifica che `MEDIA_FFMPEG_PATH` punti a un `ffmpeg` con supporto JPEG, WebP e AVIF (encoder + muxer AVIF). Nel container backend usa `/usr/bin/ffmpeg` per evitare binari bundled da OrcaSlicer con supporto incompleto. Se gli URL media restituiti dalle API admin non sono raggiungibili, controlla che `APP_FRONTEND_BASE_URL` punti al dominio corretto, che `location /media/` sia esposto da Nginx e che il volume `storage_media` sia montato correttamente.
|
||||
|
||||
### Database connection
|
||||
Verifica le credenziali in `application.properties`. Se usi Docker, puoi passare `DB_URL`, `DB_USERNAME` e `DB_PASSWORD` come variabili d'ambiente.
|
||||
|
||||
@@ -14,7 +14,7 @@ ARG ORCA_VERSION=2.3.1
|
||||
ARG ORCA_DOWNLOAD_URL
|
||||
|
||||
# Install system dependencies for OrcaSlicer and media processing.
|
||||
# The build fails fast if the packaged ffmpeg lacks JPEG/WebP/AVIF encoders.
|
||||
# The build fails fast if the packaged ffmpeg lacks JPEG/WebP/AVIF support.
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
@@ -30,6 +30,9 @@ RUN set -eux; \
|
||||
grep -Eq '[[:space:]]mjpeg[[:space:]]' /tmp/ffmpeg-encoders.txt; \
|
||||
grep -Eq '[[:space:]](libwebp|webp)[[:space:]]' /tmp/ffmpeg-encoders.txt; \
|
||||
grep -Eq '[[:space:]](libaom-av1|librav1e|libsvtav1)[[:space:]]' /tmp/ffmpeg-encoders.txt; \
|
||||
ffmpeg -hide_banner -muxers > /tmp/ffmpeg-muxers.txt; \
|
||||
grep -Eq '[[:space:]]avif([[:space:]]|,|$)' /tmp/ffmpeg-muxers.txt; \
|
||||
rm -f /tmp/ffmpeg-muxers.txt; \
|
||||
rm -f /tmp/ffmpeg-encoders.txt; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -71,6 +74,8 @@ ENV PATH="/opt/orcaslicer/usr/bin:${PATH}"
|
||||
# Set Slicer Path env variable for Java app
|
||||
ENV SLICER_PATH="/opt/orcaslicer/AppRun"
|
||||
ENV ASSIMP_PATH="assimp"
|
||||
# Keep media generation on the system ffmpeg (Orca bundles can miss AVIF muxer support).
|
||||
ENV MEDIA_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
|
||||
WORKDIR /app
|
||||
# Copy JAR from build stage
|
||||
|
||||
@@ -1,10 +1,61 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# In container default to system ffmpeg to avoid Orca-bundled binaries with partial codec support.
|
||||
if [ -z "${MEDIA_FFMPEG_PATH:-}" ]; then
|
||||
MEDIA_FFMPEG_PATH="/usr/bin/ffmpeg"
|
||||
fi
|
||||
export MEDIA_FFMPEG_PATH
|
||||
|
||||
validate_ffmpeg_support() {
|
||||
ffmpeg_bin="$1"
|
||||
if ! command -v "$ffmpeg_bin" >/dev/null 2>&1; then
|
||||
echo "ERROR: FFmpeg executable not found: ${ffmpeg_bin}" >&2
|
||||
exit 11
|
||||
fi
|
||||
|
||||
encoders="$(mktemp)"
|
||||
muxers="$(mktemp)"
|
||||
trap 'rm -f "$encoders" "$muxers"' EXIT
|
||||
|
||||
"$ffmpeg_bin" -hide_banner -encoders > "$encoders" 2>&1 || {
|
||||
echo "ERROR: Unable to inspect FFmpeg encoders from ${ffmpeg_bin}" >&2
|
||||
cat "$encoders" >&2
|
||||
exit 12
|
||||
}
|
||||
"$ffmpeg_bin" -hide_banner -muxers > "$muxers" 2>&1 || {
|
||||
echo "ERROR: Unable to inspect FFmpeg muxers from ${ffmpeg_bin}" >&2
|
||||
cat "$muxers" >&2
|
||||
exit 13
|
||||
}
|
||||
|
||||
grep -Eq '[[:space:]]mjpeg[[:space:]]' "$encoders" || {
|
||||
echo "ERROR: FFmpeg '${ffmpeg_bin}' missing JPEG encoder (mjpeg)." >&2
|
||||
exit 14
|
||||
}
|
||||
grep -Eq '[[:space:]](libwebp|webp)[[:space:]]' "$encoders" || {
|
||||
echo "ERROR: FFmpeg '${ffmpeg_bin}' missing WebP encoder." >&2
|
||||
exit 15
|
||||
}
|
||||
grep -Eq '[[:space:]](libaom-av1|librav1e|libsvtav1)[[:space:]]' "$encoders" || {
|
||||
echo "ERROR: FFmpeg '${ffmpeg_bin}' missing AVIF-capable encoder." >&2
|
||||
exit 16
|
||||
}
|
||||
grep -Eq '[[:space:]]avif([[:space:]]|,|$)' "$muxers" || {
|
||||
echo "ERROR: FFmpeg '${ffmpeg_bin}' missing AVIF muxer." >&2
|
||||
exit 17
|
||||
}
|
||||
}
|
||||
|
||||
validate_ffmpeg_support "$MEDIA_FFMPEG_PATH"
|
||||
|
||||
echo "----------------------------------------------------------------"
|
||||
echo "Starting Backend Application"
|
||||
echo "DB_URL: $DB_URL"
|
||||
echo "DB_USERNAME: $DB_USERNAME"
|
||||
echo "SPRING_DATASOURCE_URL: $SPRING_DATASOURCE_URL"
|
||||
echo "SLICER_PATH: $SLICER_PATH"
|
||||
echo "MEDIA_FFMPEG_PATH: $MEDIA_FFMPEG_PATH"
|
||||
echo "----------------------------------------------------------------"
|
||||
|
||||
# Determine which environment variables to use for database connection
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ services:
|
||||
- PROFILES_DIR=/app/profiles
|
||||
- MEDIA_STORAGE_ROOT=${MEDIA_STORAGE_ROOT:-/app/storage_media}
|
||||
- SHOP_STORAGE_ROOT=${SHOP_STORAGE_ROOT:-/app/storage_shop}
|
||||
- MEDIA_FFMPEG_PATH=${MEDIA_FFMPEG_PATH:-ffmpeg}
|
||||
- MEDIA_FFMPEG_PATH=${MEDIA_FFMPEG_PATH:-/usr/bin/ffmpeg}
|
||||
- MEDIA_UPLOAD_MAX_FILE_SIZE_BYTES=${MEDIA_UPLOAD_MAX_FILE_SIZE_BYTES:-26214400}
|
||||
restart: always
|
||||
logging:
|
||||
|
||||
Reference in New Issue
Block a user