@@ -6,4 +6,4 @@ logs | |||
dependency-reduced-pom.xml | |||
*~ | |||
*.log | |||
*.mp4 | |||
AssetManager.output.* |
@@ -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. | |||
@@ -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"); | |||
} |
@@ -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; | |||
@@ -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); } | |||
} | |||
} |
@@ -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) { | |||
@@ -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); | |||
@@ -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 | |||
} | |||
} | |||
] | |||