Skip to main content
  1. Posts/

Serving Videos with Golang via HLS

··1198 words·6 mins
Author
Hairizuan Noorazman
Software engineering experiments, implementation notes, and lessons learned.
Table of Contents

I was watching a bunch of tiktok and youtube videos recently and kind of started to wonder how such companies serve videos to their consumers. That is where I started to going down the rabbit hole of how videos are served and how to try to ensure the possibility that videos can be played without requiring to download the entire video.

Apparently, one of the technologies that was mentioned for streaming videos from server to a consumer device was HLS. HLS stands for HTTP Live Streaming. Although we’re not exactly doing “live streaming” if we’re just attempting to serve video - however, if we were to think twice about it, we’re kind of “streaming” the contents of the video to the user. We would want the user to be able to consume the content even before the entire video is loaded.

To do this HLS thing, we can rely on the usual video manipulation tool - ffmpeg. In order to get the HLS form of the video, we would need to re-encode the video to the HLS format; this blog post will mention an example command that can be used for this. The HLS format consists of a single file that would provide a list of all files that would point to the various video segments of the entire video. The reason for breaking this up the file into smaller video segments is to allow the consumer device to download a smaller piece of content and start playing without requiring to download the entire video. Downloading large files over the internet is usually not the best thing for a app/websites - smaller files usually work way better; if there is any broken connections, it would still be possible to restart downloads of small video chunks. At the same time, with the HLS format, we would be able to download maybe a couple of video chunks and immediately start playing the video.

This blog post won’t cover on how one can obtain ffmpeg on their workstation. But if you’re on a Mac, you can probably get it by utilizing brew.

The next step would be utilize the ffmpeg tool to convert the target video which we wish to serve to a “HLS” format. The HLS format of video consist of a m3u8 file which serves to be some sort of “manifest” file. This is the “single file” that point to the various video segments. This is the command that would help to do so:

ffmpeg -i sample.mkv -c:a copy -f hls -hls_playlist_type vod output.m3u8

Let’s cover the effects of some of the flags being used above:

Once we have done with running the command - we would generate the files. However, how shall we test that the video has been encoded to the hls format. We can do so by utilizing some common video player (in my case, I usually use VLC media player) and point it to some server that would be a file server to serve the m3u8 file as well as the ts video segment files.

The golang code to do so is available here:

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.Handle("/", http.FileServer(http.Dir(".")))
	fmt.Printf("Starting server on %v\n", 8080)
	log.Printf("Serving %s on HTTP port: %v\n", ".", 8080)

	// serve and log errors
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", 8080), nil))
}

For some guides on how to play m3u8 based videos, refer to the following reference: https://www.5kplayer.com/vlc/m3u8-vlc.htm

The next step after this is to try to render the video on a html page served by the Golang server. Unfortunately, a naive approach of using the <video> html5 tag doesn’t work in Chrome browsers. Surprisingly, HLS is not natively supported in Chrome browsers - there are formats that are properly supported but a deeper dive is needed to find out how those work and how we can utilize ffmpeg to generate said video streams. We have to use javascript based solutions to provide said functionality on Chrome.

One might argue - why not just develop for Chrome? Unfortunately, the Chrome browser is still one of the more popular browsers in common use. At the same, other browsers such as the edge browser are based of the Chromium project - it is pretty safe to assume that if chrome doesn’t support HLS formats natively, then, other chromium based browsers wouldn’t provide such support as well.

For the following quick example, I decided to go with video.js as it is one of the libraries that provide a “working” example (also, quite a number of reputable video based companies are listed on its website.). With that, we can then include serving of a html page with video.js javascript functionality.

This would be the Golang server code:

package main

import (
	"fmt"
	"html/template"
	"log"
	"net/http"
)

type VideoServe struct {
}

func (h VideoServe) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	t, _ := template.ParseFiles("aaa.html")
	t.Execute(w, nil)
}

func main() {
	http.Handle("/yoyo", VideoServe{})
	http.Handle("/", http.FileServer(http.Dir(".")))
	fmt.Printf("Starting server on %v\n", 8080)
	log.Printf("Serving %s on HTTP port: %v\n", ".", 8080)

	// serve and log errors
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", 8080), nil))
}

For the html page named aaa.html that would be served by the Golang server coded above.

<html>
  <head>
    <title>Hls.js demo - basic usage</title>
    <script src="//cdn.jsdelivr.net/npm/hls.js@latest"></script>
  </head>

  <body>

    <center>
      <h1>Hls.js demo - basic usage</h1>
      <video height="600" id="video" controls></video>
    </center>

    <script>
      var video = document.getElementById('video');
      if (Hls.isSupported()) {
        var hls = new Hls({
          debug: true,
        });
        // hls.loadSource('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8');
        hls.loadSource('http://localhost:8080/output.m3u8');
        hls.attachMedia(video);
        hls.on(Hls.Events.MEDIA_ATTACHED, function () {
          video.muted = true;
          video.play();
        });
      }
      // hls.js is not supported on platforms that do not have Media Source Extensions (MSE) enabled.
      // When the browser has built-in HLS support (check using `canPlayType`), we can provide an HLS manifest (i.e. .m3u8 URL) directly to the video element through the `src` property.
      // This is using the built-in support of the plain video element, without using hls.js.
      else if (video.canPlayType('application/vnd.apple.mpegurl')) {
        // video.src = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';
        video.src = 'http://localhost:8080/output.m3u8';
        video.addEventListener('canplay', function () {
          video.play();
        });
      }
    </script>
  </body>
</html>

With that, we should be able to stream some sort of video from our Golang server to our browser. However, we would probably need to research further into it; there are many other things that we would need to take note and attempt to fix for: one of which would be to do the encoding ffmpeg job on demand - the example above requires us to encode an other video and have the transcoded videos available before the user accesses it. That would require unnecessary space on our part (especially if storage space is tight in the first place)

I’ll probably look to cover some of these in another blog post.

References
#

Related

Nginx as API Gateway - focusing on auth_request directive

··1245 words·6 mins
On virtual machine How to “protect” api requests https://www.nginx.com/blog/deploying-nginx-plus-as-an-api-gateway-part-1/ Mostly is the auth_request directive Microservices are a software architectural style that structures an application as a collection of loosely coupled, independently deployable services. Each service in a microservices architecture represents a specific business capability and communicates with other services through well-defined APIs (Application Programming Interfaces). These services are designed to be small, focused, and can be developed, deployed, and scaled independently. Its a somewhat common architectural pattern that many companies go to when it comes to scaling out their development teams to build out their product.

Multiple Database Support - MySQL and SQLite support

··722 words·4 mins
I intend to try out the Turso service in order to see if there is any other potential serverless database that would have pretty decent type of billing for small projects. There isn’t a proper SQL based database that can be billed in a similar way to the Cloud Run product - it’ll be great if the billing of the database product would be along the amount of data being stored or amount of read/write requests done for the data instead of the usual charged based on how long the instance being run (based on how Cloud SQL is billed).

Serverless Applications with Cloud Run with Serverless MySQL from PlanetScale

··806 words·4 mins
Serverless computing, as seen in platforms like Cloud Run or AWS Lambda, allows developers to run code without managing the underlying infrastructure. This is achieved by automatically scaling the resources based on the incoming requests, and users are billed based on the actual execution time and resources consumed during each function or container invocation.