Lambda Function to Insert Ads with Consuo
Tags: aws, javascript, hlssplice, streaming
Published: 2020-06-05 07:34:51

With Consuo you can create virtual linear TV channels from the video on demand files that you already have on your CDN and in this post we will describe how you can write a simple Lambda-function to insert ads between the programs in the channel.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ip3zau3738ii155s40sn.png)

A Lambda function in this case is a code snippet that is running in the cloud without you having to think about servers. Both Amazon Web Service, Azure and Heroku offers this functionality and in this blog post we will be using the service provided by AWS that is called AWS Lambda.

We will create a code snippet that will be given a URI to an HLS packaged video on demand file on the CDN and will rewrite the manifest of this HLS file and insert markers and ad segments. This modified manifest will then be scheduled in Consuo. Consuo preserves these ad markers in the virtual linear TV stream which means that by using a server-side ad replacer we can replace these ads with more targeted ads for a specific user.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2u5ggrcfwy62ffb5q923.png)

## How it works

We will create an endpoint that will respond with the modified HLS manifest and what will be served to a video player, or in our case, Consuo. As a query parameter you provide a base64 encoded payload with the location of the source VOD and the locations of the ads. Actually we will need two endpoints. One to handle the master manifest and another for each media manifest.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/cocdm39vlgvisrc6ya65.png)

The process will be as follows:
0. Consuo (or a general video player) will be given an URI to the Lambda endpoint together with the base64 encoded JSON payload.
1. Consuo fetches the master manifest from the Lambda endpoint. The Lambda endpoint will return a master manifest where the media manifest are pointing to the Lambda endpoint together with the base64 encoded payload.
2. To start playing the media manifests are now fetched from this Lambda endpoint.
3. When a media manifest request is handled the Lambda function will retrieve the original media manifest from the VOD CDN and parse it. It will then manipulate this media manifest and insert the ad segments, adding ad markers and other necessary HLS tags.
4. It then returns this manipulated media manifest to Consuo or the video player. As it is only the manifest files that are manipulated the video segments will be still fetched directly from the CDN and not through this Lambda.

## The Code

The code to handle a master manifest request is shown below. Full [Source Code](https://github.com/Eyevinn/lambda-stitch) is available on GitHub. 

```javascript
const handleMasterManifestRequest = async (event) => {
  try {
    const encodedPayload = event.queryStringParameters.payload;
    console.log(`Received request /master.m3u8 (payload=${encodedPayload})`);
    const manifest = await getMasterManifest(encodedPayload);
    const rewrittenManifest = await rewriteMasterManifest(manifest, encodedPayload);
    return generateManifestResponse(rewrittenManifest);
  } catch (exc) {
    console.error(exc);
    return generateErrorResponse(500, "Failed to generate master manifest");
  }
};
```
And as previously mentioned it returns a rewritten master manifest where the media manifest locations from the original manifest has just been replaced to point to this Lambda endpoint instead.

To handle the media manifest request we will have these lines of code:

```javascript
const handleMediaManifestRequest = async (event) => {
  try {
    const bw = event.queryStringParameters.bw;
    const encodedPayload = event.queryStringParameters.payload;
    console.log(`Received request /media.m3u8 (bw=${bw}, payload=${encodedPayload})`);
    const hlsVod = await createVodFromPayload(encodedPayload, { baseUrlFromSource: true, subdir: event.queryStringParameters.subdir });
    const mediaManifest = (await hlsVod).getMediaManifest(bw);
    return generateManifestResponse(mediaManifest);
  } catch (exc) {
    console.error(exc);
    return generateErrorResponse(500, "Failed to generate media manifest");
  }
};
```

And the interesting function here is the `createVodFromPayload()` which we can have a look into.

```javascript
const createVodFromPayload = async (encodedPayload, opts) => {
  const payload = deserialize(encodedPayload);

  const uri = payload.uri;
  let vodOpts = {
    merge: true
  };
  if (opts && opts.baseUrlFromSource) {
    const m = uri.match('^(.*)/.*?');
    if (m) {
      vodOpts.baseUrl = m[1] + "/";
    }
    if (opts.subdir) {
      vodOpts.baseUrl += opts.subdir + "/";
    }
  }

  const hlsVod = new HLSSpliceVod(uri, vodOpts);
  await hlsVod.load();
  adpromises = [];
  for (let i = 0; i < payload.breaks.length; i++) {
    const b = payload.breaks[i];
    adpromises.push(() => hlsVod.insertAdAt(b.pos, b.url));
  }
  for (let promiseFn of adpromises.reverse()) {
    await promiseFn();
  }
  return hlsVod;
};
```
It utilizes the open source library [@eyevinn/hls-splice](https://www.npmjs.com/package/@eyevinn/hls-splice) to actually do the manifest manipulation.

## Making it available

We now have the code for the Lambda function and once we have created a Lambda function in AWS we now need to make this available.

To do that we need to add an Application Load Balancer where HTTP requests on port 80 are forwarded to a target group of type Lambda.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/rkww2rxdqibqx74yudub.png)

Requests that are coming from the ALB are then handled in the main entry function of the Lambda.

```javascript
exports.handler = async event => {
  let response;

  if (event.path === "/stitch/" && event.httpMethod === "POST") {
    response = await handleCreateRequest(event);
  } else if (event.path === "/stitch/master.m3u8") {
    response = await handleMasterManifestRequest(event);
  } else if (event.path === "/stitch/media.m3u8") {
    response = await handleMediaManifestRequest(event);
  } else {
    response = generateErrorResponse({ code: 404 });
  }

  return response;
};
```

## Example

Here is an example with a pre-roll ad: `http://lambda.eyevinn.technology/stitch/master.m3u8?payload=eyJ1cmkiOiJodHRwczovL21haXR2LXZvZC5sYWIuZXlldmlubi50ZWNobm9sb2d5L3N0c3dlMTctd2lsbGxhdy5tNHYvbWFzdGVyLm0zdTgiLCJicmVha3MiOlt7InBvcyI6MCwiZHVyYXRpb24iOjE2MDAwLCJ1cmwiOiJodHRwczovL21haXR2LXZvZC5sYWIuZXlldmlubi50ZWNobm9sb2d5L2Fkcy82Y2Q3ZDc2OC1lMjE0LTRlYmMtOWYxNC03ZWQ4OTcxMDExNWUubXA0L21hc3Rlci5tM3U4In1dfQ==` and decoding the base64 show you the payload.

```json
{"uri":"https://maitv-vod.lab.eyevinn.technology/stswe17-willlaw.m4v/master.m3u8","breaks":[{"pos":0,"duration":16000,"url":"https://maitv-vod.lab.eyevinn.technology/ads/6cd7d768-e214-4ebc-9f14-7ed89710115e.mp4/master.m3u8"}]}
```

Then we can use the example above and put it in a schedule in Consuo which could look like this.

```json
[
...
 {
    "channelId": "eyevinn",
    "assetId": "urn:uuid:ee16c6bf-70b9-4246-9b70-b132b706beda",
    "eventId": "dd02e9ea-0ec4-4d26-9e6c-12a85e762c65",
    "id": "urn:uuid:ee16c6bf-70b9-4246-9b70-b132b706beda",
    "title": "STSWE17 Will Law",
    "start_time": 1591335742455,
    "end_time": 1591337431455,
    "start": "2020-06-05T05:42:22.455Z",
    "end": "2020-06-05T06:10:31.455Z",
    "uri": "http://lambda.eyevinn.technology/stitch/master.m3u8?payload=eyJ1cmkiOiJodHRwczovL21haXR2LXZvZC5sYWIuZXlldmlubi50ZWNobm9sb2d5L3N0c3dlMTctd2lsbGxhdy5tNHYvbWFzdGVyLm0zdTgiLCJicmVha3MiOlt7InBvcyI6MCwiZHVyYXRpb24iOjE2MDAwLCJ1cmwiOiJodHRwczovL21haXR2LXZvZC5sYWIuZXlldmlubi50ZWNobm9sb2d5L2Fkcy9iYzU1ZmZkYy1kMDcxLTQ4NTgtYTk3ZC1jMjI5M2YwNTlmMTkubXA0L21hc3Rlci5tM3U4In1dfQ==",
    "duration": 1689
  },
...
]
```

## Summary

Adding this Lambda function you can generate virtual linear TV channels with ads and using this in combination with a server-side ad inserter you can have linear TV channels with individually targeted ads. If you want to know more about Consuo visit [www.consuo.tv](https://www.consuo.tv) and request a free 30-days trial.