@@ -6,4 +6,4 @@ logs | |||||
dependency-reduced-pom.xml | dependency-reduced-pom.xml | ||||
*~ | *~ | ||||
*.log | *.log | ||||
*.mp4 | |||||
AssetManager.output.* |
@@ -1,18 +1,14 @@ | |||||
# Javicle - a JSON Video Composition Language | # 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. | In JVCL there are two main concepts: assets and operations. | ||||
@@ -38,6 +38,6 @@ public class JvclOptions extends BaseMainOptions { | |||||
public static final String OPT_SCRATCH_DIR = "-t"; | public static final String OPT_SCRATCH_DIR = "-t"; | ||||
public static final String LONGOPT_SCRATCH_DIR = "--temp-dir"; | public static final String LONGOPT_SCRATCH_DIR = "--temp-dir"; | ||||
@Option(name=OPT_SCRATCH_DIR, aliases=LONGOPT_SCRATCH_DIR, usage=USAGE_SCRATCH_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"); | |||||
} | } |
@@ -30,7 +30,9 @@ public class JAsset { | |||||
public static final JAsset NULL_ASSET = new JAsset().setName("~null asset~").setPath("/dev/null"); | public static final JAsset NULL_ASSET = new JAsset().setName("~null asset~").setPath("/dev/null"); | ||||
public static final String PREFIX_CLASSPATH = "classpath:"; | 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 name; | ||||
@Getter @Setter private String path; | @Getter @Setter private String path; | ||||
@@ -14,17 +14,15 @@ import org.cobbzilla.util.handlebars.HandlebarsUtil; | |||||
import java.io.File; | import java.io.File; | ||||
import java.math.BigDecimal; | import java.math.BigDecimal; | ||||
import java.math.RoundingMode; | |||||
import java.util.HashMap; | import java.util.HashMap; | ||||
import java.util.Map; | import java.util.Map; | ||||
import static jvcl.model.JAsset.json2asset; | 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.daemon.ZillaRuntime.empty; | ||||
import static org.cobbzilla.util.io.FileUtil.abs; | import static org.cobbzilla.util.io.FileUtil.abs; | ||||
import static org.cobbzilla.util.json.JsonUtil.json; | import static org.cobbzilla.util.json.JsonUtil.json; | ||||
import static org.cobbzilla.util.system.CommandShell.execScript; | import static org.cobbzilla.util.system.CommandShell.execScript; | ||||
import static org.cobbzilla.util.time.TimeUtil.parseDuration; | |||||
@Slf4j | @Slf4j | ||||
public class SplitOperation implements JOperator { | public class SplitOperation implements JOperator { | ||||
@@ -42,6 +40,7 @@ public class SplitOperation implements JOperator { | |||||
// if any format settings are missing, use settings from source | // if any format settings are missing, use settings from source | ||||
output.mergeFormat(source.getFormat()); | output.mergeFormat(source.getFormat()); | ||||
assetManager.addOperationArrayAsset(output); | |||||
// get format type | // get format type | ||||
final JFileExtension formatType = output.getFormat().getFileExtension(); | final JFileExtension formatType = output.getFormat().getFileExtension(); | ||||
@@ -50,8 +49,9 @@ public class SplitOperation implements JOperator { | |||||
ctx.put("ffmpeg", toolbox.getFfmpeg()); | ctx.put("ffmpeg", toolbox.getFfmpeg()); | ||||
ctx.put("source", source); | ctx.put("source", source); | ||||
final BigDecimal incr = config.getIntervalIncr(); | final BigDecimal incr = config.getIntervalIncr(); | ||||
final BigDecimal endTime = config.getEndTime(source); | |||||
for (BigDecimal i = config.getStartTime(); | for (BigDecimal i = config.getStartTime(); | ||||
i.compareTo(config.getEndTime(source)) < 0; | |||||
i.compareTo(endTime) < 0; | |||||
i = i.add(incr)) { | i = i.add(incr)) { | ||||
final File outfile = assetManager.assetPath(op, source, formatType, new Object[]{i, 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("startSeconds", i); | ||||
ctx.put("endSeconds", i.add(incr)); | ctx.put("endSeconds", i.add(incr)); | ||||
final String script = HandlebarsUtil.apply(toolbox.getHandlebars(), SPLIT_TEMPLATE, ctx); | final String script = HandlebarsUtil.apply(toolbox.getHandlebars(), SPLIT_TEMPLATE, ctx); | ||||
log.debug("operate: running script: "+script); | log.debug("operate: running script: "+script); | ||||
final String scriptOutput = execScript(script); | final String scriptOutput = execScript(script); | ||||
log.debug("operate: command output: "+scriptOutput); | log.debug("operate: command output: "+scriptOutput); | ||||
output.addAsset(slice); | |||||
assetManager.addOperationAssetSlice(output, slice); | |||||
} | } | ||||
assetManager.addOperationAsset(output); | |||||
log.info("operate: completed"); | |||||
} | } | ||||
@NoArgsConstructor | @NoArgsConstructor | ||||
@@ -80,13 +81,13 @@ public class SplitOperation implements JOperator { | |||||
@Getter @Setter private String split; | @Getter @Setter private String split; | ||||
@Getter @Setter private String interval; | @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; | @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; | @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); } | |||||
} | } | ||||
} | } |
@@ -68,12 +68,20 @@ public class AssetManager { | |||||
public void addOperationAsset(JAsset asset) { | public void addOperationAsset(JAsset asset) { | ||||
if (asset == null || asset == NULL_ASSET) return; | 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) { | public JAsset[] resolve(String[] assets) { | ||||
@@ -10,15 +10,19 @@ import org.cobbzilla.util.io.FileUtil; | |||||
import org.cobbzilla.util.javascript.StandardJsEngine; | import org.cobbzilla.util.javascript.StandardJsEngine; | ||||
import java.io.File; | import java.io.File; | ||||
import java.math.BigDecimal; | |||||
import java.math.RoundingMode; | |||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.concurrent.ConcurrentHashMap; | import java.util.concurrent.ConcurrentHashMap; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | 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.abs; | ||||
import static org.cobbzilla.util.io.FileUtil.replaceExt; | 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.FULL_MAPPER_ALLOW_UNKNOWN_FIELDS; | ||||
import static org.cobbzilla.util.json.JsonUtil.json; | import static org.cobbzilla.util.json.JsonUtil.json; | ||||
import static org.cobbzilla.util.system.CommandShell.execScript; | import static org.cobbzilla.util.system.CommandShell.execScript; | ||||
import static org.cobbzilla.util.time.TimeUtil.parseDuration; | |||||
@Slf4j | @Slf4j | ||||
public class Toolbox { | public class Toolbox { | ||||
@@ -26,6 +30,11 @@ public class Toolbox { | |||||
public static final Toolbox DEFAULT_TOOLBOX = new Toolbox(); | public static final Toolbox DEFAULT_TOOLBOX = new Toolbox(); | ||||
@Getter(lazy=true) private final Handlebars handlebars = initHandlebars(); | @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() { | private Handlebars initHandlebars() { | ||||
final Handlebars hbs = new Handlebars(new HandlebarsUtil(Toolbox.class.getSimpleName())); | final Handlebars hbs = new Handlebars(new HandlebarsUtil(Toolbox.class.getSimpleName())); | ||||
HandlebarsUtil.registerUtilityHelpers(hbs); | HandlebarsUtil.registerUtilityHelpers(hbs); | ||||
@@ -14,10 +14,12 @@ | |||||
"operations": [ | "operations": [ | ||||
{ | { | ||||
"operation": "split", // name of the operation | "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": { | "perform": { | ||||
"split": "vid1", // split this source asset | "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 | |||||
} | } | ||||
} | } | ||||
] | ] | ||||