Created
February 9, 2021 03:40
-
-
Save don-code/d903b05f910a4f55d852c45a4c4e3d68 to your computer and use it in GitHub Desktop.
Transcode and burn DVRed shows to a DVD.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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