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 inputDirectory = "./tmp/ffmpeg-input";
private String outputDirectory = "./tmp/ffmpeg-output";
private boolean cleanupLocalFiles = true;
public String getFfmpegBinary() {
return ffmpegBinary;
@ -203,5 +204,13 @@ public class VideoClippingProperties {
public void setOutputDirectory(String 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 java.nio.file.Path;
import java.nio.file.Files;
import java.time.Instant;
import java.util.List;
import java.util.Comparator;
import java.io.IOException;
import java.util.stream.Stream;
@Component
public class ClipProcessor {
@ -41,6 +45,8 @@ public class ClipProcessor {
}
public void processNow(String jobId) {
Path localInputPath = null;
List<GeneratedClip> generatedClips = List.of();
try {
ClipJob job = requireJob(jobId);
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");
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);
recordEvent(jobId, "SOURCE_MATERIALIZED", "Source object staged to " + localInputPath);
List<GeneratedClip> generatedClips = videoClipperPort.generateClips(
generatedClips = videoClipperPort.generateClips(
jobId,
asset.id(),
asset.sourceObjectKey(),
@ -88,6 +94,8 @@ public class ClipProcessor {
recordEvent(jobId, "FAILED", ex.getMessage());
}
throw new IllegalStateException("Clip processing failed for job " + jobId, ex);
} finally {
cleanupLocalFiles(jobId, localInputPath, generatedClips);
}
}
@ -108,4 +116,57 @@ public class ClipProcessor {
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 java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@ -48,9 +50,10 @@ public class FfmpegVideoClipperAdapter implements VideoClipperPort {
processBuilder.redirectErrorStream(true);
try {
Process process = processBuilder.start();
String processOutput = readProcessOutput(process.getInputStream());
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IllegalStateException("FFmpeg exited with code " + exitCode);
throw new IllegalStateException("FFmpeg exited with code " + exitCode + ": " + trimProcessOutput(processOutput));
}
} catch (IOException 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);
}
private String readProcessOutput(InputStream inputStream) throws IOException {
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
private Path resolveInputFile(String sourceObjectKey) {
String normalized = sourceObjectKey.startsWith("/") ? sourceObjectKey.substring(1) : sourceObjectKey;
return Path.of(properties.getFfmpeg().getInputDirectory()).resolve(normalized);
@ -136,4 +143,12 @@ public class FfmpegVideoClipperAdapter implements VideoClipperPort {
}
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.input-directory=./tmp/ffmpeg-input
video-clipping.ffmpeg.output-directory=./tmp/ffmpeg-output
video-clipping.ffmpeg.cleanup-local-files=true