diff --git a/README.md b/README.md index 6df093c..988dea6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/main/java/jvcl/operation/OverlayOperation.java b/src/main/java/jvcl/operation/OverlayOperation.java index a214f0c..67438dd 100644 --- a/src/main/java/jvcl/operation/OverlayOperation.java +++ b/src/main/java/jvcl/operation/OverlayOperation.java @@ -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 ctx, JsEngine js) { - final Map 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 ctx, JsEngine js) { + return empty(start) ? BigDecimal.ZERO : getDuration(eval(start, ctx, js)); } - @Getter @Setter private String offset; - public BigDecimal getOffsetSeconds (Map ctx, JsEngine js) { - return empty(offset) ? BigDecimal.ZERO : getDuration(eval(offset, ctx, js)); + @Getter @Setter private String end; + public BigDecimal getEndSeconds(Map ctx, JsEngine js) { + return empty(end) ? BigDecimal.ZERO : getDuration(eval(end, ctx, js)); } - @Getter @Setter private String overlayStart; - public BigDecimal getOverlayStartSeconds (Map 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 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 ctx, JsEngine js) { return eval(width, ctx, js); } + @Getter @Setter private String start; + public BigDecimal getStartTime(Map 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 ctx, JsEngine js) { return eval(height, ctx, js); } + @Getter @Setter private String end; + public boolean hasEndTime () { return !empty(end); } + public BigDecimal getEndTime(Map ctx, JsEngine js) { + return getDuration(eval(end, ctx, js)); + } - @Getter @Setter private String x; - public boolean hasX () { return !empty(x); } - public String getX(Map ctx, JsEngine js) { return eval(x, ctx, js); } + @Getter @Setter private String width; + public boolean hasWidth () { return !empty(width); } + public String getWidth(Map ctx, JsEngine js) { return eval(width, ctx, js); } - @Getter @Setter private String y; - public boolean hasY () { return !empty(y); } - public String getY(Map ctx, JsEngine js) { return eval(y, ctx, js); } + @Getter @Setter private String height; + public boolean hasHeight () { return !empty(height); } + public String getHeight(Map ctx, JsEngine js) { return eval(height, ctx, js); } - @Getter @Setter private String outputWidth; - public boolean hasOutputWidth () { return !empty(outputWidth); } - public String getOutputWidth(Map ctx, JsEngine js) { return eval(outputWidth, ctx, js); } + @Getter @Setter private String x; + public boolean hasX () { return !empty(x); } + public String getX(Map ctx, JsEngine js) { return eval(x, ctx, js); } - @Getter @Setter private String outputHeight; - public boolean hasOutputHeight () { return !empty(outputHeight); } - public String getOutputHeight(Map ctx, JsEngine js) { return eval(outputHeight, ctx, js); } + @Getter @Setter private String y; + public boolean hasY () { return !empty(y); } + public String getY(Map ctx, JsEngine js) { return eval(y, ctx, js); } + public BigDecimal aspectRatio () { + return big(getWidth()).divide(big(getHeight()), RoundingMode.HALF_EVEN); + } + } } diff --git a/src/main/java/jvcl/operation/exec/OverlayExec.java b/src/main/java/jvcl/operation/exec/OverlayExec.java index 7f73c52..2725a4b 100644 --- a/src/main/java/jvcl/operation/exec/OverlayExec.java +++ b/src/main/java/jvcl/operation/exec/OverlayExec.java @@ -22,16 +22,16 @@ import static org.cobbzilla.util.system.CommandShell.execScript; public class OverlayExec extends ExecBase { 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 { final Map 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); diff --git a/src/main/java/jvcl/service/Toolbox.java b/src/main/java/jvcl/service/Toolbox.java index 8239a5d..5eaad92 100644 --- a/src/main/java/jvcl/service/Toolbox.java +++ b/src/main/java/jvcl/service/Toolbox.java @@ -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 ctx, JsEngine js) { + final Map 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); } diff --git a/src/test/java/javicle/test/BasicTest.java b/src/test/java/javicle/test/BasicTest.java index 559c29a..2677258 100644 --- a/src/test/java/javicle/test/BasicTest.java +++ b/src/test/java/javicle/test/BasicTest.java @@ -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 { diff --git a/src/test/resources/tests/test_overlay.jvcl b/src/test/resources/tests/test_overlay.jvcl index adf037f..6303338 100644 --- a/src/test/resources/tests/test_overlay.jvcl +++ b/src/test/resources/tests/test_overlay.jvcl @@ -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 } } ]