forked from jsl/video_editing_poc
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 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) + "...";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue