@@ -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 | |||
} | |||
@@ -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()); | |||
@@ -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; } | |||
} |
@@ -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)); | |||
} | |||
} | |||
} |
@@ -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()); | |||
} | |||
@@ -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; | |||
@@ -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; } | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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); } | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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() : ""); } | |||
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
@@ -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)) { | |||
@@ -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); | |||
@@ -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); | |||
@@ -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,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); | |||
} | |||
} |
@@ -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 { | |||
@@ -1,2 +1,9 @@ | |||
*.mp4 | |||
*.mkv | |||
*.mp3 | |||
*.aac | |||
*.flac | |||
*.json | |||
*.jpg | |||
*.jpeg | |||
*.png |
@@ -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 | |||
@@ -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" | |||
}, | |||