@@ -123,7 +123,7 @@ Most of the operation settings can be JavaScript expressions, for example: | |||
The above would set the `start` value to ten seconds before the end of `someAsset`. | |||
### Supported Operations | |||
Today, JVCL supports seven basic operations. | |||
Today, JVCL supports several basic operations. | |||
For each operation listed below, the header links to an example from the JVCL test suite. | |||
@@ -149,26 +149,38 @@ For transforming still images into video via a fade-pan (aka Ken Burns) effect. | |||
Transform a video in one size to another size using black letterboxes on the sides or top/bottom. | |||
Handy for embedding mobile videos into other screen formats. | |||
### [remove-track](src/test/resources/tests/test_remove_track.jvcl) | |||
For transforming still images into video via a fade-pan (aka Ken Burns) effect. | |||
# Complex Example | |||
Here is a complex example using multiple assets and operations. | |||
Note that comments, which are not usually legal in JSON, are allowed in JVCL files. | |||
If you have other JSON-aware tools that need to read JVLC files, you may not want to | |||
use this comment syntax. The `asset` and `operation` JSON objects also support a `comment` | |||
field, which can be used as well. | |||
```json | |||
{ | |||
"assets": [ | |||
// file -- will be referenced directory | |||
{ | |||
"comment": "first video, already local", | |||
"name": "vid1", | |||
"path": "/tmp/path/to/video1.mp4" | |||
}, | |||
// URL -- will be downloaded to scratch directory and referenced from there | |||
{ | |||
"comment": "second video, will be downloaded", | |||
"name": "vid2", | |||
"path": "https://archive.org/download/gov.archives.arc.1257628/gov.archives.arc.1257628_512kb.mp4" | |||
}, | |||
// URL -- will be downloaded to `dest` directory and referenced from there | |||
{ | |||
"comment": "third video, will be downloaded", | |||
"name": "vid3", | |||
"path": "https://archive.org/download/gov.archives.arc.49442/gov.archives.arc.49442_512kb.mp4", | |||
"dest": "src/test/resources/sources/" | |||
@@ -176,13 +188,16 @@ Here is a complex example using multiple assets and operations. | |||
// Image URL | |||
{ | |||
"comment": "JPEG image, will be downloaded", | |||
"name": "img1", | |||
"path": "https://live.staticflickr.com/65535/48159911972_01efa0e5ea_b.jpg", | |||
"dest": "src/test/resources/sources/" | |||
} | |||
], | |||
"operations": [ | |||
// scale examples | |||
{ | |||
"comment": "scale using explicity height x width", | |||
"operation": "scale", // name of the operation | |||
"creates": "vid2_scaled", // asset it creates | |||
"source": "vid2", // source asset | |||
@@ -190,33 +205,45 @@ Here is a complex example using multiple assets and operations. | |||
"height": "768" // height of scaled asset. if omitted and width is present, height will be proportional | |||
}, | |||
{ | |||
"comment": "scale proportionally by a scale factor", | |||
"operation": "scale", // name of the operation | |||
"creates": "vid2_big", // asset it creates | |||
"source": "vid2", // source asset | |||
"factor": "2.2" // scale factor. if factor is set, width and height are ignored. | |||
}, | |||
// split example | |||
{ | |||
"comment": "split one asset into many", | |||
"operation": "split", // name of the operation | |||
"creates": "vid1_split_%", // assets it creates, the '%' will be replaced with a counter | |||
"source": "vid1", // split this source asset | |||
"interval": "10" // split every ten seconds | |||
}, | |||
// concat examples | |||
{ | |||
"comment": "re-combine previously split assets back together", | |||
"operation": "concat", // name of the operation | |||
"creates": "recombined_vid1", // assets it creates, the '%' will be replaced with a counter | |||
"source": ["vid1_split"] // recombine all split assets | |||
}, | |||
{ | |||
"comment": "append vid2 to the end of vid1 and create a new asset", | |||
"operation": "concat", // name of the operation | |||
"creates": "combined_vid", // asset it creates, can be referenced later | |||
"creates": "combined_vid2", // asset it creates, can be referenced later | |||
"source": ["vid1", "vid2"] // operation-specific: this says, concatenate these named assets | |||
}, | |||
{ | |||
"comment": "re-combine only some of the previously split assets", | |||
"operation": "concat", // name of the operation | |||
"creates": "combined_vid", // the asset it creates, can be referenced later | |||
"source": ["vid1", "vid2"] // operation-specific: this says, concatenate these named assets | |||
"sources": ["vid1_splits[1..2]"],// concatentate these sources -- the 2nd and 3rd files only | |||
"creates": "combined_vid3" // name of the output asset, will be written to scratch directory | |||
}, | |||
// trim example | |||
{ | |||
"comment": "trim all of the assets that were split above", | |||
"operation": "trim", // name of the operation | |||
"creates": { // create multiple files, will be prefixed with `name`, store them in `dest` | |||
"name": "vid1_trims", | |||
@@ -226,7 +253,10 @@ Here is a complex example using multiple assets and operations. | |||
"start": "1", // cropped region starts here, default is zero | |||
"end": "6" // cropped region ends here, default is end of video | |||
}, | |||
// overlay example | |||
{ | |||
"comment": "overlay one video onto another", | |||
"operation": "overlay", // name of the operation | |||
"creates": "overlay1", // asset it creates | |||
"source": "combined_vid1", // main video asset | |||
@@ -242,7 +272,10 @@ Here is a complex example using multiple assets and operations. | |||
"y": "source.height / 2" // vertical overlay position on main video. default is 0 | |||
} | |||
}, | |||
// ken-burns example | |||
{ | |||
"comment": "apply zoom-pan effect to image, creates video", | |||
"operation": "ken-burns", // name of the operation | |||
"creates": "ken1", // asset it creates | |||
"source": "img1", // source image | |||
@@ -256,13 +289,43 @@ Here is a complex example using multiple assets and operations. | |||
"width": "1024", // width of output video | |||
"height": "768" // height of output video | |||
}, | |||
// letterbox example | |||
{ | |||
"comment": "increase video size without scaling, add letterboxes as needed", | |||
"operation": "letterbox", // name of the operation | |||
"creates": "boxed1", // asset it creates | |||
"source": "ken1", // source asset | |||
"width": "source.width * 1.5", // make it wider | |||
"height": "source.height * 0.9", // and shorter | |||
"color": "AliceBlue" // default is black. can be a hex value (0xff0000 for red) or a color name from here: https://ffmpeg.org/ffmpeg-utils.html#color-syntax | |||
}, | |||
// remove-track examples | |||
{ | |||
"comment": "remove all audio tracks", | |||
"operation": "remove-track", // name of the operation | |||
"creates": "vid2_video_only", // name of the output asset | |||
"source": "vid2", // main video asset | |||
"track": "audio" // remove all audio tracks | |||
}, | |||
{ | |||
"comment": "remove all video tracks", | |||
"operation": "remove-track", // name of the operation | |||
"creates": "vid2_audio_only", // name of the output asset | |||
"source": "vid2", // main video asset | |||
"track": "video" // remove all video tracks | |||
}, | |||
{ | |||
"comment": "remove a specific audio track", | |||
"operation": "remove-track", // name of the operation | |||
"creates": "vid2_video_only2", // name of the output asset | |||
"source": "vid2", // main video asset | |||
"track": { | |||
// only remove the first audio track | |||
"type": "audio", // track type to remove | |||
"number": "0" // track number to remove | |||
} | |||
} | |||
] | |||
} | |||
@@ -3,6 +3,8 @@ package jvcl.model; | |||
import com.fasterxml.jackson.annotation.JsonIgnore; | |||
import com.fasterxml.jackson.databind.JsonNode; | |||
import jvcl.model.info.JMediaInfo; | |||
import jvcl.model.info.JTrack; | |||
import jvcl.model.info.JTrackType; | |||
import jvcl.service.AssetManager; | |||
import jvcl.service.Toolbox; | |||
import lombok.*; | |||
@@ -113,6 +115,9 @@ public class JAsset implements JsObjectView { | |||
} | |||
public boolean hasInfo() { return info != null; } | |||
@Getter @Setter private String comment; | |||
public boolean hasComment () { return !empty(comment); } | |||
@Override public boolean equals(Object o) { | |||
if (this == o) return true; | |||
if (o == null || getClass() != o.getClass()) return false; | |||
@@ -153,6 +158,10 @@ public class JAsset implements JsObjectView { | |||
public BigDecimal height() { return hasInfo() ? getInfo().height() : null; } | |||
@JsonIgnore public BigDecimal getHeight () { return height(); } | |||
public int numTracks(JTrackType type) { return hasInfo() ? getInfo().numTracks(type) : 0; } | |||
public JTrack firstTrack(JTrackType type) { return hasInfo() ? getInfo().firstTrack(type) : null; } | |||
public BigDecimal aspectRatio() { | |||
final BigDecimal width = width(); | |||
final BigDecimal height = height(); | |||
@@ -1,12 +1,14 @@ | |||
package jvcl.model; | |||
import com.fasterxml.jackson.annotation.JsonCreator; | |||
import jvcl.model.info.JTrack; | |||
import jvcl.model.info.JTrackType; | |||
import lombok.AllArgsConstructor; | |||
import lombok.extern.slf4j.Slf4j; | |||
import static jvcl.model.info.JTrackType.*; | |||
@AllArgsConstructor | |||
@AllArgsConstructor @Slf4j | |||
public enum JFileExtension { | |||
mp4 (".mp4", video), | |||
@@ -16,7 +18,9 @@ public enum JFileExtension { | |||
flac (".flac", audio), | |||
png (".png", image), | |||
jpg (".jpg", image), | |||
jpeg (".jpeg", image); | |||
jpeg (".jpeg", image), | |||
sub (".sub", subtitle), | |||
dat (".dat", data); | |||
@JsonCreator public static JFileExtension fromString(String v) { return valueOf(v.toLowerCase()); } | |||
@@ -31,4 +35,22 @@ public enum JFileExtension { | |||
private final JTrackType mediaType; | |||
public JTrackType mediaType() { return mediaType; } | |||
public static JFileExtension fromTrack(JTrack track) { | |||
if (track.hasFileExtension()) { | |||
try { | |||
return fromString(track.getFileExtension()); | |||
} catch (Exception e) { | |||
log.warn("fromTrack: unrecognized file extension: "+track.getFileExtension()); | |||
} | |||
} | |||
if (track.hasFormat()) { | |||
try { | |||
return fromString(track.getFormat()); | |||
} catch (Exception e) { | |||
log.warn("fromTrack: unrecognized format: "+track.getFormat()); | |||
} | |||
} | |||
return null; | |||
} | |||
} |
@@ -0,0 +1,34 @@ | |||
package jvcl.model; | |||
import com.fasterxml.jackson.databind.JsonNode; | |||
import jvcl.model.info.JTrackType; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import lombok.experimental.Accessors; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
@Accessors(chain=true) | |||
public class JTrackId { | |||
@Getter @Setter private JTrackType type; | |||
@Getter @Setter private Integer number; | |||
public boolean hasNumber () { return number != null; } | |||
public static JTrackId createTrackId (JsonNode val) { | |||
if (val == null) return die("createTrackId: constructor val was null"); | |||
if (val.isObject()) { | |||
return json(json(val), JTrackId.class); | |||
} else { | |||
final JTrackType trackType; | |||
try { | |||
trackType = JTrackType.fromString(val.asText()); | |||
} catch (Exception e) { | |||
return die("createTrackId: not a valid track type: "+val.asText()); | |||
} | |||
return new JTrackId().setType(trackType); | |||
} | |||
} | |||
} |
@@ -1,22 +1,34 @@ | |||
package jvcl.model.info; | |||
import jvcl.model.JFormat; | |||
import jvcl.model.JFileExtension; | |||
import jvcl.model.JFormat; | |||
import lombok.Getter; | |||
import lombok.NoArgsConstructor; | |||
import lombok.Setter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import java.math.BigDecimal; | |||
import java.util.concurrent.atomic.AtomicReference; | |||
import static java.math.BigDecimal.ZERO; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | |||
@Slf4j | |||
@NoArgsConstructor @Slf4j | |||
public class JMediaInfo { | |||
public JMediaInfo (JMediaInfo other, JFormat format) { | |||
this.media = other.getMedia(); | |||
this.formatRef.set(format); | |||
} | |||
@Getter @Setter private JMedia media; | |||
@Getter(lazy=true) private final JFormat format = initFormat(); | |||
public boolean hasFormat () { return getFormat() != null; } | |||
private final AtomicReference<JFormat> formatRef = new AtomicReference<>(); | |||
public JFormat getFormat () { | |||
if (formatRef.get() == null) formatRef.set(initFormat()); | |||
return formatRef.get(); | |||
} | |||
private JFormat initFormat () { | |||
if (media == null || empty(media.getTrack())) return null; | |||
@@ -106,4 +118,21 @@ public class JMediaInfo { | |||
return null; | |||
} | |||
public int numTracks(JTrackType type) { | |||
if (media == null || empty(media.getTrack())) return 0; | |||
int count = 0; | |||
for (JTrack t : media.getTrack()) { | |||
if (t.type() == type) count++; | |||
} | |||
return count; | |||
} | |||
public JTrack firstTrack(JTrackType type) { | |||
if (media == null || empty(media.getTrack())) return null; | |||
for (JTrack t : media.getTrack()) { | |||
if (t.type() == type) return t; | |||
} | |||
return null; | |||
} | |||
} |
@@ -33,6 +33,8 @@ public class JTrack { | |||
public boolean hasFileExtension () { return !empty(fileExtension); } | |||
@JsonProperty("Format") @Getter @Setter private String format; | |||
public boolean hasFormat () { return !empty(format); } | |||
@JsonProperty("Format_AdditionalFeatures") @Getter @Setter private String formatAdditionalFeatures; | |||
@JsonProperty("Format_Profile") @Getter @Setter private String formatProfile; | |||
@JsonProperty("Format_Level") @Getter @Setter private String formatLevel; | |||
@@ -7,16 +7,21 @@ import lombok.AllArgsConstructor; | |||
@AllArgsConstructor | |||
public enum JTrackType { | |||
general (null), | |||
audio (JFileExtension.flac), | |||
video (JFileExtension.mp4), | |||
image (JFileExtension.png), | |||
other (null); | |||
general (null, null), | |||
audio (JFileExtension.flac, "a"), | |||
video (JFileExtension.mp4, "v"), | |||
image (JFileExtension.png, null), | |||
subtitle(JFileExtension.png, "s"), | |||
data (JFileExtension.png, "d"), | |||
other (null, null); | |||
@JsonCreator public static JTrackType fromString(String val) { return valueOf(val.toLowerCase()); } | |||
private final JFileExtension ext; | |||
public JFileExtension ext() { return ext; } | |||
private final String ffmpegType; | |||
public String ffmpegType() { return ffmpegType; } | |||
public boolean hasFfmpegType() { return ffmpegType != null; } | |||
} |
@@ -14,6 +14,7 @@ import java.util.HashMap; | |||
import java.util.Map; | |||
import static jvcl.service.json.JOperationFactory.getOperationExecClass; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.hashOf; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
import static org.cobbzilla.util.reflect.ReflectionUtil.instantiate; | |||
@@ -33,6 +34,9 @@ public abstract class JOperation { | |||
@Getter @Setter private JsonNode creates; | |||
@Getter @Setter private boolean noExec = false; | |||
@Getter @Setter private String comment; | |||
public boolean hasComment () { return !empty(comment); } | |||
public String hash(JAsset[] sources) { return hash(sources, null); } | |||
public String hash(JAsset[] sources, Object[] args) { | |||
@@ -23,7 +23,7 @@ public class JSingleSourceOperation extends JOperation { | |||
final JAsset output = json2asset(getCreates()); | |||
output.mergeFormat(source.getFormat()); | |||
// ensure output is in the correct fprmat | |||
// ensure output is in the correct format | |||
final JFormat format = output.getFormat(); | |||
final JTrackType type = outputMediaType(); | |||
if (!format.hasFileExtension() || format.getFileExtension().mediaType() != type) { | |||
@@ -33,12 +33,12 @@ public class JSingleSourceOperation extends JOperation { | |||
} | |||
format.setFileExtension(ext); | |||
} | |||
final JFileExtension formatType = getFileExtension(output); | |||
final JFileExtension formatType = getFileExtension(source, output); | |||
return new JSingleOperationContext(source, output, formatType); | |||
} | |||
protected JFileExtension getFileExtension(JAsset output) { | |||
protected JFileExtension getFileExtension(JAsset source, JAsset output) { | |||
return output.getFormat().getFileExtension(); | |||
} | |||
@@ -0,0 +1,58 @@ | |||
package jvcl.operation; | |||
import com.fasterxml.jackson.annotation.JsonIgnore; | |||
import com.fasterxml.jackson.databind.JsonNode; | |||
import jvcl.model.JAsset; | |||
import jvcl.model.JFileExtension; | |||
import jvcl.model.JFormat; | |||
import jvcl.model.JTrackId; | |||
import jvcl.model.info.JMediaInfo; | |||
import jvcl.model.info.JTrack; | |||
import jvcl.model.info.JTrackType; | |||
import jvcl.model.operation.JSingleSourceOperation; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import static jvcl.model.JTrackId.createTrackId; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||
public class RemoveTrackOperation extends JSingleSourceOperation { | |||
@Getter @Setter private JsonNode track; | |||
@JsonIgnore @Getter(lazy=true) private final JTrackId trackId = initTrackId(); | |||
private JTrackId initTrackId() { | |||
final JTrackId trackId = createTrackId(getTrack()); | |||
final JTrackType trackType = trackId.getType(); | |||
if (!trackType.hasFfmpegType()) die("initTrackType: cannot remove tracks of type "+ trackType); | |||
return trackId; | |||
} | |||
@Override protected JFileExtension getFileExtension(JAsset source, JAsset output) { | |||
final JTrackId trackId = getTrackId(); | |||
final JTrackType trackType = trackId.getType(); | |||
// if we are removing all video tracks, the output will be an audio asset | |||
final int trackCount = source.numTracks(trackType); | |||
if (trackCount == 0) return die("getFileExtension: no tracks of type "+ trackType +" found in source: "+source); | |||
if (wouldRemoveAllVideoTracks(trackId, trackCount)) { | |||
// find the format of the first audio track | |||
final JTrack audio = source.firstTrack(JTrackType.audio); | |||
if (audio == null) return die("getFileExtension: no audio tracks found!"); | |||
final JFileExtension ext = JFileExtension.fromTrack(audio); | |||
source.setInfo(new JMediaInfo(source.getInfo(), new JFormat().setFileExtension(ext))); | |||
return ext; | |||
} | |||
return super.getFileExtension(source, output); | |||
} | |||
private boolean wouldRemoveAllVideoTracks(JTrackId trackId, int trackCount) { | |||
if (trackId.getType() != JTrackType.video) return false; | |||
if (!trackId.hasNumber()) return true; | |||
return trackCount == 1; | |||
} | |||
} |
@@ -64,7 +64,6 @@ public class LetterboxExec extends SingleOrMultiSourceExecBase<LetterboxOperatio | |||
JAsset subOutput, | |||
Toolbox toolbox, | |||
AssetManager assetManager) { | |||
ctx.put("source", source); | |||
ctx.put("output", subOutput); | |||
final String script = renderScript(toolbox, ctx, LETTERBOX_TEMPLATE); | |||
@@ -72,11 +71,6 @@ public class LetterboxExec extends SingleOrMultiSourceExecBase<LetterboxOperatio | |||
log.debug("operate: running script: "+script); | |||
final String scriptOutput = exec(script, op.isNoExec()); | |||
log.debug("operate: command output: "+scriptOutput); | |||
if (output == subOutput) { | |||
assetManager.addOperationAsset(output); | |||
} else { | |||
assetManager.addOperationAssetSlice(output, subOutput); | |||
} | |||
} | |||
} |
@@ -0,0 +1,60 @@ | |||
package jvcl.operation.exec; | |||
import jvcl.model.JAsset; | |||
import jvcl.model.JFileExtension; | |||
import jvcl.model.JTrackId; | |||
import jvcl.model.info.JTrackType; | |||
import jvcl.model.operation.JSingleOperationContext; | |||
import jvcl.operation.RemoveTrackOperation; | |||
import jvcl.service.AssetManager; | |||
import jvcl.service.Toolbox; | |||
import lombok.extern.slf4j.Slf4j; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
@Slf4j | |||
public class RemoveTrackExec extends SingleOrMultiSourceExecBase<RemoveTrackOperation> { | |||
public static final String REMOVE_TRACK_TEMPLATE | |||
= "{{{ffmpeg}}} -i {{{source.path}}} " | |||
+ "-map 0 -map -0:{{trackType}}{{#exists trackNumber}}:{{trackNumber}}{{/exists}} " | |||
+ "-c copy " | |||
+ "-y {{{output.path}}}"; | |||
@Override public void operate(RemoveTrackOperation op, Toolbox toolbox, AssetManager assetManager) { | |||
final JSingleOperationContext opCtx = op.getSingleInputContext(assetManager); | |||
final JAsset source = opCtx.source; | |||
final JAsset output = opCtx.output; | |||
final JFileExtension formatType = opCtx.formatType; | |||
final Map<String, Object> ctx = new HashMap<>(); | |||
ctx.put("ffmpeg", toolbox.getFfmpeg()); | |||
ctx.put("source", source); | |||
final JTrackId trackId = op.getTrackId(); | |||
final JTrackType trackType = trackId.getType(); | |||
ctx.put("trackType", trackType.ffmpegType()); | |||
if (trackId.hasNumber()) ctx.put("trackNumber", trackId.getNumber()); | |||
operate(op, toolbox, assetManager, source, output, formatType, ctx); | |||
} | |||
@Override protected void process(Map<String, Object> ctx, | |||
RemoveTrackOperation op, | |||
JAsset source, | |||
JAsset output, | |||
JAsset subOutput, | |||
Toolbox toolbox, | |||
AssetManager assetManager) { | |||
ctx.put("source", source); | |||
ctx.put("output", subOutput); | |||
final String script = renderScript(toolbox, ctx, REMOVE_TRACK_TEMPLATE); | |||
log.debug("operate: running script: "+script); | |||
final String scriptOutput = exec(script, op.isNoExec()); | |||
log.debug("operate: command output: "+scriptOutput); | |||
} | |||
} |
@@ -58,11 +58,6 @@ public class ScaleExec extends SingleOrMultiSourceExecBase<ScaleOperation> { | |||
log.debug("operate: running script: "+script); | |||
final String scriptOutput = exec(script, op.isNoExec()); | |||
log.debug("operate: command output: "+scriptOutput); | |||
if (output == subOutput) { | |||
assetManager.addOperationAsset(output); | |||
} else { | |||
assetManager.addOperationAssetSlice(output, subOutput); | |||
} | |||
} | |||
} |
@@ -37,6 +37,7 @@ public abstract class SingleOrMultiSourceExecBase<OP extends JOperation> extends | |||
} | |||
subOutput.setPath(abs(outfile)); | |||
process(ctx, op, asset, output, subOutput, toolbox, assetManager); | |||
assetManager.addOperationAssetSlice(output, subOutput); | |||
} | |||
} else { | |||
final File defaultOutfile = assetManager.assetPath(op, source, formatType); | |||
@@ -44,6 +45,7 @@ public abstract class SingleOrMultiSourceExecBase<OP extends JOperation> extends | |||
if (path == null) return; | |||
output.setPath(abs(path)); | |||
process(ctx, op, source, output, output, toolbox, assetManager); | |||
assetManager.addOperationAsset(output); | |||
} | |||
} | |||
@@ -55,5 +57,4 @@ public abstract class SingleOrMultiSourceExecBase<OP extends JOperation> extends | |||
Toolbox toolbox, | |||
AssetManager assetManager); | |||
} |
@@ -55,11 +55,6 @@ public class TrimExec extends SingleOrMultiSourceExecBase<TrimOperation> { | |||
log.debug("operate: running script: "+script); | |||
final String scriptOutput = exec(script, op.isNoExec()); | |||
log.debug("operate: command output: "+scriptOutput); | |||
if (output == subOutput) { | |||
assetManager.addOperationAsset(output); | |||
} else { | |||
assetManager.addOperationAssetSlice(output, subOutput); | |||
} | |||
} | |||
} |
@@ -26,8 +26,9 @@ public class BasicTest { | |||
runSpec("tests/test_letterbox.jvcl"); | |||
} | |||
@Test public void testOverlay() { runSpec("tests/test_overlay.jvcl"); } | |||
@Test public void testKenBurns() { runSpec("tests/test_ken_burns.jvcl"); } | |||
@Test public void testOverlay () { runSpec("tests/test_overlay.jvcl"); } | |||
@Test public void testKenBurns () { runSpec("tests/test_ken_burns.jvcl"); } | |||
@Test public void testRemoveTrack () { runSpec("tests/test_remove_track.jvcl"); } | |||
private void runSpec(String specPath) { | |||
try { | |||
@@ -15,7 +15,7 @@ | |||
{ | |||
"operation": "concat", // name of the operation | |||
"sources": ["vid1_splits[1..2]"], // concatentate these sources -- the 2nd and 3rd files only | |||
"creates": "combined_vid2", // name of the output asset, will be written to scratch directory | |||
"creates": "combined_vid2" // name of the output asset, will be written to scratch directory | |||
} | |||
] | |||
} |
@@ -0,0 +1,42 @@ | |||
{ | |||
"assets": [ | |||
// this is a US government video not covered by copyright | |||
{ | |||
"name": "vid2", | |||
"path": "https://archive.org/download/gov.archives.arc.49442/gov.archives.arc.49442_512kb.mp4", | |||
"dest": "src/test/resources/sources/" | |||
} | |||
], | |||
"operations": [ | |||
// trim video first, so test runs faster | |||
{ | |||
"operation": "trim", | |||
"creates": "v2", | |||
"source": "vid2", | |||
"start": "0", | |||
"end": "20" | |||
}, | |||
{ | |||
"operation": "remove-track", // name of the operation | |||
"creates": "vid2_video_only", // name of the output asset | |||
"source": "v2", // main video asset | |||
"track": "audio" // remove all audio tracks | |||
}, | |||
{ | |||
"operation": "remove-track", // name of the operation | |||
"creates": "vid2_audio_only", // name of the output asset | |||
"source": "v2", // main video asset | |||
"track": "video" // remove all video tracks | |||
}, | |||
{ | |||
"operation": "remove-track", // name of the operation | |||
"creates": "vid2_video_only2", // name of the output asset | |||
"source": "v2", // main video asset | |||
"track": { | |||
// only remove the first audio track | |||
"type": "audio", // track type to remove | |||
"number": "0" // track number to remove | |||
} | |||
} | |||
] | |||
} |