Skip to content

Instantly share code, notes, and snippets.

@lelandbatey
Forked from mrbar42/README.md
Last active February 8, 2021 07:46
Show Gist options
  • Save lelandbatey/19c4b6501b2c3a1d13908118e166be5d to your computer and use it in GitHub Desktop.
Save lelandbatey/19c4b6501b2c3a1d13908118e166be5d to your computer and use it in GitHub Desktop.
bash scripts to create VOD HLS stream with ffmpeg almighty (tested on Linux and OS X)

running:

bash create-vod-hls.sh beach.mkv

will produce:

    beach/
      |- playlist.m3u8
      |- 360p.m3u8
      |- 360p_001.ts
      |- 360p_002.ts
      |- 480p.m3u8
      |- 480p_001.ts
      |- 480p_002.ts
      |- 720p.m3u8
      |- 720p_001.ts
      |- 720p_002.ts
      |- 1080p.m3u8
      |- 1080p_001.ts
      |- 1080p_002.ts  

origin: http://docs.peer5.com/guides/production-ready-hls-vod/

#!/usr/bin/env bash
# A modified version of the script for creating HLS streams of videos. From mbar42:
# https://gist.github.com/mrbar42/ae111731906f958b396f30906004b3fa
#
# This version adds support for burning in subtitles and selecting different languages.
set -e
# Usage create-vod-hls.sh SOURCE_FILE [OUTPUT_NAME]
[[ ! "${1}" ]] && echo "Usage: create-vod-hls.sh SOURCE_FILE [OUTPUT_NAME]" && exit 1
# comment/add lines here to control which renditions would be created
renditions=(
# resolution bitrate audio-rate audio_stream subtitle_stream
#"426x240 400k 64k eng"
# "640x360 800k 96k"
"842x480 2000k 128k jpn eng"
#"1280x720 2800k 128k"
#"1920x1080 5000k 192k eng eng"
#"1280x720 2000k 192k jpn eng"
#"1280x720 3000k 192k jpn eng"
"1280x720 4000k 192k jpn eng"
"1920x1080 5000k 192k jpn eng"
"1920x1080 10000k 192k jpn eng"
"1920x1080 15000k 192k jpn eng"
)
segment_target_duration=4 # try to create a new segment every X seconds
max_bitrate_ratio=1.07 # maximum accepted bitrate fluctuations
rate_monitor_buffer_ratio=1.5 # maximum buffer size between bitrate conformance checks
#########################################################################
source="${1}"
target="${2}"
if [[ ! "${target}" ]]; then
target="${source##*/}" # leave only last component of path
target="${target%.*}" # strip extension
fi
mkdir -p ${target}
key_frames_interval="$(echo `ffprobe ${source} 2>&1 | grep -oE '[[:digit:]]+(.[[:digit:]]+)? fps' | grep -oE '[[:digit:]]+(.[[:digit:]]+)?'`*2 | bc || echo '')"
key_frames_interval=${key_frames_interval:-50}
key_frames_interval=$(echo `printf "%.1f\n" $(bc -l <<<"$key_frames_interval/10")`*10 | bc) # round
key_frames_interval=${key_frames_interval%.*} # truncate to integer
# static parameters that are similar for all renditions
static_params="-c:a aac -ac 2 -ar 48000 -c:v h264 -profile:v main -crf 15 -sc_threshold 0 -pix_fmt yuv420p -map 0:0"
static_params+=" -g ${key_frames_interval} -keyint_min ${key_frames_interval} -hls_time ${segment_target_duration}"
static_params+=" -hls_playlist_type vod"
# misc params
misc_params="-hide_banner -y"
master_playlist="#EXTM3U
#EXT-X-VERSION:3
"
cmd=""
for rendition in "${renditions[@]}"; do
# drop extraneous spaces
rendition="${rendition/[[:space:]]+/ }"
# rendition fields
resolution="$(echo ${rendition} | cut -d ' ' -f 1)"
bitrate="$(echo ${rendition} | cut -d ' ' -f 2)"
audiorate="$(echo ${rendition} | cut -d ' ' -f 3)"
audiostream="$(echo ${rendition} | cut -d ' ' -f 4)"
subtitlestream="$(echo ${rendition} | cut -d ' ' -f 5)"
# calculated fields
width="$(echo ${resolution} | grep -oE '^[[:digit:]]+')"
height="$(echo ${resolution} | grep -oE '[[:digit:]]+$')"
maxrate="$(echo "`echo ${bitrate} | grep -oE '[[:digit:]]+'`*${max_bitrate_ratio}" | bc)"
bufsize="$(echo "`echo ${bitrate} | grep -oE '[[:digit:]]+'`*${rate_monitor_buffer_ratio}" | bc)"
bandwidth="$(echo ${bitrate} | grep -oE '[[:digit:]]+')000"
name="${height}p_${bitrate}bitrate"
audiostreamindex=""
if [[ -n "$audiostream" && "$audiostream" != "none" ]]; then
name+="_${audiostream}audio"
audiostreamindex="$(ffprobe -loglevel error -select_streams a -show_entries stream=index:stream_tags=language -of csv=p=0 "$source" | grep "$audiostream" | cut -d ',' -f 1)"
else
audiostreamindex="$(ffprobe -loglevel error -select_streams a -show_entries stream=index:stream_tags=language -of csv=p=0 "$source" | head -n1 | cut -d ',' -f 1)"
fi
subtitlestreamindex=""
if [[ -n "$subtitlestream" && "$subtitlestream" != "none" ]]; then
name+="_${subtitlestream}subs"
subtitlestreamindex="$(ffprobe -loglevel error -select_streams s -show_entries stream=index:stream_tags=language -of csv=p=0 "$source" | grep "$subtitlestream" | awk NF | nl --number-separator=',' --number-width=1 --starting-line-number=0 | cut -d ',' -f 1)"
fi
cmd+=" ${static_params} -vf scale=w=${width}:h=${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:-1:-1:color=black"
if [[ -n "$subtitlestreamindex" ]]; then # only burn in subtitles if sub language was specified, otherwise skip it and FFMPEG will use the defaults (of not including subtitles)
cmd+=",subtitles=$source:stream_index=$subtitlestreamindex"
fi
cmd+=" -b:v ${bitrate} -maxrate ${maxrate%.*}k -bufsize ${bufsize%.*}k -b:a ${audiorate}"
if [[ -n "$audiostreamindex" ]]; then # only choose an audio stream if one was specified, otherwise skip it and FFMPEG will use the defaults
cmd+=" -map 0:$audiostreamindex"
fi
cmd+=" -hls_segment_filename ${target}/${name}_%03d.ts ${target}/${name}.m3u8"
# add rendition entry in the master playlist
master_playlist+="#EXT-X-STREAM-INF:BANDWIDTH=${bandwidth},RESOLUTION=${resolution}\n${name}.m3u8\n"
done
# start conversion
echo -e "Executing command:\nffmpeg ${misc_params} -i ${source} ${cmd}"
ffmpeg ${misc_params} -i ${source} ${cmd}
# create master playlist file
echo -e "${master_playlist}" > ${target}/playlist.m3u8
echo "Done - encoded HLS is at ${target}/"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment