@@ -67,6 +67,9 @@ Ratio of width / height (video or image). | |||
#### `samplingRate` | |||
Sampling rate in Hz (audio). | |||
#### `channelLayout` | |||
Channel layout (audio). | |||
#### `tracks` | |||
An array of the A/V tracks in a video. Only includes audio and video tracks. | |||
@@ -18,7 +18,7 @@ public enum JStreamType { | |||
mkv (".mkv", video), | |||
mp3 (".mp3", audio), | |||
mpeg_audio (".mp3", audio), | |||
aac (".aac", audio), | |||
aac (".m4a", audio), | |||
flac (".flac", audio), | |||
png (".png", image), | |||
jpg (".jpg", image), | |||
@@ -45,7 +45,11 @@ public class JTrack { | |||
public String channelLayout () { | |||
if (!empty(channelLayout)) { | |||
return channelLayout.equals("L R") ? "stereo": channelLayout; | |||
return channelLayout.equals("L R") | |||
? "stereo" | |||
: channelLayout.equals("C") | |||
? "mono" | |||
: channelLayout; | |||
} | |||
if (!empty(channels)) { | |||
if (isOnlyDigits(channels)) { | |||
@@ -20,6 +20,7 @@ public class JAssetJs { | |||
@Getter public final Integer width; | |||
@Getter public final Integer height; | |||
@Getter public final Double aspectRatio; | |||
@Getter public final String channelLayout; | |||
@Getter public final Integer samplingRate; | |||
@Getter public JTrackJs[] allTracks = EMPTY_TRACKS; | |||
@Getter public JTrackJs[] tracks = EMPTY_TRACKS; | |||
@@ -42,6 +43,7 @@ public class JAssetJs { | |||
this.height = h == null ? null : h.intValue(); | |||
this.aspectRatio = asset.aspectRatio() == null ? Double.NaN : asset.aspectRatio().doubleValue(); | |||
this.channelLayout = asset.hasChannelLayout() ? asset.channelLayout() : null; | |||
this.samplingRate = asset.hasSamplingRate() ? asset.samplingRate().intValue() : 0; | |||
if (asset.hasInfo()) { | |||
@@ -78,6 +78,7 @@ public abstract class ExecBase<OP extends JOperation> { | |||
System.out.println(script); | |||
return ""; | |||
} else { | |||
log.info("exec: "+script); | |||
return execScript(script); | |||
} | |||
} | |||
@@ -27,21 +27,30 @@ public class MergeAudioExec extends SingleOrMultiSourceExecBase<MergeAudioOperat | |||
= "cd {{{tempDir}}} && {{{ffmpeg}}} -f concat -i {{{playlist.path}}} -codec copy -y {{{padded}}}"; | |||
public static final String MERGE_AUDIO_TEMPLATE | |||
= "{{{ffmpeg}}} -i {{{source.path}}} -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 \"" | |||
+ "{{#if source.hasAudio}}" | |||
+ "-filter_complex \"" | |||
// source has audio -- mix with insertion | |||
+ "[0:a][1:a] amix=inputs=2 [merged]" | |||
+ "\" " | |||
+ "-map 0:v -map \"[merged]\" -c:v copy " | |||
+ "{{else}}" | |||
+ "-map 0:v -map 1:a -c:v copy -c:a copy " | |||
// 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]" | |||
+ "{{/if}}" | |||
+ "\" " | |||
+ "-map 0:v -map \"[merged]\" -c:v copy " | |||
+ "-y {{{output.path}}}"; | |||
@Override protected String getProcessTemplate() { return MERGE_AUDIO_TEMPLATE; } | |||
@Override | |||
protected void addCommandContext(MergeAudioOperation op, JSingleOperationContext opCtx, Map<String, Object> ctx) { | |||
@Override protected void addCommandContext(MergeAudioOperation op, | |||
JSingleOperationContext opCtx, | |||
Map<String, Object> ctx) { | |||
final JAsset audio = opCtx.assetManager.resolve(op.getInsert()); | |||
final JsEngine js = opCtx.toolbox.getJs(); | |||
final BigDecimal insertAt = op.getAt(ctx, js); | |||
@@ -64,7 +73,7 @@ public class MergeAudioExec extends SingleOrMultiSourceExecBase<MergeAudioOperat | |||
final Map<String, Object> ctx = new HashMap<>(); | |||
ctx.put("ffmpeg", toolbox.getFfmpeg()); | |||
final JStreamType streamType = audio.getFormat().getStreamType(); | |||
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); | |||
@@ -98,6 +107,9 @@ public class MergeAudioExec extends SingleOrMultiSourceExecBase<MergeAudioOperat | |||
log.debug("padWithSilence: command output: "+scriptOutput); | |||
// initialize metadata | |||
padded.init(assetManager, toolbox); | |||
return padded; | |||
} | |||
@@ -13,19 +13,38 @@ import java.io.File; | |||
import java.math.BigDecimal; | |||
import java.util.Map; | |||
import static java.math.BigDecimal.ZERO; | |||
import static org.cobbzilla.util.io.FileUtil.abs; | |||
@Slf4j | |||
public class OverlayExec extends ExecBase<OverlayOperation> { | |||
public static final String OVERLAY_TEMPLATE | |||
= "ffmpeg -i {{{source.path}}} -i {{{overlay.path}}} " | |||
= "ffmpeg -i {{{source.path}}} " | |||
+ "{{#if hasMainStart}}-ss {{mainStart}} {{/if}}" | |||
+ "{{#if hasMainEnd}}-t {{mainDuration}} {{/if}}" | |||
+ "-i {{{overlay.path}}} " | |||
+ "-filter_complex \"" | |||
+ "[1:v] setpts=PTS-STARTPTS+(1/TB){{#exists width}}, scale={{width}}x{{height}}{{/exists}} [1v]; " | |||
+ "[0:v][1v] overlay={{{overlayFilterConfig}}} " | |||
+ "\" -y {{{output.path}}}"; | |||
+ "[1:v] setpts=PTS-STARTPTS" | |||
+ "{{#exists width}}, scale={{width}}x{{height}}{{/exists}}" | |||
+ " [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]" | |||
+ "{{/if}}" | |||
+ "\" " | |||
+ "-map \"[v]\" -map \"[merged]\" " | |||
+ " -y {{{output.path}}}"; | |||
@Override public Map<String, Object> operate(OverlayOperation op, Toolbox toolbox, AssetManager assetManager) { | |||
@@ -81,7 +100,14 @@ public class OverlayExec extends ExecBase<OverlayOperation> { | |||
final StringBuilder b = new StringBuilder(); | |||
final BigDecimal startTime = overlay.getStartTime(ctx, js); | |||
final BigDecimal endTime = overlay.hasEndTime() ? overlay.getEndTime(ctx, js) : overlaySource.duration(); | |||
b.append("enable=between(t\\,").append(startTime).append("\\,").append(endTime).append(")"); | |||
ctx.put("overlayStart", startTime.doubleValue()); | |||
ctx.put("hasOverlayStart", !startTime.equals(ZERO)); | |||
ctx.put("overlayEnd", endTime.doubleValue()); | |||
b.append("enable=between(t\\,") | |||
.append(startTime.doubleValue()) | |||
.append("\\,") | |||
.append(endTime.doubleValue()) | |||
.append(")"); | |||
if (overlay.hasX() && overlay.hasY()) { | |||
b.append(":x=").append(overlay.getX(ctx, js).intValue()) | |||