AWS for M&E Blog

Playing your ads-featured content with AWS Elemental MediaTailor and VideoJS

In the previous installments of this blog series, we showed how to monetize your content with a custom workflow with FFmpeg on Amazon Elastic Container Service at Fargate and with Amazon Rekognition.

In this post, we show you how to display and interact with an HLS stream in a simple webpage similar to this.

 

sample HTML page displaying a player object and a list of ad breaks. The user can click on each item of the list to seek to the position of the respective ad. The player in the page displays a frame of Big Buck Bunny (c) 2008 Blender Foundation.

Sample HTML page with player and interactive list of ad breaks. Click on an item to seek to the position of an ad. The page displays a frame of Big Buck Bunny (c) 2008 Blender Foundation.

Prerequisites

To get the most out of this post, you need:

  1. A basic understanding of HTML and Javascript
  2. An AWS account to deploy your player on an Amazon Simple Storage Service (S3) hosted website.

Overview

AWS Elemental MediaTailor campaigns provide you a URL to an HLS manifest for your video content, featured with server-side inserted ads. This URL can be used by an HLS compatible player to display your ads-featured content.

In this blog post, you will

  • learn about the VMAP specifications that AWS Elemental MediaTailor uses to insert ads
  • create a player in a webpage able to display ads-featured content
  • fetch the position of the ad breaks from the VMAP file
  • generate clickable elements in the UI to skip to the ads position, so you can verify the ad insertion

Anatomy of a VMAP File

AWS Elemental MediaTailor uses VMAP files to include ads from your Ads Decision Server in your media content.

Here’s an example VMAP file that describes a simple ads inventory.

<?xml version="1.0" encoding="UTF-8"?>
<vmap:VMAP xmlns:vmap="http://www.iab.net/videosuite/vmap"version="1.0">
    <vmap:AdBreak timeOffset="00:05:04.125"breakType="linear" breakId="1">
        <vmap:AdSource id="ad-source-1"followRedirects="true">
            <vmap:AdTagURI templateType="vast3">
                <![CDATA[ ${ADS_URL} ]]>
            </vmap:AdTagURI>
        </vmap:AdSource>
    </vmap:AdBreak>
</vmap:VMAP>

The vmap:VMAP tag is the root of each VMAP document and represents the playlist of ads available for the media content. It may contain zero or more vmap:AdBreak tags, which represent a single ad break, but may include more than one ad.

The vmap:AdBreak tag lets you specify when an ad break should appear with the timeOffset parameter. In this example, we represent timeOffset in SMPTE format (hh:mm:ss.mmm), but other format options are available, such as percentage, start, or end position.

The breakType parameter can assume a combination of the following values, ordered by display preference:

  • linear – for video ads inserted in the media content
  • nonlinear – used to describe non-linear ads, overlays for example
  • display – used to describe companion ads outside of the player

breakId is an optional string parameter that can be used as unique identifier for the ad break.

Each vmap:AdBreak may contain up to one vmap:AdSource element. It provides the player with either an inline ad response or a reference to an ad response. In this example, we use the vmap:AdTagURI to provide a URI that references an ad response from another source, your Ads Decision Server for example. The URI must be contained within a CDATA block.

The VMAP specification offers many more configuration options for your ads inventory, but for the scope of this post we only consider the elements discussed so far. You can learn more about VMAP here.

Building the Player Web Page

In this section, we build a basic HTML page to display your content. We also download a VMAP file to gather the position of the ad-breaks and create clickable elements to seek to their position, so you can verify where the ads have been inserted.

Basic HTML

Begin by creating a basic HTML page and include VideoJS, an HTML5 web video player that supports HTML5 video and modern streaming formats. Create a file called index.html and paste the following code.

<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>AWS Elemental MediaTailor</title>

<link href="//vjs.zencdn.net/7.3.0/video-js.min.css"rel="stylesheet">
<script src="//vjs.zencdn.net/7.3.0/video.min.js"></script>

</head>
<body>
       <h1>My Ads Featured Video</h1>
</body>
</html>

The link tag includes the CSS styles required for VideoJS, while the script tag imports the necessary scripts.

Add the Player

Next, add and configure VideoJS player to the page with the tag <video>

<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>AWS Elemental MediaTailor</title>
  
<link href="//vjs.zencdn.net/7.3.0/video-js.min.css"
rel="stylesheet">
<script src="//vjs.zencdn.net/7.3.0/video.min.js"></script>

