From b55ba3ce51c85ddaefac6c309b4270f84ec8a85e Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Fri, 19 Jun 2026 22:04:42 +0100 Subject: [PATCH] feat(api): add error handling for save endpoint --- .../controllers/StreamController.java | 2 +- .../ddf/vodsystem/services/StreamService.java | 48 +++++++++++++++++-- .../services/media/StreamActionsService.java | 45 ++--------------- 3 files changed, 50 insertions(+), 45 deletions(-) diff --git a/src/main/java/com/ddf/vodsystem/controllers/StreamController.java b/src/main/java/com/ddf/vodsystem/controllers/StreamController.java index 74cb465..c0b33c9 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/StreamController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/StreamController.java @@ -50,7 +50,7 @@ public class StreamController { public ResponseEntity> saveSection( @RequestBody SaveSectionRequest request) throws IOException, InterruptedException { streamService.saveSection(request.startTime(), request.endTime()); - return ResponseEntity.ok(new APIResponse<>("success", "Section saved", null)); + return ResponseEntity.accepted().body(new APIResponse<>("success", "Section saved", null)); } @GetMapping("/history/{userId}") diff --git a/src/main/java/com/ddf/vodsystem/services/StreamService.java b/src/main/java/com/ddf/vodsystem/services/StreamService.java index 6e67c6b..f56a65f 100644 --- a/src/main/java/com/ddf/vodsystem/services/StreamService.java +++ b/src/main/java/com/ddf/vodsystem/services/StreamService.java @@ -3,6 +3,7 @@ package com.ddf.vodsystem.services; import com.ddf.vodsystem.entities.Stream; import com.ddf.vodsystem.entities.User; import com.ddf.vodsystem.exceptions.AlreadyStreaming; +import com.ddf.vodsystem.exceptions.FFMPEGException; import com.ddf.vodsystem.exceptions.KeyNotFound; import com.ddf.vodsystem.exceptions.NotAuthenticated; import com.ddf.vodsystem.repositories.StreamRepository; @@ -18,9 +19,11 @@ import java.io.File; import java.io.IOException; import java.time.Instant; import java.time.LocalDateTime; -import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.Comparator; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; @Service public class StreamService { @@ -91,14 +94,27 @@ public class StreamService { } public void saveSection(Instant startTime, Instant endTime) throws IOException, InterruptedException { + if (startTime.isAfter(endTime)) { + throw new IllegalArgumentException("Start time must be before end time"); + } + User user = userService.getLoggedInUser() .orElseThrow(() -> new NotAuthenticated("User is not authenticated")); String streamDirectory = streamDataPath + File.separator + user.getStreamKey(); - streamActionsService.saveSection(streamDirectory, startTime, endTime) + List fileSegments = getSegmentsInRange(streamDirectory, startTime, endTime); + + if (fileSegments.isEmpty()) { + throw new IllegalArgumentException("No stream segments found in the given time range"); + } + + long firstSegmentMs = parseTimestampMs(fileSegments.getFirst()); + float trimOffset = Math.max(0f, (startTime.toEpochMilli() - firstSegmentMs) / 1000f); + float duration = (endTime.toEpochMilli() - startTime.toEpochMilli()) / 1000f; + + streamActionsService.saveSection(fileSegments, trimOffset, duration) .exceptionally(ex -> { - logger.error("saveSection failed for user {}: {}", user.getUsername(), ex.getMessage(), ex); - return null; + throw new FFMPEGException("Saving stream section failed due to FFMPEG Error"); }); } @@ -121,4 +137,28 @@ public class StreamService { return userService.getUserByStreamKey(streamKey) .orElseThrow(() -> new KeyNotFound("Stream key not found: " + streamKey)); } + + private List getSegmentsInRange(String streamDirectory, Instant startTime, Instant endTime) { + File dir = new File(streamDirectory); + File[] tsFiles = dir.listFiles((d, name) -> name.endsWith(".ts")); + if (tsFiles == null) return List.of(); + + long startMs = startTime.toEpochMilli(); + long endMs = endTime.toEpochMilli(); + + return Arrays.stream(tsFiles) + .filter(f -> { + long segMs = parseTimestampMs(f); + return segMs < endMs && segMs + 3_000 > startMs; + }) + .sorted(Comparator.comparingLong(this::parseTimestampMs)) + .collect(Collectors.toList()); + } + + private long parseTimestampMs(File file) { + String name = file.getName().replace(".ts", ""); + long value = Long.parseLong(name); + // nginx hls_fragment_naming system uses seconds; convert to ms + return value < 1_000_000_000_000L ? value * 1000L : value; + } } diff --git a/src/main/java/com/ddf/vodsystem/services/media/StreamActionsService.java b/src/main/java/com/ddf/vodsystem/services/media/StreamActionsService.java index 831d992..a4d4bf4 100644 --- a/src/main/java/com/ddf/vodsystem/services/media/StreamActionsService.java +++ b/src/main/java/com/ddf/vodsystem/services/media/StreamActionsService.java @@ -8,9 +8,6 @@ import org.springframework.stereotype.Service; import java.io.File; import java.io.IOException; -import java.time.Instant; -import java.util.Arrays; -import java.util.Comparator; import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -28,19 +25,10 @@ public class StreamActionsService { @Async("ffmpegTaskExecutor") public CompletableFuture saveSection( - String streamDirectory, - Instant startTime, - Instant endTime + List segments, + float trimOffset, + float duration ) throws IOException, InterruptedException { - List segments = getSegmentsInRange(streamDirectory, startTime, endTime); - if (segments.isEmpty()) { - throw new IllegalArgumentException("No segments found in range [" + startTime + ", " + endTime + "]"); - } - - long firstSegmentMs = parseTimestampMs(segments.get(0)); - float trimOffset = Math.max(0f, (startTime.toEpochMilli() - firstSegmentMs) / 1000f); - float duration = (endTime.toEpochMilli() - startTime.toEpochMilli()) / 1000f; - String concatInput = segments.stream() .map(File::getAbsolutePath) .collect(Collectors.joining("|")); @@ -57,34 +45,11 @@ public class StreamActionsService { outputFile.getAbsolutePath() ); - logger.info("Saving section [{} - {}] from '{}' ({} segments) to '{}'", - startTime, endTime, streamDirectory, segments.size(), outputFile.getAbsolutePath()); + logger.info("Saving section with ({} segments) to '{}'", + segments.size(), outputFile.getAbsolutePath()); CommandRunner.run(command, line -> logger.debug("{}", line)); return CompletableFuture.completedFuture(outputFile); } - private List getSegmentsInRange(String streamDirectory, Instant startTime, Instant endTime) { - File dir = new File(streamDirectory); - File[] tsFiles = dir.listFiles((d, name) -> name.endsWith(".ts")); - if (tsFiles == null) return List.of(); - - long startMs = startTime.toEpochMilli(); - long endMs = endTime.toEpochMilli(); - - return Arrays.stream(tsFiles) - .filter(f -> { - long segMs = parseTimestampMs(f); - return segMs < endMs && segMs + 3_000 > startMs; - }) - .sorted(Comparator.comparingLong(this::parseTimestampMs)) - .collect(Collectors.toList()); - } - - private long parseTimestampMs(File file) { - String name = file.getName().replace(".ts", ""); - long value = Long.parseLong(name); - // nginx hls_fragment_naming system uses seconds; convert to ms - return value < 1_000_000_000_000L ? value * 1000L : value; - } }