From 1175cbf116ed30bbb0a3c28e35de656f9545814a Mon Sep 17 00:00:00 2001 From: Jonathan Cobb Date: Sat, 12 Dec 2020 12:01:38 -0500 Subject: [PATCH] WIP. adding overlay operation --- README.md | 22 +++--- src/main/java/jvcl/model/JAsset.java | 1 + src/main/java/jvcl/op/ConcatOperation.java | 6 +- src/main/java/jvcl/op/OverlayOperation.java | 88 +++++++++++++++++++++ src/main/java/jvcl/op/SplitOperation.java | 6 +- src/main/java/jvcl/op/TrimOperation.java | 6 +- src/main/java/jvcl/service/JOperator.java | 13 +++ src/test/resources/tests/test_overlay.json | 39 +++++++++ src/test/resources/tests/test_split.json | 4 +- 9 files changed, 160 insertions(+), 25 deletions(-) create mode 100644 src/main/java/jvcl/op/OverlayOperation.java create mode 100644 src/test/resources/tests/test_overlay.json diff --git a/README.md b/README.md index dd3f5d2..84c6942 100644 --- a/README.md +++ b/README.md @@ -133,20 +133,20 @@ Here is a complex example using multiple assets and operations: "creates": "overlay1", // asset it creates "perform": { "source": "combined_vid1", // main video asset - "overlay": "vid1", // overlay this video on the main video + "overlay": "vid2", // overlay this video on the main video - "start": "vid1.end_ts", // when (on the main video timeline) to start the overlay. default is 0 (beginning) - "duration": "vid1.duration", // how long to play the overlay. default is to play the entire overlay asset + "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 - "width": 400, // how wide the overlay will be, in pixels. default is "overlay.width" - "height": 300, // how tall the overlay will be, in pixels. default is "overlay.height" + "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. default is 0 - "y": "source.height / 2", // vertical overlay position. default is 0 - - "out": "1080p", // this is a shortcut to the two lines below, and is the preferred way of specifying the output resolution - "out_width": 1920, // output width in pixels. default is source width - "out_height": 1024 // output height in pixes. default is source height + "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 } } ] diff --git a/src/main/java/jvcl/model/JAsset.java b/src/main/java/jvcl/model/JAsset.java index 15feb60..e047880 100644 --- a/src/main/java/jvcl/model/JAsset.java +++ b/src/main/java/jvcl/model/JAsset.java @@ -143,6 +143,7 @@ public class JAsset { } public BigDecimal duration() { return getInfo().duration(); } + @JsonIgnore public BigDecimal getDuration () { return duration(); } public JAsset init(AssetManager assetManager, Toolbox toolbox) { final JAsset asset = initPath(assetManager); diff --git a/src/main/java/jvcl/op/ConcatOperation.java b/src/main/java/jvcl/op/ConcatOperation.java index 8c37a7c..634e134 100644 --- a/src/main/java/jvcl/op/ConcatOperation.java +++ b/src/main/java/jvcl/op/ConcatOperation.java @@ -10,7 +10,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.cobbzilla.util.handlebars.HandlebarsUtil; import java.io.File; import java.util.HashMap; @@ -22,7 +21,6 @@ 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.json.JsonUtil.json; import static org.cobbzilla.util.system.CommandShell.execScript; @Slf4j @@ -49,7 +47,7 @@ public class ConcatOperation implements JOperator { @Override public void operate(JOperation op, Toolbox toolbox, AssetManager assetManager) { - final ConcatConfig config = json(json(op.getPerform()), ConcatConfig.class); + final ConcatConfig config = loadConfig(op, ConcatConfig.class); // validate sources final List sources = flattenAssetList(assetManager.resolve(config.getConcat())); @@ -82,7 +80,7 @@ public class ConcatOperation implements JOperator { ctx.put("ffmpeg", toolbox.getFfmpeg()); ctx.put("sources", sources); ctx.put("output", output); - final String script = HandlebarsUtil.apply(toolbox.getHandlebars(), CONCAT_RECODE_TEMPLATE_1, ctx); + final String script = renderScript(toolbox, ctx, CONCAT_RECODE_TEMPLATE_1); log.debug("operate: running script: "+script); final String scriptOutput = execScript(script); diff --git a/src/main/java/jvcl/op/OverlayOperation.java b/src/main/java/jvcl/op/OverlayOperation.java new file mode 100644 index 0000000..fba1cf9 --- /dev/null +++ b/src/main/java/jvcl/op/OverlayOperation.java @@ -0,0 +1,88 @@ +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 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.empty; +import static org.cobbzilla.util.system.CommandShell.execScript; + +@Slf4j +public class OverlayOperation implements JOperator { + + public static final String OVERLAY_TEMPLATE + = "{{ffmpeg}} "; + + @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 Map ctx = new HashMap<>(); + ctx.put("source", source); + ctx.put("overlay", overlay); + ctx.put("output", output); + ctx.put("offset", config.getOffsetSeconds()); + ctx.put("overlayStart", config.getOverlayStartSeconds()); + if (config.hasOverlayEnd()) ctx.put("overlayEnd", config.getOverlayEndSeconds()); + if (config.hasWidth()) ctx.put("width", config.getWidth()); + if (config.hasHeight()) ctx.put("height", config.getHeight()); + + 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; + + @Getter @Setter private String offset; + public BigDecimal getOffsetSeconds () { return empty(offset) ? BigDecimal.ZERO : getDuration(offset); } + + @Getter @Setter private String overlayStart; + public BigDecimal getOverlayStartSeconds () { return empty(overlayStart) ? BigDecimal.ZERO : getDuration(overlayStart); } + + @Getter @Setter private String overlayEnd; + public boolean hasOverlayEnd () { return !empty(overlayEnd); } + public BigDecimal getOverlayEndSeconds () { return getDuration(overlayEnd); } + + @Getter @Setter private String width; + public boolean hasWidth () { return !empty(width); } + + @Getter @Setter private String height; + public boolean hasHeight () { return !empty(height); } + + @Getter @Setter private String x; + public boolean hasX () { return !empty(x); } + + @Getter @Setter private String y; + public boolean hasY () { return !empty(y); } + + @Getter @Setter private String outputWidth; + public boolean hasOutputWidth () { return !empty(outputWidth); } + + @Getter @Setter private String outputHeight; + public boolean hasOutputHeight () { return !empty(outputHeight); } + } +} diff --git a/src/main/java/jvcl/op/SplitOperation.java b/src/main/java/jvcl/op/SplitOperation.java index 972475b..438241c 100644 --- a/src/main/java/jvcl/op/SplitOperation.java +++ b/src/main/java/jvcl/op/SplitOperation.java @@ -10,7 +10,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.cobbzilla.util.handlebars.HandlebarsUtil; import java.io.File; import java.math.BigDecimal; @@ -23,7 +22,6 @@ 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.json.JsonUtil.json; import static org.cobbzilla.util.system.CommandShell.execScript; @Slf4j @@ -33,7 +31,7 @@ public class SplitOperation implements JOperator { = "{{ffmpeg}} -i {{{source.path}}} -ss {{startSeconds}} -t {{interval}} {{{output.path}}}"; @Override public void operate(JOperation op, Toolbox toolbox, AssetManager assetManager) { - final SplitConfig config = json(json(op.getPerform()), SplitConfig.class); + final SplitConfig config = loadConfig(op, SplitConfig.class); final JAsset source = assetManager.resolve(config.getSplit()); @@ -85,7 +83,7 @@ public class SplitOperation implements JOperator { ctx.put("output", slice); ctx.put("startSeconds", i); ctx.put("interval", incr); - final String script = HandlebarsUtil.apply(toolbox.getHandlebars(), SPLIT_TEMPLATE, ctx); + final String script = renderScript(toolbox, ctx, SPLIT_TEMPLATE); log.debug("operate: running script: "+script); final String scriptOutput = execScript(script); diff --git a/src/main/java/jvcl/op/TrimOperation.java b/src/main/java/jvcl/op/TrimOperation.java index f5a71aa..d7d0828 100644 --- a/src/main/java/jvcl/op/TrimOperation.java +++ b/src/main/java/jvcl/op/TrimOperation.java @@ -11,7 +11,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.cobbzilla.util.handlebars.HandlebarsUtil; import java.io.File; import java.math.BigDecimal; @@ -23,7 +22,6 @@ 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.json.JsonUtil.json; import static org.cobbzilla.util.system.CommandShell.execScript; @Slf4j @@ -34,7 +32,7 @@ public class TrimOperation implements JOperator { @Override public void operate(JOperation op, Toolbox toolbox, AssetManager assetManager) { - final TrimConfig config = json(json(op.getPerform()), TrimConfig.class); + final TrimConfig config = loadConfig(op, TrimConfig.class); final JAsset source = assetManager.resolve(config.getTrim()); final JAsset output = json2asset(op.getCreates()); @@ -85,7 +83,7 @@ public class TrimOperation implements JOperator { final BigDecimal startTime = config.getStartTime(); ctx.put("startSeconds", startTime); if (config.hasEnd()) ctx.put("interval", config.getEndTime().subtract(startTime)); - final String script = HandlebarsUtil.apply(toolbox.getHandlebars(), TRIM_TEMPLATE, ctx); + final String script = renderScript(toolbox, ctx, TRIM_TEMPLATE); log.debug("operate: running script: "+script); final String scriptOutput = execScript(script); diff --git a/src/main/java/jvcl/service/JOperator.java b/src/main/java/jvcl/service/JOperator.java index 89da6fe..f5bbfac 100644 --- a/src/main/java/jvcl/service/JOperator.java +++ b/src/main/java/jvcl/service/JOperator.java @@ -1,9 +1,22 @@ package jvcl.service; import jvcl.model.JOperation; +import org.cobbzilla.util.handlebars.HandlebarsUtil; + +import java.util.Map; + +import static org.cobbzilla.util.json.JsonUtil.json; public interface JOperator { void operate(JOperation op, Toolbox toolbox, AssetManager assetManager); + default T loadConfig(JOperation op, Class configClass) { + return json(json(op.getPerform()), configClass); + } + + default String renderScript(Toolbox toolbox, Map ctx, String template) { + return HandlebarsUtil.apply(toolbox.getHandlebars(), template, ctx); + } + } diff --git a/src/test/resources/tests/test_overlay.json b/src/test/resources/tests/test_overlay.json new file mode 100644 index 0000000..a51b950 --- /dev/null +++ b/src/test/resources/tests/test_overlay.json @@ -0,0 +1,39 @@ +{ + "assets": [ + { + "name": "vid1", + "path": "https://archive.org/download/gov.archives.arc.1257628/gov.archives.arc.1257628_512kb.mp4", + "dest": "src/test/resources/sources/" + }, + { + "name": "vid2", + "path": "https://archive.org/download/gov.archives.arc.49442/gov.archives.arc.49442_512kb.mp4", + "dest": "src/test/resources/sources/" + } + ], + "operations": [ + { + "operation": "overlay", // name of the operation + "creates": { + "name": "overlay1", + "dest": "src/test/resources/outputs/overlay/" + }, + "perform": { + "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 + + "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 + } + ] +} diff --git a/src/test/resources/tests/test_split.json b/src/test/resources/tests/test_split.json index 71fbc8a..4c272a2 100644 --- a/src/test/resources/tests/test_split.json +++ b/src/test/resources/tests/test_split.json @@ -3,12 +3,12 @@ { "name": "vid1", "path": "https://archive.org/download/gov.archives.arc.1257628/gov.archives.arc.1257628_512kb.mp4", - "dest": "src/test/resources/sources/gov.archives.arc.1257628_512kb.mp4" + "dest": "src/test/resources/sources/" }, { "name": "vid2", "path": "https://archive.org/download/gov.archives.arc.49442/gov.archives.arc.49442_512kb.mp4", - "dest": "src/test/resources/sources/gov.archives.arc.49442_512kb.mp4" + "dest": "src/test/resources/sources/" } ], "operations": [