adaptive-rektnetwork/images/ffmpeg/ffmpeg-hls.sh

314 lines
9.7 KiB
Bash
Executable File

#!/bin/bash -e
# a
# .og
# ...f = OGG/FLAC
# ...o = OGG/Opus
# ...v = OGG/Vorbis
# .mpe = MP3
# .a
# ..lc = LC-AAC
# ..he = AAC-HE
# ..hp = AAC-HE v2 (PS)
# v
# .avc = H.264
# .hvc = H.265
# .vp
# ...g = VP-7
# ...h = VP-8
# s
# .wvt = WebVTT subtitles
ffmpeg_version="$(ffmpeg -version | awk '{print $3}' | head -n1)"
# function version_gt() { test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" != "$1"; }
# function version_le() { test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" == "$1"; }
# function version_lt() { test "$(echo "$@" | tr " " "\n" | sort -rV | head -n 1)" != "$1"; }
# function version_ge() { test "$(echo "$@" | tr " " "\n" | sort -rV | head -n 1)" == "$1"; }
function contains_audio() {
case "$1" in
a*|*_a*)
true
;;
*)
false
;;
esac
}
function contains_video() {
case "$1" in
v*|*_v*)
true
;;
*)
false
;;
esac
}
function contains_subtitles() {
case "$1" in
s*|*_s*)
true
;;
*)
false
;;
esac
}
function is_hls_compatible() {
result="1"
for i in ${1//_/ }; do
case "$i" in
ampe|aalc|aah[ep]|vavc)
;;
aogo)
# opus, ffmpeg can code this but no player supports it and it can even confuse bitrade adaption
result="0"
;;
*)
result="0"
;;
esac
done
if [ "$result" -eq 1 ]; then true; else false; fi
}
function is_webm_dash_compatible() {
result="1"
for i in ${1//_/ }; do
case "$i" in
aog[ov]|vvp[gh]|svvt)
;;
*)
result="0"
;;
esac
done
if [ "$result" -eq 1 ]; then true; else false; fi
}
function parse_bitrate() {
echo "$1" |\
sed \
-e 's/g/\*1000000000/gi' \
-e 's/m/\*1000000/gi' \
-e 's/k/\*1000/gi' |\
bc
}
source_url="http://publish.streaminginter.net:61120/${CHANNEL_ID}/master_signal"
if [ -n "${INPUT_URL}" ]; then
source_url="${INPUT_URL}"
fi
echo "* Source URL: ${source_url}"
ffmpeg_log_level="${LOG_LEVEL:-warning}"
ffmpeg_args=()
ffmpeg_args+=(-loglevel "${ffmpeg_log_level}")
ffmpeg_args+=(-i "${source_url}")
if [ -n "${FFMPEG_ARGS}" ]; then
ffmpeg_args+=(${FFMPEG_ARGS})
fi
hls_args=""
if [ -n "${HLS_ARGS}" ]; then
hls_args+=":${HLS_ARGS}"
fi
hls_args="${hls_args}:use_localtime=1"
hls_args="${hls_args}:use_localtime_mkdir=1"
# Reference: https://developer.apple.com/library/content/documentation/General/Reference/HLSAuthoringSpec/Requirements.html
# - 8.4. -> program_data_time
# - 8.13. -> discont_start
hls_args="${hls_args}:hls_flags=discont_start+append_list+omit_endlist+temp_file+delete_segments+program_date_time"
hls_args="${hls_args}:hls_init_time=${HLS_INIT_TIME:-0}"
hls_args="${hls_args}:hls_time=${HLS_TIME:-6}"
hls_args="${hls_args}:hls_list_size=${HLS_LIST_SIZE:-10}"
hls_args="${hls_args}:hls_start_number_source=generic"
hls_args="${hls_args}:hls_ts_options=mpegts_service_type=digital_radio\\\\:mpegts_copyts=1"
# if version_gt ${ffmpeg_version} 3.3.3; then
hls_args="${hls_args}:hls_segment_type=${HLS_SEGMENT_TYPE:-mpegts}"
# else
# echo "* HLS_SEGMENT_TYPE forced to mpegts, currently installed ffmpge $ffmpeg_version does not support hls_segment_type"
# fi
# if version_gt ${ffmpeg_version} 3.3.3; then
if [ ! -z "${HLS_ENCRYPT}" ] && [ "${HLS_ENCRYPT}" -ne 0 ]; then
echo "* Will generate encrypted HLS chunks"
hls_args="${hls_args}:hls_enc=1"
else
echo "* Will NOT generate encrypted HLS chunks"
fi
# else
# echo "* HLS_ENCRYPT forced to 0, currently installed ffmpeg $ffmpeg_version does not support hls_enc"
# fi
webm_chunk_args=""
if [ -n "${WEBM_CHUNK_ARGS}" ]; then
webm_chunk_args+=":${WEBM_CHUNK_ARGS}"
fi
webm_chunk_duration="${WEBM_CHUNK_DURATION:-5000}"
webm_chunk_start_index="${WEBM_CHUNK_START_INDEX:-0}"
webm_chunk_args="${webm_chunk_args}:audio_chunk_duration=${webm_chunk_duration}"
webm_chunk_args="${webm_chunk_args}:chunk_start_index=${webm_chunk_start_index}"
webm_manifest_input_args=()
webm_manifest_output_args=()
webm_manifest_adaptation_sets=""
wemb_manifest_mpds=()
webm_manifest_next_adaptationset_index=0
webm_manifest_next_stream_index=0
function make_stream() {
local codec_shorthand=$1
local codec_id=$2
local codec_options_="$3"
local codec_options=(${codec_options_})
local bitrates_=$4[@]
local bitrates=("${!bitrates_}")
if [ -z "${bitrates}" ]; then
return
fi
echo "* Bitrates for ${codec_shorthand}: ${bitrates[*]}" >&2
old_webm_manifest_next_stream_index=${webm_manifest_next_stream_index}
for bitrate in ${bitrates}; do
parsed_bitrate="$(parse_bitrate "$bitrate")"
output=""
# HLS
if is_hls_compatible "${codec_shorthand}"; then
# if [ -n "$output" ]; then
# # append to tee output name
# output+="|"
# fi
hls_m3u8="hls_${codec_shorthand}${parsed_bitrate}.m3u8"
hls_chk="hls_${codec_shorthand}${parsed_bitrate}/c%s.ts"
mkdir -p "$(dirname "${hls_m3u8}")" "$(dirname "${hls_chk}")"
output+="[f=hls${hls_args}:hls_segment_filename=${hls_chk}]${hls_m3u8}"
# Add format to HLS index
cat <<EOF >>hls.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,AVERAGE-BANDWIDTH=$(echo "${parsed_bitrate}*1.1" | bc),BANDWIDTH=$(echo "${parsed_bitrate} * 1.1" | bc),CODECS="$codec_id"
${hls_m3u8}
EOF
fi
# WebM-DASH
if is_webm_dash_compatible "${codec_shorthand}"; then
if [ -n "$output" ]; then
# append to tee output name
output+="|"
fi
dash_prefix="dash_${codec_shorthand}${parsed_bitrate}"
dash_hdr="${dash_prefix}.hdr"
dash_chk="${dash_prefix}_%d.chk"
rm -f "${dash_hdr}" *.chk
mkdir -p "$(dirname "${dash_hdr}")" "$(dirname "${dash_chk}")"
webm_manifest_mpds+=("${dash_hdr}")
extra_args=""
output+="[f=webm_chunk${webm_chunk_args}${extra_args}:header=${dash_hdr}]${dash_chk}"
# Temporary file to store bandwidth for final manifest
webm_manifest_input_args+=(
-y -f webm_dash_manifest
-live 1
-bandwidth "${parsed_bitrate}"
-i "${dash_hdr}"
)
if [ "${old_webm_manifest_next_stream_index}" = "${webm_manifest_next_stream_index}" ]; then
if [ -n "${webm_manifest_adaptation_sets}" ]; then
webm_manifest_adaptation_sets="${webm_manifest_adaptation_sets%,} "
fi
webm_manifest_adaptation_sets+="id=${webm_manifest_next_adaptationset_index},streams="
webm_manifest_next_adaptationset_index=$((${webm_manifest_next_adaptationset_index}+1))
fi
webm_manifest_adaptation_sets+="${webm_manifest_next_stream_index},"
webm_manifest_output_args+=(-map ${webm_manifest_next_stream_index})
webm_manifest_next_stream_index=$((webm_manifest_next_stream_index+1))
fi
ffmpeg_args+=(
"${codec_options[@]}"
-b:a "${parsed_bitrate}"
-map_metadata 0
-metadata "service_provider=streaminginter.net"
-metadata "service_name=${CHANNEL_NAME:-Livestream}"
-y -f tee
)
if contains_audio "${codec_shorthand}"; then
ffmpeg_args+=(-map 0:a)
fi
if contains_video "${codec_shorthand}"; then
ffmpeg_args+=(-map 0:v)
fi
ffmpeg_args+=("${output}")
done
}
make_webm_dash_manifest() {
ffmpeg_args_2=(
-loglevel "${ffmpeg_log_level}"
"${webm_manifest_input_args[@]}"
-c copy
"${webm_manifest_output_args[@]}"
-y -f webm_dash_manifest -live 1
-adaptation_sets "${webm_manifest_adaptation_sets}"
-chunk_start_index "${webm_chunk_start_index}"
-chunk_duration_ms "${webm_chunk_duration}"
-time_shift_buffer_depth 7200
-minimum_update_period 7200
)
if [ -n "${UTC_TIMING_URL}" ]; then
ffmpeg_args_2+=(-utc_timing_url "${UTC_TIMING_URL}")
fi
ffmpeg_args_2+=(dash.mpd)
set -x
ffmpeg "${ffmpeg_args_2[@]}"
}
find -name '*.mpd' -or -name '*.chk' -or -name '*.m3u8' -delete
echo "#EXTM3U" >hls.m3u8
make_stream aogo opus "-c:a libopus -compression_level 10" OUTPUT_OPUS_BITRATES
make_stream aogv vorbis "-c:a libvorbis" OUTPUT_VORBIS_BITRATES
make_stream aalc mp4a.40.2 "-c:a libfdk_aac -cutoff 20000" OUTPUT_AAC_BITRATES
make_stream aahe mp4a.40.5 "-c:a libfdk_aac -profile:a aac_he" OUTPUT_AAC_HE_BITRATES
make_stream aahp mp4a.40.29 "-c:a libfdk_aac -profile:a aac_he_v2" OUTPUT_AAC_HEV2_BITRATES
make_stream ampe mp4a.40.34 "-c:a libmp3lame" OUTPUT_MP3_BITRATES
# WebM DASH master manifest generation
(
echo "Waiting for DASH manifests to be generated..."
for mpd in "${webm_manifest_mpds[@]}"; do
echo "Waiting for $mpd..."
while [ ! -f "$mpd" ]; do sleep 1; done
done
echo "Generating WebM DASH manifest..."
make_webm_dash_manifest
echo "WebM DASH manifest generated."
) &
# Delete old WebM chunks regularly
watch -t -n 30 'find -name "*.chk" -mmin 120 -delete' &
echo "Starting ffmpeg..."
ffmpeg "${ffmpeg_args[@]}"