Skip to content

Instantly share code, notes, and snippets.

@don-code
Created February 9, 2021 03:40
Show Gist options
  • Save don-code/d903b05f910a4f55d852c45a4c4e3d68 to your computer and use it in GitHub Desktop.
Save don-code/d903b05f910a4f55d852c45a4c4e3d68 to your computer and use it in GitHub Desktop.
Transcode and burn DVRed shows to a DVD.
#!/bin/bash
set -euo pipefail
# This script determines a set of files recorded by a no-name ATSC DVR between a start and end
# date, concatenates them by recording, then transcodes then to H.264 and burns them to a DVD. It
# uses a constant bitrate for the audio track, then calculates how much of the DVD is available for
# video and calculates a bitrate for use with two-pass encoding. It also guesses whether the source
# is interlaced, and enables interlaced encoding if so.
function usage() {
echo "USAGE: $0 start_date end_date [audio_bitrate [channel_prefix]]"
}
START_DATE="${1:-}"
if [ -z "$START_DATE" ]; then
echo "ERROR: Start date is unset."
usage
exit 1
fi
END_DATE="${2:-}"
if [ -z "$END_DATE" ]; then
echo "ERROR:: end date is unset."
usage
exit 1
fi
AUDIO_BITRATE_KBITS="${3:-}"
if [ -z "$AUDIO_BITRATE" ]; then
AUDIO_BITRATE_KBITS=64
fi
CHANNEL_PREFIX="${4:-}"
MEDIUM_MB=4600 # DVD is 4700MB, but ffmpeg VBR is *variable* and imprecise
# Of interest:
# `-ac 2` knocks Dolby 5.1 down to 2-channel stereo
# `-map 0:a:1` ensures SAP streams aren't brought along
# `-map 0:v` is necessary, because the above line would strip video otherwise
COMMON_FFMPEG_FLAGS="-b:a ${AUDIO_BITRATE_KBITS}k -c:a aac -c:v libx264 -ac 2 -map 0:a:1 -map 0:v -f mp4 -v quiet -stats"
# Get the list of files
date_specifiers=()
this_date_ts=$(date -d $START_DATE +%s)
end_date_ts=$(date -d $END_DATE +%s)
while [ $this_date_ts -le $end_date_ts ]; do
this_date=$(date -d @$this_date_ts +%m%d%Y)
date_specifiers+=($this_date)
files+=($(find . -name "$CHANNEL_PREFIX*$this_date*" -not -name '*.meta'))
this_date_ts=$((this_date_ts + 86400))
done
# Get total length in seconds
total_video_secs=0
echo "Found files:"
for file in ${files[@]}; do
this_video_secs=$(ffprobe -loglevel -8 -show_format_entry duration $file | sed -e '/FORMAT/d' -e 's/duration=//' -e 's/\..*//')
echo "$file (${this_video_secs}sec)"
total_video_secs=$((total_video_secs + this_video_secs))
done
echo "Total video seconds: ${total_video_secs}sec"
echo
# Determine total audio size
audio_bitrate_kbytes=$((AUDIO_BITRATE_KBITS / 8))
audio_kbytes=$((audio_bitrate_kbytes * total_video_secs))
audio_mbytes=$((audio_kbytes / 1024))
echo "${audio_mbytes}MB out of ${MEDIUM_MB}MB will be used for audio"
# Determine video bitrate
video_mbytes=$((MEDIUM_MB - audio_mbytes))
video_mbits=$((video_mbytes * 8))
# need floating point with a leading zero for meaningful input to ffmpeg
video_bitrate_mbits=$(echo "scale=3; $video_mbits / $total_video_secs" | bc | sed 's/^\./0./')
echo "Video bitrate will be ${video_bitrate_mbits}Mbps"
echo
# Process each day's videos to shared memory using two-pass encoding
video_dir="/dev/shm/$BASHPID-video"
stats_dir="/dev/shm/$BASHPID-log"
mkdir $video_dir $stats_dir
for date_specifier in ${date_specifiers[@]}; do
base_file=$(find . -name "$CHANNEL_PREFIX*$date_specifier*.mts")
dest_filename="$(basename -s .mts $base_file).mp4"
concat_string=$(find $PWD -name "$CHANNEL_PREFIX*$date_specifier*" -not -name '*.meta' | sort | paste -s -d '|')
frame_rate=$(ffprobe -loglevel -8 -select_streams v:0 -show_entries stream=avg_frame_rate WBZ-DT-02072021-1830.mts | grep avg_frame_rate | head -n 1 | sed -e 's/.*=//' | bc)
interlaced_flag=""
if [ $frame_rate -lt 49 ]; then # probably interlaced if under 50fps
interlaced_flag="-x264-params interlaced=1"
fi
pushd $stats_dir > /dev/null # ffmpeg always puts the stats file in the working directory
echo "Pass 1 for $concat_string"
ffmpeg -i "concat:$concat_string" $COMMON_FFMPEG_FLAGS $interlaced_flag -b:v ${video_bitrate_mbits}M -pass 1 -y /dev/null
echo "Pass 2 for $concat_string"
ffmpeg -i "concat:$concat_string" $COMMON_FFMPEG_FLAGS $interlaced_flag -b:v ${video_bitrate_mbits}M -pass 2 $video_dir/$dest_filename
popd > /dev/null
rm -f $stats_dir/*
echo
done
rm -rf $stats_dir
# Queue it all up in brasero for burning
brasero $video_dir/* > /dev/null 2>&1
# Prompt to delete
choice=a
while [ $choice != "y" -a $choice != "n" ]; do
read -p "Delete video data? [y/n] " choice
done
if [ $choice == "y" ]; then
rm -rf $video_dir
else
echo "Videos left in $video_dir"
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment