cleaned up StreamActions interface
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-27 23:00:37 +01:00
parent e19bc68006
commit df8afcb428
3 changed files with 215 additions and 130 deletions

View File

@@ -247,6 +247,8 @@ class StreamActionsServiceTest {
assertThat(selection.segments()).extracting(p -> p.getFileName().toString())
.containsExactly("30.ts", "33.ts");
assertThat(selection.trimOffset()).isEqualTo(1.5f);
// effective start equals the requested 31.5s, so duration runs to endTime: 40 - 31.5
assertThat(selection.duration()).isEqualTo(8.5f);
}
@Test
@@ -262,6 +264,10 @@ class StreamActionsServiceTest {
assertThat(selection.segments()).extracting(p -> p.getFileName().toString())
.containsExactly("100.ts", "110.ts");
assertThat(selection.trimOffset()).isZero();
// the dropped head shifts the effective start to 100s, so the duration
// shrinks to end at endTime (111 - 100 = 11s) instead of the 41s the
// original 70s->111s window would have implied
assertThat(selection.duration()).isEqualTo(11f);
}
@Test
@@ -351,13 +357,15 @@ class StreamActionsServiceTest {
stubRunnerProducingHead(commandOutput);
var future = service.saveSection(List.of(segmentA, segmentB), 5.0f, 12.0f, output, progress);
var future = service.saveSection(
new StreamActionsService.SegmentSelection(List.of(segmentA, segmentB), 5.0f, 12.0f), output, progress);
assertThat(future.get()).isSameAs(commandOutput);
assertThat(progress.isComplete()).isTrue();
// saveSection issues two ffmpeg passes: the head re-encode (trims the
// first segment via -ss) followed by the stream-copy concat (-t/-c copy).
// With a non-zero trim offset saveSection issues two ffmpeg passes: the
// head re-encode (trims the first segment via -ss) followed by the
// concat-demuxer stream copy (-f concat / -t / -c copy).
@SuppressWarnings("unchecked")
ArgumentCaptor<List<String>> captor = ArgumentCaptor.forClass(List.class);
Mockito.verify(commandRunner, Mockito.times(2)).run(captor.capture(), any());
@@ -365,20 +373,50 @@ class StreamActionsServiceTest {
List<String> concatCommand = captor.getAllValues().get(1);
assertThat(headCommand).containsSequence("-ss", "5.0");
assertThat(concatCommand).containsSequence("-f", "concat");
assertThat(concatCommand).containsSequence("-t", "12.0");
assertThat(concatCommand).containsSequence("-c", "copy");
assertThat(concatCommand).endsWith(output.toAbsolutePath().toString());
}
@Test
void saveSection_zeroTrimOffset_skipsHeadReencodeAndRunsSingleConcatPass() throws Exception {
Path output = Path.of("/tmp/out/clip.mp4");
ProgressTracker progress = new ProgressTracker();
CommandOutput commandOutput = new CommandOutput();
when(commandRunner.run(anyList(), any())).thenReturn(commandOutput);
var future = service.saveSection(
new StreamActionsService.SegmentSelection(
List.of(Path.of("/tmp/a.ts"), Path.of("/tmp/b.ts")), 0f, 10f), output, progress);
assertThat(future.get()).isSameAs(commandOutput);
assertThat(progress.isComplete()).isTrue();
// trimOffset 0 means the first segment already starts at the requested point:
// no head re-encode pass, just a single concat-demuxer copy (no -ss).
@SuppressWarnings("unchecked")
ArgumentCaptor<List<String>> captor = ArgumentCaptor.forClass(List.class);
Mockito.verify(commandRunner, Mockito.times(1)).run(captor.capture(), any());
List<String> command = captor.getValue();
assertThat(command).containsSequence("-f", "concat");
assertThat(command).containsSequence("-t", "10.0");
assertThat(command).doesNotContain("-ss");
}
@Test
void saveSection_commandRunnerThrowsIOException_completesExceptionallyWithoutMarkingProgressComplete() throws Exception {
Path output = Path.of("/tmp/out/clip.mp4");
ProgressTracker progress = new ProgressTracker();
// The head re-encode is the first ffmpeg pass; its failure must surface.
// With a non-zero trim offset the head re-encode is the first ffmpeg pass;
// its failure must surface.
when(commandRunner.run(anyList(), any())).thenThrow(new IOException("ffmpeg boom"));
var future = service.saveSection(List.of(Path.of("/tmp/a.ts")), 0f, 10f, output, progress);
var future = service.saveSection(
new StreamActionsService.SegmentSelection(List.of(Path.of("/tmp/a.ts")), 2f, 10f), output, progress);
assertThatThrownBy(future::get)
.isInstanceOf(ExecutionException.class)
@@ -391,7 +429,8 @@ class StreamActionsServiceTest {
Path output = Path.of("/tmp/out/clip.mp4");
ProgressTracker progress = new ProgressTracker();
var future = service.saveSection(List.of(), 0f, 10f, output, progress);
var future = service.saveSection(
new StreamActionsService.SegmentSelection(List.of(), 0f, 10f), output, progress);
assertThatThrownBy(future::get)
.isInstanceOf(ExecutionException.class)
@@ -400,13 +439,13 @@ class StreamActionsServiceTest {
}
@Test
void saveSection_nullOutputFile_completesExceptionally() throws Exception {
void saveSection_nullOutputFile_completesExceptionally() {
ProgressTracker progress = new ProgressTracker();
// Let the head pass succeed so execution reaches the null output file.
stubRunnerProducingHead(new CommandOutput());
var future = service.saveSection(List.of(Path.of("/tmp/a.ts")), 0f, 10f, null, progress);
// trimOffset 0 skips the head pass; building the concat command dereferences
// the null output path, so no ffmpeg pass ever runs.
var future = service.saveSection(
new StreamActionsService.SegmentSelection(List.of(Path.of("/tmp/a.ts")), 0f, 10f), null, progress);
assertThatThrownBy(future::get)
.isInstanceOf(ExecutionException.class)