fix edge case where first segment is longer than offset
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user