Creating Adaptive Video Streams with HLS and FFmpeg in Python

Creating Adaptive Video Streams with HLS and FFmpeg in Python
2024-10-25Intermediate8 min

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:

  • Smooth streaming even in unstable network conditions.
  • Streaming on various devices due to its wide compatibility.
  • Download FFMPEG from

    ffmpeg.org

    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.")
  • We use ffprobe to extract the width and height of the input video. FFmpeg’s ffprobe command provides metadata, which helps us determine the aspect ratio of the original video.
  • If there’s an error retrieving this data, the function raises an error.
  • 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
  • We create the output directory to store our HLS files.
  • The function get_video_resolution gets the original dimensions, and we determine if the video is in portrait mode (height > width) to set quality levels accordingly.
  • 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]
    
  • quality_levels defines resolutions for 360p, 480p, 720p, and 1080p streams. Each one has a width and height based on the aspect ratio.
  • valid_qualities only includes levels that the original resolution can support, so it won’t try to generate a 1080p stream from a low-quality video.
  • 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}")
  • Bitrate: Estimated based on video height (video_bitrate = f"{quality['height'] * 500}k"), to keep each level’s quality consistent.
  • Each quality level is converted and saved in .m3u8 playlist format, and its segments are saved in .ts files.
  • 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.

    Creating Adaptive Video Streams with HLS and FFmpeg in Python

    Visit YouTube Channel to See the Preview and Working of the Code by Clicking Below :

    CodeWithNitikshOfficial