Skip to content

Instantly share code, notes, and snippets.

@stenuto
Created November 7, 2024 16:58
Show Gist options
  • Save stenuto/9ff19ce89f07c7419a8d0976736ebe12 to your computer and use it in GitHub Desktop.
Save stenuto/9ff19ce89f07c7419a8d0976736ebe12 to your computer and use it in GitHub Desktop.
HLS ffmpeg script
#!/bin/bash
# Function to display usage information
usage() {
echo "Usage: $0 /path/to/input.mp4 [ /path/to/output_directory ]"
exit 1
}
# Check if at least one argument (input file) is provided
if [ $# -lt 1 ]; then
usage
fi
# Assign input file and output directory from arguments
INPUT_FILE="$1"
# Check if input file exists
if [ ! -f "$INPUT_FILE" ]; then
echo "Error: Input file '$INPUT_FILE' does not exist."
exit 1
fi
# Check for required commands
for cmd in ffmpeg ffprobe; do
if ! command -v $cmd &> /dev/null; then
echo "Error: $cmd is not installed or not in PATH."
exit 1
fi
done
# Get the base filename without extension
FILENAME=$(basename "$INPUT_FILE")
BASENAME="${FILENAME%.*}"
# Determine input directory
INPUT_DIR=$(dirname "$INPUT_FILE")
# Assign output directory: second argument or default to current directory
if [ $# -ge 2 ]; then
OUTPUT_BASE_DIR="$2"
# Create the output base directory if it doesn't exist
mkdir -p "$OUTPUT_BASE_DIR"
else
OUTPUT_BASE_DIR="$PWD"
fi
# Create specific output directory within the base output directory using the base filename
OUTPUT_DIR="$OUTPUT_BASE_DIR/$BASENAME"
# Remove existing output directory if it exists
if [ -d "$OUTPUT_DIR" ]; then
echo "Output directory '$OUTPUT_DIR' already exists. Removing it to avoid unused .ts files."
rm -rf "$OUTPUT_DIR"
fi
mkdir -p "$OUTPUT_DIR"
# Get the frame rate of the input video
FRAME_RATE=$(ffprobe -v 0 -of default=noprint_wrappers=1:nokey=1 \
-select_streams v:0 -show_entries stream=avg_frame_rate "$INPUT_FILE")
# Convert frame rate to a number
IFS='/' read -r num denom <<< "$FRAME_RATE"
if [ -z "$denom" ] || [ "$denom" -eq 0 ]; then
denom=1
fi
FRAME_RATE=$(echo "scale=2; $num/$denom" | bc)
FRAME_RATE=${FRAME_RATE%.*}
# Calculate GOP size (number of frames per 4 seconds)
GOP_SIZE=$((FRAME_RATE * 4))
# Define resolutions, bitrates, and output names
RESOLUTIONS=("1280x720" "1920x1080" "3840x2160")
BITRATES=("1200k" "2500k" "8000k")
OUTPUTS=("720p" "1080p" "2160p")
PLAYLISTS=()
# Loop over the variants
for i in "${!RESOLUTIONS[@]}"; do
RES="${RESOLUTIONS[$i]}"
BITRATE="${BITRATES[$i]}"
OUTPUT_NAME="${OUTPUTS[$i]}"
PLAYLIST="${OUTPUT_NAME}.m3u8"
PLAYLISTS+=("$PLAYLIST")
# Set profile and level based on resolution
if [ "$OUTPUT_NAME" == "2160p" ]; then
PROFILE="high"
LEVEL="5.1"
elif [ "$OUTPUT_NAME" == "1080p" ]; then
PROFILE="high"
LEVEL="4.2"
else
PROFILE="main"
LEVEL="3.1"
fi
echo "Processing $OUTPUT_NAME..."
if ! ffmpeg -y -i "$INPUT_FILE" \
-c:v libx264 -preset veryfast -profile:v "$PROFILE" -level:v "$LEVEL" -b:v "$BITRATE" -s "$RES" \
-c:a aac -b:a 128k -ac 2 \
-g $GOP_SIZE -keyint_min $GOP_SIZE -sc_threshold 0 \
-force_key_frames "expr:gte(t,n_forced*4)" \
-hls_time 4 -hls_list_size 0 -hls_flags independent_segments \
-hls_segment_filename "$OUTPUT_DIR/${OUTPUT_NAME}_%03d.ts" \
"$OUTPUT_DIR/$PLAYLIST"; then
echo "Error: Failed to process $OUTPUT_NAME."
exit 1
fi
done
# Generate master playlist
MASTER_PLAYLIST="$OUTPUT_DIR/playlist.m3u8"
echo "Generating master playlist..."
{
echo "#EXTM3U"
echo "#EXT-X-VERSION:3"
for i in "${!RESOLUTIONS[@]}"; do
RESOLUTION="${RESOLUTIONS[$i]}"
OUTPUT_NAME="${OUTPUTS[$i]}"
PLAYLIST="${OUTPUTS[$i]}.m3u8"
BITRATE="${BITRATES[$i]}"
BANDWIDTH=$(( ${BITRATE%k} * 1000 + 128000 )) # Video bitrate + audio bitrate in bits per second
echo ""
echo "#EXT-X-STREAM-INF:BANDWIDTH=$BANDWIDTH,RESOLUTION=$RESOLUTION"
echo "$PLAYLIST"
done
} > "$MASTER_PLAYLIST"
echo "Processing completed successfully."
echo "Output directory: $OUTPUT_DIR"
@stenuto
Copy link
Author

stenuto commented Nov 7, 2024

Usage: ./hls.sh /path/to/input.mp4 [ /path/to/output_directory ]

@wesbos
Copy link

wesbos commented Nov 8, 2024

@stenuto awesome! I wrote something similar in TypeScript because bash is hard - I only specify the height and let the width of the video be detected in case I ever have videos that are different aspect ratios: https://github.com/wesbos/R2-video-streaming/blob/main/transcode.ts

@stenuto
Copy link
Author

stenuto commented Nov 8, 2024

@wesbos yep, good idea on the different aspect ratios! It'd be cool if the bitrates also adjusted based on the resolution/actual pixel count. For example, a square 1080p video should have a higher bitrate than a 16:9 1080p video. Splitting hairs, but these are the things we think about. 🤷‍♂️

@wesbos
Copy link

wesbos commented Nov 8, 2024

Pretty quickly you realize why people pay for a service haha :D

@Spikeysanju
Copy link

Awesome work @stenuto

@nwaughachukwuma
Copy link

This is awesome! Thanks for sharing @stenuto. I made a python version which is slightly faster due to preset: ultrafast | threads: 0 config options.

https://gist.github.com/nwaughachukwuma/ad42141a8c0ab37bdf95ad15abad0b20

@patel-dev-03
Copy link

Pretty Awesome Neat . Thanks for sharing , I have made something similar using JavaScript but I generate a dynamic ffmpeg hls command based input video and audio quality and ratio and so all in just one command .

@amman20
Copy link

amman20 commented Nov 10, 2024

Thank you for sharing. Awesome work.

@alangabrielbs
Copy link

Thanks!

@Xybron
Copy link

Xybron commented Nov 16, 2024

Thank you very much for sharing, this is really awesome and helpful .

@anastygnome
Copy link

@stenuto thanks for the script :)

I believe the framerate part can be done a bit more simply with

FRAME_RATE=$(ffprobe -v 0 -select_streams v:0 -show_entries stream=avg_frame_rate \
    -of csv=p=0 "$INPUT_FILE" | awk -F'/' '{printf "%.0f", ($1 / ($2 ? $2 : 1))}')

The of flag does what you need in your case.

Btw, the awk part is only needed if the denom is zero, if you are sure it doesn't happen, you could just pipe this to bc

@woeterman94
Copy link

Cool script, but no support for 480? :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment