diff --git a/.gitignore b/.gitignore index fc4a95e..4ea8804 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ logs dependency-reduced-pom.xml *~ *.log -*.mp4 +AssetManager.output.* diff --git a/README.md b/README.md index 99bb639..6b18cf8 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,14 @@ # Javicle - a JSON Video Composition Language -Javicle is not a replacement for Final Cut Pro or even iMovie. +Javicle is a JSON DSL for ffmpeg transformations. -Javicle might be right for you if your video composition and manipulation needs are relatively simple -and you enjoy doing things with the command line and some JSON config instead of a GUI. +Describe your input assets and transformations in a JSON spec file, then run `jvcl spec-file` to +perform the transformations and produce output files. -This also give you the ability to track more of your workflow in source control - if you commit all -the original assets and the .jvcl files that describe how to create the output assets, you don't need -to save/archive the output assets anywhere. +If you like GUIs, Javicle is not for you. Javicle is not a replacement for Final Cut Pro or even iMovie. -Note, for those who want truly 100% re-creatable builds, you would also need to record the versions -of the various tools used (ffmpeg, etc) and reuse those same versions when recreating a build. This is -generally overkill though, since the options we use on the different tools have been stable for a while -and I see a low likelihood of a significant change in behavior, or a bug being introduced. +If you like CLIs, Javicle might be for you. If your video composition needs are relatively simple +and you enjoy capturing repeatable stuff in source control. In JVCL there are two main concepts: assets and operations. diff --git a/src/main/java/jvcl/main/JvclOptions.java b/src/main/java/jvcl/main/JvclOptions.java index aba0053..0c2fbc4 100644 --- a/src/main/java/jvcl/main/JvclOptions.java +++ b/src/main/java/jvcl/main/JvclOptions.java @@ -38,6 +38,6 @@ public class JvclOptions extends BaseMainOptions { public static final String OPT_SCRATCH_DIR = "-t"; public static final String LONGOPT_SCRATCH_DIR = "--temp-dir"; @Option(name=OPT_SCRATCH_DIR, aliases=LONGOPT_SCRATCH_DIR, usage=USAGE_SCRATCH_DIR) - @Getter @Setter private File scratchDir; + @Getter @Setter private File scratchDir = new File("/tmp"); } diff --git a/src/main/java/jvcl/model/JAsset.java b/src/main/java/jvcl/model/JAsset.java index bac540c..d1c80a6 100644 --- a/src/main/java/jvcl/model/JAsset.java +++ b/src/main/java/jvcl/model/JAsset.java @@ -30,7 +30,9 @@ public class JAsset { public static final JAsset NULL_ASSET = new JAsset().setName("~null asset~").setPath("/dev/null"); public static final String PREFIX_CLASSPATH = "classpath:"; - public JAsset(JAsset other) { copy(this, other); } + public static final String[] COPY_EXCLUDE_FIELDS = {"list"}; + + public JAsset(JAsset other) { copy(this, other, null, COPY_EXCLUDE_FIELDS); } @Getter @Setter private String name; @Getter @Setter private String path; diff --git a/src/main/java/jvcl/op/SplitOperation.java b/src/main/java/jvcl/op/SplitOperation.java index b65801a..28c1d17 100644 --- a/src/main/java/jvcl/op/SplitOperation.java +++ b/src/main/java/jvcl/op/SplitOperation.java @@ -14,17 +14,15 @@ import org.cobbzilla.util.handlebars.HandlebarsUtil; import java.io.File; import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.HashMap; import java.util.Map; import static jvcl.model.JAsset.json2asset; -import static org.cobbzilla.util.daemon.ZillaRuntime.big; +import static jvcl.service.Toolbox.getDuration; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; import static org.cobbzilla.util.io.FileUtil.abs; import static org.cobbzilla.util.json.JsonUtil.json; import static org.cobbzilla.util.system.CommandShell.execScript; -import static org.cobbzilla.util.time.TimeUtil.parseDuration; @Slf4j public class SplitOperation implements JOperator { @@ -42,6 +40,7 @@ public class SplitOperation implements JOperator { // if any format settings are missing, use settings from source output.mergeFormat(source.getFormat()); + assetManager.addOperationArrayAsset(output); // get format type final JFileExtension formatType = output.getFormat().getFileExtension(); @@ -50,8 +49,9 @@ public class SplitOperation implements JOperator { ctx.put("ffmpeg", toolbox.getFfmpeg()); ctx.put("source", source); final BigDecimal incr = config.getIntervalIncr(); + final BigDecimal endTime = config.getEndTime(source); for (BigDecimal i = config.getStartTime(); - i.compareTo(config.getEndTime(source)) < 0; + i.compareTo(endTime) < 0; i = i.add(incr)) { final File outfile = assetManager.assetPath(op, source, formatType, new Object[]{i, incr}); @@ -66,12 +66,13 @@ public class SplitOperation implements JOperator { ctx.put("startSeconds", i); ctx.put("endSeconds", i.add(incr)); final String script = HandlebarsUtil.apply(toolbox.getHandlebars(), SPLIT_TEMPLATE, ctx); + log.debug("operate: running script: "+script); final String scriptOutput = execScript(script); log.debug("operate: command output: "+scriptOutput); - output.addAsset(slice); + assetManager.addOperationAssetSlice(output, slice); } - assetManager.addOperationAsset(output); + log.info("operate: completed"); } @NoArgsConstructor @@ -80,13 +81,13 @@ public class SplitOperation implements JOperator { @Getter @Setter private String split; @Getter @Setter private String interval; - public BigDecimal getIntervalIncr() { return big(parseDuration(interval)).divide(big(1000), RoundingMode.UNNECESSARY); } + public BigDecimal getIntervalIncr() { return getDuration(interval); } @Getter @Setter private String start; - public BigDecimal getStartTime() { return empty(start) ? BigDecimal.ZERO : big(start); } + public BigDecimal getStartTime() { return empty(start) ? BigDecimal.ZERO : getDuration(start); } @Getter @Setter private String end; - public BigDecimal getEndTime(JAsset source) { return empty(end) ? source.duration() : big(end); } + public BigDecimal getEndTime(JAsset source) { return empty(end) ? source.duration() : getDuration(end); } } } diff --git a/src/main/java/jvcl/service/AssetManager.java b/src/main/java/jvcl/service/AssetManager.java index c3f1409..d17f128 100644 --- a/src/main/java/jvcl/service/AssetManager.java +++ b/src/main/java/jvcl/service/AssetManager.java @@ -68,12 +68,20 @@ public class AssetManager { public void addOperationAsset(JAsset asset) { if (asset == null || asset == NULL_ASSET) return; - if (asset.hasList()) { - for (JAsset a : asset.getList()) addOperationAsset(a); - } else { - final String name = checkName(asset); - assets.put(name, asset.init(this, toolbox)); - } + final String name = checkName(asset); + assets.put(name, asset.init(this, toolbox)); + } + + public void addOperationArrayAsset(JAsset asset) { + if (asset == null || asset == NULL_ASSET) return; + final String name = checkName(asset); + assets.put(name, asset); + } + + public void addOperationAssetSlice(JAsset asset, JAsset slice) { + if (!assets.containsKey(asset.getName())) die("asset not found: "+asset.getName()); + final JAsset found = assets.get(asset.getName()); + found.addAsset(slice.init(this, toolbox)); } public JAsset[] resolve(String[] assets) { diff --git a/src/main/java/jvcl/service/Toolbox.java b/src/main/java/jvcl/service/Toolbox.java index 3bc0a29..5f5677f 100644 --- a/src/main/java/jvcl/service/Toolbox.java +++ b/src/main/java/jvcl/service/Toolbox.java @@ -10,15 +10,19 @@ import org.cobbzilla.util.io.FileUtil; import org.cobbzilla.util.javascript.StandardJsEngine; import java.io.File; +import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import static org.cobbzilla.util.daemon.ZillaRuntime.*; +import static org.cobbzilla.util.daemon.ZillaRuntime.big; import static org.cobbzilla.util.io.FileUtil.abs; import static org.cobbzilla.util.io.FileUtil.replaceExt; import static org.cobbzilla.util.json.JsonUtil.FULL_MAPPER_ALLOW_UNKNOWN_FIELDS; import static org.cobbzilla.util.json.JsonUtil.json; import static org.cobbzilla.util.system.CommandShell.execScript; +import static org.cobbzilla.util.time.TimeUtil.parseDuration; @Slf4j public class Toolbox { @@ -26,6 +30,11 @@ public class Toolbox { public static final Toolbox DEFAULT_TOOLBOX = new Toolbox(); @Getter(lazy=true) private final Handlebars handlebars = initHandlebars(); + + public static BigDecimal getDuration(String t) { + return big(parseDuration(t)).divide(big(1000), RoundingMode.UNNECESSARY); + } + private Handlebars initHandlebars() { final Handlebars hbs = new Handlebars(new HandlebarsUtil(Toolbox.class.getSimpleName())); HandlebarsUtil.registerUtilityHelpers(hbs); diff --git a/src/test/resources/tests/test_split.json b/src/test/resources/tests/test_split.json index 27ee9bc..a63b503 100644 --- a/src/test/resources/tests/test_split.json +++ b/src/test/resources/tests/test_split.json @@ -14,10 +14,12 @@ "operations": [ { "operation": "split", // name of the operation - "creates": "vid1_split_%", // assets it creates, the '%' will be replaced with a counter + "creates": "vid1_splits", // assets it creates, will be an array asset "perform": { "split": "vid1", // split this source asset - "interval": "10s" // split every ten seconds + "interval": "10s", // split every ten seconds + "start": "65s", // start one minute and five seconds into the video + "end": "100s" // end 100 seconds into the video } } ]