Skip to content

Instantly share code, notes, and snippets.

@interjection
Last active December 10, 2020 01:22
Show Gist options
  • Save interjection/4b83c0790ce82982caec to your computer and use it in GitHub Desktop.
Save interjection/4b83c0790ce82982caec to your computer and use it in GitHub Desktop.
#!/bin/bash
# webm.bash
# Requires: ffmpeg, mplayer, gawk
set -o errexit
shopt -s nocaseglob
shopt -s nocasematch
# Default temporary font dir, rescursively deleted at the end
FONTDIR="$HOME/.fonts/tmp/"
# Log file ffmpeg uses by default
LOG="ffmpeg2pass-0.log"
# Default codec
CODEC="libvpx"
# Default width of output
W="-1"
# Default height of output
H="-1"
# Default CRF
CRF="4"
# Default output limit in megabytes
MBLIMIT="3"
# Default number of threads that ffmpeg will use
THREADS="$(nproc)"
# Default lag in frames
LAG="16"
# Default slices
SLICES="8"
# Default X display for screen recorder
XDISP=":0"
# Default screen recorder FPS
FPS="60"
# Default follow distance for screen recorder when using -follow_mouse
DIS="75"
# Slave input config file, delete ~/.mplayer/webmcrop if you make changes
MPCONFIG="$(cat <<EOF
z pausing_keep get_time_pos
w pausing_keep change_rectangle 3 -10
a pausing_keep change_rectangle 2 -10
s pausing_keep change_rectangle 3 10
d pausing_keep change_rectangle 2 10
KP8 pausing_keep change_rectangle 1 -10
KP4 pausing_keep change_rectangle 0 -10
KP2 pausing_keep change_rectangle 1 10
KP6 pausing_keep change_rectangle 0 10
EOF
)"
Usage()
{
cat <<EOF
webm.bash
It just werks.
Usage:
-i Input file, e.g. inputfilename.mkv (required)
-o Output file, e.g. output.webm
Defaults to inputfilename_HH:MM:SS.MS_HH:MM:SS.MS.webm if you
specify a starting time and ending time, otherwise defaults to
inputfilename.webm.
-s Starting time in HH:MM:SS.MS, e.g. 02:23:59.00
Defaults to starting time of input file.
-e Ending time in HH:MM:SS.MS, e.g. 02:24:01.30
Defaults to ending time of input file.
-q CRF, e.g. 10
Enables constant quality mode and sets quality: values 0-63
with lower values being lower quality.
Defaults to $CRF
-b Output bitrate in kbps, e.g. 1000
-y Segmented MKV mode
When using a video player that plays back ordered chapters or
segmented MKVs (mpv) to manually get the starting and ending
times, use this mode to correct times for ffmpeg automatically.
-c Crop output using mplayer (interactive)
Launches mplayer to crop with your w, a, s and d keys to move
the rectangle and your keypad arrow keys to adjust the size of
the rectangle filter.
-u Burn subtitles into output (interactive)
Prompts you to select the stream containing the subtitles you
wish to hardsub into the output video.
-v Cut output video times using mplayer (interactive)
Launches mplayer so you can select your start and end times
using the z key. Only the first and the last inputs will be
taken into account.
-w Output width, e.g. 640
When overriding either the default width or height, the output
will be scaled to the correct aspect ratio, but not when you
override both.
Defaults to $W
-h Output height, e.g. 480
Defaults to $H
-m Filesize limit in megabytes, e.g. 4
Defaults to $MBLIMIT
-t Threads, e.g. 8
Number of threads to use for encoding, try setting this option
to 1 if the file gets cut off (i.e. slightly over the size
limit.)
Defaults to $THREADS
-l Lag in frames, e.g. 25
Number of frames for ffmpeg to look ahead for when encoding.
Defaults to $LAG
-x Slices, e.g. 4
Directs the encoder to split the coefficient encoding across
multiple data partitions that can be encoded independently.
A value of 1 or 2 is recommended for small images, and 4 or
8 is recommended for high definition images.
Defaults to $SLICES
-f Additional ffmpeg filters, e.g. "crop=1140:574:450:100,"
-p Additional ffmpeg parameters, e.g. "-loglevel quiet"
-r Record screen with ffmpeg (interactive)
-d Screen recorder window cropping (interactive)
-z Follow the mouse when recording the screen
Requires width and/or height parameter to be passed.
-9 Use the vp9 codec instead of vp8
EOF
}
while getopts "cuvyrdz9i:o:s:e:b:w:h:q:m:t:l:x:f:p:" OPTION; do
case "$OPTION" in
c) CROP=true;;
u) SUB=true;;
v) CUT=true;;
y) SEGMENTED=true;;
r) XRECORD=true;;
d) XCROP=true;;
z) XMOUSE=true;;
9) CODEC="libvpx-vp9";;
i) IF="$OPTARG";;
o) OF="$OPTARG";;
s) ST="$OPTARG";;
e) ET="$OPTARG";;
b) BR="$OPTARG";;
w) W="$OPTARG";;
h) H="$OPTARG";;
q) CRF="$OPTARG"
CQ=true;;
m) MBLIMIT="$OPTARG";;
t) THREADS="$OPTARG";;
l) LAG="$OPTARG";;
x) SLICES="$OPTARG";;
f) ADDFILTERS="$OPTARG";;
p) ADDPARAMS="$OPTARG";;
?) Usage
exit 1;;
:) Usage
exit 1;;
esac
done
ScreenRecorder()
{
IF="$(date +'%Y-%m-%d %k:%M:%S').mkv"
# Current display resolution
RES="$(xrandr -q | awk -F 'current' -F ',' 'NR==1 {gsub("( |current)",""); print $2}')"
if [[ "$XMOUSE" = true ]] ; then
if [[ "$W" -eq "-1" && "$H" -eq "-1" ]] ; then
echo "Specify a width and/or height" && exit 1
exit 1
elif [[ "$W" -eq "-1" ]] ; then
W="$H"
elif [[ "$H" -eq "-1" ]] ; then
H="$W"
fi
RES="${W}x${H}"
ffmpeg -f x11grab -follow_mouse "$DIS" -s "$RES" -i "$XDISP" -r "$FPS" \
-threads "$THREADS" -crf 0 -preset ultrafast -c:v libx264 "$IF"
elif [[ $XCROP = true ]] ; then
echo "Click on the window you wish to record..."
XINFO="$(xwininfo)"
WINX="$(echo "$XINFO" | grep "Absolute upper-left X" | awk '{print $4}')"
WINY="$(echo "$XINFO" | grep "Absolute upper-left Y" | awk '{print $4}')"
WINW="$(echo "$XINFO" | grep Width | awk '{print $2}')"
WINH="$(echo "$XINFO" | grep Height | awk '{print $2}')"
XCROPS="crop=${WINW}:${WINH}:${WINX}:${WINY}"
ffmpeg -f x11grab -s "$RES" -i "$XDISP" -r "$FPS" -vf "$XCROPS" \
-threads "$THREADS" -crf 0 -preset ultrafast -c:v libx264 "$IF"
else
ffmpeg -f x11grab -s "$RES" -i "$XDISP" -r "$FPS" -threads "$THREADS" \
-threads "$THREADS" -crf 0 -preset ultrafast -c:v libx264 "$IF"
fi
}
SlaveInput()
{
# Check if ~/.mplayer/webmcrop exists, otherwise create it
if [[ ! -f "$HOME/.mplayer/webmcrop" ]] ; then
echo "$MPCONFIG" > "$HOME/.mplayer/webmcrop"
fi
if [[ -z "$STS" ]] ; then
# ffmpeg with the rectangle filter, use webmcrop as input conf
mplayer -title "Cut Video" -fixed-vo -idle -vf rectangle \
-osdlevel 3 -osd-fractions 2 -input conf=webmcrop "$IF" 2>&1
else
mplayer -title "Crop Video" -ss "$STS" -fixed-vo -idle -vf rectangle \
-osdlevel 3 -osd-fractions 2 -input conf=webmcrop "$IF" 2>&1
fi
}
SecondsToTimestamp()
{
# SS.MS to HH:MM:SS.MS, ANS_TIME_POSITION only returns MS to one digit
awk -F . '{printf("%02d:%02d:%02d.%1d",($1/60/60%24),($1/60%60),($1%60),($2))}'
}
CutVideo()
{
STARTEND="$(SlaveInput | grep ANS_TIME_POSITION)"
# First and last occurance of ANS_TIME_POSITION in $STARTEND
STS="$(echo "$STARTEND" | head -1 | awk -F = '{print $2}')"
ETS="$(echo "$STARTEND" | tail -1 | awk -F = '{print $2}')"
ST="$(echo "$STS" | SecondsToTimestamp)"
ET="$(echo "$ETS" | SecondsToTimestamp)"
}
SetOutputFilename()
{
if [[ -z "$ST" || -z "$ET" ]] ; then
# Output filename when converting the complete input file
OF="$(echo "${IF%.*}" | awk -F / '{print $NF}').webm"
else
OF="$(echo "${IF%.*}" | awk -F / '{print $NF}')_${ST}_${ET}.webm"
fi
}
CalculateRunningTime()
{
# Get start (not sure if this matters) and end time from ffmpeg
if [[ -z "$ST" ]] ; then
ST="$(ffmpeg -i "$IF" 2>&1 | grep Duration | awk '{print $4}' | tr -d ,)"
fi
if [[ -z "$ET" ]] ; then
ET="$(ffmpeg -i "$IF" 2>&1 | grep Duration | awk '{print $2}' | tr -d ,)"
fi
if [[ -z "$STARTEND" ]] ; then
# Convert HH:MM:SS.MS to simple seconds with awk, if not already set
ETS="$(echo "$ET" | awk -F : '{print ($1*3600) + ($2*60) + $3}')"
STS="$(echo "$ST" | awk -F : '{print ($1*3600) + ($2*60) + $3}')"
fi
if [[ "$SEGMENTED" = true ]] ; then
OP="$(ffmpeg -i "$IF" 2>&1 | grep 'Chapter #0.0' | awk '{print $6}')"
STS="$(bc <<< "$STS - $OP")"
ETS="$(bc <<< "$ETS - $OP")"
fi
# Running time of output selection
TIME="$(bc <<< "$ETS - $STS")"
}
CalculateBitrate()
{
if [[ "$(bc <<< "$TIME < 10")" -eq 1 && -z "$BR" ]] || [[ "$CQ" = true ]] ; then
# If running time less than 10 and no bitrate specified or CQ mode
BITRATE="-crf $CRF -qmax $(bc <<< "$CRF + 6")"
elif [[ -z "$BR" ]] ; then
# Calculates bitrate depending on $MBLIMIT
BITRATE="-b:v $(bc <<< "($MBLIMIT * 8192) / $TIME")K"
else
BITRATE="-b:v ${BR}K"
fi
}
CropOutput()
{
# Last occurence of WIDTH:HEIGHT:X:Y from SlaveInput()
WHXY="$(SlaveInput | grep rectangle | tail -1 | awk -F = '{print $2}')"
# http://ffmpeg.org/ffmpeg-filters.html#crop
CROPS="crop=${WHXY},"
}
ShiftSubtitleTimings()
{
awk -v "offset=$STS" '
function stamp2sec(timestamp) {
split(timestamp,tfields,":")
ret=0
if (tfields[3]) {
ret = tfields[1]*60*60 + tfields[2]*60 + tfields[3]
} else if (tfields[2]) {
ret = tfields[1]*60 + tfields[2]
} else {
ret = tfields[1]
}
return ret
}
function sec2stamp(time) {
hour = sprintf("%d",time/(60*60))
minute = sprintf("%d",time/60)
sec = sprintf("%d",time)
dec = sprintf("%d",time*100)%100
return hour":"minute":"sec"."dec
}
BEGIN {FS=",";off=offset}
{doprint=1}
/\[Events\]/ { events=1 }
events && /^Dialogue/ {
doprint=1
time=stamp2sec($2)
if (time-off >= 0) {
sub($2,sec2stamp(time-off))
time=stamp2sec($3)
sub($3,sec2stamp(time-off))
} else { doprint=0 }
}
doprint {print}'
}
DetermineSubtitleFormat() {
SUBTYPE="$(ffmpeg -i "$IF" 2>&1 | grep "#0:${STREAM}" | awk '{print $4}')"
}
HardsubOutput()
{
# Dump fonts if there are any, and continue
ffmpeg -v verbose -dump_attachment:t "" -i "$IF" -y 2>/dev/null || true
mkdir -p "$FONTDIR"
# Move dumped fonts to $FONTDIR if there are any, and continue
mv -f ./*.otf ./*.ttf ./*.ttc "$FONTDIR" 2>/dev/null || true
# Invoke ffmpeg, because mediainfo is a piece of shit, to grep
# subtitles contained in file and two additional lines
ffmpeg -v verbose -i "$IF" 2>&1 | grep -A2 Subtitle
read -p "Stream number? [e.g. #0.3 would be 3] " STREAM
DetermineSubtitleFormat
if [[ "$SUBTYPE" = "subrip" ]] ; then
# Dump subtitle stream from start of selection to end of selection
ffmpeg -v verbose -ss "$(bc<<<"$STS - 30")" -i "$IF" -ss 30 -t "$TIME" \
-an -vn -codec:s:${STREAM} srt webmsubsns.srt
# Shift the subtitle timings with awk so we can use fastseek
ShiftSubtitleTimings < webmsubsns.srt > webmsubs.srt
# http://ffmpeg.org/ffmpeg-filters.html#subtitles-1
SUBS=",subtitles=$(pwd)/webmsubs.srt"
elif [[ "$SUBTYPE" = "mov_text" ]] || [[ "$SUBTYPE" = "tx3g" ]] ; then
ffmpeg -v verbose -ss "$(bc<<<"$STS - 30")" -i "$IF" -ss 30 -t "$TIME" \
-an -vn -codec:s:${STREAM} mov_text webmsubsns.ttxt
ShiftSubtitleTimings < webmsubsns.ttxt > webmsubs.ttxt
SUBS=",subtitles=$(pwd)/webmsubs.ttxt"
elif [[ "$SUBTYPE" = "ass" ]] || [[ "$SUBTYPE" = "ssa" ]] ; then
ffmpeg -v verbose -ss "$(bc<<<"$STS - 30")" -i "$IF" -ss 30 -t "$TIME" \
-an -vn -codec:s:${STREAM} ass webmsubsns.ass
ShiftSubtitleTimings < webmsubsns.ass > webmsubs.ass
SUBS=",ass=$(pwd)/webmsubs.ass"
else
echo "Unsupported subtitle format" && exit 1
fi
}
Encode()
{
# http://trac.ffmpeg.org/wiki/FilteringGuide
FILTERS="${ADDFILTERS}${CROPS}scale=${W}:${H}${SUBS}"
# Do a 2-pass encode, throwing the first output into /dev/null
# It may work with '-accurate_seek', but this works for now
# See: https://ffmpeg.org/ffmpeg.html#Main-options
ffmpeg -v verbose -ss "$STS" -i "$IF" -t "$TIME" -threads "$THREADS" \
-quality best -slices "$SLICES" -auto-alt-ref 1 -lag-in-frames "$LAG" \
-an -sn -fs "${MBLIMIT}M" -vf "$FILTERS" -c:v "$CODEC" $BITRATE \
$ADDPARAMS -pass 1 -f webm /dev/null -y || true
ffmpeg -v verbose -ss "$STS" -i "$IF" -t "$TIME" -threads "$THREADS" \
-quality best -slices "$SLICES" -auto-alt-ref 1 -lag-in-frames "$LAG" \
-an -sn -fs "${MBLIMIT}M" -vf "$FILTERS" -c:v "$CODEC" $BITRATE \
$ADDPARAMS -pass 2 -f webm "$OF" -y || true
}
Cleanup()
{
if [[ "$SUB" = true ]] ; then
# Remove temporary font dir and subs
rm -vrf "$FONTDIR"
rm -v webmsubsns.*
rm -v webmsubs.*
fi
if [[ "$XRECORD" = true ]] ; then
# Remove screenrecorder output
rm -v "$IF"
fi
rm -v "$LOG"
}
PrintVariables()
{
echo "Input file: $IF"
echo "Output file: $OF"
echo "Starting time: ${ST}/${STS} seconds"
echo "Ending time: ${ET}/${ETS} seconds"
echo "Running time of output: $TIME seconds"
echo "Bitrate was $BITRATE"
echo "Slices: $SLICES, lag in frames: $LAG"
echo "Threads: $THREADS, set option to one output was slightly cut off"
echo "Filters used: $FILTERS"
echo "Additional parameters: $ADDPARAMS"
}
if [[ "$XRECORD" = true ]] ; then
ScreenRecorder
fi
if [[ -z "$IF" ]] ; then
Usage && exit 1
fi
if [[ "$CUT" = true ]] ; then
CutVideo
fi
if [[ -z "$OF" ]] ; then
SetOutputFilename
fi
CalculateRunningTime
CalculateBitrate
if [[ "$CROP" = true ]] ; then
CropOutput
fi
if [[ "$SUB" = true ]] ; then
HardsubOutput
fi
Encode
Cleanup
PrintVariables
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment