@@ -20,14 +20,12 @@ With JVCL, you'd create this spec: | |||
{ | |||
"assets": [ {"name": "src", "path": "/tmp/my/source.mp4"} ], | |||
"operations": [{ | |||
"operation": "split", | |||
"creates": "src_splits", | |||
"perform": { | |||
"operation": "split", | |||
"creates": "src_split_files", | |||
"split": "src", | |||
"interval": "10s", | |||
"start": "10s", | |||
"end": "130s" | |||
} | |||
}] | |||
} | |||
``` | |||
@@ -100,53 +98,44 @@ Here is a complex example using multiple assets and 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 | |||
"perform": { | |||
"split": "vid1", // split this source asset | |||
"interval": "10s" // split every ten seconds | |||
} | |||
"split": "vid1", // split this source asset | |||
"interval": "10s" // split every ten seconds | |||
}, | |||
{ | |||
"operation": "concat", // name of the operation | |||
"operation": "concat", // name of the operation, | |||
"creates": "recombined_vid1", // assets it creates, the '%' will be replaced with a counter | |||
"perform": { | |||
"concat": ["vid1_split"] // recombine all split assets | |||
} | |||
"concat": ["vid1_split"] // recombine all split assets | |||
}, | |||
{ | |||
"operation": "concat", // name of the operation | |||
"operation": "concat", // name of the operation, | |||
"creates": "combined_vid", // asset it creates, can be referenced later | |||
"perform": { | |||
"concat": ["vid1", "vid2"] // operation-specific: this says, concatenate these named assets | |||
} | |||
"concat": ["vid1", "vid2"] // operation-specific: this says, concatenate these named assets | |||
}, | |||
{ | |||
"operation": "concat", // name of the operation | |||
"operation": "concat", // name of the operation, | |||
"creates": "combined_vid", // the asset it creates, can be referenced later | |||
"perform": { | |||
"concat": ["vid1", "vid2"] // operation-specific: this says, concatenate these named assets | |||
} | |||
"concat": ["vid1", "vid2"] // operation-specific: this says, concatenate these named assets | |||
}, | |||
{ | |||
"operation": "overlay", // name of the operation | |||
"creates": "overlay1", // asset it creates | |||
"perform": { | |||
"source": "combined_vid1", // main video asset | |||
"overlay": "vid2", // overlay this video on the main video | |||
"offset": "30", // when (on the main video timeline) to begin showing the overlay. default is 0 (beginning) | |||
"overlayStart": "0", // when (on the overlay video timeline) to begin playback on the overlay. default is 0 (beginning) | |||
"overlayEnd": "0", // when (on the overlay video timeline) to end playback on the overlay. default is to play the whole overlay | |||
"operation": "overlay", // name of the operation, | |||
"creates": { | |||
"name": "overlay1", // asset it creates | |||
"width": "1920", // output width in pixels. default is source width | |||
"height": "1024" // output height in pixes. default is source height | |||
}, | |||
"main": "combined_vid1", // main video asset | |||
"startTime": "30", // when (on the main video timeline) to begin showing the overlay. default is 0 (beginning) | |||
"endTime": "60", // when (on the main video timeline) to stop showing the overlay. default is to play the entire overlay | |||
"overlay": { | |||
"source": "vid2", // overlay this video on the main video | |||
"startTime": "0", // when (on the overlay video timeline) to begin playback on the overlay. default is 0 (beginning) | |||
"endTime": "0", // when (on the overlay video timeline) to end playback on the overlay. default is to play the entire overlay | |||
"width": "overlay.width / 2", // how wide the overlay will be, in pixels. default is the full overlay width, or maintain aspect ratio if height was set | |||
"height": "", // how tall the overlay will be, in pixels. default is the full overlay height, or maintain aspect ratio if width was set | |||
"x": "source.width/2", // horizontal overlay position on main video. default is 0 | |||
"y": "source.height/2", // vertical overlay position on main video. default is 0 | |||
"outputWidth": "1920", // output width in pixels. default is source width | |||
"outputHeight": "1024" // output height in pixes. default is source height | |||
"height": "source.height", // how tall the overlay will be, in pixels. default is the full overlay height, or maintain aspect ratio if width was set | |||
"x": "source.width / 2", // horizontal overlay position on main video. default is 0 | |||
"y": "source.height / 2" // vertical overlay position on main video. default is 0 | |||
} | |||
} | |||
] | |||
@@ -41,6 +41,7 @@ javicle is available under the Apache License, version 2: http://www.apache.org/ | |||
<logback.version>1.2.3</logback.version> | |||
<handlebars.version>4.2.0</handlebars.version> | |||
<junit.version>4.13.1</junit.version> | |||
<cglib.version>3.3.0</cglib.version> | |||
</properties> | |||
<profiles> | |||
@@ -146,6 +147,12 @@ javicle is available under the Apache License, version 2: http://www.apache.org/ | |||
</exclusions> | |||
</dependency> | |||
<dependency> | |||
<groupId>cglib</groupId> | |||
<artifactId>cglib</artifactId> | |||
<version>${cglib.version}</version> | |||
</dependency> | |||
<dependency> | |||
<groupId>junit</groupId> | |||
<artifactId>junit</artifactId> | |||
@@ -9,11 +9,11 @@ import org.kohsuke.args4j.Option; | |||
import java.io.File; | |||
import static jvcl.service.Toolbox.JSON_MAPPER; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.readStdin; | |||
import static org.cobbzilla.util.io.FileUtil.abs; | |||
import static org.cobbzilla.util.io.FileUtil.toStringOrDie; | |||
import static org.cobbzilla.util.json.JsonUtil.FULL_MAPPER_ALLOW_COMMENTS; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
@Slf4j | |||
@@ -26,12 +26,19 @@ public class JvclOptions extends BaseMainOptions { | |||
@Getter @Setter private File specFile; | |||
public JSpec getSpec() { | |||
final String json; | |||
if (specFile != null && !specFile.getName().equals("-")) { | |||
if (!specFile.exists()) return die("File not found: "+abs(specFile)); | |||
return json(toStringOrDie(specFile), JSpec.class, FULL_MAPPER_ALLOW_COMMENTS); | |||
if (!specFile.exists() || !specFile.canRead()) return die("File not found or unreadable: "+abs(specFile)); | |||
json = toStringOrDie(specFile); | |||
} else { | |||
log.info("reading JVCL spec from stdin..."); | |||
json = readStdin(); | |||
} | |||
try { | |||
return json(json, JSpec.class, JSON_MAPPER); | |||
} catch (Exception e) { | |||
return die("getSpec: invalid spec: "+specFile); | |||
} | |||
log.info("reading JVCL spec from stdin..."); | |||
return json(readStdin(), JSpec.class); | |||
} | |||
public static final String USAGE_SCRATCH_DIR = "Scratch directory. Default is to create a temp directory under /tmp"; | |||
@@ -1,25 +1,47 @@ | |||
package jvcl.model; | |||
import com.fasterxml.jackson.annotation.JsonTypeInfo; | |||
import com.fasterxml.jackson.databind.JsonNode; | |||
import jvcl.operation.exec.ExecBase; | |||
import lombok.Getter; | |||
import lombok.NoArgsConstructor; | |||
import lombok.Setter; | |||
import lombok.experimental.Accessors; | |||
import lombok.extern.slf4j.Slf4j; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
import static jvcl.service.json.JOperationFactory.getOperationExecClass; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.hashOf; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
import static org.cobbzilla.util.reflect.ReflectionUtil.instantiate; | |||
@NoArgsConstructor @Accessors(chain=true) | |||
public class JOperation { | |||
@NoArgsConstructor @Accessors(chain=true) @Slf4j | |||
@JsonTypeInfo( | |||
use = JsonTypeInfo.Id.NAME, | |||
include = JsonTypeInfo.As.EXISTING_PROPERTY, | |||
property = "operation", | |||
visible = true | |||
) | |||
public abstract class JOperation { | |||
@Getter @Setter private JOperationType operation; | |||
@Getter @Setter private String operation; | |||
@Getter @Setter private JsonNode creates; | |||
@Getter @Setter private JsonNode perform; | |||
public String hash(JAsset[] sources) { return hash(sources, null); } | |||
public String hash(JAsset[] sources, Object[] args) { | |||
return hashOf(getOperation(), json(creates), json(perform), sources, args); | |||
return hashOf(operation, json(this), sources, args); | |||
} | |||
private static final Map<Class<? extends JOperation>, ExecBase<?>> execMap = new HashMap<>(); | |||
public <OP extends JOperation> ExecBase<OP> getExec() { | |||
return (ExecBase<OP>) execMap.computeIfAbsent(getClass(), c -> instantiate(getExecClass())); | |||
} | |||
protected <OP extends JOperation> Class<? extends ExecBase<OP>> getExecClass() { | |||
return getOperationExecClass(getClass()); | |||
} | |||
} |
@@ -1,32 +0,0 @@ | |||
package jvcl.model; | |||
import com.fasterxml.jackson.annotation.JsonCreator; | |||
import jvcl.op.ConcatOperation; | |||
import jvcl.op.OverlayOperation; | |||
import jvcl.op.SplitOperation; | |||
import jvcl.op.TrimOperation; | |||
import jvcl.service.AssetManager; | |||
import jvcl.service.JOperator; | |||
import jvcl.service.Toolbox; | |||
import lombok.AllArgsConstructor; | |||
@AllArgsConstructor | |||
public enum JOperationType { | |||
concat (new ConcatOperation()), | |||
split (new SplitOperation()), | |||
trim (new TrimOperation()), | |||
overlay (new OverlayOperation()), | |||
ken_burns (null), | |||
letterbox (null), | |||
split_silence (null); | |||
@JsonCreator public static JOperationType fromString(String v) { return valueOf(v.toLowerCase().replace("-", "_")); } | |||
private final JOperator operator; | |||
public void perform(JOperation op, Toolbox toolbox, AssetManager assetManager) { | |||
operator.operate(op, toolbox, assetManager); | |||
} | |||
} |
@@ -1,140 +0,0 @@ | |||
package jvcl.op; | |||
import jvcl.model.JAsset; | |||
import jvcl.model.JFileExtension; | |||
import jvcl.model.JOperation; | |||
import jvcl.service.AssetManager; | |||
import jvcl.service.JOperator; | |||
import jvcl.service.Toolbox; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.util.javascript.JsEngine; | |||
import org.cobbzilla.util.javascript.StandardJsEngine; | |||
import java.io.File; | |||
import java.math.BigDecimal; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
import static java.math.RoundingMode.HALF_EVEN; | |||
import static jvcl.model.JAsset.json2asset; | |||
import static jvcl.service.Toolbox.getDuration; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.big; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.io.FileUtil.abs; | |||
import static org.cobbzilla.util.system.CommandShell.execScript; | |||
@Slf4j | |||
public class OverlayOperation implements JOperator { | |||
public static final String OVERLAY_TEMPLATE | |||
= "{{ffmpeg}} -i {{{source.path}}} -filter_complex \"" + | |||
"movie={{{overlay.path}}}{{#exists overlayStart}}:seek_point={{overlayStart}}{{/exists}} [ovl]; " + | |||
"[v:0] setpts=PTS-(STARTPTS+{{#exists offset}}{{offset}}{{else}}0{{/exists}}) [main]; " + | |||
"[ovl] setpts=PTS-STARTPTS{{#exists width}}, scale={{width}}x{{height}}{{/exists}} ; " + | |||
"[main][ovl] overlay=shortest=1{{#exists x}}:x={{x}}{{/exists}}{{#exists y}}:y={{y}}{{/exists}} " + | |||
"\" {{{output.path}}}"; | |||
@Override public void operate(JOperation op, Toolbox toolbox, AssetManager assetManager) { | |||
final OverlayConfig config = loadConfig(op, OverlayConfig.class); | |||
final JAsset source = assetManager.resolve(config.getSource()); | |||
final JAsset overlay = assetManager.resolve(config.getOverlay()); | |||
final JAsset output = json2asset(op.getCreates()); | |||
output.mergeFormat(source.getFormat()); | |||
final JFileExtension formatType = output.getFormat().getFileExtension(); | |||
final File defaultOutfile = assetManager.assetPath(op, source, formatType, new Object[]{config}); | |||
final File path = resolveOutputPath(output, defaultOutfile); | |||
if (path == null) return; | |||
output.setPath(abs(path)); | |||
final StandardJsEngine js = toolbox.getJs(); | |||
final Map<String, Object> ctx = new HashMap<>(); | |||
ctx.put("ffmpeg", toolbox.getFfmpeg()); | |||
ctx.put("source", source); | |||
ctx.put("overlay", overlay); | |||
ctx.put("output", output); | |||
ctx.put("offset", config.getOffsetSeconds(ctx, js)); | |||
ctx.put("overlayStart", config.getOverlayStartSeconds(ctx, js)); | |||
if (config.hasOverlayEnd()) ctx.put("overlayEnd", config.getOverlayEndSeconds(ctx, js)); | |||
if (config.hasWidth()) { | |||
final String width = config.getWidth(ctx, js); | |||
ctx.put("width", width); | |||
if (!config.hasHeight()) { | |||
final int height = big(width).divide(overlay.aspectRatio(), HALF_EVEN).intValue(); | |||
ctx.put("height", height); | |||
} | |||
} | |||
if (config.hasHeight()) { | |||
final String height = config.getHeight(ctx, js); | |||
ctx.put("height", height); | |||
if (!config.hasWidth()) { | |||
final int width = big(height).multiply(overlay.aspectRatio()).intValue(); | |||
ctx.put("width", width); | |||
} | |||
} | |||
if (config.hasX()) ctx.put("x", config.getX(ctx, js)); | |||
if (config.hasY()) ctx.put("y", config.getY(ctx, js)); | |||
final String script = renderScript(toolbox, ctx, OVERLAY_TEMPLATE); | |||
log.debug("operate: running script: "+script); | |||
final String scriptOutput = execScript(script); | |||
log.debug("operate: command output: "+scriptOutput); | |||
assetManager.addOperationAsset(output); | |||
} | |||
private static class OverlayConfig { | |||
@Getter @Setter private String source; | |||
@Getter @Setter private String overlay; | |||
private String eval(String val, Map<String, Object> ctx, JsEngine js) { | |||
final Map<String, Object> jsCtx = Toolbox.jsContext(ctx); | |||
final Object result = js.evaluate(val, jsCtx); | |||
return result == null ? null : result.toString(); | |||
} | |||
@Getter @Setter private String offset; | |||
public BigDecimal getOffsetSeconds (Map<String, Object> ctx, JsEngine js) { | |||
return empty(offset) ? BigDecimal.ZERO : getDuration(eval(offset, ctx, js)); | |||
} | |||
@Getter @Setter private String overlayStart; | |||
public BigDecimal getOverlayStartSeconds (Map<String, Object> ctx, JsEngine js) { | |||
return empty(overlayStart) ? BigDecimal.ZERO : getDuration(eval(overlayStart, ctx, js)); | |||
} | |||
@Getter @Setter private String overlayEnd; | |||
public boolean hasOverlayEnd () { return !empty(overlayEnd); } | |||
public BigDecimal getOverlayEndSeconds (Map<String, Object> ctx, JsEngine js) { | |||
return getDuration(eval(overlayEnd, ctx, js)); | |||
} | |||
@Getter @Setter private String width; | |||
public boolean hasWidth () { return !empty(width); } | |||
public String getWidth(Map<String, Object> ctx, JsEngine js) { return eval(width, ctx, js); } | |||
@Getter @Setter private String height; | |||
public boolean hasHeight () { return !empty(height); } | |||
public String getHeight(Map<String, Object> ctx, JsEngine js) { return eval(height, ctx, js); } | |||
@Getter @Setter private String x; | |||
public boolean hasX () { return !empty(x); } | |||
public String getX(Map<String, Object> ctx, JsEngine js) { return eval(x, ctx, js); } | |||
@Getter @Setter private String y; | |||
public boolean hasY () { return !empty(y); } | |||
public String getY(Map<String, Object> ctx, JsEngine js) { return eval(y, ctx, js); } | |||
@Getter @Setter private String outputWidth; | |||
public boolean hasOutputWidth () { return !empty(outputWidth); } | |||
public String getOutputWidth(Map<String, Object> ctx, JsEngine js) { return eval(outputWidth, ctx, js); } | |||
@Getter @Setter private String outputHeight; | |||
public boolean hasOutputHeight () { return !empty(outputHeight); } | |||
public String getOutputHeight(Map<String, Object> ctx, JsEngine js) { return eval(outputHeight, ctx, js); } | |||
} | |||
} |
@@ -0,0 +1,29 @@ | |||
package jvcl.operation; | |||
import jvcl.model.JAsset; | |||
import jvcl.model.JFileExtension; | |||
import jvcl.model.JOperation; | |||
import jvcl.service.AssetManager; | |||
import jvcl.service.Toolbox; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import java.io.File; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import static jvcl.model.JAsset.flattenAssetList; | |||
import static jvcl.model.JAsset.json2asset; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.io.FileUtil.abs; | |||
import static org.cobbzilla.util.system.CommandShell.execScript; | |||
@Slf4j | |||
public class ConcatOperation extends JOperation { | |||
@Getter @Setter private String[] concat; | |||
} |
@@ -0,0 +1,79 @@ | |||
package jvcl.operation; | |||
import jvcl.model.JAsset; | |||
import jvcl.model.JFileExtension; | |||
import jvcl.model.JOperation; | |||
import jvcl.service.AssetManager; | |||
import jvcl.service.Toolbox; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.util.javascript.JsEngine; | |||
import org.cobbzilla.util.javascript.StandardJsEngine; | |||
import java.io.File; | |||
import java.math.BigDecimal; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
import static java.math.RoundingMode.HALF_EVEN; | |||
import static jvcl.model.JAsset.json2asset; | |||
import static jvcl.service.Toolbox.getDuration; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.big; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.io.FileUtil.abs; | |||
import static org.cobbzilla.util.system.CommandShell.execScript; | |||
@Slf4j | |||
public class OverlayOperation extends JOperation { | |||
@Getter @Setter private String source; | |||
@Getter @Setter private String overlay; | |||
private String eval(String val, Map<String, Object> ctx, JsEngine js) { | |||
final Map<String, Object> jsCtx = Toolbox.jsContext(ctx); | |||
final Object result = js.evaluate(val, jsCtx); | |||
return result == null ? null : result.toString(); | |||
} | |||
@Getter @Setter private String offset; | |||
public BigDecimal getOffsetSeconds (Map<String, Object> ctx, JsEngine js) { | |||
return empty(offset) ? BigDecimal.ZERO : getDuration(eval(offset, ctx, js)); | |||
} | |||
@Getter @Setter private String overlayStart; | |||
public BigDecimal getOverlayStartSeconds (Map<String, Object> ctx, JsEngine js) { | |||
return empty(overlayStart) ? BigDecimal.ZERO : getDuration(eval(overlayStart, ctx, js)); | |||
} | |||
@Getter @Setter private String overlayEnd; | |||
public boolean hasOverlayEnd () { return !empty(overlayEnd); } | |||
public BigDecimal getOverlayEndSeconds (Map<String, Object> ctx, JsEngine js) { | |||
return getDuration(eval(overlayEnd, ctx, js)); | |||
} | |||
@Getter @Setter private String width; | |||
public boolean hasWidth () { return !empty(width); } | |||
public String getWidth(Map<String, Object> ctx, JsEngine js) { return eval(width, ctx, js); } | |||
@Getter @Setter private String height; | |||
public boolean hasHeight () { return !empty(height); } | |||
public String getHeight(Map<String, Object> ctx, JsEngine js) { return eval(height, ctx, js); } | |||
@Getter @Setter private String x; | |||
public boolean hasX () { return !empty(x); } | |||
public String getX(Map<String, Object> ctx, JsEngine js) { return eval(x, ctx, js); } | |||
@Getter @Setter private String y; | |||
public boolean hasY () { return !empty(y); } | |||
public String getY(Map<String, Object> ctx, JsEngine js) { return eval(y, ctx, js); } | |||
@Getter @Setter private String outputWidth; | |||
public boolean hasOutputWidth () { return !empty(outputWidth); } | |||
public String getOutputWidth(Map<String, Object> ctx, JsEngine js) { return eval(outputWidth, ctx, js); } | |||
@Getter @Setter private String outputHeight; | |||
public boolean hasOutputHeight () { return !empty(outputHeight); } | |||
public String getOutputHeight(Map<String, Object> ctx, JsEngine js) { return eval(outputHeight, ctx, js); } | |||
} |
@@ -0,0 +1,28 @@ | |||
package jvcl.operation; | |||
import jvcl.model.JAsset; | |||
import jvcl.model.JOperation; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import java.math.BigDecimal; | |||
import static jvcl.service.Toolbox.getDuration; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
@Slf4j | |||
public class SplitOperation extends JOperation { | |||
@Getter @Setter private String split; | |||
@Getter @Setter private String interval; | |||
public BigDecimal getIntervalIncr() { return getDuration(interval); } | |||
@Getter @Setter private String 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() : getDuration(end); } | |||
} |
@@ -0,0 +1,39 @@ | |||
package jvcl.operation; | |||
import jvcl.model.JAsset; | |||
import jvcl.model.JFileExtension; | |||
import jvcl.model.JOperation; | |||
import jvcl.service.AssetManager; | |||
import jvcl.service.Toolbox; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import java.io.File; | |||
import java.math.BigDecimal; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
import static jvcl.model.JAsset.json2asset; | |||
import static jvcl.service.Toolbox.getDuration; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.io.FileUtil.*; | |||
import static org.cobbzilla.util.system.CommandShell.execScript; | |||
@Slf4j | |||
public class TrimOperation extends JOperation { | |||
@Getter @Setter private String trim; | |||
@Getter @Setter private String start; | |||
public BigDecimal getStartTime() { return empty(start) ? BigDecimal.ZERO : getDuration(start); } | |||
@Getter @Setter private String end; | |||
public boolean hasEnd() { return !empty(end); } | |||
public BigDecimal getEndTime() { return getDuration(end); } | |||
public String shortString() { return "trim_"+getStart()+(hasEnd() ? "_"+getEnd() : ""); } | |||
public String toString() { return trim+"_"+getStart()+(hasEnd() ? "_"+getEnd() : ""); } | |||
} |
@@ -1,14 +1,10 @@ | |||
package jvcl.op; | |||
package jvcl.operation.exec; | |||
import jvcl.model.JAsset; | |||
import jvcl.model.JFileExtension; | |||
import jvcl.model.JOperation; | |||
import jvcl.operation.ConcatOperation; | |||
import jvcl.service.AssetManager; | |||
import jvcl.service.JOperator; | |||
import jvcl.service.Toolbox; | |||
import lombok.Getter; | |||
import lombok.NoArgsConstructor; | |||
import lombok.Setter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import java.io.File; | |||
@@ -24,7 +20,7 @@ import static org.cobbzilla.util.io.FileUtil.abs; | |||
import static org.cobbzilla.util.system.CommandShell.execScript; | |||
@Slf4j | |||
public class ConcatOperation implements JOperator { | |||
public class ConcatExec extends ExecBase<ConcatOperation> { | |||
public static final String CONCAT_RECODE_TEMPLATE_OLD | |||
// concat inputs | |||
@@ -45,12 +41,10 @@ public class ConcatOperation implements JOperator { | |||
// output combined result | |||
+ "-map \"[v]\" -map \"[a]\" {{{output.path}}}"; | |||
@Override public void operate(JOperation op, Toolbox toolbox, AssetManager assetManager) { | |||
final ConcatConfig config = loadConfig(op, ConcatConfig.class); | |||
@Override public void operate(ConcatOperation op, Toolbox toolbox, AssetManager assetManager) { | |||
// validate sources | |||
final List<JAsset> sources = flattenAssetList(assetManager.resolve(config.getConcat())); | |||
final List<JAsset> sources = flattenAssetList(assetManager.resolve(op.getConcat())); | |||
if (empty(sources)) die("operate: no sources"); | |||
// create output object | |||
@@ -78,9 +72,4 @@ public class ConcatOperation implements JOperator { | |||
assetManager.addOperationAsset(output); | |||
} | |||
@NoArgsConstructor | |||
private static class ConcatConfig { | |||
@Getter @Setter private String[] concat; | |||
} | |||
} |
@@ -1,33 +1,28 @@ | |||
package jvcl.service; | |||
package jvcl.operation.exec; | |||
import jvcl.model.JAsset; | |||
import jvcl.model.JOperation; | |||
import jvcl.service.AssetManager; | |||
import jvcl.service.Toolbox; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.util.handlebars.HandlebarsUtil; | |||
import org.slf4j.Logger; | |||
import org.slf4j.LoggerFactory; | |||
import java.io.File; | |||
import java.util.Map; | |||
import static org.cobbzilla.util.io.FileUtil.abs; | |||
import static org.cobbzilla.util.io.FileUtil.basename; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
public interface JOperator { | |||
@Slf4j | |||
public abstract class ExecBase<OP extends JOperation> { | |||
Logger log = LoggerFactory.getLogger(JOperator.class); | |||
public abstract void operate(OP operation, Toolbox toolbox, AssetManager assetManager); | |||
void operate(JOperation op, Toolbox toolbox, AssetManager assetManager); | |||
default <T> T loadConfig(JOperation op, Class<T> configClass) { | |||
return json(json(op.getPerform()), configClass); | |||
} | |||
default String renderScript(Toolbox toolbox, Map<String, Object> ctx, String template) { | |||
protected String renderScript(Toolbox toolbox, Map<String, Object> ctx, String template) { | |||
return HandlebarsUtil.apply(toolbox.getHandlebars(), template, ctx); | |||
} | |||
default File resolveOutputPath(JAsset output, File defaultOutfile) { | |||
protected File resolveOutputPath(JAsset output, File defaultOutfile) { | |||
if (output.hasDest()) { | |||
if (output.destExists() && !output.destIsDirectory()) { | |||
log.info("resolveOutputPath: dest exists: " + output.getDest()); | |||
@@ -41,6 +36,4 @@ public interface JOperator { | |||
return defaultOutfile; | |||
} | |||
} | |||
} |
@@ -0,0 +1,82 @@ | |||
package jvcl.operation.exec; | |||
import jvcl.model.JAsset; | |||
import jvcl.model.JFileExtension; | |||
import jvcl.operation.OverlayOperation; | |||
import jvcl.service.AssetManager; | |||
import jvcl.service.Toolbox; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.util.javascript.StandardJsEngine; | |||
import java.io.File; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
import static java.math.RoundingMode.HALF_EVEN; | |||
import static jvcl.model.JAsset.json2asset; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.big; | |||
import static org.cobbzilla.util.io.FileUtil.abs; | |||
import static org.cobbzilla.util.system.CommandShell.execScript; | |||
@Slf4j | |||
public class OverlayExec extends ExecBase<OverlayOperation> { | |||
public static final String OVERLAY_TEMPLATE | |||
= "{{ffmpeg}} -i {{{source.path}}} -filter_complex \"" + | |||
"movie={{{overlay.path}}}{{#exists overlayStart}}:seek_point={{overlayStart}}{{/exists}} [ovl]; " + | |||
"[v:0] setpts=PTS-(STARTPTS+{{#exists offset}}{{offset}}{{else}}0{{/exists}}) [main]; " + | |||
"[ovl] setpts=PTS-STARTPTS{{#exists width}}, scale={{width}}x{{height}}{{/exists}} ; " + | |||
"[main][ovl] overlay=shortest=1{{#exists x}}:x={{x}}{{/exists}}{{#exists y}}:y={{y}}{{/exists}} " + | |||
"\" {{{output.path}}}"; | |||
@Override public void operate(OverlayOperation op, Toolbox toolbox, AssetManager assetManager) { | |||
final JAsset source = assetManager.resolve(op.getSource()); | |||
final JAsset overlay = assetManager.resolve(op.getOverlay()); | |||
final JAsset output = json2asset(op.getCreates()); | |||
output.mergeFormat(source.getFormat()); | |||
final JFileExtension formatType = output.getFormat().getFileExtension(); | |||
final File defaultOutfile = assetManager.assetPath(op, source, formatType); | |||
final File path = resolveOutputPath(output, defaultOutfile); | |||
if (path == null) return; | |||
output.setPath(abs(path)); | |||
final StandardJsEngine js = toolbox.getJs(); | |||
final Map<String, Object> ctx = new HashMap<>(); | |||
ctx.put("ffmpeg", toolbox.getFfmpeg()); | |||
ctx.put("source", source); | |||
ctx.put("overlay", overlay); | |||
ctx.put("output", output); | |||
ctx.put("offset", op.getOffsetSeconds(ctx, js)); | |||
ctx.put("overlayStart", op.getOverlayStartSeconds(ctx, js)); | |||
if (op.hasOverlayEnd()) ctx.put("overlayEnd", op.getOverlayEndSeconds(ctx, js)); | |||
if (op.hasWidth()) { | |||
final String width = op.getWidth(ctx, js); | |||
ctx.put("width", width); | |||
if (!op.hasHeight()) { | |||
final int height = big(width).divide(overlay.aspectRatio(), HALF_EVEN).intValue(); | |||
ctx.put("height", height); | |||
} | |||
} | |||
if (op.hasHeight()) { | |||
final String height = op.getHeight(ctx, js); | |||
ctx.put("height", height); | |||
if (!op.hasWidth()) { | |||
final int width = big(height).multiply(overlay.aspectRatio()).intValue(); | |||
ctx.put("width", width); | |||
} | |||
} | |||
if (op.hasX()) ctx.put("x", op.getX(ctx, js)); | |||
if (op.hasY()) ctx.put("y", op.getY(ctx, js)); | |||
final String script = renderScript(toolbox, ctx, OVERLAY_TEMPLATE); | |||
log.debug("operate: running script: "+script); | |||
final String scriptOutput = execScript(script); | |||
log.debug("operate: command output: "+scriptOutput); | |||
assetManager.addOperationAsset(output); | |||
} | |||
} |
@@ -1,14 +1,10 @@ | |||
package jvcl.op; | |||
package jvcl.operation.exec; | |||
import jvcl.model.JAsset; | |||
import jvcl.model.JFileExtension; | |||
import jvcl.model.JOperation; | |||
import jvcl.operation.SplitOperation; | |||
import jvcl.service.AssetManager; | |||
import jvcl.service.JOperator; | |||
import jvcl.service.Toolbox; | |||
import lombok.Getter; | |||
import lombok.NoArgsConstructor; | |||
import lombok.Setter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import java.io.File; | |||
@@ -17,23 +13,20 @@ import java.util.HashMap; | |||
import java.util.Map; | |||
import static jvcl.model.JAsset.json2asset; | |||
import static jvcl.service.Toolbox.getDuration; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.io.FileUtil.abs; | |||
import static org.cobbzilla.util.io.FileUtil.mkdirOrDie; | |||
import static org.cobbzilla.util.system.CommandShell.execScript; | |||
@Slf4j | |||
public class SplitOperation implements JOperator { | |||
public class SplitExec extends ExecBase<SplitOperation> { | |||
public static final String SPLIT_TEMPLATE | |||
= "{{ffmpeg}} -i {{{source.path}}} -ss {{startSeconds}} -t {{interval}} {{{output.path}}}"; | |||
@Override public void operate(JOperation op, Toolbox toolbox, AssetManager assetManager) { | |||
final SplitConfig config = loadConfig(op, SplitConfig.class); | |||
@Override public void operate(SplitOperation op, Toolbox toolbox, AssetManager assetManager) { | |||
final JAsset source = assetManager.resolve(config.getSplit()); | |||
final JAsset source = assetManager.resolve(op.getSplit()); | |||
// create output object | |||
final JAsset output = json2asset(op.getCreates()); | |||
@@ -48,9 +41,9 @@ public class SplitOperation implements JOperator { | |||
final Map<String, Object> ctx = new HashMap<>(); | |||
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(); | |||
final BigDecimal incr = op.getIntervalIncr(); | |||
final BigDecimal endTime = op.getEndTime(source); | |||
for (BigDecimal i = op.getStartTime(); | |||
i.compareTo(endTime) < 0; | |||
i = i.add(incr)) { | |||
@@ -97,19 +90,4 @@ public class SplitOperation implements JOperator { | |||
return new File(output.destDirectory(), output.getName() + "_" + i + "_" + incr + formatType.ext()); | |||
} | |||
@NoArgsConstructor | |||
private static class SplitConfig { | |||
@Getter @Setter private String split; | |||
@Getter @Setter private String interval; | |||
public BigDecimal getIntervalIncr() { return getDuration(interval); } | |||
@Getter @Setter private String 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() : getDuration(end); } | |||
} | |||
} |
@@ -1,15 +1,10 @@ | |||
package jvcl.op; | |||
package jvcl.operation.exec; | |||
import jvcl.model.JAsset; | |||
import jvcl.model.JFileExtension; | |||
import jvcl.model.JOperation; | |||
import jvcl.operation.TrimOperation; | |||
import jvcl.service.AssetManager; | |||
import jvcl.service.JOperator; | |||
import jvcl.service.Toolbox; | |||
import lombok.EqualsAndHashCode; | |||
import lombok.Getter; | |||
import lombok.NoArgsConstructor; | |||
import lombok.Setter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import java.io.File; | |||
@@ -18,22 +13,19 @@ import java.util.HashMap; | |||
import java.util.Map; | |||
import static jvcl.model.JAsset.json2asset; | |||
import static jvcl.service.Toolbox.getDuration; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.io.FileUtil.*; | |||
import static org.cobbzilla.util.system.CommandShell.execScript; | |||
@Slf4j | |||
public class TrimOperation implements JOperator { | |||
public class TrimExec extends ExecBase<TrimOperation> { | |||
public static final String TRIM_TEMPLATE | |||
= "{{ffmpeg}} -i {{{source.path}}} -ss {{startSeconds}} {{#exists interval}}-t {{interval}} {{/exists}}{{{output.path}}}"; | |||
@Override public void operate(JOperation op, Toolbox toolbox, AssetManager assetManager) { | |||
@Override public void operate(TrimOperation op, Toolbox toolbox, AssetManager assetManager) { | |||
final TrimConfig config = loadConfig(op, TrimConfig.class); | |||
final JAsset source = assetManager.resolve(config.getTrim()); | |||
final JAsset source = assetManager.resolve(op.getTrim()); | |||
final JAsset output = json2asset(op.getCreates()); | |||
output.mergeFormat(source.getFormat()); | |||
@@ -47,10 +39,10 @@ public class TrimOperation implements JOperator { | |||
assetManager.addOperationArrayAsset(output); | |||
for (JAsset asset : source.getList()) { | |||
final JAsset subOutput = new JAsset(output); | |||
final File defaultOutfile = assetManager.assetPath(op, asset, formatType, new Object[]{config}); | |||
final File defaultOutfile = assetManager.assetPath(op, asset, formatType); | |||
final File outfile; | |||
if (output.hasDest()) { | |||
outfile = new File(output.destDirectory(), basename(appendToFileNameBeforeExt(asset.getPath(), "_"+config.shortString()))); | |||
outfile = new File(output.destDirectory(), basename(appendToFileNameBeforeExt(asset.getPath(), "_"+op.shortString()))); | |||
if (outfile.exists()) { | |||
log.info("operate: dest exists: "+abs(outfile)); | |||
return; | |||
@@ -59,18 +51,18 @@ public class TrimOperation implements JOperator { | |||
outfile = defaultOutfile; | |||
} | |||
subOutput.setPath(abs(outfile)); | |||
trim(config, asset, output, subOutput, toolbox, assetManager); | |||
trim(op, asset, output, subOutput, toolbox, assetManager); | |||
} | |||
} else { | |||
final File defaultOutfile = assetManager.assetPath(op, source, formatType, new Object[]{config}); | |||
final File defaultOutfile = assetManager.assetPath(op, source, formatType); | |||
final File path = resolveOutputPath(output, defaultOutfile); | |||
if (path == null) return; | |||
output.setPath(abs(path)); | |||
trim(config, source, output, output, toolbox, assetManager); | |||
trim(op, source, output, output, toolbox, assetManager); | |||
} | |||
} | |||
private void trim(TrimConfig config, | |||
private void trim(TrimOperation op, | |||
JAsset source, | |||
JAsset output, | |||
JAsset subOutput, | |||
@@ -81,9 +73,9 @@ public class TrimOperation implements JOperator { | |||
ctx.put("source", source); | |||
ctx.put("output", subOutput); | |||
final BigDecimal startTime = config.getStartTime(); | |||
final BigDecimal startTime = op.getStartTime(); | |||
ctx.put("startSeconds", startTime); | |||
if (config.hasEnd()) ctx.put("interval", config.getEndTime().subtract(startTime)); | |||
if (op.hasEnd()) ctx.put("interval", op.getEndTime().subtract(startTime)); | |||
final String script = renderScript(toolbox, ctx, TRIM_TEMPLATE); | |||
log.debug("operate: running script: "+script); | |||
@@ -96,18 +88,4 @@ public class TrimOperation implements JOperator { | |||
} | |||
} | |||
@NoArgsConstructor @EqualsAndHashCode | |||
private static class TrimConfig { | |||
@Getter @Setter private String trim; | |||
@Getter @Setter private String start; | |||
public BigDecimal getStartTime() { return empty(start) ? BigDecimal.ZERO : getDuration(start); } | |||
@Getter @Setter private String end; | |||
public boolean hasEnd() { return !empty(end); } | |||
public BigDecimal getEndTime() { return getDuration(end); } | |||
public String shortString() { return "trim_"+getStart()+(hasEnd() ? "_"+getEnd() : ""); } | |||
public String toString() { return trim+"_"+getStart()+(hasEnd() ? "_"+getEnd() : ""); } | |||
} | |||
} |
@@ -13,6 +13,6 @@ public class OperationEngine { | |||
} | |||
public void perform(JOperation op) { | |||
op.getOperation().perform(op, toolbox, assetManager); | |||
op.getExec().operate(op, toolbox, assetManager); | |||
} | |||
} |
@@ -1,13 +1,14 @@ | |||
package jvcl.service; | |||
import com.fasterxml.jackson.databind.ObjectMapper; | |||
import com.github.jknack.handlebars.Handlebars; | |||
import jvcl.model.JAsset; | |||
import jvcl.model.JsObjectView; | |||
import jvcl.model.info.JMediaInfo; | |||
import jvcl.service.json.JOperationModule; | |||
import lombok.Getter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.util.handlebars.HandlebarsUtil; | |||
import org.cobbzilla.util.io.FileUtil; | |||
import org.cobbzilla.util.javascript.StandardJsEngine; | |||
import java.io.File; | |||
@@ -18,10 +19,8 @@ import java.util.concurrent.ConcurrentHashMap; | |||
import static java.math.RoundingMode.HALF_EVEN; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | |||
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.io.FileUtil.*; | |||
import static org.cobbzilla.util.json.JsonUtil.*; | |||
import static org.cobbzilla.util.system.CommandShell.execScript; | |||
import static org.cobbzilla.util.time.TimeUtil.parseDuration; | |||
@@ -30,6 +29,9 @@ public class Toolbox { | |||
public static final Toolbox DEFAULT_TOOLBOX = new Toolbox(); | |||
public static final ObjectMapper JSON_MAPPER = FULL_MAPPER_ALLOW_COMMENTS; | |||
static { JSON_MAPPER.registerModule(new JOperationModule()); } | |||
@Getter(lazy=true) private final Handlebars handlebars = initHandlebars(); | |||
@Getter(lazy=true) private final StandardJsEngine js = new StandardJsEngine(); | |||
@@ -90,10 +92,11 @@ public class Toolbox { | |||
} | |||
return infoCache.computeIfAbsent(infoPath, p -> { | |||
try { | |||
return json(FileUtil.toStringOrDie(infoFile), JMediaInfo.class, FULL_MAPPER_ALLOW_UNKNOWN_FIELDS); | |||
return json(toStringOrDie(infoFile), JMediaInfo.class, FULL_MAPPER_ALLOW_UNKNOWN_FIELDS); | |||
} catch (Exception e) { | |||
return die("getInfo: "+shortError(e), e); | |||
} | |||
}); | |||
} | |||
} |
@@ -0,0 +1,58 @@ | |||
package jvcl.service.json; | |||
import com.fasterxml.jackson.databind.DeserializationContext; | |||
import com.fasterxml.jackson.databind.JavaType; | |||
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler; | |||
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver; | |||
import jvcl.model.JOperation; | |||
import jvcl.operation.exec.ExecBase; | |||
import java.io.IOException; | |||
import static com.fasterxml.jackson.databind.type.TypeBindings.emptyBindings; | |||
import static org.apache.commons.lang3.StringUtils.capitalize; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.shortError; | |||
import static org.cobbzilla.util.reflect.ReflectionUtil.forName; | |||
public class JOperationFactory extends DeserializationProblemHandler { | |||
@Override public JavaType handleUnknownTypeId(DeserializationContext ctxt, | |||
JavaType baseType, | |||
String subTypeId, | |||
TypeIdResolver idResolver, | |||
String failureMsg) throws IOException { | |||
try { | |||
return new JOperationType(getOperationClass(subTypeId), emptyBindings(), baseType, null); | |||
} catch (Exception e) { | |||
throw new IOException("handleUnknownTypeId: '"+subTypeId+"' is not a valid operation type: "+shortError(e)); | |||
} | |||
} | |||
public static final String OPERATION_DEFAULT_PACKAGE = "jvcl.operation"; | |||
public static final String OPERATION_CLASSNAME_SUFFIX = "Operation"; | |||
public static Class<?> getOperationClass(String id) { | |||
final String className; | |||
if (id.contains(".")) { | |||
className = id; | |||
} else { | |||
className = OPERATION_DEFAULT_PACKAGE + "." + capitalize(id) + OPERATION_CLASSNAME_SUFFIX; | |||
} | |||
return forName(className); | |||
} | |||
public static <OP extends JOperation> Class<? extends ExecBase<OP>> getOperationExecClass(Class<? extends JOperation> opClass) { | |||
final String name = opClass.getSimpleName(); | |||
if (!name.endsWith(OPERATION_CLASSNAME_SUFFIX)) { | |||
return die("getOperationExecClass: expected JOperation class to end with '" + OPERATION_CLASSNAME_SUFFIX + "'"); | |||
} | |||
final String execClassName | |||
= opClass.getPackageName() | |||
+ ".exec." | |||
+ name.substring(0, name.length() - OPERATION_CLASSNAME_SUFFIX.length()) | |||
+ "Exec"; | |||
return forName(execClassName); | |||
} | |||
} |
@@ -0,0 +1,15 @@ | |||
package jvcl.service.json; | |||
import com.fasterxml.jackson.core.Version; | |||
import com.fasterxml.jackson.databind.Module; | |||
public class JOperationModule extends Module { | |||
@Override public String getModuleName() { return "JOperationFactoryModule"; } | |||
@Override public Version version() { return new Version(1, 0, 0, "", "", ""); } | |||
@Override public void setupModule(SetupContext context) { | |||
context.addDeserializationProblemHandler(new JOperationFactory()); | |||
} | |||
} |
@@ -0,0 +1,26 @@ | |||
package jvcl.service.json; | |||
import com.fasterxml.jackson.databind.JavaType; | |||
import com.fasterxml.jackson.databind.type.SimpleType; | |||
import com.fasterxml.jackson.databind.type.TypeBase; | |||
import com.fasterxml.jackson.databind.type.TypeBindings; | |||
public class JOperationType extends SimpleType { | |||
protected JOperationType(Class<?> cls) { super(cls); } | |||
public JOperationType(Class<?> cls, TypeBindings bindings, JavaType superClass, JavaType[] superInts) { | |||
super(cls, bindings, superClass, superInts); | |||
} | |||
protected JOperationType(TypeBase base) { super(base); } | |||
protected JOperationType(Class<?> cls, TypeBindings bindings, JavaType superClass, JavaType[] superInts, Object valueHandler, Object typeHandler, boolean asStatic) { | |||
super(cls, bindings, superClass, superInts, valueHandler, typeHandler, asStatic); | |||
} | |||
protected JOperationType(Class<?> cls, TypeBindings bindings, JavaType superClass, JavaType[] superInts, int extraHash, Object valueHandler, Object typeHandler, boolean asStatic) { | |||
super(cls, bindings, superClass, superInts, extraHash, valueHandler, typeHandler, asStatic); | |||
} | |||
} |
@@ -9,9 +9,7 @@ | |||
"name": "combined_vid", | |||
"dest": "src/test/resources/outputs/combined.mp4" | |||
}, | |||
"perform": { | |||
"concat": ["vid1_splits[1..]"] | |||
} | |||
"concat": ["vid1_splits[1..]"] | |||
} | |||
] | |||
} |
@@ -13,17 +13,15 @@ | |||
], | |||
"operations": [ | |||
{ | |||
"operation": "split", // name of the operation | |||
"operation": "split", // name of the operation | |||
"creates": { | |||
"name": "vid1_splits", | |||
"dest": "src/test/resources/outputs/" | |||
}, | |||
"perform": { | |||
"split": "vid1", // split this source asset | |||
"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 | |||
} | |||
"split": "vid1", // split this source asset | |||
"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 | |||
} | |||
] | |||
} |
@@ -4,16 +4,14 @@ | |||
], | |||
"operations": [ | |||
{ | |||
"operation": "trim", // name of the operation | |||
"operation": "trim", // name of the operation | |||
"creates": { | |||
"name": "vid1_trims", | |||
"dest": "src/test/resources/outputs/trims/" | |||
}, | |||
"perform": { | |||
"trim": "vid1_splits", // trim these source assets | |||
"start": "1s", // cropped region starts here, default is zero | |||
"end": "6s" // cropped region ends here, default is end of video | |||
} | |||
"trim": "vid1_splits", // trim these source assets | |||
"start": "1s", // cropped region starts here, default is zero | |||
"end": "6s" // cropped region ends here, default is end of video | |||
} | |||
] | |||
} |