From 99209654e385415485c1008f6f93b7f0a2edca5a Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Sun, 26 Jul 2026 14:36:57 +0100 Subject: [PATCH] re-encode first segment --- .../services/media/StreamActionsService.java | 71 ++++++++++++++----- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/ddf/vodsystem/services/media/StreamActionsService.java b/src/main/java/com/ddf/vodsystem/services/media/StreamActionsService.java index 2c81c6a..aeb8339 100644 --- a/src/main/java/com/ddf/vodsystem/services/media/StreamActionsService.java +++ b/src/main/java/com/ddf/vodsystem/services/media/StreamActionsService.java @@ -64,32 +64,31 @@ public class StreamActionsService { float duration, Path outputFile, ProgressTracker progressTracker - ) throws IOException { + ) { + if (segments.isEmpty()) { return CompletableFuture.failedFuture(new IllegalArgumentException("Segment list cannot be empty")); } - Path firstSegment = segments.getFirst(); - Path trimmedFirst = Files.createTempFile("trimmed-first-", ".ts"); - try { - // First segment must be trimmed manually due to keyframe issues with the concat protocol - List trimCommand = List.of( - "ffmpeg", "-y", - "-ss", String.valueOf(trimOffset), - "-i", firstSegment.toAbsolutePath().toString(), - "-c", "copy", - trimmedFirst.toAbsolutePath().toString() - ); - commandRunner.run(trimCommand); + if (trimOffset < 0 || duration <= 0) { + return CompletableFuture.failedFuture(new IllegalArgumentException( + "trimOffset must be >= 0 and duration must be > 0 (got " + trimOffset + ", " + duration + ")")); + } - String concatInput = Stream.concat(Stream.of(trimmedFirst), segments.stream().skip(1)) + Path headSegment = null; + try { + // Unique per invocation so concurrent saves by different users never + // collide on the same head fragment; cleaned up in the finally block. + headSegment = Files.createTempFile("reencode-section-", ".ts"); + encodeHead(segments.getFirst(), trimOffset, headSegment); + + String concatInput = Stream.concat(Stream.of(headSegment), segments.stream().skip(1)) .map(p -> p.toAbsolutePath().toString()) .collect(Collectors.joining("|")); List command = List.of( "ffmpeg", "-y", - "-ss", String.valueOf(trimOffset), "-i", "concat:" + concatInput, "-t", String.valueOf(duration), "-c", "copy", @@ -113,7 +112,13 @@ public class StreamActionsService { logger.error("Failed to save section to '{}': {}", outputFile, e.toString()); return CompletableFuture.failedFuture(e); } finally { - Files.deleteIfExists(trimmedFirst); + if (headSegment != null) { + try { + Files.deleteIfExists(headSegment); + } catch (IOException e) { + logger.warn("Failed to delete temp head fragment '{}': {}", headSegment, e.toString()); + } + } } } @@ -162,4 +167,38 @@ public class StreamActionsService { // nginx hls_fragment_naming system uses seconds; convert to ms return value < 1_000_000_000_000L ? value * 1000L : value; } + + /** + * Re-encodes {@code firstSegment} from {@code trimOffset} to its end, + * writing an MPEG-TS fragment that can be stream-copy concatenated with + * the untouched fragments that follow it. + */ + private void encodeHead(Path firstSegment, float trimOffset, Path head) + throws IOException, InterruptedException { + + List command = List.of( + "ffmpeg", + "-y", + "-i", firstSegment.toAbsolutePath().toString(), + "-ss", String.valueOf(trimOffset), + "-c:v", "libx264", + "-preset", "veryfast", + "-crf", "20", + "-pix_fmt", "yuv420p", + "-c:a", "aac", + "-b:a", "160k", + "-muxdelay", "0", + "-muxpreload", "0", + "-f", "mpegts", + head.toAbsolutePath().toString() + ); + + logger.debug("Re-encoding head fragment '{}' from {}s", firstSegment, trimOffset); + commandRunner.run(command, line -> { /* head pass is short; not worth reporting */ }); + + if (!Files.exists(head) || Files.size(head) == 0) { + throw new IOException("Head re-encode produced no output — trimOffset " + trimOffset + + "s may exceed the duration of " + firstSegment); + } + } }