From d9dd34fd9265fc51cde1cbadc460da761fc7f20c Mon Sep 17 00:00:00 2001 From: Jonathan Cobb Date: Sat, 19 Dec 2020 11:01:10 -0500 Subject: [PATCH] add validation --- docs/jvc_js.md | 52 +++++++++++++++- src/main/java/jvc/model/JAsset.java | 21 ++----- src/main/java/jvc/model/info/JMediaInfo.java | 2 +- src/main/java/jvc/model/js/JAssetJs.java | 61 +++++++++++++++++++ src/main/java/jvc/model/js/JTrackJs.java | 16 +++++ .../exec/SingleOrMultiSourceExecBase.java | 4 +- .../java/jvc/operation/exec/SplitExec.java | 9 +-- src/main/java/jvc/service/JvcEngine.java | 7 +++ .../resources/tests/test_adjust_speed.jvc | 12 +++- src/test/resources/tests/test_concat.jvc | 12 +++- src/test/resources/tests/test_ken_burns.jvc | 9 ++- src/test/resources/tests/test_letterbox.jvc | 26 +++++++- src/test/resources/tests/test_merge_audio.jvc | 11 +++- src/test/resources/tests/test_overlay.jvc | 17 +++++- .../resources/tests/test_remove_track.jvc | 36 +++++++++-- src/test/resources/tests/test_scale.jvc | 17 +++++- src/test/resources/tests/test_split.jvc | 14 ++++- src/test/resources/tests/test_trim.jvc | 12 +++- 18 files changed, 292 insertions(+), 46 deletions(-) create mode 100644 src/main/java/jvc/model/js/JAssetJs.java create mode 100644 src/main/java/jvc/model/js/JTrackJs.java diff --git a/docs/jvc_js.md b/docs/jvc_js.md index e1418f3..b0685b9 100644 --- a/docs/jvc_js.md +++ b/docs/jvc_js.md @@ -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 operation. -Assets are referenced by their asset name. \ No newline at end of file +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 +``` + diff --git a/src/main/java/jvc/model/JAsset.java b/src/main/java/jvc/model/JAsset.java index 71c3bf8..6ec0d63 100644 --- a/src/main/java/jvc/model/JAsset.java +++ b/src/main/java/jvc/model/JAsset.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonNode; import jvc.model.info.JMediaInfo; import jvc.model.info.JTrack; import jvc.model.info.JTrackType; +import jvc.model.js.JAssetJs; import jvc.service.AssetManager; import jvc.service.Toolbox; 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, File file) { this(other); setPath(abs(file)); } + @Getter @Setter private String name; @Getter @Setter private String path; + public boolean hasPath() { return !empty(path); } // an asset can specify where its file should live @@ -114,7 +118,7 @@ public class JAsset implements JsObjectView { setFormat(info.getFormat()); return this; } - public boolean hasInfo() { return info != null; } + public boolean hasInfo() { return info != null && !info.emptyMedia(); } @Getter @Setter private String comment; public boolean hasComment () { return !empty(comment); } @@ -285,19 +289,4 @@ public class JAsset implements JsObjectView { @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(); - } - } } diff --git a/src/main/java/jvc/model/info/JMediaInfo.java b/src/main/java/jvc/model/info/JMediaInfo.java index 131a610..c53c786 100644 --- a/src/main/java/jvc/model/info/JMediaInfo.java +++ b/src/main/java/jvc/model/info/JMediaInfo.java @@ -22,7 +22,7 @@ public class JMediaInfo { } @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 formatRef = new AtomicReference<>(); diff --git a/src/main/java/jvc/model/js/JAssetJs.java b/src/main/java/jvc/model/js/JAssetJs.java new file mode 100644 index 0000000..281dd97 --- /dev/null +++ b/src/main/java/jvc/model/js/JAssetJs.java @@ -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]); + } + } + } +} diff --git a/src/main/java/jvc/model/js/JTrackJs.java b/src/main/java/jvc/model/js/JTrackJs.java new file mode 100644 index 0000000..69029fe --- /dev/null +++ b/src/main/java/jvc/model/js/JTrackJs.java @@ -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; + +} diff --git a/src/main/java/jvc/operation/exec/SingleOrMultiSourceExecBase.java b/src/main/java/jvc/operation/exec/SingleOrMultiSourceExecBase.java index 62929b5..d64da9a 100644 --- a/src/main/java/jvc/operation/exec/SingleOrMultiSourceExecBase.java +++ b/src/main/java/jvc/operation/exec/SingleOrMultiSourceExecBase.java @@ -43,7 +43,8 @@ public abstract class SingleOrMultiSourceExecBase { if (output.destIsDirectory()) { outfile = sliceFile(output, formatType, i, incr); } 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 { outfile = assetManager.assetPath(op, source, formatType, new Object[]{i, incr}); } + final JAsset slice = new JAsset(output, outfile); if (outfile.exists()) { log.info("operate: outfile exists, not re-creating: "+abs(outfile)); + assetManager.addOperationAssetSlice(output, slice); continue; } else { mkdirOrDie(outfile.getParentFile()); } - final JAsset slice = new JAsset(output); - slice.setPath(abs(outfile)); slice.setName(source.getName()+"_"+i+"_"+incr); ctx.put("output", slice); @@ -76,7 +75,9 @@ public class SplitExec extends ExecBase { log.debug("operate: command output: "+scriptOutput); assetManager.addOperationAssetSlice(output, slice); } + log.info("operate: completed"); + ctx.put("output", output); return ctx; } diff --git a/src/main/java/jvc/service/JvcEngine.java b/src/main/java/jvc/service/JvcEngine.java index 3bf00b6..264e324 100644 --- a/src/main/java/jvc/service/JvcEngine.java +++ b/src/main/java/jvc/service/JvcEngine.java @@ -5,6 +5,7 @@ import jvc.model.operation.JOperation; import jvc.model.operation.JValidationResult; import jvc.operation.exec.ExecBase; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.javascript.JsEngine; import java.util.ArrayList; @@ -13,6 +14,7 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +@Slf4j public class JvcEngine { private final Toolbox toolbox; @@ -38,7 +40,12 @@ public class JvcEngine { .setExecIndex(completed.size()) .setNoExec(noExec) .getExec(); + final Map ctx = exec.operate(op, toolbox, assetManager); + if (ctx == null) { + log.debug("runOp("+op.getOperation()+"): noop"); + return; + } if (op.hasValidate()) { final JsEngine js = toolbox.getJs(); diff --git a/src/test/resources/tests/test_adjust_speed.jvc b/src/test/resources/tests/test_adjust_speed.jvc index 127f5f9..febf0ce 100644 --- a/src/test/resources/tests/test_adjust_speed.jvc +++ b/src/test/resources/tests/test_adjust_speed.jvc @@ -14,15 +14,23 @@ "creates": "v2", "source": "vid2", "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 "creates": "quickened", // output asset name "source": "v2", // main video asset "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 + "validate": [{ + "comment": "expect output to be about 5 seconds long, give or take 0.1 seconds", + "test": "is_within(output.duration, 5, 0.1)" + }] } ] } diff --git a/src/test/resources/tests/test_concat.jvc b/src/test/resources/tests/test_concat.jvc index 5c21997..6b2a2c5 100644 --- a/src/test/resources/tests/test_concat.jvc +++ b/src/test/resources/tests/test_concat.jvc @@ -10,12 +10,20 @@ "creates": { "name": "combined_vid1", // name of the output asset "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 "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)" + }] } ] } diff --git a/src/test/resources/tests/test_ken_burns.jvc b/src/test/resources/tests/test_ken_burns.jvc index bf08acc..21b920e 100644 --- a/src/test/resources/tests/test_ken_burns.jvc +++ b/src/test/resources/tests/test_ken_burns.jvc @@ -19,7 +19,14 @@ "x": "source.width * 0.6", // pan to this x-position "y": "source.height * 0.4", // pan to this y-position "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" + }] } ] } diff --git a/src/test/resources/tests/test_letterbox.jvc b/src/test/resources/tests/test_letterbox.jvc index 863f42b..a2f4fa5 100644 --- a/src/test/resources/tests/test_letterbox.jvc +++ b/src/test/resources/tests/test_letterbox.jvc @@ -1,6 +1,6 @@ { "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" } ], "operations": [ @@ -10,7 +10,17 @@ "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 "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 @@ -18,7 +28,17 @@ "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 "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" + }] } ] } diff --git a/src/test/resources/tests/test_merge_audio.jvc b/src/test/resources/tests/test_merge_audio.jvc index 318c36c..e7653d0 100644 --- a/src/test/resources/tests/test_merge_audio.jvc +++ b/src/test/resources/tests/test_merge_audio.jvc @@ -20,14 +20,21 @@ "creates": "v2", "source": "vid2", "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 "creates": "with_roar", // output asset name "source": "v2", // main video asset "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? + ] } ] } diff --git a/src/test/resources/tests/test_overlay.jvc b/src/test/resources/tests/test_overlay.jvc index 18b705f..9562269 100644 --- a/src/test/resources/tests/test_overlay.jvc +++ b/src/test/resources/tests/test_overlay.jvc @@ -20,7 +20,11 @@ "creates": "v1", "source": "vid1", "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 @@ -28,7 +32,11 @@ "creates": "v2", "source": "vid2", "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 @@ -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 "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 - } + }, + "validate": [ + // todo: how to validate overlay was added? + ] } ] } diff --git a/src/test/resources/tests/test_remove_track.jvc b/src/test/resources/tests/test_remove_track.jvc index e37d923..f35d819 100644 --- a/src/test/resources/tests/test_remove_track.jvc +++ b/src/test/resources/tests/test_remove_track.jvc @@ -14,19 +14,37 @@ "creates": "v2", "source": "vid2", "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 "creates": "vid2_video_only", // name of the output 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 "creates": "vid2_audio_only", // name of the output 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 @@ -36,7 +54,17 @@ // only remove the first audio track "type": "audio", // track type 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" + }] } ] } diff --git a/src/test/resources/tests/test_scale.jvc b/src/test/resources/tests/test_scale.jvc index 64cc9dd..113b24c 100644 --- a/src/test/resources/tests/test_scale.jvc +++ b/src/test/resources/tests/test_scale.jvc @@ -1,6 +1,6 @@ { "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" } ], "operations": [ @@ -9,13 +9,24 @@ "creates": "scaled_test1", // output asset name "source": "vid1_splits[3]", // scale this source asset "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 "creates": "scaled_small", // asset it creates "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" + }] } ] } diff --git a/src/test/resources/tests/test_split.jvc b/src/test/resources/tests/test_split.jvc index a606abc..afe23b7 100644 --- a/src/test/resources/tests/test_split.jvc +++ b/src/test/resources/tests/test_split.jvc @@ -1,6 +1,6 @@ { "assets": [ - // these are US government videos not covered by copyright + // these are US government videos not covered by copyright. resolution is 320x240 { "name": "vid1", "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 "interval": "10", // split every ten seconds "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)" + }] } ] } diff --git a/src/test/resources/tests/test_trim.jvc b/src/test/resources/tests/test_trim.jvc index b57f4ea..af95925 100644 --- a/src/test/resources/tests/test_trim.jvc +++ b/src/test/resources/tests/test_trim.jvc @@ -12,7 +12,17 @@ }, "source": "vid1_splits", // trim these source assets "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)" + }] } ] }