</head>
<body>
  <h1>My Ads Featured Video</h1>
  <video
     id="my_video" class="video-js"
     controls preload="auto"
     width="960" height="400"
     withCredentials="true"
   >
    <source src="${STREAM_URL}" >
  </video>
  
  <ul id="adsUl" />
  
</body>
</html>

The property id is a handler we use to control the player programmatically via Javascript, while class is set to “video-js” to style the element with the default VideoJS appearance. The controls toggle enables visual controls on the player. height and width set the respective display properties of the player. preload indicates what should be downloaded and displayed before the media is ready to be interacted with. Finally, the property withCredentials set to true enables storing and passing cookies to the server where manifests and segments are hosted on.

You can find more info on how to personalize your player experience here.

The source tag specifies what content the player should reproduce. The URL of the content can be specified with the property src. For example, try to play this stream by replacing ${STREAM_URL} with

https://171f6efea34043088c4d6017e788915a.mediatailor.eu-west-1.amazonaws.com/v1/master/17c705d483d32a0dc3058c7390c4e299d563b47c/episode3/playlist.m3u8

We also add a placeholder for the list of ad-breaks, the <ul /> tag. We populate this list programmatically when the webpage loads. The end user can click on each item to seek to the position of the ad. The positions of the ads are loaded from the VMAP file. You can download a sample VMAP file here.

Add functionality

Now it’s time to add functionality to the page with a script that will

  • download and parse the VMAP file
  • extract the SMPTE timestamps of the ad breaks in the vmap:AdBreak tags
  • convert the ads timestamps to seconds
  • for each ad, create a list element that when clicked skips to the position of the ad

Add the following snippet between the <ul /> tag and </body>.

<script>
    // 1. VMAP File URL
    const vmapUrl = '${VMAP_URL}';
  
    // 2. [hours, minutes, seconds] expressed in seconds
    const smpteConversion = [3600, 60, 1];
    
    // 3. get hold of the player
    const player = videojs('my_video');

    // 4. get handle of the UL element
    const adsUl = document.querySelector('#adsUl');

    // 5. utility functions
    const map = fn => list => list.map(fn);
    const dot = x => y => x.reduce((acc, next, i) => acc + next * y[i], 0);
    const int10 = x => parseInt(x, 10);

    // 6. workflow steps
    const getVMAP = response => response.text();
    const parseVMAP = text => new DOMParser().parseFromString(text, 'text/xml');
    const getAdBreaks = xmlDoc => xmlDoc.getElementsByTagName('vmap:AdBreak');
    const getTimeOffsets = adBreak => adBreak.getAttribute('timeOffset');
    const splitSMPTEString = timeOffset => timeOffset.split(':');
    const SMPTEToSec = dot(smpteConversion);
    const minus5Sec = x => x > 5 ? x-5 : 0;

    // 7. create <li> for a given ad-break timestamp
    const createAdLi = ts => {
      const li = document.createElement('li');
      li.setAttribute('data-ts', ts)
      li.appendChild(document.createTextNode(`ad-break at ${ts} sec`));
      li.addEventListener('click', liCallback);
      return li;
    };
    
    // 8. callback invoked when an <li> is clicked on
    const liCallback = ({target}) => {
      const ts = parseInt(target.getAttribute('data-ts'),10);
      player.currentTime(ts);
      player.play();
    };

    // 9. append to <ul>
    const append = li => adsUl.appendChild(li);
    
    // 10. handles errors and returns an empty list
    const errorHandler = error => console.error(`error with VMAP file ${error}`) || [];

    // 11. the whole workflow
    fetch(vmapUrl)
        .then(getVMAP)
        .then(parseVMAP)
        .then(getAdBreaks)
        .then(Array.from)
        .then(map(getTimeOffsets))
        .then(map(splitSMPTEString))
        .then(map(map(int10)))
        .then(map(SMPTEToSec))
        .then(map(minus5Sec))
        .then(map(createAdLi))
        .then(map(append))
        .catch(errorHandler);
</script>

In step 1, we define the URL of the VMAP file: update the placeholder ${VMAP_URL} with your VMAP file or the one provided in the previous section.

Alternatively, you can use the following URL:

https://dev-automads-solution-ads-bucket.s3-eu-west-1.amazonaws.com/episode3/ad_breaks.vmap 

In step 2, we define an array with the values used to convert SMPTE timestamps to seconds.

With steps 3 and 4, we create a reference to the elements of the page we want to interact with programmatically: player is a reference to the VideoJS player element, and adsUl references the list placeholder.

In step 5, we define three utility functions

  • map – wraps the built-in Array.prototype.map method, so we can use it conveniently in a promise chain.
  • dot – defines the scalar product of two arrays. We use this function convert SMPTE timestamps to seconds.
  • int10 – wraps the built-in function parseInt to use it conveniently with map

