Browse Source

simplify json formatting. complicates parser, but it's a relatively clean solution

master
Jonathan Cobb 3 years ago
parent
commit
f5473ab3b7
23 changed files with 484 additions and 340 deletions
  1. +27
    -38
      README.md
  2. +7
    -0
      pom.xml
  3. +12
    -5
      src/main/java/jvcl/main/JvclOptions.java
  4. +27
    -5
      src/main/java/jvcl/model/JOperation.java
  5. +0
    -32
      src/main/java/jvcl/model/JOperationType.java
  6. +0
    -140
      src/main/java/jvcl/op/OverlayOperation.java
  7. +29
    -0
      src/main/java/jvcl/operation/ConcatOperation.java
  8. +79
    -0
      src/main/java/jvcl/operation/OverlayOperation.java
  9. +28
    -0
      src/main/java/jvcl/operation/SplitOperation.java
  10. +39
    -0
      src/main/java/jvcl/operation/TrimOperation.java
  11. +5
    -16
      src/main/java/jvcl/operation/exec/ConcatExec.java
  12. +9
    -16
      src/main/java/jvcl/operation/exec/ExecBase.java
  13. +82
    -0
      src/main/java/jvcl/operation/exec/OverlayExec.java
  14. +8
    -30
      src/main/java/jvcl/operation/exec/SplitExec.java
  15. +13
    -35
      src/main/java/jvcl/operation/exec/TrimExec.java
  16. +1
    -1
      src/main/java/jvcl/service/OperationEngine.java
  17. +9
    -6
      src/main/java/jvcl/service/Toolbox.java
  18. +58
    -0
      src/main/java/jvcl/service/json/JOperationFactory.java
  19. +15
    -0
      src/main/java/jvcl/service/json/JOperationModule.java
  20. +26
    -0
      src/main/java/jvcl/service/json/JOperationType.java
  21. +1
    -3
      src/test/resources/tests/test_concat.json
  22. +5
    -7
      src/test/resources/tests/test_split.json
  23. +4
    -6
      src/test/resources/tests/test_trim.json

+ 27
- 38
README.md View File

