Browse Source

Ken Burns, eat your heart out

master
Jonathan Cobb 3 years ago
parent
commit
572d038893
23 changed files with 326 additions and 113 deletions
  1. +1
    -0
      README.md
  2. +13
    -4
      src/main/java/jvcl/model/JAsset.java
  3. +14
    -3
      src/main/java/jvcl/model/JFileExtension.java
  4. +0
    -39
      src/main/java/jvcl/model/JFormat.java
  5. +27
    -9
      src/main/java/jvcl/model/info/JMediaInfo.java
  6. +6
    -1
      src/main/java/jvcl/model/info/JTrack.java
  7. +12
    -1
      src/main/java/jvcl/model/info/JTrackType.java
  8. +22
    -1
      src/main/java/jvcl/model/operation/JSingleSourceOperation.java
  9. +53
    -4
      src/main/java/jvcl/operation/KenBurnsOperation.java
  10. +10
    -15
      src/main/java/jvcl/operation/OverlayOperation.java
  11. +13
    -5
      src/main/java/jvcl/operation/SplitOperation.java
  12. +6
    -3
      src/main/java/jvcl/operation/TrimOperation.java
  13. +74
    -5
      src/main/java/jvcl/operation/exec/KenBurnsExec.java
  14. +9
    -8
      src/main/java/jvcl/operation/exec/OverlayExec.java
  15. +6
    -3
      src/main/java/jvcl/operation/exec/SplitExec.java
  16. +5
    -2
      src/main/java/jvcl/operation/exec/TrimExec.java
  17. +15
    -4
      src/main/java/jvcl/service/Toolbox.java
  18. +23
    -2
      src/main/java/jvcl/service/json/JOperationFactory.java
  19. +5
    -0
      src/main/java/jvcl/service/json/JOperationType.java
  20. +1
    -0
      src/test/java/javicle/test/BasicTest.java
  21. +7
    -0
      src/test/resources/sources/.gitignore
  22. +2
    -2
      src/test/resources/tests/test_ken_burns.jvcl
  23. +2
    -2
      src/test/resources/tests/test_overlay.jvcl

+ 1
- 0
README.md View File

@@ -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
}


+ 13
- 4
src/main/java/jvcl/model/JAsset.java View File

@@ -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());


+ 14
- 3
src/main/java/jvcl/model/JFileExtension.java View File

@@ -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; }

}

+ 0
- 39
src/main/java/jvcl/model/JFormat.java View File

@@ -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<JAsset> 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));
}
}

}

+ 27
- 9
src/main/java/jvcl/model/info/JMediaInfo.java View File

@@ -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<media.getTrack().length; i++) {
final JTrack t = media.getTrack()[i];
if (t.video()) {
@@ -37,28 +38,45 @@ public class JMediaInfo {
} else {
log.warn("initFormat: multiple audio tracks found, only using the first one");
}
} else if (t.image()) {
if (image == null) {
image = t;
} else {
log.warn("initFormat: multiple image tracks found, only using the first one");
}
} else if (t.getType().equals("General") && general == null) {
general = t;
}
}
if (general == null) return die("initFormat: no general track found");

final JFormat format = new JFormat();
if (video != null) {
format.setFileExtension(JFileExtension.fromString(general.getFileExtension()))
.setHeight(video.height())
.setWidth(video.width());

} else if (audio != null) {
format.setFileExtension(JFileExtension.fromString(audio.getFileExtension()));
format.setFileExtension(JFileExtension.fromString(general.getFileExtension()));

} else if (image != null) {
format.setFileExtension(JFileExtension.fromString(general.getFileExtension()))
.setHeight(image.height())
.setWidth(image.width());

} else {
return die("initFormat: no media tracks could be found in file");
}
return format;
}

public BigDecimal duration() {
if (media == null || empty(media.getTrack())) return BigDecimal.ZERO;
if (media == null || empty(media.getTrack())) return ZERO;

// find the longest media track
BigDecimal longest = null;
for (JTrack t : media.getTrack()) {
if (!t.media()) continue;
if (!t.audioOrVideo()) continue;
if (!t.hasDuration()) continue;
final BigDecimal d = big(t.getDuration());
if (longest == null || longest.compareTo(d) < 0) longest = d;
@@ -67,10 +85,10 @@ public class JMediaInfo {
}

public BigDecimal width() {
if (media == null || empty(media.getTrack())) return BigDecimal.ZERO;
if (media == null || empty(media.getTrack())) return ZERO;
// find the first video track
for (JTrack t : media.getTrack()) {
if (!t.video()) continue;
if (!t.imageOrVideo()) continue;
if (!t.hasWidth()) continue;
return big(t.getWidth());
}
@@ -78,10 +96,10 @@ public class JMediaInfo {
}

public BigDecimal height() {
if (media == null || empty(media.getTrack())) return BigDecimal.ZERO;
if (media == null || empty(media.getTrack())) return ZERO;
// find the first video track
for (JTrack t : media.getTrack()) {
if (!t.video()) continue;
if (!t.imageOrVideo()) continue;
if (!t.hasHeight()) continue;
return big(t.getHeight());
}


+ 6
- 1
src/main/java/jvcl/model/info/JTrack.java View File

@@ -20,13 +20,18 @@ public class JTrack {
}
public boolean audio() { return type() == JTrackType.audio; }
public boolean video() { return type() == JTrackType.video; }
public boolean media() { return audio() || video(); }
public boolean image() { return type() == JTrackType.image; }
public boolean audioOrVideo() { return audio() || video(); }
public boolean imageOrVideo() { return image() || video(); }

@JsonProperty("ID") @Getter @Setter private String id;
@JsonProperty("StreamOrder") @Getter @Setter private String streamOrder;
@JsonProperty("VideoCount") @Getter @Setter private String videoCount;
@JsonProperty("AudioCount") @Getter @Setter private String audioCount;

@JsonProperty("FileExtension") @Getter @Setter private String fileExtension;
public boolean hasFileExtension () { return !empty(fileExtension); }

@JsonProperty("Format") @Getter @Setter private String format;
@JsonProperty("Format_AdditionalFeatures") @Getter @Setter private String formatAdditionalFeatures;
@JsonProperty("Format_Profile") @Getter @Setter private String formatProfile;


+ 12
- 1
src/main/java/jvcl/model/info/JTrackType.java View File

@@ -1,11 +1,22 @@
package jvcl.model.info;

import com.fasterxml.jackson.annotation.JsonCreator;
import jvcl.model.JFileExtension;
import lombok.AllArgsConstructor;

@AllArgsConstructor
public enum JTrackType {

general, audio, video, other;
general (null),
audio (JFileExtension.flac),
video (JFileExtension.mp4),
image (JFileExtension.png),
other (null);

@JsonCreator public static JTrackType fromString(String val) { return valueOf(val.toLowerCase()); }

private final JFileExtension ext;

public JFileExtension ext() { return ext; }

}

+ 22
- 1
src/main/java/jvcl/model/operation/JSingleSourceOperation.java View File

@@ -2,23 +2,44 @@ package jvcl.model.operation;

import jvcl.model.JAsset;
import jvcl.model.JFileExtension;
import jvcl.model.JFormat;
import jvcl.model.info.JTrackType;
import jvcl.service.AssetManager;
import lombok.Getter;
import lombok.Setter;

import static jvcl.model.JAsset.json2asset;
import static jvcl.model.info.JTrackType.video;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;

public class JSingleSourceOperation extends JOperation {

@Getter @Setter private String source;

protected JTrackType outputMediaType() { return video; }

public JSingleOperationContext getSingleInputContext(AssetManager assetManager) {
final JAsset source = assetManager.resolve(getSource());
final JAsset output = json2asset(getCreates());
output.mergeFormat(source.getFormat());
final JFileExtension formatType = output.getFormat().getFileExtension();

// ensure output is in the correct fprmat
final JFormat format = output.getFormat();
final JTrackType type = outputMediaType();
if (!format.hasFileExtension() || format.getFileExtension().mediaType() != type) {
final JFileExtension ext = type.ext();
if (ext == null) {
return die("getSingleInputContext: no file extension found for output media type: " + type);
}
format.setFileExtension(ext);
}
final JFileExtension formatType = getFileExtension(output);

return new JSingleOperationContext(source, output, formatType);
}

protected JFileExtension getFileExtension(JAsset output) {
return output.getFormat().getFileExtension();
}

}

+ 53
- 4
src/main/java/jvcl/operation/KenBurnsOperation.java View File

@@ -3,16 +3,65 @@ package jvcl.operation;
import jvcl.model.operation.JSingleSourceOperation;
import lombok.Getter;
import lombok.Setter;
import org.cobbzilla.util.javascript.JsEngine;

import java.math.BigDecimal;
import java.util.Map;

import static java.math.BigDecimal.ZERO;
import static jvcl.service.Toolbox.evalBig;
import static org.cobbzilla.util.daemon.ZillaRuntime.big;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;

public class KenBurnsOperation extends JSingleSourceOperation {

public static final BigDecimal DEFAULT_FPS = big(25);
public static final BigDecimal DEFAULT_UPSCALE = big(8);

@Getter @Setter private String zoom;
public BigDecimal getZoom(Map<String, Object> 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<String, Object> ctx, JsEngine js) {
return evalBig(duration, ctx, js);
}
@Getter @Setter private String start;
public boolean hasStart () { return !empty(start); }
public BigDecimal getStartTime(Map<String, Object> ctx, JsEngine js) {
return evalBig(start, ctx, js, ZERO);
}

@Getter @Setter private String end;
public boolean hasEndTime () { return !empty(end); }
public BigDecimal getEndTime(Map<String, Object> 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<String, Object> ctx, JsEngine js) { return evalBig(x, ctx, js); }

@Getter @Setter private String y;
public boolean hasY () { return !empty(y); }
public BigDecimal getY(Map<String, Object> ctx, JsEngine js) { return evalBig(y, ctx, js); }

@Getter @Setter private String width;
public boolean hasWidth () { return !empty(width); }
public BigDecimal getWidth(Map<String, Object> ctx, JsEngine js) { return evalBig(width, ctx, js); }

@Getter @Setter private String height;
public boolean hasHeight () { return !empty(height); }
public BigDecimal getHeight(Map<String, Object> ctx, JsEngine js) { return evalBig(height, ctx, js); }

@Getter @Setter private String fps;
public boolean hasFps () { return !empty(fps); }
public BigDecimal getFps(Map<String, Object> 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<String, Object> ctx, JsEngine js) { return evalBig(fps, ctx, js, DEFAULT_UPSCALE); }

}

+ 10
- 15
src/main/java/jvcl/operation/OverlayOperation.java View File

@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> ctx, JsEngine js) { return eval(width, ctx, js); }
public BigDecimal getWidth(Map<String, Object> ctx, JsEngine js) { return evalBig(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); }
public BigDecimal getHeight(Map<String, Object> ctx, JsEngine js) { return evalBig(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); }
public BigDecimal getX(Map<String, Object> ctx, JsEngine js) { return evalBig(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); }
public BigDecimal getY(Map<String, Object> ctx, JsEngine js) { return evalBig(y, ctx, js); }

public BigDecimal aspectRatio () {
return big(getWidth()).divide(big(getHeight()), RoundingMode.HALF_EVEN);
}
}
}

+ 13
- 5
src/main/java/jvcl/operation/SplitOperation.java View File

@@ -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<String, Object> 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<String, Object> 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<String, Object> ctx, JsEngine js) {
return evalBig(end, ctx, js, asset.duration());
}

}

