@@ -90,7 +90,7 @@ goes to new files. | |||||
# Requirements | # Requirements | ||||
* Java 11 | * Java 11 | ||||
* Maven 3 | * Maven 3 | ||||
* ffmpeg | |||||
* ffmpeg (`HEAD` is required for `overlay` and `merge-audio` operations) | |||||
* mediainfo | * mediainfo | ||||
These programs should executable (given your `PATH`): `javac`, `java`, `mvn`, | These programs should executable (given your `PATH`): `javac`, `java`, `mvn`, | ||||
@@ -82,6 +82,12 @@ An array of the audio tracks in a video. | |||||
#### `videoTracks` | #### `videoTracks` | ||||
An array of the video tracks in a video. | An array of the video tracks in a video. | ||||
#### `audioTrack` | |||||
The track number of the first audio track. | |||||
#### `videoTrack` | |||||
The track number of the first video track. | |||||
#### `hasAudio` | #### `hasAudio` | ||||
A boolean, true if the asset has any audio tracks, false otherwise | A boolean, true if the asset has any audio tracks, false otherwise | ||||
@@ -47,7 +47,9 @@ public class JAsset implements JsObjectView { | |||||
public JAsset(JAsset other, File file) { this(other); setPath(abs(file)); } | public JAsset(JAsset other, File file) { this(other); setPath(abs(file)); } | ||||
@Getter @Setter private String name; | |||||
@Setter private String name; | |||||
public String getName () { return empty(name) ? basename(path) : name; } | |||||
@Getter @Setter private String path; | @Getter @Setter private String path; | ||||
public boolean hasPath() { return !empty(path); } | public boolean hasPath() { return !empty(path); } | ||||
@@ -159,9 +161,11 @@ public class JAsset implements JsObjectView { | |||||
public BigDecimal width() { return hasInfo() ? getInfo().width() : null; } | public BigDecimal width() { return hasInfo() ? getInfo().width() : null; } | ||||
@JsonIgnore public BigDecimal getWidth () { return width(); } | @JsonIgnore public BigDecimal getWidth () { return width(); } | ||||
public boolean hasWidth () { return width() != null; } | |||||
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 boolean hasHeight () { return height() != null; } | |||||
public int numTracks(JTrackType type) { return hasInfo() ? getInfo().numTracks(type) : 0; } | public int numTracks(JTrackType type) { return hasInfo() ? getInfo().numTracks(type) : 0; } | ||||
@@ -26,6 +26,10 @@ public class JAssetJs { | |||||
@Getter public JTrackJs[] tracks = EMPTY_TRACKS; | @Getter public JTrackJs[] tracks = EMPTY_TRACKS; | ||||
@Getter public JTrackJs[] videoTracks = EMPTY_TRACKS; | @Getter public JTrackJs[] videoTracks = EMPTY_TRACKS; | ||||
@Getter public JTrackJs[] audioTracks = EMPTY_TRACKS; | @Getter public JTrackJs[] audioTracks = EMPTY_TRACKS; | ||||
@Getter public final Integer videoTrack; | |||||
@Getter public final Integer audioTrack; | |||||
@Getter public JAssetJs[] assets = EMPTY_ASSETS; | @Getter public JAssetJs[] assets = EMPTY_ASSETS; | ||||
@Getter public final boolean hasAudio; | @Getter public final boolean hasAudio; | ||||
@Getter public final boolean hasVideo; | @Getter public final boolean hasVideo; | ||||
@@ -44,10 +48,13 @@ public class JAssetJs { | |||||
this.aspectRatio = asset.aspectRatio() == null ? Double.NaN : asset.aspectRatio().doubleValue(); | this.aspectRatio = asset.aspectRatio() == null ? Double.NaN : asset.aspectRatio().doubleValue(); | ||||
this.channelLayout = asset.hasChannelLayout() ? asset.channelLayout() : null; | this.channelLayout = asset.hasChannelLayout() ? asset.channelLayout() : null; | ||||
this.samplingRate = asset.hasSamplingRate() ? asset.samplingRate().intValue() : 0; | |||||
this.samplingRate = asset.hasSamplingRate() ? asset.samplingRate().intValue() : null; | |||||
Integer vTrack = null; | |||||
Integer aTrack = null; | |||||
if (asset.hasInfo()) { | if (asset.hasInfo()) { | ||||
final JMediaInfo info = asset.getInfo(); | final JMediaInfo info = asset.getInfo(); | ||||
int trackCount = 0; | |||||
for (JTrack track : info.getMedia().getTrack()) { | for (JTrack track : info.getMedia().getTrack()) { | ||||
final JTrackJs trackJs = new JTrackJs(track.type().name()); | final JTrackJs trackJs = new JTrackJs(track.type().name()); | ||||
@@ -59,13 +66,18 @@ public class JAssetJs { | |||||
switch (track.type()) { | switch (track.type()) { | ||||
case audio: | case audio: | ||||
audioTracks = ArrayUtil.append(audioTracks, trackJs); | audioTracks = ArrayUtil.append(audioTracks, trackJs); | ||||
if (aTrack == null) aTrack = trackCount; | |||||
break; | break; | ||||
case video: | case video: | ||||
videoTracks = ArrayUtil.append(videoTracks, trackJs); | videoTracks = ArrayUtil.append(videoTracks, trackJs); | ||||
if (vTrack == null) vTrack = trackCount; | |||||
break; | break; | ||||
} | } | ||||
trackCount++; | |||||
} | } | ||||
} | } | ||||
this.videoTrack = vTrack; | |||||
this.audioTrack = aTrack; | |||||
if (asset.hasListAssets()) { | if (asset.hasListAssets()) { | ||||
final JAsset[] list = asset.getList(); | final JAsset[] list = asset.getList(); | ||||
@@ -6,6 +6,7 @@ import jvc.service.AssetManager; | |||||
import jvc.service.Toolbox; | import jvc.service.Toolbox; | ||||
import lombok.Getter; | import lombok.Getter; | ||||
import lombok.Setter; | import lombok.Setter; | ||||
import lombok.experimental.Accessors; | |||||
import java.util.List; | import java.util.List; | ||||
@@ -14,6 +15,7 @@ import static jvc.model.JAsset.json2asset; | |||||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | import static org.cobbzilla.util.daemon.ZillaRuntime.die; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | ||||
@Accessors(chain=true) | |||||
public abstract class JMultiSourceOperation extends JOperation { | public abstract class JMultiSourceOperation extends JOperation { | ||||
@Getter @Setter private String[] sources; | @Getter @Setter private String[] sources; | ||||
@@ -1,7 +1,18 @@ | |||||
package jvc.operation; | package jvc.operation; | ||||
import jvc.model.operation.JMultiSourceOperation; | import jvc.model.operation.JMultiSourceOperation; | ||||
import lombok.Getter; | |||||
import lombok.Setter; | |||||
import lombok.experimental.Accessors; | |||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
@Slf4j | |||||
public class ConcatOperation extends JMultiSourceOperation {} | |||||
@Slf4j @Accessors(chain=true) | |||||
public class ConcatOperation extends JMultiSourceOperation { | |||||
@Getter @Setter private Boolean audioOnly; | |||||
public boolean audioOnly () { return audioOnly != null && audioOnly; } | |||||
@Getter @Setter private Boolean videoOnly; | |||||
public boolean videoOnly () { return videoOnly != null && videoOnly; } | |||||
} |
@@ -2,6 +2,7 @@ package jvc.operation.exec; | |||||
import jvc.model.JAsset; | import jvc.model.JAsset; | ||||
import jvc.model.JStreamType; | import jvc.model.JStreamType; | ||||
import jvc.model.js.JAssetJs; | |||||
import jvc.model.operation.JMultiOperationContext; | import jvc.model.operation.JMultiOperationContext; | ||||
import jvc.operation.ConcatOperation; | import jvc.operation.ConcatOperation; | ||||
import jvc.service.AssetManager; | import jvc.service.AssetManager; | ||||
@@ -23,13 +24,31 @@ public class ConcatExec extends ExecBase<ConcatOperation> { | |||||
= "{{ffmpeg}} {{#each sources}} -i {{{this.path}}}{{/each}} " | = "{{ffmpeg}} {{#each sources}} -i {{{this.path}}}{{/each}} " | ||||
// filter: list inputs | // filter: list inputs | ||||
+ "-filter_complex \"{{#each sources}}[{{@index}}:v] [{{@index}}:a] {{/each}} " | |||||
+ "-filter_complex \"" | |||||
// create null sources for missing audio/video streams | |||||
+ "{{#each sources}}" | |||||
+ "{{#if anyVideo}}{{#unless hasVideo}} nullsrc=s={{firstWidth}}x{{firstHeight}}:duration={{duration}} [null_video_{{@index}}]; {{/unless}}{{/if}}" | |||||
+ "{{#if anyAudio}}{{#unless hasAudio}} anullsrc=channel_layout={{firstChannelLayout}}:sample_rate={{firstSamplingRate}}:duration={{duration}} [null_audio_{{@index}}]; {{/unless}}{{/if}}" | |||||
+ "{{/each}} " | |||||
+ "{{#each sources}}" | |||||
+ "{{#if anyVideo}}{{#if hasVideo}}[{{@index}}:{{videoTrack}}]{{else}}[null_video_{{@index}}]{{/if}}{{/if}} " | |||||
+ "{{#if anyAudio}}{{#if hasAudio}}[{{@index}}:{{audioTrack}}]{{else}}[null_audio_{{@index}}]{{/if}}{{/if}} " | |||||
+ "{{/each}} " | |||||
// filter: concat filter them together | // filter: concat filter them together | ||||
+ "concat=n={{sources.length}}:v=1:a=1 [v] [a]\" " | |||||
+ "concat=n={{sources.length}}" | |||||
+ ":v={{#if anyVideo}}1{{else}}0{{/if}}" | |||||
+ ":a={{#if anyAudio}}1{{else}}0{{/if}} " | |||||
+ "{{#if anyVideo}}[v]{{/if}} " | |||||
+ "{{#if anyAudio}}[a]{{/if}}" | |||||
+ "\" " | |||||
// output combined result | // output combined result | ||||
+ "-map \"[v]\" -map \"[a]\" -y {{{output.path}}}"; | |||||
+ "{{#if anyVideo}}-map \"[v]\"{{else}}-vn{{/if}} " | |||||
+ "{{#if anyAudio}}-map \"[a]\"{{else}}-an{{/if}} " | |||||
+ "-y {{{output.path}}}"; | |||||
@Override public Map<String, Object> operate(ConcatOperation op, Toolbox toolbox, AssetManager assetManager) { | @Override public Map<String, Object> operate(ConcatOperation op, Toolbox toolbox, AssetManager assetManager) { | ||||
@@ -39,13 +58,45 @@ public class ConcatExec extends ExecBase<ConcatOperation> { | |||||
final JStreamType streamType = opCtx.streamType; | final JStreamType streamType = opCtx.streamType; | ||||
final File defaultOutfile = assetManager.assetPath(op, sources, streamType); | final File defaultOutfile = assetManager.assetPath(op, sources, streamType); | ||||
final File path = resolveOutputPath(output, defaultOutfile); | |||||
final File path = resolveOutputPath(assetManager, output, defaultOutfile); | |||||
if (path == null) return null; | if (path == null) return null; | ||||
output.setPath(abs(path)); | output.setPath(abs(path)); | ||||
final Map<String, Object> ctx = new HashMap<>(); | final Map<String, Object> ctx = new HashMap<>(); | ||||
ctx.put("ffmpeg", toolbox.getFfmpeg()); | ctx.put("ffmpeg", toolbox.getFfmpeg()); | ||||
ctx.put("sources", sources); | ctx.put("sources", sources); | ||||
final boolean anyVideo = !op.audioOnly() && sources.stream().anyMatch(JAsset::hasHeight); | |||||
final boolean anyAudio = !op.videoOnly() && sources.stream().anyMatch(JAsset::hasChannelLayout); | |||||
ctx.put("anyVideo", anyVideo); | |||||
ctx.put("anyAudio", anyAudio); | |||||
if (anyVideo) { | |||||
ctx.put("firstWidth", sources.stream() | |||||
.map(s -> ((JAssetJs) s.toJs())) | |||||
.filter(a -> a.hasVideo && a.width != null) | |||||
.map(a -> a.width) | |||||
.findFirst().orElse(null)); | |||||
ctx.put("firstHeight", sources.stream() | |||||
.map(s -> ((JAssetJs) s.toJs())) | |||||
.filter(a -> a.hasVideo && a.height != null) | |||||
.map(a -> a.height) | |||||
.findFirst().orElse(null)); | |||||
} | |||||
if (anyAudio) { | |||||
ctx.put("firstChannelLayout", sources.stream() | |||||
.map(s -> ((JAssetJs) s.toJs())) | |||||
.filter(a -> a.hasAudio && a.channelLayout != null) | |||||
.map(a -> a.channelLayout) | |||||
.findFirst().orElse(null)); | |||||
ctx.put("firstSamplingRate", sources.stream() | |||||
.map(s -> ((JAssetJs) s.toJs())) | |||||
.filter(a -> a.hasAudio && a.samplingRate != null) | |||||
.map(a -> a.samplingRate) | |||||
.findFirst().orElse(null)); | |||||
} | |||||
ctx.put("output", output); | ctx.put("output", output); | ||||
final String script = renderScript(toolbox, ctx, CONCAT_RECODE_TEMPLATE_1); | final String script = renderScript(toolbox, ctx, CONCAT_RECODE_TEMPLATE_1); | ||||
@@ -42,10 +42,11 @@ public abstract class ExecBase<OP extends JOperation> { | |||||
return HandlebarsUtil.apply(toolbox.getHandlebars(), template, jsContext(ctx)); | return HandlebarsUtil.apply(toolbox.getHandlebars(), template, jsContext(ctx)); | ||||
} | } | ||||
protected File resolveOutputPath(JAsset output, File defaultOutfile) { | |||||
protected File resolveOutputPath(AssetManager assetManager, JAsset output, File defaultOutfile) { | |||||
if (output.hasDest()) { | if (output.hasDest()) { | ||||
if (output.destExists() && !output.destIsDirectory()) { | if (output.destExists() && !output.destIsDirectory()) { | ||||
log.info("resolveOutputPath: dest exists: " + output.getDest()); | log.info("resolveOutputPath: dest exists: " + output.getDest()); | ||||
assetManager.addOperationAsset(output.setPath(output.getDest())); | |||||
return null; | return null; | ||||
} else if (output.destIsDirectory()) { | } else if (output.destIsDirectory()) { | ||||
return new File(output.destDirectory(), basename(abs(defaultOutfile))); | return new File(output.destDirectory(), basename(abs(defaultOutfile))); | ||||
@@ -115,6 +116,7 @@ public abstract class ExecBase<OP extends JOperation> { | |||||
final String scriptOutput = exec(script, op.isNoExec()); | final String scriptOutput = exec(script, op.isNoExec()); | ||||
log.debug("createSilence: command output: "+scriptOutput); | log.debug("createSilence: command output: "+scriptOutput); | ||||
assetManager.addOperationAsset(silence); | |||||
return silence; | return silence; | ||||
} | } | ||||
@@ -52,7 +52,7 @@ public class KenBurnsExec extends ExecBase<KenBurnsOperation> { | |||||
final JStreamType streamType = opCtx.streamType; | final JStreamType streamType = opCtx.streamType; | ||||
final File defaultOutfile = assetManager.assetPath(op, source, streamType); | final File defaultOutfile = assetManager.assetPath(op, source, streamType); | ||||
final File path = resolveOutputPath(output, defaultOutfile); | |||||
final File path = resolveOutputPath(assetManager, output, defaultOutfile); | |||||
if (path == null) return null; | if (path == null) return null; | ||||
output.setPath(abs(path)); | output.setPath(abs(path)); | ||||
@@ -1,46 +1,38 @@ | |||||
package jvc.operation.exec; | package jvc.operation.exec; | ||||
import com.fasterxml.jackson.databind.JsonNode; | |||||
import jvc.model.JAsset; | import jvc.model.JAsset; | ||||
import jvc.model.JStreamType; | import jvc.model.JStreamType; | ||||
import jvc.model.operation.JOperation; | |||||
import jvc.model.operation.JSingleOperationContext; | import jvc.model.operation.JSingleOperationContext; | ||||
import jvc.operation.ConcatOperation; | |||||
import jvc.operation.MergeAudioOperation; | import jvc.operation.MergeAudioOperation; | ||||
import jvc.service.AssetManager; | import jvc.service.AssetManager; | ||||
import jvc.service.Toolbox; | import jvc.service.Toolbox; | ||||
import lombok.Cleanup; | |||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.cobbzilla.util.io.TempDir; | |||||
import org.cobbzilla.util.javascript.JsEngine; | import org.cobbzilla.util.javascript.JsEngine; | ||||
import java.io.File; | |||||
import java.math.BigDecimal; | import java.math.BigDecimal; | ||||
import java.util.HashMap; | |||||
import java.util.Map; | import java.util.Map; | ||||
import static java.math.BigDecimal.ZERO; | import static java.math.BigDecimal.ZERO; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||||
import static org.cobbzilla.util.io.FileUtil.*; | |||||
import static org.cobbzilla.util.io.FileUtil.abs; | |||||
import static org.cobbzilla.util.json.JsonUtil.json; | |||||
@Slf4j | @Slf4j | ||||
public class MergeAudioExec extends SingleOrMultiSourceExecBase<MergeAudioOperation> { | public class MergeAudioExec extends SingleOrMultiSourceExecBase<MergeAudioOperation> { | ||||
public static final String PAD_WITH_SILENCE_TEMPLATE | |||||
= "cd {{{tempDir}}} && {{{ffmpeg}}} -f concat -i {{{playlist.path}}} -codec copy -y {{{padded}}}"; | |||||
public static final String MERGE_AUDIO_TEMPLATE | public static final String MERGE_AUDIO_TEMPLATE | ||||
= "{{{ffmpeg}}} -i {{{source.path}}} -itsoffset {{start}} -i {{audio.path}} " | = "{{{ffmpeg}}} -i {{{source.path}}} -itsoffset {{start}} -i {{audio.path}} " | ||||
// if source has no audio, define a null audio input source | |||||
+ "{{#unless source.hasAudio}}" | |||||
+ "-i anullsrc=channel_layout={{audio.channelLayout}}:sample_rate={{audio.samplingRate}} " | |||||
+ "{{/unless}}" | |||||
+ "-filter_complex \"" | + "-filter_complex \"" | ||||
+ "{{#if source.hasAudio}}" | + "{{#if source.hasAudio}}" | ||||
// source has audio -- mix with insertion | // source has audio -- mix with insertion | ||||
+ "[0:a][1:a] amix=inputs=2 [merged]" | |||||
+ "[0:a][1:{{audio.audioTrack}}] amix=inputs=2 [merged]" | |||||
+ "{{else}}" | + "{{else}}" | ||||
// source has no audio -- mix null source with insertion | // source has no audio -- mix null source with insertion | ||||
+ "[1:a] aresample=ocl={{audio.channelLayout}}:osr={{audio.samplingRate}} [1a]; " | |||||
+ "[2:a][1a] amix=inputs=2 [merged]" | |||||
+ "anullsrc=channel_layout={{audio.channelLayout}}:sample_rate={{audio.samplingRate}}:duration={{source.duration}} [silence]; " | |||||
+ "[silence][1:{{audio.audioTrack}}] amix=inputs=2 [merged]" | |||||
+ "{{/if}}" | + "{{/if}}" | ||||
+ "\" " | + "\" " | ||||
+ "-map 0:v -map \"[merged]\" -c:v copy " | + "-map 0:v -map \"[merged]\" -c:v copy " | ||||
@@ -70,47 +62,24 @@ public class MergeAudioExec extends SingleOrMultiSourceExecBase<MergeAudioOperat | |||||
AssetManager assetManager, | AssetManager assetManager, | ||||
JAsset audio, | JAsset audio, | ||||
JAsset silence) { | JAsset silence) { | ||||
final Map<String, Object> ctx = new HashMap<>(); | |||||
ctx.put("ffmpeg", toolbox.getFfmpeg()); | |||||
final JStreamType streamType = audio.audioExtension(); | final JStreamType streamType = audio.audioExtension(); | ||||
final JAsset padded = new JAsset().setPath(abs(assetManager.assetPath(op, audio, streamType))); | |||||
final String paddedName = basename(padded.getPath()); | |||||
ctx.put("padded", paddedName); | |||||
// create a temp dir for concat, it really likes to have everything in the same directory | |||||
@Cleanup("delete") final TempDir tempDir = new TempDir(assetManager.getScratchDir()); | |||||
ctx.put("tempDir", abs(tempDir)); | |||||
final String silenceName = basename(silence.getPath()); | |||||
final String audioName = basename(audio.getPath()); | |||||
// write playlist | |||||
final File playlistFile = new File(tempDir, "playlist.txt"); | |||||
toFileOrDie(playlistFile, "file "+ silenceName +"\nfile "+ audioName); | |||||
// copy audio and silence assets to temp dir | |||||
copyFile(new File(silence.getPath()), new File(tempDir, silenceName)); | |||||
copyFile(new File(audio.getPath()), new File(tempDir, audioName)); | |||||
final JAsset playlist = new JAsset().setPath(abs(playlistFile)); | |||||
ctx.put("playlist", playlist); | |||||
final String script = renderScript(toolbox, ctx, PAD_WITH_SILENCE_TEMPLATE); | |||||
log.debug("padWithSilence: running script: "+script); | |||||
final String scriptOutput = exec(script, op.isNoExec()); | |||||
final File outputFile = new File(tempDir, paddedName); | |||||
if (!outputFile.exists()) return die("padWithSilence: output file not found: "+abs(outputFile)); | |||||
copyFile(outputFile, new File(abs(padded.getPath()))); | |||||
log.debug("padWithSilence: command output: "+scriptOutput); | |||||
final JAsset padded = new JAsset() | |||||
.setPath(abs(assetManager.assetPath(op, audio, streamType))); | |||||
final JOperation concat = new ConcatOperation() | |||||
.setAudioOnly(true) | |||||
.setSources(new String[]{silence.getName(), audio.getName()}) | |||||
.setCreates(json(json(padded), JsonNode.class)) | |||||
.setOperation("concat") | |||||
.setExecIndex(op.getExecIndex()) | |||||
.setNoExec(op.isNoExec()); | |||||
final Map<String, Object> concatCtx | |||||
= concat.getExec(getSpec()).operate(concat, toolbox, assetManager); | |||||
// initialize metadata | // initialize metadata | ||||
padded.init(assetManager, toolbox); | |||||
final JAsset result = assetManager.resolve(padded.getName()); | |||||
return padded; | |||||
return result; | |||||
} | } | ||||
} | } |
@@ -2,6 +2,7 @@ package jvc.operation.exec; | |||||
import jvc.model.JAsset; | import jvc.model.JAsset; | ||||
import jvc.model.JStreamType; | import jvc.model.JStreamType; | ||||
import jvc.model.js.JAssetJs; | |||||
import jvc.model.operation.JSingleOperationContext; | import jvc.model.operation.JSingleOperationContext; | ||||
import jvc.operation.OverlayOperation; | import jvc.operation.OverlayOperation; | ||||
import jvc.service.AssetManager; | import jvc.service.AssetManager; | ||||
@@ -29,21 +30,31 @@ public class OverlayExec extends ExecBase<OverlayOperation> { | |||||
+ "[1:v] setpts=PTS-STARTPTS" | + "[1:v] setpts=PTS-STARTPTS" | ||||
+ "{{#exists width}}, scale={{width}}x{{height}}{{/exists}}" | + "{{#exists width}}, scale={{width}}x{{height}}{{/exists}}" | ||||
+ " [1v]; " | + " [1v]; " | ||||
+ "[0:v][1v] overlay={{{overlayFilterConfig}}} [v]; " | |||||
+ "{{#if source.hasAudio}}" | |||||
// source has audio -- mix with overlay | |||||
+ "[1:a] setpts=PTS-STARTPTS{{#if hasOverlayStart}}-({{overlayStart}}/TB){{/if}} [1a]; " | |||||
+ "[0:a][1a] amix=inputs=2 [merged]" | |||||
+ "{{else}}" | |||||
// source has no audio -- mix null source with overlay | |||||
+ "anullsrc=channel_layout={{overlay.channelLayout}}:sample_rate={{overlay.samplingRate}}:duration={{source.duration}} [silence]; " | |||||
// + "[1:a] setpts=PTS-STARTPTS{{#if hasOverlayStart}}-({{overlayStart}}/TB){{/if}} [1a]; " | |||||
+ "[silence][1:a] amix=inputs=2 [merged]" | |||||
+ "[0:v][1v] overlay={{{overlayFilterConfig}}} [v] " | |||||
+ "{{#if hasAudio}}" | |||||
+ "{{#if source.hasAudio}}" | |||||
+ "{{#if overlay.hasAudio}}" | |||||
// source and overlay both audio -- mix them | |||||
+ "; [1:a] setpts=PTS-STARTPTS{{#if hasOverlayStart}}-({{overlayStart}}/TB){{/if}} [1a] " | |||||
+ "; [0:a][1a] amix=inputs=2 [merged]" | |||||
+ "{{else}}" | |||||
// source has audio but overlay has none, use source audio | |||||
+ "; anullsrc=channel_layout={{source.channelLayout}}:sample_rate={{source.samplingRate}}:duration={{source.duration}} [silence] " | |||||
+ "; [silence][0:a] amix=inputs=2 [merged]" | |||||
+ "{{/if}}" | |||||
+ "{{else}}" | |||||
+ "{{#if overlay.hasAudio}}" | |||||
// source has no audio -- mix null source with overlay | |||||
+ "; anullsrc=channel_layout={{overlay.channelLayout}}:sample_rate={{overlay.samplingRate}}:duration={{source.duration}} [silence] " | |||||
+ "; [silence][1:a] amix=inputs=2 [merged]" | |||||
+ "{{/if}}" | |||||
+ "{{/if}}" | |||||
+ "{{/if}}" | + "{{/if}}" | ||||
+ "\" " | + "\" " | ||||
+ "-map \"[v]\" -map \"[merged]\" " | |||||
+ "-map \"[v]\" " | |||||
+ "{{#if hasAudio}} -map \"[merged]\"{{else}}-an{{/if}} " | |||||
+ " -y {{{output.path}}}"; | + " -y {{{output.path}}}"; | ||||
@Override public Map<String, Object> operate(OverlayOperation op, Toolbox toolbox, AssetManager assetManager) { | @Override public Map<String, Object> operate(OverlayOperation op, Toolbox toolbox, AssetManager assetManager) { | ||||
@@ -57,7 +68,7 @@ public class OverlayExec extends ExecBase<OverlayOperation> { | |||||
final JAsset overlaySource = assetManager.resolve(overlay.getSource()); | final JAsset overlaySource = assetManager.resolve(overlay.getSource()); | ||||
final File defaultOutfile = assetManager.assetPath(op, source, streamType); | final File defaultOutfile = assetManager.assetPath(op, source, streamType); | ||||
final File path = resolveOutputPath(output, defaultOutfile); | |||||
final File path = resolveOutputPath(assetManager, output, defaultOutfile); | |||||
if (path == null) return null; | if (path == null) return null; | ||||
output.setPath(abs(path)); | output.setPath(abs(path)); | ||||
@@ -103,6 +114,7 @@ public class OverlayExec extends ExecBase<OverlayOperation> { | |||||
ctx.put("overlayStart", startTime.doubleValue()); | ctx.put("overlayStart", startTime.doubleValue()); | ||||
ctx.put("hasOverlayStart", !startTime.equals(ZERO)); | ctx.put("hasOverlayStart", !startTime.equals(ZERO)); | ||||
ctx.put("overlayEnd", endTime.doubleValue()); | ctx.put("overlayEnd", endTime.doubleValue()); | ||||
ctx.put("hasAudio", ((JAssetJs) source.toJs()).hasAudio || ((JAssetJs) overlaySource.toJs()).hasAudio); | |||||
b.append("enable=between(t\\,") | b.append("enable=between(t\\,") | ||||
.append(startTime.doubleValue()) | .append(startTime.doubleValue()) | ||||
.append("\\,") | .append("\\,") | ||||
@@ -61,7 +61,7 @@ public abstract class SingleOrMultiSourceExecBase<OP extends JSingleSourceOperat | |||||
} | } | ||||
} else { | } else { | ||||
final File defaultOutfile = assetManager.assetPath(op, source, streamType); | final File defaultOutfile = assetManager.assetPath(op, source, streamType); | ||||
final File path = resolveOutputPath(output, defaultOutfile); | |||||
final File path = resolveOutputPath(assetManager, output, defaultOutfile); | |||||
if (path == null) return null; | if (path == null) return null; | ||||
output.setPath(abs(path)); | output.setPath(abs(path)); | ||||
process(ctx, op, source, output, output, toolbox, assetManager); | process(ctx, op, source, output, output, toolbox, assetManager); | ||||