fix edge case where first segment is longer than offset

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

View File

@@ -15,6 +15,7 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -31,9 +32,11 @@ import java.util.stream.Stream;
public class StreamActionsService {
private static final Logger logger = LoggerFactory.getLogger(StreamActionsService.class);
private final CommandRunner commandRunner;
private final MetadataService metadataService;
public StreamActionsService(CommandRunner commandRunner) {
public StreamActionsService(CommandRunner commandRunner, MetadataService metadataService) {
this.commandRunner = commandRunner;
this.metadataService = metadataService;
}
/**
@@ -141,6 +144,7 @@ public class StreamActionsService {
for (int i = 0; i < streamFiles.size(); i++) {
Path streamFile = streamFiles.get(i);
long segmentStart = parseTimestampMs(streamFile);
long segmentEnd = (i + 1 < streamFiles.size())
? parseTimestampMs(streamFiles.get(i + 1))
@@ -155,6 +159,59 @@ public class StreamActionsService {
return results;
}
/**
* The segments backing a save and the offset into the first one at which
* the requested section begins.
*
* @param segments the ordered segments to concatenate, in playback order
* @param trimOffset the offset, in seconds, into {@code segments.getFirst()}
* at which the requested section starts
*/
public record SegmentSelection(List<Path> segments, float trimOffset) {}
/**
* Selects the recorded {@code .ts} segments overlapping {@code [startTime, endTime)}
* and computes the trim offset into the first segment.
* <p>
* If {@code startTime} falls in a stream gap after the earliest overlapping segment
* (so that segment holds no footage for the requested start), the segment is dropped
* and the selection begins at the first genuinely-recorded footage instead.
*
* @throws IllegalArgumentException if no segments hold footage for the requested range
* @throws IOException if listing the stream directory or probing a segment fails
*/
public SegmentSelection selectSegments(Path streamDirectory, Instant startTime, Instant endTime) throws IOException {
List<Path> segments = getSegmentsInRange(streamDirectory, startTime, endTime);
if (segments.isEmpty()) {
throw new IllegalArgumentException("No stream segments found in the given time range");
}
long firstMs = parseTimestampMs(segments.getFirst());
float trimOffset = Math.max(0f, (startTime.toEpochMilli() - firstMs) / 1000f);
// if the first segment is longer than the trim-offset, then ggs
if (trimOffset > 0 && probeDurationSeconds(segments.getFirst()) < trimOffset) {
segments.removeFirst();
if (segments.isEmpty()) {
throw new IllegalArgumentException("No recorded footage at the requested start time");
}
trimOffset = 0f;
}
return new SegmentSelection(segments, trimOffset);
}
private float probeDurationSeconds(Path segment) throws IOException {
try {
return metadataService.getVideoMetadata(segment).get().getDuration();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while probing segment duration: " + segment, e);
} catch (ExecutionException e) {
throw new IOException("Failed to probe segment duration: " + segment, e);
}
}
public long parseTimestampMs(Path path) {
String fullName = path.getFileName().toString();