Open source HLS proxy library for manifest manipulation
Tags: opensource, node, programming, videostreaming
Published: 2021-11-03 12:56:04

There are a number of use cases where HLS manifest manipulation techniques are applicable. Multi CDN switching, server-side ad insertion, looping or truncating a VOD, to mention a few. The fundamental principle is that you need some HLS proxy between the video player and the original HLS you wish to modify somehow. As this is a very common component in all these use cases we have developed and open sourced an NPM library, [hls-proxy](https://github.com/Eyevinn/hls-proxy), that takes care of that. In this post we will describe how to use it and examples of some use cases.

## Get started

```
npm install --save @eyevinn/hls-proxy
```

Once installed in your NodeJS project you can start by creating a `server.js` that could look like this:

```javascript
const { HLSProxy } = require("@eyevinn/hls-proxy");

const handlers = {...};
const proxy = new HLSProxy(handlers);
proxy.listen(process.env.PORT || 8000);
```

The proxy server is built on [fastify.io](https://www.fastify.io).

The handlers object implements the following interface:
```javascript
interface IHandlers {
  originHandler: (request: IProxyRequest) => Promise<string>;
  masterManifestHandler?: (request: IProxyRequest, baseUrl: URL, m3u: any) => Promise<string>;
  mediaManifestHandler?: (request: IProxyRequest, baseUrl: URL, m3u: any) => Promise<string>;
  segmentRedirectHandler?: (request: IProxyRequest, baseUrl: URL) => Promise<string>;
}
```
### `originHandler`
This handler is called on every request handled by the proxy. The function implementing this handler is expected to return the base URL for where the proxy can fetch the original manifest. For example if the video player requests `http://<proxy>/VINN.mp4/master.m3u8` and the `originHandler` returns `https://maitv-vod.lab.eyevinn.technology` the proxy will fetch the master manifest from `https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8`.

### `masterManifestHandler`
This handler is called after the proxy has fetched the master manifest from the origin. It provides the original request received by the proxy, base URL set by the `originHandler` and a parsed M3U object ([@eyevinn/m3u8](https://github.com/Eyevinn/node-m3u8)). It is expected to return an HLS master manifest.

### `mediaManifestHandler`
Similar to the `masterManifestHandler` this is called after the proxy has fetched the manifest from the origin. In this case when the media manifest is retrieved. Also in this case a parsed M3U object is provided to the handler. The handler is expected to return an HLS media manifest.

### `segmentRedirectHandler`
Assuming that the segment URLs are not pointing directly to another server the proxy will receive all requests for segments. If the segment URLs are not rewritten by the `mediaManifestHandler` this handler needs to be implemented. It is expected to return the URL for where the segment can be found. The proxy will respond with a 302 redirect to the video player.

# Example use cases

We will present some example use cases where this proxy can be used.

## Multi CDN switching
There might be several reasons to have a multi CDN strategy. It could be cost related, reliability related or just pure business rules related. What ever the reason using an HLS proxy and manifest manipulation is one way of switching between the CDNs. The first example below shows how you can do that with this HLS proxy-library using 302 segment redirects. That gives you the possibility to switch in the middle of the stream even when playing a VOD (the media manifest is only fetched once).

```javascript
const { HLSProxy } = require("@eyevinn/hls-proxy");

const cdnSelector = () => {
  // Make decision on which CDN that is best to use here
  return "https://maitv-vod.lab.eyevinn.technology";
};

const proxy = new HLSProxy({
  originHandler: async () => {
    return cdnSelector();
  },
  segmentRedirectHandler: async (request, baseUrl) => {
    const redirectUrl = new URL(request.raw.url, baseUrl);
    return redirectUrl.href;
  }
});
proxy.listen(8000);

// Example: http://localhost:8000/VINN.mp4/master.m3u8
```

Another example that instead rewrites the media manifest have the benefit that the proxy does not have to handle each segment request by the video player, thus reducing the load a bit.

```javascript
const { HLSProxy } = require("@eyevinn/hls-proxy");

const cdnSelector = () => {
  // Make decision on which CDN that is best to use here
  return "https://maitv-vod.lab.eyevinn.technology";
};

const proxy = new HLSProxy({
  originHandler: async () => {
    return cdnSelector();
  },
  mediaManifestHandler: async (request, baseUrl, m3u) => {
    // rewrite segment URLs to point to chosen CDN
    m3u.items.PlaylistItem.map(item => {
      const newSegmentUri = new URL(request.basePath + item.get("uri"), baseUrl.href);
      item.set("uri", newSegmentUri.href);
    });
    return m3u.toString();
  }
});
proxy.listen(8000);

// Example: http://localhost:8000/VINN.mp4/master.m3u8
```

## Looping a VOD
Let us say that you have a VOD as a slate that you use to fill gaps in a schedule. Instead of having a set of slates with different durations you can have a 3 seconds slate that you loop instead. This is something that also can be achieved using HLS proxy and manifest manipulation.

```javascript
// Example (2 reps): http://localhost:8000/slate-consuo2.mp4/master.m3u8?r=2

const { HLSProxy } = require("@eyevinn/hls-proxy");
const HLSRepeatVod = require("@eyevinn/hls-repeat");

const proxy = new HLSProxy({
  originHandler: async () => {
    // Origin where the VOD is found
    return "https://maitv-vod.lab.eyevinn.technology";
  },
  masterManifestHandler: async (request, baseUrl, m3u) => {
    const repeats = request.raw.query["r"] || 2; 
    m3u.items.StreamItem.map(item => {
      const params = require("querystring").stringify({
        bw: item.get("bandwidth"),
        r: repeats,
        src: request.raw.url
      });
      item.set("uri", item.get("uri") + "?" + params);
    });
    return m3u.toString();
  },
  mediaManifestHandler: async (request, baseUrl, m3u) => {
    const sourceUrl = new URL(request.raw.query["src"], baseUrl);
    const hlsVod = new HLSRepeatVod(sourceUrl.href, request.raw.query["r"]);
    await hlsVod.load();
    return hlsVod.getMediaManifest(request.raw.query["bw"]);
  },
  segmentRedirectHandler: async (request, baseUrl) => {
    return (new URL(request.raw.url, baseUrl)).href;
  }
});
proxy.listen(8000);
```
In addition to the HLS proxy library this example uses an HLS [manifest manipulation library](https://github.com/Eyevinn/hls-repeat) that creates a new HLS VOD by repeating the contents of another HLS VOD.

## Inserting a bumper or ad
Inserting a bumper or pre-roll ad is another use case for the HLS proxy but we are leaving that as an exercise for the reader.

All above mentioned code and libraries are available as open source. More tools and libraries that are open source can be found on our [GitHub](https://github.com/Eyevinn).

*If you need assistance in the development and implementation of this, our [team of video developers](https://video-dev.team) are happy to help you out. If you have any questions or comments just drop a line in the comments section to this post.*