From e8c57f119ca235af161d6ccb4916de17f31252b8 Mon Sep 17 00:00:00 2001 From: Dylan De Faoite Date: Sun, 23 Aug 2026 02:57:50 +0100 Subject: [PATCH] add docstrings to everything --- .../ddf/vodsystem/VodSystemApplication.java | 5 + .../vodsystem/configuration/AsyncConfig.java | 10 ++ .../vodsystem/configuration/CorsConfig.java | 5 + .../configuration/SecurityConfig.java | 8 ++ .../vodsystem/controllers/ClipController.java | 38 +++++++ .../controllers/GlobalExceptionHandler.java | 102 ++++++++++++++++++ .../vodsystem/controllers/JobController.java | 16 +++ .../controllers/MarkerController.java | 23 ++++ .../controllers/MediaController.java | 29 +++++ .../controllers/StreamController.java | 36 +++++++ .../vodsystem/controllers/UserController.java | 24 +++++ .../vodsystem/controllers/VodController.java | 38 +++++++ .../com/ddf/vodsystem/dto/CommandOutput.java | 5 + .../ddf/vodsystem/dto/ProgressTracker.java | 22 ++++ .../exceptions/ClipMediaUnavailable.java | 5 + .../exceptions/VodMediaUnavailable.java | 5 + .../repositories/ClipRepository.java | 6 ++ .../repositories/MarkerRepository.java | 12 +++ .../repositories/StreamRepository.java | 20 ++++ .../repositories/UserRepository.java | 12 +++ .../vodsystem/repositories/VodRepository.java | 6 ++ .../ddf/vodsystem/security/JwtService.java | 12 +++ .../vodsystem/services/DirectoryService.java | 84 +++++++++++++++ .../ddf/vodsystem/services/MarkerService.java | 33 ++++++ .../ddf/vodsystem/services/StreamService.java | 42 ++++++++ .../ddf/vodsystem/services/UserService.java | 24 +++++ .../services/media/CommandRunner.java | 26 +++++ .../services/media/CompressionService.java | 11 ++ .../services/media/MetadataService.java | 8 ++ .../services/media/RemuxService.java | 11 ++ .../services/media/ThumbnailService.java | 10 ++ 31 files changed, 688 insertions(+) diff --git a/src/main/java/com/ddf/vodsystem/VodSystemApplication.java b/src/main/java/com/ddf/vodsystem/VodSystemApplication.java index 308c3df..ff02ad6 100644 --- a/src/main/java/com/ddf/vodsystem/VodSystemApplication.java +++ b/src/main/java/com/ddf/vodsystem/VodSystemApplication.java @@ -10,6 +10,11 @@ import org.springframework.scheduling.annotation.EnableScheduling; @EnableAsync public class VodSystemApplication { + /** + * Application entry point; boots the Spring context. + * + * @param args command-line arguments passed to Spring Boot + */ public static void main(String[] args) { SpringApplication.run(VodSystemApplication.class, args); } diff --git a/src/main/java/com/ddf/vodsystem/configuration/AsyncConfig.java b/src/main/java/com/ddf/vodsystem/configuration/AsyncConfig.java index b6200ac..82877df 100644 --- a/src/main/java/com/ddf/vodsystem/configuration/AsyncConfig.java +++ b/src/main/java/com/ddf/vodsystem/configuration/AsyncConfig.java @@ -14,6 +14,11 @@ import java.util.concurrent.Executor; public class AsyncConfig implements AsyncConfigurer { private static final Logger logger = LoggerFactory.getLogger(AsyncConfig.class); + /** + * Provides the bounded thread pool that runs ffmpeg-backed {@code @Async} media tasks. + * + * @return the configured {@link Executor} registered as {@code ffmpegTaskExecutor} + */ @Bean(name = "ffmpegTaskExecutor") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); @@ -25,6 +30,11 @@ public class AsyncConfig implements AsyncConfigurer { return executor; } + /** + * Supplies the handler that logs uncaught exceptions from void-returning {@code @Async} methods. + * + * @return the async uncaught-exception handler + */ @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (ex, method, params) -> diff --git a/src/main/java/com/ddf/vodsystem/configuration/CorsConfig.java b/src/main/java/com/ddf/vodsystem/configuration/CorsConfig.java index c9d8c4b..1f25144 100644 --- a/src/main/java/com/ddf/vodsystem/configuration/CorsConfig.java +++ b/src/main/java/com/ddf/vodsystem/configuration/CorsConfig.java @@ -12,6 +12,11 @@ public class CorsConfig { @Value("${frontend.url}") private String frontendUrl; + /** + * Configures CORS for the API, allowing credentialed requests from the configured frontend origin. + * + * @return a {@link WebMvcConfigurer} that registers the CORS mappings for {@code /api/v1/**} + */ @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { diff --git a/src/main/java/com/ddf/vodsystem/configuration/SecurityConfig.java b/src/main/java/com/ddf/vodsystem/configuration/SecurityConfig.java index 07266dc..6747bca 100644 --- a/src/main/java/com/ddf/vodsystem/configuration/SecurityConfig.java +++ b/src/main/java/com/ddf/vodsystem/configuration/SecurityConfig.java @@ -22,6 +22,14 @@ public class SecurityConfig { this.jwtFilter = jwtFilter; } + /** + * Builds the security filter chain: stateless sessions, CSRF disabled, the {@link JwtFilter} + * installed, and per-endpoint authorization rules. + * + * @param http the HTTP security builder to configure + * @return the built {@link SecurityFilterChain} + * @throws Exception if the security configuration cannot be built + */ @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http diff --git a/src/main/java/com/ddf/vodsystem/controllers/ClipController.java b/src/main/java/com/ddf/vodsystem/controllers/ClipController.java index 517baa0..545f4c6 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/ClipController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/ClipController.java @@ -24,6 +24,11 @@ public class ClipController { this.clipService = clipService; } + /** + * Lists all clips belonging to the authenticated user. + * + * @return {@code 200 OK} wrapping the user's clips as {@link ClipResponse} DTOs + */ @GetMapping("") public ResponseEntity>> getClips() { List clips = clipService.getClipsByUser(); @@ -39,6 +44,12 @@ public class ClipController { ); } + /** + * Retrieves a single clip owned by the authenticated user. + * + * @param id the ID of the clip to retrieve + * @return {@code 200 OK} wrapping the clip as a {@link ClipResponse} DTO + */ @GetMapping("/{id}") public ResponseEntity> getClipById(@PathVariable Long id) { Clip clip = clipService.getClipById(id); @@ -52,6 +63,13 @@ public class ClipController { ); } + /** + * Applies a partial update to a clip's metadata; null fields are left unchanged. + * + * @param id the ID of the clip to update + * @param updateFields the title and/or description to overwrite + * @return {@code 200 OK} wrapping the updated clip as a {@link ClipResponse} DTO + */ @PatchMapping("/{id}") public ResponseEntity> updateClip(@PathVariable Long id, @RequestBody ClipUpdateRequest updateFields) { @@ -63,6 +81,12 @@ public class ClipController { ); } + /** + * Deletes a clip and its associated files. + * + * @param id the ID of the clip to delete + * @return {@code 200 OK} with a confirmation message + */ @DeleteMapping("/{id}") public ResponseEntity> deleteClip(@PathVariable Long id) { clipService.deleteClip(id); @@ -76,6 +100,13 @@ public class ClipController { ); } + /** + * Streams the clip's video file inline, supporting HTTP range requests. + * + * @param id the ID of the clip to download + * @return {@code 200 OK} with the video resource, or {@code 404 Not Found} if the file is missing + * @throws IOException if the resource's content length cannot be read + */ @GetMapping(value = "/{id}/media", produces = MediaType.ALL_VALUE) public ResponseEntity downloadClip(@PathVariable Long id) throws IOException { Resource resource = clipService.downloadClip(id); @@ -93,6 +124,13 @@ public class ClipController { .body(resource); } + /** + * Streams the clip's thumbnail image with a public cache policy. + * + * @param id the ID of the clip whose thumbnail to download + * @return {@code 200 OK} with the thumbnail resource, or {@code 404 Not Found} if the file is missing + * @throws IOException if the resource's last-modified time cannot be read + */ @GetMapping("/{id}/thumbnail") public ResponseEntity downloadThumbnail(@PathVariable Long id) throws IOException { Resource resource = clipService.downloadThumbnail(id); diff --git a/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java b/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java index 281e194..8b3b00f 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java +++ b/src/main/java/com/ddf/vodsystem/controllers/GlobalExceptionHandler.java @@ -18,6 +18,12 @@ public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); private static final String ERROR = "error"; + /** + * Handles malformed multipart requests, returning {@code 400 Bad Request}. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler({ MultipartException.class }) public ResponseEntity> handleMultipartException(MultipartException ex) { logger.error("MultipartException: {}", ex.getMessage()); @@ -25,6 +31,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); } + /** + * Handles requests missing a required file part, returning {@code 400 Bad Request}. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler({ MissingServletRequestPartException.class }) public ResponseEntity> handleMissingPart(MissingServletRequestPartException ex) { logger.error("MissingServletRequestPartException: {}", ex.getMessage()); @@ -32,6 +44,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); } + /** + * Handles unsupported request content types, returning {@code 415 Unsupported Media Type}. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler({ HttpMediaTypeNotSupportedException.class }) public ResponseEntity> handleUnsupportedMediaType(HttpMediaTypeNotSupportedException ex) { logger.error("HttpMediaTypeNotSupportedException: {}", ex.getMessage()); @@ -39,6 +57,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE).body(response); } + /** + * Handles invalid arguments, returning {@code 400 Bad Request} with the exception message. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity> handleIllegalArgument(IllegalArgumentException ex) { logger.error("IllegalArgumentException: {}", ex.getMessage()); @@ -46,6 +70,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); } + /** + * Handles lookups for a non-existent job, returning {@code 404 Not Found}. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler(JobNotFound.class) public ResponseEntity> handleFileNotFound(JobNotFound ex) { logger.error("JobNotFound: {}", ex.getMessage()); @@ -53,6 +83,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); } + /** + * Handles requests for a job that is still running, returning {@code 202 Accepted}. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler(JobNotFinished.class) public ResponseEntity> handleJobNotFinished(JobNotFinished ex) { logger.error("JobNotFinished: {}", ex.getMessage()); @@ -60,6 +96,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.ACCEPTED).body(response); } + /** + * Handles ffmpeg processing failures, returning {@code 500 Internal Server Error}. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler(FFMPEGException.class) public ResponseEntity> handleFFMPEGException(FFMPEGException ex) { logger.error("FFMPEGException: {}", ex.getMessage()); @@ -67,6 +109,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); } + /** + * Handles unauthenticated or unauthorized access, returning {@code 401 Unauthorized}. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler(NotAuthenticated.class) public ResponseEntity> handleNotAuthenticated(NotAuthenticated ex) { logger.error("NotAuthenticated: {}", ex.getMessage()); @@ -74,6 +122,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); } + /** + * Handles an unrecognised stream key, returning {@code 404 Not Found}. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler(KeyNotFound.class) public ResponseEntity> handleKeyNotFound(KeyNotFound ex) { logger.error("KeyNotFound: {}", ex.getMessage()); @@ -81,6 +135,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); } + /** + * Handles an attempt to start a stream while one is already active, returning {@code 409 Conflict}. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler(AlreadyStreaming.class) public ResponseEntity> handleAlreadyStreaming(AlreadyStreaming ex) { logger.error("AlreadyStreaming: {}", ex.getMessage()); @@ -88,6 +148,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.CONFLICT).body(response); } + /** + * Handles file storage failures, returning {@code 500 Internal Server Error}. + * + * @param e the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler(StorageException.class) public ResponseEntity> handleStorage(StorageException e) { logger.error("Storage operation failed", e); @@ -95,6 +161,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); } + /** + * Handles lookups for a non-existent clip, returning {@code 404 Not Found}. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler(ClipNotFound.class) public ResponseEntity> handleClipNotFound(ClipNotFound ex) { logger.error("ClipNotFound: {}", ex.getMessage()); @@ -102,6 +174,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); } + /** + * Handles a clip whose media file exists but is not readable, returning {@code 503 Service Unavailable}. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler(ClipMediaUnavailable.class) public ResponseEntity> handleClipMediaUnavailable(ClipMediaUnavailable ex) { logger.error("ClipMediaUnavailable (clip {}): {}", ex.getClipId(), ex.getMessage()); @@ -109,6 +187,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(response); } + /** + * Handles lookups for a non-existent VoD, returning {@code 404 Not Found}. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler(VodNotFound.class) public ResponseEntity> handleVodNotFound(VodNotFound ex) { logger.error("VodNotFound: {}", ex.getMessage()); @@ -116,6 +200,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); } + /** + * Handles a VoD whose media file exists but is not readable, returning {@code 503 Service Unavailable}. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler(VodMediaUnavailable.class) public ResponseEntity> handleVodMediaUnavailable(VodMediaUnavailable ex) { logger.error("VodMediaUnavailable (vod {}): {}", ex.getVodId(), ex.getMessage()); @@ -123,6 +213,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(response); } + /** + * Handles actions that require an active stream when none exists, returning {@code 409 Conflict}. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler(NotStreaming.class) public ResponseEntity> handleNotStreaming(NotStreaming ex) { logger.error("NotStreaming: {}", ex.getMessage()); @@ -130,6 +226,12 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.CONFLICT).body(response); } + /** + * Handles lookups for a non-existent marker, returning {@code 404 Not Found}. + * + * @param ex the thrown exception + * @return an error {@link APIResponse} + */ @ExceptionHandler(MarkerNotFound.class) public ResponseEntity> handleMarkerNotFound(MarkerNotFound ex) { logger.error("MarkerNotFound: {}", ex.getMessage()); diff --git a/src/main/java/com/ddf/vodsystem/controllers/JobController.java b/src/main/java/com/ddf/vodsystem/controllers/JobController.java index 8bf6352..bcb85da 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/JobController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/JobController.java @@ -28,6 +28,13 @@ public class JobController { this.jobRegistryService = jobRegistryService; } + /** + * Retrieves the status and progress of an asynchronous job. + * + * @param uuid the UUID of the job to look up + * @return {@code 200 OK} wrapping the job's state as a {@link JobResponse} DTO + * @throws com.ddf.vodsystem.exceptions.JobNotFound if no job with {@code uuid} exists + */ @GetMapping("/{uuid}") public ResponseEntity> getJob(@PathVariable String uuid) { Job job = jobRegistryService.getJob(uuid); @@ -47,6 +54,15 @@ public class JobController { )); } + /** + * Downloads the output file produced by a completed job. + * + * @param uuid the UUID of the job whose output to download + * @return {@code 200 OK} with the output file as a downloadable resource + * @throws com.ddf.vodsystem.exceptions.JobNotFound if no job with {@code uuid} exists + * @throws NotReadyException if the job has not yet produced a downloadable file + * @throws ClipNotFound if the job's output file no longer exists on disk + */ @GetMapping("/{uuid}/download") public ResponseEntity downloadJobFile(@PathVariable String uuid) { Job job = jobRegistryService.getJob(uuid); diff --git a/src/main/java/com/ddf/vodsystem/controllers/MarkerController.java b/src/main/java/com/ddf/vodsystem/controllers/MarkerController.java index 9ab63c1..f55a2bf 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/MarkerController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/MarkerController.java @@ -22,6 +22,11 @@ public class MarkerController { this.markerService = markerService; } + /** + * Lists all markers belonging to the authenticated user. + * + * @return {@code 200 OK} wrapping the user's markers as {@link MarkerResponse} DTOs + */ @GetMapping("") public ResponseEntity>> getMarkers() { List markers = markerService.getUserMarkers().stream() @@ -33,6 +38,12 @@ public class MarkerController { ); } + /** + * Retrieves a single marker owned by the authenticated user. + * + * @param id the ID of the marker to retrieve + * @return {@code 200 OK} wrapping the marker as a {@link MarkerResponse} DTO + */ @GetMapping("/{id}") public ResponseEntity> getMarkerById(@PathVariable Long id) { Marker marker = markerService.getMarkerById(id); @@ -42,6 +53,12 @@ public class MarkerController { ); } + /** + * Creates a marker at the current moment on the user's active stream. + * + * @param request the marker payload carrying the marker message + * @return {@code 200 OK} wrapping the created marker as a {@link MarkerResponse} DTO + */ @PostMapping("") public ResponseEntity> createMarker(@Valid @RequestBody MarkerCreateRequest request) { Marker marker = markerService.create(request.message()); @@ -51,6 +68,12 @@ public class MarkerController { ); } + /** + * Deletes a marker owned by the authenticated user. + * + * @param id the ID of the marker to delete + * @return {@code 200 OK} with a confirmation message + */ @DeleteMapping("/{id}") public ResponseEntity> deleteMarker(@PathVariable Long id) { markerService.deleteMarker(id); diff --git a/src/main/java/com/ddf/vodsystem/controllers/MediaController.java b/src/main/java/com/ddf/vodsystem/controllers/MediaController.java index f1591c0..4aa46ba 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/MediaController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/MediaController.java @@ -26,6 +26,14 @@ public class MediaController { this.mediaService = mediaService; } + /** + * Starts an asynchronous job to compress an uploaded video file. + * + * @param file the uploaded video to compress + * @param options trim window, resolution, fps, and target size for the output + * @return {@code 200 OK} wrapping the new job's UUID for polling + * @throws IOException if saving the upload to temporary storage fails + */ @PostMapping(value = "/compress", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> compress( @RequestParam("file")MultipartFile file, @@ -40,6 +48,13 @@ public class MediaController { } + /** + * Starts an asynchronous job to save a section of the user's stream between two timestamps as a VoD. + * + * @param saveSectionRequest the start/end timestamps and optional title/description + * @return {@code 200 OK} wrapping the new job's UUID for polling + * @throws IOException if reading the stream segments fails + */ @PostMapping("/save") public ResponseEntity> save( @RequestBody SaveSectionRequest saveSectionRequest) throws IOException { @@ -55,6 +70,13 @@ public class MediaController { )); } + /** + * Starts an asynchronous job to save the section of the user's stream between two markers as a VoD. + * + * @param saveSectionByMarkerRequest the start/end marker IDs and optional title/description + * @return {@code 200 OK} wrapping the new job's UUID for polling + * @throws IOException if reading the stream segments fails + */ @PostMapping("/save/markers") public ResponseEntity> saveByMarkers( @Valid @RequestBody SaveSectionByMarkerRequest saveSectionByMarkerRequest) throws IOException { @@ -70,6 +92,13 @@ public class MediaController { )); } + /** + * Starts an asynchronous job to save the last few seconds of the user's stream as a clip. + * + * @param clipSectionRequest the clip duration in seconds and optional title/description + * @return {@code 200 OK} wrapping the new job's UUID for polling + * @throws IOException if reading the stream segments fails + */ @PostMapping("/clip") public ResponseEntity> clip( @RequestBody ClipSectionRequest clipSectionRequest) throws IOException { diff --git a/src/main/java/com/ddf/vodsystem/controllers/StreamController.java b/src/main/java/com/ddf/vodsystem/controllers/StreamController.java index 09ce332..7228c3f 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/StreamController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/StreamController.java @@ -24,6 +24,14 @@ public class StreamController { this.streamService = streamService; } + /** + * Starts a new stream for the user owning the given stream key. + *

+ * Called by nginx via its {@code on_publish} RTMP callback when a broadcast begins. + * + * @param streamKey the RTMP stream key identifying the broadcasting user + * @return {@code 200 OK} wrapping the newly started {@link Stream} + */ // Called by nginx on_publish @PostMapping("/start") public ResponseEntity> startStream(@RequestParam("name") String streamKey) { @@ -32,6 +40,14 @@ public class StreamController { return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Stream started", stream)); } + /** + * Ends the active stream for the user owning the given stream key. + *

+ * Called by nginx via its {@code on_publish_done} RTMP callback when a broadcast stops. + * + * @param streamKey the RTMP stream key identifying the broadcasting user + * @return {@code 200 OK} confirming the stream was ended + */ // Called by nginx on_publish_done @PostMapping("/stop") public ResponseEntity> stopStream(@RequestParam("name") String streamKey) { @@ -40,6 +56,14 @@ public class StreamController { return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Stream ended", null)); } + /** + * Records a liveness heartbeat for the active stream, keeping it from being reaped as stale. + *

+ * Called periodically by nginx via its {@code on_update} RTMP callback. + * + * @param streamKey the RTMP stream key identifying the broadcasting user + * @return {@code 200 OK} confirming the heartbeat was recorded + */ // Called by nginx on_update @PostMapping("/heartbeat") public ResponseEntity> heartbeat(@RequestParam("name") String streamKey) { @@ -47,12 +71,24 @@ public class StreamController { return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Heartbeat recorded", null)); } + /** + * Retrieves the stream history for the given user. + * + * @param userId the ID of the user whose stream history to retrieve; must be the authenticated user + * @return {@code 200 OK} wrapping the user's past and current {@link Stream}s + */ @GetMapping("/history/{userId}") public ResponseEntity>> getStreamHistory(@PathVariable Long userId) { List history = streamService.getStreamHistory(userId); return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Stream history retrieved", history)); } + /** + * Reports whether the authenticated user is currently streaming. + * + * @return {@code 200 OK} wrapping a {@link StreamResponse} indicating the live status and, if live, + * the active stream's ID + */ @GetMapping("/current") public ResponseEntity> getCurrentStream() { Optional stream = streamService.getActiveStream(); diff --git a/src/main/java/com/ddf/vodsystem/controllers/UserController.java b/src/main/java/com/ddf/vodsystem/controllers/UserController.java index 0f440ec..815e8bd 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/UserController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/UserController.java @@ -27,6 +27,12 @@ public class UserController { this.userService = userService; } + /** + * Returns the currently authenticated user's profile. + * + * @return {@code 200 OK} wrapping the authenticated {@link User} + * @throws NotAuthenticated if no valid session is present + */ @GetMapping("/me") public ResponseEntity> user() { Optional user = userService.getLoggedInUser(); @@ -40,6 +46,17 @@ public class UserController { ); } + /** + * Authenticates a user from a Google ID token and issues a session JWT. + *

+ * The JWT is set as an HttpOnly {@code token} cookie (scoped by request security) and also + * returned in the response body. + * + * @param token the payload carrying the Google ID token to verify + * @param request the incoming request, used to decide cookie {@code Secure}/{@code SameSite} flags + * @param response the response the session cookie is written to + * @return {@code 200 OK} wrapping the issued JWT as a {@link Token} + */ @PostMapping("/login") public ResponseEntity> login(@RequestBody Token token, HttpServletRequest request, @@ -61,6 +78,13 @@ public class UserController { ); } + /** + * Logs the user out by clearing the session cookie. + * + * @param response the response the expired cookie is written to + * @param request the incoming request, used to match the original cookie's {@code Secure}/{@code SameSite} flags + * @return {@code 200 OK} confirming logout + */ @PostMapping("/logout") public ResponseEntity> logout(HttpServletResponse response, HttpServletRequest request) { diff --git a/src/main/java/com/ddf/vodsystem/controllers/VodController.java b/src/main/java/com/ddf/vodsystem/controllers/VodController.java index 28846c5..1ca18b3 100644 --- a/src/main/java/com/ddf/vodsystem/controllers/VodController.java +++ b/src/main/java/com/ddf/vodsystem/controllers/VodController.java @@ -24,6 +24,11 @@ public class VodController { this.vodService = vodService; } + /** + * Lists all VoDs belonging to the authenticated user. + * + * @return {@code 200 OK} wrapping the user's VoDs as {@link VodResponse} DTOs + */ @GetMapping("") public ResponseEntity>> getVods() { List vods = vodService.getUserVods().stream() @@ -33,12 +38,25 @@ public class VodController { return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Vods retrieved successfully", vods)); } + /** + * Retrieves a single VoD owned by the authenticated user. + * + * @param id the ID of the VoD to retrieve + * @return {@code 200 OK} wrapping the VoD as a {@link VodResponse} DTO + */ @GetMapping("/{id}") public ResponseEntity> getVodById(@PathVariable Long id) { VodResponse vod = convertToDTO(vodService.getVodById(id)); return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Vod retrieved successfully", vod)); } + /** + * Applies a partial update to a VoD's metadata; null fields are left unchanged. + * + * @param id the ID of the VoD to update + * @param updateFields the title and/or description to overwrite + * @return {@code 200 OK} wrapping the updated VoD as a {@link VodResponse} DTO + */ @PatchMapping("/{id}") public ResponseEntity> updateVod(@PathVariable Long id, @RequestBody VodUpdateRequest updateFields) { @@ -46,6 +64,12 @@ public class VodController { return ResponseEntity.ok(new APIResponse<>(SUCCESS, "Vod updated successfully", vod)); } + /** + * Deletes a VoD and its associated files. + * + * @param id the ID of the VoD to delete + * @return {@code 200 OK} with a confirmation message + */ @DeleteMapping("/{id}") public ResponseEntity> deleteVod(@PathVariable Long id) { vodService.deleteVod(id); @@ -53,6 +77,13 @@ public class VodController { "Vod with ID " + id + " has been deleted")); } + /** + * Streams the VoD's video file inline, supporting HTTP range requests. + * + * @param id the ID of the VoD to download + * @return {@code 200 OK} with the video resource, or {@code 404 Not Found} if the file is missing + * @throws IOException if the resource's content length cannot be read + */ @GetMapping(value = "/{id}/media", produces = MediaType.ALL_VALUE) public ResponseEntity downloadVod(@PathVariable Long id) throws IOException { Resource resource = vodService.downloadVod(id); @@ -70,6 +101,13 @@ public class VodController { .body(resource); } + /** + * Streams the VoD's thumbnail image with a public cache policy. + * + * @param id the ID of the VoD whose thumbnail to download + * @return {@code 200 OK} with the thumbnail resource, or {@code 404 Not Found} if the file is missing + * @throws IOException if the resource's last-modified time cannot be read + */ @GetMapping("/{id}/thumbnail") public ResponseEntity downloadThumbnail(@PathVariable Long id) throws IOException { Resource resource = vodService.downloadThumbnail(id); diff --git a/src/main/java/com/ddf/vodsystem/dto/CommandOutput.java b/src/main/java/com/ddf/vodsystem/dto/CommandOutput.java index 643c3a6..b760e3c 100644 --- a/src/main/java/com/ddf/vodsystem/dto/CommandOutput.java +++ b/src/main/java/com/ddf/vodsystem/dto/CommandOutput.java @@ -10,6 +10,11 @@ public class CommandOutput { private List output = new ArrayList<>(); private int exitCode; + /** + * Appends a line to the captured command output. + * + * @param line the output line to append + */ public void addLine(String line) { output.add(line); } diff --git a/src/main/java/com/ddf/vodsystem/dto/ProgressTracker.java b/src/main/java/com/ddf/vodsystem/dto/ProgressTracker.java index 8707a12..237de3a 100644 --- a/src/main/java/com/ddf/vodsystem/dto/ProgressTracker.java +++ b/src/main/java/com/ddf/vodsystem/dto/ProgressTracker.java @@ -4,14 +4,30 @@ public class ProgressTracker { private float progress = 0.0f; private boolean isComplete = false; + /** + * Returns the current progress. + * + * @return the progress as a fraction in {@code [0, 1]} + */ public synchronized float getProgress() { return progress; } + /** + * Returns whether the tracked task has been marked complete. + * + * @return {@code true} if complete + */ public synchronized boolean isComplete() { return isComplete; } + /** + * Updates the current progress. + * + * @param newProgress the progress as a fraction in {@code [0, 1]} + * @throws IllegalArgumentException if {@code newProgress} is outside {@code [0, 1]} + */ public synchronized void setProgress(float newProgress) { if (newProgress < 0 || newProgress > 1) { throw new IllegalArgumentException("Progress must be between 0 and 1"); @@ -19,10 +35,16 @@ public class ProgressTracker { this.progress = newProgress; } + /** + * Marks the tracked task as complete. + */ public synchronized void markComplete() { this.isComplete = true; } + /** + * Resets progress to zero and clears the complete flag. + */ public synchronized void reset() { this.progress = 0.0f; this.isComplete = false; diff --git a/src/main/java/com/ddf/vodsystem/exceptions/ClipMediaUnavailable.java b/src/main/java/com/ddf/vodsystem/exceptions/ClipMediaUnavailable.java index b850d2c..bfc4ef1 100644 --- a/src/main/java/com/ddf/vodsystem/exceptions/ClipMediaUnavailable.java +++ b/src/main/java/com/ddf/vodsystem/exceptions/ClipMediaUnavailable.java @@ -12,6 +12,11 @@ public class ClipMediaUnavailable extends RuntimeException { this.clipId = clipId; } + /** + * Returns the ID of the clip whose media was unavailable. + * + * @return the affected clip's ID + */ public Long getClipId() { return clipId; } diff --git a/src/main/java/com/ddf/vodsystem/exceptions/VodMediaUnavailable.java b/src/main/java/com/ddf/vodsystem/exceptions/VodMediaUnavailable.java index fb200fd..a9de256 100644 --- a/src/main/java/com/ddf/vodsystem/exceptions/VodMediaUnavailable.java +++ b/src/main/java/com/ddf/vodsystem/exceptions/VodMediaUnavailable.java @@ -12,6 +12,11 @@ public class VodMediaUnavailable extends RuntimeException { this.vodId = vodId; } + /** + * Returns the ID of the VoD whose media was unavailable. + * + * @return the affected VoD's ID + */ public Long getVodId() { return vodId; } diff --git a/src/main/java/com/ddf/vodsystem/repositories/ClipRepository.java b/src/main/java/com/ddf/vodsystem/repositories/ClipRepository.java index fd1bc8e..c097576 100644 --- a/src/main/java/com/ddf/vodsystem/repositories/ClipRepository.java +++ b/src/main/java/com/ddf/vodsystem/repositories/ClipRepository.java @@ -10,6 +10,12 @@ import java.util.List; @Repository public interface ClipRepository extends JpaRepository { + /** + * Finds all clips owned by the given user. + * + * @param user the owning user + * @return the user's clips + */ @Query("SELECT c FROM Clip c WHERE c.user = ?1") List findByUser(User user); } diff --git a/src/main/java/com/ddf/vodsystem/repositories/MarkerRepository.java b/src/main/java/com/ddf/vodsystem/repositories/MarkerRepository.java index d5b7d82..b8d65c1 100644 --- a/src/main/java/com/ddf/vodsystem/repositories/MarkerRepository.java +++ b/src/main/java/com/ddf/vodsystem/repositories/MarkerRepository.java @@ -11,9 +11,21 @@ import java.util.List; @Repository public interface MarkerRepository extends JpaRepository { + /** + * Finds all markers owned by the given user. + * + * @param user the owning user + * @return the user's markers + */ @Query("SELECT m FROM Marker m WHERE m.user = ?1") List findByUser(User user); + /** + * Finds all markers timestamped at or before the given instant. + * + * @param time the cutoff instant (inclusive) + * @return markers at or before {@code time} + */ @Query("SELECT m FROM Marker m WHERE m.timestamp <= ?1") List findAllBefore(Instant time); } diff --git a/src/main/java/com/ddf/vodsystem/repositories/StreamRepository.java b/src/main/java/com/ddf/vodsystem/repositories/StreamRepository.java index e577e36..ea63a28 100644 --- a/src/main/java/com/ddf/vodsystem/repositories/StreamRepository.java +++ b/src/main/java/com/ddf/vodsystem/repositories/StreamRepository.java @@ -11,7 +11,27 @@ import java.util.Optional; @Repository public interface StreamRepository extends JpaRepository { + /** + * Finds the user's active (not-yet-ended) stream, if any. + * + * @param user the owning user + * @return the active stream, or empty if none + */ Optional findByUserAndEndDateIsNull(User user); + + /** + * Finds all streams belonging to the given user. + * + * @param user the owning user + * @return the user's streams + */ List findByUser(User user); + + /** + * Finds active streams whose last heartbeat predates the given cutoff, i.e. stale streams. + * + * @param cutoff the last-seen cutoff; streams last seen before this are returned + * @return the stale active streams + */ List findByEndDateIsNullAndLastSeenBefore(Instant cutoff); } diff --git a/src/main/java/com/ddf/vodsystem/repositories/UserRepository.java b/src/main/java/com/ddf/vodsystem/repositories/UserRepository.java index 3a978b7..8779774 100644 --- a/src/main/java/com/ddf/vodsystem/repositories/UserRepository.java +++ b/src/main/java/com/ddf/vodsystem/repositories/UserRepository.java @@ -9,9 +9,21 @@ import java.util.Optional; @Repository public interface UserRepository extends JpaRepository { + /** + * Finds a user by their Google account ID. + * + * @param googleId the Google subject identifier + * @return the matching user, or empty if none + */ @Query("SELECT u FROM User u WHERE u.googleId = ?1") Optional findByGoogleId(String googleId); + /** + * Finds a user by their stream key. + * + * @param streamKey the stream key to match + * @return the matching user, or empty if none + */ @Query("SELECT u FROM User u WHERE u.streamKey = ?1") Optional findByStreamKey(String streamKey); } diff --git a/src/main/java/com/ddf/vodsystem/repositories/VodRepository.java b/src/main/java/com/ddf/vodsystem/repositories/VodRepository.java index 52863e3..52fd1c2 100644 --- a/src/main/java/com/ddf/vodsystem/repositories/VodRepository.java +++ b/src/main/java/com/ddf/vodsystem/repositories/VodRepository.java @@ -11,6 +11,12 @@ import java.util.List; @Repository public interface VodRepository extends JpaRepository { + /** + * Finds all VoDs owned by the given user. + * + * @param user the owning user + * @return the user's VoDs + */ @Query("SELECT v FROM Vod v WHERE v.user = ?1") List findByUser(User user); } diff --git a/src/main/java/com/ddf/vodsystem/security/JwtService.java b/src/main/java/com/ddf/vodsystem/security/JwtService.java index d9f860d..ce8bfb5 100644 --- a/src/main/java/com/ddf/vodsystem/security/JwtService.java +++ b/src/main/java/com/ddf/vodsystem/security/JwtService.java @@ -26,6 +26,12 @@ public class JwtService { this.algorithm = Algorithm.HMAC256(jwtSecretKey); } + /** + * Generates a signed JWT carrying the given user ID, valid for the configured expiry. + * + * @param userId the user ID to embed as a claim + * @return the signed JWT string + */ public String generateToken(Long userId) { return JWT.create() .withClaim(USER_ID_CLAIM, userId) @@ -34,6 +40,12 @@ public class JwtService { .sign(algorithm); } + /** + * Verifies a JWT's signature, issuer, and expiry, returning the embedded user ID. + * + * @param token the JWT to validate + * @return the user ID claim if the token is valid and unexpired, otherwise {@code null} + */ public Long validateTokenAndGetUserId(String token) { try { JWTVerifier verifier = JWT.require(algorithm) diff --git a/src/main/java/com/ddf/vodsystem/services/DirectoryService.java b/src/main/java/com/ddf/vodsystem/services/DirectoryService.java index 537b9aa..94283bc 100644 --- a/src/main/java/com/ddf/vodsystem/services/DirectoryService.java +++ b/src/main/java/com/ddf/vodsystem/services/DirectoryService.java @@ -40,61 +40,132 @@ public class DirectoryService { private static final String CLIPS_DIR_NAME = "clips"; private static final String THUMBNAIL_DIR_NAME = "thumbnails"; + /** + * Returns the directory for temporary upload inputs. + * + * @return the configured temporary input directory path + */ public Path getTempInputDir() { return Path.of(tempInputsDir); } + /** + * Returns the directory for temporary processing outputs. + * + * @return the configured temporary output directory path + */ public Path getTempOutputDir() { return Path.of(tempOutputsDir); } + /** + * Returns the given user's root media directory, creating it if necessary. + * + * @param userId the ID of the user + * @return the user's media directory path + * @throws IOException if the directory cannot be created + */ public Path getUserDir(Long userId) throws IOException { Path userDir = Path.of(usersDir + File.separator + userId); Files.createDirectories(userDir); return userDir; } + /** + * Returns the given user's VoDs directory, creating it if necessary. + * + * @param userId the ID of the user + * @return the user's VoDs directory path + * @throws IOException if the directory cannot be created + */ public Path getVodsDir(Long userId) throws IOException { Path vodDir = getUserDir(userId).resolve(VODS_DIR_NAME); Files.createDirectories(vodDir); return vodDir; } + /** + * Returns the given user's clips directory, creating it if necessary. + * + * @param userId the ID of the user + * @return the user's clips directory path + * @throws IOException if the directory cannot be created + */ public Path getClipsDir(Long userId) throws IOException { Path clipsDir = getUserDir(userId).resolve(CLIPS_DIR_NAME); Files.createDirectories(clipsDir); return clipsDir; } + /** + * Returns the given user's thumbnails directory, creating it if necessary. + * + * @param userId the ID of the user + * @return the user's thumbnails directory path + * @throws IOException if the directory cannot be created + */ public Path getThumbnailsDir(Long userId) throws IOException { Path thumbnailsDir = getUserDir(userId).resolve(THUMBNAIL_DIR_NAME); Files.createDirectories(thumbnailsDir); return thumbnailsDir; } + /** + * Returns the directory holding a stream's recorded HLS segments, creating it if necessary. + * + * @param streamKey the stream key naming the segment directory + * @return the stream's segment directory path + * @throws IOException if the directory cannot be created + */ public Path getStreamDir(String streamKey) throws IOException { Path streamFolder = Path.of(streamsDir, streamKey); Files.createDirectories(streamFolder); return streamFolder; } + /** + * Converts an absolute path into one relative to the users media root, for storage in the database. + * + * @param path the absolute path to relativise + * @return the path relative to the users media directory + */ public Path relativisePath(Path path) { Path media = Path.of(usersDir).toAbsolutePath(); Path newPath = path.toAbsolutePath(); return media.relativize(newPath); } + /** + * Resolves a stored relative path back into an absolute path under the users media root. + * + * @param path the relative path, as stored in the database + * @return the absolute path on disk + */ public Path resolvePath(String path) { Path media = Path.of(usersDir).toAbsolutePath(); Path newPath = Path.of(path); return media.resolve(newPath); } + /** + * Writes an uploaded file to the given path, creating parent directories and overwriting any existing file. + * + * @param path the destination path + * @param multipartFile the uploaded file to save + * @throws IOException if writing the file fails + */ public void saveMultipartFile(Path path, MultipartFile multipartFile) throws IOException { Files.createDirectories(path.getParent()); Files.copy(multipartFile.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING); } + /** + * Copies a file, creating parent directories and overwriting any existing target. + * + * @param source the file to copy from + * @param target the destination path + * @throws IOException if the copy fails + */ public void copyFile(Path source, Path target) throws IOException { Files.createDirectories(target.getParent()); Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); @@ -146,6 +217,12 @@ public class DirectoryService { } } + /** + * Creates the temporary and users media directories on startup and verifies they are usable. + * + * @throws IOException if a directory cannot be created + * @throws IllegalStateException if any required directory is not a readable, writable directory + */ @PostConstruct public void createDirectoriesIfNotExist() throws IOException { Files.createDirectories(Path.of(tempInputsDir)); @@ -155,6 +232,13 @@ public class DirectoryService { checkDirPermissions(); } + /** + * Scheduled cleanup that deletes stale files from the temporary input and output directories. + *

+ * Runs every 30 minutes; files older than 3 hours are removed. + * + * @throws IOException if listing a temporary directory fails + */ @Scheduled(fixedRate = TEMP_DIR_CLEANUP_RATE) public void cleanTempDirectories() throws IOException { cleanUpDirectory(tempInputsDir); diff --git a/src/main/java/com/ddf/vodsystem/services/MarkerService.java b/src/main/java/com/ddf/vodsystem/services/MarkerService.java index 77059fc..afd6bd9 100644 --- a/src/main/java/com/ddf/vodsystem/services/MarkerService.java +++ b/src/main/java/com/ddf/vodsystem/services/MarkerService.java @@ -32,6 +32,14 @@ public class MarkerService { this.markerRepository = markerRepository; } + /** + * Returns the marker with the given ID, verifying ownership by the current user. + * + * @param id the ID of the marker to retrieve + * @return the matching {@link Marker} + * @throws MarkerNotFound if no marker with {@code id} exists + * @throws NotAuthenticated if the current user does not own the marker + */ public Marker getMarkerById(Long id) { Optional marker = markerRepository.findById(id); @@ -47,6 +55,12 @@ public class MarkerService { return marker.get(); } + /** + * Returns all markers belonging to the currently authenticated user. + * + * @return list of the current user's markers + * @throws NotAuthenticated if no user session is present + */ public List getUserMarkers() { Optional user = userService.getLoggedInUser(); @@ -57,6 +71,13 @@ public class MarkerService { return markerRepository.findByUser(user.get()); } + /** + * Creates a marker timestamped now on the current user's active stream. + * + * @param message the marker's descriptive message + * @return the persisted {@link Marker} + * @throws NotStreaming if the user has no active stream to mark + */ public Marker create(String message) { Optional stream = streamService.getActiveStream(); @@ -72,11 +93,23 @@ public class MarkerService { return markerRepository.saveAndFlush(marker); } + /** + * Deletes a marker owned by the current user. + * + * @param id the ID of the marker to delete + * @throws MarkerNotFound if no marker with {@code id} exists + * @throws NotAuthenticated if the current user does not own the marker + */ public void deleteMarker(Long id) { Marker marker = getMarkerById(id); markerRepository.delete(marker); } + /** + * Scheduled cleanup that deletes markers older than the configured maximum stream length. + *

+ * Runs periodically (every 6 minutes) to purge markers left behind by streams that have ended. + */ @Scheduled(fixedDelay = 360_000) @Transactional public void deleteOldMarkers() { diff --git a/src/main/java/com/ddf/vodsystem/services/StreamService.java b/src/main/java/com/ddf/vodsystem/services/StreamService.java index 4603511..6db4156 100644 --- a/src/main/java/com/ddf/vodsystem/services/StreamService.java +++ b/src/main/java/com/ddf/vodsystem/services/StreamService.java @@ -30,6 +30,14 @@ public class StreamService { this.userService = userService; } + /** + * Starts a new stream for the user identified by the given stream key. + * + * @param streamKey the stream key resolving to the broadcasting user + * @return the newly persisted {@link Stream} + * @throws KeyNotFound if no user owns {@code streamKey} + * @throws AlreadyStreaming if the user already has an active stream + */ public Stream startStream(String streamKey) { User user = resolveUser(streamKey); @@ -45,6 +53,13 @@ public class StreamService { return streamRepository.saveAndFlush(stream); } + /** + * Ends the active stream for the user identified by the given stream key. + * + * @param streamKey the stream key resolving to the broadcasting user + * @throws KeyNotFound if no user owns {@code streamKey} + * @throws IllegalStateException if the user has no active stream + */ public void endStream(String streamKey) { User user = resolveUser(streamKey); @@ -56,6 +71,13 @@ public class StreamService { streamRepository.saveAndFlush(stream); } + /** + * Records a liveness heartbeat on the active stream, refreshing its last-seen time. + * + * @param streamKey the stream key resolving to the broadcasting user + * @throws KeyNotFound if no user owns {@code streamKey} + * @throws IllegalStateException if the user has no active stream + */ public void heartbeatStream(String streamKey) { User user = resolveUser(streamKey); @@ -67,6 +89,12 @@ public class StreamService { streamRepository.saveAndFlush(stream); } + /** + * Returns the current user's active (not-yet-ended) stream, if any. + * + * @return the active {@link Stream}, or empty if the user is not currently streaming + * @throws NotAuthenticated if no user session is present + */ public Optional getActiveStream() { Optional user = userService.getLoggedInUser(); @@ -77,6 +105,14 @@ public class StreamService { return streamRepository.findByUserAndEndDateIsNull(user.get()); } + /** + * Returns the full stream history for the given user. + * + * @param userId the ID of the user whose history to retrieve + * @return the user's streams, past and present + * @throws IllegalArgumentException if no user with {@code userId} exists + * @throws NotAuthenticated if no user is authenticated, or the caller is not that user + */ public List getStreamHistory(Long userId) { User streamUser = userService.getUserById(userId) .orElseThrow(() -> new IllegalArgumentException("User not found: " + userId)); @@ -94,6 +130,12 @@ public class StreamService { return streamRepository.findByUser(streamUser); } + /** + * Scheduled sweep that ends streams which have missed heartbeats past the timeout. + *

+ * Runs every 30 seconds; any active stream whose last heartbeat is older than + * {@value #HEARTBEAT_TIMEOUT_SECONDS} seconds is marked ended. + */ @Scheduled(fixedDelay = 30_000) @Transactional public void endStaleStreams() { diff --git a/src/main/java/com/ddf/vodsystem/services/UserService.java b/src/main/java/com/ddf/vodsystem/services/UserService.java index a312f64..f29d16c 100644 --- a/src/main/java/com/ddf/vodsystem/services/UserService.java +++ b/src/main/java/com/ddf/vodsystem/services/UserService.java @@ -44,10 +44,21 @@ public class UserService { this.jwtService = jwtService; } + /** + * Looks up a user by their database ID. + * + * @param userId the ID of the user to find + * @return the matching {@link User}, or empty if none exists + */ public Optional getUserById(Long userId) { return userRepository.findById(userId); } + /** + * Returns the user backing the current Spring Security authentication, if authenticated. + * + * @return the authenticated {@link User}, or empty if no user is authenticated + */ public Optional getLoggedInUser() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof User user) { @@ -56,6 +67,13 @@ public class UserService { return Optional.empty(); } + /** + * Verifies a Google ID token, creating or updating the corresponding user, and issues a session JWT. + * + * @param idToken the Google ID token to verify + * @return a signed JWT for the authenticated user + * @throws NotAuthenticated if the token is invalid or its subject is missing + */ public String login(String idToken) { GoogleIdToken googleIdToken = getGoogleIdToken(idToken); String googleId = googleIdToken.getPayload().getSubject(); @@ -70,6 +88,12 @@ public class UserService { return jwtService.generateToken(user.getId()); } + /** + * Looks up a user by their stream key. + * + * @param streamKey the stream key to match + * @return the matching {@link User}, or empty if none exists + */ public Optional getUserByStreamKey(String streamKey) { return userRepository.findByStreamKey(streamKey); } diff --git a/src/main/java/com/ddf/vodsystem/services/media/CommandRunner.java b/src/main/java/com/ddf/vodsystem/services/media/CommandRunner.java index c0f00b6..129d869 100644 --- a/src/main/java/com/ddf/vodsystem/services/media/CommandRunner.java +++ b/src/main/java/com/ddf/vodsystem/services/media/CommandRunner.java @@ -16,6 +16,15 @@ import java.util.regex.Pattern; public class CommandRunner { private static final Pattern timePattern = Pattern.compile("out_time_ms=(\\d+)"); + /** + * Runs an external command, capturing its merged stdout/stderr and streaming each line to a callback. + * + * @param command the command and its arguments + * @param onOutput invoked for each output line as it is read, or {@code null} to skip streaming + * @return the captured output and exit code + * @throws IOException if the process fails to start or exits with a non-zero code + * @throws InterruptedException if the current thread is interrupted while awaiting the process + */ public CommandOutput run(List command, Consumer onOutput) throws IOException, InterruptedException { ProcessBuilder processBuilder = new ProcessBuilder(command); processBuilder.redirectErrorStream(true); @@ -43,10 +52,27 @@ public class CommandRunner { return commandOutput; } + /** + * Runs an external command, capturing its output without streaming. + * + * @param command the command and its arguments + * @return the captured output and exit code + * @throws IOException if the process fails to start or exits with a non-zero code + * @throws InterruptedException if the current thread is interrupted while awaiting the process + */ public CommandOutput run(List command) throws IOException, InterruptedException { return run(command, null); } + /** + * Parses an ffmpeg progress line and updates the tracker with the fraction completed. + *

+ * Lines without an {@code out_time_ms} field are ignored. + * + * @param line a single line of ffmpeg {@code -progress} output + * @param progress the tracker to update + * @param length the total media length in seconds, used as the progress denominator + */ public void setProgress(String line, ProgressTracker progress, float length) { Matcher matcher = timePattern.matcher(line); if (matcher.find()) { diff --git a/src/main/java/com/ddf/vodsystem/services/media/CompressionService.java b/src/main/java/com/ddf/vodsystem/services/media/CompressionService.java index b1299c0..76917e1 100644 --- a/src/main/java/com/ddf/vodsystem/services/media/CompressionService.java +++ b/src/main/java/com/ddf/vodsystem/services/media/CompressionService.java @@ -27,6 +27,17 @@ public class CompressionService { this.commandRunner = commandRunner; } + /** + * Compresses (and optionally trims/rescales) a video with ffmpeg on a background executor. + *

+ * Runs asynchronously; failures complete the returned future exceptionally rather than throwing. + * + * @param inputFile the source video file + * @param outputFile the destination for the compressed output + * @param clipOptions the trim window, target resolution/fps, and size driving the ffmpeg command + * @param progress the tracker updated as encoding progresses + * @return a future completing with the ffmpeg output, or exceptionally on failure + */ @Async("ffmpegTaskExecutor") public CompletableFuture compress(Path inputFile, Path outputFile, diff --git a/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java b/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java index 08bf15b..3767407 100644 --- a/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java +++ b/src/main/java/com/ddf/vodsystem/services/media/MetadataService.java @@ -26,6 +26,14 @@ public class MetadataService { this.commandRunner = commandRunner; } + /** + * Probes a video file with ffprobe and returns its metadata (duration, dimensions, fps, size). + *

+ * Runs asynchronously; failures complete the returned future exceptionally rather than throwing. + * + * @param file the video file to probe + * @return a future completing with the parsed {@link ClipOptions}, or exceptionally on failure + */ @Async("ffmpegTaskExecutor") public CompletableFuture getVideoMetadata(Path file) { logger.info("Getting metadata for file {}", file); diff --git a/src/main/java/com/ddf/vodsystem/services/media/RemuxService.java b/src/main/java/com/ddf/vodsystem/services/media/RemuxService.java index 38ac85c..e347eee 100644 --- a/src/main/java/com/ddf/vodsystem/services/media/RemuxService.java +++ b/src/main/java/com/ddf/vodsystem/services/media/RemuxService.java @@ -21,6 +21,17 @@ public class RemuxService { this.commandRunner = commandRunner; } + /** + * Re-encodes a video to H.264/AAC in an MP4 container with ffmpeg on a background executor. + *

+ * Runs asynchronously; failures complete the returned future exceptionally rather than throwing. + * + * @param inputFile the source video file + * @param outputFile the destination for the remuxed MP4 + * @param remuxProgress the tracker updated as encoding progresses + * @param length the media length in seconds, used to compute progress + * @return a future completing with the ffmpeg output, or exceptionally on failure + */ @Async("ffmpegTaskExecutor") public CompletableFuture remux(File inputFile, File outputFile, diff --git a/src/main/java/com/ddf/vodsystem/services/media/ThumbnailService.java b/src/main/java/com/ddf/vodsystem/services/media/ThumbnailService.java index 70fa517..9613b04 100644 --- a/src/main/java/com/ddf/vodsystem/services/media/ThumbnailService.java +++ b/src/main/java/com/ddf/vodsystem/services/media/ThumbnailService.java @@ -20,6 +20,16 @@ public class ThumbnailService { this.commandRunner = commandRunner; } + /** + * Extracts a single frame from a video as a thumbnail image with ffmpeg on a background executor. + *

+ * Runs asynchronously; failures complete the returned future exceptionally rather than throwing. + * + * @param inputFile the source video file + * @param outputFile the destination for the thumbnail image + * @param timeInVideo the timestamp in seconds at which to capture the frame + * @return a future completing with the ffmpeg output, or exceptionally on failure + */ @Async("ffmpegTaskExecutor") public CompletableFuture createThumbnail(Path inputFile, Path outputFile, Float timeInVideo) { logger.info("Creating thumbnail at {} seconds", timeInVideo); -- 2.49.1