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:
parent
abf1259397
commit
8f4804c2ab
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) + "...";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue