The Hidden Challenges of Video Streaming: Why Safari Refuses to Play Along
As I implemented AI-powered video tagging in my product, Sortal, I thought video streaming would be a breeze. After all, it’s just a few lines of code, right? But when I tested it in Safari, I discovered the harsh reality: video streaming is simple for Chrome, but not so much for Safari.
Try it for Yourself
Before we dive into the code, try it out for yourself! The code accompanying this post is available on GitHub. Download or clone the repository, install Node.js, and start the server as instructed in the readme. Navigate your browser to http://localhost:3000, and you’ll see either Figure 1 (Chrome) or Figure 2 (Safari). Notice how the video on the left side doesn’t work in Safari?
The Basic Form of Video Streaming
The basic form of video streaming that works in Chrome is surprisingly simple. We’re simply streaming the entire video file from the backend to the frontend, as illustrated in Figure 3.
The Frontend
To render a video in the frontend, we use the HTML5 video element. It’s straightforward, as shown in Listing 1. This version only works in Chrome.
The Backend
The backend is a simple HTTP server built on Express framework running on Node.js, as shown in Listing 2. This is where the /works-in-chrome route is implemented. In response to the HTTP GET request, we stream the whole file to the browser, setting various HTTP response headers along the way.
But Safari Refuses to Play Along
Unfortunately, Safari doesn’t want the entire file delivered in one go. It wants to stream portions of the file so that it can be incrementally buffered in a piecemeal fashion. It also wants random, ad hoc access to any portion of the file that it requires.
The Solution: Supporting HTTP Range Requests
To make it work in Safari, we need to modify our HTTP server to support HTTP range requests. Specifically, we need to implement the HTTP request range header that starts with the prefix “bytes=”.
Parsing the HTTP Range Header
We can parse the value for this header to obtain starting and ending values for the range of bytes, as shown in Listing 3.
Responding to the HTTP HEAD Request
We need to take care when handling the HTTP HEAD request, as shown in Listing 4. We don’t have to return any data from the file, but we do have to configure the response headers to tell the frontend that we’re supporting the HTTP range request and to let it know the full size of the video file.
Full File vs. Partial File
Now for the tricky part: are we sending the full file or a portion of the file? With some care, we can make our request handler support both methods, as shown in Listing 5.
Sending Status Code and Response Headers
We’ve dealt with the HEAD request. All that’s left is to handle the HTTP GET request, as shown in Listing 6.
Streaming a Portion of the File
The easiest part: streaming a portion of the file, as shown in Listing 7.
Putting it All Together
Let’s put all the code together into a complete request handler for streaming video that works in both Chrome and Safari, as shown in Listing 8.
Updated Frontend Code
Nothing needs to change in the frontend code besides making sure the video element is pointing to an HTTP route that can handle HTTP range requests, as shown in Listing 9.
The Takeaway
Video streaming might seem simple at first, but it’s quite a bit more difficult to figure out for Safari. Lucky for you, I’ve already trodden that path, and this post has laid the groundwork for your own streaming video implementation.