Creating Adaptive Video Streams with HLS and FFmpeg in Python
Streaming video content online has become standard practice, and HLS (HTTP Live Streaming) is one of the most reliable formats to make it happen. Developed by Apple, HLS is great for delivering video because it breaks videos into smaller segments and adjusts the quality based on the viewer’s internet speed. This tutorial will walk you through how to set up a Python script to convert a video into HLS format with multiple quality levels using FFmpeg.
Why Use HLS?
HLS dynamically adapts video quality to give the best viewing experience. It’s ideal for:
Download FFMPEG from
Understanding the Code to Convert Video to HLS
Let’s look at the script that does the heavy lifting:
1. Getting the Video Resolution
Our script begins by determining the video’s resolution. Why? Because the quality of each HLS stream is based on this resolution.
def get_video_resolution(input_file):
result = subprocess.run(
["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
"stream=width,height", "-of", "csv=p=0", input_file],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
if result.returncode == 0:
width, height = map(int, result.stdout.strip().split(','))
return width, height
else:
raise RuntimeError("Error retrieving video resolution.")
2. Converting the Video to HLS
The main function here, convert_to_hls, takes an input file and a designated output directory and then performs the conversion.
def convert_to_hls(input_file, output_dir, segment_time=10):
os.makedirs(output_dir, exist_ok=True)
# Get the input video’s resolution and determine aspect ratio
width, height = get_video_resolution(input_file)
is_portrait = height > width
3. Defining Quality Levels
Based on the original video’s aspect ratio, the script sets up different quality levels for HLS streaming.
quality_levels = [
{"name": "360p", "width": 360 if is_portrait else 640, "height": 640 if is_portrait else 360},
{"name": "480p", "width": 480 if is_portrait else 854, "height": 854 if is_portrait else 480},
{"name": "720p", "width": 720 if is_portrait else 1280, "height": 1280 if is_portrait else 720},
{"name": "1080p", "width": 1080 if is_portrait else 1920, "height": 1920 if is_portrait else 1080}
]
valid_qualities = [q for q in quality_levels if q["width"] <= width and q["height"] <= height]
4. Generating HLS Segments for Each Quality Level
Now we loop through each valid quality level, create HLS segments, and generate playlists.
for quality in valid_qualities:
resolution = f"{quality['width']}x{quality['height']}"
output_playlist = os.path.join(output_dir, f"playlist_{quality['name']}.m3u8")
output_segment_path = os.path.join(output_dir, f"segment_{quality['name']}_%03d.ts")
# Estimate bitrate based on resolution
video_bitrate = f"{quality['height'] * 500}k"
command = [
"ffmpeg",
"-i", input_file,
"-vf", f"scale={resolution}",
"-c:v", "h264",
"-b:v", video_bitrate,
"-c:a", "aac",
"-b:a", "128k",
"-start_number", "0",
"-hls_time", str(segment_time),
"-hls_list_size", "0",
"-f", "hls",
"-hls_segment_filename", output_segment_path,
output_playlist
]
try:
subprocess.run(command, check=True)
print(f"Generated HLS for {quality['name']}! Playlist saved at: {output_playlist}")
except subprocess.CalledProcessError as e:
print(f"Error during HLS conversion for {quality['name']}: {e}")
Code that converts Video to HLS
import subprocess
import os
def get_video_resolution(input_file):
# Extracts video width and height to determine resolution and aspect ratio
result = subprocess.run(
["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries",
"stream=width,height", "-of", "csv=p=0", input_file],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
if result.returncode == 0:
width, height = map(int, result.stdout.strip().split(','))
return width, height
else:
raise RuntimeError("Error retrieving video resolution.")
def convert_to_hls(input_file, output_dir, segment_time=10):
os.makedirs(output_dir, exist_ok=True)
# Get the input video’s resolution and determine aspect ratio
width, height = get_video_resolution(input_file)
is_portrait = height > width
# Define quality levels based on aspect ratio
quality_levels = [
{"name": "360p", "width": 360 if is_portrait else 640, "height": 640 if is_portrait else 360},
{"name": "480p", "width": 480 if is_portrait else 854, "height": 854 if is_portrait else 480},
{"name": "720p", "width": 720 if is_portrait else 1280, "height": 1280 if is_portrait else 720},
{"name": "1080p", "width": 1080 if is_portrait else 1920, "height": 1920 if is_portrait else 1080}
]
# Filter quality levels up to the video’s resolution
valid_qualities = [q for q in quality_levels if q["width"] <= width and q["height"] <= height]
# Create HLS streams for each valid quality level
for quality in valid_qualities:
resolution = f"{quality['width']}x{quality['height']}"
output_playlist = os.path.join(output_dir, f"playlist_{quality['name']}.m3u8")
output_segment_path = os.path.join(output_dir, f"segment_{quality['name']}_%03d.ts")
# Estimate bitrate based on resolution
video_bitrate = f"{quality['height'] * 500}k"
command = [
"ffmpeg",
"-i", input_file,
"-vf", f"scale={resolution}",
"-c:v", "h264",
"-b:v", video_bitrate,
"-c:a", "aac",
"-b:a", "128k",
"-start_number", "0",
"-hls_time", str(segment_time),
"-hls_list_size", "0",
"-f", "hls",
"-hls_segment_filename", output_segment_path,
output_playlist
]
# Run FFmpeg command
try:
subprocess.run(command, check=True)
print(f"Generated HLS for {quality['name']}! Playlist saved at: {output_playlist}")
except subprocess.CalledProcessError as e:
print(f"Error during HLS conversion for {quality['name']}: {e}")
# Example usage
input_file = "C:\\Users\\hp\\Desktop\\python video processing\\videos\\chat_app_with_firebase.mp4"
output_dir = "hls_output"
convert_to_hls(input_file, output_dir)
After running, you should see multiple .m3u8 playlists and .ts segment files in the specified output directory. Each playlist corresponds to a different quality level, allowing you to deliver an adaptive HLS stream to your users.
Visit YouTube Channel to See the Preview and Working of the Code by Clicking Below :