Explorar el Código

WIP. overlay still in development. fixes to trim, add js support

master
Jonathan Cobb hace 4 años
padre
commit
83e5efb072
Se han modificado 10 ficheros con 188 adiciones y 32 borrados
  1. +44
    -9
      src/main/java/jvcl/model/JAsset.java
  2. +2
    -1
      src/main/java/jvcl/model/JOperationType.java
  3. +7
    -0
      src/main/java/jvcl/model/JsObjectView.java
  4. +22
    -0
      src/main/java/jvcl/model/info/JMediaInfo.java
  5. +2
    -0
      src/main/java/jvcl/model/info/JTrack.java
  6. +66
    -9
      src/main/java/jvcl/op/OverlayOperation.java
  7. +24
    -10
      src/main/java/jvcl/op/TrimOperation.java
  8. +19
    -3
      src/main/java/jvcl/service/Toolbox.java
  9. +1
    -0
      src/test/java/javicle/test/BasicTest.java
  10. +1
    -0
      src/test/resources/tests/test_overlay.json

+ 44
- 9
src/main/java/jvcl/model/JAsset.java Ver fichero

@@ -21,6 +21,7 @@ import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static java.math.RoundingMode.HALF_EVEN;
import static java.util.Comparator.comparing;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.http.HttpSchemes.isHttpOrHttps;
@@ -31,7 +32,7 @@ import static org.cobbzilla.util.reflect.ReflectionUtil.copy;
import static org.cobbzilla.util.system.CommandShell.execScript;

