- #!/bin/sh
- #
- # TODO: Set predictive GOP size for WebM (when tested that it works)
- set -eu
- # TODO: Externalize to site-specific configfile
- [ $# -gt 0 ] || set -- morla 5002 -- dvcam ../../content/icon_small.png
- ##
- ## defaults
- ##
- RATIO=4:3
- ##
- ## generic functions
- ##
- exit1() {
- echo >&2 "ERROR: $1"
- exit 1
- }
- # shellcheck disable=SC2048,SC2059
- echo_n() {
- printf -- "$*"
- }
- # shellcheck disable=SC2048,SC2059
- printf_each() {
- skel=$1; shift
- for string in $*; do
- printf -- "$skel" "$string"
- done
- }
- uniqwords() {
- echo_n "$@" | tr ' ' '\012' | sort -u
- }
- valuedargcount() {
- nonempty=
- while [ $# -gt 0 ]; do
- [ -z "$1" ] || nonempty=$((nonempty+1))
- shift
- done
- echo_n "$nonempty"
- }
- while [ $# -gt 0 ]; do
- case $1 in
- --)
- shift; break;;
- *)
- if [ -z "${HOST:-}" ]; then
- HOST=$1
- elif [ -z "${FIRSTPORT:-}" ]; then
- FIRSTPORT=$1
- else
- exit1 "Too many arguments: Max. 2 about target"
- fi
- ;;
- esac
- shift
- done
- # TODO: Externalize to site-specific configfile
- [ $# -gt 0 ] || set -- dvcam ../../content/icon_small.png
- AINPUT=
- VINPUT=
- WINPUT=
- XINPUT=
- while [ $# -gt 0 ]; do
- case $1 in
- alsa=*) ALSA=${1#*=}; AINPUT=$((AINPUT+1));;
- alsa) ALSA=default; AINPUT=$((AINPUT+1));;
- dvcam=*) DVCAM=${1#*=}; DEINT=${DEINT:-yadif}; XINPUT=$((XINPUT+1));;
- dvcam) DVCAM=auto; XINPUT=$((XINPUT+1));;
- dc=*) IIDC=${1#*=}; VINPUT=$((VINPUT+1));;
- dc) IIDC=/dev/fw1; VINPUT=$((VINPUT+1));;
- dv-stream=*) AVISTREAM="-f avi -i ${1#*=}"; XINPUT=$((XINPUT+1));;
- dv-stream) AVISTREAM="-f avi -i udp://localhost:10000"; XINPUT=$((XINPUT+1));;
- videofile=*) VFILE=${1#*=}; VINPUT=$((VINPUT+1));;
- *.ffv1|*.yuv|*.vp8|*.vp9) VFILE=$1; VINPUT=$((VINPUT+1));;
- container=*) XFILE=${1#*=}; XINPUT=$((XINPUT+1));;
- *.avi|*.dv|*.mkv|*.mov|*.mp4|*.nut|*.ogg|*.ogv|*.webm) XFILE=$1; XINPUT=$((XINPUT+1));;
- *.png) LOGO=$1; WINPUT=$((WINPUT+1));;
- --) shift; break;;
- *) exit1 "Unsupported input: $1";;
- esac
- shift
- done
- HOST=${HOST:-127.0.0.1}
- if [ "$HOST" = "$(hostname --short)" ]; then
- IP=127.0.0.1
- else
- IP=$(host "$HOST" | grep -Po 'address \K\S+')
- fi
- [ -n "$AINPUT$VINPUT$XINPUT" ] || exit1 "Too few arguments: Min. 1 A/V source"
- [ -z "$AINPUT" ] || [ -z "$VINPUT" ] || [ -z "$XINPUT" ] || exit1 "Too many arguments: Max. 2 A/V sources"
- [ -z "$AINPUT" ] || [ $AINPUT -eq 1 ] || exit1 "Too many arguments: Max. 1 audio source"
- [ -z "$VINPUT" ] || [ $VINPUT -eq 1 ] || exit1 "Too many arguments: Max. 1 video source"
- [ -z "$WINPUT" ] || [ $WINPUT -eq 1 ] || exit1 "Too many arguments: Max. 1 watermark source"
- [ -z "$XINPUT" ] || [ $XINPUT -eq 1 ] || exit1 "Too many arguments: Max. 1 multimedia source"
- [ -n "${NOAUDIO:-}" ] || [ -z "$AINPUT$XINPUT" ] || HASAUDIO=1
- [ -n "${NOVIDEO:-}" ] || [ -z "$VINPUT$XINPUT" ] || HASVIDEO=1
- VSTREAMINDEX=1
- [ -n "$AINPUT" ] || VSTREAMINDEX=0
- FIRSTPORT=${FIRSTPORT:-5002} # even number - next 7 ports used too
- ACHANNELS=1
- AFRAMERATE_SRC=48000
- AFRAMERATE_OPUS=48000
- AFRAMERATE_AAC=44100
- ABITRATE_OPUS=48000
- ABITRATE_AAC=64000
- # FIXME: support multiple heights
- HEIGHT=264
- ENCODINGS_WEBM="$HEIGHT"
- ENCODINGS_MPEG="$HEIGHT"
- videosize() { height=$1; width_or_ratio=$2;
- case $width_or_ratio in
- *:*) num=${width_or_ratio%:*}; den=${width_or_ratio#*:}; width=$((height*num/den));;
- *) width=$width_or_ratio;;
- esac
- echo_n $((height*width))
- }
- BITS=$(videosize "$HEIGHT" "$RATIO")
- # * bitrates and bits in parens based on https://developer.apple.com/library/content/documentation/General/Reference/HLSAuthoringSpec/Requirements.html
- # + bits rounded up to include nearby modulo 16 or 8 sizes
- # * Recommended modulo 16 or 8 (or 4) sizes:
- # + 4:3: [96 216] 264 312 408 480 624 816
- # + 16:9: [72 160] 216 288 360 432 496(540) 720
- # + 2.40:1: [80 160] 200 240 320 360 480 600(620)
- # (highest size not "pumping" in bridge scene 20s into Tears of Steel)
- # quantified using http://aarmstrong.org/content/aspect_ratio_calc.php
- # with ratio as modulo 4 integers, and size a multiplum of that
- # * speeds tuned to just below 100% cpu usage for each combination on a multi-core computer
- # TODO: Externalize speeds to site-specific configfile
- if [ -z "$BITS" ] || [ $BITS -eq 0 ]; then # 192x80 → 15360
- AFRAMERATE_AAC=22050
- ABITRATE_OPUS=16000
- ABITRATE_AAC=32000
- HASVIDEO=
- elif [ $BITS -le 15360 ]; then # 4:3'96→16²*8*6=12288, 16:9'72→8²*16*9=9216, 2.40:1'80→16²*12*5=15360
- AFRAMERATE_AAC=22050
- ABITRATE_OPUS=16000
- ABITRATE_AAC=32000
- VFRAMERATE=10
- VBITRATE=32000
- SPEED_X264=fast; SPEED_X264_ALONE=fast
- SPEED_VP8=3; SPEED_VP8_ALONE=2
- elif [ $BITS -le 62208 ]; then # 4:3'216→8²*27*36=62208, 16:9'160→16²*24*10=61440, 2.40:1'160→16²*24*10=61440
- AFRAMERATE_AAC=22050
- ABITRATE_OPUS=16000
- ABITRATE_AAC=32000
- VFRAMERATE=10
- VBITRATE=64000
- SPEED_X264=fast; SPEED_X264_ALONE=fast
- SPEED_VP8=3; SPEED_VP8_ALONE=2
- elif [ $BITS -le 110592 ]; then # 4:3'264→8²*44*33=92928, 16:9'216→8²*48*27=82944, 2.40:1'200→8²*60*25=96000 (234p→97344)
- VBITRATE=145000
- SPEED_X264=fast; SPEED_X264_ALONE=fast
- SPEED_VP8=3; SPEED_VP8_ALONE=2
- elif [ $BITS -le 150528 ]; then # 4:3'312→8²*52*39=129792, 16:9'288→16²*32*18=147456, 2.40:1'240→16²*36*15=138240 (270p→129600)
- VBITRATE=365000
- SPEED_X264=faster; SPEED_X264_ALONE=fast
- SPEED_VP8=4; SPEED_VP8_ALONE=2
- elif [ $BITS -le 245760 ]; then # 4:3'408→8²*68*51=221952, 16:9'360→8²*80*45=230400, 2.40:1'320→16²*48*20=245760 (360p→230400)
- VBITRATE=730000
- SPEED_X264=veryfast; SPEED_X264_ALONE=fast
- SPEED_VP8=5; SPEED_VP8_ALONE=3
- elif [ $BITS -le 331776 ]; then # 4:3'480→16²*40*30=307200, 16:9'432→16²*48*27=331776, 2.40:1'360→8²*108*45=311040 (432p→331776)
- VBITRATE=1100000
- SPEED_X264=ultrafast; SPEED_X264_ALONE=fast
- SPEED_VP8=8; SPEED_VP8_ALONE=4
- elif [ $BITS -le 552960 ]; then # 4:3'624→16²*52*39=519168, 16:9'496→16²*56*31=444416(540→4²*240*135=518400), 2.40:1'480→16²*72*30=552960 (540p→518400)
- VBITRATE=2000000
- SPEED_X264=toofast; SPEED_X264_ALONE=veryfast
- SPEED_VP8_ALONE=5
- elif [ $BITS -le 922560 ]; then # 4:3'816→16²*68*51=887808, 16:9'720→16²*80*45=921600, 2.40:1'600→8²*180*75=864000(620→4²*372*155=922560) (720p→921600)
- VBITRATE=3000000
- SPEED_X264=toofast; SPEED_X264_ALONE=ultrafast
- SPEED_VP8_ALONE=15
- fi
- [ toofast != "${SPEED_X264:-}" ] || ENCODINGS_MPEG=
- [ -n "$ENCODINGS_MPEG" ] || SPEED_VP8="${SPEED_VP8_ALONE:-}"
- [ -n "$ENCODINGS_WEBM" ] || SPEED_X264="${SPEED_X264_ALONE:-}"
- TARGETS_WEBM="${HASAUDIO:+rtp_opus} ${HASVIDEO:+rtp_vp8}"
- TARGETS_MPEG="rtp_mpegts"
- ENCODINGS=$(uniqwords "$ENCODINGS_WEBM $ENCODINGS_MPEG")
- [ -z "${SAVEDIR:-}" ] || SAVESTEM="${SAVEDIR:-}/$(date +%Y%m%d-%H%M%S)"
- [ -z "${SAVEDIR:-}" ] || export FFREPORT=file="$SAVESTEM.log"
- # * scale+watermark trick based on http://stackoverflow.com/a/10937357
- # + scale to square pixels as needed
- filters_compose() { logo=$1;
- heightcount=$(echo "$ENCODINGS" | wc --words)
- echo_n "[$VSTREAMINDEX:v]${DEINT:+$DEINT,}${VFRAMERATE:+framerate=$VFRAMERATE,}format=pix_fmts=yuv420p,split=$heightcount"
- printf_each '[s%s]' "$ENCODINGS"
- echo_n ';'
- for height in $ENCODINGS; do
- echo_n "[s$height]scale=trunc(iw*sar*$height/ih/2)*2:$height,setsar=1"
- [ -z "$logo" ] || echo_n "[bg$height];[bg$height][$logo:v]overlay=main_w-overlay_w-20:main_h-overlay_h-20"
- echo_n "[v$height]"
- done
- }
- filter_split_codec() { heights_webm=$1; heights_mpeg=$2;
- codeccount=$(valuedargcount "$@")
- for height in $ENCODINGS; do
- echo_n "[v$height]split=$codeccount"
- printf_each '[v%swebm]' "$heights_webm"
- printf_each '[v%smpeg]' "$heights_mpeg"
- done
- }
- # * VP8 encoding based on http://www.webmproject.org/docs/encoder-parameters/#real-time-cbr-encoding-and-streaming
- # + Add 1s latency (deadline)
- # * Predictive GOP size preserving scene-change GOP based on https://superuser.com/a/1098329
- encode_opus() {
- echo_n "-codec:a libopus -ac $ACHANNELS -ar $AFRAMERATE_OPUS -b:a $ABITRATE_OPUS"
- }
- encode_aac() {
- echo_n "-codec:a aac -strict experimental -ac $ACHANNELS -ar $AFRAMERATE_AAC -b:a $((ACHANNELS*ABITRATE_AAC))"
- }
- encode_vp8() { bitrate=$1; speed=$2;
- echo_n "-codec:v vp8 -quality realtime -deadline 1000000 -cpu-used $speed \
- -b:v $bitrate -minrate $bitrate -maxrate $bitrate \
- -undershoot-pct 95 -bufsize $((6000*bitrate/1000)) -rc_init_occupancy $((4000*bitrate/1000)) \
- -max-intra-rate 0 \
- -qmin 4 -qmax 56 \
- -force_key_frames expr:gte(t,n_forced*2)"
- }
- encode_x264() { bitrate=$1; speed=$2;
- echo_n "-codec:v libx264 -preset $speed -tune zerolatency \
- -maxrate $bitrate -bufsize $((bitrate*2)) -crf 23 \
- -force_key_frames expr:gte(t,n_forced*2)"
- }
- # * routing based on http://trac.ffmpeg.org/wiki/Creating%20multiple%20outputs#Teepseudo-muxer
- # * Use same RTP payload types as GStreamer
- mux() { targets="$1";
- targetcount=$(echo "$targets" | wc --words)
- if [ $targetcount -lt 2 ]; then
- case "$targets" in
- hls_mpeg) echo_n "-f hls -hls_time 6 -hls_flags delete_segments"
- echo_n " -hls_flags discont_start -hls_flags append_list"
- echo_n " -use_localtime 1 -hls_segment_filename file-%Y%m%d-%s.ts"
- echo_n " index.m3u8";;
- rtp_opus) echo_n "-f rtp -payload_type 111"
- echo_n " rtp://$IP:$FIRSTPORT?pkt_size=1440";;
- rtp_vp8) echo_n "-f rtp -payload_type 100"
- echo_n " rtp://$IP:$((FIRSTPORT+2))?pkt_size=1440";;
- rtp_mpegts) echo_n "-f rtp_mpegts"
- echo_n " rtp://$IP:10000?pkt_size=1440";;
- *) exit1 "Unknown target $targets";;
- esac
- else
- echo_n "-f tee "
- for target in $targets; do
- case "$target" in
- hls_mpeg) echo_n "[f=hls:hls_time=6:hls_flags=delete_segments"
- echo_n ":hls_flags=discont_start:hls_flags=append_list"
- echo_n ":use_localtime=1:hls_segment_filename=file-%Y%m%d-%s.ts]"
- echo_n "index.m3u8";;
- rtp_opus) echo_n "[select=\'a\':f=rtp:payload_type=111]"
- echo_n "rtp://$IP:$FIRSTPORT?pkt_size=1440";;
- rtp_vp8) echo_n "[select=\'v\':f=rtp:payload_type=100]"
- echo_n "rtp://$IP:$((FIRSTPORT+2))?pkt_size=1440";;
- rtp_mpegts) echo_n "[f=rtp_mpegts]"
- echo_n "rtp://$IP:$((FIRSTPORT+2))?pkt_size=1440";;
- *) exit1 "Unknown target $target";;
- esac
- targetcount=$((targetcount-1))
- [ $targetcount -lt 1 ] || echo_n '|'
- done
- fi
- }
- ffmpeg -hide_banner -threads auto \
- ${ALSA:+-f alsa -sample_rate "$AFRAMERATE_SRC" -channels "$ACHANNELS" -thread_queue_size 2048 -i "$ALSA"} \
- ${DVCAM:+-f iec61883 -thread_queue_size 64 -i $DVCAM} \
- ${AVISTREAM:-} \
- ${XFILE:+-re${SEEK:+ -ss $SEEK} -i "$XFILE"} \
- ${IIDC:+-f libdc1394 -video_size 640x480 -framerate 15 -thread_queue_size 256 -i "$IIDC"} \
- ${VFILE:+-re${SEEK:+ -ss $SEEK} -i "$VFILE"} \
- ${LOGO:+-i "$LOGO"} \
- ${ENCODINGS:+-filter_complex "\
- ${HASVIDEO:+
- $(filters_compose "${LOGO:+$((VSTREAMINDEX+1))}");
- $(filter_split_codec "$ENCODINGS_WEBM" "$ENCODINGS_MPEG") }"} \
- ${SAVEDIR:+\
- ${DVCAM:+-map $((0${ALSA:++1})) \
- -codec copy -f dv "$SAVESTEM.dv" }} \
- ${ENCODINGS_WEBM:+\
- ${HASAUDIO:+-map '0:a' }${HASVIDEO:+$(printf_each ' -map [v%swebm]' "$ENCODINGS_WEBM") } -shortest \
- ${HASAUDIO:+\
- $(encode_opus) } \
- ${HASVIDEO:+\
- $(encode_vp8 "$VBITRATE" "$SPEED_VP8") } \
- $(mux "$TARGETS_WEBM") } \
- ${ENCODINGS_MPEG:+\
- ${HASAUDIO:+-map '0:a' }${HASVIDEO:+$(printf_each ' -map [v%smpeg]' "$ENCODINGS_MPEG") } -shortest \
- ${HASAUDIO:+\
- $(encode_aac) } \
- ${HASVIDEO:+\
- $(encode_x264 "$VBITRATE" "$SPEED_X264") } \
- $(mux "$TARGETS_MPEG") }
|