@@ -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`. | The above would set the `start` value to ten seconds before the end of `someAsset`. | ||||
### Supported Operations | ### 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. | 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. | 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. | 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 | # Complex Example | ||||
Here is a complex example using multiple assets and operations. | 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 | ```json | ||||
{ | { | ||||
"assets": [ | "assets": [ | ||||
// file -- will be referenced directory | // file -- will be referenced directory | ||||
{ | { | ||||
"comment": "first video, already local", | |||||
"name": "vid1", | "name": "vid1", | ||||
"path": "/tmp/path/to/video1.mp4" | "path": "/tmp/path/to/video1.mp4" | ||||
}, | }, | ||||
// URL -- will be downloaded to scratch directory and referenced from there | // URL -- will be downloaded to scratch directory and referenced from there | ||||
{ | { | ||||
"comment": "second video, will be downloaded", | |||||
"name": "vid2", | "name": "vid2", | ||||
"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" | ||||
}, | }, | ||||
// URL -- will be downloaded to `dest` directory and referenced from there | // URL -- will be downloaded to `dest` directory and referenced from there | ||||
{ | { | ||||
"comment": "third video, will be downloaded", | |||||
"name": "vid3", | "name": "vid3", | ||||
"path": "https://archive.org/download/gov.archives.arc.49442/gov.archives.arc.49442_512kb.mp4", | "path": "https://archive.org/download/gov.archives.arc.49442/gov.archives.arc.49442_512kb.mp4", | ||||
"dest": "src/test/resources/sources/" | "dest": "src/test/resources/sources/" | ||||
@@ -176,13 +188,16 @@ Here is a complex example using multiple assets and operations. | |||||
// Image URL | // Image URL | ||||
{ | { | ||||
"comment": "JPEG image, will be downloaded", | |||||
"name": "img1", | "name": "img1", | ||||
"path": "https://live.staticflickr.com/65535/48159911972_01efa0e5ea_b.jpg", | "path": "https://live.staticflickr.com/65535/48159911972_01efa0e5ea_b.jpg", | ||||
"dest": "src/test/resources/sources/" | "dest": "src/test/resources/sources/" | ||||
} | } | ||||
], | ], | ||||
"operations": [ | "operations": [ | ||||
// scale examples | |||||
{ | { | ||||
"comment": "scale using explicity height x width", | |||||
"operation": "scale", // name of the operation | "operation": "scale", // name of the operation | ||||
"creates": "vid2_scaled", // asset it creates | "creates": "vid2_scaled", // asset it creates | ||||
"source": "vid2", // source asset | "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 | "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 | "operation": "scale", // name of the operation | ||||
"creates": "vid2_big", // asset it creates | "creates": "vid2_big", // asset it creates | ||||
"source": "vid2", // source asset | "source": "vid2", // source asset | ||||
"factor": "2.2" // scale factor. if factor is set, width and height are ignored. | "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 | "operation": "split", // name of the operation | ||||
"creates": "vid1_split_%", // assets it creates, the '%' will be replaced with a counter | "creates": "vid1_split_%", // assets it creates, the '%' will be replaced with a counter | ||||
"source": "vid1", // split this source asset | "source": "vid1", // split this source asset | ||||
"interval": "10" // split every ten seconds | "interval": "10" // split every ten seconds | ||||
}, | }, | ||||
// concat examples | |||||
{ | { | ||||
"comment": "re-combine previously split assets back together", | |||||
"operation": "concat", // name of the operation | "operation": "concat", // name of the operation | ||||
"creates": "recombined_vid1", // assets it creates, the '%' will be replaced with a counter | "creates": "recombined_vid1", // assets it creates, the '%' will be replaced with a counter | ||||
"source": ["vid1_split"] // recombine all split assets | "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 | "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 | "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 | "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 | "operation": "trim", // name of the operation | ||||
"creates": { // create multiple files, will be prefixed with `name`, store them in `dest` | "creates": { // create multiple files, will be prefixed with `name`, store them in `dest` | ||||
"name": "vid1_trims", | "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 | "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 | ||||
}, | }, | ||||
// overlay example | |||||
{ | { | ||||
"comment": "overlay one video onto another", | |||||
"operation": "overlay", // name of the operation | "operation": "overlay", // name of the operation | ||||
"creates": "overlay1", // asset it creates | "creates": "overlay1", // asset it creates | ||||
"source": "combined_vid1", // main video asset | "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 | "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 | "operation": "ken-burns", // name of the operation | ||||
"creates": "ken1", // asset it creates | "creates": "ken1", // asset it creates | ||||
"source": "img1", // source image | "source": "img1", // source image | ||||
@@ -256,13 +289,43 @@ Here is a complex example using multiple assets and operations. | |||||
"width": "1024", // width of output video | "width": "1024", // width of output video | ||||
"height": "768" // height 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 | "operation": "letterbox", // name of the operation | ||||
"creates": "boxed1", // asset it creates | "creates": "boxed1", // asset it creates | ||||
"source": "ken1", // source asset | "source": "ken1", // source asset | ||||
"width": "source.width * 1.5", // make it wider | "width": "source.width * 1.5", // make it wider | ||||
"height": "source.height * 0.9", // and shorter | "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 | "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.annotation.JsonIgnore; | ||||
import com.fasterxml.jackson.databind.JsonNode; | import com.fasterxml.jackson.databind.JsonNode; | ||||
import jvcl.model.info.JMediaInfo; | import jvcl.model.info.JMediaInfo; | ||||
import jvcl.model.info.JTrack; | |||||
import jvcl.model.info.JTrackType; | |||||
import jvcl.service.AssetManager; | import jvcl.service.AssetManager; | ||||
import jvcl.service.Toolbox; | import jvcl.service.Toolbox; | ||||
import lombok.*; | import lombok.*; | ||||
@@ -113,6 +115,9 @@ public class JAsset implements JsObjectView { | |||||
} | } | ||||
public boolean hasInfo() { return info != null; } | public boolean hasInfo() { return info != null; } | ||||
@Getter @Setter private String comment; | |||||
public boolean hasComment () { return !empty(comment); } | |||||
@Override public boolean equals(Object o) { | @Override public boolean equals(Object o) { | ||||
if (this == o) return true; | if (this == o) return true; | ||||
if (o == null || getClass() != o.getClass()) return false; | 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; } | public BigDecimal height() { return hasInfo() ? getInfo().height() : null; } | ||||
@JsonIgnore public BigDecimal getHeight () { return height(); } | @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() { | public BigDecimal aspectRatio() { | ||||
final BigDecimal width = width(); | final BigDecimal width = width(); | ||||
final BigDecimal height = height(); | final BigDecimal height = height(); | ||||
@@ -1,12 +1,14 @@ | |||||
package jvcl.model; | package jvcl.model; | ||||
import com.fasterxml.jackson.annotation.JsonCreator; | import com.fasterxml.jackson.annotation.JsonCreator; | ||||
import jvcl.model.info.JTrack; | |||||
import jvcl.model.info.JTrackType; | import jvcl.model.info.JTrackType; | ||||
import lombok.AllArgsConstructor; | import lombok.AllArgsConstructor; | ||||
import lombok.extern.slf4j.Slf4j; | |||||
import static jvcl.model.info.JTrackType.*; | import static jvcl.model.info.JTrackType.*; | ||||
@AllArgsConstructor | |||||
@AllArgsConstructor @Slf4j | |||||
public enum JFileExtension { | public enum JFileExtension { | ||||
mp4 (".mp4", video), | mp4 (".mp4", video), | ||||
@@ -16,7 +18,9 @@ public enum JFileExtension { | |||||
flac (".flac", audio), | flac (".flac", audio), | ||||
png (".png", image), | png (".png", image), | ||||
jpg (".jpg", 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()); } | @JsonCreator public static JFileExtension fromString(String v) { return valueOf(v.toLowerCase()); } | ||||
@@ -31,4 +35,22 @@ public enum JFileExtension { | |||||
private final JTrackType mediaType; | private final JTrackType mediaType; | ||||
public JTrackType mediaType() { return 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; | package jvcl.model.info; | ||||
import jvcl.model.JFormat; | |||||
import jvcl.model.JFileExtension; | import jvcl.model.JFileExtension; | ||||
import jvcl.model.JFormat; | |||||
import lombok.Getter; | import lombok.Getter; | ||||
import lombok.NoArgsConstructor; | |||||
import lombok.Setter; | import lombok.Setter; | ||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import java.math.BigDecimal; | import java.math.BigDecimal; | ||||
import java.util.concurrent.atomic.AtomicReference; | |||||
import static java.math.BigDecimal.ZERO; | import static java.math.BigDecimal.ZERO; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | import static org.cobbzilla.util.daemon.ZillaRuntime.*; | ||||
@Slf4j | |||||
@NoArgsConstructor @Slf4j | |||||
public class JMediaInfo { | public class JMediaInfo { | ||||
public JMediaInfo (JMediaInfo other, JFormat format) { | |||||
this.media = other.getMedia(); | |||||
this.formatRef.set(format); | |||||
} | |||||
@Getter @Setter private JMedia media; | @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 () { | private JFormat initFormat () { | ||||
if (media == null || empty(media.getTrack())) return null; | if (media == null || empty(media.getTrack())) return null; | ||||
@@ -106,4 +118,21 @@ public class JMediaInfo { | |||||
return null; | 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); } | public boolean hasFileExtension () { return !empty(fileExtension); } | ||||
@JsonProperty("Format") @Getter @Setter private String format; | @JsonProperty("Format") @Getter @Setter private String format; | ||||
public boolean hasFormat () { return !empty(format); } | |||||
@JsonProperty("Format_AdditionalFeatures") @Getter @Setter private String formatAdditionalFeatures; | @JsonProperty("Format_AdditionalFeatures") @Getter @Setter private String formatAdditionalFeatures; | ||||
@JsonProperty("Format_Profile") @Getter @Setter private String formatProfile; | @JsonProperty("Format_Profile") @Getter @Setter private String formatProfile; | ||||
@JsonProperty("Format_Level") @Getter @Setter private String formatLevel; | @JsonProperty("Format_Level") @Getter @Setter private String formatLevel; | ||||
@@ -7,16 +7,21 @@ import lombok.AllArgsConstructor; | |||||
@AllArgsConstructor | @AllArgsConstructor | ||||
public enum JTrackType { | 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()); } | @JsonCreator public static JTrackType fromString(String val) { return valueOf(val.toLowerCase()); } | ||||
private final JFileExtension ext; | private final JFileExtension ext; | ||||
public JFileExtension ext() { return 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 java.util.Map; | ||||
import static jvcl.service.json.JOperationFactory.getOperationExecClass; | 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.daemon.ZillaRuntime.hashOf; | ||||
import static org.cobbzilla.util.json.JsonUtil.json; | import static org.cobbzilla.util.json.JsonUtil.json; | ||||
import static org.cobbzilla.util.reflect.ReflectionUtil.instantiate; | import static org.cobbzilla.util.reflect.ReflectionUtil.instantiate; | ||||
@@ -33,6 +34,9 @@ public abstract class JOperation { | |||||
@Getter @Setter private JsonNode creates; | @Getter @Setter private JsonNode creates; | ||||
@Getter @Setter private boolean noExec = false; | @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) { return hash(sources, null); } | ||||
public String hash(JAsset[] sources, Object[] args) { | public String hash(JAsset[] sources, Object[] args) { | ||||
@@ -23,7 +23,7 @@ public class JSingleSourceOperation extends JOperation { | |||||
final JAsset output = json2asset(getCreates()); | final JAsset output = json2asset(getCreates()); | ||||
output.mergeFormat(source.getFormat()); | output.mergeFormat(source.getFormat()); | ||||
// ensure output is in the correct fprmat | |||||
// ensure output is in the correct format | |||||
final JFormat format = output.getFormat(); | final JFormat format = output.getFormat(); | ||||
final JTrackType type = outputMediaType(); | final JTrackType type = outputMediaType(); | ||||
if (!format.hasFileExtension() || format.getFileExtension().mediaType() != type) { | if (!format.hasFileExtension() || format.getFileExtension().mediaType() != type) { | ||||
@@ -33,12 +33,12 @@ public class JSingleSourceOperation extends JOperation { | |||||
} | } | ||||
format.setFileExtension(ext); | format.setFileExtension(ext); | ||||
} | } | ||||
final JFileExtension formatType = getFileExtension(output); | |||||
final JFileExtension formatType = getFileExtension(source, output); | |||||
return new JSingleOperationContext(source, output, formatType); | return new JSingleOperationContext(source, output, formatType); | ||||
} | } | ||||
protected JFileExtension getFileExtension(JAsset output) { | |||||
protected JFileExtension getFileExtension(JAsset source, JAsset output) { | |||||
return output.getFormat().getFileExtension(); | 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, | JAsset subOutput, | ||||
Toolbox toolbox, | Toolbox toolbox, | ||||
AssetManager assetManager) { | AssetManager assetManager) { | ||||
ctx.put("source", source); | ctx.put("source", source); | ||||
ctx.put("output", subOutput); | ctx.put("output", subOutput); | ||||
final String script = renderScript(toolbox, ctx, LETTERBOX_TEMPLATE); | final String script = renderScript(toolbox, ctx, LETTERBOX_TEMPLATE); | ||||
@@ -72,11 +71,6 @@ public class LetterboxExec extends SingleOrMultiSourceExecBase<LetterboxOperatio | |||||
log.debug("operate: running script: "+script); | log.debug("operate: running script: "+script); | ||||
final String scriptOutput = exec(script, op.isNoExec()); | final String scriptOutput = exec(script, op.isNoExec()); | ||||
log.debug("operate: command output: "+scriptOutput); | 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); | log.debug("operate: running script: "+script); | ||||
final String scriptOutput = exec(script, op.isNoExec()); | final String scriptOutput = exec(script, op.isNoExec()); | ||||
log.debug("operate: command output: "+scriptOutput); | 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)); | subOutput.setPath(abs(outfile)); | ||||
process(ctx, op, asset, output, subOutput, toolbox, assetManager); | process(ctx, op, asset, output, subOutput, toolbox, assetManager); | ||||
assetManager.addOperationAssetSlice(output, subOutput); | |||||
} | } | ||||
} else { | } else { | ||||
final File defaultOutfile = assetManager.assetPath(op, source, formatType); | final File defaultOutfile = assetManager.assetPath(op, source, formatType); | ||||
@@ -44,6 +45,7 @@ public abstract class SingleOrMultiSourceExecBase<OP extends JOperation> extends | |||||
if (path == null) return; | if (path == null) return; | ||||
output.setPath(abs(path)); | output.setPath(abs(path)); | ||||
process(ctx, op, source, output, output, toolbox, assetManager); | 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, | Toolbox toolbox, | ||||
AssetManager assetManager); | AssetManager assetManager); | ||||
} | } |
@@ -55,11 +55,6 @@ public class TrimExec extends SingleOrMultiSourceExecBase<TrimOperation> { | |||||
log.debug("operate: running script: "+script); | log.debug("operate: running script: "+script); | ||||
final String scriptOutput = exec(script, op.isNoExec()); | final String scriptOutput = exec(script, op.isNoExec()); | ||||
log.debug("operate: command output: "+scriptOutput); | 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"); | 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) { | private void runSpec(String specPath) { | ||||
try { | try { | ||||
@@ -15,7 +15,7 @@ | |||||
{ | { | ||||
"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 | |||||
} | } | ||||
] | ] | ||||
} | } |
@@ -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 | |||||
} | |||||
} | |||||
] | |||||
} |