@NoArgsConstructor @AllArgsConstructor @Accessors(chain=true) @Slf4j
public class JAsset {
public class JAsset implements JsObjectView {

public static final JAsset NULL_ASSET = new JAsset().setName("~null asset~").setPath("/dev/null");
public static final String PREFIX_CLASSPATH = "classpath:";
@@ -86,7 +87,8 @@ public class JAsset {
return hasDest() && (dest.endsWith("/") || new File(dest).isDirectory());
}
public File destDirectory() {
return mkdirOrDie(new File(dest.endsWith("/") ? dest.substring(0, dest.length()-1) : dest));
final String dir = destIsDirectory() ? dest : dirname(dest);
return mkdirOrDie(new File(dir.endsWith("/") ? dir.substring(0, dir.length()-1) : dir));
}

// if path was not a file, it got resolved to a file
@@ -142,9 +144,21 @@ public class JAsset {
}
}

public BigDecimal duration() { return getInfo().duration(); }
public BigDecimal duration() { return hasInfo() ? getInfo().duration() : null; }
@JsonIgnore public BigDecimal getDuration () { return duration(); }

public BigDecimal width() { return hasInfo() ? getInfo().width() : null; }
@JsonIgnore public BigDecimal getWidth () { return width(); }

public BigDecimal height() { return hasInfo() ? getInfo().height() : null; }
@JsonIgnore public BigDecimal getHeight () { return height(); }

public BigDecimal aspectRatio() {
final BigDecimal width = width();
final BigDecimal height = height();
return width == null || height == null ? null : width.divide(height, HALF_EVEN);
}

public JAsset init(AssetManager assetManager, Toolbox toolbox) {
final JAsset asset = initPath(assetManager);
if (!asset.hasListAssets()) {
@@ -210,12 +224,16 @@ public class JAsset {
});
final Pattern regex = Pattern.compile(b.toString());
final File dir = new File(path.substring(0, lastSlash));
final String filesInDir = execScript("find " + dir + " -type f");
final Set<String> matches = Arrays.stream(filesInDir.split("\n"))
.filter(f -> regex.matcher(f).matches())
.collect(Collectors.toCollection(() -> new TreeSet<>(comparing(String::toString))));
for (String f : matches) {
addAsset(new JAsset(this).setPath(f));
if (dir.exists()) {
final String filesInDir = execScript("find " + dir + " -type f");
final Set<String> matches = Arrays.stream(filesInDir.split("\n"))
.filter(f -> regex.matcher(f).matches())
.collect(Collectors.toCollection(() -> new TreeSet<>(comparing(String::toString))));
for (String f : matches) {
addAsset(new JAsset(this).setPath(f));
}
} else {
return die("initPath: no files matched: "+path);
}

} else {
@@ -227,4 +245,21 @@ public class JAsset {
return this;
}

@Override public Object toJs() { return new JAssetJs(this); }

public static class JAssetJs {
public Integer duration;
public Integer width;
public Integer height;
public JAssetJs (JAsset asset) {
final BigDecimal d = asset.duration();
this.duration = d == null ? null : d.intValue();

final BigDecimal w = asset.width();
this.width = w == null ? null : w.intValue();

final BigDecimal h = asset.height();
this.height = h == null ? null : h.intValue();
}
}
}

+ 2
- 1
src/main/java/jvcl/model/JOperationType.java Ver fichero

@@ -2,6 +2,7 @@ package jvcl.model;

import com.fasterxml.jackson.annotation.JsonCreator;
import jvcl.op.ConcatOperation;
import jvcl.op.OverlayOperation;
import jvcl.op.SplitOperation;
import jvcl.op.TrimOperation;
import jvcl.service.AssetManager;
@@ -15,7 +16,7 @@ public enum JOperationType {
concat (new ConcatOperation()),
split (new SplitOperation()),
trim (new TrimOperation()),
overlay (null),
overlay (new OverlayOperation()),
ken_burns (null),
letterbox (null),
split_silence (null);


+ 7
- 0
src/main/java/jvcl/model/JsObjectView.java Ver fichero

@@ -0,0 +1,7 @@
package jvcl.model;

public interface JsObjectView {

Object toJs();

}

+ 22
- 0
src/main/java/jvcl/model/info/JMediaInfo.java Ver fichero

@@ -66,4 +66,26 @@ public class JMediaInfo {
return longest;
}

public BigDecimal width() {
if (media == null || empty(media.getTrack())) return BigDecimal.ZERO;
// find the first video track
for (JTrack t : media.getTrack()) {
if (!t.video()) continue;
if (!t.hasWidth()) continue;
return big(t.getWidth());
}
return null;
}

public BigDecimal height() {
if (media == null || empty(media.getTrack())) return BigDecimal.ZERO;
// find the first video track
for (JTrack t : media.getTrack()) {
if (!t.video()) continue;
if (!t.hasHeight()) continue;
return big(t.getHeight());
}
return null;
}

}

+ 2
- 0
src/main/java/jvcl/model/info/JTrack.java Ver fichero

@@ -42,9 +42,11 @@ public class JTrack {

@JsonProperty("Width") @Getter @Setter private String width;
public Integer width () { return parseInt(width); }
public boolean hasWidth () { return !empty(width); }

@JsonProperty("Height") @Getter @Setter private String height;
public Integer height () { return parseInt(height); }
public boolean hasHeight () { return !empty(height); }

@JsonProperty("Sampled_Width") @Getter @Setter private String sampledWidth;
@JsonProperty("Sampled_Height") @Getter @Setter private String sampledHeight;


+ 66
- 9
src/main/java/jvcl/op/OverlayOperation.java Ver fichero

@@ -9,21 +9,32 @@ import jvcl.service.Toolbox;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.javascript.JsEngine;

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

import static java.math.RoundingMode.HALF_EVEN;
import static jvcl.model.JAsset.json2asset;
import static jvcl.service.Toolbox.getDuration;
import static org.cobbzilla.util.daemon.ZillaRuntime.big;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.io.FileUtil.abs;
import static org.cobbzilla.util.io.FileUtil.basename;
import static org.cobbzilla.util.system.CommandShell.execScript;

@Slf4j
public class OverlayOperation implements JOperator {

public static final String OVERLAY_TEMPLATE
= "{{ffmpeg}} ";
= "{{ffmpeg}} -i {{{source.path}}} -filter_complex \"" +
"movie={{{overlay.path}}}{{#exists overlayStart}}:seek_point={{overlayStart}}{{/exists}} [ovl]; " +
"[v:0] setpts=PTS-(STARTPTS+{{#exists offset}}{{offset}}{{else}}0{{/exists}}) [main]; " +
"[ovl] setpts=PTS-STARTPTS{{#exists width}}, scale={{width}}x{{height}}{{/exists}} ; " +
"[main][ovl] overlay=shortest=1{{#exists x}}:x={{x}}{{/exists}}{{#exists y}}:y={{y}}{{/exists}} " +
"\" {{{output.path}}}";

@Override public void operate(JOperation op, Toolbox toolbox, AssetManager assetManager) {
final OverlayConfig config = loadConfig(op, OverlayConfig.class);
@@ -34,16 +45,44 @@ public class OverlayOperation implements JOperator {
output.mergeFormat(source.getFormat());

final JFileExtension formatType = output.getFormat().getFileExtension();
if (output.hasDest()) {
if (output.destExists() && !output.destIsDirectory()) {
log.info("operate: dest exists, not trimming: " + output.getDest());
return;
} else if (output.destIsDirectory()) {
final File defaultFile = assetManager.assetPath(op, source, formatType, new Object[]{config});
output.setPath(abs(new File(output.destDirectory(), basename(abs(defaultFile)))));
} else {
output.setPath(output.destPath());
}
}

final Map<String, Object> ctx = new HashMap<>();
ctx.put("ffmpeg", toolbox.getFfmpeg());
ctx.put("source", source);
ctx.put("overlay", overlay);
ctx.put("output", output);
ctx.put("offset", config.getOffsetSeconds());
ctx.put("overlayStart", config.getOverlayStartSeconds());
if (config.hasOverlayEnd()) ctx.put("overlayEnd", config.getOverlayEndSeconds());
if (config.hasWidth()) ctx.put("width", config.getWidth());
if (config.hasHeight()) ctx.put("height", config.getHeight());
ctx.put("offset", config.getOffsetSeconds(ctx, toolbox.getJs()));
ctx.put("overlayStart", config.getOverlayStartSeconds(ctx, toolbox.getJs()));
if (config.hasOverlayEnd()) ctx.put("overlayEnd", config.getOverlayEndSeconds(ctx, toolbox.getJs()));
if (config.hasWidth()) {
final String width = config.getWidth(ctx, toolbox.getJs());
ctx.put("width", width);
if (!config.hasHeight()) {
final int height = big(width).divide(overlay.aspectRatio(), HALF_EVEN).intValue();
ctx.put("height", height);
}
}
if (config.hasHeight()) {
final String height = config.getHeight(ctx, toolbox.getJs());
ctx.put("height", height);
if (!config.hasWidth()) {
final int width = big(height).multiply(overlay.aspectRatio()).intValue();
ctx.put("width", width);
}
}
if (config.hasX()) ctx.put("x", config.getX(ctx, toolbox.getJs()));
if (config.hasY()) ctx.put("y", config.getY(ctx, toolbox.getJs()));

final String script = renderScript(toolbox, ctx, OVERLAY_TEMPLATE);

@@ -57,32 +96,50 @@ public class OverlayOperation implements JOperator {
@Getter @Setter private String source;
@Getter @Setter private String overlay;

private String eval(String val, Map<String, Object> ctx, JsEngine js) {
final Map<String, Object> jsCtx = Toolbox.jsContext(ctx);
final Object result = js.evaluate(val, jsCtx);
return result == null ? null : result.toString();
}

@Getter @Setter private String offset;
public BigDecimal getOffsetSeconds () { return empty(offset) ? BigDecimal.ZERO : getDuration(offset); }
public BigDecimal getOffsetSeconds (Map<String, Object> ctx, JsEngine js) {
return empty(offset) ? BigDecimal.ZERO : getDuration(eval(offset, ctx, js));
}

@Getter @Setter private String overlayStart;
public BigDecimal getOverlayStartSeconds () { return empty(overlayStart) ? BigDecimal.ZERO : getDuration(overlayStart); }
public BigDecimal getOverlayStartSeconds (Map<String, Object> ctx, JsEngine js) {
return empty(overlayStart) ? BigDecimal.ZERO : getDuration(eval(overlayStart, ctx, js));
}

@Getter @Setter private String overlayEnd;
public boolean hasOverlayEnd () { return !empty(overlayEnd); }
public BigDecimal getOverlayEndSeconds () { return getDuration(overlayEnd); }
public BigDecimal getOverlayEndSeconds (Map<String, Object> ctx, JsEngine js) {
return getDuration(eval(overlayEnd, ctx, js));
}

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

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

@Getter @Setter private String x;
public boolean hasX () { return !empty(x); }
public String getX(Map<String, Object> ctx, JsEngine js) { return eval(x, ctx, js); }

@Getter @Setter private String y;
public boolean hasY () { return !empty(y); }
public String getY(Map<String, Object> ctx, JsEngine js) { return eval(y, ctx, js); }

@Getter @Setter private String outputWidth;
public boolean hasOutputWidth () { return !empty(outputWidth); }
public String getOutputWidth(Map<String, Object> ctx, JsEngine js) { return eval(outputWidth, ctx, js); }

@Getter @Setter private String outputHeight;
public boolean hasOutputHeight () { return !empty(outputHeight); }
public String getOutputHeight(Map<String, Object> ctx, JsEngine js) { return eval(outputHeight, ctx, js); }
}
}

+ 24
- 10
src/main/java/jvcl/op/TrimOperation.java Ver fichero

@@ -51,34 +51,44 @@ public class TrimOperation implements JOperator {
final File outfile;
if (output.hasDest()) {
outfile = new File(output.destDirectory(), basename(appendToFileNameBeforeExt(asset.getPath(), "_"+config.shortString())));
if (outfile.exists()) {
log.info("operate: dest exists: "+abs(outfile));
return;
}
} else {
outfile = defaultOutfile;
}
subOutput.setPath(abs(outfile));
trim(config, asset, subOutput, toolbox, assetManager);
trim(config, asset, output, subOutput, toolbox, assetManager);
}
} else {
if (output.hasDest() && output.destExists()) {
log.info("operate: dest exists, not trimming: "+output.getDest());
final File defaultOutfile = assetManager.assetPath(op, source, formatType, new Object[]{config});
if (output.hasDest()) {
if (output.destExists() && !output.destIsDirectory()) {
log.info("operate: dest exists, not trimming: " + output.getDest());
return;
} else if (output.destIsDirectory()) {
output.setPath(abs(new File(output.destDirectory(), basename(abs(defaultOutfile)))));
} else {
output.setPath(output.destPath());
}
} else {
trim(config, source, output, toolbox, assetManager);
output.setPath(abs(defaultOutfile));
}
trim(config, source, output, output, toolbox, assetManager);
}
}

private void trim(TrimConfig config,
JAsset source,
JAsset output,
JAsset subOutput,
Toolbox toolbox,
AssetManager assetManager) {
if (output.destExists()) {
log.info("trim: dest exists: "+output.getDest());
return;
}
final Map<String, Object> ctx = new HashMap<>();
ctx.put("ffmpeg", toolbox.getFfmpeg());
ctx.put("source", source);
ctx.put("output", output);
ctx.put("output", subOutput);

final BigDecimal startTime = config.getStartTime();
ctx.put("startSeconds", startTime);
@@ -88,7 +98,11 @@ public class TrimOperation implements JOperator {
log.debug("operate: running script: "+script);
final String scriptOutput = execScript(script);
log.debug("operate: command output: "+scriptOutput);
assetManager.addOperationAssetSlice(output, output);
if (output == subOutput) {
assetManager.addOperationAsset(output);
} else {
assetManager.addOperationAssetSlice(output, subOutput);
}
}

@NoArgsConstructor @EqualsAndHashCode


+ 19
- 3
src/main/java/jvcl/service/Toolbox.java Ver fichero

@@ -2,6 +2,7 @@ package jvcl.service;

import com.github.jknack.handlebars.Handlebars;
import jvcl.model.JAsset;
import jvcl.model.JsObjectView;
import jvcl.model.info.JMediaInfo;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@@ -11,12 +12,12 @@ import org.cobbzilla.util.javascript.StandardJsEngine;

import java.io.File;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static java.math.RoundingMode.HALF_EVEN;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.daemon.ZillaRuntime.big;
import static org.cobbzilla.util.io.FileUtil.abs;
import static org.cobbzilla.util.io.FileUtil.replaceExt;
import static org.cobbzilla.util.json.JsonUtil.FULL_MAPPER_ALLOW_UNKNOWN_FIELDS;
@@ -31,8 +32,23 @@ public class Toolbox {

@Getter(lazy=true) private final Handlebars handlebars = initHandlebars();

@Getter(lazy=true) private final StandardJsEngine js = new StandardJsEngine();

public static BigDecimal getDuration(String t) {
return big(parseDuration(t)).divide(big(1000), RoundingMode.UNNECESSARY);
return big(parseDuration(t)).divide(big(1000), HALF_EVEN);
}

public static Map<String, Object> jsContext(Map<String, Object> ctx) {
final Map<String, Object> jsCtx = new HashMap<>();
for (Map.Entry<String, Object> entry : ctx.entrySet()) {
final Object value = entry.getValue();
if (value instanceof JsObjectView) {
jsCtx.put(entry.getKey(), ((JsObjectView) value).toJs());
} else {
jsCtx.put(entry.getKey(), value);
}
}
return jsCtx;
}

private Handlebars initHandlebars() {


+ 1
- 0
src/test/java/javicle/test/BasicTest.java Ver fichero

@@ -16,6 +16,7 @@ public class BasicTest {
@Test public void testSplit () { runSpec("tests/test_split.json"); }
@Test public void testConcat () { runSpec("tests/test_concat.json"); }
@Test public void testTrim () { runSpec("tests/test_trim.json"); }
@Test public void testOverlay() { runSpec("tests/test_overlay.json"); }

private void runSpec(String specPath) {
@Cleanup("delete") final File specFile = stream2file(loadResourceAsStream(specPath));


+ 1
- 0
src/test/resources/tests/test_overlay.json Ver fichero

@@ -35,5 +35,6 @@
"outputWidth": "1920", // output width in pixels. default is source width
"outputHeight": "1024" // output height in pixes. default is source height
}
}
]
}

Cargando…
Cancelar
Guardar