feat(api): add error handling for save endpoint
This commit is contained in:
@@ -50,7 +50,7 @@ public class StreamController {
|
|||||||
public ResponseEntity<APIResponse<Void>> saveSection(
|
public ResponseEntity<APIResponse<Void>> saveSection(
|
||||||
@RequestBody SaveSectionRequest request) throws IOException, InterruptedException {
|
@RequestBody SaveSectionRequest request) throws IOException, InterruptedException {
|
||||||
streamService.saveSection(request.startTime(), request.endTime());
|
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}")
|
@GetMapping("/history/{userId}")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.ddf.vodsystem.services;
|
|||||||
import com.ddf.vodsystem.entities.Stream;
|
import com.ddf.vodsystem.entities.Stream;
|
||||||
import com.ddf.vodsystem.entities.User;
|
import com.ddf.vodsystem.entities.User;
|
||||||
import com.ddf.vodsystem.exceptions.AlreadyStreaming;
|
import com.ddf.vodsystem.exceptions.AlreadyStreaming;
|
||||||
|
import com.ddf.vodsystem.exceptions.FFMPEGException;
|
||||||
import com.ddf.vodsystem.exceptions.KeyNotFound;
|
import com.ddf.vodsystem.exceptions.KeyNotFound;
|
||||||
import com.ddf.vodsystem.exceptions.NotAuthenticated;
|
import com.ddf.vodsystem.exceptions.NotAuthenticated;
|
||||||
import com.ddf.vodsystem.repositories.StreamRepository;
|
import com.ddf.vodsystem.repositories.StreamRepository;
|
||||||
@@ -18,9 +19,11 @@ import java.io.File;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneOffset;
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class StreamService {
|
public class StreamService {
|
||||||
@@ -91,14 +94,27 @@ public class StreamService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void saveSection(Instant startTime, Instant endTime) throws IOException, InterruptedException {
|
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()
|
User user = userService.getLoggedInUser()
|
||||||
.orElseThrow(() -> new NotAuthenticated("User is not authenticated"));
|
.orElseThrow(() -> new NotAuthenticated("User is not authenticated"));
|
||||||
|
|
||||||
String streamDirectory = streamDataPath + File.separator + user.getStreamKey();
|
String streamDirectory = streamDataPath + File.separator + user.getStreamKey();
|
||||||
streamActionsService.saveSection(streamDirectory, startTime, endTime)
|
List<File> 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 -> {
|
.exceptionally(ex -> {
|
||||||
logger.error("saveSection failed for user {}: {}", user.getUsername(), ex.getMessage(), ex);
|
throw new FFMPEGException("Saving stream section failed due to FFMPEG Error");
|
||||||
return null;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,4 +137,28 @@ public class StreamService {
|
|||||||
return userService.getUserByStreamKey(streamKey)
|
return userService.getUserByStreamKey(streamKey)
|
||||||
.orElseThrow(() -> new KeyNotFound("Stream key not found: " + streamKey));
|
.orElseThrow(() -> new KeyNotFound("Stream key not found: " + streamKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<File> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,6 @@ import org.springframework.stereotype.Service;
|
|||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
@@ -28,19 +25,10 @@ public class StreamActionsService {
|
|||||||
|
|
||||||
@Async("ffmpegTaskExecutor")
|
@Async("ffmpegTaskExecutor")
|
||||||
public CompletableFuture<File> saveSection(
|
public CompletableFuture<File> saveSection(
|
||||||
String streamDirectory,
|
List<File> segments,
|
||||||
Instant startTime,
|
float trimOffset,
|
||||||
Instant endTime
|
float duration
|
||||||
) throws IOException, InterruptedException {
|
) throws IOException, InterruptedException {
|
||||||
List<File> 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()
|
String concatInput = segments.stream()
|
||||||
.map(File::getAbsolutePath)
|
.map(File::getAbsolutePath)
|
||||||
.collect(Collectors.joining("|"));
|
.collect(Collectors.joining("|"));
|
||||||
@@ -57,34 +45,11 @@ public class StreamActionsService {
|
|||||||
outputFile.getAbsolutePath()
|
outputFile.getAbsolutePath()
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.info("Saving section [{} - {}] from '{}' ({} segments) to '{}'",
|
logger.info("Saving section with ({} segments) to '{}'",
|
||||||
startTime, endTime, streamDirectory, segments.size(), outputFile.getAbsolutePath());
|
segments.size(), outputFile.getAbsolutePath());
|
||||||
CommandRunner.run(command, line -> logger.debug("{}", line));
|
CommandRunner.run(command, line -> logger.debug("{}", line));
|
||||||
|
|
||||||
return CompletableFuture.completedFuture(outputFile);
|
return CompletableFuture.completedFuture(outputFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<File> 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user