From 444e42c091826b56219a7921cd767b7489bdc7a0 Mon Sep 17 00:00:00 2001 From: MrLetsplay Date: Wed, 19 Feb 2025 23:32:14 +0100 Subject: [PATCH] Improve thumbnail creation --- .../me/mrletsplay/videobase/VideoBase.java | 1 + .../mrletsplay/videobase/rest/LibraryAPI.java | 2 +- .../videobase/util/ThumbnailCreator.java | 54 ++++++++++--------- 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/main/java/me/mrletsplay/videobase/VideoBase.java b/src/main/java/me/mrletsplay/videobase/VideoBase.java index f480591..f4bc1b0 100644 --- a/src/main/java/me/mrletsplay/videobase/VideoBase.java +++ b/src/main/java/me/mrletsplay/videobase/VideoBase.java @@ -85,6 +85,7 @@ public class VideoBase { private static void loadLibrary() { library = Library.load(Path.of(config.getLibraryPath()), config.isReadOnly()); ThumbnailCreator.clearCache(); + ThumbnailCreator.createThumbnails(); } } diff --git a/src/main/java/me/mrletsplay/videobase/rest/LibraryAPI.java b/src/main/java/me/mrletsplay/videobase/rest/LibraryAPI.java index 363afa8..355027e 100644 --- a/src/main/java/me/mrletsplay/videobase/rest/LibraryAPI.java +++ b/src/main/java/me/mrletsplay/videobase/rest/LibraryAPI.java @@ -157,7 +157,7 @@ public class LibraryAPI implements EndpointCollection { return; } - BufferedImage thumbnail = ThumbnailCreator.createThumbnail(video); + BufferedImage thumbnail = ThumbnailCreator.getThumbnail(video); if(thumbnail == null) { ctx.respond(HttpStatusCodes.NOT_FOUND_404, new TextResponse("No thumbnail found")); return; diff --git a/src/main/java/me/mrletsplay/videobase/util/ThumbnailCreator.java b/src/main/java/me/mrletsplay/videobase/util/ThumbnailCreator.java index eab0c79..183f455 100644 --- a/src/main/java/me/mrletsplay/videobase/util/ThumbnailCreator.java +++ b/src/main/java/me/mrletsplay/videobase/util/ThumbnailCreator.java @@ -8,8 +8,10 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import javax.imageio.ImageIO; @@ -19,33 +21,34 @@ import me.mrletsplay.videobase.library.Video; public class ThumbnailCreator { - private static Map cachedThumbnails = new HashMap<>(); + private static Map cachedThumbnails = new ConcurrentHashMap<>(); + private static ExecutorService executorService = Executors.newFixedThreadPool(10); public static void clearCache() { cachedThumbnails.clear(); } + public static void createThumbnails() { + for(Video v : VideoBase.getLibrary().getVideos()) { + executorService.submit(() -> createThumbnail(v)); + } + } + public static BufferedImage createThumbnail(Video video) { if(cachedThumbnails.containsKey(video.getId())) { return cachedThumbnails.get(video.getId()); } String cachePath = VideoBase.getConfig().getCachePath(); - Path thumbnailFile = null; + Path thumbnailFile = cachePath == null ? null : Path.of(cachePath).resolve("thumbnails").resolve(video.getId() + ".png").toAbsolutePath(); - if(cachePath != null) { - thumbnailFile = Path.of(cachePath).resolve("thumbnails").resolve(video.getId() + ".png").toAbsolutePath(); + if(thumbnailFile != null && Files.isRegularFile(thumbnailFile)) { + try(InputStream in = Files.newInputStream(thumbnailFile)) { + BufferedImage thumbnail = ImageIO.read(in); + if(thumbnail == null) return null; - try { - if(Files.exists(thumbnailFile)) { - try(InputStream in = Files.newInputStream(thumbnailFile)) { - BufferedImage thumbnail = ImageIO.read(in); - if(thumbnail == null) return null; - - cachedThumbnails.put(video.getId(), thumbnail); - return thumbnail; - } - } + cachedThumbnails.put(video.getId(), thumbnail); + return thumbnail; } catch (IOException e) { VideoBase.LOGGER.warn("Failed to load thumbnail", e); return null; @@ -54,8 +57,8 @@ public class ThumbnailCreator { ProcessBuilder ffmpegBuilder = new ProcessBuilder("ffmpeg", "-i", video.getPath().toAbsolutePath().toString(), - "-vf", "thumbnail", - "-vf", "scale=960:540:force_original_aspect_ratio=decrease,pad=960:540:-1:-1:color=black", + "-r", "0.5", + "-vf", "thumbnail,scale=960:540:force_original_aspect_ratio=decrease,pad=960:540:-1:-1:color=black", "-frames:v", "1", "-f", "image2pipe", "-vcodec", "png", @@ -69,18 +72,18 @@ public class ThumbnailCreator { ByteArrayOutputStream bOut = new ByteArrayOutputStream(); byte[] buf = new byte[1024]; int len; - while(System.currentTimeMillis() - start < 1000 - && (len = ffmpeg.getInputStream().read(buf)) > 0) { + while(System.currentTimeMillis() - start < 60000 + && (len = ffmpeg.getInputStream().read(buf)) != -1) { bOut.write(buf, 0, len); } - ffmpeg.waitFor(10, TimeUnit.SECONDS); + ffmpeg.waitFor(60, TimeUnit.SECONDS); if(ffmpeg.isAlive()) { - VideoBase.LOGGER.warn("ffmpeg didn't exit after 10 seconds, destroying"); + VideoBase.LOGGER.warn("Video " + video.getId() + ": ffmpeg didn't exit after 60 seconds, destroying"); ffmpeg.destroy(); - if(!ffmpeg.waitFor(10, TimeUnit.SECONDS)) { - VideoBase.LOGGER.warn("ffmpeg didn't exit 10 seconds after destroying, destroying forcibly"); + if(!ffmpeg.waitFor(60, TimeUnit.SECONDS)) { + VideoBase.LOGGER.warn("Video " + video.getId() + ": ffmpeg didn't exit 60 seconds after destroying, destroying forcibly"); ffmpeg.destroyForcibly(); } @@ -97,14 +100,17 @@ public class ThumbnailCreator { } } + VideoBase.LOGGER.debug("Created thumbnail for video at " + video.getPath()); cachedThumbnails.put(video.getId(), thumbnail); return thumbnail; } catch (InterruptedException | IOException e) { VideoBase.LOGGER.warn("Failed to create thumbnail", e); return null; } - } + public static BufferedImage getThumbnail(Video video) { + return cachedThumbnails.get(video.getId()); + } }