#!/bin/bash # I, Ian Kelling, follow the GNU license recommendations at # https://www.gnu.org/licenses/license-recommendations.en.html. They # recommend that small programs, < 300 lines, be licensed under the # Apache License 2.0. This file contains or is part of one or more small # programs. If a small program grows beyond 300 lines, I plan to change # to a recommended GPL license. # Copyright 2024 Ian Kelling # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ffs = ffmpeg stream # potential improvement: it might be nice that we could have a tall terminal but only use # the top half for a 1080p stream, this is how: # https://superuser.com/questions/1106674/how-to-add-blank-lines-above-the-bottom-in-terminal if ! test "$BASH_VERSION"; then echo "error: shell is not bash" >&2; exit 1; fi shopt -s inherit_errexit 2>/dev/null ||: # ignore fail in bash < 4.4 set -eE -o pipefail trap 'echo "$0:$LINENO:error: \"$BASH_COMMAND\" exit status: $?, PIPESTATUS: ${PIPESTATUS[*]}" >&2' ERR usage() { cat <&2; exit 1; } ffp_args=() debug=false delay=true loglevel=fatal watch=true volume=0 fullscreen=false tall=true temp=$(getopt -l help hdlr:uw "$@") || usage 1 eval set -- "$temp" while true; do case $1 in -d) debug=true loglevel=debug loglevel=info ffp_args+=(-d) ;; -l) volume=1 ;; -r) case $2 in tall) fullscreen=false tall=true ;; quarter) fullscreen=false tall=false ;; full) fullscreen=true tall=false ;; esac shift ;; -w) watch=false ;; -u) delay=false ;; -h|--help) usage ;; --) shift; break ;; *) echo "$0: unexpected args: $*" >&2 ; usage 1 ;; esac shift done mount_suffix=-sysops if [[ $1 ]]; then case $1 in sysops|tech) mount_suffix=-$1 ;;& tech) delay=false ;; staff) mount_suffix= ;; *) echo "error: unexpected \$1: $1" >&2 exit 1 ;; esac ffp_args+=($1) fi if $delay; then # 2500 gets us around a 4 second delay, up from 1.5s. delay_arg=,tpad=start_duration=2500ms fi ##### end command line parsing ######## host=live.iankelling.org:8443 live_host=$(dig +timeout=1 +short @iankelling.org live.iankelling.org) vps_host=$(dig +timeout=1 +short iankelling.org) if [[ $live_host != "$vps_host" ]] && ip n show 10.2.0.1 | grep . &>/dev/null && \ [[ $(dig +timeout=1 +short @10.2.0.1 -x 10.2.0.2 2>&1 ||:) == kd.b8.nz. ]]; then host=127.0.0.1:8000 if ! pgrep '^icecast2$' >/dev/null; then sudo systemctl start icecast2 fi else find_prefix="ssh live.iankelling.org" fi if $find_prefix find /var/icecast -type f | grep .; then echo "warning: suggest clearing /var/icecast with icrmr or moving files. sleeping for 4 seconds" sleep 4 fi pass=$(sed -n 's/ *\([^<]*\).*/\1/p' /p/c/icecast.xml) tmpf=$(mktemp) xrandr >$tmpf # example xrandr output: 1280x800+0+0 primary_res=$(awk '$2 == "connected" && $3 == "primary" { print $4 }' $tmpf | sed 's/+.*//') tmp=$(awk '$2 == "connected" && $3 != "primary" { print $3 }' $tmpf | sed 's/+/ /g') read -r secondary_res x_offset _ <<<"$tmp" if [[ $secondary_res ]]; then secondary_x=${secondary_res%%x*} secondary_y=${secondary_res##*x} if $fullscreen; then stream_res=$secondary_res elif $tall; then stream_res=$(( secondary_x / 2 ))x$secondary_y else stream_res=$(( secondary_x / 2 ))x$(( secondary_y / 2)) fi else x_offset=0 stream_res=$primary_res fi stream_x=${stream_res%x*} stream_y=${stream_res#*x} # leave out our i3 window borders stream_res=$(( stream_x - 4 ))x$(( stream_y - 4)) # if hardware acceleration exists, use it to save power & cpu. if vainfo |& grep -i VAProfileVP9Profile &>/dev/null; then # 1500 seems almost flawless bitrate_1080=1500 encode_settings=( -c:v vp9_vaapi # these options increase compression based on random internet reference. -bsf:v vp9_raw_reorder,vp9_superframe ) # https://trac.ffmpeg.org/wiki/Hardware/VAAPI global_extra_args=( -vaapi_device /dev/dri/renderD128 ) extra_filter_arg=",format=nv12|vaapi,hwupload" else # 1000 is a bit blury, 1500 is pretty clear, 2000 makes scrolling # adjust much faster, 2500 has marginal improvement on that. # # note https://livekit.io/webrtc/bitrate-guide (our framerate is lower) bitrate_1080=2000 encode_settings=( -vcodec libvpx -quality realtime -error-resilient 1 ) fi bitrate=$(( ( stream_x * stream_y ) / ( (1920*1080) / bitrate_1080 ) )) # 8 seems fine. be conservative by going a bit higher. framerate=10 keyframe_interval=$((framerate * 2)) # Monitor of default sink. # eg: alsa_output.usb-Audio-gd_Audio-gd-00.analog-stereo pa_sink=$(pactl get-default-sink).monitor # this is for ffmpeg warnings. doesnt seem to affect latency. # 160 was too small. at 300, it occasionally complains, # probably only when we are using delayed output thread_queue_size_arg="-thread_queue_size 500" opts=( # global options # be relatively quiet. switch to debug when testing. -v $loglevel -hide_banner -nostats ${global_extra_args[@]} # tested for decreasing latency: did not help. # -probesize 32 # tested for warning "Queue input is backward in time". did not help. #-rtbufsize 500M # note: ordering of inputs also affects zmqsend commands. ## audio input options -f pulse -name ffs # note: duplicated $thread_queue_size_arg -fragment_size 512 -i default -f pulse $thread_queue_size_arg # pulse knows this name somewhere -name ffsdesktop # This fixes latency. i haven't tried tuning it, but going too low creates # choppy output. -fragment_size 512 -i "$pa_sink" ## video input options -video_size $stream_res $thread_queue_size_arg -f x11grab -framerate $framerate -i :0.0+$x_offset.0 # Video + audio filter. Note: this has only the things we actually need in it. # # volume=precision=fixed fixes this error: # The following filters could not choose their formats: Parsed_amerge_4. # # Default volume precision is float. Our input is fixed. maybe ffmpeg # thinks the input could change and so can't commit to an output. # The error suggests using aformat, which seems like it would probably # also fix the error. # # man page say zmq url default includes "localhost", but specifying a # localhost url caused an error for me. -filter_complex "[0]azmq,volume=precision=fixed: volume=$volume [vol0]; [1]azmq='b=tcp\://127.0.0.1\:5556',volume=precision=fixed: volume=0 [vol1]; [vol0][vol1] amerge=inputs=2; [2]zmq='b=tcp\://127.0.0.1\:5557',drawbox=color=0x262626,drawtext=fontsize=90: fontcolor=beige: x=40: y=40: text=''${delay_arg}${extra_filter_arg}[out]" # An online source says to match a 5 second vid delay, we can do an # audio delay filter: "adelay=5000|5000". However, we already get # a stream delay of about 2 seconds, and having the audio be about # 2 seconds ahead is fine, they do that intentionally in soccer # matches. # Based on error message and poking around, it seems ffmpeg is not # smart enough to see that [vol0] and [vol1] are inputs to the amerge # filter, and thus we would not want them as final outputs. So, we # have to identify the amerge output and pass it to -out. This # identifier is called an "output pad" in man ffmpeg-filters, and a # "link label" in man ffmpeg. -map '[out]' # video output options -g $keyframe_interval ${encode_settings[@]} -b:v ${bitrate}k ## audio output options -c:a libvorbis -b:a 128k # afaik, this ensures that the amerge doesn't make 4 channel output if # our output format supported it. -ac 2 -content_type video/webm -f webm icecast://source:$pass@$host/fsf$mount_suffix.webm ) rm -f /tmp/iank-ffmpeg-interlude-toggle # system mute. disabled, just using application level mute atm. # # pactl set-source-mute @DEFAULT_SOURCE@ true if pkill -f ^ffmpeg.\*icecast://source.\*/fsf; then sleep 1 fi echo executing: ffmpeg ${opts[@]} #{ sleep 1; ffp &>/dev/null & } if $debug; then ffmpeg "${opts[@]}" exit 0 fi ##### begin clipboard history checkup #### # Avoid streaming with secrets in our clipboard history. We could just # clear the history, but here I truncate it to a max and then show it, # and then I can press super+y if I want to clear it, or close the # window if I want to keep it. copyqcount=$(copyq count) regex='^[1-9][0-9]*$' if [[ $copyqcount =~ $regex ]]; then # i dont want to think about more than this max_rows=40 if (( copyqcount >= max_rows )); then rows_arg=() for ((i=max_rows; i/dev/null; then sleep .5 else gone=true break fi done if ! $gone; then msg="ffs: copyq not gone. aborting. super+y = copyq-restart / clear" if [[ -t 0 ]]; then echo $msg else dunstify -u critical -h string:x-dunst-stack-tag:alert "$msg" fi exit 1 fi fi ##### end clipboard history checkup #### if [[ $mount_suffix == -sysops ]]; then touch $HOME/.iank-stream-on fi echo $volume >$HOME/.iank-stream-muted ffmpeg "${opts[@]}" & if $watch; then # watch the stream and end the stream when we stop watching. sleep 2 ffp -d "${ffp_args[@]}" ||: kill %% rm -f $HOME/.iank-stream-on fi ### begin background/development docs ### # zmq vs stdin commands: # # * zmq allows targeting a specific filter when there are duplicates. # # * if you type stdin command too slow, ffmpeg will die because it stops # doing normal work while it is waiting. # # * zmq returns a result to the calling process, rather than printing to # stdout. # # * the only simple zmq tool I found, zmqsend, requires compiling. I # used the latest ffmpeg 7.0.1. Build like so: # # p build-dep ffmpeg/aramo # ./configure --enable-libzmq # i already had libzmq3-dev installed # make alltools # cp tools/zmqsend /a/opt/bin # # * ffmpeg debug output was useful in testing zmq commands. # # * Important documentation for stdin commands is only found by typing # "c" into the stdin of ffmpeg and reading the output. # # # # stdin command docs, before I abandoned it for zmq: # mkfifo -m 0600 /tmp/iank-ffmpeg # # ffmpeg sits and waits until we do this. dunno why. # echo >/tmp/iank-ffmpeg & # ffmpeg ... /tmp/iank-ffmpeg # echo "cdrawbox -1 t fill" >/tmp/iank-ffmpeg # echo "cdrawtext -1 reinit text=''" >/tmp/iank-ffmpeg # echo "cvolume -1 volume=1" >/tmp/iank-ffmpeg # For testing: to show the number of audio channels in a resulting file # https://stackoverflow.com/questions/47905083/how-to-check-number-of-channels-in-my-audio-wav-file-using-ffmpeg-command # # ffprobe -i /tmp/out.wav -show_entries stream=channels -select_streams a:0 -of compact=p=0:nk=1 -v 0 # for a right/left speaker test: # https://askubuntu.com/questions/148363/which-linux-command-can-i-use-to-test-my-speakers-for-current-talk-radio-output # p install alsa-utils # speaker-test -t wav -c 2 -l 1 # There are 2 other options for audio, so I wanted to do a little # performance measurement of this method. # 1 is to combine the 2 audio sources in pulse, # https://unix.stackexchange.com/questions/351764/create-combined-source-in-pulseaudio . # 1 is to record mumble and combine in post processing. ### benchmark / perf tests: these are pretty inaccurate. # 29 seconds cpu use. video bitrate 1500k, 8 fps, 2x keyframe interval. # * 64k vorbis: 69.7% # * 128k vorbis: 70.1% (used in subsequent tests) # * 1 audio input: 64.3% # * 0 audio inputs: 59.2% # how I did perf testing: add -to 00:00:30 to ffmpeg opts to # conveniently exit after measurement. Then run: # # ffmpeg "${opts[@]}" & # pid=$! # sleep 29 # ps -p $pid -o %cpu # kill %% # filter for only 1 audio input: #-filter_complex "[0]azmq,volume=precision=fixed;[1]zmq='b=tcp\://127.0.0.1\:5557',drawbox=color=0x262626,drawtext=fontsize=90: fontcolor=beige: x=40: y=40: text=''" # filter with 0 audio input: # -filter_complex "[0]zmq='b=tcp\://127.0.0.1\:5557',drawbox=color=0x262626,drawtext=fontsize=90: fontcolor=beige: x=40: y=40: text=''" # When things weren't working, I did some checking on newer ffmpeg to # see if that helped. It never did. I compiled the latest ffmpeg release # tarball, 7.0.1, and tried the version in debian bullseye by schrooting # before running ffmpeg. Building was just configure; make, but then I # found some flags that were needed. gpl flags r just because I noticed them. # ./configure --enable-libzmq --enable-libpulse --enable-libvorbis --enable-gpl --enable-version3 # # note: when playing back, text is going to look aliased unless you # watch it in a window that is exactly as bit or bigger than the # recording: tabbed i3 window shrinks things. or, use: mpv # --video-unscaled ### end background/development docs ###