Inserting HLS Interstitials for an existing VOD library
Published: 2021-11-10 16:16:03

To make it easier to create and deliver features such as bumpers and mid-roll ads, Apple added support for [interstitials](https://developer.apple.com/streaming/GettingStartedWithHLSInterstitials.pdf) in their streaming protocol HLS. This feature also intends to solve late binding to current ad inventory and dynamic scheduling.

An HLS interstitial is a new class of EXT-X-DATERANGE tags that can be added to the media playlist (the one containing the list of media segments) of the primary content. It refers to a self-contained media asset which does not have to match the bitrate ladder of the primary content, though it is recommended. User navigation restrictions such as to prevent skipping and fast-forwarding an ad can be signalled.

Interstitial playback is available in AVFoundation from iOS 15, iPadOS 15 and macOS 12+. Note that Safari and HTML5 player does not support interstitial playback yet. Players that does not support interstitial playback will simply just ignore these tags. Below is an example of an interstitial in an HLS media playlist.


```
#EXTM3U
#EXT-X-TARGETDURATION:6
#EXT-X-PROGRAM-DATE-TIME:2020-01-02T21:55:40.000Z
#EXTINF:6,
main1.0.ts
#EXT-X-ENDLIST
#EXT-X-DATERANGE:ID="ad1",CLASS="com.apple.hls.interstitial",START- DATE="2020-01-02T21:55:44.000Z",DURATION=15.0,X-ASSET-URI="http://example.com/ad1.m3u8",X-RESUME-OFFSET=0, X-RESTRICT="SKIP,JUMP",X-COM-EXAMPLE-BEACON=123
```

The `START-DATE` attribute of the `EXT-X-DATERANGE` indicates when the player should schedule playback of the interstitial. In the above example 4 seconds after the primary content started and should continue playing the primary content where it left off.

## Adding interstitials to existing assets
To avoid having to repackage and create new media playlists when adding interstitials to existing assets in your content library we have extended the HLS manifest manipulation library [@eyevinn/hls-splice](https://github.com/Eyevinn/hls-splice) with the support to insert an interstitial instead of "splicing" in the ad segments.

```javascript
const HLSSpliceVod = require("@eyevinn/hls-splice");

const run = async () => {
  const hlsVod = new HLSSpliceVod("https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8");
  await hlsVod.load();
  hlsVod.insertInterstitialAt(4000, "ad1", "http://example.com/ad1.m3u8", false, { resumeOffset: 0 });

  const playlist = hlsVod.getMediaManifest(1252000);
  console.log(playlist);
}
run();
```
The above example would print out a similar playlist as in the example playlist further up.

This HLS manifest manipulation library can rewrite a media playlist and insert an interstitial. But to actually implement this we need a service that will provide this manipulated playlist. There are different ways this can be done, either using the [@eyevinn/hls-proxy](https://github.com/Eyevinn/hls-proxy) library or using an AWS Lambda function with an application gateway. An option we described in this [blog post](https://dev.to/video/aws-lambda-function-to-insert-pre-roll-ads-in-hls-stream-4gip).

The principle here is that when requesting a media playlist from this service you provide a payload including the URI to the primary content and an array of interstitials. The payload can for example be a base64 encoded JSON:

```json
{
      "uri": "https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8",
      "breaks": [
        { "pos": 0, "duration": 15000, "url": "https://maitv-vod.lab.eyevinn.technology/ads/apotea-15s.mp4/master.m3u8" },
        { "pos": 55000, "duration": 15000, "url": "https://maitv-vod.lab.eyevinn.technology/ads/apotea-15s.mp4/master.m3u8" }
      ]
}
```

Based on this payload and the hls-splice library it is easy to generate a rewritten media playlist.

```javascript
const hlsVod = new HLSSpliceVod(uri, vodOpts);
await hlsVod.load();
adpromises = [];
let id = payload.breaks.length + 1;
for (let i = 0; i < payload.breaks.length; i++) {
  const b = payload.breaks[i];
  const assetListPayload = {
    assets: [ { uri: b.url, dur: b.duration / 1000 }]
  };
  const encodedAssetListPayload = encodeURIComponent(serialize(assetListPayload));
  const baseUrl = process.env.ASSET_LIST_BASE_URL || "";
  const assetListUrl = new URL(baseUrl + `/stitch/assetlist/${encodedAssetListPayload}`);
  adpromises.push(() => hlsVod.insertInterstitialAt(b.pos, `${--id}`, assetListUrl.href, true));
}
for (let promiseFn of adpromises.reverse()) {
  await promiseFn();
}
return hlsVod.getMediaManifest(bw)
```

As you find in the above code we provide an URL to an asset list endpoint. This endpoint will basically just take a base64 encoded list of interstitial assets and return a JSON according to the HLS interstitial specification. Instead, we could have pointed to an endpoint that generates this list dynamically based on ad inventory for example. Then the decision is taken just in time and not on media playlist request.

## AWS Lambda that generates example streams
If you need example streams with HLS interstitial tags we have developed an AWS Lambda function that uses the manifest manipulation technique to accomplish this. The [source code is available as open source](https://github.com/Eyevinn/lambda-stitch).

- [Pre-roll](https://lambda-ssl.eyevinn.technology/stitch/master.m3u8?payload=ewogICAgICAidXJpIjogImh0dHBzOi8vbWFpdHYtdm9kLmxhYi5leWV2aW5uLnRlY2hub2xvZ3kvVklOTi5tcDQvbWFzdGVyLm0zdTgiLAogICAgICAiYnJlYWtzIjogWwogICAgICAgIHsgInBvcyI6IDAsICJkdXJhdGlvbiI6IDE1MDAwLCAidXJsIjogImh0dHBzOi8vbWFpdHYtdm9kLmxhYi5leWV2aW5uLnRlY2hub2xvZ3kvYWRzL2Fwb3RlYS0xNXMubXA0L21hc3Rlci5tM3U4IiB9CiAgICAgIF0KfQ==&i=1)
- [Pre-roll & mid-roll](https://lambda-ssl.eyevinn.technology/stitch/master.m3u8?payload=ewogICAgICAidXJpIjogImh0dHBzOi8vbWFpdHYtdm9kLmxhYi5leWV2aW5uLnRlY2hub2xvZ3kvVklOTi5tcDQvbWFzdGVyLm0zdTgiLAogICAgICAiYnJlYWtzIjogWwogICAgICAgIHsgInBvcyI6IDAsICJkdXJhdGlvbiI6IDE1MDAwLCAidXJsIjogImh0dHBzOi8vbWFpdHYtdm9kLmxhYi5leWV2aW5uLnRlY2hub2xvZ3kvYWRzL2Fwb3RlYS0xNXMubXA0L21hc3Rlci5tM3U4IiB9LAogICAgICAgIHsgInBvcyI6IDU1MDAwLCAiZHVyYXRpb24iOiAxNTAwMCwgInVybCI6ICJodHRwczovL21haXR2LXZvZC5sYWIuZXlldmlubi50ZWNobm9sb2d5L2Fkcy9hcG90ZWEtMTVzLm1wNC9tYXN0ZXIubTN1OCIgfQogICAgICBdCn0=&i=1)
- [Pre-roll with resume-offset 5 seconds](https://lambda-ssl.eyevinn.technology/stitch/master.m3u8?payload=ewogICAgICAidXJpIjogImh0dHBzOi8vbWFpdHYtdm9kLmxhYi5leWV2aW5uLnRlY2hub2xvZ3kvVklOTi5tcDQvbWFzdGVyLm0zdTgiLAogICAgICAiYnJlYWtzIjogWwogICAgICAgIHsgInBvcyI6IDAsICJybyI6IDUwMDAsICJkdXJhdGlvbiI6IDE1MDAwLCAidXJsIjogImh0dHBzOi8vbWFpdHYtdm9kLmxhYi5leWV2aW5uLnRlY2hub2xvZ3kvYWRzL2Fwb3RlYS0xNXMubXA0L21hc3Rlci5tM3U4IiB9CiAgICAgIF0KfQ==&i=1)
- [Pre-roll with 10 seconds playout limit](https://lambda-ssl.eyevinn.technology/stitch/master.m3u8?payload=ewogICAgICAidXJpIjogImh0dHBzOi8vbWFpdHYtdm9kLmxhYi5leWV2aW5uLnRlY2hub2xvZ3kvVklOTi5tcDQvbWFzdGVyLm0zdTgiLAogICAgICAiYnJlYWtzIjogWwogICAgICAgIHsgInBvcyI6IDAsICJwb2wiOiAxMDAwMCwgImR1cmF0aW9uIjogMTUwMDAsICJ1cmwiOiAiaHR0cHM6Ly9tYWl0di12b2QubGFiLmV5ZXZpbm4udGVjaG5vbG9neS9hZHMvYXBvdGVhLTE1cy5tcDQvbWFzdGVyLm0zdTgiIH0KICAgICAgXQp9&i=1)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wda39l3cxdp5ndw2jg0t.png)

To try it out in your application you can simply just do:

```swift
import SwiftUI
import AVKit

struct ContentView: View {
    var body: some View {
        VideoPlayer(player: AVPlayer(url: URL(string: "https://lambda-ssl.eyevinn.technology/stitch/master.m3u8?payload=ewogICAgICAidXJpIjogImh0dHBzOi8vbWFpdHYtdm9kLmxhYi5leWV2aW5uLnRlY2hub2xvZ3kvVklOTi5tcDQvbWFzdGVyLm0zdTgiLAogICAgICAiYnJlYWtzIjogWwogICAgICAgIHsgInBvcyI6IDAsICJkdXJhdGlvbiI6IDE1MDAwLCAidXJsIjogImh0dHBzOi8vbWFpdHYtdm9kLmxhYi5leWV2aW5uLnRlY2hub2xvZ3kvYWRzL2Fwb3RlYS0xNXMubXA0L21hc3Rlci5tM3U4IiB9CiAgICAgIF0KfQ==&i=1")!))
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```

Interstitial playback on Apple devices is executed by having two video players. One that plays the primary asset and another player (created automatically) for playback of the interstitial asset. This model can be tricky to implement seamlessly on all devices so for some platforms you might have to have a fallback server-side inserted stream. Something that can be combined into one single stream and using the `X-RESUME-OFFSET` attribute to have the player to skip the server-side inserted segments. An example and extension to the above Lambda might come in a follow-up article. But for now, happy interstitial hacking!

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