瀏覽代碼

WIP. added merge-audio operation. refactoring. adding add-silence operation

master
Jonathan Cobb 3 年之前
父節點
當前提交
e296f9138b
共有 29 個檔案被更改,包括 519 行新增85 行删除
  1. +6
    -0
      README.md
  2. +35
    -0
      bin/jaddsilence
  3. +43
    -0
      bin/jmergeaudio
  4. +16
    -0
      docs/complex_example.md
  5. +12
    -0
      pom.xml
  6. +10
    -1
      src/main/java/jvcl/model/JAsset.java
  7. +13
    -11
      src/main/java/jvcl/model/JFileExtension.java
  8. +32
    -8
      src/main/java/jvcl/model/info/JMediaInfo.java
  9. +19
    -0
      src/main/java/jvcl/model/info/JTrack.java
  10. +5
    -0
      src/main/java/jvcl/operation/AddSilenceOperation.java
  11. +32
    -0
      src/main/java/jvcl/operation/HasStartAndEnd.java
  12. +2
    -16
      src/main/java/jvcl/operation/KenBurnsOperation.java
  13. +21
    -0
      src/main/java/jvcl/operation/MergeAudioOperation.java
  14. +4
    -19
      src/main/java/jvcl/operation/OverlayOperation.java
  15. +1
    -10
      src/main/java/jvcl/operation/SplitOperation.java
  16. +3
    -14
      src/main/java/jvcl/operation/TrimOperation.java
  17. +36
    -0
      src/main/java/jvcl/operation/exec/AddSilenceExec.java
  18. +1
    -1
      src/main/java/jvcl/operation/exec/KenBurnsExec.java
  19. +161
    -0
      src/main/java/jvcl/operation/exec/MergeAudioExec.java
  20. +1
    -1
      src/main/java/jvcl/operation/exec/SplitExec.java
  21. +1
    -1
      src/main/java/jvcl/operation/exec/TrimExec.java
  22. +2
    -1
      src/main/java/jvcl/service/AssetManager.java
  23. +2
    -1
      src/main/java/jvcl/service/Toolbox.java
  24. +2
    -0
      src/test/java/javicle/test/BasicTest.java
  25. 二進制
     
  26. 二進制
     
  27. +25
    -0
      src/test/resources/tests/test_add_silence.jvcl
  28. +33
    -0
      src/test/resources/tests/test_merge_audio.jvcl
  29. +1
    -1
      utils/cobbzilla-utils

+ 6
- 0
README.md 查看文件

@@ -88,6 +88,9 @@ Today, JVCL supports several basic operations.
For each operation listed below, the header links to an example from the JVCL
test suite.

### [add-silence](src/test/resources/tests/test_add_silence.jvcl)
Add a silent audio track to a video asset.

### [concat](src/test/resources/tests/test_concat.jvcl)
Concatenate audio/video assets together into one asset.

@@ -99,6 +102,9 @@ Transform a video from one size to another size, maintaining the aspect ratio
of the video and adding letterboxes on the sides or top/bottom.
Handy for embedding mobile videos into other screen formats.

### [merge-audio](src/test/resources/tests/test_merge_audio.jvcl)
Merge an audio asset into the audio track of a video asset.

### [overlay](src/test/resources/tests/test_overlay.jvcl)
Overlay one asset onto another.



+ 35
- 0
bin/jaddsilence 查看文件

@@ -0,0 +1,35 @@
#!/bin/bash
#
# Add a silent audio track to a video asset
#
# Usage:
#
# jaddsilence in-file out-file
#
# in-file : input video file
# out-file : write output file here
#
SCRIPT="${0}"
SCRIPT_DIR="$(cd "$(dirname "${SCRIPT}")" && pwd)"
. "${SCRIPT_DIR}"/jvcl_common

IN_FILE="${1?no video-file provided}"
OUT_FILE="${2?no out-file provided}"

echo "
{
\"assets\": [
{ \"name\": \"input\", \"path\": \"${IN_FILE}\" }
],
\"operations\": [
{
\"operation\": \"add-silence\",
\"creates\": {
\"name\": \"with_silence\",
\"dest\": \"${OUT_FILE}\"
},
\"source\": \"input\"
}
]
}
" | "${SCRIPT_DIR}"/jvcl ${JVCL_OPTIONS}

+ 43
- 0
bin/jmergeaudio 查看文件

@@ -0,0 +1,43 @@
#!/bin/bash
#
# Merge an audio asset into the audio track of a video asset
#
# Usage:
#
# jmergeaudio video-file audio-file out-file [at]
#
# video-file : input video file
# audio-file : audio file to insert into video
# out-file : write output file here
# at : when (on the video timeline) to start playing the audio
# If omitted, audio will start when video starts
#
SCRIPT="${0}"
SCRIPT_DIR="$(cd "$(dirname "${SCRIPT}")" && pwd)"
. "${SCRIPT_DIR}"/jvcl_common

VIDEO_FILE="${1?no video-file provided}"
AUDIO_FILE="${2?no audio-file provided}"
OUT_FILE="${3?no out-file provided}"
T_START="${4}"

echo "
{
\"assets\": [
{ \"name\": \"video_file\", \"path\": \"${VIDEO_FILE}\" },
{ \"name\": \"audio_file\", \"path\": \"${AUDIO_FILE}\" }
],
\"operations\": [
{
\"operation\": \"merge-audio\",
\"creates\": {
\"name\": \"with_audio\",
\"dest\": \"${OUT_FILE}\"
},
\"source\": \"video_file\",
\"audio\": \"audio_file\"$(if [[ -n "${T_START}" ]] ; then echo ",
\"at\": \"${T_START}\""; fi)
}
]
}
" | "${SCRIPT_DIR}"/jvcl ${JVCL_OPTIONS}

+ 16
- 0
docs/complex_example.md 查看文件

@@ -38,6 +38,13 @@ field, which can be used as well.
"name": "img1",
"path": "https://live.staticflickr.com/65535/48159911972_01efa0e5ea_b.jpg",
"dest": "src/test/resources/sources/"
},

// Audio clip
{
"name": "bull-roar",
"path": "http://soundbible.com/grab.php?id=2073&type=mp3",
"dest": "src/test/resources/sources/"
}
],
"operations": [
@@ -172,6 +179,15 @@ field, which can be used as well.
"type": "audio", // track type to remove
"number": "0" // track number to remove
}
},

// merge-audio example
{
"operation": "merge-audio", // name of the operation
"creates": "with_roar", // output asset name
"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)
}
]
}


+ 12
- 0
pom.xml 查看文件

@@ -108,6 +108,10 @@ javicle is available under the Apache License, version 2: http://www.apache.org/
<groupId>com.codeborne</groupId>
<artifactId>*</artifactId>
</exclusion>
<exclusion>
<groupId>com.nixxcode.jvmbrotli</groupId>
<artifactId>*</artifactId>
</exclusion>
<exclusion>
<groupId>jtidy</groupId>
<artifactId>*</artifactId>
@@ -124,6 +128,14 @@ javicle is available under the Apache License, version 2: http://www.apache.org/
<groupId>org.apache.ant</groupId>
<artifactId>*</artifactId>
</exclusion>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>*</artifactId>
</exclusion>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>*</artifactId>
</exclusion>
<exclusion>
<groupId>com.opencsv</groupId>
<artifactId>*</artifactId>


+ 10
- 1
src/main/java/jvcl/model/JAsset.java 查看文件

@@ -13,6 +13,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.cobbzilla.util.collection.ArrayUtil;
import org.cobbzilla.util.http.HttpUtil;
import org.cobbzilla.util.http.URIUtil;

import java.io.File;
import java.io.FileOutputStream;
@@ -168,6 +169,14 @@ public class JAsset implements JsObjectView {
return width == null || height == null ? null : divideBig(width, height);
}

public BigDecimal samplingRate() { return hasInfo() ? getInfo().samplingRate() : null; }
@JsonIgnore public BigDecimal getSamplingRate() { return samplingRate(); }
public boolean hasSamplingRate() { return samplingRate() != null; }

public String channelLayout() { return hasInfo() ? getInfo().channelLayout() : null; }
@JsonIgnore public String getChannelLayout() { return channelLayout(); }
public boolean hasChannelLayout() { return channelLayout() != null; }

public JAsset init(AssetManager assetManager, Toolbox toolbox) {
final JAsset asset = initPath(assetManager);
if (!asset.hasListAssets()) {
@@ -195,7 +204,7 @@ public class JAsset implements JsObjectView {
final File sourcePath;
if (hasDest()) {
if (destIsDirectory()) {
sourcePath = new File(getDest(), basename(getPath()));
sourcePath = new File(getDest(), basename(URIUtil.getPath(getPath())));
} else {
sourcePath = new File(getDest());
}


+ 13
- 11
src/main/java/jvcl/model/JFileExtension.java 查看文件

@@ -11,16 +11,18 @@ import static jvcl.model.info.JTrackType.*;
@AllArgsConstructor @Slf4j
public enum JFileExtension {

mp4 (".mp4", video),
mkv (".mkv", video),
mp3 (".mp3", audio),
aac (".aac", audio),
flac (".flac", audio),
png (".png", image),
jpg (".jpg", image),
jpeg (".jpeg", image),
sub (".sub", subtitle),
dat (".dat", data);
mp4 (".mp4", video),
mkv (".mkv", video),
mp3 (".mp3", audio),
mpeg_audio (".mp3", audio),
aac (".aac", audio),
flac (".flac", audio),
png (".png", image),
jpg (".jpg", image),
jpeg (".jpeg", image),
sub (".sub", subtitle),
dat (".dat", data),
txt (".txt", data);

@JsonCreator public static JFileExtension fromString(String v) { return valueOf(v.toLowerCase()); }

@@ -45,7 +47,7 @@ public enum JFileExtension {
}
if (track.hasFormat()) {
try {
return fromString(track.getFormat());
return fromString(track.getFormat().replace(" ", "_"));
} catch (Exception e) {
log.warn("fromTrack: unrecognized format: "+track.getFormat());
}


+ 32
- 8
src/main/java/jvcl/model/info/JMediaInfo.java 查看文件

@@ -22,6 +22,7 @@ public class JMediaInfo {
}

@Getter @Setter private JMedia media;
private boolean emptyMedia() { return media == null || empty(media.getTrack()); }

private final AtomicReference<JFormat> formatRef = new AtomicReference<>();

@@ -31,7 +32,7 @@ public class JMediaInfo {
}

private JFormat initFormat () {
if (media == null || empty(media.getTrack())) return null;
if (emptyMedia()) return null;
JTrack general = null;
JTrack video = null;
JTrack audio = null;
@@ -64,12 +65,16 @@ public class JMediaInfo {

final JFormat format = new JFormat();
if (video != null) {
format.setFileExtension(JFileExtension.fromString(general.getFileExtension()))
format.setFileExtension(video.hasFormat()
? JFileExtension.fromTrack(video)
: JFileExtension.fromString(general.getFileExtension()))
.setHeight(video.height())
.setWidth(video.width());

} else if (audio != null) {
format.setFileExtension(JFileExtension.fromString(general.getFileExtension()));
format.setFileExtension(audio.hasFormat()
? JFileExtension.fromTrack(audio)
: JFileExtension.fromString(general.getFileExtension()));

} else if (image != null) {
format.setFileExtension(JFileExtension.fromString(general.getFileExtension()))
@@ -83,7 +88,7 @@ public class JMediaInfo {
}

public BigDecimal duration() {
if (media == null || empty(media.getTrack())) return ZERO;
if (emptyMedia()) return ZERO;

// find the longest media track
BigDecimal longest = null;
@@ -96,8 +101,27 @@ public class JMediaInfo {
return longest;
}

public BigDecimal samplingRate() {
if (emptyMedia()) return null;
for (JTrack t : media.getTrack()) {
if (!t.audioOrVideo()) continue;
if (t.hasSamplingRate()) return big(t.getSamplingRate());
}
return null;
}

public String channelLayout() {
if (emptyMedia()) return null;
for (JTrack t : media.getTrack()) {
if (!t.audioOrVideo()) continue;
final String channelLayout = t.channelLayout();
if (!empty(channelLayout)) return channelLayout;
}
return null;
}

public BigDecimal width() {
if (media == null || empty(media.getTrack())) return ZERO;
if (emptyMedia()) return ZERO;
// find the first video track
for (JTrack t : media.getTrack()) {
if (!t.imageOrVideo()) continue;
@@ -108,7 +132,7 @@ public class JMediaInfo {
}

public BigDecimal height() {
if (media == null || empty(media.getTrack())) return ZERO;
if (emptyMedia()) return ZERO;
// find the first video track
for (JTrack t : media.getTrack()) {
if (!t.imageOrVideo()) continue;
@@ -119,7 +143,7 @@ public class JMediaInfo {
}

public int numTracks(JTrackType type) {
if (media == null || empty(media.getTrack())) return 0;
if (emptyMedia()) return 0;
int count = 0;
for (JTrack t : media.getTrack()) {
if (t.type() == type) count++;
@@ -128,7 +152,7 @@ public class JMediaInfo {
}

public JTrack firstTrack(JTrackType type) {
if (media == null || empty(media.getTrack())) return null;
if (emptyMedia()) return null;
for (JTrack t : media.getTrack()) {
if (t.type() == type) return t;
}


+ 19
- 0
src/main/java/jvcl/model/info/JTrack.java 查看文件

@@ -7,6 +7,7 @@ import lombok.Setter;

import static java.lang.Integer.parseInt;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.string.StringUtil.isOnlyDigits;

public class JTrack {

@@ -41,8 +42,26 @@ public class JTrack {
@JsonProperty("Channels") @Getter @Setter private String channels;
@JsonProperty("ChannelPositions") @Getter @Setter private String channelPositions;
@JsonProperty("ChannelLayout") @Getter @Setter private String channelLayout;

public String channelLayout () {
if (!empty(channelLayout)) return channelLayout;
if (!empty(channels)) {
if (isOnlyDigits(channels)) {
switch (parseInt(channels)) {
case 1: return "mono";
case 2: return "stereo";
}
}
return channels;
}
return null;
}

@JsonProperty("SamplesPerFrame") @Getter @Setter private String samplesPerFrame;

@JsonProperty("SamplingRate") @Getter @Setter private String samplingRate;
public boolean hasSamplingRate () { return !empty(samplingRate); }

@JsonProperty("SamplingCount") @Getter @Setter private String samplingCount;
@JsonProperty("Compression_Mode") @Getter @Setter private String compressionMode;
@JsonProperty("BitRate") @Getter @Setter private String bitrate;


+ 5
- 0
src/main/java/jvcl/operation/AddSilenceOperation.java 查看文件

@@ -0,0 +1,5 @@
package jvcl.operation;

import jvcl.model.operation.JSingleSourceOperation;

public class AddSilenceOperation extends JSingleSourceOperation {}

+ 32
- 0
src/main/java/jvcl/operation/HasStartAndEnd.java 查看文件

@@ -0,0 +1,32 @@
package jvcl.operation;

import org.cobbzilla.util.javascript.JsEngine;

import java.math.BigDecimal;
import java.util.Map;

import static java.math.BigDecimal.ZERO;
import static jvcl.service.Toolbox.evalBig;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;

public interface HasStartAndEnd {

String getStart ();
default boolean hasStartTime () { return !empty(getStart()); }

String getEnd ();
default boolean hasEndTime () { return !empty(getEnd()); }

default BigDecimal getStartTime(Map<String, Object> ctx, JsEngine js) {
return evalBig(getStart(), ctx, js, ZERO);
}

default BigDecimal getEndTime(Map<String, Object> ctx, JsEngine js) {
return getEndTime(ctx, js, null);
}

default BigDecimal getEndTime(Map<String, Object> ctx, JsEngine js, BigDecimal defaultValue) {
return evalBig(getEnd(), ctx, js, defaultValue);
}

}

+ 2
- 16
src/main/java/jvcl/operation/KenBurnsOperation.java 查看文件

@@ -8,12 +8,12 @@ import org.cobbzilla.util.javascript.JsEngine;
import java.math.BigDecimal;
import java.util.Map;

import static java.math.BigDecimal.ZERO;
import static jvcl.service.Toolbox.evalBig;
import static org.cobbzilla.util.daemon.ZillaRuntime.big;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;

public class KenBurnsOperation extends JSingleSourceOperation {
public class KenBurnsOperation extends JSingleSourceOperation
implements HasWidthAndHeight, HasStartAndEnd {

public static final BigDecimal DEFAULT_FPS = big(25);
public static final BigDecimal DEFAULT_UPSCALE = big(8);
@@ -29,16 +29,7 @@ public class KenBurnsOperation extends JSingleSourceOperation {
}

@Getter @Setter private String start;
public boolean hasStart () { return !empty(start); }
public BigDecimal getStartTime(Map<String, Object> ctx, JsEngine js) {
return evalBig(start, ctx, js, ZERO);
}

@Getter @Setter private String end;
public boolean hasEndTime () { return !empty(end); }
public BigDecimal getEndTime(Map<String, Object> ctx, JsEngine js, BigDecimal defaultValue) {
return evalBig(end, ctx, js, defaultValue);
}

@Getter @Setter private String x;
public boolean hasX () { return !empty(x); }
@@ -49,12 +40,7 @@ public class KenBurnsOperation extends JSingleSourceOperation {
public BigDecimal getY(Map<String, Object> ctx, JsEngine js) { return evalBig(y, ctx, js); }

@Getter @Setter private String width;
public boolean hasWidth () { return !empty(width); }
public BigDecimal getWidth(Map<String, Object> ctx, JsEngine js) { return evalBig(width, ctx, js); }

@Getter @Setter private String height;
public boolean hasHeight () { return !empty(height); }
public BigDecimal getHeight(Map<String, Object> ctx, JsEngine js) { return evalBig(height, ctx, js); }

@Getter @Setter private String fps;
public boolean hasFps () { return !empty(fps); }


+ 21
- 0
src/main/java/jvcl/operation/MergeAudioOperation.java 查看文件

@@ -0,0 +1,21 @@
package jvcl.operation;

import jvcl.model.operation.JSingleSourceOperation;
import lombok.Getter;
import lombok.Setter;
import org.cobbzilla.util.javascript.JsEngine;

import java.math.BigDecimal;
import java.util.Map;

import static java.math.BigDecimal.ZERO;
import static jvcl.service.Toolbox.evalBig;

public class MergeAudioOperation extends JSingleSourceOperation {

@Getter @Setter private String insert;

@Getter @Setter private String at;
public BigDecimal getAt(Map<String, Object> ctx, JsEngine js) { return evalBig(at, ctx, js, ZERO); }

}

+ 4
- 19
src/main/java/jvcl/operation/OverlayOperation.java 查看文件

@@ -9,38 +9,23 @@ import org.cobbzilla.util.javascript.JsEngine;
import java.math.BigDecimal;
import java.util.Map;

import static java.math.BigDecimal.ZERO;
import static jvcl.service.Toolbox.evalBig;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;

@Slf4j
public class OverlayOperation extends JSingleSourceOperation {
public class OverlayOperation extends JSingleSourceOperation implements HasStartAndEnd {

@Getter @Setter private OverlayConfig overlay;

@Getter @Setter private String start;
public BigDecimal getStartTime(Map<String, Object> ctx, JsEngine js) {
return evalBig(start, ctx, js, ZERO);
}

@Getter @Setter private String end;
public BigDecimal getEndTime(Map<String, Object> ctx, JsEngine js) {
return evalBig(end, ctx, js);
}

public static class OverlayConfig implements HasWidthAndHeight {
public static class OverlayConfig implements HasStartAndEnd, HasWidthAndHeight {

@Getter @Setter private String source;

@Getter @Setter private String start;
public BigDecimal getStartTime(Map<String, Object> ctx, JsEngine js) {
return evalBig(start, ctx, js, ZERO);
}

@Getter @Setter private String end;
public boolean hasEndTime () { return !empty(end); }
public BigDecimal getEndTime(Map<String, Object> ctx, JsEngine js) {
return evalBig(end, ctx, js);
}

@Getter @Setter private String width;
@Getter @Setter private String height;
@@ -52,6 +37,6 @@ public class OverlayOperation extends JSingleSourceOperation {
@Getter @Setter private String y;
public boolean hasY () { return !empty(y); }
public BigDecimal getY(Map<String, Object> ctx, JsEngine js) { return evalBig(y, ctx, js); }

}

}

+ 1
- 10
src/main/java/jvcl/operation/SplitOperation.java 查看文件

@@ -1,6 +1,5 @@
package jvcl.operation;

import jvcl.model.JAsset;
import jvcl.model.operation.JSingleSourceOperation;
import lombok.Getter;
import lombok.Setter;
@@ -10,11 +9,10 @@ import org.cobbzilla.util.javascript.JsEngine;
import java.math.BigDecimal;
import java.util.Map;

import static java.math.BigDecimal.ZERO;
import static jvcl.service.Toolbox.evalBig;

@Slf4j
public class SplitOperation extends JSingleSourceOperation {
public class SplitOperation extends JSingleSourceOperation implements HasStartAndEnd {

@Getter @Setter private String interval;
public BigDecimal getIntervalIncr(Map<String, Object> ctx, JsEngine js) {
@@ -22,13 +20,6 @@ public class SplitOperation extends JSingleSourceOperation {
}

@Getter @Setter private String start;
public BigDecimal getStartTime(Map<String, Object> ctx, JsEngine js) {
return evalBig(start, ctx, js, ZERO);
}

@Getter @Setter private String end;
public BigDecimal getEndTime(JAsset asset, Map<String, Object> ctx, JsEngine js) {
return evalBig(end, ctx, js, asset.duration());
}

}

+ 3
- 14
src/main/java/jvcl/operation/TrimOperation.java 查看文件

@@ -4,27 +4,16 @@ import jvcl.model.operation.JSingleSourceOperation;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.javascript.JsEngine;

import java.math.BigDecimal;
import java.util.Map;

import static java.math.BigDecimal.ZERO;
import static jvcl.service.Toolbox.evalBig;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.string.StringUtil.safeShellArg;

@Slf4j
public class TrimOperation extends JSingleSourceOperation {
public class TrimOperation extends JSingleSourceOperation implements HasStartAndEnd {

@Getter @Setter private String start;
public BigDecimal getStartTime(Map<String, Object> ctx, JsEngine js) { return evalBig(start, ctx, js, ZERO); }

@Getter @Setter private String end;
public boolean hasEnd() { return !empty(end); }
public BigDecimal getEndTime(Map<String, Object> ctx, JsEngine js) { return evalBig(end, ctx, js); }

@Override public String shortString() { return safeShellArg("trim_" + getStart() + (hasEnd() ? "_" + getEnd() : "")); }
public String toString() { return getSource()+"_"+getStart()+(hasEnd() ? "_"+getEnd() : ""); }
@Override public String shortString() { return safeShellArg("trim_" + getStart() + (hasEndTime() ? "_" + getEnd() : "")); }
public String toString() { return getSource()+"_"+getStart()+(hasEndTime() ? "_"+getEnd() : ""); }

}

+ 36
- 0
src/main/java/jvcl/operation/exec/AddSilenceExec.java 查看文件

@@ -0,0 +1,36 @@
package jvcl.operation.exec;

import jvcl.model.JAsset;
import jvcl.model.JFileExtension;
import jvcl.model.operation.JSingleOperationContext;
import jvcl.operation.AddSilenceOperation;
import jvcl.service.AssetManager;
import jvcl.service.Toolbox;
import lombok.extern.slf4j.Slf4j;

import java.util.HashMap;
import java.util.Map;

@Slf4j
public class AddSilenceExec extends SingleOrMultiSourceExecBase<AddSilenceOperation> {

public static final String ADD_SILENCE_TEMPLATE = "";

@Override public void operate(AddSilenceOperation op, Toolbox toolbox, AssetManager assetManager) {
final JSingleOperationContext opCtx = op.getSingleInputContext(assetManager);
final JAsset source = opCtx.source;
final JAsset output = opCtx.output;
final JFileExtension formatType = opCtx.formatType;

final Map<String, Object> ctx = new HashMap<>();
ctx.put("ffmpeg", toolbox.getFfmpeg());
ctx.put("source", source);

operate(op, toolbox, assetManager, source, output, formatType, ctx);
}

@Override protected void process(Map<String, Object> ctx, AddSilenceOperation addSilenceOperation, JAsset source, JAsset output, JAsset asset, Toolbox toolbox, AssetManager assetManager) {
// todo
}

}

+ 1
- 1
src/main/java/jvcl/operation/exec/KenBurnsExec.java 查看文件

@@ -67,7 +67,7 @@ public class KenBurnsExec extends ExecBase<KenBurnsOperation> {

final BigDecimal start = op.getStartTime(ctx, js);
final BigDecimal end = op.getEndTime(ctx, js, duration.subtract(start));
if (op.hasStart() || op.hasEndTime()) {
if (op.hasStartTime() || op.hasEndTime()) {
ctx.put("startFrame", start.multiply(fps).intValue());
ctx.put("endFrame", end.multiply(fps).intValue());
}


+ 161
- 0
src/main/java/jvcl/operation/exec/MergeAudioExec.java 查看文件

@@ -0,0 +1,161 @@
package jvcl.operation.exec;

import jvcl.model.JAsset;
import jvcl.model.JFileExtension;
import jvcl.model.operation.JSingleOperationContext;
import jvcl.operation.MergeAudioOperation;
import jvcl.service.AssetManager;
import jvcl.service.Toolbox;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.io.TempDir;
import org.cobbzilla.util.javascript.StandardJsEngine;

import java.io.File;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

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.system.OsType.CURRENT_OS;
import static org.cobbzilla.util.system.OsType.windows;

@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}}}";

public static final String MERGE_AUDIO_TEMPLATE
= "{{{ffmpeg}}} -i {{{source.path}}} -i {{audio.path}} -filter_complex \""
+ "[0:a][1:a] amix=inputs=2 [merged]"
+ "\" "
+ "-map 0:v -map \"[merged]\" -c:v copy "
+ "-y {{{output.path}}}";

@Override public void operate(MergeAudioOperation op, Toolbox toolbox, AssetManager assetManager) {
final JSingleOperationContext opCtx = op.getSingleInputContext(assetManager);
final JAsset source = opCtx.source;
final JAsset output = opCtx.output;
final JFileExtension formatType = opCtx.formatType;

final JAsset audio = assetManager.resolve(op.getInsert());

final StandardJsEngine js = toolbox.getJs();
final Map<String, Object> ctx = new HashMap<>();
ctx.put("ffmpeg", toolbox.getFfmpeg());
ctx.put("source", source);
ctx.put("audio", audio);

final BigDecimal insertAt = op.getAt(ctx, js);
ctx.put("start", insertAt);

if (insertAt.compareTo(ZERO) > 0) {
final JAsset silence = createSilence(op, toolbox, assetManager, insertAt, audio);
final JAsset padded = padWithSilence(op, toolbox, assetManager, audio, silence);
ctx.put("audio", padded);
}

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,
JAsset audio,
JAsset silence) {
final Map<String, Object> ctx = new HashMap<>();
ctx.put("ffmpeg", toolbox.getFfmpeg());

final JFileExtension ext = audio.getFormat().getFileExtension();
final JAsset padded = new JAsset().setPath(abs(assetManager.assetPath(op, audio, ext)));
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
final boolean doChmod = CURRENT_OS != windows; // don't chmod the dir on windows
@Cleanup("delete") final TempDir tempDir = new TempDir(assetManager.getScratchDir(), doChmod);
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);

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);
}

}

+ 1
- 1
src/main/java/jvcl/operation/exec/SplitExec.java 查看文件

@@ -38,7 +38,7 @@ public class SplitExec extends ExecBase<SplitOperation> {

assetManager.addOperationArrayAsset(output);
final BigDecimal incr = op.getIntervalIncr(ctx, js);
final BigDecimal endTime = op.getEndTime(source, ctx, js);
final BigDecimal endTime = op.getEndTime(ctx, js, source.duration());
for (BigDecimal i = op.getStartTime(ctx, js);
i.compareTo(endTime) < 0;
i = i.add(incr)) {


+ 1
- 1
src/main/java/jvcl/operation/exec/TrimExec.java 查看文件

@@ -49,7 +49,7 @@ public class TrimExec extends SingleOrMultiSourceExecBase<TrimOperation> {
final StandardJsEngine js = toolbox.getJs();
final BigDecimal startTime = op.getStartTime(ctx, js);
ctx.put("startSeconds", startTime);
if (op.hasEnd()) ctx.put("interval", op.getEndTime(ctx, js).subtract(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);


+ 2
- 1
src/main/java/jvcl/service/AssetManager.java 查看文件

@@ -3,6 +3,7 @@ package jvcl.service;
import jvcl.model.JAsset;
import jvcl.model.JFileExtension;
import jvcl.model.operation.JOperation;
import lombok.Getter;
import org.cobbzilla.util.handlebars.HandlebarsUtil;

import java.io.File;
@@ -23,7 +24,7 @@ public class AssetManager {
public static final String RANGE_SEP = "..";

private final Toolbox toolbox;
private final File scratchDir;
@Getter private final File scratchDir;
private final Map<String, JAsset> assets = new ConcurrentHashMap<>();

public AssetManager(Toolbox toolbox, File scratchDir) {


+ 2
- 1
src/main/java/jvcl/service/Toolbox.java 查看文件

@@ -108,7 +108,8 @@ public class Toolbox {
final File infoFile = new File(infoName);
final String infoPath = abs(infoFile);
if (!infoFile.exists() || infoFile.length() == 0) {
execScript(getMediainfo() + " --Output=JSON " + abs(asset.getPath())+" > "+infoPath);
final String mediaInfoScript = getMediainfo() + " --Output=JSON " + abs(asset.getPath()) + " > " + infoPath;
execScript(mediaInfoScript);
}
if (!infoFile.exists() || infoFile.length() == 0) {
return die("getInfo: info file was not created or was empty: "+infoPath);


+ 2
- 0
src/test/java/javicle/test/BasicTest.java 查看文件

@@ -29,6 +29,8 @@ public class BasicTest {
@Test public void testOverlay () { runSpec("tests/test_overlay.jvcl"); }
@Test public void testKenBurns () { runSpec("tests/test_ken_burns.jvcl"); }
@Test public void testRemoveTrack () { runSpec("tests/test_remove_track.jvcl"); }
@Test public void testMergeAudio () { runSpec("tests/test_merge_audio.jvcl"); }
@Test public void testAddSilence () { runSpec("tests/test_add_silence.jvcl"); }

private void runSpec(String specPath) {
try {


二進制
查看文件


二進制
查看文件


+ 25
- 0
src/test/resources/tests/test_add_silence.jvcl 查看文件

@@ -0,0 +1,25 @@
{
"assets": [
// this US government videos is covered by copyright
{
"name": "vid2",
"path": "https://archive.org/download/gov.archives.arc.49442/gov.archives.arc.49442_512kb.mp4",
"dest": "src/test/resources/sources/"
}
],
"operations": [
// trim video first, so test runs faster
{
"operation": "trim",
"creates": "v2",
"source": "vid2",
"start": "10",
"end": "30"
},
{
"operation": "add-silence", // name of the operation
"creates": "v2_silent", // output asset name
"source": "v2" // main video asset
}
]
}

+ 33
- 0
src/test/resources/tests/test_merge_audio.jvcl 查看文件

@@ -0,0 +1,33 @@
{
"assets": [
// this US government videos is covered by copyright
{
"name": "vid2",
"path": "https://archive.org/download/gov.archives.arc.49442/gov.archives.arc.49442_512kb.mp4",
"dest": "src/test/resources/sources/"
},
// this sound clip is in the public domain: http://soundbible.com/2073-Red-Stag-Roar.html
{
"name": "bull-roar",
"path": "http://soundbible.com/grab.php?id=2073&type=mp3",
"dest": "src/test/resources/sources/"
}
],
"operations": [
// trim video first, so test runs faster
{
"operation": "trim",
"creates": "v2",
"source": "vid2",
"start": "10",
"end": "30"
},
{
"operation": "merge-audio", // name of the operation
"creates": "with_roar", // output asset name
"source": "v2", // 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)
}
]
}

+ 1
- 1
utils/cobbzilla-utils

@@ -1 +1 @@
Subproject commit 81913bcc9d42c625451043dfa3b0fd482a2ea832
Subproject commit d9e13332dbba7d3d95a582f50609e62becaab9cc

Loading…
取消
儲存