-
Star
(197)
You must be signed in to star a gist -
Fork
(79)
You must be signed in to fork a gist
-
-
Save stenuto/9ff19ce89f07c7419a8d0976736ebe12 to your computer and use it in GitHub Desktop.
#!/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 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
@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. 🤷♂️
Pretty quickly you realize why people pay for a service haha :D
Awesome work @stenuto
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
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 .
Thank you for sharing. Awesome work.
Thanks!
Thank you very much for sharing, this is really awesome and helpful .
@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
Cool script, but no support for 480? :(
Usage:
./hls.sh /path/to/input.mp4 [ /path/to/output_directory ]