fix edge case where first segment is longer than offset
This commit is contained in:
@@ -133,7 +133,7 @@ public class MediaService {
|
||||
.orElseThrow(() -> new NotAuthenticated("User is not authenticated"));
|
||||
|
||||
Job job = jobRegistryService.generateJob();
|
||||
SegmentContext ctx = resolveSegments(user, startTime, endTime);
|
||||
StreamActionsService.SegmentSelection ctx = resolveSegments(user, startTime, endTime);
|
||||
float duration = (endTime.toEpochMilli() - startTime.toEpochMilli()) / 1000f;
|
||||
Path outputFile = directoryService.getVodsDir(user.getId()).resolve(UUID.randomUUID() + ".mp4");
|
||||
|
||||
@@ -211,7 +211,7 @@ public class MediaService {
|
||||
.orElseThrow(() -> new NotAuthenticated("User is not authenticated"));
|
||||
|
||||
Job job = jobRegistryService.generateJob();
|
||||
SegmentContext ctx = resolveSegments(user, startTime, endTime);
|
||||
StreamActionsService.SegmentSelection ctx = resolveSegments(user, startTime, endTime);
|
||||
Path outputFile = directoryService.getTempOutputDir().resolve(UUID.randomUUID() + ".mp4");
|
||||
|
||||
return dispatchSaveJob(job, ctx, duration, outputFile, () ->
|
||||
@@ -225,20 +225,12 @@ public class MediaService {
|
||||
);
|
||||
}
|
||||
|
||||
private record SegmentContext(List<Path> segments, float trimOffset) {}
|
||||
|
||||
private SegmentContext resolveSegments(User user, Instant startTime, Instant endTime) throws IOException {
|
||||
private StreamActionsService.SegmentSelection resolveSegments(User user, Instant startTime, Instant endTime) throws IOException {
|
||||
Path streamDir = directoryService.getStreamDir(user.getStreamKey());
|
||||
List<Path> segments = streamActionsService.getSegmentsInRange(streamDir, startTime, endTime);
|
||||
if (segments.isEmpty()) {
|
||||
throw new IllegalArgumentException("No stream segments found in the given time range");
|
||||
}
|
||||
long firstMs = streamActionsService.parseTimestampMs(segments.getFirst());
|
||||
float trimOffset = Math.max(0f, (startTime.toEpochMilli() - firstMs) / 1000f);
|
||||
return new SegmentContext(segments, trimOffset);
|
||||
return streamActionsService.selectSegments(streamDir, startTime, endTime);
|
||||
}
|
||||
|
||||
private Job dispatchSaveJob(Job job, SegmentContext ctx, float duration, Path outputFile, Runnable onSuccess) throws IOException {
|
||||
private Job dispatchSaveJob(Job job, StreamActionsService.SegmentSelection ctx, float duration, Path outputFile, Runnable onSuccess) {
|
||||
job.setState(JobState.PROCESSING);
|
||||
streamActionsService.saveSection(ctx.segments(), ctx.trimOffset(), duration, outputFile, job.getProgressTracker())
|
||||
.thenRun(() -> {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user