Sfoglia il codice sorgente

add letterbox, refactor operations that can take one more many assets

master
Jonathan Cobb 4 anni fa
parent
commit
870d16b4a1
12 ha cambiato i file con 230 aggiunte e 85 eliminazioni
  1. +9
    -1
      README.md
  2. +4
    -0
      src/main/java/jvcl/model/operation/JOperation.java
  3. +24
    -0
      src/main/java/jvcl/operation/LetterboxOperation.java
  4. +2
    -1
      src/main/java/jvcl/operation/TrimOperation.java
  5. +83
    -0
      src/main/java/jvcl/operation/exec/LetterboxExec.java
  6. +9
    -38
      src/main/java/jvcl/operation/exec/ScaleExec.java
  7. +59
    -0
      src/main/java/jvcl/operation/exec/SingleOrMultiSourceExecBase.java
  8. +9
    -39
      src/main/java/jvcl/operation/exec/TrimExec.java
  9. +3
    -2
      src/main/java/jvcl/service/Toolbox.java
  10. +2
    -1
      src/test/java/javicle/test/BasicTest.java
  11. +23
    -0
      src/test/resources/tests/test_letterbox.jvcl
  12. +3
    -3
      src/test/resources/tests/test_scale.jvcl

+ 9
- 1
README.md Vedi File

