docs(docstrings): add docstrings to public method interfaces (#25)
- Resolves #18 Reviewed-on: #25
This commit was merged in pull request #25.
This commit is contained in:
@@ -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.
|
||||
* <p>
|
||||
* 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);
|
||||
|
||||
@@ -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> 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<Marker> getUserMarkers() {
|
||||
Optional<User> 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> 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.
|
||||
* <p>
|
||||
* Runs periodically (every 6 minutes) to purge markers left behind by streams that have ended.
|
||||
*/
|
||||
@Scheduled(fixedDelay = 360_000)
|
||||
@Transactional
|
||||
public void deleteOldMarkers() {
|
||||
|
||||
@@ -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<Stream> getActiveStream() {
|
||||
Optional<User> 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<Stream> 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.
|
||||
* <p>
|
||||
* 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() {
|
||||
|
||||
@@ -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<User> 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<User> 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<User> getUserByStreamKey(String streamKey) {
|
||||
return userRepository.findByStreamKey(streamKey);
|
||||
}
|
||||
|
||||
@@ -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<String> command, Consumer<String> 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<String> command) throws IOException, InterruptedException {
|
||||
return run(command, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an ffmpeg progress line and updates the tracker with the fraction completed.
|
||||
* <p>
|
||||
* 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()) {
|
||||
|
||||
@@ -27,6 +27,17 @@ public class CompressionService {
|
||||
this.commandRunner = commandRunner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compresses (and optionally trims/rescales) a video with ffmpeg on a background executor.
|
||||
* <p>
|
||||
* 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<CommandOutput> compress(Path inputFile,
|
||||
Path outputFile,
|
||||
|
||||
@@ -26,6 +26,14 @@ public class MetadataService {
|
||||
this.commandRunner = commandRunner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Probes a video file with ffprobe and returns its metadata (duration, dimensions, fps, size).
|
||||
* <p>
|
||||
* 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<ClipOptions> getVideoMetadata(Path file) {
|
||||
logger.info("Getting metadata for file {}", file);
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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<CommandOutput> remux(File inputFile,
|
||||
File outputFile,
|
||||
|
||||
@@ -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.
|
||||
* <p>
|
||||
* 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<CommandOutput> createThumbnail(Path inputFile, Path outputFile, Float timeInVideo) {
|
||||
logger.info("Creating thumbnail at {} seconds", timeInVideo);
|
||||
|
||||
Reference in New Issue
Block a user