Paul Bradley • Solutions Architect & Software Developer


Published:

HLS Video Streaming with MP4 Legacy Fallback

A concise guide to preparing and hosting HLS streaming video with legacy fallback support.

midjourney ai - terraform

Table of Contents
  1. Introduction
  2. Demonstration
  3. Converting an MP4 video into an HLS m3u8
  4. Pulling a frame from the video to act as the poster
  5. HTML5 Video Container
  6. Using HLS.js
  7. Creating a fallback for older clients
  8. CSP Content Security Policy
  9. JavaScript disabled

 Introduction

The medical image acquisition project I’m working on, MIA, converts all video uploaded to an MP4 file. So, for example, when users of Apple devices upload videos in MOV format, the system converts the files in the background.

I’ve been looking into making the default video format for the mia application HLS.

HLS stands for HTTP Live Streaming. It’s an HTTP-based adaptive streaming protocol developed by Apple. The protocol has widespread support in media players, and the HTML5 video control can play the videos in most browsers.

The streaming protocol appears to be the most efficient way to host and deliver video.

The video encoder has a segmenter which divides the video content into fragments of equal length. Each fragment is roughly ten seconds of video. The web browser then requests the fragments as they are needed. The segmenter also creates an index file containing fragmented files’ references, saved as .m3u8.

This article covers creating an m3u8 file and how to include it within your web pages. I’ll also cover some gotchas around CORS policies and how to provide a fallback option for older clients that don’t support HLS.

 Demonstration

 Converting an MP4 video into an HLS m3u8

You’ll need to start by converting your MP4 video into an HLS m3u8 format. Your video editor might be able to export in the HLS format natively. If it doesn’t support this format, then the open-source video convertor ffmpeg can perform the conversion using the following command:

ffmpeg -i ./input.mp4
    -profile:v baseline
    -level 3.0 
    -start_number 0 
    -hls_time 10 
    -hls_list_size 0 
    -f hls body-chart.m3u8

 Pulling a frame from the video to act as the poster

We can also use ffmpeg to extract a single frame from the video to act as our poster for the HTML 5 video container. The example below shows how to extract the 34th frame from the video and save it to a PNG file.

ffmpeg -i input.mp4 -vf "select=eq(n\,34)" -vframes 1 poster.png

Adjust this value to extract the frame you want. For example, if your video is one hundred and twenty seconds long and recorded at twenty-five frames per second, if you want to extract a frame at the centre of the video, the value would be 1,500. (120x25)/2.

If you want an automatic way to generate video thumbnails using ffmpeg then read this post.

 HTML5 Video Container

Ideally, you want to host your HLS video segments and the fallback MP4 on a CDN (Content Distribution Network). Using a CDN means your videos are distributed across the globe, making them closer to the user’s location. I use AWS’s CloudFront service as my CDN.

Once we have our video files hosted onto a CDN, we can define the HTML 5 video container within our web page.

<video 
    id="video"
    controls="controls"
    preload="none"
    poster="https://du89og34n262.cloudfront.net/bodychart/poster.png"
    width="100%">
</video>

As you can see from the example above, I’m defining that the video player should have playback controls and specifying the URL for the poster image. However, I’m not defining the URL’s of the videos. To do this, we’ll use JavaScript to define a fallback if the user’s browser doesn’t support HLS.

 Using HLS.js

Not all browsers support HLS, so we need to use hls.js, a JavaScript library that implements an HTTP Live Streaming client. Once we’ve included the script into our web page like:

<script type="text/javascript" src="js/hls.js"></script>

Then we write the JavaScript to detect if HLS is supported in the user’s browser. If the video format is supported, then we use the loadSource function to load the URL for the HLS version of the file. We then attach the media to the video container using its unique ID to find it on our web page.

var video = document.getElementById('video');
var videoHLS = 'https://du89og34n262.cloudfront.net/video/bodychart/body-chart.m3u8';
var videoMP4 = 'https://du89og34n262.cloudfront.net/video/bodychart/body-chart.mp4';

if (Hls.isSupported()) {
    var hls = new Hls();
    hls.loadSource(videoHLS);
    hls.attachMedia(video);
}

 Creating a fallback for older clients

To add fallback for older clients/browsers, we create a function called addSourceToVideo, which takes three parameters. The first is the video container ID, and the second is the source URL of the fallback video. The third is the mime type for the fallback video.

The function then appends the source to the video container.

function addSourceToVideo(element, src, type) {
    var source = document.createElement('source');
    source.src = src;
    source.type = type;
    element.appendChild(source);
}

var video = document.getElementById('video');
var videoHLS = 'https://du89og34n262.cloudfront.net/video/bodychart/body-chart.m3u8';
var videoMP4 = 'https://du89og34n262.cloudfront.net/video/bodychart/body-chart.mp4';

if (Hls.isSupported()) {
    var hls = new Hls();
    hls.loadSource(videoHLS);
    hls.attachMedia(video);
} else {
    addSourceToVideo(video, videoMP4, 'video/mp4');
}

Within our isSupported() check, we can now add an else statement to call this new function if HLS video is not supported.

 CSP Content Security Policy

If you’re using a content security policy for your website, you’ll need to add the blob: keyword to the script-src and default-src sections. You’ll also need to add the base URL to your CDN. Below is a copy of the content security policy I’m using for this website.

Content-Security-Policy "
    default-src 'self' blob: data: https://du89og34n262.cloudfront.net https://www.youtube-nocookie.com;
    font-src    'self';
    style-src   'self' 'unsafe-inline';
    script-src  'self' blob: 'unsafe-inline' 'unsafe-eval' https://www.youtube-nocookie.com;
    connect-src 'self' https://du89og34n262.cloudfront.net;"

 JavaScript disabled

As this solution relies on JavaScript to attach the video files to the video container, if a user has JavaScript disabled, they won’t be able to play the video. As such, you might want to consider having a noscript tag which contains a helpful message and a link to the MP4 version of the video. This will allow the user to click on the link and watch the video, even if they have JavaScript disabled.

<noscript>
    <p><i>As you have JavaScript disabled, the video can't be played directly.
    However, you can <a href="https://du89og34n262.cloudfront.net/video/bodychart/body-chart.mp4">watch the video here</a>.</i><p>
</noscript>

<video
    id="video"
    controls="controls"
    preload="none"
    poster="https://du89og34n262.cloudfront.net/bodychart/poster.png"
    width="100%">
</video>