@@ -128,7 +128,7 @@ The above would set the `start` value to ten seconds before the end of `someAsse
Today, JVCL supports these operations:

### scale
Scale a video asset from one size to another
Scale a video asset from one size to another. Scaling can be proportional or anamorphic

### split
Split an audio/video asset into multiple assets of equal time lengths
@@ -254,6 +254,14 @@ Here is a complex example using multiple assets and operations.
"upscale": "8", // upscale factor. upscaling the image results in a smoother pan, but a longer encode, default is 8
"width": "1024", // width of output video
"height": "768" // height of output video
},
{
"operation": "letterbox", // name of the operation
"creates": "boxed1", // asset it creates
"source": "ken1", // source asset
"width": "source.width * 1.5", // make it wider
"height": "source.height * 0.9", // and shorter
"color": "AliceBlue" // default is black. can be a hex value (0xff0000 for red) or a color name from here: https://ffmpeg.org/ffmpeg-utils.html#color-syntax
}
]
}


+ 4
- 0
src/main/java/jvcl/model/operation/JOperation.java Vedi File

@@ -17,6 +17,8 @@ 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;
import static org.cobbzilla.util.security.ShaUtil.sha256_hex;
import static org.cobbzilla.util.string.StringUtil.safeShellArg;

@NoArgsConstructor @Accessors(chain=true) @Slf4j
@JsonTypeInfo(
@@ -41,4 +43,6 @@ public abstract class JOperation {
return (ExecBase<OP>) execMap.computeIfAbsent(getClass(), c -> instantiate(getOperationExecClass(getClass())));
}

public String shortString() { return safeShellArg(operation+"_"+sha256_hex(json(this))); }

}

+ 24
- 0
src/main/java/jvcl/operation/LetterboxOperation.java Vedi File

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

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

import java.util.Map;

import static org.cobbzilla.util.daemon.ZillaRuntime.empty;

public class LetterboxOperation extends JSingleSourceOperation implements HasWidthAndHeight {

@Getter @Setter private String width;
@Getter @Setter private String height;

@Getter @Setter private String color;
public boolean hasColor () { return !empty(color); }

public String shortString(Map<String, Object> ctx, JsEngine js) {
return "letterbox_"+color+"_"+getWidth(ctx, js)+"x"+getHeight(ctx, js);
}

}

+ 2
- 1
src/main/java/jvcl/operation/TrimOperation.java Vedi File

@@ -12,6 +12,7 @@ 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 {
@@ -23,7 +24,7 @@ public class TrimOperation extends JSingleSourceOperation {
public boolean hasEnd() { return !empty(end); }
public BigDecimal getEndTime(Map<String, Object> ctx, JsEngine js) { return evalBig(end, ctx, js); }

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

}

+ 83
- 0
src/main/java/jvcl/operation/exec/LetterboxExec.java Vedi File

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

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

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

import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.string.StringUtil.safeShellArg;
import static org.cobbzilla.util.system.CommandShell.execScript;

@Slf4j
public class LetterboxExec extends SingleOrMultiSourceExecBase<LetterboxOperation> {

public static final String LETTERBOX_TEMPLATE
= "{{ffmpeg}} -i {{{source.path}}} -filter_complex \""
+ "pad="
+ "width={{width}}:"
+ "height={{height}}:"
+ "x=({{width}}-iw*min({{width}}/iw\\,{{height}}/ih))/2:"
+ "y=({{height}}-ih*min({{width}}/iw\\,{{height}}/ih))/2:"
+ "color={{{color}}}"
+ "\" -y {{{output.path}}}";

public static final String DEFAULT_LETTERBOX_COLOR = "black";

@Override public void operate(LetterboxOperation 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 StandardJsEngine js = toolbox.getJs();
final Map<String, Object> ctx = new HashMap<>();
ctx.put("ffmpeg", toolbox.getFfmpeg());
ctx.put("source", source);

if (!op.hasWidth() || !op.hasHeight()) {
die("operate: both width and height must be set");
}
ctx.put("width", op.getWidth(ctx, js).intValue());
ctx.put("height", op.getHeight(ctx, js).intValue());

if (op.hasColor()) {
ctx.put("color", safeShellArg(op.getColor()));
} else {
ctx.put("color", DEFAULT_LETTERBOX_COLOR);
}

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

@Override protected void process(Map<String, Object> ctx,
LetterboxOperation 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, LETTERBOX_TEMPLATE);

log.debug("operate: running script: "+script);
final String scriptOutput = execScript(script);
log.debug("operate: command output: "+scriptOutput);
if (output == subOutput) {
assetManager.addOperationAsset(output);
} else {
assetManager.addOperationAssetSlice(output, subOutput);
}
}

}

+ 9
- 38
src/main/java/jvcl/operation/exec/ScaleExec.java Vedi File

@@ -9,17 +9,14 @@ import jvcl.service.Toolbox;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.javascript.StandardJsEngine;

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

import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.io.FileUtil.*;
import static org.cobbzilla.util.system.CommandShell.execScript;

@Slf4j
public class ScaleExec extends ExecBase<ScaleOperation> {
public class ScaleExec extends SingleOrMultiSourceExecBase<ScaleOperation> {

public static final String SCALE_TEMPLATE
= "{{ffmpeg}} -i {{{source.path}}} -filter_complex \""
@@ -45,42 +42,16 @@ public class ScaleExec extends ExecBase<ScaleOperation> {
op.setProportionalWidthAndHeight(ctx, js, source);
}

if (source.hasList()) {
if (output.hasDest()) {
if (!output.destIsDirectory()) die("operate: dest is not a directory: "+output.getDest());
}
assetManager.addOperationArrayAsset(output);
for (JAsset asset : source.getList()) {
final JAsset subOutput = new JAsset(output);
final File defaultOutfile = assetManager.assetPath(op, asset, formatType);
final File outfile;
if (output.hasDest()) {
outfile = new File(output.destDirectory(), basename(appendToFileNameBeforeExt(asset.getPath(), "_"+op.shortString(ctx, js))));
if (outfile.exists()) {
log.info("operate: dest exists: "+abs(outfile));
return;
}
} else {
outfile = defaultOutfile;
}
subOutput.setPath(abs(outfile));
scale(ctx, asset, output, subOutput, toolbox, assetManager);
}
} else {
final File defaultOutfile = assetManager.assetPath(op, source, formatType);
final File path = resolveOutputPath(output, defaultOutfile);
if (path == null) return;
output.setPath(abs(path));
scale(ctx, source, output, output, toolbox, assetManager);
}
operate(op, toolbox, assetManager, source, output, formatType, ctx);
}

private void scale(Map<String, Object> ctx,
JAsset source,
JAsset output,
JAsset subOutput,
Toolbox toolbox,
AssetManager assetManager) {
@Override protected void process(Map<String, Object> ctx,
ScaleOperation op,
JAsset source,
JAsset output,
JAsset subOutput,
Toolbox toolbox,
AssetManager assetManager) {

ctx.put("source", source);
ctx.put("output", subOutput);


+ 59
- 0
src/main/java/jvcl/operation/exec/SingleOrMultiSourceExecBase.java Vedi File

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

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

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

import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.io.FileUtil.*;

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

protected void operate(OP op, Toolbox toolbox, AssetManager assetManager, JAsset source, JAsset output, JFileExtension formatType, Map<String, Object> ctx) {
if (source.hasList()) {
if (output.hasDest()) {
if (!output.destIsDirectory()) die("operate: dest is not a directory: "+output.getDest());
}
assetManager.addOperationArrayAsset(output);
for (JAsset asset : source.getList()) {
final JAsset subOutput = new JAsset(output);
final File defaultOutfile = assetManager.assetPath(op, asset, formatType);
final File outfile;
if (output.hasDest()) {
outfile = new File(output.destDirectory(), basename(appendToFileNameBeforeExt(asset.getPath(), "_"+op.shortString())));
if (outfile.exists()) {
log.info("operate: dest exists: "+abs(outfile));
return;
}
} else {
outfile = defaultOutfile;
}
subOutput.setPath(abs(outfile));
process(ctx, op, asset, output, subOutput, toolbox, assetManager);
}
} else {
final File defaultOutfile = assetManager.assetPath(op, source, formatType);
final File path = resolveOutputPath(output, defaultOutfile);
if (path == null) return;
output.setPath(abs(path));
process(ctx, op, source, output, output, toolbox, assetManager);
}
}

protected abstract void process(Map<String, Object> ctx,
OP op,
JAsset source,
JAsset output,
JAsset asset,
Toolbox toolbox,
AssetManager assetManager);


}

+ 9
- 39
src/main/java/jvcl/operation/exec/TrimExec.java Vedi File

@@ -9,17 +9,14 @@ import jvcl.service.Toolbox;
import lombok.extern.slf4j.Slf4j;
import org.cobbzilla.util.javascript.StandardJsEngine;

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

import static org.cobbzilla.util.daemon.ZillaRuntime.die;
import static org.cobbzilla.util.io.FileUtil.*;
import static org.cobbzilla.util.system.CommandShell.execScript;

@Slf4j
public class TrimExec extends ExecBase<TrimOperation> {
public class TrimExec extends SingleOrMultiSourceExecBase<TrimOperation> {

public static final String TRIM_TEMPLATE
= "{{ffmpeg}} -i {{{source.path}}} " +
@@ -37,43 +34,16 @@ public class TrimExec extends ExecBase<TrimOperation> {
final Map<String, Object> ctx = new HashMap<>();
ctx.put("ffmpeg", toolbox.getFfmpeg());

if (source.hasList()) {
if (output.hasDest()) {
if (!output.destIsDirectory()) die("operate: dest is not a directory: "+output.getDest());
}
assetManager.addOperationArrayAsset(output);
for (JAsset asset : source.getList()) {
final JAsset subOutput = new JAsset(output);
final File defaultOutfile = assetManager.assetPath(op, asset, formatType);
final File outfile;
if (output.hasDest()) {
outfile = new File(output.destDirectory(), basename(appendToFileNameBeforeExt(asset.getPath(), "_"+op.shortString())));
if (outfile.exists()) {
log.info("operate: dest exists: "+abs(outfile));
return;
}
} else {
outfile = defaultOutfile;
}
subOutput.setPath(abs(outfile));
trim(ctx, op, asset, output, subOutput, toolbox, assetManager);
}
} else {
final File defaultOutfile = assetManager.assetPath(op, source, formatType);
final File path = resolveOutputPath(output, defaultOutfile);
if (path == null) return;
output.setPath(abs(path));
trim(ctx, op, source, output, output, toolbox, assetManager);
}
operate(op, toolbox, assetManager, source, output, formatType, ctx);
}

private void trim(Map<String, Object> ctx,
TrimOperation op,
JAsset source,
JAsset output,
JAsset subOutput,
Toolbox toolbox,
AssetManager assetManager) {
@Override protected void process(Map<String, Object> ctx,
TrimOperation op,
JAsset source,
JAsset output,
JAsset subOutput,
Toolbox toolbox,
AssetManager assetManager) {

ctx.put("source", source);
ctx.put("output", subOutput);


+ 3
- 2
src/main/java/jvcl/service/Toolbox.java Vedi File

@@ -22,6 +22,7 @@ import static java.math.RoundingMode.HALF_EVEN;
import static org.cobbzilla.util.daemon.ZillaRuntime.*;
import static org.cobbzilla.util.io.FileUtil.*;
import static org.cobbzilla.util.json.JsonUtil.*;
import static org.cobbzilla.util.string.StringUtil.safeShellArg;
import static org.cobbzilla.util.system.CommandShell.execScript;

@Slf4j
@@ -84,10 +85,10 @@ public class Toolbox {
}

@Getter(lazy=true) private final String ffmpeg = initFfmpeg();
private String initFfmpeg() { return loadPath("ffmpeg"); }
private String initFfmpeg() { return safeShellArg(loadPath("ffmpeg")); }

@Getter(lazy=true) private final String mediainfo = initMediainfo();
private String initMediainfo() { return loadPath("mediainfo"); }
private String initMediainfo() { return safeShellArg(loadPath("mediainfo")); }

private static String loadPath(String p) {
try {


+ 2
- 1
src/test/java/javicle/test/BasicTest.java Vedi File

@@ -18,11 +18,12 @@ import static org.junit.Assert.fail;
@Slf4j
public class BasicTest {

@Test public void testSplitConcatTrimScale () {
@Test public void testSplitConcatTrimScaleLetterbox () {
runSpec("tests/test_split.jvcl");
runSpec("tests/test_concat.jvcl");
runSpec("tests/test_trim.jvcl");
runSpec("tests/test_scale.jvcl");
runSpec("tests/test_letterbox.jvcl");
}

@Test public void testOverlay() { runSpec("tests/test_overlay.jvcl"); }


+ 23
- 0
src/test/resources/tests/test_letterbox.jvcl Vedi File

@@ -0,0 +1,23 @@
{
"assets": [
{ "name": "vid1_splits", "path": "src/test/resources/outputs/vid1_splits_*.mp4" }
],
"operations": [
{
"operation": "letterbox", // name of the operation
"creates": "boxed_wide",
"source": "vid1_splits[2]", // box this source asset
"width": "source.width * 2", // width of output asset. if omitted and height is present, width will be proportional
"height": "source.height", // height of output asset. if omitted and width is present, height will be proportional
"color": "AliceBlue"
},
{
"operation": "letterbox", // name of the operation
"creates": "boxed_tall",
"source": "vid1_splits[2]", // box this source assets
"width": "source.width", // width of output asset. if omitted and height is present, width will be proportional
"height": "source.height * 2", // height of output asset. if omitted and width is present, height will be proportional
"color": "DarkCyan"
}
]
}

+ 3
- 3
src/test/resources/tests/test_scale.jvcl Vedi File

@@ -4,16 +4,16 @@
],
"operations": [
{
"operation": "scale", // name of the operation
"operation": "scale", // name of the operation
"creates": "scaled_test1",
"source": "vid1_splits[3]", // trim these source assets
"source": "vid1_splits[3]", // scale this source asset
"width": "1024", // width of scaled asset. if omitted and height is present, width will be proportional
"height": "768" // height of scaled asset. if omitted and width is present, height will be proportional
},
{
"operation": "scale", // name of the operation
"creates": "scaled_small", // asset it creates
"source": "vid1_splits[3]", // source asset
"source": "vid1_splits[3]", // scale this source asset
"factor": "0.5" // scale factor. if factor is set, width and height are ignored.
}
]


Caricamento…
Annulla
Salva