@@ -47,4 +47,54 @@ What variables are available in the JS context? And what properties do they have | |||||
Assets can be defined in the JVC's `assets` array, or as the output of an | Assets can be defined in the JVC's `assets` array, or as the output of an | ||||
operation. | operation. | ||||
Assets are referenced by their asset name. | |||||
Assets are referenced by their asset name. Assets have some useful properties: | |||||
#### `duration` | |||||
Duration in seconds of the asset (audio or video). | |||||
#### `width` | |||||
Resolution width in pixes (video or image). | |||||
#### `height` | |||||
Resolution height in pixes (video or image). | |||||
#### `tracks` | |||||
An array of the tracks in a video. Only includes audio and video tracks. | |||||
#### `audioTracks` | |||||
An array of the audio tracks in a video. | |||||
#### `videoTracks` | |||||
An array of the video tracks in a video. | |||||
#### `assets` | |||||
If an asset is a list asset, this is an array of the sub-assets. Each sub-asset | |||||
has the same properties described above. | |||||
#### more properties | |||||
The list of asset properties supported today is fairly limited. | |||||
More properties will be exposed in the future. | |||||
### JS Functions | |||||
The JavaScript environment that JVC sets up includes some useful built-in | |||||
functions. | |||||
#### `is_within` | |||||
Check if a variable is "close" to another value by comparing against a delta. | |||||
```js | |||||
x = 10.2 | |||||
is_within(x, 10, 1); // true | |||||
is_within(x, 9.5, 0.5); // false | |||||
is_within(x, 10.3, 0.1); // true | |||||
``` | |||||
#### `is_within_pct` | |||||
Check if a variable is "close" to another value by percentage. The percentage | |||||
argument is an integer where 1 == 1% and 100 == 100% | |||||
```js | |||||
x = 10.2 | |||||
is_within_pct(x, 10, 2); // true | |||||
is_within_pct(x, 9, 10); // false (9 + (10% of 9) only gets you to 9.9, x is 10.2) | |||||
is_within_pct(x, 10.3, 10); // true | |||||
``` | |||||
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode; | |||||
import jvc.model.info.JMediaInfo; | import jvc.model.info.JMediaInfo; | ||||
import jvc.model.info.JTrack; | import jvc.model.info.JTrack; | ||||
import jvc.model.info.JTrackType; | import jvc.model.info.JTrackType; | ||||
import jvc.model.js.JAssetJs; | |||||
import jvc.service.AssetManager; | import jvc.service.AssetManager; | ||||
import jvc.service.Toolbox; | import jvc.service.Toolbox; | ||||
import lombok.*; | import lombok.*; | ||||
@@ -44,8 +45,11 @@ public class JAsset implements JsObjectView { | |||||
public JAsset(JAsset other) { copy(this, other, null, COPY_EXCLUDE_FIELDS); } | public JAsset(JAsset other) { copy(this, other, null, COPY_EXCLUDE_FIELDS); } | ||||
public JAsset(JAsset other, File file) { this(other); setPath(abs(file)); } | |||||
@Getter @Setter private String name; | @Getter @Setter private String name; | ||||
@Getter @Setter private String path; | @Getter @Setter private String path; | ||||
public boolean hasPath() { return !empty(path); } | public boolean hasPath() { return !empty(path); } | ||||
// an asset can specify where its file should live | // an asset can specify where its file should live | ||||
@@ -114,7 +118,7 @@ public class JAsset implements JsObjectView { | |||||
setFormat(info.getFormat()); | setFormat(info.getFormat()); | ||||
return this; | return this; | ||||
} | } | ||||
public boolean hasInfo() { return info != null; } | |||||
public boolean hasInfo() { return info != null && !info.emptyMedia(); } | |||||
@Getter @Setter private String comment; | @Getter @Setter private String comment; | ||||
public boolean hasComment () { return !empty(comment); } | public boolean hasComment () { return !empty(comment); } | ||||
@@ -285,19 +289,4 @@ public class JAsset implements JsObjectView { | |||||
@Override public Object toJs() { return new JAssetJs(this); } | @Override public Object toJs() { return new JAssetJs(this); } | ||||
public static class JAssetJs { | |||||
public Double duration; | |||||
public Integer width; | |||||
public Integer height; | |||||
public JAssetJs (JAsset asset) { | |||||
final BigDecimal d = asset.duration(); | |||||
this.duration = d == null ? null : d.doubleValue(); | |||||
final BigDecimal w = asset.width(); | |||||
this.width = w == null ? null : w.intValue(); | |||||
final BigDecimal h = asset.height(); | |||||
this.height = h == null ? null : h.intValue(); | |||||
} | |||||
} | |||||
} | } |
@@ -22,7 +22,7 @@ public class JMediaInfo { | |||||
} | } | ||||
@Getter @Setter private JMedia media; | @Getter @Setter private JMedia media; | ||||
private boolean emptyMedia() { return media == null || empty(media.getTrack()); } | |||||
public boolean emptyMedia() { return media == null || empty(media.getTrack()); } | |||||
private final AtomicReference<JFormat> formatRef = new AtomicReference<>(); | private final AtomicReference<JFormat> formatRef = new AtomicReference<>(); | ||||
@@ -0,0 +1,61 @@ | |||||
package jvc.model.js; | |||||
import jvc.model.JAsset; | |||||
import jvc.model.info.JMediaInfo; | |||||
import jvc.model.info.JTrack; | |||||
import org.cobbzilla.util.collection.ArrayUtil; | |||||
import java.math.BigDecimal; | |||||
import static jvc.model.js.JTrackJs.EMPTY_TRACKS; | |||||
public class JAssetJs { | |||||
public static final JAssetJs[] EMPTY_ASSETS = new JAssetJs[0]; | |||||
public Double duration; | |||||
public Integer width; | |||||
public Integer height; | |||||
public JTrackJs[] tracks = EMPTY_TRACKS; | |||||
public JTrackJs[] videoTracks = EMPTY_TRACKS; | |||||
public JTrackJs[] audioTracks = EMPTY_TRACKS; | |||||
public JAssetJs[] assets = EMPTY_ASSETS; | |||||
public JAssetJs(JAsset asset) { | |||||
final BigDecimal d = asset.duration(); | |||||
this.duration = d == null ? null : d.doubleValue(); | |||||
final BigDecimal w = asset.width(); | |||||
this.width = w == null ? null : w.intValue(); | |||||
final BigDecimal h = asset.height(); | |||||
this.height = h == null ? null : h.intValue(); | |||||
if (asset.hasInfo()) { | |||||
final JMediaInfo info = asset.getInfo(); | |||||
for (JTrack track : info.getMedia().getTrack()) { | |||||
if (!track.audioOrVideo()) continue; | |||||
final JTrackJs trackJs = new JTrackJs(track.type().name()); | |||||
tracks = ArrayUtil.append(tracks, trackJs); | |||||
switch (track.type()) { | |||||
case audio: | |||||
audioTracks = ArrayUtil.append(audioTracks, trackJs); | |||||
break; | |||||
case video: | |||||
videoTracks = ArrayUtil.append(videoTracks, trackJs); | |||||
break; | |||||
} | |||||
} | |||||
} | |||||
if (asset.hasListAssets()) { | |||||
final JAsset[] list = asset.getList(); | |||||
this.assets = new JAssetJs[list.length]; | |||||
for (int i = 0; i < assets.length; i++) { | |||||
this.assets[i] = new JAssetJs(list[i]); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,16 @@ | |||||
package jvc.model.js; | |||||
import lombok.AllArgsConstructor; | |||||
import lombok.Getter; | |||||
import lombok.NoArgsConstructor; | |||||
import lombok.Setter; | |||||
import lombok.experimental.Accessors; | |||||
@NoArgsConstructor @AllArgsConstructor @Accessors(chain=true) | |||||
public class JTrackJs { | |||||
public static final JTrackJs[] EMPTY_TRACKS = new JTrackJs[0]; | |||||
@Getter @Setter public String type; | |||||
} |
@@ -43,7 +43,8 @@ public abstract class SingleOrMultiSourceExecBase<OP extends JSingleSourceOperat | |||||
outfile = new File(output.destDirectory(), basename(appendToFileNameBeforeExt(asset.getPath(), "_"+op.shortString()))); | outfile = new File(output.destDirectory(), basename(appendToFileNameBeforeExt(asset.getPath(), "_"+op.shortString()))); | ||||
if (outfile.exists()) { | if (outfile.exists()) { | ||||
log.info("operate: dest exists: "+abs(outfile)); | log.info("operate: dest exists: "+abs(outfile)); | ||||
return ctx; | |||||
assetManager.addOperationAssetSlice(output, subOutput.setPath(abs(outfile))); | |||||
continue; | |||||
} | } | ||||
} else { | } else { | ||||
outfile = defaultOutfile; | outfile = defaultOutfile; | ||||
@@ -60,6 +61,7 @@ public abstract class SingleOrMultiSourceExecBase<OP extends JSingleSourceOperat | |||||
process(ctx, op, source, output, output, toolbox, assetManager); | process(ctx, op, source, output, output, toolbox, assetManager); | ||||
assetManager.addOperationAsset(output); | assetManager.addOperationAsset(output); | ||||
} | } | ||||
ctx.put("output", output); | |||||
return ctx; | return ctx; | ||||
} | } | ||||
@@ -48,22 +48,21 @@ public class SplitExec extends ExecBase<SplitOperation> { | |||||
if (output.destIsDirectory()) { | if (output.destIsDirectory()) { | ||||
outfile = sliceFile(output, formatType, i, incr); | outfile = sliceFile(output, formatType, i, incr); | ||||
} else { | } else { | ||||
die("dest exists and is not a directory: "+output.getDest()); | |||||
return null; | |||||
return die("dest exists and is not a directory: "+output.getDest()); | |||||
} | } | ||||
} | } | ||||
} else { | } else { | ||||
outfile = assetManager.assetPath(op, source, formatType, new Object[]{i, incr}); | outfile = assetManager.assetPath(op, source, formatType, new Object[]{i, incr}); | ||||
} | } | ||||
final JAsset slice = new JAsset(output, outfile); | |||||
if (outfile.exists()) { | if (outfile.exists()) { | ||||
log.info("operate: outfile exists, not re-creating: "+abs(outfile)); | log.info("operate: outfile exists, not re-creating: "+abs(outfile)); | ||||
assetManager.addOperationAssetSlice(output, slice); | |||||
continue; | continue; | ||||
} else { | } else { | ||||
mkdirOrDie(outfile.getParentFile()); | mkdirOrDie(outfile.getParentFile()); | ||||
} | } | ||||
final JAsset slice = new JAsset(output); | |||||
slice.setPath(abs(outfile)); | |||||
slice.setName(source.getName()+"_"+i+"_"+incr); | slice.setName(source.getName()+"_"+i+"_"+incr); | ||||
ctx.put("output", slice); | ctx.put("output", slice); | ||||
@@ -76,7 +75,9 @@ public class SplitExec extends ExecBase<SplitOperation> { | |||||
log.debug("operate: command output: "+scriptOutput); | log.debug("operate: command output: "+scriptOutput); | ||||
assetManager.addOperationAssetSlice(output, slice); | assetManager.addOperationAssetSlice(output, slice); | ||||
} | } | ||||
log.info("operate: completed"); | log.info("operate: completed"); | ||||
ctx.put("output", output); | |||||
return ctx; | return ctx; | ||||
} | } | ||||
@@ -5,6 +5,7 @@ import jvc.model.operation.JOperation; | |||||
import jvc.model.operation.JValidationResult; | import jvc.model.operation.JValidationResult; | ||||
import jvc.operation.exec.ExecBase; | import jvc.operation.exec.ExecBase; | ||||
import lombok.Getter; | import lombok.Getter; | ||||
import lombok.extern.slf4j.Slf4j; | |||||
import org.cobbzilla.util.javascript.JsEngine; | import org.cobbzilla.util.javascript.JsEngine; | ||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
@@ -13,6 +14,7 @@ import java.util.List; | |||||
import java.util.Map; | import java.util.Map; | ||||
import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||
@Slf4j | |||||
public class JvcEngine { | public class JvcEngine { | ||||
private final Toolbox toolbox; | private final Toolbox toolbox; | ||||
@@ -38,7 +40,12 @@ public class JvcEngine { | |||||
.setExecIndex(completed.size()) | .setExecIndex(completed.size()) | ||||
.setNoExec(noExec) | .setNoExec(noExec) | ||||
.getExec(); | .getExec(); | ||||
final Map<String, Object> ctx = exec.operate(op, toolbox, assetManager); | final Map<String, Object> ctx = exec.operate(op, toolbox, assetManager); | ||||
if (ctx == null) { | |||||
log.debug("runOp("+op.getOperation()+"): noop"); | |||||
return; | |||||
} | |||||
if (op.hasValidate()) { | if (op.hasValidate()) { | ||||
final JsEngine js = toolbox.getJs(); | final JsEngine js = toolbox.getJs(); | ||||
@@ -14,15 +14,23 @@ | |||||
"creates": "v2", | "creates": "v2", | ||||
"source": "vid2", | "source": "vid2", | ||||
"start": "10", | "start": "10", | ||||
"end": "30" | |||||
"end": "30", | |||||
"validate": [{ | |||||
"comment": "expect output to be about 20 seconds long, give or take 0.1 seconds", | |||||
"test": "is_within(output.duration, 20, 0.1)" | |||||
}] | |||||
}, | }, | ||||
{ | { | ||||
"operation": "adjust-speed", // name of the operation | "operation": "adjust-speed", // name of the operation | ||||
"creates": "quickened", // output asset name | "creates": "quickened", // output asset name | ||||
"source": "v2", // main video asset | "source": "v2", // main video asset | ||||
"factor": "4", // factor=1 is no change, factor>1 is faster, factor<1 is slower | "factor": "4", // factor=1 is no change, factor>1 is faster, factor<1 is slower | ||||
"audio": "silent" // audio: silent (default), unchanged, match | |||||
"audio": "silent", // audio: silent (default), unchanged, match | |||||
// if audio is match, then factor must be between 0.5 and 100 | // if audio is match, then factor must be between 0.5 and 100 | ||||
"validate": [{ | |||||
"comment": "expect output to be about 5 seconds long, give or take 0.1 seconds", | |||||
"test": "is_within(output.duration, 5, 0.1)" | |||||
}] | |||||
} | } | ||||
] | ] | ||||
} | } |
@@ -10,12 +10,20 @@ | |||||
"creates": { | "creates": { | ||||
"name": "combined_vid1", // name of the output asset | "name": "combined_vid1", // name of the output asset | ||||
"dest": "src/test/resources/outputs/combined1.mp4" // asset will be written to this file | "dest": "src/test/resources/outputs/combined1.mp4" // asset will be written to this file | ||||
} | |||||
}, | |||||
"validate": [{ | |||||
"comment": "expect output to be about 40 seconds long, give or take 0.1 seconds", | |||||
"test": "is_within(output.duration, 40, 0.1)" | |||||
}] | |||||
}, | }, | ||||
{ | { | ||||
"operation": "concat", // name of the operation | "operation": "concat", // name of the operation | ||||
"sources": ["vid1_splits[1..2]"], // concatentate these sources -- the 2nd and 3rd files only | "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 | |||||
"validate": [{ | |||||
"comment": "expect output to be about 20 seconds long, give or take 0.1 seconds", | |||||
"test": "is_within(output.duration, 20, 0.1)" | |||||
}] | |||||
} | } | ||||
] | ] | ||||
} | } |
@@ -19,7 +19,14 @@ | |||||
"x": "source.width * 0.6", // pan to this x-position | "x": "source.width * 0.6", // pan to this x-position | ||||
"y": "source.height * 0.4", // pan to this y-position | "y": "source.height * 0.4", // pan to this y-position | ||||
"width": "1024", // width of output video | "width": "1024", // width of output video | ||||
"height": "768" // height of output video | |||||
"height": "768", // height of output video | |||||
"validate": [{ | |||||
"comment": "expect output to be about 5 seconds long, give or take 0.1 seconds", | |||||
"test": "is_within(output.duration, 5, 0.1)" | |||||
}, { | |||||
"comment": "expect output resolution of 1024x768", | |||||
"test": "output.width === 1024 && output.height === 768" | |||||
}] | |||||
} | } | ||||
] | ] | ||||
} | } |
@@ -1,6 +1,6 @@ | |||||
{ | { | ||||
"assets": [ | "assets": [ | ||||
// wildcard matches multiple files, vid1_splits becomes a "list" asset | |||||
// wildcard matches multiple files, vid1_splits becomes a "list" asset. resolution is 320x240 | |||||
{ "name": "vid1_splits", "path": "src/test/resources/outputs/vid1_splits_*.mp4" } | { "name": "vid1_splits", "path": "src/test/resources/outputs/vid1_splits_*.mp4" } | ||||
], | ], | ||||
"operations": [ | "operations": [ | ||||
@@ -10,7 +10,17 @@ | |||||
"source": "vid1_splits[2]", // box this source asset | "source": "vid1_splits[2]", // box this source asset | ||||
"width": "source.width * 2", // width of output asset. if omitted and height is present, width will be proportional | "width": "source.width * 2", // width of output asset. if omitted and height is present, width will be proportional | ||||
"height": "source.height", // height of output asset. if omitted and width is present, height will be proportional | "height": "source.height", // height of output asset. if omitted and width is present, height will be proportional | ||||
"color": "AliceBlue" // letterbox padding color | |||||
"color": "AliceBlue", // letterbox padding color | |||||
"validate": [{ | |||||
"comment": "expect output resolution with double source width", | |||||
"test": "output.width === 2 * source.width" | |||||
}, { | |||||
"comment": "expect output resolution with same height as source", | |||||
"test": "output.height === source.height" | |||||
}, { | |||||
"comment": "expect output resolution of 640x240", | |||||
"test": "output.width === 640 && output.height === 240" | |||||
}] | |||||
}, | }, | ||||
{ | { | ||||
"operation": "letterbox", // name of the operation | "operation": "letterbox", // name of the operation | ||||
@@ -18,7 +28,17 @@ | |||||
"source": "vid1_splits[2]", // box this source assets | "source": "vid1_splits[2]", // box this source assets | ||||
"width": "source.width", // width of output asset. if omitted and height is present, width will be proportional | "width": "source.width", // width of output asset. if omitted and height is present, width will be proportional | ||||
"height": "source.height * 2", // height of output asset. if omitted and width is present, height will be proportional | "height": "source.height * 2", // height of output asset. if omitted and width is present, height will be proportional | ||||
"color": "DarkCyan" // letterbox padding color | |||||
"color": "DarkCyan", // letterbox padding color | |||||
"validate": [{ | |||||
"comment": "expect output resolution with double source height", | |||||
"test": "output.height === 2 * source.height" | |||||
}, { | |||||
"comment": "expect output resolution with same width as source", | |||||
"test": "output.width === source.width" | |||||
}, { | |||||
"comment": "expect output resolution of 320x480", | |||||
"test": "output.width === 320 && output.height === 480" | |||||
}] | |||||
} | } | ||||
] | ] | ||||
} | } |
@@ -20,14 +20,21 @@ | |||||
"creates": "v2", | "creates": "v2", | ||||
"source": "vid2", | "source": "vid2", | ||||
"start": "10", | "start": "10", | ||||
"end": "30" | |||||
"end": "30", | |||||
"validate": [{ | |||||
"comment": "expect output to be about 20 seconds long, give or take 0.1 seconds", | |||||
"test": "is_within(output.duration, 20, 0.1)" | |||||
}] | |||||
}, | }, | ||||
{ | { | ||||
"operation": "merge-audio", // name of the operation | "operation": "merge-audio", // name of the operation | ||||
"creates": "with_roar", // output asset name | "creates": "with_roar", // output asset name | ||||
"source": "v2", // main video asset | "source": "v2", // main video asset | ||||
"insert": "bull-roar", // audio asset to insert | "insert": "bull-roar", // audio asset to insert | ||||
"at": "5" // when (on the video timeline) to start playing the audio. default is 0 (beginning) | |||||
"at": "5", // when (on the video timeline) to start playing the audio. default is 0 (beginning) | |||||
"validate": [ | |||||
// todo: how to validate audio was inserted? | |||||
] | |||||
} | } | ||||
] | ] | ||||
} | } |
@@ -20,7 +20,11 @@ | |||||
"creates": "v1", | "creates": "v1", | ||||
"source": "vid1", | "source": "vid1", | ||||
"start": "0", | "start": "0", | ||||
"end": "60" | |||||
"end": "60", | |||||
"validate": [{ | |||||
"comment": "expect output to be about 60 seconds long, give or take 0.1 seconds", | |||||
"test": "is_within(output.duration, 60, 0.1)" | |||||
}] | |||||
}, | }, | ||||
{ | { | ||||
// overlay video is 10 seconds long | // overlay video is 10 seconds long | ||||
@@ -28,7 +32,11 @@ | |||||
"creates": "v2", | "creates": "v2", | ||||
"source": "vid2", | "source": "vid2", | ||||
"start": "10", | "start": "10", | ||||
"end": "20" | |||||
"end": "20", | |||||
"validate": [{ | |||||
"comment": "expect output to be about 10 seconds long, give or take 0.1 seconds", | |||||
"test": "is_within(output.duration, 10, 0.1)" | |||||
}] | |||||
}, | }, | ||||
{ | { | ||||
"operation": "overlay", // name of the operation | "operation": "overlay", // name of the operation | ||||
@@ -49,7 +57,10 @@ | |||||
"height": "source.height", // how tall the overlay will be, in pixels. default is the full overlay height, or maintain aspect ratio if width was set | "height": "source.height", // how tall the overlay will be, in pixels. default is the full overlay height, or maintain aspect ratio if width was set | ||||
"x": "source.width / 2", // horizontal overlay position on main video. default is 0 | "x": "source.width / 2", // horizontal overlay position on main video. default is 0 | ||||
"y": "source.height / 2" // vertical overlay position on main video. default is 0 | "y": "source.height / 2" // vertical overlay position on main video. default is 0 | ||||
} | |||||
}, | |||||
"validate": [ | |||||
// todo: how to validate overlay was added? | |||||
] | |||||
} | } | ||||
] | ] | ||||
} | } |
@@ -14,19 +14,37 @@ | |||||
"creates": "v2", | "creates": "v2", | ||||
"source": "vid2", | "source": "vid2", | ||||
"start": "0", | "start": "0", | ||||
"end": "20" | |||||
"end": "20", | |||||
"validate": [{ | |||||
"comment": "expect output to be about 20 seconds long, give or take 0.1 seconds", | |||||
"test": "is_within(output.duration, 20, 0.1)" | |||||
}] | |||||
}, | }, | ||||
{ | { | ||||
"operation": "remove-track", // name of the operation | "operation": "remove-track", // name of the operation | ||||
"creates": "vid2_video_only", // name of the output asset | "creates": "vid2_video_only", // name of the output asset | ||||
"source": "v2", // main video asset | "source": "v2", // main video asset | ||||
"track": "audio" // remove all audio tracks | |||||
"track": "audio", // remove all audio tracks | |||||
"validate": [{ | |||||
"comment": "expect only one track", | |||||
"test": "output.tracks.length === 1" | |||||
}, { | |||||
"comment": "expect only track to be video", | |||||
"test": "output.videoTracks.length === 1" | |||||
}] | |||||
}, | }, | ||||
{ | { | ||||
"operation": "remove-track", // name of the operation | "operation": "remove-track", // name of the operation | ||||
"creates": "vid2_audio_only", // name of the output asset | "creates": "vid2_audio_only", // name of the output asset | ||||
"source": "v2", // main video asset | "source": "v2", // main video asset | ||||
"track": "video" // remove all video tracks | |||||
"track": "video", // remove all video tracks | |||||
"validate": [{ | |||||
"comment": "expect only one track", | |||||
"test": "output.tracks.length === 1" | |||||
}, { | |||||
"comment": "expect only track to be audio", | |||||
"test": "output.audioTracks.length === 1" | |||||
}] | |||||
}, | }, | ||||
{ | { | ||||
"operation": "remove-track", // name of the operation | "operation": "remove-track", // name of the operation | ||||
@@ -36,7 +54,17 @@ | |||||
// only remove the first audio track | // only remove the first audio track | ||||
"type": "audio", // track type to remove | "type": "audio", // track type to remove | ||||
"number": "0" // track number to remove | "number": "0" // track number to remove | ||||
} | |||||
}, | |||||
"validate": [{ | |||||
"comment": "expect one less track", | |||||
"test": "output.tracks.length === source.tracks.length - 1" | |||||
}, { | |||||
"comment": "expect one less audio track", | |||||
"test": "output.audioTracks.length === source.audioTracks.length - 1" | |||||
}, { | |||||
"comment": "expect video tracks unchanged", | |||||
"test": "output.videoTracks.length === source.videoTracks.length" | |||||
}] | |||||
} | } | ||||
] | ] | ||||
} | } |
@@ -1,6 +1,6 @@ | |||||
{ | { | ||||
"assets": [ | "assets": [ | ||||
// wildcard matches multiple files, vid1_splits becomes a "list" asset | |||||
// wildcard matches multiple files, vid1_splits becomes a "list" asset. resolution is 320x240 | |||||
{ "name": "vid1_splits", "path": "src/test/resources/outputs/vid1_splits_*.mp4" } | { "name": "vid1_splits", "path": "src/test/resources/outputs/vid1_splits_*.mp4" } | ||||
], | ], | ||||
"operations": [ | "operations": [ | ||||
@@ -9,13 +9,24 @@ | |||||
"creates": "scaled_test1", // output asset name | "creates": "scaled_test1", // output asset name | ||||
"source": "vid1_splits[3]", // scale this source asset | "source": "vid1_splits[3]", // scale this source asset | ||||
"width": "1024", // width of scaled asset. if omitted and height is present, width will be proportional | "width": "1024", // width of scaled asset. if omitted and height is present, width will be proportional | ||||
"height": "768" // height of scaled asset. if omitted and width is present, height will be proportional | |||||
"height": "768", // height of scaled asset. if omitted and width is present, height will be proportional | |||||
"validate": [{ | |||||
"comment": "expect output resolution of 1024x768", | |||||
"test": "output.width === 1024 && output.height === 768" | |||||
}] | |||||
}, | }, | ||||
{ | { | ||||
"operation": "scale", // name of the operation | "operation": "scale", // name of the operation | ||||
"creates": "scaled_small", // asset it creates | "creates": "scaled_small", // asset it creates | ||||
"source": "vid1_splits[3]", // scale this source asset | "source": "vid1_splits[3]", // scale this source asset | ||||
"factor": "0.5" // scale factor. if factor is set, width and height are ignored. | |||||
"factor": "0.5", // scale factor. if factor is set, width and height are ignored. | |||||
"validate": [{ | |||||
"comment": "expect output resolution that is half of source", | |||||
"test": "output.width === source.width/2 && output.height === source.height/2" | |||||
}, { | |||||
"comment": "expect output resolution that is 160x120", | |||||
"test": "output.width === 160 && output.height === 120" | |||||
}] | |||||
} | } | ||||
] | ] | ||||
} | } |
@@ -1,6 +1,6 @@ | |||||
{ | { | ||||
"assets": [ | "assets": [ | ||||
// these are US government videos not covered by copyright | |||||
// these are US government videos not covered by copyright. resolution is 320x240 | |||||
{ | { | ||||
"name": "vid1", | "name": "vid1", | ||||
"path": "https://archive.org/download/gov.archives.arc.1257628/gov.archives.arc.1257628_512kb.mp4", | "path": "https://archive.org/download/gov.archives.arc.1257628/gov.archives.arc.1257628_512kb.mp4", | ||||
@@ -17,7 +17,17 @@ | |||||
"source": "vid1", // split this source asset | "source": "vid1", // split this source asset | ||||
"interval": "10", // split every ten seconds | "interval": "10", // split every ten seconds | ||||
"start": "65", // start one minute and five seconds into the video | "start": "65", // start one minute and five seconds into the video | ||||
"end": "100" // end 100 seconds into the video | |||||
"end": "100", // end 100 seconds into the video | |||||
"validate": [{ | |||||
"comment": "expect output contain 4 assets", | |||||
"test": "output.assets.length === 4" | |||||
}, { | |||||
"comment": "expect first asset to be about 10 seconds long", | |||||
"test": "is_within(output.assets[0].duration, 10, 0.1)" | |||||
}, { | |||||
"comment": "expect last asset to be about 10 seconds long", | |||||
"test": "is_within(output.assets[output.assets.length-1].duration, 10, 0.1)" | |||||
}] | |||||
} | } | ||||
] | ] | ||||
} | } |
@@ -12,7 +12,17 @@ | |||||
}, | }, | ||||
"source": "vid1_splits", // trim these source assets | "source": "vid1_splits", // trim these source assets | ||||
"start": "1", // cropped region starts here, default is zero | "start": "1", // cropped region starts here, default is zero | ||||
"end": "6" // cropped region ends here, default is end of video | |||||
"end": "6", // cropped region ends here, default is end of video | |||||
"validate": [{ | |||||
"comment": "expect output contain 4 assets", | |||||
"test": "output.assets.length === 4" | |||||
}, { | |||||
"comment": "expect first asset to be about 5 seconds long", | |||||
"test": "is_within(output.assets[0].duration, 5, 0.1)" | |||||
}, { | |||||
"comment": "expect last asset to be about 5 seconds long", | |||||
"test": "is_within(output.assets[output.assets.length-1].duration, 5, 0.1)" | |||||
}] | |||||
} | } | ||||
] | ] | ||||
} | } |