Finished ffmpeg implementation.
This commit is contained in:
parent
d66fa71ae2
commit
63550c880d
|
@ -2,6 +2,10 @@ package nl.andrewlalis.gymboardcdn;
|
||||||
|
|
||||||
import nl.andrewlalis.gymboardcdn.files.FileStorageService;
|
import nl.andrewlalis.gymboardcdn.files.FileStorageService;
|
||||||
import nl.andrewlalis.gymboardcdn.files.util.ULID;
|
import nl.andrewlalis.gymboardcdn.files.util.ULID;
|
||||||
|
import nl.andrewlalis.gymboardcdn.uploads.service.process.FfmpegThumbnailGenerator;
|
||||||
|
import nl.andrewlalis.gymboardcdn.uploads.service.process.FfmpegVideoProcessor;
|
||||||
|
import nl.andrewlalis.gymboardcdn.uploads.service.process.ThumbnailGenerator;
|
||||||
|
import nl.andrewlalis.gymboardcdn.uploads.service.process.VideoProcessor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
@ -11,6 +15,8 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
import org.springframework.web.filter.CorsFilter;
|
import org.springframework.web.filter.CorsFilter;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
|
@ -42,4 +48,19 @@ public class Config {
|
||||||
public FileStorageService fileStorageService() {
|
public FileStorageService fileStorageService() {
|
||||||
return new FileStorageService(ulid(), "cdn-files");
|
return new FileStorageService(ulid(), "cdn-files");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public VideoProcessor videoProcessor() {
|
||||||
|
return new FfmpegVideoProcessor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ThumbnailGenerator thumbnailGenerator() {
|
||||||
|
return new FfmpegThumbnailGenerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Executor videoProcessingExecutor() {
|
||||||
|
return Executors.newFixedThreadPool(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import nl.andrewlalis.gymboardcdn.files.FileStorageService;
|
||||||
import nl.andrewlalis.gymboardcdn.files.util.ULID;
|
import nl.andrewlalis.gymboardcdn.files.util.ULID;
|
||||||
import nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTask;
|
import nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTask;
|
||||||
import nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTaskRepository;
|
import nl.andrewlalis.gymboardcdn.uploads.model.VideoProcessingTaskRepository;
|
||||||
|
import nl.andrewlalis.gymboardcdn.uploads.service.process.ThumbnailGenerator;
|
||||||
|
import nl.andrewlalis.gymboardcdn.uploads.service.process.VideoProcessor;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
@ -13,8 +15,6 @@ import org.springframework.stereotype.Service;
|
||||||
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.time.Duration;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.Executor;
|
import java.util.concurrent.Executor;
|
||||||
|
@ -24,14 +24,22 @@ import java.util.concurrent.TimeUnit;
|
||||||
public class VideoProcessingService {
|
public class VideoProcessingService {
|
||||||
private static final Logger log = LoggerFactory.getLogger(VideoProcessingService.class);
|
private static final Logger log = LoggerFactory.getLogger(VideoProcessingService.class);
|
||||||
|
|
||||||
private final Executor taskExecutor;
|
private final Executor videoProcessingExecutor;
|
||||||
private final VideoProcessingTaskRepository taskRepo;
|
private final VideoProcessingTaskRepository taskRepo;
|
||||||
private final FileStorageService fileStorageService;
|
private final FileStorageService fileStorageService;
|
||||||
|
private final VideoProcessor videoProcessor;
|
||||||
|
private final ThumbnailGenerator thumbnailGenerator;
|
||||||
|
|
||||||
public VideoProcessingService(Executor taskExecutor, VideoProcessingTaskRepository taskRepo, FileStorageService fileStorageService) {
|
public VideoProcessingService(Executor videoProcessingExecutor,
|
||||||
this.taskExecutor = taskExecutor;
|
VideoProcessingTaskRepository taskRepo,
|
||||||
|
FileStorageService fileStorageService,
|
||||||
|
VideoProcessor videoProcessor,
|
||||||
|
ThumbnailGenerator thumbnailGenerator) {
|
||||||
|
this.videoProcessingExecutor = videoProcessingExecutor;
|
||||||
this.taskRepo = taskRepo;
|
this.taskRepo = taskRepo;
|
||||||
this.fileStorageService = fileStorageService;
|
this.fileStorageService = fileStorageService;
|
||||||
|
this.videoProcessor = videoProcessor;
|
||||||
|
this.thumbnailGenerator = thumbnailGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
|
@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
|
||||||
|
@ -40,7 +48,7 @@ public class VideoProcessingService {
|
||||||
for (var task : waitingTasks) {
|
for (var task : waitingTasks) {
|
||||||
log.info("Queueing processing of task {}.", task.getId());
|
log.info("Queueing processing of task {}.", task.getId());
|
||||||
updateTask(task, VideoProcessingTask.Status.IN_PROGRESS);
|
updateTask(task, VideoProcessingTask.Status.IN_PROGRESS);
|
||||||
taskExecutor.execute(() -> processVideo(task));
|
videoProcessingExecutor.execute(() -> processVideo(task));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,20 +76,20 @@ public class VideoProcessingService {
|
||||||
private void processVideo(VideoProcessingTask task) {
|
private void processVideo(VideoProcessingTask task) {
|
||||||
log.info("Started processing task {}.", task.getId());
|
log.info("Started processing task {}.", task.getId());
|
||||||
|
|
||||||
Path tempFilePath = fileStorageService.getStoragePathForFile(task.getUploadFileId());
|
Path uploadFile = fileStorageService.getStoragePathForFile(task.getUploadFileId());
|
||||||
if (Files.notExists(tempFilePath) || !Files.isReadable(tempFilePath)) {
|
if (Files.notExists(uploadFile) || !Files.isReadable(uploadFile)) {
|
||||||
log.error("Temp file {} doesn't exist or isn't readable.", tempFilePath);
|
log.error("Uploaded video file {} doesn't exist or isn't readable.", uploadFile);
|
||||||
updateTask(task, VideoProcessingTask.Status.FAILED);
|
updateTask(task, VideoProcessingTask.Status.FAILED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then begin running the actual FFMPEG processing.
|
Path videoFile = uploadFile.resolveSibling(task.getUploadFileId() + "-vid-out");
|
||||||
Path tempDir = tempFilePath.getParent();
|
Path thumbnailFile = uploadFile.resolveSibling(task.getUploadFileId() + "-thm-out");
|
||||||
Path ffmpegOutputFile = tempDir.resolve(task.getUploadFileId() + "-video-out");
|
|
||||||
Path ffmpegThumbnailOutputFile = tempDir.resolve(task.getUploadFileId() + "-thumbnail-out");
|
|
||||||
try {
|
try {
|
||||||
generateThumbnailWithFFMPEG(tempDir, tempFilePath, ffmpegThumbnailOutputFile);
|
log.info("Processing video for uploaded video file {}.", uploadFile.getFileName());
|
||||||
processVideoWithFFMPEG(tempDir, tempFilePath, ffmpegOutputFile);
|
videoProcessor.processVideo(uploadFile, videoFile);
|
||||||
|
log.info("Generating thumbnail for uploaded video file {}.", uploadFile.getFileName());
|
||||||
|
thumbnailGenerator.generateThumbnailImage(uploadFile, thumbnailFile);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
log.error("""
|
log.error("""
|
||||||
|
@ -90,8 +98,8 @@ public class VideoProcessingService {
|
||||||
Output file: {}
|
Output file: {}
|
||||||
Exception message: {}""",
|
Exception message: {}""",
|
||||||
task.getId(),
|
task.getId(),
|
||||||
tempFilePath,
|
uploadFile,
|
||||||
ffmpegOutputFile,
|
videoFile,
|
||||||
e.getMessage()
|
e.getMessage()
|
||||||
);
|
);
|
||||||
updateTask(task, VideoProcessingTask.Status.FAILED);
|
updateTask(task, VideoProcessingTask.Status.FAILED);
|
||||||
|
@ -100,8 +108,8 @@ public class VideoProcessingService {
|
||||||
|
|
||||||
// And finally, copy the output to the final location.
|
// And finally, copy the output to the final location.
|
||||||
try (
|
try (
|
||||||
var videoIn = Files.newInputStream(ffmpegOutputFile);
|
var videoIn = Files.newInputStream(videoFile);
|
||||||
var thumbnailIn = Files.newInputStream(ffmpegThumbnailOutputFile)
|
var thumbnailIn = Files.newInputStream(thumbnailFile)
|
||||||
) {
|
) {
|
||||||
// Save the video to a final file location.
|
// Save the video to a final file location.
|
||||||
var originalMetadata = fileStorageService.getMetadata(task.getUploadFileId());
|
var originalMetadata = fileStorageService.getMetadata(task.getUploadFileId());
|
||||||
|
@ -110,14 +118,14 @@ public class VideoProcessingService {
|
||||||
originalMetadata.mimeType(),
|
originalMetadata.mimeType(),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
fileStorageService.save(ULID.parseULID(task.getVideoFileId()), videoIn, metadata, Files.size(ffmpegOutputFile));
|
fileStorageService.save(ULID.parseULID(task.getVideoFileId()), videoIn, metadata, Files.size(videoFile));
|
||||||
// Save the thumbnail too.
|
// Save the thumbnail too.
|
||||||
FileMetadata thumbnailMetadata = new FileMetadata(
|
FileMetadata thumbnailMetadata = new FileMetadata(
|
||||||
"thumbnail.jpeg",
|
"thumbnail.jpeg",
|
||||||
"image/jpeg",
|
"image/jpeg",
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
fileStorageService.save(thumbnailIn, thumbnailMetadata, Files.size(ffmpegThumbnailOutputFile));
|
fileStorageService.save(thumbnailIn, thumbnailMetadata, Files.size(thumbnailFile));
|
||||||
updateTask(task, VideoProcessingTask.Status.COMPLETED);
|
updateTask(task, VideoProcessingTask.Status.COMPLETED);
|
||||||
log.info("Finished processing task {}.", task.getId());
|
log.info("Finished processing task {}.", task.getId());
|
||||||
|
|
||||||
|
@ -128,62 +136,15 @@ public class VideoProcessingService {
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
fileStorageService.delete(task.getUploadFileId());
|
fileStorageService.delete(task.getUploadFileId());
|
||||||
Files.deleteIfExists(ffmpegOutputFile);
|
Files.deleteIfExists(videoFile);
|
||||||
Files.deleteIfExists(ffmpegThumbnailOutputFile);
|
Files.deleteIfExists(thumbnailFile);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
log.error("Couldn't delete temporary FFMPEG output file: {}", ffmpegOutputFile);
|
log.error("Couldn't delete temporary output files for uploaded video {}", uploadFile);
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Uses the `ffmpeg` system command to process a raw input video and produce
|
|
||||||
* a compressed, reduced-size output video that's ready for usage in the
|
|
||||||
* application.
|
|
||||||
* @param dir The working directory.
|
|
||||||
* @param inFile The input file to read from.
|
|
||||||
* @param outFile The output file to write to. MUST have a ".mp4" extension.
|
|
||||||
* @throws IOException If a filesystem error occurs.
|
|
||||||
* @throws CommandFailedException If the ffmpeg command fails.
|
|
||||||
* @throws InterruptedException If the ffmpeg command is interrupted.
|
|
||||||
*/
|
|
||||||
private void processVideoWithFFMPEG(Path dir, Path inFile, Path outFile) throws IOException, InterruptedException {
|
|
||||||
Path tmpStdout = Files.createTempFile(dir, "stdout-", ".log");
|
|
||||||
Path tmpStderr = Files.createTempFile(dir, "stderr-", ".log");
|
|
||||||
final String[] command = {
|
|
||||||
"ffmpeg",
|
|
||||||
"-i", inFile.getFileName().toString(),
|
|
||||||
"-vf", "scale=640x480:flags=lanczos",
|
|
||||||
"-vcodec", "libx264",
|
|
||||||
"-crf", "28",
|
|
||||||
"-f", "mp4",
|
|
||||||
outFile.getFileName().toString()
|
|
||||||
};
|
|
||||||
|
|
||||||
long startSize = Files.size(inFile);
|
|
||||||
Instant startTime = Instant.now();
|
|
||||||
|
|
||||||
Process ffmpegProcess = new ProcessBuilder()
|
|
||||||
.command(command)
|
|
||||||
.redirectOutput(tmpStdout.toFile())
|
|
||||||
.redirectError(tmpStderr.toFile())
|
|
||||||
.directory(dir.toFile())
|
|
||||||
.start();
|
|
||||||
int result = ffmpegProcess.waitFor();
|
|
||||||
if (result != 0) throw new CommandFailedException(command, result, tmpStdout, tmpStderr);
|
|
||||||
|
|
||||||
long endSize = Files.size(outFile);
|
|
||||||
Duration dur = Duration.between(startTime, Instant.now());
|
|
||||||
double reductionFactor = startSize / (double) endSize;
|
|
||||||
String reductionFactorStr = String.format("%.3f%%", reductionFactor * 100);
|
|
||||||
log.info("Processed video from {} bytes to {} bytes in {} seconds, {} reduction.", startSize, endSize, dur.getSeconds(), reductionFactorStr);
|
|
||||||
|
|
||||||
// Delete the logs if everything was successful.
|
|
||||||
Files.deleteIfExists(tmpStdout);
|
|
||||||
Files.deleteIfExists(tmpStderr);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateTask(VideoProcessingTask task, VideoProcessingTask.Status status) {
|
private void updateTask(VideoProcessingTask task, VideoProcessingTask.Status status) {
|
||||||
task.setStatus(status);
|
task.setStatus(status);
|
||||||
taskRepo.saveAndFlush(task);
|
taskRepo.saveAndFlush(task);
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
package nl.andrewlalis.gymboardcdn.uploads.service.process;
|
||||||
|
|
||||||
|
import nl.andrewlalis.gymboardcdn.uploads.service.CommandFailedException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public abstract class FfmpegCommandExecutor {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(FfmpegCommandExecutor.class);
|
||||||
|
|
||||||
|
protected abstract String[] buildCommand(Path inputFile, Path outputFile);
|
||||||
|
|
||||||
|
public void run(String label, Path inputFile, Path outputFile) throws IOException {
|
||||||
|
String inputFilename = inputFile.getFileName().toString().strip();
|
||||||
|
Path stdout = inputFile.resolveSibling(inputFilename + "-ffmpeg-" + label + "-out.log");
|
||||||
|
Path stderr = inputFile.resolveSibling(inputFilename + "-ffmpeg-" + label + "-err.log");
|
||||||
|
String[] command = buildCommand(inputFile, outputFile);
|
||||||
|
Process process = new ProcessBuilder(buildCommand(inputFile, outputFile))
|
||||||
|
.redirectOutput(stdout.toFile())
|
||||||
|
.redirectError(stderr.toFile())
|
||||||
|
.start();
|
||||||
|
try {
|
||||||
|
int result = process.waitFor();
|
||||||
|
if (result != 0) {
|
||||||
|
throw new CommandFailedException(command, result, stdout, stderr);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new IOException("Interrupted while waiting for ffmpeg to finish.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to clean up output files when the command exited successfully.
|
||||||
|
try {
|
||||||
|
Files.deleteIfExists(stdout);
|
||||||
|
Files.deleteIfExists(stderr);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to delete output files after successful ffmpeg execution.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package nl.andrewlalis.gymboardcdn.uploads.service.process;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class FfmpegThumbnailGenerator extends FfmpegCommandExecutor implements ThumbnailGenerator {
|
||||||
|
@Override
|
||||||
|
public void generateThumbnailImage(Path videoInputFile, Path outputFilePath) throws IOException {
|
||||||
|
super.run("thm", videoInputFile, outputFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String[] buildCommand(Path inputFile, Path outputFile) {
|
||||||
|
return new String[]{
|
||||||
|
"ffmpeg",
|
||||||
|
"-i", inputFile.toString(),
|
||||||
|
"-vframes", "1",
|
||||||
|
outputFile.toString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
package nl.andrewlalis.gymboardcdn.uploads.service.process;
|
package nl.andrewlalis.gymboardcdn.uploads.service.process;
|
||||||
|
|
||||||
import nl.andrewlalis.gymboardcdn.uploads.service.CommandFailedException;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -10,38 +9,40 @@ import java.nio.file.Path;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
public class FfmpegVideoProcessor implements VideoProcessor {
|
public class FfmpegVideoProcessor extends FfmpegCommandExecutor implements VideoProcessor {
|
||||||
private static final Logger log = LoggerFactory.getLogger(FfmpegVideoProcessor.class);
|
private static final Logger log = LoggerFactory.getLogger(FfmpegVideoProcessor.class);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void processVideo(Path inputFilePath, Path outputFilePath) throws IOException {
|
public void processVideo(Path inputFilePath, Path outputFilePath) throws IOException {
|
||||||
String inputFilename = inputFilePath.getFileName().toString().strip();
|
Instant start = Instant.now();
|
||||||
Path stdoutFile = inputFilePath.resolveSibling(inputFilename + "-ffmpeg-video-stdout.log");
|
long inputFileSize = Files.size(inputFilePath);
|
||||||
Path stderrFile = inputFilePath.resolveSibling(inputFilename + "-ffmpeg-video-stderr.log");
|
|
||||||
final String[] command = {
|
super.run("vid", inputFilePath, outputFilePath);
|
||||||
|
|
||||||
|
Duration duration = Duration.between(start, Instant.now());
|
||||||
|
long outputFileSize = Files.size(outputFilePath);
|
||||||
|
double reductionFactor = inputFileSize / (double) outputFileSize;
|
||||||
|
double durationSeconds = duration.toMillis() / 1000.0;
|
||||||
|
log.info(
|
||||||
|
"Processed video {} from {} to {} bytes, {} reduction in {} seconds.",
|
||||||
|
inputFilePath.getFileName().toString(),
|
||||||
|
inputFileSize,
|
||||||
|
outputFileSize,
|
||||||
|
String.format("%.3f%%", reductionFactor),
|
||||||
|
String.format("%.3f", durationSeconds)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String[] buildCommand(Path inputFile, Path outputFile) {
|
||||||
|
return new String[]{
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-i", inputFilePath.toAbsolutePath().toString(),
|
"-i", inputFile.toString(),
|
||||||
"-vf", "scale=640x480:flags=lanczos",
|
"-vf", "scale=640x480:flags=lanczos",
|
||||||
"-vcodec", "libx264",
|
"-vcodec", "libx264",
|
||||||
"-crf", "28",
|
"-crf", "28",
|
||||||
"-f", "mp4",
|
"-f", "mp4",
|
||||||
outputFilePath.toAbsolutePath().toString()
|
outputFile.toString()
|
||||||
};
|
};
|
||||||
long startFileSize = Files.size(inputFilePath);
|
|
||||||
Instant startTime = Instant.now();
|
|
||||||
Process process = new ProcessBuilder(command)
|
|
||||||
.redirectOutput(stdoutFile.toAbsolutePath().toFile())
|
|
||||||
.redirectError(stderrFile.toAbsolutePath().toFile())
|
|
||||||
.start();
|
|
||||||
int result = process.waitFor();
|
|
||||||
if (result != 0) {
|
|
||||||
throw new CommandFailedException(command, result, stdoutFile, stderrFile);
|
|
||||||
}
|
|
||||||
long endFileSize = Files.size(outputFilePath);
|
|
||||||
Duration duration = Duration.between(startTime, Instant.now());
|
|
||||||
double reductionFactor = startFileSize / (double) endFileSize;
|
|
||||||
String reductionFactorStr = String.format("%.3f%%", reductionFactor * 100);
|
|
||||||
log.info("Processed video from {} bytes to {} bytes in {} seconds, {} reduction.", startSize, endSize, dur.getSeconds(), reductionFactorStr);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
package nl.andrewlalis.gymboardcdn.uploads.service.process;
|
package nl.andrewlalis.gymboardcdn.uploads.service.process;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
public interface ThumbnailGenerator {
|
public interface ThumbnailGenerator {
|
||||||
void generateThumbnailImage(Path videoInputFile, Path outputFilePath);
|
void generateThumbnailImage(Path videoInputFile, Path outputFilePath) throws IOException;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue