Jonathan Cobb преди 3 години
родител
ревизия
d9dd34fd92
променени са 18 файла, в които са добавени 292 реда и са изтрити 46 реда
  1. +51
    -1
      docs/jvc_js.md
  2. +5
    -16
      src/main/java/jvc/model/JAsset.java
  3. +1
    -1
      src/main/java/jvc/model/info/JMediaInfo.java
  4. +61
    -0
      src/main/java/jvc/model/js/JAssetJs.java
  5. +16
    -0
      src/main/java/jvc/model/js/JTrackJs.java
  6. +3
    -1
      src/main/java/jvc/operation/exec/SingleOrMultiSourceExecBase.java
  7. +5
    -4
      src/main/java/jvc/operation/exec/SplitExec.java
  8. +7
    -0
      src/main/java/jvc/service/JvcEngine.java
  9. +10
    -2
      src/test/resources/tests/test_adjust_speed.jvc
  10. +10
    -2
      src/test/resources/tests/test_concat.jvc
  11. +8
    -1
      src/test/resources/tests/test_ken_burns.jvc
  12. +23
    -3
      src/test/resources/tests/test_letterbox.jvc
  13. +9
    -2
      src/test/resources/tests/test_merge_audio.jvc
  14. +14
    -3
      src/test/resources/tests/test_overlay.jvc
  15. +32
    -4
      src/test/resources/tests/test_remove_track.jvc
  16. +14
    -3
      src/test/resources/tests/test_scale.jvc
  17. +12
    -2
      src/test/resources/tests/test_split.jvc
  18. +11
    -1
      src/test/resources/tests/test_trim.jvc

+ 51
- 1
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.
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
- 16
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();
}
}
}

+ 1
- 1
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<JFormat> formatRef = new AtomicReference<>();



+ 61
- 0
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]);
}
}
}
}

+ 16
- 0
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;

}

+ 3
- 1
src/main/java/jvc/operation/exec/SingleOrMultiSourceExecBase.java Целия файл

@@ -43,7 +43,8 @@ public abstract class SingleOrMultiSourceExecBase<OP extends JSingleSourceOperat
outfile = new File(output.destDirectory(), basename(appendToFileNameBeforeExt(asset.getPath(), "_"+op.shortString())));
if (outfile.exists()) {
log.info("operate: dest exists: "+abs(outfile));
return ctx;
assetManager.addOperationAssetSlice(output, subOutput.setPath(abs(outfile)));
continue;
}
} else {
outfile = defaultOutfile;
@@ -60,6 +61,7 @@ public abstract class SingleOrMultiSourceExecBase<OP extends JSingleSourceOperat
process(ctx, op, source, output, output, toolbox, assetManager);
assetManager.addOperationAsset(output);
}
ctx.put("output", output);
return ctx;
}



+ 5
- 4
src/main/java/jvc/operation/exec/SplitExec.java Целия файл

@@ -48,22 +48,21 @@ public class SplitExec extends ExecBase<SplitOperation> {
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<SplitOperation> {
log.debug("operate: command output: "+scriptOutput);
assetManager.addOperationAssetSlice(output, slice);
}

log.info("operate: completed");
ctx.put("output", output);
return ctx;
}



+ 7
- 0
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<String, Object> ctx = exec.operate(op, toolbox, assetManager);
if (ctx == null) {
log.debug("runOp("+op.getOperation()+"): noop");
return;
}

if (op.hasValidate()) {
final JsEngine js = toolbox.getJs();


+ 10
- 2
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)"
}]
}
]
}

+ 10
- 2
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)"
}]
}
]
}

+ 8
- 1
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"
}]
}
]
}

+ 23
- 3
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"
}]
}
]
}

+ 9
- 2
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?
]
}
]
}

+ 14
- 3
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?
]
}
]
}

+ 32
- 4
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"
}]
}
]
}

+ 14
- 3
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"
}]
}
]
}

+ 12
- 2
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)"
}]
}
]
}

+ 11
- 1
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)"
}]
}
]
}

Зареждане…
Отказ
Запис