diff --git a/bin/jaddsilence b/bin/jaddsilence old mode 100644 new mode 100755 index 0723510..7cdd4b0 --- a/bin/jaddsilence +++ b/bin/jaddsilence @@ -4,10 +4,12 @@ # # Usage: # -# jaddsilence in-file out-file +# jaddsilence in-file out-file [channel-mode] [sampling-rate] # -# in-file : input video file -# out-file : write output file here +# in-file : input video file +# out-file : write output file here +# channel-mode : channel layout, usually 'mono' or 'stereo'. Default is stereo +# sampling-rate : sampling rate, in Hz. Default is 48000 # SCRIPT="${0}" SCRIPT_DIR="$(cd "$(dirname "${SCRIPT}")" && pwd)" @@ -15,6 +17,8 @@ SCRIPT_DIR="$(cd "$(dirname "${SCRIPT}")" && pwd)" IN_FILE="${1?no video-file provided}" OUT_FILE="${2?no out-file provided}" +CHANNEL_LAYOUT="${3}" +SAMPLING_RATE="${4}" echo " { @@ -28,7 +32,9 @@ echo " \"name\": \"with_silence\", \"dest\": \"${OUT_FILE}\" }, - \"source\": \"input\" + \"source\": \"input\"$(if [[ -n "${CHANNEL_LAYOUT}" ]] ; then echo ", + \"channelLayout\": \"${CHANNEL_LAYOUT}\"" ; fi)$(if [[ -n "${SAMPLING_RATE}" ]] ; then echo ", + \"samplingRate: \"${SAMPLING_RATE}\"" ; fi) } ] } diff --git a/bin/jmergeaudio b/bin/jmergeaudio old mode 100644 new mode 100755 diff --git a/docs/complex_example.md b/docs/complex_example.md index ed38fec..2a2d916 100644 --- a/docs/complex_example.md +++ b/docs/complex_example.md @@ -1,11 +1,15 @@ # Complex Example Here is a complex example using multiple assets and operations. -Note that comments, which are not usually legal in JSON, are allowed in JVCL files. +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. +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. + +Doug C: I promise these will always be just comments; jvcl will never +use [comments as parsing directives or otherwise break interoperability](https://web.archive.org/web/20120507155137/https://plus.google.com/118095276221607585885/posts/RK8qyGVaGSr) (note: disable javascript to view this link) ```js { @@ -188,6 +192,15 @@ field, which can be used as well. "source": "vid2", // 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) + }, + + // add-silence example + { + "operation": "add-silence", // name of the operation + "creates": "v2_silent", // output asset name + "source": "v2", // main video asset + "channelLayout": "stereo", // optional channel layout, usually 'mono' or 'stereo'. Default is 'stereo' + "samplingRate": 48000 // optional samping rate, in Hz. default is 48000 } ] } diff --git a/src/main/java/jvcl/model/JAsset.java b/src/main/java/jvcl/model/JAsset.java index 932b9bb..aa7f6a7 100644 --- a/src/main/java/jvcl/model/JAsset.java +++ b/src/main/java/jvcl/model/JAsset.java @@ -177,6 +177,10 @@ public class JAsset implements JsObjectView { @JsonIgnore public String getChannelLayout() { return channelLayout(); } public boolean hasChannelLayout() { return channelLayout() != null; } + public JFileExtension audioExtension() { return hasInfo() ? getInfo().audioExtension() : null; } + @JsonIgnore public JFileExtension getAudioExtension() { return audioExtension(); } + public boolean hasAudioExtension() { return audioExtension() != null; } + public JAsset init(AssetManager assetManager, Toolbox toolbox) { final JAsset asset = initPath(assetManager); if (!asset.hasListAssets()) { diff --git a/src/main/java/jvcl/model/JFileExtension.java b/src/main/java/jvcl/model/JFileExtension.java index 2178b2d..89f6821 100644 --- a/src/main/java/jvcl/model/JFileExtension.java +++ b/src/main/java/jvcl/model/JFileExtension.java @@ -7,10 +7,12 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import static jvcl.model.info.JTrackType.*; +import static org.cobbzilla.util.daemon.ZillaRuntime.die; @AllArgsConstructor @Slf4j public enum JFileExtension { + avc (".mp4", video), mp4 (".mp4", video), mkv (".mkv", video), mp3 (".mp3", audio), @@ -48,11 +50,9 @@ public enum JFileExtension { if (track.hasFormat()) { try { return fromString(track.getFormat().replace(" ", "_")); - } catch (Exception e) { - log.warn("fromTrack: unrecognized format: "+track.getFormat()); - } + } catch (Exception ignored) { } } - return null; + return die("fromTrack: unrecognized file extension/format: "+track.getFileExtension()+"/"+track.getFormat()); } } diff --git a/src/main/java/jvcl/model/info/JMediaInfo.java b/src/main/java/jvcl/model/info/JMediaInfo.java index c5a829b..cd904ed 100644 --- a/src/main/java/jvcl/model/info/JMediaInfo.java +++ b/src/main/java/jvcl/model/info/JMediaInfo.java @@ -120,6 +120,11 @@ public class JMediaInfo { return null; } + public JFileExtension audioExtension() { + final JTrack audio = firstTrack(JTrackType.audio); + return audio == null ? null : JFileExtension.fromTrack(audio); + } + public BigDecimal width() { if (emptyMedia()) return ZERO; // find the first video track diff --git a/src/main/java/jvcl/model/info/JTrack.java b/src/main/java/jvcl/model/info/JTrack.java index 11b78f5..5d98596 100644 --- a/src/main/java/jvcl/model/info/JTrack.java +++ b/src/main/java/jvcl/model/info/JTrack.java @@ -44,7 +44,9 @@ public class JTrack { @JsonProperty("ChannelLayout") @Getter @Setter private String channelLayout; public String channelLayout () { - if (!empty(channelLayout)) return channelLayout; + if (!empty(channelLayout)) { + return channelLayout.equals("L R") ? "stereo": channelLayout; + } if (!empty(channels)) { if (isOnlyDigits(channels)) { switch (parseInt(channels)) { diff --git a/src/main/java/jvcl/operation/AddSilenceOperation.java b/src/main/java/jvcl/operation/AddSilenceOperation.java index 80c1983..2b972f3 100644 --- a/src/main/java/jvcl/operation/AddSilenceOperation.java +++ b/src/main/java/jvcl/operation/AddSilenceOperation.java @@ -1,5 +1,15 @@ package jvcl.operation; import jvcl.model.operation.JSingleSourceOperation; +import lombok.Getter; +import lombok.Setter; -public class AddSilenceOperation extends JSingleSourceOperation {} +public class AddSilenceOperation extends JSingleSourceOperation { + + private static final String DEFAULT_CHANNEL_LAYOUT = "stereo"; + private static final Integer DEFAULT_SAMPLING_RATE = 48000; + + @Getter @Setter private String channelLayout = DEFAULT_CHANNEL_LAYOUT; + @Getter @Setter private Integer samplingRate = DEFAULT_SAMPLING_RATE; + +} diff --git a/src/main/java/jvcl/operation/exec/AddSilenceExec.java b/src/main/java/jvcl/operation/exec/AddSilenceExec.java index 6be65bd..4d6bcdf 100644 --- a/src/main/java/jvcl/operation/exec/AddSilenceExec.java +++ b/src/main/java/jvcl/operation/exec/AddSilenceExec.java @@ -14,7 +14,12 @@ import java.util.Map; @Slf4j public class AddSilenceExec extends SingleOrMultiSourceExecBase { - public static final String ADD_SILENCE_TEMPLATE = ""; + public static final String ADD_SILENCE_TEMPLATE + = "{{{ffmpeg}}} -i {{{source.path}}} -i {{{silence.path}}} " + + "-map 0:v -map 1:a -c:v copy -shortest " + + "-y {{{output.path}}}"; + + @Override protected String getProcessTemplate() { return ADD_SILENCE_TEMPLATE; } @Override public void operate(AddSilenceOperation op, Toolbox toolbox, AssetManager assetManager) { final JSingleOperationContext opCtx = op.getSingleInputContext(assetManager); @@ -26,11 +31,10 @@ public class AddSilenceExec extends SingleOrMultiSourceExecBase ctx, AddSilenceOperation addSilenceOperation, JAsset source, JAsset output, JAsset asset, Toolbox toolbox, AssetManager assetManager) { - // todo + operate(op, toolbox, assetManager, source, output, formatType, ctx); } } diff --git a/src/main/java/jvcl/operation/exec/ExecBase.java b/src/main/java/jvcl/operation/exec/ExecBase.java index 9c75001..bc2a3b6 100644 --- a/src/main/java/jvcl/operation/exec/ExecBase.java +++ b/src/main/java/jvcl/operation/exec/ExecBase.java @@ -1,6 +1,7 @@ package jvcl.operation.exec; import jvcl.model.JAsset; +import jvcl.model.JFileExtension; import jvcl.model.operation.JOperation; import jvcl.service.AssetManager; import jvcl.service.Toolbox; @@ -8,8 +9,11 @@ import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.handlebars.HandlebarsUtil; import java.io.File; +import java.math.BigDecimal; +import java.util.HashMap; import java.util.Map; +import static org.cobbzilla.util.daemon.ZillaRuntime.die; import static org.cobbzilla.util.io.FileUtil.abs; import static org.cobbzilla.util.io.FileUtil.basename; import static org.cobbzilla.util.system.CommandShell.execScript; @@ -46,4 +50,40 @@ public abstract class ExecBase { return execScript(script); } } + + public static final String CREATE_SILENCE_TEMPLATE + = "{{{ffmpeg}}} -f lavfi " + + "-i anullsrc=channel_layout={{channelLayout}}:sample_rate={{samplingRate}} " + + "-t {{duration}} " + + "-y {{{silence.path}}}"; + + protected JAsset createSilence(OP op, + Toolbox toolbox, + AssetManager assetManager, + BigDecimal duration, + JAsset asset) { + final Map ctx = new HashMap<>(); + ctx.put("ffmpeg", toolbox.getFfmpeg()); + ctx.put("duration", duration); + + if (!asset.hasSamplingRate()) return die("createSilence: no sampling rate could be determined: "+asset); + ctx.put("samplingRate", asset.samplingRate()); + + if (!asset.hasChannelLayout()) return die("createSilence: no channel layout could be determined: "+asset); + ctx.put("channelLayout", asset.channelLayout()); + + final JFileExtension ext = asset.audioExtension(); + final File silenceFile = assetManager.assetPath(op, asset, ext, new Object[]{duration}); + final JAsset silence = new JAsset().setPath(abs(silenceFile)); + ctx.put("silence", silence); + + final String script = renderScript(toolbox, ctx, CREATE_SILENCE_TEMPLATE); + + log.debug("createSilence: running script: "+script); + final String scriptOutput = exec(script, op.isNoExec()); + log.debug("createSilence: command output: "+scriptOutput); + + return silence; + } + } diff --git a/src/main/java/jvcl/operation/exec/LetterboxExec.java b/src/main/java/jvcl/operation/exec/LetterboxExec.java index bd19c3b..49919d5 100644 --- a/src/main/java/jvcl/operation/exec/LetterboxExec.java +++ b/src/main/java/jvcl/operation/exec/LetterboxExec.java @@ -18,6 +18,8 @@ import static org.cobbzilla.util.string.StringUtil.safeShellArg; @Slf4j public class LetterboxExec extends SingleOrMultiSourceExecBase { + public static final String DEFAULT_LETTERBOX_COLOR = "black"; + public static final String LETTERBOX_TEMPLATE = "{{ffmpeg}} -i {{{source.path}}} -filter_complex \"" + "pad=" @@ -28,7 +30,7 @@ public class LetterboxExec extends SingleOrMultiSourceExecBase ctx, - LetterboxOperation 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, LETTERBOX_TEMPLATE); - - log.debug("operate: running script: "+script); - final String scriptOutput = exec(script, op.isNoExec()); - log.debug("operate: command output: "+scriptOutput); - } - } diff --git a/src/main/java/jvcl/operation/exec/MergeAudioExec.java b/src/main/java/jvcl/operation/exec/MergeAudioExec.java index 984d5e6..8058a91 100644 --- a/src/main/java/jvcl/operation/exec/MergeAudioExec.java +++ b/src/main/java/jvcl/operation/exec/MergeAudioExec.java @@ -23,12 +23,6 @@ import static org.cobbzilla.util.io.FileUtil.*; @Slf4j public class MergeAudioExec extends SingleOrMultiSourceExecBase { - public static final String CREATE_SILENCE_TEMPLATE - = "{{{ffmpeg}}} -f lavfi " - + "-i anullsrc=channel_layout={{channelLayout}}:sample_rate={{samplingRate}} " - + "-t {{duration}} " - + "-y {{{silence.path}}}"; - public static final String PAD_WITH_SILENCE_TEMPLATE = "cd {{{tempDir}}} && {{{ffmpeg}}} -f concat -i {{{playlist.path}}} -codec copy -y {{{padded}}}"; @@ -39,6 +33,8 @@ public class MergeAudioExec extends SingleOrMultiSourceExecBase ctx = new HashMap<>(); - ctx.put("ffmpeg", toolbox.getFfmpeg()); - ctx.put("duration", duration); - - if (!audio.hasSamplingRate()) return die("createSilence: no sampling rate could be determined: "+audio); - ctx.put("samplingRate", audio.samplingRate()); - - if (!audio.hasChannelLayout()) return die("createSilence: no channel layout could be determined: "+audio); - ctx.put("channelLayout", audio.channelLayout()); - - final JFileExtension ext = audio.getFormat().getFileExtension(); - final File silenceFile = assetManager.assetPath(op, audio, ext, new Object[]{duration}); - final JAsset silence = new JAsset().setPath(abs(silenceFile)); - ctx.put("silence", silence); - - final String script = renderScript(toolbox, ctx, CREATE_SILENCE_TEMPLATE); - - log.debug("operate: running script: "+script); - final String scriptOutput = exec(script, op.isNoExec()); - log.debug("operate: command output: "+scriptOutput); - - return silence; - } - protected JAsset padWithSilence(MergeAudioOperation op, Toolbox toolbox, AssetManager assetManager, @@ -139,20 +106,4 @@ public class MergeAudioExec extends SingleOrMultiSourceExecBase ctx, - MergeAudioOperation 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, MERGE_AUDIO_TEMPLATE); - - log.debug("operate: running script: "+script); - final String scriptOutput = exec(script, op.isNoExec()); - log.debug("operate: command output: "+scriptOutput); - } - } diff --git a/src/main/java/jvcl/operation/exec/RemoveTrackExec.java b/src/main/java/jvcl/operation/exec/RemoveTrackExec.java index 453f4d5..3574782 100644 --- a/src/main/java/jvcl/operation/exec/RemoveTrackExec.java +++ b/src/main/java/jvcl/operation/exec/RemoveTrackExec.java @@ -22,6 +22,8 @@ public class RemoveTrackExec extends SingleOrMultiSourceExecBase 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); - } - } diff --git a/src/main/java/jvcl/operation/exec/ScaleExec.java b/src/main/java/jvcl/operation/exec/ScaleExec.java index 5a12264..c9a622a 100644 --- a/src/main/java/jvcl/operation/exec/ScaleExec.java +++ b/src/main/java/jvcl/operation/exec/ScaleExec.java @@ -21,6 +21,8 @@ public class ScaleExec extends SingleOrMultiSourceExecBase { + "scale={{width}}x{{height}}" + "\" -y {{{output.path}}}"; + @Override protected String getProcessTemplate() { return SCALE_TEMPLATE; } + @Override public void operate(ScaleOperation op, Toolbox toolbox, AssetManager assetManager) { final JSingleOperationContext opCtx = op.getSingleInputContext(assetManager); @@ -43,21 +45,4 @@ public class ScaleExec extends SingleOrMultiSourceExecBase { operate(op, toolbox, assetManager, source, output, formatType, ctx); } - @Override protected void process(Map ctx, - ScaleOperation 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, SCALE_TEMPLATE); - - log.debug("operate: running script: "+script); - final String scriptOutput = exec(script, op.isNoExec()); - log.debug("operate: command output: "+scriptOutput); - } - } diff --git a/src/main/java/jvcl/operation/exec/SingleOrMultiSourceExecBase.java b/src/main/java/jvcl/operation/exec/SingleOrMultiSourceExecBase.java index 6232974..df10076 100644 --- a/src/main/java/jvcl/operation/exec/SingleOrMultiSourceExecBase.java +++ b/src/main/java/jvcl/operation/exec/SingleOrMultiSourceExecBase.java @@ -49,12 +49,22 @@ public abstract class SingleOrMultiSourceExecBase extends } } - protected abstract void process(Map ctx, - OP op, - JAsset source, - JAsset output, - JAsset asset, - Toolbox toolbox, - AssetManager assetManager); + protected abstract String getProcessTemplate(); + + protected void process(Map ctx, + OP 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, getProcessTemplate()); + + log.debug("operate: running script: "+script); + final String scriptOutput = exec(script, op.isNoExec()); + log.debug("operate: command output: "+scriptOutput); + } } diff --git a/src/main/java/jvcl/operation/exec/TrimExec.java b/src/main/java/jvcl/operation/exec/TrimExec.java index cffd5d5..bf2c14d 100644 --- a/src/main/java/jvcl/operation/exec/TrimExec.java +++ b/src/main/java/jvcl/operation/exec/TrimExec.java @@ -22,6 +22,8 @@ public class TrimExec extends SingleOrMultiSourceExecBase { "{{#exists interval}}-t {{interval}} {{/exists}}" + "-y {{{output.path}}}"; + @Override protected String getProcessTemplate() { return TRIM_TEMPLATE; } + @Override public void operate(TrimOperation op, Toolbox toolbox, AssetManager assetManager) { final JSingleOperationContext opCtx = op.getSingleInputContext(assetManager); @@ -42,19 +44,12 @@ public class TrimExec extends SingleOrMultiSourceExecBase { JAsset subOutput, Toolbox toolbox, AssetManager assetManager) { - - ctx.put("source", source); - ctx.put("output", subOutput); - final StandardJsEngine js = toolbox.getJs(); final BigDecimal startTime = op.getStartTime(ctx, js); ctx.put("startSeconds", startTime); if (op.hasEndTime()) ctx.put("interval", op.getEndTime(ctx, js).subtract(startTime)); - final String script = renderScript(toolbox, ctx, TRIM_TEMPLATE); - log.debug("operate: running script: "+script); - final String scriptOutput = exec(script, op.isNoExec()); - log.debug("operate: command output: "+scriptOutput); + super.process(ctx, op, source, output, subOutput, toolbox, assetManager); } } diff --git a/src/test/resources/tests/test_add_silence.jvcl b/src/test/resources/tests/test_add_silence.jvcl index ef1a7bc..f7203f0 100644 --- a/src/test/resources/tests/test_add_silence.jvcl +++ b/src/test/resources/tests/test_add_silence.jvcl @@ -19,7 +19,9 @@ { "operation": "add-silence", // name of the operation "creates": "v2_silent", // output asset name - "source": "v2" // main video asset + "source": "v2", // main video asset + "channelLayout": "stereo", // optional channel layout, usually 'mono' or 'stereo'. Default is 'stereo' + "samplingRate": 48000 // optional samping rate, in Hz. default is 48000 } ] }