diff --git a/README.md b/README.md index 3148772..c09ed75 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,7 @@ Here is a complex example using multiple assets and operations. "end": "duration", // when to end zooming, default is duration "x": "source.width * 0.6", // pan to this x-position "y": "source.height * 0.4", // pan to this y-position + "upscale": "8", // upscale factor. upscaling the image results in a smoother pan, but a longer encode, default is 8 "width": "1024", // width of output video "height": "768" // height of output video } diff --git a/src/main/java/jvcl/model/JAsset.java b/src/main/java/jvcl/model/JAsset.java index e865c3b..cc7985c 100644 --- a/src/main/java/jvcl/model/JAsset.java +++ b/src/main/java/jvcl/model/JAsset.java @@ -21,8 +21,8 @@ import java.util.*; import java.util.regex.Pattern; import java.util.stream.Collectors; -import static java.math.RoundingMode.HALF_EVEN; import static java.util.Comparator.comparing; +import static jvcl.service.Toolbox.divideBig; import static org.cobbzilla.util.daemon.ZillaRuntime.*; import static org.cobbzilla.util.http.HttpSchemes.isHttpOrHttps; import static org.cobbzilla.util.io.FileUtil.*; @@ -156,7 +156,7 @@ public class JAsset implements JsObjectView { public BigDecimal aspectRatio() { final BigDecimal width = width(); final BigDecimal height = height(); - return width == null || height == null ? null : width.divide(height, HALF_EVEN); + return width == null || height == null ? null : divideBig(width, height); } public JAsset init(AssetManager assetManager, Toolbox toolbox) { @@ -176,14 +176,23 @@ public class JAsset implements JsObjectView { // if dest already exists, use that if (hasDest()) { - if (destExists()) { + if (destExists() && !destIsDirectory()) { setOriginalPath(path); setPath(destPath()); return this; } } - final File sourcePath = hasDest() ? new File(getDest()) : assetManager.sourcePath(getName()); + final File sourcePath; + if (hasDest()) { + if (destIsDirectory()) { + sourcePath = new File(getDest(), basename(getName())); + } else { + sourcePath = new File(getDest()); + } + } else { + sourcePath = assetManager.sourcePath(getName()); + } if (path.startsWith(PREFIX_CLASSPATH)) { // it's a classpath resource final String resource = path.substring(PREFIX_CLASSPATH.length()); diff --git a/src/main/java/jvcl/model/JFileExtension.java b/src/main/java/jvcl/model/JFileExtension.java index 9aef8d4..0bdd9f4 100644 --- a/src/main/java/jvcl/model/JFileExtension.java +++ b/src/main/java/jvcl/model/JFileExtension.java @@ -1,14 +1,22 @@ package jvcl.model; import com.fasterxml.jackson.annotation.JsonCreator; +import jvcl.model.info.JTrackType; import lombok.AllArgsConstructor; +import static jvcl.model.info.JTrackType.*; + @AllArgsConstructor public enum JFileExtension { - mp4 (".mp4"), - mkv (".mkv"), - raw (".yuv"); + mp4 (".mp4", video), + mkv (".mkv", video), + mp3 (".mp3", audio), + aac (".aac", audio), + flac (".flac", audio), + png (".png", image), + jpg (".jpg", image), + jpeg (".jpeg", image); @JsonCreator public static JFileExtension fromString(String v) { return valueOf(v.toLowerCase()); } @@ -20,4 +28,7 @@ public enum JFileExtension { private final String ext; public String ext() { return ext; } + private final JTrackType mediaType; + public JTrackType mediaType() { return mediaType; } + } diff --git a/src/main/java/jvcl/model/JFormat.java b/src/main/java/jvcl/model/JFormat.java index 38f3e3b..6af0e0f 100644 --- a/src/main/java/jvcl/model/JFormat.java +++ b/src/main/java/jvcl/model/JFormat.java @@ -1,17 +1,10 @@ package jvcl.model; -import com.fasterxml.jackson.databind.JsonNode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; -import java.util.Arrays; -import java.util.Optional; - -import static org.cobbzilla.util.daemon.ZillaRuntime.die; -import static org.cobbzilla.util.daemon.ZillaRuntime.empty; -import static org.cobbzilla.util.json.JsonUtil.json; import static org.cobbzilla.util.reflect.ReflectionUtil.copy; @NoArgsConstructor @Accessors(chain=true) @@ -34,36 +27,4 @@ public class JFormat { if (!hasFileExtension()) setFileExtension(other.getFileExtension()); } - public static JFormat getFormat(JsonNode formatNode, JAsset[] sources) { - if (formatNode == null) { - // no format supplied, use format from first source - return new JFormat().setFileExtension(sources[0].getFormat().getFileExtension()); - } - if (formatNode.isObject()) { - return json(formatNode, JFormat.class); - - } else if (formatNode.isTextual()) { - final JFileExtension formatType; - final String formatTypeString = formatNode.textValue(); - if (!empty(formatTypeString)) { - // is the format the name of an input? - final Optional asset = Arrays.stream(sources).filter(s -> s.getName().equals(formatTypeString)).findFirst(); - if (asset.isEmpty()) { - // not the name of an asset, must be the name of a format - formatType = JFileExtension.valueOf(formatTypeString); - } else { - // it's the name of an asset, use that asset's format - formatType = asset.get().getFormat().getFileExtension(); - } - return new JFormat().setFileExtension(formatType); - } else { - // is the format a valid format type? - if (JFileExtension.isValid(formatTypeString)) return new JFormat().setFileExtension(JFileExtension.valueOf(formatTypeString)); - return die("getFormat: invalid format type: "+formatTypeString); - } - } else { - return die("getFormat: invalid format node: "+json(formatNode)); - } - } - } diff --git a/src/main/java/jvcl/model/info/JMediaInfo.java b/src/main/java/jvcl/model/info/JMediaInfo.java index 59713c2..cf9bd7a 100644 --- a/src/main/java/jvcl/model/info/JMediaInfo.java +++ b/src/main/java/jvcl/model/info/JMediaInfo.java @@ -8,8 +8,8 @@ import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; -import static org.cobbzilla.util.daemon.ZillaRuntime.big; -import static org.cobbzilla.util.daemon.ZillaRuntime.empty; +import static java.math.BigDecimal.ZERO; +import static org.cobbzilla.util.daemon.ZillaRuntime.*; @Slf4j public class JMediaInfo { @@ -23,6 +23,7 @@ public class JMediaInfo { JTrack general = null; JTrack video = null; JTrack audio = null; + JTrack image = null; for (int i=0; i ctx, JsEngine js) { + return evalBig(zoom, ctx, js); + } + @Getter @Setter private String duration; - @Getter @Setter private String width; - @Getter @Setter private String height; - @Getter @Setter private String x; - @Getter @Setter private String y; + public BigDecimal getDuration(Map ctx, JsEngine js) { + return evalBig(duration, ctx, js); + } + @Getter @Setter private String start; + public boolean hasStart () { return !empty(start); } + public BigDecimal getStartTime(Map ctx, JsEngine js) { + return evalBig(start, ctx, js, ZERO); + } + @Getter @Setter private String end; + public boolean hasEndTime () { return !empty(end); } + public BigDecimal getEndTime(Map ctx, JsEngine js, BigDecimal defaultValue) { + return evalBig(end, ctx, js, defaultValue); + } + + @Getter @Setter private String x; + public boolean hasX () { return !empty(x); } + public BigDecimal getX(Map ctx, JsEngine js) { return evalBig(x, ctx, js); } + + @Getter @Setter private String y; + public boolean hasY () { return !empty(y); } + public BigDecimal getY(Map ctx, JsEngine js) { return evalBig(y, ctx, js); } + + @Getter @Setter private String width; + public boolean hasWidth () { return !empty(width); } + public BigDecimal getWidth(Map ctx, JsEngine js) { return evalBig(width, ctx, js); } + + @Getter @Setter private String height; + public boolean hasHeight () { return !empty(height); } + public BigDecimal getHeight(Map ctx, JsEngine js) { return evalBig(height, ctx, js); } + + @Getter @Setter private String fps; + public boolean hasFps () { return !empty(fps); } + public BigDecimal getFps(Map ctx, JsEngine js) { return evalBig(fps, ctx, js, DEFAULT_FPS); } + + @Getter @Setter private String upscale; + public boolean hasUpscale () { return !empty(upscale); } + public BigDecimal getUpscale(Map ctx, JsEngine js) { return evalBig(fps, ctx, js, DEFAULT_UPSCALE); } } diff --git a/src/main/java/jvcl/operation/OverlayOperation.java b/src/main/java/jvcl/operation/OverlayOperation.java index 4f2f32a..1addd44 100644 --- a/src/main/java/jvcl/operation/OverlayOperation.java +++ b/src/main/java/jvcl/operation/OverlayOperation.java @@ -7,12 +7,10 @@ import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.javascript.JsEngine; import java.math.BigDecimal; -import java.math.RoundingMode; import java.util.Map; -import static jvcl.service.Toolbox.eval; -import static jvcl.service.Toolbox.getDuration; -import static org.cobbzilla.util.daemon.ZillaRuntime.big; +import static java.math.BigDecimal.ZERO; +import static jvcl.service.Toolbox.evalBig; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; @Slf4j @@ -22,12 +20,12 @@ public class OverlayOperation extends JSingleSourceOperation { @Getter @Setter private String start; public BigDecimal getStartTime(Map ctx, JsEngine js) { - return empty(start) ? BigDecimal.ZERO : getDuration(eval(start, ctx, js)); + return evalBig(start, ctx, js, ZERO); } @Getter @Setter private String end; public BigDecimal getEndTime(Map ctx, JsEngine js) { - return empty(end) ? BigDecimal.ZERO : getDuration(eval(end, ctx, js)); + return evalBig(end, ctx, js); } public static class OverlayConfig { @@ -35,33 +33,30 @@ public class OverlayOperation extends JSingleSourceOperation { @Getter @Setter private String start; public BigDecimal getStartTime(Map ctx, JsEngine js) { - return empty(start) ? BigDecimal.ZERO : getDuration(eval(start, ctx, js)); + return evalBig(start, ctx, js, ZERO); } @Getter @Setter private String end; public boolean hasEndTime () { return !empty(end); } public BigDecimal getEndTime(Map ctx, JsEngine js) { - return getDuration(eval(end, ctx, js)); + return evalBig(end, 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); } + public BigDecimal getWidth(Map ctx, JsEngine js) { return evalBig(width, 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); } + public BigDecimal getHeight(Map ctx, JsEngine js) { return evalBig(height, 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); } + public BigDecimal getX(Map ctx, JsEngine js) { return evalBig(x, 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 getY(Map ctx, JsEngine js) { return evalBig(y, ctx, js); } - public BigDecimal aspectRatio () { - return big(getWidth()).divide(big(getHeight()), RoundingMode.HALF_EVEN); - } } } diff --git a/src/main/java/jvcl/operation/SplitOperation.java b/src/main/java/jvcl/operation/SplitOperation.java index 3b5f0a6..f410570 100644 --- a/src/main/java/jvcl/operation/SplitOperation.java +++ b/src/main/java/jvcl/operation/SplitOperation.java @@ -5,22 +5,30 @@ import jvcl.model.operation.JSingleSourceOperation; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.cobbzilla.util.javascript.JsEngine; import java.math.BigDecimal; +import java.util.Map; -import static jvcl.service.Toolbox.getDuration; -import static org.cobbzilla.util.daemon.ZillaRuntime.empty; +import static java.math.BigDecimal.ZERO; +import static jvcl.service.Toolbox.evalBig; @Slf4j public class SplitOperation extends JSingleSourceOperation { @Getter @Setter private String interval; - public BigDecimal getIntervalIncr() { return getDuration(interval); } + public BigDecimal getIntervalIncr(Map ctx, JsEngine js) { + return evalBig(this.interval, ctx, js); + } @Getter @Setter private String start; - public BigDecimal getStartTime() { return empty(start) ? BigDecimal.ZERO : getDuration(start); } + public BigDecimal getStartTime(Map ctx, JsEngine js) { + return evalBig(start, ctx, js, ZERO); + } @Getter @Setter private String end; - public BigDecimal getEndTime(JAsset source) { return empty(end) ? source.duration() : getDuration(end); } + public BigDecimal getEndTime(JAsset asset, Map ctx, JsEngine js) { + return evalBig(end, ctx, js, asset.duration()); + } } diff --git a/src/main/java/jvcl/operation/TrimOperation.java b/src/main/java/jvcl/operation/TrimOperation.java index d043a75..348aa77 100644 --- a/src/main/java/jvcl/operation/TrimOperation.java +++ b/src/main/java/jvcl/operation/TrimOperation.java @@ -4,21 +4,24 @@ import jvcl.model.operation.JSingleSourceOperation; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.cobbzilla.util.javascript.JsEngine; import java.math.BigDecimal; +import java.util.Map; -import static jvcl.service.Toolbox.getDuration; +import static java.math.BigDecimal.ZERO; +import static jvcl.service.Toolbox.evalBig; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; @Slf4j public class TrimOperation extends JSingleSourceOperation { @Getter @Setter private String start; - public BigDecimal getStartTime() { return empty(start) ? BigDecimal.ZERO : getDuration(start); } + public BigDecimal getStartTime(Map ctx, JsEngine js) { return evalBig(start, ctx, js, ZERO); } @Getter @Setter private String end; public boolean hasEnd() { return !empty(end); } - public BigDecimal getEndTime() { return getDuration(end); } + public BigDecimal getEndTime(Map ctx, JsEngine js) { return evalBig(end, ctx, js); } public String shortString() { return "trim_"+getStart()+(hasEnd() ? "_"+getEnd() : ""); } public String toString() { return getSource()+"_"+getStart()+(hasEnd() ? "_"+getEnd() : ""); } diff --git a/src/main/java/jvcl/operation/exec/KenBurnsExec.java b/src/main/java/jvcl/operation/exec/KenBurnsExec.java index 86f027f..c2d48a0 100644 --- a/src/main/java/jvcl/operation/exec/KenBurnsExec.java +++ b/src/main/java/jvcl/operation/exec/KenBurnsExec.java @@ -8,18 +8,36 @@ import jvcl.operation.KenBurnsOperation; 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.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +import static java.math.BigDecimal.ONE; +import static java.math.BigDecimal.ZERO; +import static jvcl.service.Toolbox.TWO; +import static jvcl.service.Toolbox.divideBig; +import static org.cobbzilla.util.io.FileUtil.abs; +import static org.cobbzilla.util.system.CommandShell.execScript; @Slf4j public class KenBurnsExec extends ExecBase { public static final String KEN_BURNS_TEMPLATE = "{{{ffmpeg}}} -i {{{source.path}}} -filter_complex \"" - + "scale={{expr width '*' 16}}x{{expr height '*' 16}}, " + + "scale={{expr width '*' upscale}}x{{expr height '*' upscale}}, " + "zoompan=" - + "z='min(zoom*{{zoomIncrementFactor}},{{zoom}})':" - + "d={{duration}}:" - + "x='if(gte(zoom,{{zoom}}),x,x+{{deltaX}}/a)':" - + "y='if(gte(zoom,{{zoom}}),y,y+{{deltaY}})':" + + "z='{{#exists startFrame}}" + + "if(between(in,{{startFrame}},{{endFrame}}),min(zoom+{{zoomIncrementFactor}},{{zoom}}),{{zoom}})" + + "{{else}}" + + "z='min(zoom+{{zoomIncrementFactor}},{{zoom}})':" + + "{{/exists}}':" + + "d={{totalFrames}}:" + + "fps={{fps}}:" + + "x='if(gte(zoom,{{zoom}}),x,x{{deltaXSign}}{{deltaX}}/a)':" + + "y='if(gte(zoom,{{zoom}}),y,y{{deltaYSign}}{{deltaY}})':" + "s={{width}}x{{height}}" + "\" -y {{{output.path}}}"; @@ -30,7 +48,58 @@ public class KenBurnsExec extends ExecBase { final JAsset output = opCtx.output; final JFileExtension formatType = opCtx.formatType; + 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 ctx = new HashMap<>(); + ctx.put("ffmpeg", toolbox.getFfmpeg()); + ctx.put("source", source); + ctx.put("output", output); + ctx.put("width", op.getWidth(ctx, js)); + ctx.put("height", op.getHeight(ctx, js)); + ctx.put("upscale", op.getUpscale(ctx, js)); + + final BigDecimal fps = op.getFps(ctx, js); + final BigDecimal duration = op.getDuration(ctx, js); + ctx.put("duration", duration); + + final BigDecimal start = op.getStartTime(ctx, js); + final BigDecimal end = op.getEndTime(ctx, js, duration.subtract(start)); + if (op.hasStart() || op.hasEndTime()) { + ctx.put("startFrame", start.multiply(fps).intValue()); + ctx.put("endFrame", end.multiply(fps).intValue()); + } + + final BigDecimal zoom = op.getZoom(ctx, js); + final BigDecimal totalFrames = duration.multiply(fps); + final BigDecimal zoomIncrementFactor = divideBig(zoom.subtract(ONE), totalFrames); + + ctx.put("zoom", zoom); + ctx.put("fps", fps.intValue()); + ctx.put("totalFrames", totalFrames.intValue()); + ctx.put("zoomIncrementFactor", zoomIncrementFactor); + + final BigDecimal midX = divideBig(source.getWidth(), TWO); + final BigDecimal midY = divideBig(source.getHeight(), TWO); + final BigDecimal destX = op.hasX() ? op.getX(ctx, js) : midX; + final BigDecimal destY = op.hasY() ? op.getY(ctx, js) : midY; + final BigDecimal deltaX = divideBig(destX.subtract(midX), totalFrames); + final BigDecimal deltaY = divideBig(destY.subtract(midY), totalFrames); + + ctx.put("deltaXSign", deltaX.compareTo(ZERO) < 0 ? "-" : "+"); + ctx.put("deltaX", deltaX.abs()); + ctx.put("deltaYSign", deltaY.compareTo(ZERO) < 0 ? "-" : "+"); + ctx.put("deltaY", deltaY.abs()); + + final String script = renderScript(toolbox, ctx, KEN_BURNS_TEMPLATE); + log.debug("operate: running script: "+script); + final String scriptOutput = execScript(script); + log.debug("operate: command output: "+scriptOutput); + assetManager.addOperationAsset(output); } } diff --git a/src/main/java/jvcl/operation/exec/OverlayExec.java b/src/main/java/jvcl/operation/exec/OverlayExec.java index 9e78a2d..7fa4944 100644 --- a/src/main/java/jvcl/operation/exec/OverlayExec.java +++ b/src/main/java/jvcl/operation/exec/OverlayExec.java @@ -14,8 +14,7 @@ import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; -import static java.math.RoundingMode.HALF_EVEN; -import static org.cobbzilla.util.daemon.ZillaRuntime.big; +import static jvcl.service.Toolbox.divideBig; import static org.cobbzilla.util.io.FileUtil.abs; import static org.cobbzilla.util.system.CommandShell.execScript; @@ -57,18 +56,20 @@ public class OverlayExec extends ExecBase { ctx.put("output", output); if (overlay.hasWidth()) { - final String width = overlay.getWidth(ctx, js); - ctx.put("width", width); + final BigDecimal width = overlay.getWidth(ctx, js); + ctx.put("width", width.intValue()); if (!overlay.hasHeight()) { - final int height = big(width).divide(overlay.aspectRatio(), HALF_EVEN).intValue(); + final BigDecimal aspectRatio = overlaySource.aspectRatio(); + final int height = divideBig(width, aspectRatio).intValue(); ctx.put("height", height); } } if (overlay.hasHeight()) { - final String height = overlay.getHeight(ctx, js); - ctx.put("height", height); + final BigDecimal height = overlay.getHeight(ctx, js); + ctx.put("height", height.intValue()); if (!overlay.hasWidth()) { - final int width = big(height).multiply(overlay.aspectRatio()).intValue(); + final BigDecimal aspectRatio = overlaySource.aspectRatio(); + final int width = height.multiply(aspectRatio).intValue(); ctx.put("width", width); } } diff --git a/src/main/java/jvcl/operation/exec/SplitExec.java b/src/main/java/jvcl/operation/exec/SplitExec.java index 6ff2756..43bbcf6 100644 --- a/src/main/java/jvcl/operation/exec/SplitExec.java +++ b/src/main/java/jvcl/operation/exec/SplitExec.java @@ -7,6 +7,7 @@ import jvcl.operation.SplitOperation; import jvcl.service.AssetManager; import jvcl.service.Toolbox; import lombok.extern.slf4j.Slf4j; +import org.cobbzilla.util.javascript.JsEngine; import java.io.File; import java.math.BigDecimal; @@ -31,12 +32,14 @@ public class SplitExec extends ExecBase { final JAsset output = opCtx.output; final JFileExtension formatType = opCtx.formatType; + final JsEngine js = toolbox.getJs(); final Map ctx = new HashMap<>(); ctx.put("ffmpeg", toolbox.getFfmpeg()); ctx.put("source", source); - final BigDecimal incr = op.getIntervalIncr(); - final BigDecimal endTime = op.getEndTime(source); - for (BigDecimal i = op.getStartTime(); + + final BigDecimal incr = op.getIntervalIncr(ctx, js); + final BigDecimal endTime = op.getEndTime(source, ctx, js); + for (BigDecimal i = op.getStartTime(ctx, js); i.compareTo(endTime) < 0; i = i.add(incr)) { diff --git a/src/main/java/jvcl/operation/exec/TrimExec.java b/src/main/java/jvcl/operation/exec/TrimExec.java index 20b7e22..92443fa 100644 --- a/src/main/java/jvcl/operation/exec/TrimExec.java +++ b/src/main/java/jvcl/operation/exec/TrimExec.java @@ -7,6 +7,7 @@ import jvcl.operation.TrimOperation; 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.math.BigDecimal; @@ -69,14 +70,16 @@ public class TrimExec extends ExecBase { JAsset subOutput, Toolbox toolbox, AssetManager assetManager) { + + final StandardJsEngine js = toolbox.getJs(); final Map ctx = new HashMap<>(); ctx.put("ffmpeg", toolbox.getFfmpeg()); ctx.put("source", source); ctx.put("output", subOutput); - final BigDecimal startTime = op.getStartTime(); + final BigDecimal startTime = op.getStartTime(ctx, js); ctx.put("startSeconds", startTime); - if (op.hasEnd()) ctx.put("interval", op.getEndTime().subtract(startTime)); + if (op.hasEnd()) ctx.put("interval", op.getEndTime(ctx, js).subtract(startTime)); final String script = renderScript(toolbox, ctx, TRIM_TEMPLATE); log.debug("operate: running script: "+script); diff --git a/src/main/java/jvcl/service/Toolbox.java b/src/main/java/jvcl/service/Toolbox.java index 3f0fc35..d5b8e4c 100644 --- a/src/main/java/jvcl/service/Toolbox.java +++ b/src/main/java/jvcl/service/Toolbox.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.Map; 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.*; import static org.cobbzilla.util.json.JsonUtil.*; @@ -28,7 +29,11 @@ public class Toolbox { public static final Toolbox DEFAULT_TOOLBOX = new Toolbox(); + public static final BigDecimal TWO = big(2); + public static final int DIVISION_SCALE = 12; + public static final ObjectMapper JSON_MAPPER = FULL_MAPPER_ALLOW_COMMENTS; + static { JSON_MAPPER.registerModule(new JOperationModule()); } @Getter(lazy=true) private final Handlebars handlebars = initHandlebars(); @@ -45,10 +50,12 @@ public class Toolbox { } } - public static BigDecimal getDuration(String t) { - // we may want to support other time formats. - // for now everything is in seconds - return big(t); + public static BigDecimal evalBig(String val, Map ctx, JsEngine js) { + return big(eval(val, ctx, js)); + } + + public static BigDecimal evalBig(String val, Map ctx, JsEngine js, BigDecimal defaultValue) { + return empty(val) ? defaultValue : evalBig(val, ctx, js); } public static Map jsContext(Map ctx) { @@ -64,6 +71,10 @@ public class Toolbox { return jsCtx; } + public static BigDecimal divideBig(BigDecimal numerator, BigDecimal denominator) { + return numerator.divide(denominator, DIVISION_SCALE, HALF_EVEN); + } + private Handlebars initHandlebars() { final Handlebars hbs = new Handlebars(new HandlebarsUtil(Toolbox.class.getSimpleName())); HandlebarsUtil.registerUtilityHelpers(hbs); diff --git a/src/main/java/jvcl/service/json/JOperationFactory.java b/src/main/java/jvcl/service/json/JOperationFactory.java index c7dee80..34bf875 100644 --- a/src/main/java/jvcl/service/json/JOperationFactory.java +++ b/src/main/java/jvcl/service/json/JOperationFactory.java @@ -8,6 +8,8 @@ import jvcl.model.operation.JOperation; import jvcl.operation.exec.ExecBase; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; import java.util.StringTokenizer; import static com.fasterxml.jackson.databind.type.TypeBindings.emptyBindings; @@ -18,13 +20,32 @@ import static org.cobbzilla.util.reflect.ReflectionUtil.forName; public class JOperationFactory extends DeserializationProblemHandler { - @Override public JavaType handleUnknownTypeId(DeserializationContext ctxt, + private final Map, JavaType> typeCache = new HashMap<>(); + + @Override public JavaType handleUnknownTypeId(DeserializationContext ctx, JavaType baseType, String subTypeId, TypeIdResolver idResolver, String failureMsg) throws IOException { + try { - return new JOperationType(getOperationClass(subTypeId), emptyBindings(), baseType, null); + final Class opClass = (Class) getOperationClass(subTypeId); + + Class superclass = (Class) opClass.getSuperclass(); + JavaType base = null; + while (!superclass.equals(JOperation.class)) { + if (superclass.equals(Object.class)) { + return die("Invalid operation class, not a subclass of JOperation: "+opClass.getName()); + } + base = typeCache.computeIfAbsent(superclass, c -> JOperationType.create(c, baseType)); + superclass = (Class) superclass.getSuperclass(); + } + if (base == null) return die("Invalid operation class, not a subclass of JOperation: "+opClass.getName()); + + typeCache.computeIfAbsent(superclass, c -> JOperationType.create(c, baseType)); + + return new JOperationType(opClass, emptyBindings(), base, null); + } catch (Exception e) { throw new IOException("handleUnknownTypeId: '"+subTypeId+"' is not a valid operation type: "+shortError(e)); } diff --git a/src/main/java/jvcl/service/json/JOperationType.java b/src/main/java/jvcl/service/json/JOperationType.java index 9c4e1fb..6685951 100644 --- a/src/main/java/jvcl/service/json/JOperationType.java +++ b/src/main/java/jvcl/service/json/JOperationType.java @@ -5,6 +5,8 @@ import com.fasterxml.jackson.databind.type.SimpleType; import com.fasterxml.jackson.databind.type.TypeBase; import com.fasterxml.jackson.databind.type.TypeBindings; +import static com.fasterxml.jackson.databind.type.TypeBindings.emptyBindings; + public class JOperationType extends SimpleType { protected JOperationType(Class cls) { super(cls); } @@ -23,4 +25,7 @@ public class JOperationType extends SimpleType { super(cls, bindings, superClass, superInts, extraHash, valueHandler, typeHandler, asStatic); } + public static JavaType create(Class c, JavaType baseType) { + return new JOperationType(c, emptyBindings(), baseType, null); + } } diff --git a/src/test/java/javicle/test/BasicTest.java b/src/test/java/javicle/test/BasicTest.java index ec4c55c..0829f29 100644 --- a/src/test/java/javicle/test/BasicTest.java +++ b/src/test/java/javicle/test/BasicTest.java @@ -25,6 +25,7 @@ public class BasicTest { } @Test public void testOverlay() { runSpec("tests/test_overlay.jvcl"); } + @Test public void testKenBurns() { runSpec("tests/test_ken_burns.jvcl"); } private void runSpec(String specPath) { try { diff --git a/src/test/resources/sources/.gitignore b/src/test/resources/sources/.gitignore index 327c2b4..ea69caa 100644 --- a/src/test/resources/sources/.gitignore +++ b/src/test/resources/sources/.gitignore @@ -1,2 +1,9 @@ *.mp4 +*.mkv +*.mp3 +*.aac +*.flac *.json +*.jpg +*.jpeg +*.png \ No newline at end of file diff --git a/src/test/resources/tests/test_ken_burns.jvcl b/src/test/resources/tests/test_ken_burns.jvcl index bbf0998..cc75e21 100644 --- a/src/test/resources/tests/test_ken_burns.jvcl +++ b/src/test/resources/tests/test_ken_burns.jvcl @@ -1,7 +1,7 @@ { "assets": [ { - "name": "img1", + "name": "javelin.jpg", "path": "https://live.staticflickr.com/65535/48159911972_01efa0e5ea_b.jpg", "dest": "src/test/resources/sources/" } @@ -10,7 +10,7 @@ { "operation": "ken-burns", // name of the operation "creates": "ken1", // asset it creates - "source": "img1", // source image + "source": "javelin.jpg", // source image "zoom": "1.3", // zoom level, from 1 to 10 "duration": "5", // how long the resulting video will be "start": "0", // when to start zooming, default is 0 diff --git a/src/test/resources/tests/test_overlay.jvcl b/src/test/resources/tests/test_overlay.jvcl index 9f44814..47c8e51 100644 --- a/src/test/resources/tests/test_overlay.jvcl +++ b/src/test/resources/tests/test_overlay.jvcl @@ -16,14 +16,14 @@ { "operation": "trim", "creates": "v1", - "trim": "vid1", + "source": "vid1", "start": "0", "end": "60" }, { "operation": "trim", "creates": "v2", - "trim": "vid2", + "source": "vid2", "start": "10", "end": "20" },