@@ -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} |
@@ -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} |
@@ -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 |
@@ -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} |
@@ -19,6 +19,7 @@ IN_FILE="${1?no in-file provided}" | |||||
OUT_FILE="${2?no out-file provided}" | OUT_FILE="${2?no out-file provided}" | ||||
T_START="${3}" | T_START="${3}" | ||||
T_END="${4}" | T_END="${4}" | ||||
echo " | echo " | ||||
{ | { | ||||
\"assets\": [ | \"assets\": [ | ||||
@@ -31,10 +32,10 @@ echo " | |||||
\"name\": \"trimmed\", | \"name\": \"trimmed\", | ||||
\"dest\": \"${OUT_FILE}\" | \"dest\": \"${OUT_FILE}\" | ||||
}, | }, | ||||
\"trim\": \"input\", | |||||
\"source\": \"input\", | |||||
\"start\": \"${T_START}\", | \"start\": \"${T_START}\", | ||||
\"end\": \"${T_END}\" | \"end\": \"${T_END}\" | ||||
} | } | ||||
] | ] | ||||
} | } | ||||
" | "${SCRIPT_DIR}"/jvcl | |||||
" | "${SCRIPT_DIR}"/jvcl ${JVCL_OPTIONS} |
@@ -19,6 +19,15 @@ function handle_help_request() { | |||||
break | break | ||||
fi | fi | ||||
done <"${1}" | 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 | exit 1 | ||||
fi | fi | ||||
} | } | ||||
@@ -33,4 +42,15 @@ if [[ -z "${JVCL_JAR}" ]] ; then | |||||
fi | fi | ||||
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}" | handle_help_request "${0}" "${1}" |
@@ -12,8 +12,7 @@ import org.kohsuke.args4j.Option; | |||||
import java.io.File; | import java.io.File; | ||||
import static jvcl.service.Toolbox.JSON_MAPPER; | 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.abs; | ||||
import static org.cobbzilla.util.io.FileUtil.toStringOrDie; | import static org.cobbzilla.util.io.FileUtil.toStringOrDie; | ||||
import static org.cobbzilla.util.json.JsonUtil.json; | import static org.cobbzilla.util.json.JsonUtil.json; | ||||
@@ -37,7 +36,7 @@ public class JvclOptions extends BaseMainOptions { | |||||
try { | try { | ||||
return json(json, JSpec.class, JSON_MAPPER); | return json(json, JSpec.class, JSON_MAPPER); | ||||
} catch (Exception e) { | } catch (Exception e) { | ||||
return die("getSpec: invalid spec: "+specFile); | |||||
return die("getSpec: invalid spec: "+specFile+": "+shortError(e)); | |||||
} | } | ||||
} | } | ||||
@@ -120,7 +120,7 @@ public class AssetManager { | |||||
final int fromIndex = parseIndexExpression(from, list, JIndexType.from); | final int fromIndex = parseIndexExpression(from, list, JIndexType.from); | ||||
final int toIndex = parseIndexExpression(to, list, JIndexType.to); | final int toIndex = parseIndexExpression(to, list, JIndexType.to); | ||||
if (toIndex < fromIndex) return die("parseIndexExpression("+indexExpr+"): 'to' index ("+toIndex+") < 'from' index ("+fromIndex+")"); | 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]; | final JAsset[] subList = new JAsset[len]; | ||||
System.arraycopy(list, fromIndex, subList, 0, len); | System.arraycopy(list, fromIndex, subList, 0, len); | ||||
return new JAsset(asset).setList(subList); | return new JAsset(asset).setList(subList); | ||||
@@ -142,7 +142,7 @@ public class AssetManager { | |||||
if (empty(indexExpr)) { | if (empty(indexExpr)) { | ||||
switch (type) { | switch (type) { | ||||
case from: return 0; | case from: return 0; | ||||
case to: return list.length; | |||||
case to: return list.length-1; | |||||
case single: return die("parseIndexExpression(): no expression provided!"); | case single: return die("parseIndexExpression(): no expression provided!"); | ||||
default: return die("parseIndexExpression(): invalid type: "+type); | default: return die("parseIndexExpression(): invalid type: "+type); | ||||
} | } | ||||
@@ -6,11 +6,16 @@ | |||||
"operations": [ | "operations": [ | ||||
{ | { | ||||
"operation": "concat", // name of the operation | "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": { | "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 | |||||
} | } | ||||
] | ] | ||||
} | } |