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