Skip to content

Instantly share code, notes, and snippets.

@ericpruitt
Last active July 17, 2021 20:50
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save ericpruitt/dc5f74382b5d8595a92268424d30fda4 to your computer and use it in GitHub Desktop.
Save ericpruitt/dc5f74382b5d8595a92268424d30fda4 to your computer and use it in GitHub Desktop.
HDMI capture script.
#!/bin/sh
set -e -u
# Base command used to invoke FFmpeg; run "ffmpeg" with as high a priority as
# possible, and minimize its logging output.
FFMPEG="nice -n 20 ffmpeg -loglevel error -hide_banner"
# These control the format of the audio and video.
DEFAULT_VIDEO_EXTENSION="mkv"
SELF="${0##*/}"
run()
(
case "$1" in
--log=.*) log_file="${1#--log=}"
shift
echo "$@"
exec "$@" > "$log_file" 2>&1 ;;
*) echo "$@"
exec "$@" ;;
esac
)
silently()
(
"$@" >/dev/null 2>&1 || return 0
)
usage()
{
echo TODO
}
die()
{
echo "$SELF:" "$@" >&2
exit 2
}
handle_sigint()
{
echo && echo "Caught SIGINT; stopping video capture."
echo "Killing FFmpeg processes:"
run kill -INT $ffmpeg_pids
wait
echo "Combining audio and video streams into a single file:"
run $FFMPEG -i "$audio_tempfile" -i "$video_tempfile" -codec copy -map 0:a:0 -map 1:v:0 "$destination"
du -h "$destination"
run rm "$audio_tempfile" "$video_tempfile" "$ffmpeg_log_file"
run rmdir "$tempdir"
}
main()
{
if [ "$#" -gt 1 ] || [ "${1:-}" = "--help" ]; then
usage
test "$#" -eq 1 && return || return 2
elif [ "$#" -eq 0 ] || [ -d "$1" ]; then
basename="$(date +%Y-%m-%dT%H:%M:%S%z)"
extension="$DEFAULT_VIDEO_EXTENSION"
destination="${1+${1%/}/}$basename.$extension"
else
destination="$1"
extension="${destination##*.}"
fi
if [ -z "$extension" ] || [ "$extension" = "$destination" ]; then
die "$destination: destination path does not include an extension"
fi
# Make sure FFmpeg does not interpret the paths as URIs or options.
parent="$(dirname "$destination")"
case "$parent" in
[!/]*:*|-*) parent="./$parent" ;;
esac
case "$destination" in
[!/]*:*|-*) destination="./$destination" ;;
esac
tempdir=".$SELF.tmp" && test "$parent" = "." || tempdir="$parent/$tempdir"
ffmpeg_log_file="$tempdir/ffmpeg.log"
if [ -e "$tempdir" ]; then
printf "%s: %s already exists; overwrite and continue anyway? (n/Y) " \
"$SELF" "$tempdir"
read -r response
case "$response" in
*[yY]*) FFMPEG="$FFMPEG -y" ;;
*) return 2 ;;
esac
else
mkdir "$tempdir"
FFMPEG="$FFMPEG -n"
fi
# The video grabber device connects and disconnects constantly for some
# reason, so the audio and video device identifiers are queried repeatedly
# until both are found.
while :; do
pulse_audio_id="$(
pactl list short sources \
| awk '/usb-Video_Grabber_HDMI_to_U3_capture/ { print $1; exit }'
)"
video_device="$(
find /sys/devices/ -type f -name name \
-exec grep -l -e '^HDMI to U3 capture$' {} + \
| awk -F "/" '/video4linux/ { print "/dev/" $(NF - 1); exit }'
)"
if [ -n "$pulse_audio_id" ] && [ -n "$video_device" ]; then
break
elif [ -n "$pulse_audio_id" ]; then
what="video"
elif [ -n "$video_device" ]; then
what="audio"
else
what="audio or video"
fi
echo "$SELF: unable to find $what device; sleeping then trying again"
sleep 1
done
# Invocation of these FFmpeg processes races against the disconnect, but in
# practice, FFmpeg is usually able to connect to the device quickly enough
# that it's rarely a problem.
> "$ffmpeg_log_file"
ffmpeg_pids=""
audio_tempfile="$tempdir/audio.wav"
run --log="$ffmpeg_log_file" $FFMPEG -f pulse -ac 2 -ar 44100 -i "$pulse_audio_id" -c copy -map 0 "$audio_tempfile" &
ffmpeg_pids="$ffmpeg_pids $!"
video_tempfile="$tempdir/video.$extension"
run --log="$ffmpeg_log_file" $FFMPEG -f v4l2 -i "$video_device" -c copy -map 0 "$video_tempfile" &
ffmpeg_pids="$ffmpeg_pids $!"
# If there was a problem invoking FFmpeg, the subprocesses usually die
# quickly, so wait briefly before checking to see if any invocations
# failed.
sleep 1 &&
if ! kill -0 $ffmpeg_pids; then
silently kill -INT $ffmpeg_pids
cat "$ffmpeg_log_file"
die "unable to start FFmpeg"
fi
# Once everything is running, display the size of the captured data until
# the user stops recording with SIGINT / ^C.
trap handle_sigint INT
trap EXIT
(
while :; do
printf "\r%s " "$(du -h "$tempdir" | tr "\t" " ")"
sleep 1
done
) || :
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment