@@ -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) | |||
} | |||
] | |||
} | |||
@@ -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. | |||
<sub><sup>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)</sup></sub> | |||
```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 | |||
} | |||
] | |||
} | |||
@@ -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()) { | |||
@@ -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()); | |||
} | |||
} |
@@ -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 | |||
@@ -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)) { | |||
@@ -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; | |||
} |
@@ -14,7 +14,12 @@ import java.util.Map; | |||
@Slf4j | |||
public class AddSilenceExec extends SingleOrMultiSourceExecBase<AddSilenceOperation> { | |||
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<AddSilenceOperat | |||
ctx.put("ffmpeg", toolbox.getFfmpeg()); | |||
ctx.put("source", source); | |||
operate(op, toolbox, assetManager, source, output, formatType, ctx); | |||
} | |||
final JAsset silence = createSilence(op, toolbox, assetManager, source.duration(), source); | |||
ctx.put("silence", silence); | |||
@Override protected void process(Map<String, Object> ctx, AddSilenceOperation addSilenceOperation, JAsset source, JAsset output, JAsset asset, Toolbox toolbox, AssetManager assetManager) { | |||
// todo | |||
operate(op, toolbox, assetManager, source, output, formatType, ctx); | |||
} | |||
} |
@@ -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<OP extends JOperation> { | |||
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<String, Object> 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; | |||
} | |||
} |
@@ -18,6 +18,8 @@ import static org.cobbzilla.util.string.StringUtil.safeShellArg; | |||
@Slf4j | |||
public class LetterboxExec extends SingleOrMultiSourceExecBase<LetterboxOperation> { | |||
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<LetterboxOperatio | |||
+ "color={{{color}}}" | |||
+ "\" -y {{{output.path}}}"; | |||
public static final String DEFAULT_LETTERBOX_COLOR = "black"; | |||
@Override protected String getProcessTemplate() { return LETTERBOX_TEMPLATE; } | |||
@Override public void operate(LetterboxOperation op, Toolbox toolbox, AssetManager assetManager) { | |||
@@ -57,20 +59,4 @@ public class LetterboxExec extends SingleOrMultiSourceExecBase<LetterboxOperatio | |||
operate(op, toolbox, assetManager, source, output, formatType, ctx); | |||
} | |||
@Override protected void process(Map<String, Object> 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); | |||
} | |||
} |
@@ -23,12 +23,6 @@ import static org.cobbzilla.util.io.FileUtil.*; | |||
@Slf4j | |||
public class MergeAudioExec extends SingleOrMultiSourceExecBase<MergeAudioOperation> { | |||
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<MergeAudioOperat | |||
+ "-map 0:v -map \"[merged]\" -c:v copy " | |||
+ "-y {{{output.path}}}"; | |||
@Override protected String getProcessTemplate() { return MERGE_AUDIO_TEMPLATE; } | |||
@Override public void operate(MergeAudioOperation op, Toolbox toolbox, AssetManager assetManager) { | |||
final JSingleOperationContext opCtx = op.getSingleInputContext(assetManager); | |||
final JAsset source = opCtx.source; | |||
@@ -65,35 +61,6 @@ public class MergeAudioExec extends SingleOrMultiSourceExecBase<MergeAudioOperat | |||
operate(op, toolbox, assetManager, source, output, formatType, ctx); | |||
} | |||
protected JAsset createSilence(MergeAudioOperation op, | |||
Toolbox toolbox, | |||
AssetManager assetManager, | |||
BigDecimal duration, | |||
JAsset audio) { | |||
final Map<String, Object> 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<MergeAudioOperat | |||
return padded; | |||
} | |||
@Override protected void process(Map<String, Object> 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); | |||
} | |||
} |
@@ -22,6 +22,8 @@ public class RemoveTrackExec extends SingleOrMultiSourceExecBase<RemoveTrackOper | |||
+ "-c copy " | |||
+ "-y {{{output.path}}}"; | |||
@Override protected String getProcessTemplate() { return REMOVE_TRACK_TEMPLATE; } | |||
@Override public void operate(RemoveTrackOperation op, Toolbox toolbox, AssetManager assetManager) { | |||
final JSingleOperationContext opCtx = op.getSingleInputContext(assetManager); | |||
@@ -41,20 +43,4 @@ public class RemoveTrackExec extends SingleOrMultiSourceExecBase<RemoveTrackOper | |||
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); | |||
} | |||
} |
@@ -21,6 +21,8 @@ public class ScaleExec extends SingleOrMultiSourceExecBase<ScaleOperation> { | |||
+ "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<ScaleOperation> { | |||
operate(op, toolbox, assetManager, source, output, formatType, ctx); | |||
} | |||
@Override protected void process(Map<String, Object> 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); | |||
} | |||
} |
@@ -49,12 +49,22 @@ public abstract class SingleOrMultiSourceExecBase<OP extends JOperation> extends | |||
} | |||
} | |||
protected abstract void process(Map<String, Object> ctx, | |||
OP op, | |||
JAsset source, | |||
JAsset output, | |||
JAsset asset, | |||
Toolbox toolbox, | |||
AssetManager assetManager); | |||
protected abstract String getProcessTemplate(); | |||
protected void process(Map<String, Object> 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); | |||
} | |||
} |
@@ -22,6 +22,8 @@ public class TrimExec extends SingleOrMultiSourceExecBase<TrimOperation> { | |||
"{{#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<TrimOperation> { | |||
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); | |||
} | |||
} |
@@ -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 | |||
} | |||
] | |||
} |