AWS for M&E Blog

How to filter live streaming renditions by device type at the Edge

Filtering Streaming Renditions by Device Type with AWS Elemental MediaPackage, Amazon CloudFront, and Amazon Lambda@Edge

Introduction

When customers choose their bitrate ladder for adaptive bit rate (ABR) streaming video, they often discover that there is not always a “one size fits all” set of renditions that will work for all of their client devices. For example, on smaller screen devices (such as smartphones or tablets), we may wish to filter out certain large frame size renditions (such as 4K or 1080p) at higher bitrates, because the perceived video quality on a small screen doesn’t differ enough to justify the added cost of delivering more bits. Conversely, on larger screen devices (such as desktop PCs or smart TVs), we may wish to remove certain lower resolution and frame-rate options to ensure that the device does not switch to a rendition that provides a poor quality of experience on a larger screen.

There are two ways of achieving this type of rendition filtering: client-side and server-side. With client-side filtering, the client application running on the viewing device will contain logic instructing it which bitrates to use or to not use based on the options available in the manifest file. While this solution is simpler from a streaming infrastructure point of view, it also provides several challenges as it requires custom application development for numerous player platforms (some of which do not provide developers complete control over how the player functions). The other option, which we will investigate in this blog, is server-side filtering. With this methodology, we will inspect the User-Agent header in requests for the manifest file, and direct the request to the appropriate manifest file based on our own logic.

Getting Started

Let’s start with a channel that is being encoded by AWS Elemental MediaLive, using the following bitrate ladder (as recommended by Apple’s HLS Authoring Specification for Apple Devices):

  • 1920 x 1080 @ 7.8 Mbps
  • 1920 x 1080 @ 6 Mbps
  • 1280 x 720 @ 4.5 Mbps
  • 1280 x 720 @ 3 Mbps
  • 960 x 540 @ 2 Mbps
  • 768 x 432 @ 1.1 Mbps
  • 768 x 432 @ 0.8 Mbps
  • 640 x 360 @ 0.4 Mbps
  • 416 x 234 @ 0.2 Mbps

For details on how to set-up MediaLive with different options for the source see this series of post:
Part 1: Connecting AWS Elemental Live On-Premises to AWS Media Services in the Cloud
Part 2: Connecting OBS Studio to AWS Media Services in the Cloud
Part 3: Connecting FFmpeg Using RTP to AWS Media Services in the Cloud
Part 4: Connecting FFmpeg Using RTMP to AWS Media Services in the Cloud
Part 5: Connecting VLC Media Player Using RTP to AWS Media Services in the Cloud

In this walk-through, we’re going to assume that we want Smart TV and Desktop viewers to have access to the complete set of streams, while viewers on Tablets and Smartphones will receive all but the 1920×1080 renditions. To achieve this, we will utilize Amazon CloudFront and AWS Lambda@Edge to inspect the user-agent of incoming playback requests and direct the request to different AWS Elemental MediaPackage Endpoints based on the logic described above.

Workflow

First, we need to create our MediaPackage channel if we don’t already have one.

Create Channel

Select “Create a CloudFront distribution for this channel” to have MediaPackage automatically create a CloudFront distribution and link it to your MediaPackage endpoints.

Once our channel is created, we’re going to create 2 similar MediaPackage endpoints. The two endpoints should be identical (including the “Manifest name”) except one of the streams should be configured to use the “Filter incoming streams” option to only include streams that are less than 5.5 Mbps.

For our first endpoint:

First Endpoint

And our second endpoint:

Second Endpoint

Streams to Include

Let’s curl them and see if they work individually.

The first endpoint:


$ curl https://example12345.mediapackage.us-west-2.amazonaws.com/out/v1/8ebc56efb8ab4b86bb27b76126b4c8de/index.m3u8

You should see this result:

 #EXTM3U
#EXT-X-VERSION:3
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:BANDWIDTH=9234192,RESOLUTION=1920x1080,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_1.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=7122720,RESOLUTION=1920x1080,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_2.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5391289,RESOLUTION=1280x720,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_3.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3631755,RESOLUTION=1280x720,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_4.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2430560,RESOLUTION=960x540,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_5.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1346633,RESOLUTION=768x432,CODECS="avc1.42C01E,mp4a.40.2",SUBTITLES="subtitles"
index_6.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=994747,RESOLUTION=768x432,CODECS="avc1.42C01E,mp4a.40.2",SUBTITLES="subtitles"
index_7.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=525531,RESOLUTION=640x360,CODECS="avc1.4D401E,mp4a.40.2",SUBTITLES="subtitles"
index_8.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=290923,RESOLUTION=416x234,CODECS="avc1.4D4029,mp4a.40.2",SUBTITLES="subtitles"
index_9.m3u8
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subtitles",NAME="caption_1",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="index_10_0.m3u8"

The second endpoint:


$ curl https://example12345.mediapackage.us-west-2.amazonaws.com/out/v1/7360a56565a1455b92424aaf8a3dc4aa/index.m3u8

You should see this result:


#EXTM3U
#EXT-X-VERSION:3
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:BANDWIDTH=5391289,RESOLUTION=1280x720,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_1.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3631755,RESOLUTION=1280x720,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_2.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2430560,RESOLUTION=960x540,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_3.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1346633,RESOLUTION=768x432,CODECS="avc1.42C01E,mp4a.40.2",SUBTITLES="subtitles"
index_4.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=994747,RESOLUTION=768x432,CODECS="avc1.42C01E,mp4a.40.2",SUBTITLES="subtitles"
index_5.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=525531,RESOLUTION=640x360,CODECS="avc1.4D401E,mp4a.40.2",SUBTITLES="subtitles"
index_6.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=290923,RESOLUTION=416x234,CODECS="avc1.4D4029,mp4a.40.2",SUBTITLES="subtitles"
index_7.m3u8
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subtitles",NAME="caption_1",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="index_8_0.m3u8"

As you can see, the first endpoint contains our two 1920×1080 streams, and the second endpoint does not.

Before we proceed, we need to make a note of the random string of letters and characters in the 2nd to last section of each URL. For example, for our 1920×1080 manifest (https://example12345.mediapackage.us-west-2.amazonaws.com/out/v1/8ebc56efb8ab4b86bb27b76126b4c8de/index.m3u8) we need to make a note of 8ebc56efb8ab4b86bb27b76126b4c8de and for our 1280×720 manifest, we should record 7360a56565a1455b92424aaf8a3dc4aa. We will need these strings in order to create our Lambda@Edge function.

Set up our User-Agent filtering

First, no matter which region we are running our MediaPackage channels in, we must switch our region to US East (N. Virginia), or us-east-1.

Did You Know? All Lambda@Edge functions must be created in the US East (N. Virginia) region.

Open the Lambda console and create a new function. Give the function any name you want, and select Node.js 6.10as the runtime.

lambda from scratch

Use the following example of function code for our Lambda@Edge function:

'use strict';

/* This is an origin request function */
exports.handler = (event, context, callback) => {
    const request = event.Records[0].cf.request;
    const headers = request.headers;

    const bigScreenManifest = "8ebc56efb8ab4b86bb27b76126b4c8de"
    const smallScreenManifest = "7360a56565a1455b92424aaf8a3dc4aa"

    var reqSplit = request.uri.split("/")
    
    if ((headers['cloudfront-is-desktop-viewer']
        && headers['cloudfront-is-desktop-viewer'][0].value === 'true') ||  
        (headers['cloudfront-is-smarttv-viewer']
               && headers['cloudfront-is-smarttv-viewer'][0].value === 'true')) 
    {
        reqSplit[3] = bigScreenManifest
        request.uri = reqSplit.join("/")
    } else {
        reqSplit[3] = smallScreenManifest
        request.uri = reqSplit.join("/")
    }
    
    console.log(`Request uri set to "${request.uri}"`);

    callback(null, request);
};

Replace the ‘bigScreenManifest’ and ‘smallScreenManifest’ variable values with the strings of letters and numbers that we recorded previously from our MediaPackage endpoint URLs.

The code for the Lambda function above is very simple – basically, it takes advantage of the fact that Amazon CloudFront has built in ability to detect user-agent device types and will insert headers in its request to the origin server identifying the class of device. For example, if the request comes from a Smart TV, CloudFront will insert the header ‘cloudfront-is-smarttv-viewer: true’. Our Lambda function looks for the presence of these headers, and replaces the second to last section of the origin URI to direct the viewer to the manifest that contains the full set of renditions, or the one with the 1920×1080 streams omitted.

  • Click “Save” in the Lambda console
  • Then click “Actions” and “Deploy to Lambda@Edge”

Select the CloudFront Distribution that was created by MediaPackage for our channel above, select the default (*) cache behavior, and use “Origin request” as the CloudFront event. This will cause our Lambda@Edge function to be triggered whenever CloudFront receives a request for content and before it forwards the request to the origin server (in this case, MediaPackage).

Deploy Lambda@Edge

  • Click “Deploy”

In order for our “cloudfront-is-*-viewer’ headers to work, we need to enable this functionality on our CloudFront distribution. Switch to the CloudFront console and click on the distribution.

  • Click on the “Behaviors” tab, select the “*” behavior, and click “Edit”

Behaviors

In the “Whitelist Headers” category, select the following headers and click Add :

CloudFront-Is-Desktop-Viewer
CloudFront-Is-Mobile-Viewer
CloudFront-Is-SmartTV-Viewer
CloudFront-Is-Tablet-Viewer

This will ensure that these headers are forwarded to our Lambda function and origin server.

Lambda Function

  • Click “Yes, Edit”.

Once your CloudFront distribution has changed status from “In Progress” to “Deployed”, we can test our user agent filtering!

Testing the User-Agent filtering

Now request to our CloudFront URL, try one with user-agent simulating iOS device, try another simulating a Smart TV.

Curl simulating an iOS device:

$ curl -A "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_3_3 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8J2 Safari/6533.18.5" https://example12345.cloudfront.net/out/v1/8ebc56efb8ab4b86bb27b76126b4c8de/index.m3u8

You should see this result:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:BANDWIDTH=5391289,RESOLUTION=1280x720,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_1.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3631755,RESOLUTION=1280x720,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_2.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2430560,RESOLUTION=960x540,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_3.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1346633,RESOLUTION=768x432,CODECS="avc1.42C01E,mp4a.40.2",SUBTITLES="subtitles"
index_4.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=994747,RESOLUTION=768x432,CODECS="avc1.42C01E,mp4a.40.2",SUBTITLES="subtitles"
index_5.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=525531,RESOLUTION=640x360,CODECS="avc1.4D401E,mp4a.40.2",SUBTITLES="subtitles"
index_6.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=290923,RESOLUTION=416x234,CODECS="avc1.4D4029,mp4a.40.2",SUBTITLES="subtitles"
index_7.m3u8
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subtitles",NAME="caption_1",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="index_8_0.m3u8"

Curl simulating a Smart TV device:

$ curl -A "Mozilla/5.0 (SMART-TV; Linux; Tizen 2.4.0) AppleWebkit/538.1 (KHTML, like Gecko) SamsungBrowser/1.1 TV Safari/538.1" https://example12345.cloudfront.net/out/v1/8ebc56efb8ab4b86bb27b76126b4c8de/index.m3u8

You should see this result

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:BANDWIDTH=9234192,RESOLUTION=1920x1080,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_1.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=7122720,RESOLUTION=1920x1080,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_2.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=5391289,RESOLUTION=1280x720,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_3.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3631755,RESOLUTION=1280x720,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_4.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2430560,RESOLUTION=960x540,CODECS="avc1.640029,mp4a.40.2",SUBTITLES="subtitles"
index_5.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1346633,RESOLUTION=768x432,CODECS="avc1.42C01E,mp4a.40.2",SUBTITLES="subtitles"
index_6.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=994747,RESOLUTION=768x432,CODECS="avc1.42C01E,mp4a.40.2",SUBTITLES="subtitles"
index_7.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=525531,RESOLUTION=640x360,CODECS="avc1.4D401E,mp4a.40.2",SUBTITLES="subtitles"
index_8.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=290923,RESOLUTION=416x234,CODECS="avc1.4D4029,mp4a.40.2",SUBTITLES="subtitles"
index_9.m3u8
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subtitles",NAME="caption_1",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="eng",URI="index_10_0.m3u8"

As you can see, both requests are made to the exact same CloudFront URL, but the request with the Smart TV user-agent receives the 1920×1080 stream, whereas the request simulating a mobile phone only received streams up to 1280×720.

This is just one example of how Lambda@Edge can be combined with the AWS Media Services to provide additional control and functionality for your streaming applications. There are several other examples of how Lambda@Edge can be used – for example, for A/B testing between MediaPackage Endpoints with different settings or DRM. This page contains some great examples of sample Lambda@Edge functions – I encourage you to try them out and let us know what you come up with!