For step 6, we define the core steps of the workflow

  • getVMAP – extracts the plain text from an HTTP Response Object Body
  • parseVMAP – parses the text as an XML Document
  • getAdBreaks – gets all the vmap:AdBreaks tags in an HTMLCollection
  • getTimeOffsets – gets all of the timeOffsets values from the AdBreaks tags
  • composed, splitSMPTEString and SMPTEToSec convert a timestamp expressed in SMPTE format into seconds
  • minus5Sec decreases by 5 seconds a timestamp expressed in seconds.

In step 7, 8 and 9, we define the following functions to manipulate the UI

  • createAdLi is a function that populates adsUl with clickable <li> elements.
  • The function accepts two parameters ts and text. ts is the timestamp in seconds of an ad. text is the text we want to display in the <li> element.
  • liCallback is the callback function each <li> element invokes when clicked on. The goal of this function is to seek to the provided position in the video and start the playback. This function is bound to each element by the createAdLi function.
  • append – adds the newly created <li> elements to the list

For step 10, we define a simple error handler function that logs to the console in case of error.

Finally, step 11, is where we put together the workflow in a promise chain. We begin with fetching the VMAP file from the remote server, we parse it into an XML document. Then we extract the timestamps, convert them to integers and generate clickable elements on the UI.

Test the player locally

You are now ready to test the player locally. To do so, start up a simple HTTP server on your local machine. The following are instructions for starting it up with Node.js and Python3.

Node.js

If you have Node.js installed, run the following command in a terminal in your working directory.

$ npm install -g http-server
$ http-server .

You should see a response similar to the following:

Starting up http-server, serving.
Available on:
   http://127.0.0.1:8080   
   http://192.168.0.48:8080   
   http://192.168.4.131:8080 
Hit CTRL-C to stop the server

Open up a browser window and route to http://localhost:8080 to test your player.

Python3

If you have Python3 installed, open up a terminal in the current working directory and run the following:

$ python3 -m http.server

You should see a response similar to the following:

> Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Open up a browser window and route to http://localhost:8000 to test your player.

Deploy the Player to an Amazon S3 Website

You can use Amazon S3 to host your webpage. Make sure that your AWS CLI is configured with a profile that can manipulate S3 resources. Then open up a terminal in the current folder and issue the following commands:

DEPLOY_BUCKET=<insert-a-bucket-name-here>

AWS_REGION=<insert-a-region-here>

aws s3 mb s3://$DEPLOY_BUCKET
aws s3 website s3://$DEPLOY_BUCKET --index-document index.html --error-document index.html
aws s3 cp index.html s3://$DEPLOY_BUCKET --acl public-read

echo "website endpoint:  http://$DEPLOY_BUCKET.s3-website-$AWS_REGION.amazonaws.com"

Open up a browser window and route to the URL displayed in your terminal: you can now share this URL and start streaming and monetizing your content.

Next Steps

Now that you have a quick-start for displaying and interacting with an HLS stream, consider the following applications:

  • provide markers on the player track to tell the user when ads are inserted
  • provide a countdown to the beginning and the end of the inserted ads
  • gather analytics about the performance of an ad and its placement
  • disable fast-forward for the duration of the ad or provide a button to skip the ad after a fixed amount of time
  • include UI elements at the end of an ad to encourage user interaction or a call to action

Conclusion

In this blog post we built, tested, and deployed a simple webpage using VideoJS to display and interact with an HLS stream served by AWS Elemental MediaTailor.

Make sure you check out the other posts in this series:

Watch the AWS Media Blog for the next post in this series. Let us know via the comment box if you have any questions, feedback or suggestions.

Giuseppe Battista

Giuseppe Battista

Giuseppe Battista is a Solutions Architect in the UK Media, Entertainment and Telco Team. With more than 10 years of experience as software developer and architect, he helps customers in the Publishing Industry in their journey on AWS. You can find him on Twitter as @giusedroid

Henrique Fugita

Henrique Fugita

Henrique Fugita is a R&D Solutions Architect at AWS in Brazil. He helps customers envision the art of the possible on AWS by working with them on innovative prototyping engagements. With over 15 years of experience in software development and solutions architecture, he currently focuses on artificial intelligence and machine learning.

Rafael Werneck

Rafael Werneck

Rafael Werneck is a Senior Prototyping Architect at AWS Prototyping and Cloud Engineering, based in Brazil. Previously, he worked as a Software Development Engineer on Amazon.com.br and Amazon RDS Performance Insights.