+ 6
- 3
src/main/java/jvcl/operation/TrimOperation.java View File

@@ -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<String, Object> 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<String, Object> ctx, JsEngine js) { return evalBig(end, ctx, js); }

public String shortString() { return "trim_"+getStart()+(hasEnd() ? "_"+getEnd() : ""); }
public String toString() { return getSource()+"_"+getStart()+(hasEnd() ? "_"+getEnd() : ""); }


+ 74
- 5
src/main/java/jvcl/operation/exec/KenBurnsExec.java View File

@@ -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<KenBurnsOperation> {

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<KenBurnsOperation> {
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<String, Object> 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);
}

}

+ 9
- 8
src/main/java/jvcl/operation/exec/OverlayExec.java View File

@@ -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<OverlayOperation> {
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);
}
}


+ 6
- 3
src/main/java/jvcl/operation/exec/SplitExec.java View File

@@ -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<SplitOperation> {
final JAsset output = opCtx.output;
final JFileExtension formatType = opCtx.formatType;

final JsEngine js = toolbox.getJs();
final Map<String, Object> 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)) {



+ 5
- 2
src/main/java/jvcl/operation/exec/TrimExec.java View File

@@ -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<TrimOperation> {
JAsset subOutput,
Toolbox toolbox,
AssetManager assetManager) {

final StandardJsEngine js = toolbox.getJs();
final Map<String, Object> 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);


+ 15
- 4
src/main/java/jvcl/service/Toolbox.java View File

@@ -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<String, Object> ctx, JsEngine js) {
return big(eval(val, ctx, js));
}

public static BigDecimal evalBig(String val, Map<String, Object> ctx, JsEngine js, BigDecimal defaultValue) {
return empty(val) ? defaultValue : evalBig(val, ctx, js);
}

public static Map<String, Object> jsContext(Map<String, Object> 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);


+ 23
- 2
src/main/java/jvcl/service/json/JOperationFactory.java View File

@@ -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<Class<? extends JOperation>, 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<? extends JOperation> opClass = (Class<? extends JOperation>) getOperationClass(subTypeId);

Class<? extends JOperation> superclass = (Class<? extends JOperation>) 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<? extends JOperation>) 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));
}


+ 5
- 0
src/main/java/jvcl/service/json/JOperationType.java View File

@@ -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);
}
}

+ 1
- 0
src/test/java/javicle/test/BasicTest.java View File

@@ -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 {


+ 7
- 0
src/test/resources/sources/.gitignore View File

@@ -1,2 +1,9 @@
*.mp4
*.mkv
*.mp3
*.aac
*.flac
*.json
*.jpg
*.jpeg
*.png

+ 2
- 2
src/test/resources/tests/test_ken_burns.jvcl View File

@@ -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


+ 2
- 2
src/test/resources/tests/test_overlay.jvcl View File

@@ -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"
},


Loading…
Cancel
Save