src/main/java/org/example/videoclips/processing/ClipProcessor.java now cleans up staged local input/output files in a finally block, so worker temp files are removed on success and failure

instead of accumulating. Cleanup is controlled by the new video-clipping.ffmpeg.cleanup-local-files property wired through src/main/java/org/example/videoclips/config/VideoClippingProperties.java
  and src/main/resources/application.properties.

  I also improved FFmpeg failure diagnostics in src/main/java/org/example/videoclips/processing/FfmpegVideoClipperAdapter.java. It now captures the merged process output and includes a trimmed
  version in the exception instead of only reporting the exit code, which makes job failures much easier to debug.
This commit is contained in:
JSLMPR 2026-07-09 00:21:36 +02:00
parent abf1259397
commit 8f4804c2ab
4 changed files with 89 additions and 3 deletions

View File

@ -179,6 +179,7 @@ public class VideoClippingProperties {
private String ffmpegBinary = "ffmpeg"; private String ffmpegBinary = "ffmpeg";
private String inputDirectory = "./tmp/ffmpeg-input"; private String inputDirectory = "./tmp/ffmpeg-input";
private String outputDirectory = "./tmp/ffmpeg-output"; private String outputDirectory = "./tmp/ffmpeg-output";
private boolean cleanupLocalFiles = true;
public String getFfmpegBinary() { public String getFfmpegBinary() {
return ffmpegBinary; return ffmpegBinary;
@ -203,5 +204,13 @@ public class VideoClippingProperties {
public void setOutputDirectory(String outputDirectory) { public void setOutputDirectory(String outputDirectory) {
this.outputDirectory = outputDirectory; this.outputDirectory = outputDirectory;
} }
public boolean isCleanupLocalFiles() {
return cleanupLocalFiles;
}
public void setCleanupLocalFiles(boolean cleanupLocalFiles) {
this.cleanupLocalFiles = cleanupLocalFiles;
}
} }
} }

View File

@ -12,8 +12,12 @@ import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Files;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Comparator;
import java.io.IOException;
import java.util.stream.Stream;
@Component @Component
public class ClipProcessor { public class ClipProcessor {
@ -41,6 +45,8 @@ public class ClipProcessor {
} }
public void processNow(String jobId) { public void processNow(String jobId) {
Path localInputPath = null;
List<GeneratedClip> generatedClips = List.of();
try { try {
ClipJob job = requireJob(jobId); ClipJob job = requireJob(jobId);
repository.saveJob(job.withStatus(ClipJobStatus.RUNNING).withStartedAt(Instant.now())); repository.saveJob(job.withStatus(ClipJobStatus.RUNNING).withStartedAt(Instant.now()));
@ -55,10 +61,10 @@ public class ClipProcessor {
recordEvent(jobId, "CANCELLED", "Job was cancelled before clip generation"); recordEvent(jobId, "CANCELLED", "Job was cancelled before clip generation");
return; return;
} }
Path localInputPath = Path.of(properties.getFfmpeg().getInputDirectory()).resolve(asset.sourceObjectKey()); localInputPath = Path.of(properties.getFfmpeg().getInputDirectory()).resolve(asset.sourceObjectKey());
objectStoragePort.materializeSourceObject(asset.sourceObjectKey(), localInputPath); objectStoragePort.materializeSourceObject(asset.sourceObjectKey(), localInputPath);
recordEvent(jobId, "SOURCE_MATERIALIZED", "Source object staged to " + localInputPath); recordEvent(jobId, "SOURCE_MATERIALIZED", "Source object staged to " + localInputPath);
List<GeneratedClip> generatedClips = videoClipperPort.generateClips( generatedClips = videoClipperPort.generateClips(
jobId, jobId,
asset.id(), asset.id(),
asset.sourceObjectKey(), asset.sourceObjectKey(),
@ -88,6 +94,8 @@ public class ClipProcessor {
recordEvent(jobId, "FAILED", ex.getMessage()); recordEvent(jobId, "FAILED", ex.getMessage());
} }
throw new IllegalStateException("Clip processing failed for job " + jobId, ex); throw new IllegalStateException("Clip processing failed for job " + jobId, ex);
} finally {
cleanupLocalFiles(jobId, localInputPath, generatedClips);
} }
} }
@ -108,4 +116,57 @@ public class ClipProcessor {
Instant.now() Instant.now()
)); ));
} }
private void cleanupLocalFiles(String jobId, Path localInputPath, List<GeneratedClip> generatedClips) {
if (!properties.getFfmpeg().isCleanupLocalFiles()) {
return;
}
if (generatedClips != null) {
for (GeneratedClip generatedClip : generatedClips) {
deleteIfExists(generatedClip.localPath());
}
}
if (localInputPath != null) {
deleteIfExists(localInputPath);
Path inputRoot = Path.of(properties.getFfmpeg().getInputDirectory());
deleteEmptyParents(localInputPath.getParent(), inputRoot);
}
Path outputDir = Path.of(properties.getFfmpeg().getOutputDirectory(), jobId);
deleteRecursively(outputDir);
}
private void deleteIfExists(Path path) {
if (path == null) {
return;
}
try {
Files.deleteIfExists(path);
} catch (IOException ignored) {
}
}
private void deleteRecursively(Path path) {
if (path == null || !Files.exists(path)) {
return;
}
try (Stream<Path> stream = Files.walk(path)) {
stream.sorted(Comparator.reverseOrder()).forEach(this::deleteIfExists);
} catch (IOException ignored) {
}
}
private void deleteEmptyParents(Path start, Path stopAt) {
Path current = start;
while (current != null && stopAt != null && current.startsWith(stopAt) && !current.equals(stopAt)) {
try {
Files.deleteIfExists(current);
} catch (IOException ex) {
break;
}
current = current.getParent();
}
}
} }

View File

@ -7,8 +7,10 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.charset.StandardCharsets;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -48,9 +50,10 @@ public class FfmpegVideoClipperAdapter implements VideoClipperPort {
processBuilder.redirectErrorStream(true); processBuilder.redirectErrorStream(true);
try { try {
Process process = processBuilder.start(); Process process = processBuilder.start();
String processOutput = readProcessOutput(process.getInputStream());
int exitCode = process.waitFor(); int exitCode = process.waitFor();
if (exitCode != 0) { if (exitCode != 0) {
throw new IllegalStateException("FFmpeg exited with code " + exitCode); throw new IllegalStateException("FFmpeg exited with code " + exitCode + ": " + trimProcessOutput(processOutput));
} }
} catch (IOException ex) { } catch (IOException ex) {
throw new IllegalStateException("Unable to start FFmpeg process", ex); throw new IllegalStateException("Unable to start FFmpeg process", ex);
@ -62,6 +65,10 @@ public class FfmpegVideoClipperAdapter implements VideoClipperPort {
return buildClipManifest(jobId, assetId, outputDir, sourceDurationSeconds, segmentDurationSeconds, accuracyMode); return buildClipManifest(jobId, assetId, outputDir, sourceDurationSeconds, segmentDurationSeconds, accuracyMode);
} }
private String readProcessOutput(InputStream inputStream) throws IOException {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
private Path resolveInputFile(String sourceObjectKey) { private Path resolveInputFile(String sourceObjectKey) {
String normalized = sourceObjectKey.startsWith("/") ? sourceObjectKey.substring(1) : sourceObjectKey; String normalized = sourceObjectKey.startsWith("/") ? sourceObjectKey.substring(1) : sourceObjectKey;
return Path.of(properties.getFfmpeg().getInputDirectory()).resolve(normalized); return Path.of(properties.getFfmpeg().getInputDirectory()).resolve(normalized);
@ -136,4 +143,12 @@ public class FfmpegVideoClipperAdapter implements VideoClipperPort {
} }
return clips; return clips;
} }
private String trimProcessOutput(String output) {
if (output == null || output.isBlank()) {
return "no process output";
}
String normalized = output.replaceAll("\\s+", " ").trim();
return normalized.length() <= 300 ? normalized : normalized.substring(0, 300) + "...";
}
} }

View File

@ -21,3 +21,4 @@ video-clipping.database-queue.batch-size=5
video-clipping.ffmpeg.ffmpeg-binary=ffmpeg video-clipping.ffmpeg.ffmpeg-binary=ffmpeg
video-clipping.ffmpeg.input-directory=./tmp/ffmpeg-input video-clipping.ffmpeg.input-directory=./tmp/ffmpeg-input
video-clipping.ffmpeg.output-directory=./tmp/ffmpeg-output video-clipping.ffmpeg.output-directory=./tmp/ffmpeg-output
video-clipping.ffmpeg.cleanup-local-files=true