@@ -116,9 +116,9 @@ later operations. | |||
Most of the operation settings can be JavaScript expressions, for example: | |||
"startTime": "someAsset.duration - 10" | |||
"start": "someAsset.duration - 10" | |||
The above would set the `startTime` value to ten seconds before the end of `someAsset`. | |||
The above would set the `start` value to ten seconds before the end of `someAsset`. | |||
### Supported Operations | |||
Today, JVCL supports these operations: | |||
@@ -199,12 +199,12 @@ Here is a complex example using multiple assets and operations. | |||
"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 | |||
"start": "30", // when (on the main video timeline) to begin showing the overlay. default is 0 (beginning) | |||
"end": "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": "overlay.duration", // when (on the overlay video timeline) to end playback on the overlay. default is to play the entire overlay | |||
"start": "0", // when (on the overlay video timeline) to begin playback on the overlay. default is 0 (beginning) | |||
"end": "overlay.duration", // 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": "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 | |||
@@ -1,79 +1,68 @@ | |||
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.math.RoundingMode; | |||
import java.util.Map; | |||
import static java.math.RoundingMode.HALF_EVEN; | |||
import static jvcl.model.JAsset.json2asset; | |||
import static jvcl.service.Toolbox.eval; | |||
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; | |||
@Getter @Setter private OverlayConfig 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 start; | |||
public BigDecimal getStartSeconds(Map<String, Object> ctx, JsEngine js) { | |||
return empty(start) ? BigDecimal.ZERO : getDuration(eval(start, ctx, js)); | |||
} | |||
@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 end; | |||
public BigDecimal getEndSeconds(Map<String, Object> ctx, JsEngine js) { | |||
return empty(end) ? BigDecimal.ZERO : getDuration(eval(end, 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)); | |||
} | |||
public static class OverlayConfig { | |||
@Getter @Setter private String source; | |||
@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 start; | |||
public BigDecimal getStartTime(Map<String, Object> ctx, JsEngine js) { | |||
return empty(start) ? BigDecimal.ZERO : getDuration(eval(start, 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 end; | |||
public boolean hasEndTime () { return !empty(end); } | |||
public BigDecimal getEndTime(Map<String, Object> ctx, JsEngine js) { | |||
return getDuration(eval(end, 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 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 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 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 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 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 outputHeight; | |||
public boolean hasOutputHeight () { return !empty(outputHeight); } | |||
public String getOutputHeight(Map<String, Object> ctx, JsEngine js) { return eval(outputHeight, 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); } | |||
public BigDecimal aspectRatio () { | |||
return big(getWidth()).divide(big(getHeight()), RoundingMode.HALF_EVEN); | |||
} | |||
} | |||
} |
@@ -22,16 +22,16 @@ import static org.cobbzilla.util.system.CommandShell.execScript; | |||
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}}}"; | |||
= "ffmpeg -i {{{source.path}}} -vf \"" | |||
+ " movie={{{overlay.path}}}:seek_point=0 [ovl]; " | |||
+ " [ovl] setpts=PTS-STARTPTS, scale=160x160 [ovl2] ; " | |||
+ " [main][ovl2] overlay=enable=between(t\\,15\\,20):x=160:y=120\" " | |||
+ "{{{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 OverlayOperation.OverlayConfig overlay = op.getOverlay(); | |||
final JAsset overlaySource = assetManager.resolve(overlay.getSource()); | |||
final JAsset output = json2asset(op.getCreates()); | |||
output.mergeFormat(source.getFormat()); | |||
@@ -47,29 +47,29 @@ public class OverlayExec extends ExecBase<OverlayOperation> { | |||
final Map<String, Object> ctx = new HashMap<>(); | |||
ctx.put("ffmpeg", toolbox.getFfmpeg()); | |||
ctx.put("source", source); | |||
ctx.put("overlay", overlay); | |||
ctx.put("overlay", overlaySource); | |||
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("offset", op.getStartSeconds(ctx, js)); | |||
ctx.put("overlayStart", overlay.getStartTime(ctx, js)); | |||
if (overlay.hasEndTime()) ctx.put("overlayEnd", overlay.getEndTime(ctx, js)); | |||
if (overlay.hasWidth()) { | |||
final String width = overlay.getWidth(ctx, js); | |||
ctx.put("width", width); | |||
if (!op.hasHeight()) { | |||
if (!overlay.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); | |||
if (overlay.hasHeight()) { | |||
final String height = overlay.getHeight(ctx, js); | |||
ctx.put("height", height); | |||
if (!op.hasWidth()) { | |||
if (!overlay.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)); | |||
if (overlay.hasX()) ctx.put("x", overlay.getX(ctx, js)); | |||
if (overlay.hasY()) ctx.put("y", overlay.getY(ctx, js)); | |||
final String script = renderScript(toolbox, ctx, OVERLAY_TEMPLATE); | |||
@@ -9,6 +9,7 @@ import jvcl.service.json.JOperationModule; | |||
import lombok.Getter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.util.handlebars.HandlebarsUtil; | |||
import org.cobbzilla.util.javascript.JsEngine; | |||
import org.cobbzilla.util.javascript.StandardJsEngine; | |||
import java.io.File; | |||
@@ -36,6 +37,12 @@ public class Toolbox { | |||
@Getter(lazy=true) private final StandardJsEngine js = new StandardJsEngine(); | |||
public static 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(); | |||
} | |||
public static BigDecimal getDuration(String t) { | |||
return big(parseDuration(t)).divide(big(1000), HALF_EVEN); | |||
} | |||
@@ -21,7 +21,7 @@ public class BasicTest { | |||
runSpec("tests/test_trim.jvcl"); | |||
} | |||
// @Test public void test4Overlay() { runSpec("tests/test_overlay.jvcl"); } | |||
@Test public void testOverlay() { runSpec("tests/test_overlay.jvcl"); } | |||
private void runSpec(String specPath) { | |||
try { | |||
@@ -12,27 +12,31 @@ | |||
} | |||
], | |||
"operations": [ | |||
{ | |||
"operation": "trim", | |||
"creates": "v1", | |||
"trim": "vid1", | |||
"start": "0" | |||
}, | |||
{ | |||
"operation": "overlay", // name of the operation | |||
"creates": { | |||
"name": "overlay1", | |||
"width": "1920", // output width in pixels. default is source width | |||
"height": "1024", // output height in pixes. default is source height | |||
"dest": "src/test/resources/outputs/overlay/" | |||
}, | |||
"source": "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 | |||
"source": "vid1", // main video asset | |||
"start": "30", // when (on the main video timeline) to begin showing the overlay. default is 0 (beginning) | |||
"end": "30 + overlay.duration", // 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 | |||
"start": "0", // when (on the overlay video timeline) to begin playback on the overlay. default is 0 (beginning) | |||
"end": "overlay.duration", // 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 | |||
"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 | |||
"outputWidth": "1920", // output width in pixels. default is source width | |||
"outputHeight": "1024" // output height in pixes. default is source height | |||
"y": "source.height / 2" // vertical overlay position on main video. default is 0 | |||
} | |||
} | |||
] | |||