feat(stream): implement clipping functionality

This commit is contained in:
2026-06-19 22:30:30 +01:00
parent b55ba3ce51
commit 88d83be50a
6 changed files with 59 additions and 6 deletions

View File

@@ -81,6 +81,10 @@
<artifactId>google-api-client</artifactId> <artifactId>google-api-client</artifactId>
<version>2.8.0</version> <version>2.8.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -1,7 +1,8 @@
package com.ddf.vodsystem.controllers; package com.ddf.vodsystem.controllers;
import com.ddf.vodsystem.controllers.input.ClipSectionRequest;
import com.ddf.vodsystem.dto.APIResponse; import com.ddf.vodsystem.dto.APIResponse;
import com.ddf.vodsystem.dto.SaveSectionRequest; import com.ddf.vodsystem.controllers.input.SaveSectionRequest;
import com.ddf.vodsystem.entities.Stream; import com.ddf.vodsystem.entities.Stream;
import com.ddf.vodsystem.services.StreamService; import com.ddf.vodsystem.services.StreamService;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -53,6 +54,13 @@ public class StreamController {
return ResponseEntity.accepted().body(new APIResponse<>("success", "Section saved", null)); return ResponseEntity.accepted().body(new APIResponse<>("success", "Section saved", null));
} }
@PostMapping("/clip")
public ResponseEntity<APIResponse<Void>> saveClip(
@RequestBody ClipSectionRequest request) throws IOException, InterruptedException {
streamService.clip(request.duration());
return ResponseEntity.accepted().body(new APIResponse<>("success", "Clip saved", null));
}
@GetMapping("/history/{userId}") @GetMapping("/history/{userId}")
public ResponseEntity<APIResponse<List<Stream>>> getStreamHistory(@PathVariable Long userId) { public ResponseEntity<APIResponse<List<Stream>>> getStreamHistory(@PathVariable Long userId) {
List<Stream> history = streamService.getStreamHistory(userId); List<Stream> history = streamService.getStreamHistory(userId);

View File

@@ -0,0 +1,7 @@
package com.ddf.vodsystem.controllers.input;
import jakarta.validation.constraints.NotNull;
public record ClipSectionRequest(
@NotNull float duration
) {}

View File

@@ -0,0 +1,8 @@
package com.ddf.vodsystem.controllers.input;
import java.time.Instant;
import jakarta.validation.constraints.NotNull;
public record SaveSectionRequest(
@NotNull Instant startTime,
@NotNull Instant endTime) {}

View File

@@ -1,5 +0,0 @@
package com.ddf.vodsystem.dto;
import java.time.Instant;
public record SaveSectionRequest(Instant startTime, Instant endTime) {}

View File

@@ -17,6 +17,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Arrays; import java.util.Arrays;
@@ -28,7 +29,9 @@ import java.util.stream.Collectors;
@Service @Service
public class StreamService { public class StreamService {
private static final Logger logger = LoggerFactory.getLogger(StreamService.class); private static final Logger logger = LoggerFactory.getLogger(StreamService.class);
private static final int HEARTBEAT_TIMEOUT_SECONDS = 60; private static final int HEARTBEAT_TIMEOUT_SECONDS = 60;
private static final float CLIP_MAX_LENGTH = 180;
private final StreamRepository streamRepository; private final StreamRepository streamRepository;
private final UserService userService; private final UserService userService;
@@ -118,6 +121,34 @@ public class StreamService {
}); });
} }
public void clip(float duration) throws IOException, InterruptedException {
if (duration <= 0 || duration > CLIP_MAX_LENGTH) {
throw new IllegalArgumentException("Clip length must be between 0 and " + CLIP_MAX_LENGTH + " seconds");
}
User user = userService.getLoggedInUser()
.orElseThrow(() -> new NotAuthenticated("User is not authenticated"));
Instant endTime = Instant.now();
// minusSeconds() is possible, but only does integer seconds, not float
Instant startTime = Instant.now().minus(Duration.ofMillis((long) (duration * 1000)));
String streamDirectory = streamDataPath + File.separator + user.getStreamKey();
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);
streamActionsService.saveSection(fileSegments, trimOffset, duration)
.exceptionally(ex -> {
throw new FFMPEGException("Saving stream section failed due to FFMPEG Error");
});
}
@Scheduled(fixedDelay = 30_000) @Scheduled(fixedDelay = 30_000)
@Transactional @Transactional
public void endStaleStreams() { public void endStaleStreams() {