From dcd3df5d0c05f2efc66c104df6674f00ed9795e1 Mon Sep 17 00:00:00 2001 From: 0xb00bface <0xboobface@gmail.com> Date: Wed, 30 Sep 2020 16:02:58 +0200 Subject: [PATCH] Implement contact sheet creation --- .../CreateContactSheetPaneFactory.java | 60 +++++++++++++ .../settings/PostProcessingDialogFactory.java | 2 + .../ui/settings/PostProcessingStepPanel.java | 4 +- .../postprocessing/CreateContactSheet.java | 89 ++++++++++++++++++- 4 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 client/src/main/java/ctbrec/ui/settings/CreateContactSheetPaneFactory.java diff --git a/client/src/main/java/ctbrec/ui/settings/CreateContactSheetPaneFactory.java b/client/src/main/java/ctbrec/ui/settings/CreateContactSheetPaneFactory.java new file mode 100644 index 00000000..6f837260 --- /dev/null +++ b/client/src/main/java/ctbrec/ui/settings/CreateContactSheetPaneFactory.java @@ -0,0 +1,60 @@ +package ctbrec.ui.settings; + +import ctbrec.recorder.postprocessing.CreateContactSheet; +import ctbrec.recorder.postprocessing.PostProcessor; +import ctbrec.ui.settings.api.Category; +import ctbrec.ui.settings.api.Preferences; +import ctbrec.ui.settings.api.Setting; +import javafx.beans.property.SimpleStringProperty; +import javafx.scene.control.ColorPicker; +import javafx.scene.paint.Color; + +public class CreateContactSheetPaneFactory extends AbstractPostProcessingPaneFactory { + + private SimpleStringProperty background; + + @Override + public Preferences doCreatePostProcessorPane(PostProcessor pp) { + SimpleStringProperty totalSize = new SimpleStringProperty(null, CreateContactSheet.TOTAL_SIZE, pp.getConfig().getOrDefault(CreateContactSheet.TOTAL_SIZE, "1920")); + SimpleStringProperty padding = new SimpleStringProperty(null, CreateContactSheet.PADDING, pp.getConfig().getOrDefault(CreateContactSheet.PADDING, "4")); + SimpleStringProperty cols = new SimpleStringProperty(null, CreateContactSheet.COLS, pp.getConfig().getOrDefault(CreateContactSheet.COLS, "8")); + SimpleStringProperty rows = new SimpleStringProperty(null, CreateContactSheet.ROWS, pp.getConfig().getOrDefault(CreateContactSheet.ROWS, "7")); + SimpleStringProperty filename = new SimpleStringProperty(null, CreateContactSheet.FILENAME, pp.getConfig().getOrDefault(CreateContactSheet.FILENAME, "contactsheet.jpg")); + background = new SimpleStringProperty(null, CreateContactSheet.BACKGROUND, pp.getConfig().getOrDefault(CreateContactSheet.BACKGROUND, "0x333333")); + properties.add(totalSize); + properties.add(padding); + properties.add(cols); + properties.add(rows); + properties.add(filename); + properties.add(background); + + Setting backgroundSetting = Setting.of("", background, "Hexadecimal value of the background color for the space between the thumbnails"); + Preferences prefs = Preferences.of(new MapPreferencesStorage(), + Category.of(pp.getName(), + Setting.of("Total Width", totalSize, "Total width of the generated contact sheet"), + Setting.of("Padding", padding, "Padding between the thumbnails"), + Setting.of("Columns", cols ), + Setting.of("Rows", rows), + Setting.of("File Name", filename), + Setting.of("Background", createColorPicker(background.get())), + backgroundSetting + ) + ); + + try { + // hide the background color input field, because we use a color picker instead + backgroundSetting.getGui().setVisible(false); + } catch (Exception e) { + // hiding the background color input field didn't work, that's ok + } + + return prefs; + } + + private ColorPicker createColorPicker(String hexColor) { + Color preselection = Color.web(hexColor); + ColorPicker colorPicker = new ColorPicker(preselection); + colorPicker.setOnAction(e -> background.set(colorPicker.getValue().toString())); + return colorPicker; + } +} diff --git a/client/src/main/java/ctbrec/ui/settings/PostProcessingDialogFactory.java b/client/src/main/java/ctbrec/ui/settings/PostProcessingDialogFactory.java index c9657160..a0649f3f 100644 --- a/client/src/main/java/ctbrec/ui/settings/PostProcessingDialogFactory.java +++ b/client/src/main/java/ctbrec/ui/settings/PostProcessingDialogFactory.java @@ -7,6 +7,7 @@ import java.util.Map; import java.util.Optional; import ctbrec.Config; +import ctbrec.recorder.postprocessing.CreateContactSheet; import ctbrec.recorder.postprocessing.DeleteTooShort; import ctbrec.recorder.postprocessing.Move; import ctbrec.recorder.postprocessing.PostProcessor; @@ -28,6 +29,7 @@ public class PostProcessingDialogFactory { ppToDialogMap.put(Rename.class, RenamerPaneFactory.class); ppToDialogMap.put(Move.class, MoverPaneFactory.class); ppToDialogMap.put(DeleteTooShort.class, DeleteTooShortPaneFactory.class); + ppToDialogMap.put(CreateContactSheet.class, CreateContactSheetPaneFactory.class); } private PostProcessingDialogFactory() { diff --git a/client/src/main/java/ctbrec/ui/settings/PostProcessingStepPanel.java b/client/src/main/java/ctbrec/ui/settings/PostProcessingStepPanel.java index 3a659888..6e75ccd3 100644 --- a/client/src/main/java/ctbrec/ui/settings/PostProcessingStepPanel.java +++ b/client/src/main/java/ctbrec/ui/settings/PostProcessingStepPanel.java @@ -7,6 +7,7 @@ import java.util.Optional; import ctbrec.Config; import ctbrec.recorder.postprocessing.Copy; +import ctbrec.recorder.postprocessing.CreateContactSheet; import ctbrec.recorder.postprocessing.DeleteOriginal; import ctbrec.recorder.postprocessing.DeleteTooShort; import ctbrec.recorder.postprocessing.Move; @@ -42,7 +43,8 @@ public class PostProcessingStepPanel extends GridPane { Script.class, DeleteOriginal.class, DeleteTooShort.class, - RemoveKeepFile.class + RemoveKeepFile.class, + CreateContactSheet.class }; // @formatter: on ListView stepListView; diff --git a/common/src/main/java/ctbrec/recorder/postprocessing/CreateContactSheet.java b/common/src/main/java/ctbrec/recorder/postprocessing/CreateContactSheet.java index d28ed652..c13afd87 100644 --- a/common/src/main/java/ctbrec/recorder/postprocessing/CreateContactSheet.java +++ b/common/src/main/java/ctbrec/recorder/postprocessing/CreateContactSheet.java @@ -1,14 +1,33 @@ package ctbrec.recorder.postprocessing; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.Files; +import java.text.MessageFormat; +import java.util.Arrays; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import ctbrec.Config; -import ctbrec.NotImplementedExcetion; +import ctbrec.OS; import ctbrec.Recording; +import ctbrec.io.StreamRedirectThread; import ctbrec.recorder.RecordingManager; public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor { + private static final Logger LOG = LoggerFactory.getLogger(CreateContactSheet.class); + + public static final String TOTAL_SIZE = "total_size"; + public static final String PADDING = "padding"; + public static final String COLS = "columns"; + public static final String ROWS = "rows"; + public static final String BACKGROUND = "background"; + public static final String FILENAME = "filename"; + + @Override public String getName() { return "create contact sheet"; @@ -16,6 +35,72 @@ public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor { @Override public void postprocess(Recording rec, RecordingManager recordingManager, Config config) throws IOException, InterruptedException { - throw new NotImplementedExcetion(); + int totalWidth = Integer.parseInt(getConfig().getOrDefault(TOTAL_SIZE, "1920")); + int padding = Integer.parseInt(getConfig().getOrDefault(PADDING, "4")); + int cols = Integer.parseInt(getConfig().getOrDefault(COLS, "8")); + int rows = Integer.parseInt(getConfig().getOrDefault(ROWS, "7")); + String color = getConfig().getOrDefault(BACKGROUND, "0x333333"); + String filename = getConfig().getOrDefault(FILENAME, "contactsheet.jpg"); + + int thumbWidth = (int) ((totalWidth - (cols + 1) * padding) / (double)cols); + int numberOfThumbs = rows * cols; + long lengthInSeconds = rec.getLength().getSeconds(); + double thumbnailInterval = lengthInSeconds / (double)numberOfThumbs; + + String filterArg = MessageFormat.format("fps=1/{0},scale={4}:-1,tile={1}x{2}:color={6}:margin={3}:padding={3},scale={5}:-1", + thumbnailInterval, + cols, rows, + padding, + Integer.toString(thumbWidth), + Integer.toString(totalWidth), + color); + File executionDir = rec.getPostProcessedFile().isDirectory() ? rec.getPostProcessedFile() : rec.getPostProcessedFile().getParentFile(); + File output = new File(executionDir, filename); + + File input = getInputFile(rec); + String[] args = { + "-skip_frame", + "nokey", + "-i", + input.getCanonicalPath(), + "-vf", + filterArg, + "-an", + "-vsync", + "0", + "-qscale:v", + "3", + output.getCanonicalPath() + }; + String[] cmdline = OS.getFFmpegCommand(args); + LOG.info("Executing {} in working directory {}", Arrays.toString(cmdline), executionDir); + Process ffmpeg = Runtime.getRuntime().exec(cmdline, new String[0], executionDir); + int exitCode = 1; + File ffmpegLog = File.createTempFile("create_contact_sheet_" + input.getName(), ".log"); + try (FileOutputStream mergeLogStream = new FileOutputStream(ffmpegLog)) { + Thread stdout = new Thread(new StreamRedirectThread(ffmpeg.getInputStream(), mergeLogStream)); + Thread stderr = new Thread(new StreamRedirectThread(ffmpeg.getErrorStream(), mergeLogStream)); + stdout.start(); + stderr.start(); + exitCode = ffmpeg.waitFor(); + LOG.debug("FFmpeg exited with code {}", exitCode); + stdout.join(); + stderr.join(); + mergeLogStream.flush(); + } + if (exitCode != 1) { + if (ffmpegLog.exists()) { + Files.delete(ffmpegLog.toPath()); + } + } + } + + private File getInputFile(Recording rec) { + if (rec.isSingleFile()) { + return rec.getPostProcessedFile(); + } else { + // TODO this has to be changed to support other playlist types like DASH + return new File(rec.getPostProcessedFile(), "playlist.m3u8"); + } } }