Browse Source

add scale operation

master
Jonathan Cobb 3 years ago
parent
commit
77b48cc81d
11 changed files with 243 additions and 41 deletions
  1. +27
    -7
      README.md
  2. +8
    -1
      src/main/java/jvcl/model/JAsset.java
  3. +48
    -0
      src/main/java/jvcl/operation/HasWidthAndHeight.java
  4. +1
    -6
      src/main/java/jvcl/operation/OverlayOperation.java
  5. +27
    -0
      src/main/java/jvcl/operation/ScaleOperation.java
  6. +1
    -19
      src/main/java/jvcl/operation/exec/OverlayExec.java
  7. +99
    -0
      src/main/java/jvcl/operation/exec/ScaleExec.java
  8. +2
    -1
      src/main/java/jvcl/operation/exec/SplitExec.java
  9. +8
    -6
      src/main/java/jvcl/operation/exec/TrimExec.java
  10. +2
    -1
      src/test/java/javicle/test/BasicTest.java
  11. +20
    -0
      src/test/resources/tests/test_scale.jvcl

+ 27
- 7
README.md View File

@@ -127,8 +127,11 @@ The above would set the `start` value to ten seconds before the end of `someAsse
### Supported Operations
Today, JVCL supports these operations:

### scale
Scale a video asset from one size to another

### split
Split an audio/video asset into multiple assets
Split an audio/video asset into multiple assets of equal time lengths

### concat
Concatenate audio/video assets together into one asset
@@ -136,9 +139,6 @@ Concatenate audio/video assets together into one asset
### trim
Trim audio/video; crop a section of an asset, becomes a new asset

### scale
Scale a video asset from one size to another

### overlay
Overlay one asset onto another

@@ -148,9 +148,6 @@ For transforming still images into video via a fade-pan (aka Ken Burns) effect
### letterbox
Transform a video in one size to another size using black letterboxes on the sides or top/bottom. Handy for embedding mobile videos into other screen formats

### split-silence
Split an audio file according to silence

# Complex Example
Here is a complex example using multiple assets and operations.

@@ -184,6 +181,19 @@ Here is a complex example using multiple assets and operations.
}
],
"operations": [
{
"operation": "scale", // name of the operation
"creates": "vid2_scaled", // asset it creates
"source": "vid2", // 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": "vid2_big", // asset it creates
"source": "vid2", // source asset
"factor": "2.2" // scale factor. if factor is set, width and height are ignored.
},
{
"operation": "split", // name of the operation
"creates": "vid1_split_%", // assets it creates, the '%' will be replaced with a counter
@@ -205,6 +215,16 @@ Here is a complex example using multiple assets and operations.
"creates": "combined_vid", // the asset it creates, can be referenced later
"source": ["vid1", "vid2"] // operation-specific: this says, concatenate these named assets
},
{
"operation": "trim", // name of the operation
"creates": { // create multiple files, will be prefixed with `name`, store them in `dest`
"name": "vid1_trims",
"dest": "src/test/resources/outputs/trims/"
},
"source": "vid1_split", // trim these source assets
"start": "1", // cropped region starts here, default is zero
"end": "6" // cropped region ends here, default is end of video
},
{
"operation": "overlay", // name of the operation
"creates": "overlay1", // asset it creates


+ 8
- 1
src/main/java/jvcl/model/JAsset.java View File

@@ -186,13 +186,20 @@ public class JAsset implements JsObjectView {
final File sourcePath;
if (hasDest()) {
if (destIsDirectory()) {
sourcePath = new File(getDest(), basename(getName()));
sourcePath = new File(getDest(), basename(getPath()));
} else {
sourcePath = new File(getDest());
}
} else {
sourcePath = assetManager.sourcePath(getName());
}

if (sourcePath.exists()) {
setOriginalPath(path);
setPath(abs(sourcePath));
return this;
}

if (path.startsWith(PREFIX_CLASSPATH)) {
// it's a classpath resource
final String resource = path.substring(PREFIX_CLASSPATH.length());


+ 48
- 0
src/main/java/jvcl/operation/HasWidthAndHeight.java View File

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

import jvcl.model.JAsset;
import org.cobbzilla.util.javascript.JsEngine;
import org.cobbzilla.util.javascript.StandardJsEngine;

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

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

public interface HasWidthAndHeight {

String getWidth ();
default boolean hasWidth () { return !empty(getWidth()); }

String getHeight ();
default boolean hasHeight () { return !empty(getHeight()); }

default BigDecimal getWidth(Map<String, Object> ctx, JsEngine js) { return evalBig(getWidth(), ctx, js); }
default BigDecimal getHeight(Map<String, Object> ctx, JsEngine js) { return evalBig(getHeight(), ctx, js); }

default void setProportionalWidthAndHeight(Map<String, Object> ctx,
StandardJsEngine js,
JAsset asset) {
if (hasWidth()) {
final BigDecimal width = getWidth(ctx, js);
ctx.put("width", width.intValue());
if (!hasHeight()) {
final BigDecimal aspectRatio = asset.aspectRatio();
final int height = divideBig(width, aspectRatio).intValue();
ctx.put("height", height);
}
}
if (hasHeight()) {
final BigDecimal height = getHeight(ctx, js);
ctx.put("height", height.intValue());
if (!hasWidth()) {
final BigDecimal aspectRatio = asset.aspectRatio();
final int width = height.multiply(aspectRatio).intValue();
ctx.put("width", width);
}
}
}

}

+ 1
- 6
src/main/java/jvcl/operation/OverlayOperation.java View File

@@ -28,7 +28,7 @@ public class OverlayOperation extends JSingleSourceOperation {
return evalBig(end, ctx, js);
}

public static class OverlayConfig {
public static class OverlayConfig implements HasWidthAndHeight {
@Getter @Setter private String source;

@Getter @Setter private String start;
@@ -43,12 +43,7 @@ public class OverlayOperation extends JSingleSourceOperation {
}

@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 x;
public boolean hasX () { return !empty(x); }


+ 27
- 0
src/main/java/jvcl/operation/ScaleOperation.java View File

@@ -0,0 +1,27 @@
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 jvcl.service.Toolbox.evalBig;
import static org.cobbzilla.util.daemon.ZillaRuntime.empty;

public class ScaleOperation extends JSingleSourceOperation implements HasWidthAndHeight {

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

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

public String shortString(Map<String, Object> ctx, JsEngine js) {
return "scaled_"+(hasFactor() ? getFactor(ctx, js)+"x" : getWidth(ctx, js)+"x"+getHeight(ctx, js));
}

}

+ 1
- 19
src/main/java/jvcl/operation/exec/OverlayExec.java View File

@@ -14,7 +14,6 @@ import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

import static jvcl.service.Toolbox.divideBig;
import static org.cobbzilla.util.io.FileUtil.abs;
import static org.cobbzilla.util.system.CommandShell.execScript;

@@ -55,24 +54,7 @@ public class OverlayExec extends ExecBase<OverlayOperation> {
ctx.put("overlayFilterConfig", overlayFilter);
ctx.put("output", output);

if (overlay.hasWidth()) {
final BigDecimal width = overlay.getWidth(ctx, js);
ctx.put("width", width.intValue());
if (!overlay.hasHeight()) {
final BigDecimal aspectRatio = overlaySource.aspectRatio();
final int height = divideBig(width, aspectRatio).intValue();
ctx.put("height", height);
}
}
if (overlay.hasHeight()) {
final BigDecimal height = overlay.getHeight(ctx, js);
ctx.put("height", height.intValue());
if (!overlay.hasWidth()) {
final BigDecimal aspectRatio = overlaySource.aspectRatio();
final int width = height.multiply(aspectRatio).intValue();
ctx.put("width", width);
}
}
overlay.setProportionalWidthAndHeight(ctx, js, overlaySource);

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



+ 99
- 0
src/main/java/jvcl/operation/exec/ScaleExec.java View File

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

import jvcl.model.JAsset;
import jvcl.model.JFileExtension;
import jvcl.model.operation.JSingleOperationContext;
import jvcl.operation.ScaleOperation;
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.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 static final String SCALE_TEMPLATE
= "{{ffmpeg}} -i {{{source.path}}} -filter_complex \""
+ "scale={{width}}x{{height}}" +
"\" -y {{{output.path}}}";

@Override public void operate(ScaleOperation 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());

if (op.hasFactor()) {
final BigDecimal factor = op.getFactor(ctx, js);
ctx.put("width", factor.multiply(source.getWidth()).intValue());
ctx.put("height", factor.multiply(source.getHeight()).intValue());
} else {
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);
}
}

private void scale(Map<String, Object> ctx,
JAsset source,
JAsset output,
JAsset subOutput,
Toolbox toolbox,
AssetManager assetManager) {

ctx.put("source", source);
ctx.put("output", subOutput);
final String script = renderScript(toolbox, ctx, SCALE_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);
}
}

}

+ 2
- 1
src/main/java/jvcl/operation/exec/SplitExec.java View File

@@ -37,6 +37,7 @@ public class SplitExec extends ExecBase<SplitOperation> {
ctx.put("ffmpeg", toolbox.getFfmpeg());
ctx.put("source", source);

assetManager.addOperationArrayAsset(output);
final BigDecimal incr = op.getIntervalIncr(ctx, js);
final BigDecimal endTime = op.getEndTime(source, ctx, js);
for (BigDecimal i = op.getStartTime(ctx, js);
@@ -61,7 +62,7 @@ public class SplitExec extends ExecBase<SplitOperation> {

if (outfile.exists()) {
log.info("operate: outfile exists, not re-creating: "+abs(outfile));
return;
continue;
} else {
mkdirOrDie(outfile.getParentFile());
}


+ 8
- 6
src/main/java/jvcl/operation/exec/TrimExec.java View File

@@ -34,6 +34,9 @@ public class TrimExec extends ExecBase<TrimOperation> {
final JAsset output = opCtx.output;
final JFileExtension formatType = opCtx.formatType;

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());
@@ -53,30 +56,29 @@ public class TrimExec extends ExecBase<TrimOperation> {
outfile = defaultOutfile;
}
subOutput.setPath(abs(outfile));
trim(op, asset, output, subOutput, toolbox, assetManager);
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(op, source, output, output, toolbox, assetManager);
trim(ctx, op, source, output, output, toolbox, assetManager);
}
}

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

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

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


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

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

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

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


+ 20
- 0
src/test/resources/tests/test_scale.jvcl View File

@@ -0,0 +1,20 @@
{
"assets": [
{ "name": "vid1_splits", "path": "src/test/resources/outputs/vid1_splits_*.mp4" }
],
"operations": [
{
"operation": "scale", // name of the operation
"creates": "scaled_test1",
"source": "vid1_splits[3]", // trim these source assets
"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
"factor": "0.5" // scale factor. if factor is set, width and height are ignored.
}
]
}

Loading…
Cancel
Save