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