fix edge case where first segment is longer than offset
All checks were successful
CI / Maven unit tests (push) Successful in 15s
CI / Build and push images (push) Has been skipped
CI / Deploy to server (push) Has been skipped

This commit is contained in:
2026-07-26 22:19:08 +01:00
parent 41a580a531
commit b4e450a122
3 changed files with 139 additions and 17 deletions

View File

@@ -1,5 +1,6 @@
package com.ddf.vodsystem.services.media;
import com.ddf.vodsystem.dto.ClipOptions;
import com.ddf.vodsystem.dto.CommandOutput;
import com.ddf.vodsystem.dto.ProgressTracker;
import org.junit.jupiter.api.BeforeEach;
@@ -17,6 +18,7 @@ import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.time.Instant;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static org.assertj.core.api.Assertions.assertThat;
@@ -31,6 +33,9 @@ class StreamActionsServiceTest {
@Mock
private CommandRunner commandRunner;
@Mock
private MetadataService metadataService;
private StreamActionsService service;
@TempDir
@@ -38,13 +43,20 @@ class StreamActionsServiceTest {
@BeforeEach
void setUp() {
service = new StreamActionsService(commandRunner);
service = new StreamActionsService(commandRunner, metadataService);
}
private void createSegment(String name) throws IOException {
Files.createFile(tempDir.resolve(name));
}
/** Stubs the (async) metadata probe so {@code segments.getFirst()} reports {@code seconds}. */
private void stubHeadDuration(float seconds) {
ClipOptions meta = new ClipOptions();
meta.setDuration(seconds);
when(metadataService.getVideoMetadata(any())).thenReturn(CompletableFuture.completedFuture(meta));
}
/**
* Stubs the two-arg {@link CommandRunner#run} so the head re-encode pass
* actually writes a non-empty file at its output path — {@code encodeHead}
@@ -56,7 +68,7 @@ class StreamActionsServiceTest {
when(commandRunner.run(anyList(), any())).thenAnswer(inv -> {
List<String> cmd = inv.getArgument(0);
if (cmd.contains("mpegts")) {
Files.writeString(Path.of(cmd.get(cmd.size() - 1)), "x");
Files.writeString(Path.of(cmd.getLast()), "x");
}
return output;
});
@@ -240,6 +252,67 @@ class StreamActionsServiceTest {
.containsExactly("30.ts", "100.ts", "110.ts");
}
// ---------------------------------------------------------------
// selectSegments: head selection and trim-offset
// ---------------------------------------------------------------
@Test
void selectSegments_startWithinFirstSegment_keepsItAndComputesOffset() throws IOException {
createSegment("30.ts");
createSegment("33.ts");
stubHeadDuration(3f); // 30.ts spans [30s, 33s)
var selection = service.selectSegments(tempDir, Instant.ofEpochMilli(31_500), Instant.ofEpochMilli(40_000));
assertThat(selection.segments()).extracting(p -> p.getFileName().toString())
.containsExactly("30.ts", "33.ts");
assertThat(selection.trimOffset()).isEqualTo(1.5f);
}
@Test
void selectSegments_startInGapAfterFirstSegment_dropsItAndStartsAtNext() throws IOException {
createSegment("30.ts");
// user stops streaming; a gap follows 30.ts
createSegment("100.ts");
createSegment("110.ts");
stubHeadDuration(3f); // 30.ts only holds [30s, 33s); requested start 70s is past it
var selection = service.selectSegments(tempDir, Instant.ofEpochMilli(70_000), Instant.ofEpochMilli(111_000));
assertThat(selection.segments()).extracting(p -> p.getFileName().toString())
.containsExactly("100.ts", "110.ts");
assertThat(selection.trimOffset()).isZero();
}
@Test
void selectSegments_startPastOnlySegment_throwsIllegalArgument() throws IOException {
createSegment("0.ts");
stubHeadDuration(3f); // 0.ts holds [0s, 3s); requested start 50s has no footage
assertThatThrownBy(() ->
service.selectSegments(tempDir, Instant.ofEpochMilli(50_000), Instant.ofEpochMilli(60_000)))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void selectSegments_noSegmentsInRange_throwsIllegalArgument() {
assertThatThrownBy(() ->
service.selectSegments(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(10_000)))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
void selectSegments_probeFailure_surfacesAsIOException() throws IOException {
createSegment("30.ts");
createSegment("100.ts");
when(metadataService.getVideoMetadata(any()))
.thenReturn(CompletableFuture.failedFuture(new IOException("ffprobe boom")));
assertThatThrownBy(() ->
service.selectSegments(tempDir, Instant.ofEpochMilli(70_000), Instant.ofEpochMilli(111_000)))
.isInstanceOf(IOException.class);
}
// ---------------------------------------------------------------
// parseTimestampMs: unit-based conversion
// ---------------------------------------------------------------
@@ -334,7 +407,7 @@ class StreamActionsServiceTest {
}
@Test
void saveSection_emptySegmentList_completesExceptionallyWithIllegalArgumentException() throws IOException {
void saveSection_emptySegmentList_completesExceptionallyWithIllegalArgumentException() {
Path output = Path.of("/tmp/out/clip.mp4");
ProgressTracker progress = new ProgressTracker();