From f2ab6a385e7667d6b301995236abab6061334e35 Mon Sep 17 00:00:00 2001 From: Jonathan Cobb Date: Wed, 16 Dec 2020 19:26:40 -0500 Subject: [PATCH] add more command line utilities --- bin/jconcat | 66 +++++++++++++++++++ bin/jletterbox | 46 ++++++++++++++ bin/jscale | 67 ++++++++++++++++++++ bin/jsplit | 49 ++++++++++++++ bin/jtrim | 5 +- bin/jvcl_common | 20 ++++++ src/main/java/jvcl/main/JvclOptions.java | 5 +- src/main/java/jvcl/service/AssetManager.java | 4 +- src/test/resources/tests/test_concat.jvcl | 13 ++-- 9 files changed, 264 insertions(+), 11 deletions(-) create mode 100755 bin/jconcat create mode 100755 bin/jletterbox create mode 100755 bin/jscale create mode 100755 bin/jsplit mode change 100644 => 100755 bin/jvcl_common diff --git a/bin/jconcat b/bin/jconcat new file mode 100755 index 0000000..f7a4815 --- /dev/null +++ b/bin/jconcat @@ -0,0 +1,66 @@ +#!/bin/bash +# +# Concatenate media files +# +# Usage: +# +# jconcat out-file in-file-1 [in-file-2 ...] +# +# out-file : output file +# in-file-N : input files +# +# BEWARE SHELL WILDCARDS! +# Shell wildcards will match files in non-deterministic order, meaning that +# your combined file will be assembled out of order. +# +# If you are combining files that were split with `jvcl` (or `jsplit`), then +# sorting them by time (oldest first) is sufficient. +# +# For example, do this: +# +# jconcat combined-file.mp4 $(ls -1tr path/to/parts*.mp4) +# +# NOT THIS: +# +# jconcat combined-file.mp4 path/to/parts*.mp4 # args not ordered! +# +SCRIPT="${0}" +SCRIPT_DIR="$(cd "$(dirname "${SCRIPT}")" && pwd)" +. "${SCRIPT_DIR}"/jvcl_common + +OUT_FILE="${1?no out-file provided}" +shift + +IN_FILE="${1?no in-file(s) provided}" +echo " +{ + \"assets\": [ +$( +F_INDEX=0 +for path in "$@" ; do + if [[ ${F_INDEX} -gt 0 ]] ; then + echo "," + fi + F_INDEX=$((F_INDEX + 1)) + echo -n " { \"name\": \"input_${F_INDEX}\", \"path\": \"${path}\" }" + shift +done) + ], + \"operations\": [ + { + \"operation\": \"concat\", + \"creates\": { + \"name\": \"combined\", + \"dest\": \"${OUT_FILE}\" + }, + \"sources\": [$(END=$# +for ((i=1;i<=END;i++)); do + if [[ ${i} -gt 1 ]] ; then echo -n "," ; fi + echo -n " + \"input_${i}\"" +done) + ] + } + ] +} +" | "${SCRIPT_DIR}"/jvcl -z ${JVCL_OPTIONS} diff --git a/bin/jletterbox b/bin/jletterbox new file mode 100755 index 0000000..22fcb8a --- /dev/null +++ b/bin/jletterbox @@ -0,0 +1,46 @@ +#!/bin/bash +#!/bin/bash +# +# Letterbox a video file +# +# Usage: +# +# jletterbox in-file out-file width height [color] +# +# in-file : file to trim +# out-file : write scaled file here +# width : output width +# height : output height +# color : padding color, can be a hex value (0xff0000 is red), or a color +# name from https://ffmpeg.org/ffmpeg-utils.html#color-syntax +# +SCRIPT="${0}" +SCRIPT_DIR="$(cd "$(dirname "${SCRIPT}")" && pwd)" +. "${SCRIPT_DIR}"/jvcl_common + +IN_FILE="${1?no in-file provided}" +OUT_FILE="${2?no out-file provided}" +WIDTH="${3}" +HEIGHT="${4}" +COLOR="${5:-black}" + +echo " +{ + \"assets\": [ + { \"name\": \"input\", \"path\": \"${IN_FILE}\" } + ], + \"operations\": [ + { + \"operation\": \"letterbox\", + \"creates\": { + \"name\": \"boxed\", + \"dest\": \"${OUT_FILE}\" + }, + \"source\": \"input\", + \"width\": \"${WIDTH}\", + \"height\": \"${HEIGHT}\", + \"color\": \"${COLOR}\" + } + ] +} +" | "${SCRIPT_DIR}"/jvcl ${JVCL_OPTIONS} diff --git a/bin/jscale b/bin/jscale new file mode 100755 index 0000000..42ba6cb --- /dev/null +++ b/bin/jscale @@ -0,0 +1,67 @@ +#!/bin/bash +# +# Scale a media file +# +# Usage: +# +# jscale in-file out-file factor +# or +# jscale in-file out-file width height +# +# in-file : file to trim +# out-file : write scaled file here +# factor : scale factor +# width : output width +# height : output height +# +SCRIPT="${0}" +SCRIPT_DIR="$(cd "$(dirname "${SCRIPT}")" && pwd)" +. "${SCRIPT_DIR}"/jvcl_common + +IN_FILE="${1?no in-file provided}" +OUT_FILE="${2?no out-file provided}" +WIDTH="${3}" +HEIGHT="${4}" +FACTOR="" + +if [[ -z "${HEIGHT}" ]] ; then + FACTOR=${WIDTH} + echo " +{ + \"assets\": [ + { \"name\": \"input\", \"path\": \"${IN_FILE}\" } + ], + \"operations\": [ + { + \"operation\": \"scale\", + \"creates\": { + \"name\": \"scaled\", + \"dest\": \"${OUT_FILE}\" + }, + \"source\": \"input\", + \"factor\": \"${FACTOR}\" + } + ] +} +" | "${SCRIPT_DIR}"/jvcl ${JVCL_OPTIONS} +else + echo " +{ + \"assets\": [ + { \"name\": \"input\", \"path\": \"${IN_FILE}\" } + ], + \"operations\": [ + { + \"operation\": \"scale\", + \"creates\": { + \"name\": \"scaled\", + \"dest\": \"${OUT_FILE}\" + }, + \"source\": \"input\", + \"width\": \"${WIDTH}\", + \"height\": \"${HEIGHT}\" + } + ] +} +" | "${SCRIPT_DIR}"/jvcl ${JVCL_OPTIONS} +fi \ No newline at end of file diff --git a/bin/jsplit b/bin/jsplit new file mode 100755 index 0000000..f7bfc64 --- /dev/null +++ b/bin/jsplit @@ -0,0 +1,49 @@ +#!/bin/bash +# +# Split a media file into multiple files of equal time length +# +# Usage: +# +# jsplit in-file out-dir interval [start] [end] +# +# in-file : file to trim +# out-dir : write split files to this directory (will be created if it does not exist) +# interval : time duration of output files, in seconds +# start : when to start splitting the in-file. default is 0 (start) +# end : when to stop splitting the in-file. default is to continue until end of file is reached +# +SCRIPT="${0}" +SCRIPT_DIR="$(cd "$(dirname "${SCRIPT}")" && pwd)" +. "${SCRIPT_DIR}"/jvcl_common + +IN_FILE="${1?no in-file provided}" +OUT_DIR="${2?no out-dir provided}" +if [[ -e "${OUT_DIR}" && ! -d "${OUT_DIR}" ]] ; then + die "Not a directory: ${OUT_DIR}" +fi +mkdir -p "${OUT_DIR}" || die "Error creating ${OUT_DIR}" + +INTERVAL="${3?no interval provided}" +T_START="${4}" +T_END="${5}" + +echo " +{ + \"assets\": [ + { \"name\": \"input\", \"path\": \"${IN_FILE}\" } + ], + \"operations\": [ + { + \"operation\": \"split\", + \"creates\": { + \"name\": \"splits\", + \"dest\": \"${OUT_DIR}/\" + }, + \"source\": \"input\", + \"interval\": \"${INTERVAL}\"$(if [[ -n "${T_START}" ]] ; then echo ", + \"start\": \"${T_START}\"" ; fi)$(if [[ -n "${T_END}" ]] ; then echo ", + \"end\": \"${T_END}\"," ; fi) + } + ] +} +" | "${SCRIPT_DIR}"/jvcl ${JVCL_OPTIONS} diff --git a/bin/jtrim b/bin/jtrim index d7dd6bd..1ac969e 100755 --- a/bin/jtrim +++ b/bin/jtrim @@ -19,6 +19,7 @@ IN_FILE="${1?no in-file provided}" OUT_FILE="${2?no out-file provided}" T_START="${3}" T_END="${4}" + echo " { \"assets\": [ @@ -31,10 +32,10 @@ echo " \"name\": \"trimmed\", \"dest\": \"${OUT_FILE}\" }, - \"trim\": \"input\", + \"source\": \"input\", \"start\": \"${T_START}\", \"end\": \"${T_END}\" } ] } -" | "${SCRIPT_DIR}"/jvcl +" | "${SCRIPT_DIR}"/jvcl ${JVCL_OPTIONS} diff --git a/bin/jvcl_common b/bin/jvcl_common old mode 100644 new mode 100755 index 7db7fe4..4b4f27f --- a/bin/jvcl_common +++ b/bin/jvcl_common @@ -19,6 +19,15 @@ function handle_help_request() { break fi done <"${1}" + echo "# Environment Variables +# +# JVCL_SCRATCH_DIR : Use this as the scratch directory +# Default is to create a new temp directory +# +# JVCL_NO_EXEC : If set to anything, print the commands that would +# have run but do not execute anything +# +" exit 1 fi } @@ -33,4 +42,15 @@ if [[ -z "${JVCL_JAR}" ]] ; then fi fi +SCRATCH_DIR="" +if [[ -n "${JVCL_SCRATCH_DIR}" ]] ; then + SCRATCH_DIR="--temp-dir ${JVCL_SCRATCH_DIR}" +fi + +NO_EXEC="" +if [[ -n "${JVCL_NO_EXEC}" ]] ; then + NO_EXEC="--no-exec" +fi +JVCL_OPTIONS="${SCRATCH_DIR} ${NO_EXEC}" + handle_help_request "${0}" "${1}" diff --git a/src/main/java/jvcl/main/JvclOptions.java b/src/main/java/jvcl/main/JvclOptions.java index 734903a..3af5955 100644 --- a/src/main/java/jvcl/main/JvclOptions.java +++ b/src/main/java/jvcl/main/JvclOptions.java @@ -12,8 +12,7 @@ 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.daemon.ZillaRuntime.*; import static org.cobbzilla.util.io.FileUtil.abs; import static org.cobbzilla.util.io.FileUtil.toStringOrDie; import static org.cobbzilla.util.json.JsonUtil.json; @@ -37,7 +36,7 @@ public class JvclOptions extends BaseMainOptions { try { return json(json, JSpec.class, JSON_MAPPER); } catch (Exception e) { - return die("getSpec: invalid spec: "+specFile); + return die("getSpec: invalid spec: "+specFile+": "+shortError(e)); } } diff --git a/src/main/java/jvcl/service/AssetManager.java b/src/main/java/jvcl/service/AssetManager.java index b55380a..7a6bcdb 100644 --- a/src/main/java/jvcl/service/AssetManager.java +++ b/src/main/java/jvcl/service/AssetManager.java @@ -120,7 +120,7 @@ public class AssetManager { final int fromIndex = parseIndexExpression(from, list, JIndexType.from); final int toIndex = parseIndexExpression(to, list, JIndexType.to); if (toIndex < fromIndex) return die("parseIndexExpression("+indexExpr+"): 'to' index ("+toIndex+") < 'from' index ("+fromIndex+")"); - final int len = toIndex - fromIndex; + final int len = 1 + (toIndex - fromIndex); final JAsset[] subList = new JAsset[len]; System.arraycopy(list, fromIndex, subList, 0, len); return new JAsset(asset).setList(subList); @@ -142,7 +142,7 @@ public class AssetManager { if (empty(indexExpr)) { switch (type) { case from: return 0; - case to: return list.length; + case to: return list.length-1; case single: return die("parseIndexExpression(): no expression provided!"); default: return die("parseIndexExpression(): invalid type: "+type); } diff --git a/src/test/resources/tests/test_concat.jvcl b/src/test/resources/tests/test_concat.jvcl index 550de5b..8b10e87 100644 --- a/src/test/resources/tests/test_concat.jvcl +++ b/src/test/resources/tests/test_concat.jvcl @@ -6,11 +6,16 @@ "operations": [ { "operation": "concat", // name of the operation - "sources": ["vid1_splits[1..]"], // concatentate these sources (everything but the first video in vid1_splits) + "sources": ["vid1_splits"], // concatentate these sources -- all split files "creates": { - "name": "combined_vid", // name of the output asset - "dest": "src/test/resources/outputs/combined.mp4" // asset will be written to this file - }, + "name": "combined_vid1", // name of the output asset + "dest": "src/test/resources/outputs/combined1.mp4" // asset will be written to this file + } + }, + { + "operation": "concat", // name of the operation + "sources": ["vid1_splits[1..2]"], // concatentate these sources -- the 2nd and 3rd files only + "creates": "combined_vid2", // name of the output asset, will be written to scratch directory } ] }