Merge branch 'dev' into v4
This commit is contained in:
commit
a4c13fcf56
|
@ -1,10 +1,13 @@
|
||||||
package ctbrec.ui.settings;
|
package ctbrec.ui.settings;
|
||||||
|
|
||||||
import ctbrec.recorder.postprocessing.CreateContactSheet;
|
import static ctbrec.recorder.postprocessing.CreateContactSheet.*;
|
||||||
|
import static java.lang.Boolean.*;
|
||||||
|
|
||||||
import ctbrec.recorder.postprocessing.PostProcessor;
|
import ctbrec.recorder.postprocessing.PostProcessor;
|
||||||
import ctbrec.ui.settings.api.Category;
|
import ctbrec.ui.settings.api.Category;
|
||||||
import ctbrec.ui.settings.api.Preferences;
|
import ctbrec.ui.settings.api.Preferences;
|
||||||
import ctbrec.ui.settings.api.Setting;
|
import ctbrec.ui.settings.api.Setting;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.scene.control.ColorPicker;
|
import javafx.scene.control.ColorPicker;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
|
@ -15,18 +18,21 @@ public class CreateContactSheetPaneFactory extends AbstractPostProcessingPaneFac
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Preferences doCreatePostProcessorPane(PostProcessor pp) {
|
public Preferences doCreatePostProcessorPane(PostProcessor pp) {
|
||||||
SimpleStringProperty totalSize = new SimpleStringProperty(null, CreateContactSheet.TOTAL_SIZE, pp.getConfig().getOrDefault(CreateContactSheet.TOTAL_SIZE, "1920"));
|
SimpleStringProperty totalSize = new SimpleStringProperty(null, TOTAL_SIZE, pp.getConfig().getOrDefault(TOTAL_SIZE, "1920"));
|
||||||
SimpleStringProperty padding = new SimpleStringProperty(null, CreateContactSheet.PADDING, pp.getConfig().getOrDefault(CreateContactSheet.PADDING, "4"));
|
SimpleStringProperty padding = new SimpleStringProperty(null, PADDING, pp.getConfig().getOrDefault(PADDING, "4"));
|
||||||
SimpleStringProperty cols = new SimpleStringProperty(null, CreateContactSheet.COLS, pp.getConfig().getOrDefault(CreateContactSheet.COLS, "8"));
|
SimpleStringProperty cols = new SimpleStringProperty(null, COLS, pp.getConfig().getOrDefault(COLS, "8"));
|
||||||
SimpleStringProperty rows = new SimpleStringProperty(null, CreateContactSheet.ROWS, pp.getConfig().getOrDefault(CreateContactSheet.ROWS, "7"));
|
SimpleStringProperty rows = new SimpleStringProperty(null, ROWS, pp.getConfig().getOrDefault(ROWS, "7"));
|
||||||
SimpleStringProperty filename = new SimpleStringProperty(null, CreateContactSheet.FILENAME, pp.getConfig().getOrDefault(CreateContactSheet.FILENAME, "contactsheet.jpg"));
|
SimpleStringProperty filename = new SimpleStringProperty(null, FILENAME, pp.getConfig().getOrDefault(FILENAME, "contactsheet.jpg"));
|
||||||
background = new SimpleStringProperty(null, CreateContactSheet.BACKGROUND, pp.getConfig().getOrDefault(CreateContactSheet.BACKGROUND, "0x333333"));
|
background = new SimpleStringProperty(null, BACKGROUND, pp.getConfig().getOrDefault(BACKGROUND, "0x333333"));
|
||||||
|
SimpleBooleanProperty burnTimestamp = new SimpleBooleanProperty(null, BURN_IN_TIMESTAMP,
|
||||||
|
Boolean.valueOf(pp.getConfig().getOrDefault(BURN_IN_TIMESTAMP, TRUE.toString())));
|
||||||
properties.add(totalSize);
|
properties.add(totalSize);
|
||||||
properties.add(padding);
|
properties.add(padding);
|
||||||
properties.add(cols);
|
properties.add(cols);
|
||||||
properties.add(rows);
|
properties.add(rows);
|
||||||
properties.add(filename);
|
properties.add(filename);
|
||||||
properties.add(background);
|
properties.add(background);
|
||||||
|
properties.add(burnTimestamp);
|
||||||
|
|
||||||
Setting backgroundSetting = Setting.of("", background, "Hexadecimal value of the background color for the space between the thumbnails");
|
Setting backgroundSetting = Setting.of("", background, "Hexadecimal value of the background color for the space between the thumbnails");
|
||||||
Preferences prefs = Preferences.of(new MapPreferencesStorage(),
|
Preferences prefs = Preferences.of(new MapPreferencesStorage(),
|
||||||
|
@ -37,6 +43,7 @@ public class CreateContactSheetPaneFactory extends AbstractPostProcessingPaneFac
|
||||||
Setting.of("Rows", rows),
|
Setting.of("Rows", rows),
|
||||||
Setting.of("File Name", filename),
|
Setting.of("File Name", filename),
|
||||||
Setting.of("Background", createColorPicker(background.get())),
|
Setting.of("Background", createColorPicker(background.get())),
|
||||||
|
Setting.of("Timestamp (experimental)", burnTimestamp, "Burn in a timestamp on each thumb. Can be very slow on some systems."),
|
||||||
backgroundSetting
|
backgroundSetting
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -56,6 +56,8 @@ public class IoUtils {
|
||||||
if (!deletedAllFiles) {
|
if (!deletedAllFiles) {
|
||||||
throw new IOException("Couldn't delete all files in " + directory);
|
throw new IOException("Couldn't delete all files in " + directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Files.delete(directory.toPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long getDirectorySize(File dir) {
|
public static long getDirectorySize(File dir) {
|
||||||
|
|
|
@ -1,20 +1,31 @@
|
||||||
package ctbrec.recorder.postprocessing;
|
package ctbrec.recorder.postprocessing;
|
||||||
|
|
||||||
|
import static java.lang.Boolean.*;
|
||||||
|
|
||||||
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.Path;
|
||||||
|
import java.text.DecimalFormat;
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
|
import java.text.NumberFormat;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import javax.annotation.concurrent.ThreadSafe;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import ctbrec.Config;
|
import ctbrec.Config;
|
||||||
import ctbrec.OS;
|
import ctbrec.OS;
|
||||||
import ctbrec.Recording;
|
import ctbrec.Recording;
|
||||||
|
import ctbrec.io.IoUtils;
|
||||||
import ctbrec.recorder.FFmpeg;
|
import ctbrec.recorder.FFmpeg;
|
||||||
import ctbrec.recorder.RecordingManager;
|
import ctbrec.recorder.RecordingManager;
|
||||||
|
import ctbrec.recorder.download.ProcessExitedUncleanException;
|
||||||
|
|
||||||
|
@ThreadSafe
|
||||||
public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
|
public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(CreateContactSheet.class);
|
private static final Logger LOG = LoggerFactory.getLogger(CreateContactSheet.class);
|
||||||
|
@ -25,6 +36,7 @@ public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
|
||||||
public static final String ROWS = "rows";
|
public static final String ROWS = "rows";
|
||||||
public static final String BACKGROUND = "background";
|
public static final String BACKGROUND = "background";
|
||||||
public static final String FILENAME = "filename";
|
public static final String FILENAME = "filename";
|
||||||
|
public static final String BURN_IN_TIMESTAMP = "burn_in_timestamp";
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -40,15 +52,12 @@ public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
|
||||||
int rows = Integer.parseInt(getConfig().getOrDefault(ROWS, "7"));
|
int rows = Integer.parseInt(getConfig().getOrDefault(ROWS, "7"));
|
||||||
String color = getConfig().getOrDefault(BACKGROUND, "0x333333");
|
String color = getConfig().getOrDefault(BACKGROUND, "0x333333");
|
||||||
String filename = getConfig().getOrDefault(FILENAME, "contactsheet.jpg");
|
String filename = getConfig().getOrDefault(FILENAME, "contactsheet.jpg");
|
||||||
|
|
||||||
int thumbWidth = (int) ((totalWidth - (cols + 1) * padding) / (double)cols);
|
int thumbWidth = (int) ((totalWidth - (cols + 1) * padding) / (double)cols);
|
||||||
int numberOfThumbs = rows * cols;
|
|
||||||
long lengthInSeconds = rec.getLength().getSeconds();
|
|
||||||
double thumbnailInterval = lengthInSeconds / (double)numberOfThumbs;
|
|
||||||
|
|
||||||
MessageFormat mf = new MessageFormat("fps=1/{0},scale={4}:-1,tile={1}x{2}:color={6}:margin={3}:padding={3},scale={5}:-1", Locale.ENGLISH);
|
Path tempDir = createThumbnails(rec, config);
|
||||||
|
|
||||||
|
MessageFormat mf = new MessageFormat("scale={3}:-1,tile={0}x{1}:color={5}:margin={2}:padding={2},scale={4}:-1", Locale.ENGLISH);
|
||||||
String filterArg = mf.format(new Object[] {
|
String filterArg = mf.format(new Object[] {
|
||||||
thumbnailInterval,
|
|
||||||
cols, rows,
|
cols, rows,
|
||||||
padding,
|
padding,
|
||||||
Integer.toString(thumbWidth),
|
Integer.toString(thumbWidth),
|
||||||
|
@ -58,26 +67,18 @@ public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
|
||||||
File executionDir = rec.getPostProcessedFile().isDirectory() ? rec.getPostProcessedFile() : rec.getPostProcessedFile().getParentFile();
|
File executionDir = rec.getPostProcessedFile().isDirectory() ? rec.getPostProcessedFile() : rec.getPostProcessedFile().getParentFile();
|
||||||
File output = new File(executionDir, fillInPlaceHolders(filename, rec, config));
|
File output = new File(executionDir, fillInPlaceHolders(filename, rec, config));
|
||||||
|
|
||||||
File input = getInputFile(rec);
|
|
||||||
String[] args = {
|
String[] args = {
|
||||||
"-skip_frame",
|
"-y",
|
||||||
"nokey",
|
|
||||||
"-i",
|
"-i",
|
||||||
input.getCanonicalPath(),
|
tempDir.toAbsolutePath().toString() + "/image%03d.png",
|
||||||
"-vf",
|
"-vf",
|
||||||
filterArg,
|
filterArg,
|
||||||
"-an",
|
|
||||||
"-vsync",
|
|
||||||
"0",
|
|
||||||
"-qscale:v",
|
"-qscale:v",
|
||||||
"3",
|
"3",
|
||||||
"-y",
|
|
||||||
"-frames:v",
|
|
||||||
"1",
|
|
||||||
output.getCanonicalPath()
|
output.getCanonicalPath()
|
||||||
};
|
};
|
||||||
String[] cmdline = OS.getFFmpegCommand(args);
|
String[] cmdline = OS.getFFmpegCommand(args);
|
||||||
LOG.info("Executing {} in working directory {}", Arrays.toString(cmdline), executionDir);
|
LOG.debug("Executing {} in working directory {}", Arrays.toString(cmdline), executionDir);
|
||||||
File ffmpegLog = new File(System.getProperty("java.io.tmpdir"), "create_contact_sheet_" + rec.getId() + ".log");
|
File ffmpegLog = new File(System.getProperty("java.io.tmpdir"), "create_contact_sheet_" + rec.getId() + ".log");
|
||||||
FFmpeg ffmpeg = new FFmpeg.Builder()
|
FFmpeg ffmpeg = new FFmpeg.Builder()
|
||||||
.logOutput(config.getSettings().logFFmpegOutput)
|
.logOutput(config.getSettings().logFFmpegOutput)
|
||||||
|
@ -85,8 +86,68 @@ public class CreateContactSheet extends AbstractPlaceholderAwarePostProcessor {
|
||||||
.build();
|
.build();
|
||||||
ffmpeg.exec(cmdline, OS.getEnvironment(), executionDir);
|
ffmpeg.exec(cmdline, OS.getEnvironment(), executionDir);
|
||||||
int exitCode = ffmpeg.waitFor();
|
int exitCode = ffmpeg.waitFor();
|
||||||
|
if (exitCode != 0) {
|
||||||
|
throw new ProcessExitedUncleanException("FFmpeg exited unclean (" + exitCode + ") when stitching together the contactsheet");
|
||||||
|
}
|
||||||
rec.getAssociatedFiles().add(output.getCanonicalPath());
|
rec.getAssociatedFiles().add(output.getCanonicalPath());
|
||||||
return exitCode != 1;
|
IoUtils.deleteDirectory(tempDir.toFile());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path createThumbnails(Recording rec, Config config) throws IOException, InterruptedException {
|
||||||
|
Path tempDirectory = Files.createTempDirectory("ctbrec-contactsheet-" + rec.getModel().getSanitizedNamed());
|
||||||
|
File executionDir = rec.getPostProcessedFile().isDirectory() ? rec.getPostProcessedFile() : rec.getPostProcessedFile().getParentFile();
|
||||||
|
int cols = Integer.parseInt(getConfig().getOrDefault(COLS, "8"));
|
||||||
|
int rows = Integer.parseInt(getConfig().getOrDefault(ROWS, "7"));
|
||||||
|
int numberOfThumbs = rows * cols;
|
||||||
|
long lengthInSeconds = rec.getLength().getSeconds();
|
||||||
|
double thumbnailInterval = lengthInSeconds / (double)numberOfThumbs;
|
||||||
|
|
||||||
|
NumberFormat nf = new DecimalFormat("000");
|
||||||
|
for (int i = 0; i < numberOfThumbs; i++) {
|
||||||
|
double startTimeInSeconds = thumbnailInterval * i;
|
||||||
|
String tile = "image" + nf.format(i) + ".png";
|
||||||
|
String videoFilter = "scale=-1:720,select='eq(pict_type\\,I)'";
|
||||||
|
boolean includeTimestamp = getConfig().getOrDefault(BURN_IN_TIMESTAMP, TRUE.toString()).equals(TRUE.toString());
|
||||||
|
if (includeTimestamp) {
|
||||||
|
int hours = (int) (startTimeInSeconds / 3600);
|
||||||
|
int minutes = (int) (startTimeInSeconds % 3600 / 60);
|
||||||
|
int seconds = (int) (startTimeInSeconds % 60);
|
||||||
|
String timestamp = String.format("%02d\\:%02d\\:%02d", hours, minutes, seconds);
|
||||||
|
videoFilter += ",drawtext='text=" + timestamp
|
||||||
|
+ ":fontcolor=white:fontsize=48:box=1:boxcolor=black@0.5:boxborderw=5:x=(w-text_w-20):y=(h-text_h-20)'";
|
||||||
|
}
|
||||||
|
|
||||||
|
File input = getInputFile(rec);
|
||||||
|
File output = new File(tempDirectory.toFile(), tile);
|
||||||
|
String[] args = {
|
||||||
|
"-y",
|
||||||
|
"-skip_frame",
|
||||||
|
"nokey",
|
||||||
|
"-ss",
|
||||||
|
Double.toString(startTimeInSeconds),
|
||||||
|
"-i",
|
||||||
|
input.getCanonicalPath(),
|
||||||
|
"-vf",
|
||||||
|
videoFilter,
|
||||||
|
"-vframes",
|
||||||
|
"1",
|
||||||
|
output.getCanonicalPath()
|
||||||
|
};
|
||||||
|
String[] cmdline = OS.getFFmpegCommand(args);
|
||||||
|
LOG.info("Executing {} in working directory {}", Arrays.toString(cmdline), executionDir);
|
||||||
|
|
||||||
|
FFmpeg ffmpeg = new FFmpeg.Builder()
|
||||||
|
.logOutput(config.getSettings().logFFmpegOutput)
|
||||||
|
.logFile(new File(output.getParentFile(), output.getName() + ".log"))
|
||||||
|
.build();
|
||||||
|
ffmpeg.exec(cmdline, OS.getEnvironment(), executionDir);
|
||||||
|
int exitCode = ffmpeg.waitFor();
|
||||||
|
if (exitCode != 0) {
|
||||||
|
throw new ProcessExitedUncleanException("FFmpeg exited unclean (" + exitCode + ") for thumbnail " + i + ' ' + Arrays.toString(cmdline));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tempDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private File getInputFile(Recording rec) {
|
private File getInputFile(Recording rec) {
|
||||||
|
|
Loading…
Reference in New Issue