first segment's duration is probed to exclude it if needed
This commit is contained in:
@@ -125,7 +125,7 @@ public class StreamActionsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Path> getSegmentsInRange(Path streamDirectory, Instant startTime, Instant endTime) throws IOException {
|
private List<Path> getSegmentsInRange(Path streamDirectory, Instant startTime, Instant endTime) throws IOException {
|
||||||
long startMs = startTime.toEpochMilli();
|
long startMs = startTime.toEpochMilli();
|
||||||
long endMs = endTime.toEpochMilli();
|
long endMs = endTime.toEpochMilli();
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ public class StreamActionsService {
|
|||||||
long firstMs = parseTimestampMs(segments.getFirst());
|
long firstMs = parseTimestampMs(segments.getFirst());
|
||||||
float trimOffset = Math.max(0f, (startTime.toEpochMilli() - firstMs) / 1000f);
|
float trimOffset = Math.max(0f, (startTime.toEpochMilli() - firstMs) / 1000f);
|
||||||
|
|
||||||
// if the first segment is longer than the trim-offset, then ggs
|
// if the first segment is longer than the trim-offset
|
||||||
if (trimOffset > 0 && probeDurationSeconds(segments.getFirst()) < trimOffset) {
|
if (trimOffset > 0 && probeDurationSeconds(segments.getFirst()) < trimOffset) {
|
||||||
segments.removeFirst();
|
segments.removeFirst();
|
||||||
if (segments.isEmpty()) {
|
if (segments.isEmpty()) {
|
||||||
|
|||||||
@@ -75,137 +75,131 @@ class StreamActionsServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// getSegmentsInRange: missing / invalid directories
|
// selectSegments: missing / invalid directories
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSegmentsInRange_directoryDoesNotExist_throwsIOException() {
|
void selectSegments_directoryDoesNotExist_throwsIOException() {
|
||||||
Path missing = tempDir.resolve("does-not-exist");
|
Path missing = tempDir.resolve("does-not-exist");
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.getSegmentsInRange(missing, Instant.EPOCH, Instant.now()))
|
assertThatThrownBy(() -> service.selectSegments(missing, Instant.EPOCH, Instant.now()))
|
||||||
.isInstanceOf(NoSuchFileException.class);
|
.isInstanceOf(NoSuchFileException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSegmentsInRange_pathIsAFileNotADirectory_throwsIOException() throws IOException {
|
void selectSegments_pathIsAFileNotADirectory_throwsIOException() throws IOException {
|
||||||
Path file = tempDir.resolve("not-a-directory.txt");
|
Path file = tempDir.resolve("not-a-directory.txt");
|
||||||
Files.createFile(file);
|
Files.createFile(file);
|
||||||
|
|
||||||
assertThatThrownBy(() -> service.getSegmentsInRange(file, Instant.EPOCH, Instant.now()))
|
assertThatThrownBy(() -> service.selectSegments(file, Instant.EPOCH, Instant.now()))
|
||||||
.isInstanceOf(IOException.class);
|
.isInstanceOf(IOException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void getSegmentsInRange_emptyDirectory_returnsEmptyList() throws IOException {
|
|
||||||
List<Path> result = service.getSegmentsInRange(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(100_000));
|
|
||||||
|
|
||||||
assertThat(result).isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// getSegmentsInRange: filename filtering
|
// selectSegments: filename filtering
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSegmentsInRange_ignoresFilesNotEndingInTs() throws IOException {
|
void selectSegments_ignoresFilesNotEndingInTs() throws IOException {
|
||||||
createSegment("1.ts");
|
createSegment("1.ts");
|
||||||
createSegment("playlist.m3u8");
|
createSegment("playlist.m3u8");
|
||||||
createSegment("1.ts.tmp");
|
createSegment("1.ts.tmp");
|
||||||
createSegment("readme.txt");
|
createSegment("readme.txt");
|
||||||
|
|
||||||
List<Path> result = service.getSegmentsInRange(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(5000));
|
var result = service.selectSegments(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(5000)).segments();
|
||||||
|
|
||||||
assertThat(result).extracting(p -> p.getFileName().toString()).containsExactly("1.ts");
|
assertThat(result).extracting(p -> p.getFileName().toString()).containsExactly("1.ts");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSegmentsInRange_directoryNamedLikeASegment_isNotIncluded() throws IOException {
|
void selectSegments_directoryNamedLikeASegment_isNotIncluded() throws IOException {
|
||||||
Files.createDirectory(tempDir.resolve("5.ts"));
|
Files.createDirectory(tempDir.resolve("5.ts"));
|
||||||
|
|
||||||
List<Path> result = service.getSegmentsInRange(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(10_000));
|
// the directory is the only ".ts" entry; excluding it leaves nothing to select
|
||||||
|
assertThatThrownBy(() ->
|
||||||
assertThat(result).isEmpty();
|
service.selectSegments(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(10_000)))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSegmentsInRange_malformedNonNumericFilename_throwsNumberFormatException() throws IOException {
|
void selectSegments_malformedNonNumericFilename_throwsNumberFormatException() throws IOException {
|
||||||
// A real HLS fragment name is always numeric. A corrupted or partially-written
|
// A real HLS fragment name is always numeric. A corrupted or partially-written
|
||||||
// file (e.g. "index.ts") crashes the whole listing instead of just being skipped.
|
// file (e.g. "index.ts") crashes the whole listing instead of just being skipped.
|
||||||
createSegment("index.ts");
|
createSegment("index.ts");
|
||||||
|
|
||||||
assertThatThrownBy(() ->
|
assertThatThrownBy(() ->
|
||||||
service.getSegmentsInRange(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(10_000)))
|
service.selectSegments(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(10_000)))
|
||||||
.isInstanceOf(NumberFormatException.class);
|
.isInstanceOf(NumberFormatException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSegmentsInRange_emptyFilenameBeforeExtension_throwsNumberFormatException() throws IOException {
|
void selectSegments_emptyFilenameBeforeExtension_throwsNumberFormatException() throws IOException {
|
||||||
createSegment(".ts");
|
createSegment(".ts");
|
||||||
|
|
||||||
assertThatThrownBy(() ->
|
assertThatThrownBy(() ->
|
||||||
service.getSegmentsInRange(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(10_000)))
|
service.selectSegments(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(10_000)))
|
||||||
.isInstanceOf(NumberFormatException.class);
|
.isInstanceOf(NumberFormatException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// getSegmentsInRange: range-boundary arithmetic
|
// selectSegments: range-boundary arithmetic
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSegmentsInRange_segmentEndsExactlyAtRangeStart_isExcluded() throws IOException {
|
void selectSegments_segmentEndsExactlyAtRangeStart_isExcluded() throws IOException {
|
||||||
createSegment("0.ts");
|
createSegment("0.ts");
|
||||||
createSegment("3.ts");
|
createSegment("3.ts");
|
||||||
|
|
||||||
List<Path> result = service.getSegmentsInRange(tempDir, Instant.ofEpochMilli(3000), Instant.ofEpochMilli(10_000));
|
var result = service.selectSegments(tempDir, Instant.ofEpochMilli(3000), Instant.ofEpochMilli(10_000)).segments();
|
||||||
|
|
||||||
assertThat(result).extracting(p -> p.getFileName().toString())
|
assertThat(result).extracting(p -> p.getFileName().toString())
|
||||||
.containsExactly("3.ts");
|
.containsExactly("3.ts");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSegmentsInRange_segmentOverlapsRangeStartByOneMs_isIncluded() throws IOException {
|
void selectSegments_segmentOverlapsRangeStartByOneMs_isIncluded() throws IOException {
|
||||||
createSegment("0.ts"); // spans [0, 3000)
|
createSegment("0.ts"); // spans [0, 3000)
|
||||||
|
stubHeadDuration(3f); // start 2999ms trims into 0.ts, so its duration is probed
|
||||||
|
|
||||||
List<Path> result = service.getSegmentsInRange(tempDir, Instant.ofEpochMilli(2999), Instant.ofEpochMilli(10_000));
|
var result = service.selectSegments(tempDir, Instant.ofEpochMilli(2999), Instant.ofEpochMilli(10_000)).segments();
|
||||||
|
|
||||||
assertThat(result).hasSize(1);
|
assertThat(result).hasSize(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSegmentsInRange_segmentStartsExactlyAtRangeEnd_isExcluded() throws IOException {
|
void selectSegments_segmentStartsExactlyAtRangeEnd_isExcluded() throws IOException {
|
||||||
createSegment("5.ts"); // starts at 5000ms
|
createSegment("5.ts"); // starts at 5000ms
|
||||||
|
|
||||||
List<Path> result = service.getSegmentsInRange(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(5000));
|
assertThatThrownBy(() ->
|
||||||
|
service.selectSegments(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(5000)))
|
||||||
assertThat(result).isEmpty();
|
.isInstanceOf(IllegalArgumentException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSegmentsInRange_segmentStartsOneMsBeforeRangeEnd_isIncluded() throws IOException {
|
void selectSegments_segmentStartsOneMsBeforeRangeEnd_isIncluded() throws IOException {
|
||||||
createSegment("5.ts"); // starts at 5000ms
|
createSegment("5.ts"); // starts at 5000ms
|
||||||
|
|
||||||
List<Path> result = service.getSegmentsInRange(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(5001));
|
var result = service.selectSegments(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(5001)).segments();
|
||||||
|
|
||||||
assertThat(result).hasSize(1);
|
assertThat(result).hasSize(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSegmentsInRange_startAfterEnd_throwsIllegalArgument() throws IOException {
|
void selectSegments_startAfterEnd_throwsIllegalArgument() throws IOException {
|
||||||
createSegment("10.ts");
|
createSegment("10.ts");
|
||||||
|
|
||||||
assertThatThrownBy(() ->
|
assertThatThrownBy(() ->
|
||||||
service.getSegmentsInRange(tempDir, Instant.ofEpochMilli(20_000), Instant.ofEpochMilli(10_000)))
|
service.selectSegments(tempDir, Instant.ofEpochMilli(20_000), Instant.ofEpochMilli(10_000)))
|
||||||
.isInstanceOf(IllegalArgumentException.class);
|
.isInstanceOf(IllegalArgumentException.class);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSegmentsInRange_segmentsOutOfOrder_returnedInOrder() throws IOException {
|
void selectSegments_segmentsOutOfOrder_returnedInOrder() throws IOException {
|
||||||
createSegment("20.ts");
|
createSegment("20.ts");
|
||||||
createSegment("10.ts");
|
createSegment("10.ts");
|
||||||
createSegment("0.ts");
|
createSegment("0.ts");
|
||||||
|
|
||||||
List<Path> result = service.getSegmentsInRange(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(25_000));
|
var result = service.selectSegments(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(25_000)).segments();
|
||||||
|
|
||||||
assertThat(result)
|
assertThat(result)
|
||||||
.extracting(p -> p.getFileName().toString())
|
.extracting(p -> p.getFileName().toString())
|
||||||
@@ -213,45 +207,31 @@ class StreamActionsServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSegmentsInRange_mixOfOverlappingAndNonOverlappingSegments_returnsOnlyOverlapping() throws IOException {
|
void selectSegments_mixOfOverlappingAndNonOverlappingSegments_returnsOnlyOverlapping() throws IOException {
|
||||||
createSegment("0.ts"); // [0, 3000)
|
createSegment("0.ts"); // [0, 3000)
|
||||||
createSegment("3.ts"); // [3000, 6000)
|
createSegment("3.ts"); // [3000, 6000)
|
||||||
createSegment("6.ts"); // [6000, 9000)
|
createSegment("6.ts"); // [6000, 9000)
|
||||||
createSegment("9.ts"); // [9000, 12000)
|
createSegment("9.ts"); // [9000, 12000)
|
||||||
|
stubHeadDuration(3f); // start 4000ms trims into 3.ts, so its duration is probed
|
||||||
|
|
||||||
|
var result = service.selectSegments(tempDir, Instant.ofEpochMilli(4000), Instant.ofEpochMilli(8000)).segments();
|
||||||
List<Path> result = service.getSegmentsInRange(tempDir, Instant.ofEpochMilli(4000), Instant.ofEpochMilli(8000));
|
|
||||||
|
|
||||||
assertThat(result).extracting(p -> p.getFileName().toString())
|
assertThat(result).extracting(p -> p.getFileName().toString())
|
||||||
.containsExactly("3.ts", "6.ts");
|
.containsExactly("3.ts", "6.ts");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getSegmentsInRange_resultsAreSortedByTimestampRegardlessOfDirectoryOrder() throws IOException {
|
void selectSegments_resultsAreSortedByTimestampRegardlessOfDirectoryOrder() throws IOException {
|
||||||
createSegment("30.ts");
|
createSegment("30.ts");
|
||||||
createSegment("10.ts");
|
createSegment("10.ts");
|
||||||
createSegment("20.ts");
|
createSegment("20.ts");
|
||||||
|
|
||||||
List<Path> result = service.getSegmentsInRange(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(100_000));
|
var result = service.selectSegments(tempDir, Instant.ofEpochMilli(0), Instant.ofEpochMilli(100_000)).segments();
|
||||||
|
|
||||||
assertThat(result).extracting(p -> p.getFileName().toString())
|
assertThat(result).extracting(p -> p.getFileName().toString())
|
||||||
.containsExactly("10.ts", "20.ts", "30.ts");
|
.containsExactly("10.ts", "20.ts", "30.ts");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void getSegmentsInRange_gapInStream_returnsTheGap() throws IOException {
|
|
||||||
createSegment("20.ts");
|
|
||||||
createSegment("30.ts");
|
|
||||||
// user stops streaming
|
|
||||||
createSegment("100.ts");
|
|
||||||
createSegment("110.ts");
|
|
||||||
|
|
||||||
List<Path> result = service.getSegmentsInRange(tempDir, Instant.ofEpochMilli(70_000), Instant.ofEpochMilli(111_000));
|
|
||||||
|
|
||||||
assertThat(result).extracting(p -> p.getFileName().toString())
|
|
||||||
.containsExactly("30.ts", "100.ts", "110.ts");
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// selectSegments: head selection and trim-offset
|
// selectSegments: head selection and trim-offset
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user