@@ -20,14 +20,12 @@ With JVCL, you'd create this spec:
{
"assets": [ {"name": "src", "path": "/tmp/my/source.mp4"} ],
"operations": [{
"operation": "split",
"creates": "src_splits",
"perform": {
"operation": "split",
"creates": "src_split_files",
"split": "src",
"interval": "10s",
"start": "10s",
"end": "130s"
}
}]
}
```
@@ -100,53 +98,44 @@ Here is a complex example using multiple assets and operations:
],
"operations": [
{
"operation": "split", // name of the operation
"operation": "split", // name of the operation,
"creates": "vid1_split_%", // assets it creates, the '%' will be replaced with a counter
"perform": {
"split": "vid1", // split this source asset
"interval": "10s" // split every ten seconds
}
"split": "vid1", // split this source asset
"interval": "10s" // split every ten seconds
},
{
"operation": "concat", // name of the operation
"operation": "concat", // name of the operation,
"creates": "recombined_vid1", // assets it creates, the '%' will be replaced with a counter
"perform": {
"concat": ["vid1_split"] // recombine all split assets
}
"concat": ["vid1_split"] // recombine all split assets
},
{
"operation": "concat", // name of the operation
"operation": "concat", // name of the operation,
"creates": "combined_vid", // asset it creates, can be referenced later
"perform": {
"concat": ["vid1", "vid2"] // operation-specific: this says, concatenate these named assets
}
"concat": ["vid1", "vid2"] // operation-specific: this says, concatenate these named assets
},
{
"operation": "concat", // name of the operation
"operation": "concat", // name of the operation,
"creates": "combined_vid", // the asset it creates, can be referenced later
"perform": {
"concat": ["vid1", "vid2"] // operation-specific: this says, concatenate these named assets
}
"concat": ["vid1", "vid2"] // operation-specific: this says, concatenate these named assets
},
{
"operation": "overlay", // name of the operation
"creates": "overlay1", // asset it creates
"perform": {
"source": "combined_vid1", // main video asset
"overlay": "vid2", // overlay this video on the main video

"offset": "30", // when (on the main video timeline) to begin showing the overlay. default is 0 (beginning)
"overlayStart": "0", // when (on the overlay video timeline) to begin playback on the overlay. default is 0 (beginning)
"overlayEnd": "0", // when (on the overlay video timeline) to end playback on the overlay. default is to play the whole overlay

"operation": "overlay", // name of the operation,
"creates": {
"name": "overlay1", // asset it creates
"width": "1920", // output width in pixels. default is source width
"height": "1024" // output height in pixes. default is source height
},
"main": "combined_vid1", // main video asset
"startTime": "30", // when (on the main video timeline) to begin showing the overlay. default is 0 (beginning)
"endTime": "60", // when (on the main video timeline) to stop showing the overlay. default is to play the entire overlay
"overlay": {
"source": "vid2", // overlay this video on the main video
"startTime": "0", // when (on the overlay video timeline) to begin playback on the overlay. default is 0 (beginning)
"endTime": "0", // when (on the overlay video timeline) to end playback on the overlay. default is to play the entire overlay
"width": "overlay.width / 2", // how wide the overlay will be, in pixels. default is the full overlay width, or maintain aspect ratio if height was set
"height": "", // how tall the overlay will be, in pixels. default is the full overlay height, or maintain aspect ratio if width was set

"x": "source.width/2", // horizontal overlay position on main video. default is 0
"y": "source.height/2", // vertical overlay position on main video. default is 0

"outputWidth": "1920", // output width in pixels. default is source width
"outputHeight": "1024" // output height in pixes. default is source height
"height": "source.height", // how tall the overlay will be, in pixels. default is the full overlay height, or maintain aspect ratio if width was set
"x": "source.width / 2", // horizontal overlay position on main video. default is 0
"y": "source.height / 2" // vertical overlay position on main video. default is 0
}
}
]


+ 7
- 0
pom.xml View File

@@ -41,6 +41,7 @@ javicle is available under the Apache License, version 2: http://www.apache.org/
<logback.version>1.2.3</logback.version>
<handlebars.version>4.2.0</handlebars.version>
<junit.version>4.13.1</junit.version>
<cglib.version>3.3.0</cglib.version>
</properties>

<profiles>
@@ -146,6 +147,12 @@ javicle is available under the Apache License, version 2: http://www.apache.org/
</exclusions>
</dependency>

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>${cglib.version}</version>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>


+ 12
- 5
src/main/java/jvcl/main/JvclOptions.java View File

@@ -9,11 +9,11 @@ import org.kohsuke.args4j.Option;

import java.io.File;

import static jvcl.service.Toolbox.JSON_MAPPER;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.daemon.ZillaRuntime.readStdin;
import static org.cobbzilla.util.io.FileUtil.abs;
import static org.cobbzilla.util.io.FileUtil.toStringOrDie;
import static org.cobbzilla.util.json.JsonUtil.FULL_MAPPER_ALLOW_COMMENTS;
import static org.cobbzilla.util.json.JsonUtil.json;

@Slf4j
@@ -26,12 +26,19 @@ public class JvclOptions extends BaseMainOptions {
@Getter @Setter private File specFile;

public JSpec getSpec() {
final String json;
if (specFile != null && !specFile.getName().equals("-")) {
if (!specFile.exists()) return die("File not found: "+abs(specFile));
return json(toStringOrDie(specFile), JSpec.class, FULL_MAPPER_ALLOW_COMMENTS);
if (!specFile.exists() || !specFile.canRead()) return die("File not found or unreadable: "+abs(specFile));
json = toStringOrDie(specFile);
} else {
log.info("reading JVCL spec from stdin...");
json = readStdin();
}
try {
return json(json, JSpec.class, JSON_MAPPER);
} catch (Exception e) {
return die("getSpec: invalid spec: "+specFile);
}
log.info("reading JVCL spec from stdin...");
return json(readStdin(), JSpec.class);
}

public static final String USAGE_SCRATCH_DIR = "Scratch directory. Default is to create a temp directory under /tmp";


+ 27
- 5
src/main/java/jvcl/model/JOperation.java View File

@@ -1,25 +1,47 @@
package jvcl.model;

import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.JsonNode;
import jvcl.operation.exec.ExecBase;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;

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

import static jvcl.service.json.JOperationFactory.getOperationExecClass;
import static org.cobbzilla.util.daemon.ZillaRuntime.hashOf;
import static org.cobbzilla.util.json.JsonUtil.json;
import static org.cobbzilla.util.reflect.ReflectionUtil.instantiate;

@NoArgsConstructor @Accessors(chain=true)
public class JOperation {
@NoArgsConstructor @Accessors(chain=true) @Slf4j
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "operation",
visible = true
)
public abstract class JOperation {

@Getter @Setter private JOperationType operation;
@Getter @Setter private String operation;
@Getter @Setter private JsonNode creates;
@Getter @Setter private JsonNode perform;

public String hash(JAsset[] sources) { return hash(sources, null); }

public String hash(JAsset[] sources, Object[] args) {
return hashOf(getOperation(), json(creates), json(perform), sources, args);
return hashOf(operation, json(this), sources, args);
}

private static final Map<Class<? extends JOperation>, ExecBase<?>> execMap = new HashMap<>();
public <OP extends JOperation> ExecBase<OP> getExec() {
return (ExecBase<OP>) execMap.computeIfAbsent(getClass(), c -> instantiate(getExecClass()));
}

protected <OP extends JOperation> Class<? extends ExecBase<OP>> getExecClass() {
return getOperationExecClass(getClass());
}

}

+ 0
- 32
src/main/java/jvcl/model/JOperationType.java View File

@@ -1,32 +0,0 @@
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;
import jvcl.service.JOperator;
import jvcl.service.Toolbox;
import lombok.AllArgsConstructor;

@AllArgsConstructor
public enum JOperationType {

concat (new ConcatOperation()),
split (new SplitOperation()),
trim (new TrimOperation()),
overlay (new OverlayOperation()),
ken_burns (null),
letterbox (null),
split_silence (null);

@JsonCreator public static JOperationType fromString(String v) { return valueOf(v.toLowerCase().replace("-", "_")); }

private final JOperator operator;

public void perform(JOperation op, Toolbox toolbox, AssetManager assetManager) {
operator.operate(op, toolbox, assetManager);
}

}

+ 0
- 140
src/main/java/jvcl/op/OverlayOperation.java View File

@@ -1,140 +0,0 @@
package jvcl.op;

import jvcl.model.JAsset;
import jvcl.model.JFileExtension;
import jvcl.model.JOperation;
import jvcl.service.AssetManager;
import jvcl.service.JOperator;
import jvcl.service.Toolbox;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.javascript.JsEngine;
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.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.system.CommandShell.execScript;

@Slf4j
public class OverlayOperation implements JOperator {

public static final String OVERLAY_TEMPLATE
= "{{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);
final JAsset source = assetManager.resolve(config.getSource());
final JAsset overlay = assetManager.resolve(config.getOverlay());

final JAsset output = json2asset(op.getCreates());
output.mergeFormat(source.getFormat());

final JFileExtension formatType = output.getFormat().getFileExtension();

final File defaultOutfile = assetManager.assetPath(op, source, formatType, new Object[]{config});
final File path = resolveOutputPath(output, defaultOutfile);
if (path == null) return;
output.setPath(abs(path));

final StandardJsEngine js = toolbox.getJs();
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, js));
ctx.put("overlayStart", config.getOverlayStartSeconds(ctx, js));
if (config.hasOverlayEnd()) ctx.put("overlayEnd", config.getOverlayEndSeconds(ctx, js));
if (config.hasWidth()) {
final String width = config.getWidth(ctx, js);
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, js);
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, js));
if (config.hasY()) ctx.put("y", config.getY(ctx, js));

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

log.debug("operate: running script: "+script);
final String scriptOutput = execScript(script);
log.debug("operate: command output: "+scriptOutput);
assetManager.addOperationAsset(output);
}

private static class OverlayConfig {
@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 (Map<String, Object> ctx, JsEngine js) {
return empty(offset) ? BigDecimal.ZERO : getDuration(eval(offset, ctx, js));
}

@Getter @Setter private String 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 (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); }
}
}

+ 29
- 0
src/main/java/jvcl/operation/ConcatOperation.java View File

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

import jvcl.model.JAsset;
import jvcl.model.JFileExtension;
import jvcl.model.JOperation;
import jvcl.service.AssetManager;
import jvcl.service.Toolbox;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static jvcl.model.JAsset.flattenAssetList;
import static jvcl.model.JAsset.json2asset;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.io.FileUtil.abs;
import static org.cobbzilla.util.system.CommandShell.execScript;

@Slf4j
public class ConcatOperation extends JOperation {

@Getter @Setter private String[] concat;

}

+ 79
- 0
src/main/java/jvcl/operation/OverlayOperation.java View File

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

import jvcl.model.JAsset;
import jvcl.model.JFileExtension;
import jvcl.model.JOperation;
import jvcl.service.AssetManager;
import jvcl.service.Toolbox;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.javascript.JsEngine;
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.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.system.CommandShell.execScript;

@Slf4j
public class OverlayOperation extends JOperation {

@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 (Map<String, Object> ctx, JsEngine js) {
return empty(offset) ? BigDecimal.ZERO : getDuration(eval(offset, ctx, js));
}

@Getter @Setter private String 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 (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); }

}

+ 28
- 0
src/main/java/jvcl/operation/SplitOperation.java View File

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

import jvcl.model.JAsset;
import jvcl.model.JOperation;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.math.BigDecimal;

import static jvcl.service.Toolbox.getDuration;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;

@Slf4j
public class SplitOperation extends JOperation {

@Getter @Setter private String split;

@Getter @Setter private String interval;
public BigDecimal getIntervalIncr() { return getDuration(interval); }

@Getter @Setter private String start;
public BigDecimal getStartTime() { return empty(start) ? BigDecimal.ZERO : getDuration(start); }

@Getter @Setter private String end;
public BigDecimal getEndTime(JAsset source) { return empty(end) ? source.duration() : getDuration(end); }

}

+ 39
- 0
src/main/java/jvcl/operation/TrimOperation.java View File

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

import jvcl.model.JAsset;
import jvcl.model.JFileExtension;
import jvcl.model.JOperation;
import jvcl.service.AssetManager;
import jvcl.service.Toolbox;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

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

import static jvcl.model.JAsset.json2asset;
import static jvcl.service.Toolbox.getDuration;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.io.FileUtil.*;
import static org.cobbzilla.util.system.CommandShell.execScript;

@Slf4j
public class TrimOperation extends JOperation {

@Getter @Setter private String trim;

@Getter @Setter private String start;
public BigDecimal getStartTime() { return empty(start) ? BigDecimal.ZERO : getDuration(start); }

@Getter @Setter private String end;
public boolean hasEnd() { return !empty(end); }
public BigDecimal getEndTime() { return getDuration(end); }

public String shortString() { return "trim_"+getStart()+(hasEnd() ? "_"+getEnd() : ""); }
public String toString() { return trim+"_"+getStart()+(hasEnd() ? "_"+getEnd() : ""); }

}

src/main/java/jvcl/op/ConcatOperation.java → src/main/java/jvcl/operation/exec/ConcatExec.java View File

@@ -1,14 +1,10 @@
package jvcl.op;
package jvcl.operation.exec;

import jvcl.model.JAsset;
import jvcl.model.JFileExtension;
import jvcl.model.JOperation;
import jvcl.operation.ConcatOperation;
import jvcl.service.AssetManager;
import jvcl.service.JOperator;
import jvcl.service.Toolbox;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
@@ -24,7 +20,7 @@ import static org.cobbzilla.util.io.FileUtil.abs;
import static org.cobbzilla.util.system.CommandShell.execScript;

@Slf4j
public class ConcatOperation implements JOperator {
public class ConcatExec extends ExecBase<ConcatOperation> {

public static final String CONCAT_RECODE_TEMPLATE_OLD
// concat inputs
@@ -45,12 +41,10 @@ public class ConcatOperation implements JOperator {
// output combined result
+ "-map \"[v]\" -map \"[a]\" {{{output.path}}}";

@Override public void operate(JOperation op, Toolbox toolbox, AssetManager assetManager) {

final ConcatConfig config = loadConfig(op, ConcatConfig.class);
@Override public void operate(ConcatOperation op, Toolbox toolbox, AssetManager assetManager) {

// validate sources
final List<JAsset> sources = flattenAssetList(assetManager.resolve(config.getConcat()));
final List<JAsset> sources = flattenAssetList(assetManager.resolve(op.getConcat()));
if (empty(sources)) die("operate: no sources");

// create output object
@@ -78,9 +72,4 @@ public class ConcatOperation implements JOperator {
assetManager.addOperationAsset(output);
}

@NoArgsConstructor
private static class ConcatConfig {
@Getter @Setter private String[] concat;
}

}

src/main/java/jvcl/service/JOperator.java → src/main/java/jvcl/operation/exec/ExecBase.java View File

@@ -1,33 +1,28 @@
package jvcl.service;
package jvcl.operation.exec;

import jvcl.model.JAsset;
import jvcl.model.JOperation;
import jvcl.service.AssetManager;
import jvcl.service.Toolbox;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.handlebars.HandlebarsUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.util.Map;

import static org.cobbzilla.util.io.FileUtil.abs;
import static org.cobbzilla.util.io.FileUtil.basename;
import static org.cobbzilla.util.json.JsonUtil.json;

public interface JOperator {
@Slf4j
public abstract class ExecBase<OP extends JOperation> {

Logger log = LoggerFactory.getLogger(JOperator.class);
public abstract void operate(OP operation, Toolbox toolbox, AssetManager assetManager);

void operate(JOperation op, Toolbox toolbox, AssetManager assetManager);

default <T> T loadConfig(JOperation op, Class<T> configClass) {
return json(json(op.getPerform()), configClass);
}

default String renderScript(Toolbox toolbox, Map<String, Object> ctx, String template) {
protected String renderScript(Toolbox toolbox, Map<String, Object> ctx, String template) {
return HandlebarsUtil.apply(toolbox.getHandlebars(), template, ctx);
}

default File resolveOutputPath(JAsset output, File defaultOutfile) {
protected File resolveOutputPath(JAsset output, File defaultOutfile) {
if (output.hasDest()) {
if (output.destExists() && !output.destIsDirectory()) {
log.info("resolveOutputPath: dest exists: " + output.getDest());
@@ -41,6 +36,4 @@ public interface JOperator {
return defaultOutfile;
}
}


}

+ 82
- 0
src/main/java/jvcl/operation/exec/OverlayExec.java View File

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

import jvcl.model.JAsset;
import jvcl.model.JFileExtension;
import jvcl.operation.OverlayOperation;
import jvcl.service.AssetManager;
import jvcl.service.Toolbox;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.javascript.StandardJsEngine;

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

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

@Slf4j
public class OverlayExec extends ExecBase<OverlayOperation> {

public static final String OVERLAY_TEMPLATE
= "{{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(OverlayOperation op, Toolbox toolbox, AssetManager assetManager) {
final JAsset source = assetManager.resolve(op.getSource());
final JAsset overlay = assetManager.resolve(op.getOverlay());

final JAsset output = json2asset(op.getCreates());
output.mergeFormat(source.getFormat());

final JFileExtension formatType = output.getFormat().getFileExtension();

final File defaultOutfile = assetManager.assetPath(op, source, formatType);
final File path = resolveOutputPath(output, defaultOutfile);
if (path == null) return;
output.setPath(abs(path));

final StandardJsEngine js = toolbox.getJs();
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", op.getOffsetSeconds(ctx, js));
ctx.put("overlayStart", op.getOverlayStartSeconds(ctx, js));
if (op.hasOverlayEnd()) ctx.put("overlayEnd", op.getOverlayEndSeconds(ctx, js));
if (op.hasWidth()) {
final String width = op.getWidth(ctx, js);
ctx.put("width", width);
if (!op.hasHeight()) {
final int height = big(width).divide(overlay.aspectRatio(), HALF_EVEN).intValue();
ctx.put("height", height);
}
}
if (op.hasHeight()) {
final String height = op.getHeight(ctx, js);
ctx.put("height", height);
if (!op.hasWidth()) {
final int width = big(height).multiply(overlay.aspectRatio()).intValue();
ctx.put("width", width);
}
}
if (op.hasX()) ctx.put("x", op.getX(ctx, js));
if (op.hasY()) ctx.put("y", op.getY(ctx, js));

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

log.debug("operate: running script: "+script);
final String scriptOutput = execScript(script);
log.debug("operate: command output: "+scriptOutput);
assetManager.addOperationAsset(output);
}

}

src/main/java/jvcl/op/SplitOperation.java → src/main/java/jvcl/operation/exec/SplitExec.java View File

@@ -1,14 +1,10 @@
package jvcl.op;
package jvcl.operation.exec;

import jvcl.model.JAsset;
import jvcl.model.JFileExtension;
import jvcl.model.JOperation;
import jvcl.operation.SplitOperation;
import jvcl.service.AssetManager;
import jvcl.service.JOperator;
import jvcl.service.Toolbox;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
@@ -17,23 +13,20 @@ import java.util.HashMap;
import java.util.Map;

import static jvcl.model.JAsset.json2asset;
import static jvcl.service.Toolbox.getDuration;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.io.FileUtil.abs;
import static org.cobbzilla.util.io.FileUtil.mkdirOrDie;
import static org.cobbzilla.util.system.CommandShell.execScript;

@Slf4j
public class SplitOperation implements JOperator {
public class SplitExec extends ExecBase<SplitOperation> {

public static final String SPLIT_TEMPLATE
= "{{ffmpeg}} -i {{{source.path}}} -ss {{startSeconds}} -t {{interval}} {{{output.path}}}";

@Override public void operate(JOperation op, Toolbox toolbox, AssetManager assetManager) {
final SplitConfig config = loadConfig(op, SplitConfig.class);
@Override public void operate(SplitOperation op, Toolbox toolbox, AssetManager assetManager) {

final JAsset source = assetManager.resolve(config.getSplit());
final JAsset source = assetManager.resolve(op.getSplit());

// create output object
final JAsset output = json2asset(op.getCreates());
@@ -48,9 +41,9 @@ public class SplitOperation implements JOperator {
final Map<String, Object> ctx = new HashMap<>();
ctx.put("ffmpeg", toolbox.getFfmpeg());
ctx.put("source", source);
final BigDecimal incr = config.getIntervalIncr();
final BigDecimal endTime = config.getEndTime(source);
for (BigDecimal i = config.getStartTime();
final BigDecimal incr = op.getIntervalIncr();
final BigDecimal endTime = op.getEndTime(source);
for (BigDecimal i = op.getStartTime();
i.compareTo(endTime) < 0;
i = i.add(incr)) {

@@ -97,19 +90,4 @@ public class SplitOperation implements JOperator {
return new File(output.destDirectory(), output.getName() + "_" + i + "_" + incr + formatType.ext());
}

@NoArgsConstructor
private static class SplitConfig {

@Getter @Setter private String split;

@Getter @Setter private String interval;
public BigDecimal getIntervalIncr() { return getDuration(interval); }

@Getter @Setter private String start;
public BigDecimal getStartTime() { return empty(start) ? BigDecimal.ZERO : getDuration(start); }

@Getter @Setter private String end;
public BigDecimal getEndTime(JAsset source) { return empty(end) ? source.duration() : getDuration(end); }
}

}

src/main/java/jvcl/op/TrimOperation.java → src/main/java/jvcl/operation/exec/TrimExec.java View File

@@ -1,15 +1,10 @@
package jvcl.op;
package jvcl.operation.exec;

import jvcl.model.JAsset;
import jvcl.model.JFileExtension;
import jvcl.model.JOperation;
import jvcl.operation.TrimOperation;
import jvcl.service.AssetManager;
import jvcl.service.JOperator;
import jvcl.service.Toolbox;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
@@ -18,22 +13,19 @@ import java.util.HashMap;
import java.util.Map;

import static jvcl.model.JAsset.json2asset;
import static jvcl.service.Toolbox.getDuration;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;
import static org.cobbzilla.util.io.FileUtil.*;
import static org.cobbzilla.util.system.CommandShell.execScript;

@Slf4j
public class TrimOperation implements JOperator {
public class TrimExec extends ExecBase<TrimOperation> {

public static final String TRIM_TEMPLATE
= "{{ffmpeg}} -i {{{source.path}}} -ss {{startSeconds}} {{#exists interval}}-t {{interval}} {{/exists}}{{{output.path}}}";

@Override public void operate(JOperation op, Toolbox toolbox, AssetManager assetManager) {
@Override public void operate(TrimOperation op, Toolbox toolbox, AssetManager assetManager) {

final TrimConfig config = loadConfig(op, TrimConfig.class);
final JAsset source = assetManager.resolve(config.getTrim());
final JAsset source = assetManager.resolve(op.getTrim());

final JAsset output = json2asset(op.getCreates());
output.mergeFormat(source.getFormat());
@@ -47,10 +39,10 @@ public class TrimOperation implements JOperator {
assetManager.addOperationArrayAsset(output);
for (JAsset asset : source.getList()) {
final JAsset subOutput = new JAsset(output);
final File defaultOutfile = assetManager.assetPath(op, asset, formatType, new Object[]{config});
final File defaultOutfile = assetManager.assetPath(op, asset, formatType);
final File outfile;
if (output.hasDest()) {
outfile = new File(output.destDirectory(), basename(appendToFileNameBeforeExt(asset.getPath(), "_"+config.shortString())));
outfile = new File(output.destDirectory(), basename(appendToFileNameBeforeExt(asset.getPath(), "_"+op.shortString())));
if (outfile.exists()) {
log.info("operate: dest exists: "+abs(outfile));
return;
@@ -59,18 +51,18 @@ public class TrimOperation implements JOperator {
outfile = defaultOutfile;
}
subOutput.setPath(abs(outfile));
trim(config, asset, output, subOutput, toolbox, assetManager);
trim(op, asset, output, subOutput, toolbox, assetManager);
}
} else {
final File defaultOutfile = assetManager.assetPath(op, source, formatType, new Object[]{config});
final File defaultOutfile = assetManager.assetPath(op, source, formatType);
final File path = resolveOutputPath(output, defaultOutfile);
if (path == null) return;
output.setPath(abs(path));
trim(config, source, output, output, toolbox, assetManager);
trim(op, source, output, output, toolbox, assetManager);
}
}

private void trim(TrimConfig config,
private void trim(TrimOperation op,
JAsset source,
JAsset output,
JAsset subOutput,
@@ -81,9 +73,9 @@ public class TrimOperation implements JOperator {
ctx.put("source", source);
ctx.put("output", subOutput);

final BigDecimal startTime = config.getStartTime();
final BigDecimal startTime = op.getStartTime();
ctx.put("startSeconds", startTime);
if (config.hasEnd()) ctx.put("interval", config.getEndTime().subtract(startTime));
if (op.hasEnd()) ctx.put("interval", op.getEndTime().subtract(startTime));
final String script = renderScript(toolbox, ctx, TRIM_TEMPLATE);

log.debug("operate: running script: "+script);
@@ -96,18 +88,4 @@ public class TrimOperation implements JOperator {
}
}

@NoArgsConstructor @EqualsAndHashCode
private static class TrimConfig {
@Getter @Setter private String trim;

@Getter @Setter private String start;
public BigDecimal getStartTime() { return empty(start) ? BigDecimal.ZERO : getDuration(start); }

@Getter @Setter private String end;
public boolean hasEnd() { return !empty(end); }
public BigDecimal getEndTime() { return getDuration(end); }

public String shortString() { return "trim_"+getStart()+(hasEnd() ? "_"+getEnd() : ""); }
public String toString() { return trim+"_"+getStart()+(hasEnd() ? "_"+getEnd() : ""); }
}
}

+ 1
- 1
src/main/java/jvcl/service/OperationEngine.java View File

@@ -13,6 +13,6 @@ public class OperationEngine {
}

public void perform(JOperation op) {
op.getOperation().perform(op, toolbox, assetManager);
op.getExec().operate(op, toolbox, assetManager);
}
}

+ 9
- 6
src/main/java/jvcl/service/Toolbox.java View File

@@ -1,13 +1,14 @@
package jvcl.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.jknack.handlebars.Handlebars;
import jvcl.model.JAsset;
import jvcl.model.JsObjectView;
import jvcl.model.info.JMediaInfo;
import jvcl.service.json.JOperationModule;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.handlebars.HandlebarsUtil;
import org.cobbzilla.util.io.FileUtil;
import org.cobbzilla.util.javascript.StandardJsEngine;

import java.io.File;
@@ -18,10 +19,8 @@ import java.util.concurrent.ConcurrentHashMap;

import static java.math.RoundingMode.HALF_EVEN;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
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;
import static org.cobbzilla.util.json.JsonUtil.json;
import static org.cobbzilla.util.io.FileUtil.*;
import static org.cobbzilla.util.json.JsonUtil.*;
import static org.cobbzilla.util.system.CommandShell.execScript;
import static org.cobbzilla.util.time.TimeUtil.parseDuration;

@@ -30,6 +29,9 @@ public class Toolbox {

public static final Toolbox DEFAULT_TOOLBOX = new Toolbox();

public static final ObjectMapper JSON_MAPPER = FULL_MAPPER_ALLOW_COMMENTS;
static { JSON_MAPPER.registerModule(new JOperationModule()); }

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

@Getter(lazy=true) private final StandardJsEngine js = new StandardJsEngine();
@@ -90,10 +92,11 @@ public class Toolbox {
}
return infoCache.computeIfAbsent(infoPath, p -> {
try {
return json(FileUtil.toStringOrDie(infoFile), JMediaInfo.class, FULL_MAPPER_ALLOW_UNKNOWN_FIELDS);
return json(toStringOrDie(infoFile), JMediaInfo.class, FULL_MAPPER_ALLOW_UNKNOWN_FIELDS);
} catch (Exception e) {
return die("getInfo: "+shortError(e), e);
}
});
}

}

+ 58
- 0
src/main/java/jvcl/service/json/JOperationFactory.java View File

@@ -0,0 +1,58 @@
package jvcl.service.json;

import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import jvcl.model.JOperation;
import jvcl.operation.exec.ExecBase;

import java.io.IOException;

import static com.fasterxml.jackson.databind.type.TypeBindings.emptyBindings;
import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.daemon.ZillaRuntime.shortError;
import static org.cobbzilla.util.reflect.ReflectionUtil.forName;

public class JOperationFactory extends DeserializationProblemHandler {

@Override public JavaType handleUnknownTypeId(DeserializationContext ctxt,
JavaType baseType,
String subTypeId,
TypeIdResolver idResolver,
String failureMsg) throws IOException {
try {
return new JOperationType(getOperationClass(subTypeId), emptyBindings(), baseType, null);
} catch (Exception e) {
throw new IOException("handleUnknownTypeId: '"+subTypeId+"' is not a valid operation type: "+shortError(e));
}
}

public static final String OPERATION_DEFAULT_PACKAGE = "jvcl.operation";
public static final String OPERATION_CLASSNAME_SUFFIX = "Operation";

public static Class<?> getOperationClass(String id) {
final String className;
if (id.contains(".")) {
className = id;
} else {
className = OPERATION_DEFAULT_PACKAGE + "." + capitalize(id) + OPERATION_CLASSNAME_SUFFIX;
}
return forName(className);
}

public static <OP extends JOperation> Class<? extends ExecBase<OP>> getOperationExecClass(Class<? extends JOperation> opClass) {
final String name = opClass.getSimpleName();
if (!name.endsWith(OPERATION_CLASSNAME_SUFFIX)) {
return die("getOperationExecClass: expected JOperation class to end with '" + OPERATION_CLASSNAME_SUFFIX + "'");
}
final String execClassName
= opClass.getPackageName()
+ ".exec."
+ name.substring(0, name.length() - OPERATION_CLASSNAME_SUFFIX.length())
+ "Exec";
return forName(execClassName);
}

}

+ 15
- 0
src/main/java/jvcl/service/json/JOperationModule.java View File

@@ -0,0 +1,15 @@
package jvcl.service.json;

import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.Module;

public class JOperationModule extends Module {

@Override public String getModuleName() { return "JOperationFactoryModule"; }
@Override public Version version() { return new Version(1, 0, 0, "", "", ""); }

@Override public void setupModule(SetupContext context) {
context.addDeserializationProblemHandler(new JOperationFactory());
}

}

+ 26
- 0
src/main/java/jvcl/service/json/JOperationType.java View File

@@ -0,0 +1,26 @@
package jvcl.service.json;

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.SimpleType;
import com.fasterxml.jackson.databind.type.TypeBase;
import com.fasterxml.jackson.databind.type.TypeBindings;

public class JOperationType extends SimpleType {

protected JOperationType(Class<?> cls) { super(cls); }

public JOperationType(Class<?> cls, TypeBindings bindings, JavaType superClass, JavaType[] superInts) {
super(cls, bindings, superClass, superInts);
}

protected JOperationType(TypeBase base) { super(base); }

protected JOperationType(Class<?> cls, TypeBindings bindings, JavaType superClass, JavaType[] superInts, Object valueHandler, Object typeHandler, boolean asStatic) {
super(cls, bindings, superClass, superInts, valueHandler, typeHandler, asStatic);
}

protected JOperationType(Class<?> cls, TypeBindings bindings, JavaType superClass, JavaType[] superInts, int extraHash, Object valueHandler, Object typeHandler, boolean asStatic) {
super(cls, bindings, superClass, superInts, extraHash, valueHandler, typeHandler, asStatic);
}

}

+ 1
- 3
src/test/resources/tests/test_concat.json View File

@@ -9,9 +9,7 @@
"name": "combined_vid",
"dest": "src/test/resources/outputs/combined.mp4"
},
"perform": {
"concat": ["vid1_splits[1..]"]
}
"concat": ["vid1_splits[1..]"]
}
]
}

+ 5
- 7
src/test/resources/tests/test_split.json View File

@@ -13,17 +13,15 @@
],
"operations": [
{
"operation": "split", // name of the operation
"operation": "split", // name of the operation
"creates": {
"name": "vid1_splits",
"dest": "src/test/resources/outputs/"
},
"perform": {
"split": "vid1", // split this source asset
"interval": "10s", // split every ten seconds
"start": "65s", // start one minute and five seconds into the video
"end": "100s" // end 100 seconds into the video
}
"split": "vid1", // split this source asset
"interval": "10s", // split every ten seconds
"start": "65s", // start one minute and five seconds into the video
"end": "100s" // end 100 seconds into the video
}
]
}

+ 4
- 6
src/test/resources/tests/test_trim.json View File

@@ -4,16 +4,14 @@
],
"operations": [
{
"operation": "trim", // name of the operation
"operation": "trim", // name of the operation
"creates": {
"name": "vid1_trims",
"dest": "src/test/resources/outputs/trims/"
},
"perform": {
"trim": "vid1_splits", // trim these source assets
"start": "1s", // cropped region starts here, default is zero
"end": "6s" // cropped region ends here, default is end of video
}
"trim": "vid1_splits", // trim these source assets
"start": "1s", // cropped region starts here, default is zero
"end": "6s" // cropped region ends here, default is end of video
}
]
}

Loading…
Cancel
Save