#!/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 <>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[@]}"