refactor(directories): switch from File to Path objs

This commit is contained in:
2026-06-23 20:53:24 +01:00
parent 8b49a10a8c
commit 5651aa4ce9
12 changed files with 143 additions and 210 deletions

View File

@@ -8,8 +8,6 @@ services:
POSTGRES_DB: ${POSTGRES_DB} POSTGRES_DB: ${POSTGRES_DB}
volumes: volumes:
- ${POSTGRES_DATA}:/var/lib/postgresql/data - ${POSTGRES_DATA}:/var/lib/postgresql/data
ports:
- "5432:5432"
backend: backend:
build: build:

View File

@@ -15,12 +15,6 @@ RUN apt-get update && \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
ENV POSTGRES_DATA=/app/db
ENV MEDIA_DATA=/app/data/media
ENV STREAM_DATA=/app/data/media/streams
VOLUME ["app/db", "app/data/media/streams", "/app/data/media"]
WORKDIR /app WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar COPY --from=builder /app/target/*.jar app.jar
EXPOSE 8080 EXPOSE 8080

View File

@@ -2,7 +2,7 @@ package com.ddf.vodsystem.dto;
import lombok.Data; import lombok.Data;
import java.io.File; import java.nio.file.Path;
import java.time.Instant; import java.time.Instant;
@Data @Data
@@ -12,7 +12,7 @@ public class Job {
private JobState state = JobState.READY; private JobState state = JobState.READY;
private String errorOutput; private String errorOutput;
private File download; private Path download;
private Instant createdAt = Instant.now(); private Instant createdAt = Instant.now();
public Job(String uuid) { public Job(String uuid) {

View File

@@ -7,6 +7,7 @@ import com.ddf.vodsystem.entities.*;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -200,8 +201,8 @@ public class ClipService {
*/ */
public Clip saveClip(ClipOptions options, public Clip saveClip(ClipOptions options,
User user, User user,
String videoPath, Path videoPath,
String thumbnailPath) { Path thumbnailPath) {
Clip clip = new Clip(); Clip clip = new Clip();
clip.setUser(user); clip.setUser(user);
clip.setTitle(options.getTitle() != null ? options.getTitle() : "Untitled Clip"); clip.setTitle(options.getTitle() != null ? options.getTitle() : "Untitled Clip");
@@ -212,23 +213,23 @@ public class ClipService {
clip.setFps(options.getFps()); clip.setFps(options.getFps());
clip.setDuration(options.getDuration() - options.getStartPoint()); clip.setDuration(options.getDuration() - options.getStartPoint());
clip.setFileSize(options.getFileSize()); clip.setFileSize(options.getFileSize());
clip.setVideoPath(videoPath); clip.setVideoPath(videoPath.toString());
clip.setThumbnailPath(thumbnailPath); clip.setThumbnailPath(thumbnailPath.toString());
return clipRepository.save(clip); return clipRepository.save(clip);
} }
public void persistClip(String title, public void persistClip(String title,
String description, String description,
User user, User user,
File clipFile, Path clipFile,
String fileName) { String fileName) {
File newClipFile; Path newClipFile;
File thumbnailFile; Path thumbnailFile;
// Move temp file from temp dir to output dir // Move temp file from temp dir to output dir
try { try {
newClipFile = directoryService.getUserClipsFile(user.getId(), fileName); newClipFile = directoryService.getClipsDir(user.getId()).resolve(fileName);
thumbnailFile = directoryService.getUserThumbnailsFile(user.getId(), fileName + ".png"); thumbnailFile = directoryService.getThumbnailsDir(user.getId()).resolve(fileName + ".png");
directoryService.copyFile(clipFile, newClipFile); directoryService.copyFile(clipFile, newClipFile);
} catch (IOException e) { } catch (IOException e) {
throw new StorageException("Failed to move clip from temporary directory to output directory", e); throw new StorageException("Failed to move clip from temporary directory to output directory", e);
@@ -255,7 +256,7 @@ public class ClipService {
clipMetadata.setTitle(title); clipMetadata.setTitle(title);
clipMetadata.setDescription(description); clipMetadata.setDescription(description);
Clip clip = saveClip(clipMetadata, user, newClipFile.getAbsolutePath(), thumbnailFile.getAbsolutePath()); Clip clip = saveClip(clipMetadata, user, newClipFile, thumbnailFile);
logger.info("Clip created successfully with ID: {}", clip.getId()); logger.info("Clip created successfully with ID: {}", clip.getId());
} }

View File

@@ -10,7 +10,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.util.stream.Stream;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -20,135 +20,108 @@ public class DirectoryService {
private static final Logger logger = org.slf4j.LoggerFactory.getLogger(DirectoryService.class); private static final Logger logger = org.slf4j.LoggerFactory.getLogger(DirectoryService.class);
@Value("${storage.outputs}") @Value("${media.users}")
private String outputDir; private String usersDir;
@Value("${STREAM_DATA}") @Value("${media.streams}")
private String streamDir; private String streamsDir;
@Value("${storage.temp.inputs}") @Value("${media.temp.inputs}")
private String tempInputsDir; private String tempInputsDir;
@Value("${storage.temp.outputs}") @Value("${media.temp.outputs}")
private String tempOutputsDir; private String tempOutputsDir;
private static final long TEMP_DIR_TIME_LIMIT = 3 * 60 * 60 * (long) 1000; // 3 hours private static final long TEMP_DIR_TIME_LIMIT = 3 * 60 * 60 * (long) 1000; // 3 hours
private static final long TEMP_DIR_CLEANUP_RATE = 30 * 60 * (long) 1000; // 30 minutes private static final long TEMP_DIR_CLEANUP_RATE = 30 * 60 * (long) 1000; // 30 minutes
public File getTempInputFile(String filename) { private static final String VODS_DIR_NAME = "vods";
String dir = tempInputsDir + File.separator + filename; private static final String CLIPS_DIR_NAME = "clips";
return new File(dir); private static final String THUMBNAIL_DIR_NAME = "thumbnails";
public Path getTempInputDir() {
return Path.of(tempInputsDir);
} }
public File getTempOutputFile(String filename) { public Path getTempOutputDir() {
String dir = tempOutputsDir + File.separator + filename; return Path.of(tempOutputsDir);
return new File(dir);
} }
public File getUserFolder(Long userId) throws IOException { public Path getUserDir(Long userId) throws IOException {
if (userId == null) { Path userDir = Path.of(usersDir + File.separator + userId);
throw new IllegalArgumentException("User ID cannot be null or empty"); Files.createDirectories(userDir);
return userDir;
} }
String dir = outputDir + File.separator + userId; public Path getVodsDir(Long userId) throws IOException {
createDirectory(dir); Path vodDir = getUserDir(userId).resolve(VODS_DIR_NAME);
return new File(dir); Files.createDirectories(vodDir);
return vodDir;
} }
public File getUserClipsFile(Long userId, String filename) throws IOException { public Path getClipsDir(Long userId) throws IOException {
if (filename == null || filename.isEmpty()) { Path clipsDir = getUserDir(userId).resolve(CLIPS_DIR_NAME);
throw new IllegalArgumentException("File name cannot be null or empty"); Files.createDirectories(clipsDir);
return clipsDir;
} }
File userDir = getUserFolder(userId); public Path getThumbnailsDir(Long userId) throws IOException {
String fullPath = userDir.getAbsolutePath() + File.separator + "clips" + File.separator + filename; Path thumbnailsDir = getUserDir(userId).resolve(THUMBNAIL_DIR_NAME);
File file = new File(fullPath); Files.createDirectories(thumbnailsDir);
return thumbnailsDir;
createDirectory(file.getParent());
return file;
} }
public File getVodFile(Long userId, String filename) throws IOException { public Path getStreamDir(String streamKey) throws IOException {
if (filename == null || filename.isEmpty()) { Path streamFolder = Path.of(streamsDir, streamKey);
throw new IllegalArgumentException("File name cannot be null or empty"); Files.createDirectories(streamFolder);
return streamFolder;
} }
String userDir = getUserFolder(userId).getAbsolutePath(); public void saveMultipartFile(Path path, MultipartFile multipartFile) throws IOException {
File file = new File(userDir + File.separator + "vods" + File.separator + filename); Files.createDirectories(path.getParent());
createDirectory(file.getParent()); Files.copy(multipartFile.getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
return file;
} }
public File getStreamDir(String streamKey) throws IOException { public void copyFile(Path source, Path target) throws IOException {
File file = new File(streamDir + File.separator + streamKey); Files.createDirectories(target.getParent());
createDirectory(file.getAbsolutePath()); Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
return file; logger.info("Copied file from {} to {}", source, target);
}
public File getUserThumbnailsFile(Long userId, String filename) throws IOException {
if (filename == null || filename.isEmpty()) {
throw new IllegalArgumentException("File name cannot be null or empty");
}
String userDir = getUserFolder(userId).getAbsolutePath();
File file = new File(userDir + File.separator + "thumbnails" + File.separator + filename);
createDirectory(file.getParent());
return file;
}
public void saveMultipartFile(File file, MultipartFile multipartFile) throws IOException {
createDirectory(file.getParent());
Path filePath = Paths.get(file.getAbsolutePath());
Files.copy(multipartFile.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
}
public void copyFile(File source, File target) throws IOException {
Path sourcePath = Paths.get(source.getAbsolutePath());
Path destPath = Paths.get(target.getAbsolutePath());
Files.createDirectories(destPath.getParent());
Files.copy(sourcePath, destPath, StandardCopyOption.REPLACE_EXISTING);
logger.info("Copied file from {} to {}", sourcePath, destPath);
}
public void cutFile(File source, File target) throws IOException {
copyFile(source, target);
Files.deleteIfExists(source.toPath());
logger.info("Deleted source file: {}", source.getAbsolutePath());
}
private void createDirectory(String dir) throws IOException {
// Create the directory if it doesn't exist
Path outputPath = Paths.get(dir);
if (Files.notExists(outputPath)) {
Files.createDirectories(outputPath);
logger.info("Created directory: {}", outputPath);
}
} }
private void cleanUpDirectory(String dir) throws IOException { private void cleanUpDirectory(String dir) throws IOException {
File file = new File(dir); Path dirPath = Path.of(dir);
File[] files = file.listFiles();
if (files == null) { if (Files.notExists(dirPath)) {
logger.warn("No files found in directory: {}", dir); logger.warn("No files found in directory: {}", dir);
return; return;
} }
for (File f : files){ try (Stream<Path> files = Files.list(dirPath)) {
if (f.isFile() && f.lastModified() < (System.currentTimeMillis() - TEMP_DIR_TIME_LIMIT)) { files.filter(Files::isRegularFile)
Files.delete(f.toPath()); .filter(p -> {
try {
return Files.getLastModifiedTime(p).toMillis()
< System.currentTimeMillis() - TEMP_DIR_TIME_LIMIT;
} catch (IOException e) {
logger.warn("Could not read last modified time for {}", p);
return false;
} }
})
.forEach(p -> {
try {
Files.delete(p);
} catch (IOException e) {
logger.warn("Could not delete file {}", p);
}
});
} }
} }
@PostConstruct @PostConstruct
public void createDirectoriesIfNotExist() throws IOException { public void createDirectoriesIfNotExist() throws IOException {
createDirectory(tempInputsDir); Files.createDirectories(Path.of(tempInputsDir));
createDirectory(tempOutputsDir); Files.createDirectories(Path.of(tempOutputsDir));
createDirectory(outputDir); Files.createDirectories(Path.of(usersDir));
} }
@Scheduled(fixedRate = TEMP_DIR_CLEANUP_RATE) @Scheduled(fixedRate = TEMP_DIR_CLEANUP_RATE)

View File

@@ -10,12 +10,14 @@ import com.ddf.vodsystem.services.media.StreamActionsService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service @Service
public class MediaService { public class MediaService {
@@ -49,8 +51,8 @@ public class MediaService {
Optional<User> user = userService.getLoggedInUser(); Optional<User> user = userService.getLoggedInUser();
String filename = job.getUuid() + ".mp4"; String filename = job.getUuid() + ".mp4";
File tempInputFile = directoryService.getTempInputFile(filename); Path tempInputFile = directoryService.getTempInputDir().resolve(filename);
File tempOutputFile = directoryService.getTempOutputFile(filename); Path tempOutputFile = directoryService.getTempOutputDir().resolve(filename);
directoryService.saveMultipartFile(tempInputFile, file); directoryService.saveMultipartFile(tempInputFile, file);
job.setState(JobState.PROCESSING); job.setState(JobState.PROCESSING);
@@ -107,8 +109,8 @@ public class MediaService {
.orElseThrow(() -> new NotAuthenticated("User is not authenticated")); .orElseThrow(() -> new NotAuthenticated("User is not authenticated"));
Job job = jobRegistryService.generateJob(); Job job = jobRegistryService.generateJob();
File streamDirectory = directoryService.getStreamDir(user.getStreamKey()); Path streamDirectory = directoryService.getStreamDir(user.getStreamKey());
List<File> fileSegments = getSegmentsInRange(streamDirectory.getAbsolutePath(), startTime, endTime); List<Path> fileSegments = getSegmentsInRange(streamDirectory, startTime, endTime);
if (fileSegments.isEmpty()) { if (fileSegments.isEmpty()) {
throw new IllegalArgumentException("No stream segments found in the given time range"); throw new IllegalArgumentException("No stream segments found in the given time range");
@@ -119,7 +121,7 @@ public class MediaService {
float duration = (endTime.toEpochMilli() - startTime.toEpochMilli()) / 1000f; float duration = (endTime.toEpochMilli() - startTime.toEpochMilli()) / 1000f;
job.setState(JobState.PROCESSING); job.setState(JobState.PROCESSING);
File outputFile = directoryService.getVodFile(user.getId(), UUID.randomUUID() + ".mp4"); Path outputFile = directoryService.getVodsDir(user.getId()).resolve(UUID.randomUUID() + ".mp4");
streamActionsService.saveSection( streamActionsService.saveSection(
fileSegments, fileSegments,
trimOffset, trimOffset,
@@ -167,8 +169,8 @@ public class MediaService {
.orElseThrow(() -> new NotAuthenticated("User is not authenticated")); .orElseThrow(() -> new NotAuthenticated("User is not authenticated"));
Job job = jobRegistryService.generateJob(); Job job = jobRegistryService.generateJob();
File streamDirectory = directoryService.getStreamDir(user.getStreamKey()); Path streamDirectory = directoryService.getStreamDir(user.getStreamKey());
List<File> fileSegments = getSegmentsInRange(streamDirectory.getAbsolutePath(), startTime, endTime); List<Path> fileSegments = getSegmentsInRange(streamDirectory, startTime, endTime);
if (fileSegments.isEmpty()) { if (fileSegments.isEmpty()) {
throw new IllegalArgumentException("No stream segments found in the given time range"); throw new IllegalArgumentException("No stream segments found in the given time range");
@@ -177,7 +179,7 @@ public class MediaService {
long firstSegmentMs = parseTimestampMs(fileSegments.getFirst()); long firstSegmentMs = parseTimestampMs(fileSegments.getFirst());
float trimOffset = Math.max(0f, (startTime.toEpochMilli() - firstSegmentMs) / 1000f); float trimOffset = Math.max(0f, (startTime.toEpochMilli() - firstSegmentMs) / 1000f);
File outputFile = directoryService.getTempOutputFile(UUID.randomUUID() + ".mp4"); Path outputFile = directoryService.getTempOutputDir().resolve(UUID.randomUUID() + ".mp4");
streamActionsService.saveSection(fileSegments, trimOffset, duration, outputFile, job.getProgressTracker()) streamActionsService.saveSection(fileSegments, trimOffset, duration, outputFile, job.getProgressTracker())
.thenRun(() -> { .thenRun(() -> {
job.setDownload(outputFile); job.setDownload(outputFile);
@@ -188,7 +190,7 @@ public class MediaService {
"", "",
user, user,
outputFile, outputFile,
outputFile.getName() outputFile.getFileName().toString()
); );
} }
).exceptionally(ex -> { ).exceptionally(ex -> {
@@ -201,25 +203,24 @@ public class MediaService {
return job; return job;
} }
private List<File> getSegmentsInRange(String streamDirectory, Instant startTime, Instant endTime) { private List<Path> getSegmentsInRange(Path streamDirectory, Instant startTime, Instant endTime) throws IOException {
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 startMs = startTime.toEpochMilli();
long endMs = endTime.toEpochMilli(); long endMs = endTime.toEpochMilli();
return Arrays.stream(tsFiles) try (Stream<Path> files = Files.list(streamDirectory)) {
.filter(f -> { return files
long segMs = parseTimestampMs(f); .filter(p -> p.getFileName().toString().endsWith(".ts"))
.filter(p -> {
long segMs = parseTimestampMs(p);
return segMs < endMs && (segMs + HLS_SEGMENT_LENGTH * 1000) > startMs; return segMs < endMs && (segMs + HLS_SEGMENT_LENGTH * 1000) > startMs;
}) })
.sorted(Comparator.comparingLong(this::parseTimestampMs)) .sorted(Comparator.comparingLong(this::parseTimestampMs))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
}
private long parseTimestampMs(File file) { private long parseTimestampMs(Path path) {
String name = file.getName().replace(".ts", ""); String name = path.getFileName().toString().replace(".ts", "");
long value = Long.parseLong(name); long value = Long.parseLong(name);
// nginx hls_fragment_naming system uses seconds; convert to ms // nginx hls_fragment_naming system uses seconds; convert to ms
return value < 1_000_000_000_000L ? value * 1000L : value; return value < 1_000_000_000_000L ? value * 1000L : value;

View File

@@ -8,8 +8,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -26,14 +26,18 @@ public class CompressionService {
private final Pattern timePattern = Pattern.compile("out_time_ms=(\\d+)"); private final Pattern timePattern = Pattern.compile("out_time_ms=(\\d+)");
@Async("ffmpegTaskExecutor") @Async("ffmpegTaskExecutor")
public CompletableFuture<CommandOutput> compress(File inputFile, public CompletableFuture<CommandOutput> compress(Path inputFile,
File outputFile, Path outputFile,
ClipOptions clipOptions, ClipOptions clipOptions,
ProgressTracker progress ProgressTracker progress
) throws IOException, InterruptedException { ) throws IOException, InterruptedException {
logger.info("Compressing video from {} to {}", inputFile.getAbsolutePath(), outputFile.getAbsolutePath()); logger.info("Compressing video from {} to {}", inputFile.toAbsolutePath(), outputFile.toAbsolutePath());
List<String> command = buildCommand(inputFile, outputFile, clipOptions); List<String> command = buildCommand(
inputFile,
outputFile,
clipOptions
);
CommandOutput result = CommandRunner.run(command, line -> setProgress(line, progress, clipOptions.getDuration())); CommandOutput result = CommandRunner.run(command, line -> setProgress(line, progress, clipOptions.getDuration()));
progress.markComplete(); progress.markComplete();
@@ -93,14 +97,14 @@ public class CompressionService {
return command; return command;
} }
private List<String> buildInputs(File inputFile, Float startPoint, Float length) { private List<String> buildInputs(Path inputFile, Float startPoint, Float length) {
List<String> command = new ArrayList<>(); List<String> command = new ArrayList<>();
command.add("-ss"); command.add("-ss");
command.add(startPoint.toString()); command.add(startPoint.toString());
command.add("-i"); command.add("-i");
command.add(inputFile.getAbsolutePath()); command.add(inputFile.toAbsolutePath().toString());
command.add("-t"); command.add("-t");
command.add(Float.toString(length)); command.add(Float.toString(length));
@@ -108,7 +112,7 @@ public class CompressionService {
return command; return command;
} }
private List<String> buildCommand(File inputFile, File outputFile, ClipOptions clipOptions) { private List<String> buildCommand(Path inputFile, Path outputFile, ClipOptions clipOptions) {
List<String> command = new ArrayList<>(); List<String> command = new ArrayList<>();
command.add("ffmpeg"); command.add("ffmpeg");
command.add("-progress"); command.add("-progress");
@@ -126,7 +130,7 @@ public class CompressionService {
} }
// Output file // Output file
command.add(outputFile.getAbsolutePath()); command.add(outputFile.toAbsolutePath().toString());
return command; return command;
} }
} }

View File

@@ -10,8 +10,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future; import java.util.concurrent.Future;
@@ -21,8 +21,8 @@ public class MetadataService {
private static final Logger logger = LoggerFactory.getLogger(MetadataService.class); private static final Logger logger = LoggerFactory.getLogger(MetadataService.class);
@Async("ffmpegTaskExecutor") @Async("ffmpegTaskExecutor")
public Future<ClipOptions> getVideoMetadata(File file) { public Future<ClipOptions> getVideoMetadata(Path file) {
logger.info("Getting metadata for file {}", file.getAbsolutePath()); logger.info("Getting metadata for file {}", file);
List<String> command = List.of( List<String> command = List.of(
"ffprobe", "ffprobe",
@@ -30,7 +30,7 @@ public class MetadataService {
"-print_format", "json", "-print_format", "json",
"-show_format", "-select_streams", "-show_format", "-select_streams",
"v:0", "-show_entries", "stream=duration,width,height,r_frame_rate:format=size,duration", "v:0", "-show_entries", "stream=duration,width,height,r_frame_rate:format=size,duration",
"-i", file.getAbsolutePath() "-i", file.toAbsolutePath().toString()
); );
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
@@ -51,48 +51,6 @@ public class MetadataService {
} }
} }
public void validateMetadata(ClipOptions inputFileMetadata, ClipOptions outputFileMetadata) {
Float start = outputFileMetadata.getStartPoint();
Float duration = outputFileMetadata.getDuration();
Float fileSize = outputFileMetadata.getFileSize();
Integer width = outputFileMetadata.getWidth();
Integer height = outputFileMetadata.getHeight();
Float fps = outputFileMetadata.getFps();
if (start == null) {
outputFileMetadata.setStartPoint(0f);
}
if (duration == null) {
outputFileMetadata.setDuration(inputFileMetadata.getDuration());
}
if (start != null && start < 0) {
throw new IllegalArgumentException("Start point cannot be negative");
}
if (duration != null && duration < 0) {
throw new IllegalArgumentException("Duration cannot be negative");
}
if (fileSize != null && fileSize < 100) {
throw new IllegalArgumentException("File size cannot be less than 100kb");
}
if (width != null && width < 1) {
throw new IllegalArgumentException("Width cannot be less than 1");
}
if (height != null && height < 1) {
throw new IllegalArgumentException("Height cannot be less than 1");
}
if (fps != null && fps < 1) {
throw new IllegalArgumentException("FPS cannot be less than 1");
}
}
private ClipOptions parseVideoMetadata(JsonNode node) { private ClipOptions parseVideoMetadata(JsonNode node) {
ClipOptions metadata = new ClipOptions(); ClipOptions metadata = new ClipOptions();
metadata.setStartPoint(0f); metadata.setStartPoint(0f);

View File

@@ -6,7 +6,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.File; import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -47,16 +47,16 @@ public class StreamActionsService {
* failure * failure
*/ */
@Async("ffmpegTaskExecutor") @Async("ffmpegTaskExecutor")
public CompletableFuture<File> saveSection( public CompletableFuture<Path> saveSection(
List<File> segments, List<Path> segments,
float trimOffset, float trimOffset,
float duration, float duration,
File outputFile, Path outputFile,
ProgressTracker progressTracker ProgressTracker progressTracker
) { ) {
try { try {
String concatInput = segments.stream() String concatInput = segments.stream()
.map(File::getAbsolutePath) .map(p -> p.toAbsolutePath().toString())
.collect(Collectors.joining("|")); .collect(Collectors.joining("|"));
List<String> command = List.of( List<String> command = List.of(
@@ -66,16 +66,16 @@ public class StreamActionsService {
"-ss", String.valueOf(trimOffset), "-ss", String.valueOf(trimOffset),
"-t", String.valueOf(duration), "-t", String.valueOf(duration),
"-c", "copy", "-c", "copy",
outputFile.getAbsolutePath() outputFile.toAbsolutePath().toString()
); );
logger.info("Saving section with ({} segments) to '{}'", logger.info("Saving section with ({} segments) to '{}'",
segments.size(), outputFile.getAbsolutePath()); segments.size(), outputFile);
CommandRunner.run(command, line -> CommandRunner.setProgress(line, progressTracker, duration)); CommandRunner.run(command, line -> CommandRunner.setProgress(line, progressTracker, duration));
return CompletableFuture.completedFuture(outputFile); return CompletableFuture.completedFuture(outputFile);
} catch (Exception e) { } catch (Exception e) {
logger.error("Failed to save section to '{}'", outputFile.getAbsolutePath(), e); logger.error("Failed to save section to '{}'", outputFile, e);
return CompletableFuture.failedFuture(e); return CompletableFuture.failedFuture(e);
} }
} }

View File

@@ -5,8 +5,8 @@ import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path;
import java.util.List; import java.util.List;
@Service @Service
@@ -14,15 +14,15 @@ public class ThumbnailService {
private static final Logger logger = LoggerFactory.getLogger(ThumbnailService.class); private static final Logger logger = LoggerFactory.getLogger(ThumbnailService.class);
@Async("ffmpegTaskExecutor") @Async("ffmpegTaskExecutor")
public void createThumbnail(File inputFile, File outputFile, Float timeInVideo) throws IOException, InterruptedException { public void createThumbnail(Path inputFile, Path outputFile, Float timeInVideo) throws IOException, InterruptedException {
logger.info("Creating thumbnail at {} seconds", timeInVideo); logger.info("Creating thumbnail at {} seconds", timeInVideo);
List<String> command = List.of( List<String> command = List.of(
"ffmpeg", "ffmpeg",
"-ss", timeInVideo.toString(), "-ss", timeInVideo.toString(),
"-i", inputFile.getAbsolutePath(), "-i", inputFile.toAbsolutePath().toString(),
"-frames:v", "1", "-frames:v", "1",
outputFile.getAbsolutePath() outputFile.toAbsolutePath().toString()
); );
CommandRunner.run(command); CommandRunner.run(command);

View File

@@ -1 +1,6 @@
spring.docker.compose.file=docker-compose.dev.yml spring.docker.compose.file=docker-compose.dev.yml
media.temp.inputs=./video/inputs
media.temp.outputs=./videos/outputs
media.users=${MEDIA_DATA}
media.streams=${STREAM_DATA}

View File

@@ -3,9 +3,6 @@ spring.application.name=vodSystem
# VODs # VODs
spring.servlet.multipart.max-file-size=2GB spring.servlet.multipart.max-file-size=2GB
spring.servlet.multipart.max-request-size=2GB spring.servlet.multipart.max-request-size=2GB
storage.temp.inputs=videos/inputs/
storage.temp.outputs=videos/outputs/
storage.outputs=${MEDIA_DATA}
# Database # Database
spring.datasource.url=${POSTGRES_URL} spring.datasource.url=${POSTGRES_URL}
@@ -21,15 +18,17 @@ spring.sql.init.schema-locations=classpath:db/schema.sql
spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID} spring.security.oauth2.client.registration.google.client-id=${GOOGLE_CLIENT_ID}
spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET} spring.security.oauth2.client.registration.google.client-secret=${GOOGLE_CLIENT_SECRET}
spring.security.oauth2.client.registration.google.scope=profile,email spring.security.oauth2.client.registration.google.scope=profile,email
jwt.secret.key=${JWT_SECRET_KEY} jwt.secret.key=${JWT_SECRET_KEY}
jwt.expiration=3600000 jwt.expiration=3600000
# Frontend # Frontend
frontend.url=${FRONTEND_URL} frontend.url=${FRONTEND_URL}
# Stream HLS # Paths
storage.stream=${STREAM_DATA} media.temp.inputs=/tmp/inputs/
media.temp.outputs=/tmp/outputs/
media.users=/app/media/
media.streams=/app/stream/
## Server Configuration ## Server Configuration
server.servlet.session.timeout=30m server.servlet.session.timeout=30m