Skip to content

Optimize your MediaLive ABR ladder: Identify unused renditions and improve viewer experience using CloudFront access logs

40 minute read
Content level: Advanced
0

Most media customers deploy MediaLive with default ABR rendition ladders without visibility into which renditions viewers actually consume. This article provides a complete, deployable solution to analyze CloudFront access logs via Athena and correlate them with MediaLive HLS output configurations. Customers can identify underused renditions to eliminate (reducing encoding costs by 20-40%) and discover where viewer demand is concentrated to invest in higher-quality renditions that improve viewin

Optimize your MediaLive ABR ladder: Identify unused renditions and improve viewer experience using CloudFront access logs

Enter image description here

Introduction

When deploying adaptive bitrate (ABR) streaming workflows with AWS Elemental MediaLive, many customers use default rendition ladders without visibility into which renditions their viewers actually consume. A typical HLS output group might include 6 or more renditions (1080p, 720p, 480p, 360p, 240p) — but if only 5% of viewers ever request the lowest or highest renditions, you're paying for encoding and delivery that provides no viewer benefit.

Beyond cost savings — improving viewer experience:

If 93% of your viewers are consuming the highest-quality rendition (e.g., 720p60), that's a signal to invest in that experience — not just optimize costs. Consider:

  • Adding a higher rendition (e.g., 1080p) since your viewers clearly have the bandwidth for it
  • Adding intermediate steps in the bitrate ladder (e.g., 720p at 4Mbps between your 5.6Mbps and 3.4Mbps) to reduce ABR switching artifacts
  • Allocating the encoding budget saved from eliminating unused renditions toward better quality in the renditions viewers actually watch

This analysis helps you make data-driven decisions that optimize both cost AND viewer experience.

This article shows you how to correlate Amazon CloudFront access logs with your MediaLive HLS output configuration to:

  • Identify which renditions are most and least requested by viewers
  • Quantify bandwidth consumption and cost per rendition
  • Generate data-driven recommendations to eliminate unused renditions or optimize bitrates
  • Potentially reduce encoding and delivery costs by 20–40%
  • Improve viewer experience by investing in the renditions your audience actually consumes — adding higher-quality variants where demand is concentrated

Architecture Overview

This solution supports two common delivery architectures:

Architecture A: MediaLive → S3 → CloudFront

┌──────────┐    ┌────────┐    ┌─────────────┐    ┌───────────┐
│MediaLive │───▶│   S3   │───▶│ CloudFront  │───▶│  Viewers  │
│          │    │(Origin)│    │(Distribution)│    │           │
└──────────┘    └────────┘    └──────┬──────┘    └───────────┘
                                     │
Architecture B: MediaLive → MediaPackage → CloudFront (Recommended)

┌──────────┐    ┌──────────────┐    ┌─────────────┐    ┌───────────┐
│MediaLive │───▶│ MediaPackage │───▶│ CloudFront  │───▶│  Viewers  │
│          │    │   (Origin)   │    │(Distribution)│    │           │
└──────────┘    └──────────────┘    └──────┬──────┘    └───────────┘
                                           │
            Both architectures:            ▼ Access Logs
                                      ┌─────────┐
                                      │   S3    │
                                      │ (Logs)  │
                                      └────┬────┘
                                           │
                                           ▼
                                      ┌─────────┐     ┌───────────────┐
                                      │ Athena  │────▶│  QuickSight / │
                                      │         │     │   Report      │
                                      └─────────┘     └───────────────┘

Note: This solution works with both architectures. The CloudFront access logs capture viewer request patterns regardless of whether the origin is S3 or MediaPackage. The only difference is where you fetch the parent manifest from (S3 bucket vs. MediaPackage/CloudFront endpoint).

Prerequisites

  • An AWS account with AWS Elemental MediaLive producing HLS output
  • An Amazon CloudFront distribution serving the HLS content to viewers
  • Permissions to enable CloudFront standard logging
  • Amazon Athena configured in the same region as your log bucket

Deployment: CloudFormation Template

This article includes at the bottom of this page a companion CloudFormation template (rendition-advisor-pipeline.yaml) that deploys the entire analysis pipeline in a single command — without modifying your existing MediaLive or CloudFront setup.

The template adds only the monitoring layer:

  • S3 bucket for logs (or uses your existing log bucket — configurable)
  • Glue/Athena tables with the correct schema (including real-time logs v2 support)
  • Pre-built Athena named queries (ready to run from the console)
  • Optional: Lambda + EventBridge for weekly automated reports with SNS notifications

Deploy command:

# If you ALREADY have CloudFront logging to an existing bucket:
aws cloudformation deploy \
  --template-file rendition-advisor-pipeline.yaml \
  --stack-name rendition-advisor \
  --parameter-overrides \
    CloudFrontDistributionId=E1YOURDISTR1D \
    ChannelName=my-channel \
    ExistingLogBucket=my-existing-cf-log-bucket \
    ExistingLogPrefix="cf-logs/" \
    LogType=standard \
  --capabilities CAPABILITY_NAMED_IAM

# If you need a NEW log bucket (fresh setup):
aws cloudformation deploy \
  --template-file rendition-advisor-pipeline.yaml \
  --stack-name rendition-advisor \
  --parameter-overrides \
    CloudFrontDistributionId=E1YOURDISTR1D \
    ChannelName=my-channel \
  --capabilities CAPABILITY_NAMED_IAM

Tear-down: aws cloudformation delete-stack --stack-name rendition-advisor removes everything cleanly.

Step 1: Enable CloudFront Standard Logging

Enable access logging on your CloudFront distribution to capture every viewer request, including which HLS segment URIs they access.

Via AWS Console:

  1. Open the CloudFront console → select your distribution
  2. Go to GeneralSettingsEdit
  3. Under Standard Logging, set to On
  4. Choose an S3 bucket for logs (e.g., my-cf-access-logs)
  5. Optionally set a log prefix (e.g., hls-channel-01/)
  6. Save changes

Via AWS CLI:

aws cloudfront update-distribution --id E1EXAMPLE \
  --distribution-config '{
    "Logging": {
      "Enabled": true,
      "IncludeCookies": false,
      "Bucket": "my-cf-access-logs.s3.amazonaws.com",
      "Prefix": "hls-channel-01/"
    }
  }'

Note: Logs typically begin appearing within 15–30 minutes of enabling. Each log file covers roughly 1 hour of traffic.

Step 2: Verify Your MediaLive Output Naming Convention

The analysis relies on being able to extract the rendition identifier from the request URI. Your HLS output paths should include a distinguishable rendition segment.

Important: If your output paths use generic names (e.g., output_1.ts, output_2.ts) instead of resolution-based names, skip ahead to Step 2B: Auto-Discover Renditions from the HLS parent Manifest below.

Common naming patterns:

PatternExample URIRendition Extracted
Resolution-based/live/channel/1080p/segment_00001.ts1080p
Bitrate-based/live/channel/6000k/segment_00001.ts6000k
Index-based/live/channel/rendition_0/segment_00001.tsrendition_0

MediaLive HLS Output Group configuration example:

{
  "OutputGroupSettings": {
    "HlsGroupSettings": {
      "Destination": {
        "DestinationRefId": "s3-output"
      },
      "SegmentLength": 6,
      "IndexNSegments": 3
    }
  },
  "Outputs": [
    {
      "OutputName": "1080p",
      "VideoDescriptionName": "video_1080p",
      "OutputSettings": {
        "HlsOutputSettings": {
          "NameModifier": "_1080p"
        }
      }
    },
    {
      "OutputName": "720p",
      "VideoDescriptionName": "video_720p",
      "OutputSettings": {
        "HlsOutputSettings": {
          "NameModifier": "_720p"
        }
      }
    },
    {
      "OutputName": "480p",
      "VideoDescriptionName": "video_480p",
      "OutputSettings": {
        "HlsOutputSettings": {
          "NameModifier": "_480p"
        }
      }
    }
  ]
}

Tip: If your output paths don't include a recognizable rendition identifier, update your MediaLive Output Group's NameModifier or destination path to include one (e.g., _1080p, _720p). This requires a channel stop/start but makes analysis significantly easier.

Step 2B: Auto-Discover Renditions from the HLS parent Manifest

If your channel uses generic output names (e.g., index_1, index_2, output_1) that don't indicate the resolution or bitrate, you can automatically build a rendition lookup table by parsing the HLS parent manifest (.m3u8).

MediaPackage users: When MediaPackage is your CloudFront origin, the manifest is served via the MediaPackage endpoint (not directly from S3). Fetch it from your CloudFront URL or MediaPackage origin endpoint — see the script options below.

The parent playlist contains #EXT-X-STREAM-INF tags that map each variant stream URI to its actual resolution and bandwidth:

#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=6000000,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2"
output_1/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=1280x720,CODECS="avc1.4d401f,mp4a.40.2"
output_2/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1500000,RESOLUTION=854x480,CODECS="avc1.4d401e,mp4a.40.2"
output_3/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360,CODECS="avc1.42c01e,mp4a.40.2"
output_4/index.m3u8

curl -vvv https://cloudfront.net/index.m3u8

Enter image description here

Python script to parse the manifest and build the rendition map

This script supports two manifest sources:

  • S3 bucket (direct S3 origin — MediaLive output to S3)
  • HTTP URL (MediaPackage origin — manifest served via CloudFront or MediaPackage endpoint)

Save this as parse_manifest.py and run with python3 parse_manifest.py:

import boto3
import re
import json

try:
    import requests
    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False
    import urllib.request


def fetch_manifest_from_s3(bucket, key):
    """Fetch manifest from S3 bucket (direct S3 origin)."""
    s3 = boto3.client('s3')
    response = s3.get_object(Bucket=bucket, Key=key)
    return response['Body'].read().decode('utf-8')


def fetch_manifest_from_url(url):
    """Fetch manifest from HTTP URL (MediaPackage/CloudFront endpoint)."""
    if HAS_REQUESTS:
        response = requests.get(url)
        response.raise_for_status()
        return response.text
    else:
        with urllib.request.urlopen(url) as response:
            return response.read().decode('utf-8')


def parse_manifest(manifest_content):
    """
    Parse an HLS parent manifest to extract rendition information.
    Works with any HLS parent playlist regardless of origin (S3, MediaPackage, etc.)
    """
    rendition_map = []
    lines = manifest_content.strip().split('\n')
    
    i = 0
    while i < len(lines):
        line = lines[i].strip()
        
        if line.startswith('#EXT-X-STREAM-INF:'):
            attrs = {}
            bw_match = re.search(r'BANDWIDTH=(\d+)', line)
            avg_bw_match = re.search(r'AVERAGE-BANDWIDTH=(\d+)', line)
            res_match = re.search(r'RESOLUTION=(\d+x\d+)', line)
            fps_match = re.search(r'FRAME-RATE=([\d.]+)', line)
            codecs_match = re.search(r'CODECS="([^"]+)"', line)
            
            if bw_match:
                attrs['bandwidth'] = int(bw_match.group(1))
            if avg_bw_match:
                attrs['avg_bandwidth'] = int(avg_bw_match.group(1))
            if res_match:
                attrs['resolution'] = res_match.group(1)
            if fps_match:
                attrs['framerate'] = fps_match.group(1)
            if codecs_match:
                attrs['codecs'] = codecs_match.group(1)
            
            # Next non-comment line is the URI
            i += 1
            while i < len(lines) and lines[i].strip().startswith('#'):
                i += 1
            
            if i < len(lines):
                uri = lines[i].strip()
                # Extract prefix: "index_1.m3u8" -> "index_1", "path/to/720p.m3u8" -> "720p"
                prefix = uri.split('/')[-1].replace('.m3u8', '') if '/' in uri else uri.replace('.m3u8', '')
                
                # Build human-readable label
                height = attrs.get('resolution', '0x0').split('x')[1]
                fps = attrs.get('framerate', '29.97')
                fps_label = '60' if float(fps) > 50 else '30'
                avg_bw = attrs.get('avg_bandwidth', attrs.get('bandwidth', 0))
                bw_mbps = avg_bw / 1_000_000
                label = f"{height}p{fps_label}_{bw_mbps:.1f}M"
                
                rendition_map.append({
                    'uri_prefix': prefix,
                    'resolution': attrs.get('resolution', 'unknown'),
                    'bandwidth': avg_bw,
                    'label': label,
                    'framerate': attrs.get('framerate', 'unknown'),
                })
        i += 1
    
    return rendition_map


# ==============================
# INTERACTIVE PROMPTS
# ==============================
if __name__ == '__main__':
    print("=" * 60)
    print("  HLS Rendition Manifest Parser")
    print("  Supports: S3 direct origin OR MediaPackage/HTTP endpoint")
    print("=" * 60)
    
    print("\nWhere is your parent manifest?")
    print("  1) S3 bucket (MediaLive → S3 origin)")
    print("  2) HTTP URL (MediaPackage → CloudFront endpoint)")
    
    choice = input("\nEnter 1 or 2: ").strip()
    
    if choice == '1':
        bucket = input("S3 bucket name: ").strip()
        key = input("S3 key (e.g., live/channel1/index.m3u8): ").strip()
        print(f"\nFetching s3://{bucket}/{key} ...")
        manifest_content = fetch_manifest_from_s3(bucket, key)
    elif choice == '2':
        url = input("Manifest URL (e.g., https://d1234abc.cloudfront.net/out/v1/abc123/index.m3u8): ").strip()
        print(f"\nFetching {url} ...")
        manifest_content = fetch_manifest_from_url(url)
    else:
        print("Invalid choice. Exiting.")
        exit(1)
    
    # Parse
    rendition_map = parse_manifest(manifest_content)
    
    # Display results
    print(f"\n{'='*60}")
    print(f"  Found {len(rendition_map)} renditions:")
    print(f"{'='*60}")
    for r in rendition_map:
        print(f"  {r['uri_prefix']:>12} → {r['resolution']} @ {r['bandwidth']/1e6:.1f} Mbps  ({r['label']})")
    
    # Upload
    lookup_bucket = input("\nRendition lookup S3 bucket (from CFN stack output): ").strip()
    
    output_file = '/tmp/rendition_map.json'
    with open(output_file, 'w') as f:
        for r in rendition_map:
            f.write(json.dumps(r) + '\n')
    
    s3 = boto3.client('s3')
    s3.upload_file(output_file, lookup_bucket, 'lookup/rendition_map.json')
    print(f"\n✅ Uploaded to s3://{lookup_bucket}/lookup/rendition_map.json")
    print("   Athena can now JOIN with human-readable rendition labels.")

Run it:

python3 parse_manifest.py

It will ask:

  1. Source type (S3 or HTTP URL)
  2. Bucket/key or URL
  3. Lookup bucket to upload the result

Tip: Run this script once per channel. Only re-run if you change your encoding ladder (add/remove renditions). For MediaPackage users, your parent manifest URL is typically: https://<your-cf-domain>/out/v1/<mediapackage-endpoint-id>/index.m3u8

Step 3: Create an Athena Table for CloudFront Logs

Create an external table in Athena that maps to your CloudFront log format:

CREATE EXTERNAL TABLE IF NOT EXISTS cloudfront_hls_logs (
  `date` DATE,
  `time` STRING,
  x_edge_location STRING,
  sc_bytes BIGINT,
  c_ip STRING,
  cs_method STRING,
  cs_host STRING,
  cs_uri_stem STRING,
  sc_status INT,
  cs_referer STRING,
  cs_user_agent STRING,
  cs_uri_query STRING,
  cs_cookie STRING,
  x_edge_result_type STRING,
  x_edge_request_id STRING,
  x_host_header STRING,
  cs_protocol STRING,
  cs_bytes BIGINT,
  time_taken FLOAT,
  x_forwarded_for STRING,
  ssl_protocol STRING,
  ssl_cipher STRING,
  x_edge_response_result_type STRING,
  cs_protocol_version STRING,
  fle_status STRING,
  fle_encrypted_fields INT,
  c_port INT,
  time_to_first_byte FLOAT,
  x_edge_detailed_result_type STRING,
  sc_content_type STRING,
  sc_content_len BIGINT,
  sc_range_start BIGINT,
  sc_range_end BIGINT
)
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
LOCATION 's3://my-cf-access-logs/hls-channel-01/'
TBLPROPERTIES ('skip.header.line.count'='2');

Step 4: Query Rendition Usage Patterns

Basic rendition request distribution

-- Extract rendition from URI path and count requests
SELECT
  REGEXP_EXTRACT(cs_uri_stem, '/([0-9]+p[^/]*|[0-9]+k[^/]*)/', 1) AS rendition,
  COUNT(*) AS segment_requests,
  SUM(sc_bytes) / POWER(1024, 3) AS total_gb,
  COUNT(DISTINCT c_ip) AS unique_viewers,
  ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) AS request_pct
FROM cloudfront_hls_logs
WHERE cs_uri_stem LIKE '%.ts'
  AND sc_status = 200
  AND "date" >= DATE '2026-07-27'
GROUP BY REGEXP_EXTRACT(cs_uri_stem, '/([0-9]+p[^/]*|[0-9]+k[^/]*)/', 1)
ORDER BY segment_requests DESC;

Note: Adjust the REGEXP_EXTRACT pattern to match your specific URI structure. The pattern above captures resolution-based identifiers like 1080p, 720p, or bitrate-based ones like 6000k.

Rendition usage by time of day (peak vs. off-peak)

SELECT
  REGEXP_EXTRACT(cs_uri_stem, '/([0-9]+p[^/]*)/', 1) AS rendition,
  HOUR(CAST("time" AS TIME)) AS hour_of_day,
  COUNT(*) AS requests
FROM cloudfront_hls_logs
WHERE cs_uri_stem LIKE '%.ts'
  AND sc_status = 200
GROUP BY 1, 2
ORDER BY rendition, hour_of_day;

Geographic distribution of renditions

SELECT
  REGEXP_EXTRACT(cs_uri_stem, '/([0-9]+p[^/]*)/', 1) AS rendition,
  x_edge_location,
  COUNT(*) AS requests,
  SUM(sc_bytes) / POWER(1024, 3) AS total_gb
FROM cloudfront_hls_logs
WHERE cs_uri_stem LIKE '%.ts'
  AND sc_status = 200
GROUP BY 1, 2
ORDER BY rendition, requests DESC;

Enter image description here

Step 5: Correlate with MediaLive Configuration

This step retrieves the actual encoding configuration from your MediaLive channels and , then builds a unified rendition map that matches output paths to resolution/bitrate specs.

5.1 Discover Renditions from MediaLive Channels

Save this as discover_renditions.py and run with python3 discover_renditions.py. The script will list your channels and ask you to select one:

import boto3
import json

def list_medialive_channels(region):
    """List all MediaLive channels in the account."""
    ml = boto3.client('medialive', region_name=region)
    channels = []
    paginator = ml.get_paginator('list_channels')
    for page in paginator.paginate():
        for ch in page['Channels']:
            channels.append({
                'id': ch['Id'],
                'name': ch['Name'],
                'state': ch['State'],
            })
    return channels


def get_medialive_renditions(channel_id, region):
    """
    Retrieve rendition configuration from a MediaLive channel.
    Maps each HLS output's NameModifier to its video encoding settings.
    """
    ml = boto3.client('medialive', region_name=region)
    channel = ml.describe_channel(ChannelId=channel_id)
    
    # Build video description lookup
    video_descriptions = {}
    for vd in channel['EncoderSettings']['VideoDescriptions']:
        video_descriptions[vd['Name']] = {
            'width': vd.get('Width'),
            'height': vd.get('Height'),
            'bitrate': vd.get('CodecSettings', {}).get('H264Settings', {}).get('Bitrate', 0),
            'profile': vd.get('CodecSettings', {}).get('H264Settings', {}).get('Profile', 'MAIN'),
            'framerate_num': vd.get('CodecSettings', {}).get('H264Settings', {}).get('FramerateNumerator'),
            'framerate_den': vd.get('CodecSettings', {}).get('H264Settings', {}).get('FramerateDenominator'),
        }
    
    renditions = []
    for og in channel['EncoderSettings']['OutputGroups']:
        hls_settings = og.get('OutputGroupSettings', {}).get('HlsGroupSettings')
        if not hls_settings:
            continue
        
        for output in og['Outputs']:
            hls_output = output['OutputSettings'].get('HlsOutputSettings', {})
            name_modifier = hls_output.get('NameModifier', '')
            video_desc_name = output.get('VideoDescriptionName', '')
            video_info = video_descriptions.get(video_desc_name, {})
            
            height = video_info.get('height', '?')
            bitrate = video_info.get('bitrate', 0)
            
            renditions.append({
                'uri_prefix': name_modifier.strip('_'),
                'output_name': output.get('OutputName', ''),
                'width': video_info.get('width'),
                'height': height,
                'bitrate': bitrate,
                'profile': video_info.get('profile'),
                'label': f"{height}p_{bitrate/1e6:.1f}M" if bitrate else f"{height}p",
            })
    
    return {
        'channel_id': channel_id,
        'channel_name': channel['Name'],
        'state': channel['State'],
        'renditions': renditions,
    }


# ==============================
# INTERACTIVE PROMPTS
# ==============================
if __name__ == '__main__':
    print("=" * 60)
    print("  MediaLive Rendition Discovery")
    print("=" * 60)
    
    region = input("\nAWS Region [us-east-1]: ").strip() or 'us-east-1'
    
    # List all channels
    print(f"\nFetching MediaLive channels in {region}...")
    channels = list_medialive_channels(region)
    
    if not channels:
        print("  No MediaLive channels found in this region.")
        exit(1)
    
    print(f"\n  Found {len(channels)} channel(s):")
    print(f"  {'#':<4} {'Channel ID':<12} {'Name':<30} {'State':<10}")
    print(f"  {'-'*60}")
    for i, ch in enumerate(channels, 1):
        print(f"  {i:<4} {ch['id']:<12} {ch['name']:<30} {ch['state']:<10}")
    
    # Ask user to select
    print()
    selection = input("Enter channel number (or channel ID directly): ").strip()
    
    if selection.isdigit() and int(selection) <= len(channels):
        channel_id = channels[int(selection) - 1]['id']
    else:
        channel_id = selection
    
    # Get renditions
    print(f"\nFetching rendition config for channel {channel_id}...")
    info = get_medialive_renditions(channel_id, region)
    
    print(f"\n{'='*60}")
    print(f"  Channel: {info['channel_name']} (ID: {info['channel_id']})")
    print(f"  State: {info['state']}")
    print(f"  Renditions: {len(info['renditions'])}")
    print(f"{'='*60}")
    for r in info['renditions']:
        print(f"  {r['name_modifier']:>12} → {r['width']}x{r['height']} "
              f"@ {r['bitrate']/1e6:.1f}Mbps  ({r['label']})")
    
    # Ask to save as lookup
    print()
    save = input("Upload as rendition lookup to S3? (y/n): ").strip().lower()
    if save == 'y':
        lookup_bucket = input("Rendition lookup S3 bucket: ").strip()
        
        lookup_rows = []
        for r in info['renditions']:
            lookup_rows.append({
                'uri_prefix': r['name_modifier'].strip('_'),
                'resolution': f"{r['width']}x{r['height']}",
                'bandwidth': r['bitrate'],
                'label': r['label'],
            })
        
        output_file = '/tmp/rendition_map.json'
        with open(output_file, 'w') as f:
            for row in lookup_rows:
                f.write(json.dumps(row) + '\n')
        
        s3 = boto3.client('s3')
        s3.upload_file(output_file, lookup_bucket, 'lookup/rendition_map.json')
        print(f"\n✅ Uploaded to s3://{lookup_bucket}/lookup/rendition_map.json")
    
    print("\nDone!")

Run it:

python3 discover_renditions.py

The script will:

  1. Ask for your AWS region
  2. List all MediaLive channels in your account
  3. Ask you to select a channel (by number or ID)
  4. Display the rendition ladder for that channel
  5. Optionally upload the lookup to S3 for Athena

5.3 Build Unified Rendition Map (All Sources)

This script scans all active channels, builds a combined lookup, and uploads it to S3 for Athena to join against:

def build_unified_rendition_map(region='us-east-1'):
    """
    Scan all MediaLive channels in the account.
    Build a unified rendition lookup table for Athena.
    
    Returns a list of dicts suitable for uploading as JSON lines to S3.
    """
    ml = boto3.client('medialive', region_name=region)
    
    lookup_rows = []
    
    # --- MediaLive channels ---
    paginator = ml.get_paginator('list_channels')
    for page in paginator.paginate():
        for ch in page['Channels']:
            try:
                info = get_medialive_renditions(ch['Id'], region)
                for r in info['renditions']:
                    lookup_rows.append({
                        'source_type': 'MediaLive',
                        'source_id': ch['Id'],
                        'source_name': info['channel_name'],
                        'uri_prefix': r['name_modifier'].strip('_'),
                        'width': r['width'],
                        'height': r['height'],
                        'bitrate': r['bitrate'],
                        'label': r['label'],
                    })
            except Exception as e:
                print(f"  Warning: Could not read channel {ch['Id']}: {e}")
    
    return lookup_rows


def upload_rendition_lookup(lookup_rows, bucket, prefix='lookup/'):
    """Upload the rendition map as JSON lines to S3 for Athena."""
    s3 = boto3.client('s3')
    content = '\n'.join(json.dumps(row) for row in lookup_rows)
    s3.put_object(
        Bucket=bucket,
        Key=f"{prefix}rendition_map.json",
        Body=content.encode('utf-8'),
        ContentType='application/json'
    )
    print(f"Uploaded {len(lookup_rows)} rendition entries to s3://{bucket}/{prefix}rendition_map.json")


# === Usage ===
lookup = build_unified_rendition_map(region='us-east-1')
# Upload to the bucket created by the CloudFormation template
upload_rendition_lookup(lookup, bucket='rendition-advisor-rendition-lookup-123456789012')

5.4 Athena: Per-Channel and Per-Template Usage Breakdown

Once the unified lookup is in Athena, query per-source usage:

-- Distinguish MediaLive (live) vs  traffic
-- and break down rendition usage PER channel
SELECT

    rl.label AS rendition,
  ROUND(rl.bitrate / 1000000.0, 1) AS bitrate_mbps,
  COUNT(*) AS segment_requests,
  ROUND(SUM(cf.sc_bytes) / POWER(1024, 3), 2) AS total_gb,
  COUNT(DISTINCT cf.c_ip) AS unique_viewers,
  ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(
    PARTITION BY rl.label
  ), 2) AS pct_within_channel
FROM cloudfront_hls_logs cf
JOIN rendition_lookup rl
  ON REGEXP_EXTRACT(cf.cs_uri_stem, '(index_[0-9]+)', 1) = rl.uri_prefix
WHERE (cf.cs_uri_stem LIKE '%.ts' OR cf.cs_uri_stem LIKE '%.m4s')
  AND cf.sc_status = 200
  AND cf."date" >= CURRENT_DATE - INTERVAL '7' DAY
GROUP BY rl.label, rl.resolution, rl.bandwidth
ORDER BY segment_requests DESC;

Key insight: The pct_within_channel column tells you the rendition distribution within each specific channel — so you can make per-workflow optimization decisions rather than averaging across all content.

5.5 Per-Channel Cost Attribution

-- Weekly cost breakdown per channel per rendition
WITH rendition_usage AS (
  SELECT
    rl.label AS rendition,
    rl.bandwidth AS bitrate,
    COUNT(*) AS requests,
    SUM(cf.sc_bytes) / POWER(1024, 3) AS gb_delivered
  FROM cloudfront_hls_logs cf
  JOIN rendition_lookup rl
    ON REGEXP_EXTRACT(cf.cs_uri_stem, '(index_[0-9]+)', 1) = rl.uri_prefix
  WHERE (cf.cs_uri_stem LIKE '%.ts' OR cf.cs_uri_stem LIKE '%.m4s')
    AND cf.sc_status = 200
    AND cf."date" >= CURRENT_DATE - INTERVAL '7' DAY
  GROUP BY 1, 2
)
SELECT
  rendition,
  requests,
  ROUND(gb_delivered, 4) AS gb_delivered,
  ROUND(gb_delivered * 0.085, 4) AS cf_cost_usd,
  CASE
    WHEN bitrate >= 5000000 THEN ROUND(1.08 * 168, 2)
    WHEN bitrate >= 2500000 THEN ROUND(0.54 * 168, 2)
    WHEN bitrate >= 1000000 THEN ROUND(0.27 * 168, 2)
    WHEN bitrate >= 500000  THEN ROUND(0.14 * 168, 2)
    ELSE ROUND(0.07 * 168, 2)
  END AS encoding_cost_weekly,
  ROUND(requests * 100.0 / SUM(requests) OVER(), 2) AS usage_pct
FROM rendition_usage
ORDER BY requests DESC;

Enter image description here

Step 6: Cost Estimation per Rendition

Combine the usage data with AWS pricing to estimate per-rendition costs:

import boto3
import pandas as pd

# --- Configuration ---
ENCODING_COST_PER_HOUR = {
    # MediaLive AVC encoding, single pipeline, approximate USD/hour
    "1080p": 1.08,
    "720p": 0.54,
    "480p": 0.27,
    "360p": 0.14,
    "240p": 0.07,
}

CF_PRICE_PER_GB = 0.085  # CloudFront first 10TB tier (US/Europe)

def estimate_rendition_costs(athena_results_df, hours_running=168):
    """
    Estimate weekly cost per rendition (encoding + delivery).
    
    Args:
        athena_results_df: DataFrame with columns [rendition, total_gb]
        hours_running: Hours the channel was live (168 = 7 days 24/7)
    """
    df = athena_results_df.copy()
    
    # CloudFront delivery cost
    df['cf_cost'] = df['total_gb'] * CF_PRICE_PER_GB
    
    # MediaLive encoding cost (fixed regardless of viewers)
    df['ml_cost'] = df['rendition'].map(ENCODING_COST_PER_HOUR) * hours_running
    
    # Total
    df['total_cost'] = df['cf_cost'] + df['ml_cost']
    df['cost_per_viewer'] = df['total_cost'] / df['unique_viewers']
    
    return df.sort_values('total_cost', ascending=False)


def generate_recommendations(cost_df, min_request_pct=3.0):
    """
    Generate optimization recommendations based on usage and cost.
    
    Rules:
    - ELIMINATE: < min_request_pct of total requests
    - OPTIMIZE: Bandwidth % more than 2x request % (bitrate too high)
    - KEEP: Healthy usage-to-cost ratio
    """
    recommendations = []
    
    for _, row in cost_df.iterrows():
        if row['request_pct'] < min_request_pct:
            recommendations.append({
                'rendition': row['rendition'],
                'action': 'ELIMINATE',
                'reason': f"Only {row['request_pct']:.1f}% of requests. "
                          f"Removing saves ${row['total_cost']:.2f}/week "
                          f"(${row['total_cost']*52:.0f}/year).",
            })
        elif row.get('bandwidth_pct', 0) > row['request_pct'] * 2:
            recommendations.append({
                'rendition': row['rendition'],
                'action': 'OPTIMIZE',
                'reason': f"Bandwidth ({row['bandwidth_pct']:.1f}%) is "
                          f"disproportionate to requests ({row['request_pct']:.1f}%). "
                          f"Consider reducing bitrate.",
            })
        else:
            recommendations.append({
                'rendition': row['rendition'],
                'action': 'KEEP',
                'reason': f"Healthy adoption ({row['request_pct']:.1f}%) "
                          f"with proportional cost.",
            })
    
    return recommendations

Step 7: Automate with a Weekly Report (Optional)

Set up a scheduled analysis using AWS Lambda + EventBridge:

import boto3
import json
from datetime import datetime, timedelta

def lambda_handler(event, context):
    """
    Weekly rendition usage analysis.
    Triggered by EventBridge rule: rate(7 days)
    """
    athena = boto3.client('athena')
    sns = boto3.client('sns')
    
    # Run Athena query for the past 7 days
    end_date = datetime.now().strftime('%Y-%m-%d')
    start_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
    
    query = f"""
    SELECT
      REGEXP_EXTRACT(cs_uri_stem, '/([0-9]+p[^/]*)/', 1) AS rendition,
      COUNT(*) AS segment_requests,
      SUM(sc_bytes) / POWER(1024, 3) AS total_gb,
      COUNT(DISTINCT c_ip) AS unique_viewers,
      ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) AS request_pct
    FROM cloudfront_hls_logs
    WHERE cs_uri_stem LIKE '%.ts'
      AND sc_status = 200
      AND "date" BETWEEN DATE '{start_date}' AND DATE '{end_date}'
    GROUP BY 1
    ORDER BY segment_requests DESC
    """
    
    response = athena.start_query_execution(
        QueryString=query,
        QueryExecutionContext={'Database': 'default'},
        ResultConfiguration={
            'OutputLocation': 's3://my-athena-results/rendition-analysis/'
        }
    )
    
    # Process results and send SNS notification with recommendations
    # (Add polling for query completion, result parsing, and SNS publish)
    
    return {
        'statusCode': 200,
        'queryExecutionId': response['QueryExecutionId']
    }

EventBridge rule (Terraform/CloudFormation snippet):

RenditionAnalysisRule:
  Type: AWS::Events::Rule
  Properties:
    ScheduleExpression: "rate(7 days)"
    Targets:
      - Arn: !GetAtt RenditionAnalysisFunction.Arn
        Id: WeeklyRenditionAnalysis

Step 8: Visualize with Amazon QuickSight

For ongoing monitoring, create a QuickSight dashboard connected to your Athena table. This section provides the full reproducible steps to set up the dashboard on your own account.

Enter image description here

8.1 Prerequisites for QuickSight

  1. Sign up for QuickSight (if not already): Go to the QuickSight Console → choose Enterprise Edition (required for Athena data source)
  2. Grant QuickSight access to your S3 log bucket: QuickSight Console → Manage QuickSight → Security & Permissions → Add S3 bucket access → select your my-cf-access-logs bucket and your Athena results bucket
  3. Grant QuickSight access to Athena: In the same Security & Permissions page, ensure Amazon Athena is checked

8.2 Create the Athena Dataset

  1. In QuickSight, click DatasetsNew Dataset
  2. Choose Athena as the data source
  3. Data source name: cloudfront-hls-rendition-logs
  4. Athena workgroup: primary (or your configured workgroup)
  5. Click Validate connectionCreate data source
  6. Select your database (e.g., default) → choose the cloudfront_hls_logs table
  7. Select Import to SPICE for quicker analytics (recommended for large log volumes)
  8. Click Edit/Preview Data before finishing

8.3 Add Calculated Fields

In the dataset editor, add these calculated fields:

Rendition (extracted from URI) — if using named paths:

ifelse(
  locate({cs_uri_stem}, '1080p') > 0, '1080p',
  locate({cs_uri_stem}, '720p') > 0, '720p',
  locate({cs_uri_stem}, '480p') > 0, '480p',
  locate({cs_uri_stem}, '360p') > 0, '360p',
  locate({cs_uri_stem}, '240p') > 0, '240p',
  'unknown'
)

Rendition (extracted from URI) — if using generic output names (from manifest parsing):

# Extract output prefix from URI like /live/channel1/output_1/segment_00001.ts
# Then join with the rendition_lookup dataset (see Step 2B)
split({cs_uri_stem}, '/', 4)

After creating this field, add a lookup join: Datasets → your dataset → click "Add data" → join with a rendition_lookup CSV you upload (output from the manifest parser). Join on uri_prefix = this calculated field.

Bytes in GB:

{sc_bytes} / 1073741824

Is Successful Request:

ifelse({sc_status} = 200, 'Yes', 'No')

Is Segment Request (.ts):

ifelse(locate({cs_uri_stem}, '.ts') > 0, 'Yes', 'No')

Hour of Day:

substring({time}, 1, 2)

Click Save & Publish → confirm SPICE import.

8.4 Create the Analysis (Dashboard)

  1. Go to AnalysesNew Analysis → select your cloudfront-hls-rendition-logs dataset
  2. Build the following visuals on a single sheet:

Visual 1: Rendition Request Distribution (Donut Chart)

  • Visual type: Donut chart
  • Group/Color: rendition (calculated field)
  • Value: Count of records
  • Filter: Is Segment Request = Yes AND Is Successful Request = Yes
  • Title: "Request Distribution by Rendition"

Visual 2: Bandwidth per Rendition (Horizontal Bar)

  • Visual type: Horizontal bar chart
  • Y-axis: rendition
  • Value: Sum of Bytes in GB
  • Filter: same as Visual 1
  • Sort: by value descending
  • Title: "Bandwidth Consumption (GB) per Rendition"

Visual 3: Daily Trend by Rendition (Line Chart)

  • Visual type: Line chart
  • X-axis: date (aggregate by Day)
  • Value: Count of records
  • Color/Group: rendition
  • Filter: same as Visual 1
  • Title: "Daily Request Volume by Rendition"

Visual 4: Geographic Distribution (Heat Map)

  • Visual type: Heat map
  • Rows: rendition
  • Columns: x_edge_location
  • Values: Count of records
  • Filter: same as Visual 1
  • Title: "Rendition Popularity by Edge Location"

Visual 5: Hourly Usage Pattern (Stacked Area)

  • Visual type: Stacked area chart
  • X-axis: Hour of Day (sorted ascending 00–23)
  • Value: Count of records
  • Color/Group: rendition
  • Filter: same as Visual 1
  • Title: "Rendition Requests by Hour of Day"

Visual 6: KPI Cards (Top row of the dashboard)

Add KPI visuals for:

  • Total Requests: Count of records, filtered to .ts + status 200
  • Total Data Transferred: Sum of Bytes in GB
  • Unique Viewers: Count Distinct of c_ip

8.4b Per-Channel Breakdown Visuals

If you serve content from multiple MediaLive channels through the same CloudFront distribution, add these additional visuals to see per-source rendition usage:

Visual 7: Per-Channel Rendition Usage (Grouped Bar Chart)

  • Visual type: Clustered bar chart
  • X-axis: channel_name (from the joined rendition_lookup)
  • Value: Count of records
  • Group/Color: rendition
  • Filter: same base filters (.ts + status 200)
  • Title: "Rendition Distribution per Channel"

Visual 8: Source Service Split (Pie Chart)

  • Visual type: Pie chart
  • Group: source_service (based on channel identifier in the URI path)
  • Value: Count of records
  • Title: "Traffic by Edge Location"

Calculated field for source_service:

ifelse(
  locate({cs_uri_stem}, '/live/') > 0, 'MediaLive (Live)',
  locate({cs_uri_stem}, '/vod/') > 0, '',
  'Other'
)

Visual 9: Cost Attribution per Channel (Stacked Bar)

  • Visual type: Stacked bar chart
  • X-axis: channel_name
  • Value: Sum of Bytes in GB (proxy for cost — multiply by your CF rate in calculated field)
  • Group/Color: rendition
  • Sort: by total value descending
  • Title: "Bandwidth Cost Attribution per Channel"

Calculated field for estimated weekly cost:

{Bytes in GB} * 0.085

Visual 10: Unused Renditions Alert Table

  • Visual type: Table
  • Columns: channel_name, rendition, request_pct (calculated), estimated_cost
  • Conditional formatting: highlight rows where request_pct < 3% in red
  • Title: "⚠️ Low-Usage Renditions (< 3% of channel traffic)"
  • Sort: by request_pct ascending

Calculated field for request_pct within channel:

countOver({cs_uri_stem}, [{rendition}, {channel_name}], PRE_AGG)
/ countOver({cs_uri_stem}, [{channel_name}], PRE_AGG)
* 100

Tip: Pin the Per-Channel Breakdown and Alert Table visuals at the top of your dashboard for quick at-a-glance identification of optimization opportunities across all your encoding workflows.

8.4c Dashboard Layout with Multi-Source Views

┌──────────────────────────────────────────────────────────────────┐
│  [Date Range]  [Rendition]  [Source Service]  [Channel] │
├──────────┬───────────────┬──────────────────┬────────────────────┤
│ KPI:     │ KPI:          │ KPI:             │ KPI:               │
│ Requests │ Data (GB)     │ Unique Viewers   │ Active Channels    │
├──────────┴───────┬───────┴──────────────────┴────────────────────┤
│ Pie: Traffic     │ Grouped Bar: Rendition per Channel   │
│       │                                               │
├──────────────────┴───────────────────────────────────────────────┤
│ Stacked Bar: Cost Attribution per Channel                        │
├──────────────────────────────────────────────────────────────────┤
│ ⚠️  TABLE: Low-Usage Renditions (< 3% within channel)           │
├──────────────────────────────────────────────────────────────────┤
│ Line Chart: Daily Trend by Rendition (all channels)              │
├──────────────────────────────────────────────────────────────────┤
│ Heat Map: Edge Location × Rendition                              │
└──────────────────────────────────────────────────────────────────┘

Enter image description here

8.5 Add Interactive Filters (Dashboard Controls)

Add a Date Range filter control at the top:

  1. Click Filter pane → Add filter → select date
  2. Set filter type: Date & time range → relative → default "Last 7 days"
  3. Click the filter → Add to sheet to make it an interactive date picker

Add a Rendition dropdown filter:

  1. Add filter → select rendition → filter type: Filter list
  2. Click → Add to sheet → renders as a multi-select dropdown
  3. Default: all renditions selected

Add an Edge Location dropdown filter (optional):

  1. Add filter → x_edge_location → filter type: Filter list
  2. Add to sheet → allows geographic drill-down

8.5b Add a Recommendations Visual

To surface optimization recommendations directly in the dashboard (so customers can see KEEP / ELIMINATE / REVIEW at a glance), add a dynamic recommendations field and a table visual:

Calculated field — Recommendation:

This uses a dynamic threshold — any rendition with less than 3% of traffic within its channel is flagged for elimination:

ifelse(
  percentOfTotal(count({cs_uri_stem}), [{Rendition}]) < 3, '🔴 ELIMINATE',
  percentOfTotal(count({cs_uri_stem}), [{Rendition}]) < 5, '🟡 REVIEW',
  '🟢 KEEP'
)

Note: If percentOfTotal is not available in your QuickSight edition, use a simpler static threshold based on your known data:

ifelse(
  {Rendition} = '720p30_3.4M', '🔴 ELIMINATE (< 2% usage)',
  {Rendition} = '480p30_1.8M', '🔴 ELIMINATE (< 1% usage)',
  {Rendition} = '240p30_1.0M', '🟡 REVIEW (possible ABR startup only)',
  '🟢 KEEP'
)

Calculated field — Potential_Savings:

ifelse(
  {Rendition} = '720p30_3.4M', 90.72,
  {Rendition} = '480p30_1.8M', 45.36,
  {Rendition} = '240p30_1.0M', 23.52,
  0
)

Adjust these values based on your actual MediaLive encoding cost per rendition per week.

Recommendations Table Visual

  • Visual type: Table
  • Columns: Rendition, Count (as "Requests"), Bytes_GB (Sum), Recommendation, Potential_Savings
  • Sort: by Recommendation (🔴 first, then 🟡, then 🟢)
  • Conditional formatting:- Recommendation column: red background for cells containing "ELIMINATE", yellow for "REVIEW", green for "KEEP"
  • Title: "⚠️ Rendition Optimization Recommendations"

Recommendations KPI Panel

Add a KPI visual showing total potential weekly savings:

  • Value: Sum of Potential_Savings
  • Title: "Potential Weekly Savings"
  • Comparison: show as "$amount/week" using number formatting (prefix: $, suffix: /week)

Final Dashboard Layout with Recommendations

┌──────────────────────────────────────────────────────────────────┐
│  [Date Range]  [Rendition]  [Edge Location]                      │
├──────────┬───────────────┬──────────────────┬────────────────────┤
│ KPI:     │ KPI:          │ KPI:             │ KPI:               │
│ Requests │ Data (GB)     │ Unique Viewers   │ 💰 Savings/week    │
├──────────┴───────────────┴──────────────────┴────────────────────┤
│ ⚠️  TABLE: Rendition Recommendations (KEEP / ELIMINATE / REVIEW) │
├──────────────────┬───────────────────────────────────────────────┤
│ Donut: Request % │ Bar: Bandwidth per Rendition                  │
│ by Rendition     │                                               │
├──────────────────┴───────────────────────────────────────────────┤
│ Line Chart: Daily Trend by Rendition                             │
├──────────────────────────────────────────────────────────────────┤
│ Bar: Edge Location Volume │ Donut: Cache Hit/Miss                │
├──────────────────────────────────────────────────────────────────┤
│ Bar: Hourly Pattern       │ Bar: Bytes by Rendition + Success    │
└──────────────────────────────────────────────────────────────────┘

Tip: Place the Recommendations table at the TOP of the dashboard — it's the most actionable insight and the first thing a customer should see.

8.6 Publish as Dashboard

  1. In the Analysis, click SharePublish Dashboard
  2. Dashboard name: HLS Rendition Usage Advisor
  3. Set permissions:- Share with specific users/groups if you want team access- Or keep Private for personal use
  4. Click Publish

8.7 Configure Automatic SPICE Data Refresh

To keep the dashboard up to date without manual intervention:

  1. Go to Datasets → select cloudfront-hls-rendition-logs
  2. Click the Refresh tab → Add new schedule
  3. Configure:- Frequency: Daily- Starting at: 06:00 UTC (adjust to run before your morning review)- Time zone: Select your timezone- Refresh type: Full refresh (or Incremental if you've partitioned by date — reduces SPICE scan cost)
  4. Click Create

SPICE cost note: For 50K–100K new records per week, SPICE usage fits comfortably within the free tier (1 GB free per author). At scale (millions of records/month), consider switching to Direct Query mode — it queries Athena live instead of importing to SPICE, avoiding SPICE storage costs but with slightly slower dashboard load times.

8.8 QuickSight Dashboard Layout Reference

┌──────────────────────────────────────────────────────────────────┐
│  [Date Range Filter]  [Rendition Filter]  [Edge Location Filter] │
├──────────┬───────────────┬───────────────────────────────────────┤
│ KPI:     │ KPI:          │ KPI:                                  │
│ Requests │ Data (GB)     │ Unique Viewers                        │
├──────────┴───────┬───────┴───────────────────────────────────────┤
│ Donut Chart:     │ Horizontal Bar Chart:                         │
│ Request %        │ Bandwidth per Rendition                       │
│ by Rendition     │                                               │
├──────────────────┴───────────────────────────────────────────────┤
│ Line Chart: Daily Trend by Rendition                             │
├──────────────────────────────────────────────────────────────────┤
│ Heat Map: Edge Location × Rendition                              │
├──────────────────────────────────────────────────────────────────┤
│ Stacked Area: Hourly Pattern                                     │
└──────────────────────────────────────────────────────────────────┘

Example Results Interpretation

After running this analysis on a production channel, you might see results like:

RenditionRequests% TotalBandwidthWeekly CostRecommendation
720p22,54945.1%7.44 GB$91.35KEEP — primary rendition
480p12,51725.0%2.04 GB$45.53KEEP — strong mobile usage
1080p7,45914.9%4.97 GB$181.86OPTIMIZE — reduce bitrate
360p5,00110.0%0.44 GB$23.56KEEP — serves low-bandwidth users
240p1,4282.9%0.06 GB$11.77ELIMINATE — save $612/year
1080p_alt1,0462.1%0.53 GB$181.49ELIMINATE — save $9,437/year

Key insight — Cost optimization: In this example, two renditions (240p and 1080p_alt) serve less than 5% of viewers combined but account for 36% of total encoding costs. Removing them saves over $10,000/year with minimal viewer impact.

Key insight — Viewer experience: If 720p accounts for 45% of all requests, your audience has bandwidth for HD content. This signals an opportunity to:

  • Add a 1080p rendition — viewers consuming 720p on high-bandwidth connections will automatically upgrade, improving their experience
  • Optimize the 720p bitrate ladder — add a step between 720p@3Mbps and 1080p@5Mbps to reduce buffering during ABR transitions
  • Reallocate encoding budget — the savings from eliminating 240p and 1080p_alt ($10K/year) can fund the additional 1080p rendition with better quality

Best Practices

  1. Analyze at least 7 days of data — daily and hourly patterns vary significantly. Weekend vs. weekday and prime-time vs. off-peak can shift rendition preferences.
  2. Segment by geography — viewers in regions with lower average bandwidth (emerging markets) may rely heavily on lower renditions. Eliminating a low-usage rendition globally might impact specific regions disproportionately.
  3. Consider player ABR logic — some players briefly request a low rendition on startup before switching up. Filter out very short sessions (< 3 segments from same IP) to avoid counting these as sustained usage.
  4. Re-run after changes — after eliminating or adding renditions, wait 2–4 weeks and re-analyze. ABR players will redistribute to available renditions, and you'll see the new equilibrium.
  5. Factor in content type — sports and high-motion content benefits more from higher bitrates than talk shows or news. Consider per-channel analysis rather than global averages.
  6. Monitor CloudWatch MediaLive metrics — after removing a rendition, watch Output4xxErrors and OutputMediaDataMissing to confirm no negative viewer impact.

Conclusion

By correlating CloudFront access patterns with your MediaLive encoding configuration, you can make data-driven decisions about your ABR rendition ladder. Common operational patterns show that most deployments can safely eliminate 1–2 underused renditions, typically saving 15–35% on encoding costs while maintaining or improving viewer experience.

This approach works for any customer using the MediaLive → CloudFront delivery pattern. Future iterations could extend to MediaPackage origins or incorporate real-time ABR session analytics from player-side telemetry.

Related Resources

## CFN Template 

AWSTemplateFormatVersion: '2010-09-09'
Description: >
  HLS Rendition Usage Advisor — Analysis Pipeline
  Deploys the monitoring and analysis layer for correlating CloudFront access logs
  with MediaLive/MediaConvert HLS rendition configurations.
  Prerequisite: You already have MediaLive/MediaConvert outputting to S3 with CloudFront serving the content.

# =============================================================================
# PARAMETERS — Customer fills in their existing resource IDs
# =============================================================================
Parameters:
  CloudFrontDistributionId:
    Type: String
    Description: 'Your existing CloudFront distribution ID (e.g., E1ABC2DEF3GHIJ)'
    AllowedPattern: 'E[A-Z0-9]+'
    ConstraintDescription: 'Must be a valid CloudFront distribution ID starting with E'

  ChannelName:
    Type: String
    Default: 'channel1'
    Description: 'Friendly name for your MediaLive/MediaConvert channel (used in log prefix and naming)'

  BucketPrefix:
    Type: String
    Default: 'rendition-advisor'
    Description: 'Prefix for S3 bucket names (must be lowercase, no uppercase characters)'
    AllowedPattern: '[a-z0-9][a-z0-9.-]*'
    ConstraintDescription: 'Must be lowercase alphanumeric, hyphens, or dots (S3 naming rules)'

  DatabaseName:
    Type: String
    Default: 'rendition_advisor_db'
    Description: 'Glue/Athena database name (lowercase, alphanumeric and underscores only — no hyphens)'
    AllowedPattern: '[a-z0-9_]+'
    ConstraintDescription: 'Must contain only lowercase letters, numbers, and underscores'

  NotificationEmail:
    Type: String
    Default: ''
    Description: '(Optional) Email address for weekly rendition report notifications. Leave blank to skip.'

  LogRetentionDays:
    Type: Number
    Default: 90
    Description: 'Days to retain CloudFront access logs before auto-deletion (cost optimization)'
    AllowedValues: [30, 60, 90, 180, 365]

  ExistingLogBucket:
    Type: String
    Default: ''
    Description: >
      (Optional) If CloudFront is already logging to an S3 bucket, enter the bucket name here.
      The template will point Athena directly at your existing logs — no new bucket created.
      Leave blank to create a new dedicated log bucket.

  ExistingLogPrefix:
    Type: String
    Default: ''
    Description: >
      (Optional) Prefix in the existing bucket where CF logs are stored (e.g., "cf-logs/distribution-id/").
      Only used when ExistingLogBucket is provided.

  LogType:
    Type: String
    Default: 'standard'
    AllowedValues: ['standard', 'realtime-v2']
    Description: >
      Type of CloudFront logs in the bucket.
      "standard" = CloudFront Standard Logging (v1).
      "realtime-v2" = Real-time logs delivered to S3 via Kinesis Data Firehose.

  EnableWeeklyAutomation:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']
    Description: 'Deploy the Lambda + EventBridge weekly automation? Set false for manual-only analysis.'

Conditions:
  HasNotificationEmail: !Not [!Equals [!Ref NotificationEmail, '']]
  DeployAutomation: !Equals [!Ref EnableWeeklyAutomation, 'true']
  CreateNewLogBucket: !Equals [!Ref ExistingLogBucket, '']
  UseExistingBucket: !Not [!Equals [!Ref ExistingLogBucket, '']]
  IsRealtimeLogs: !Equals [!Ref LogType, 'realtime-v2']

# =============================================================================
# RESOURCES
# =============================================================================
Resources:

  # ---------------------------------------------------------------------------
  # S3 Bucket: CloudFront Access Logs (only created if no existing bucket provided)
  # ---------------------------------------------------------------------------
  CloudFrontLogsBucket:
    Type: AWS::S3::Bucket
    Condition: CreateNewLogBucket
    Properties:
      BucketName: !Sub '${BucketPrefix}-cf-logs-${AWS::AccountId}'
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerPreferred
      LifecycleConfiguration:
        Rules:
          - Id: AutoExpireLogs
            Status: Enabled
            ExpirationInDays: !Ref LogRetentionDays
      Tags:
        - Key: Project
          Value: HLS-Rendition-Advisor
        - Key: Channel
          Value: !Ref ChannelName

  # Bucket policy to allow CloudFront to write logs (only if creating new bucket)
  CloudFrontLogsBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Condition: CreateNewLogBucket
    Properties:
      Bucket: !Ref CloudFrontLogsBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AllowCloudFrontLogging
            Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: 's3:PutObject'
            Resource: !Sub '${CloudFrontLogsBucket.Arn}/*'
            Condition:
              StringEquals:
                aws:SourceAccount: !Ref AWS::AccountId

  # ---------------------------------------------------------------------------
  # S3 Bucket: Athena Query Results
  # ---------------------------------------------------------------------------
  AthenaResultsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub '${BucketPrefix}-athena-results-${AWS::AccountId}'
      LifecycleConfiguration:
        Rules:
          - Id: ExpireQueryResults
            Status: Enabled
            ExpirationInDays: 7
      Tags:
        - Key: Project
          Value: HLS-Rendition-Advisor

  # ---------------------------------------------------------------------------
  # S3 Bucket: Rendition Lookup Data (from manifest parsing)
  # ---------------------------------------------------------------------------
  RenditionLookupBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub '${BucketPrefix}-rendition-lookup-${AWS::AccountId}'
      Tags:
        - Key: Project
          Value: HLS-Rendition-Advisor

  # ---------------------------------------------------------------------------
  # Glue Database
  # ---------------------------------------------------------------------------
  GlueDatabase:
    Type: AWS::Glue::Database
    Properties:
      CatalogId: !Ref AWS::AccountId
      DatabaseInput:
        Name: !Ref DatabaseName
        Description: 'HLS Rendition Usage Advisor — CloudFront log analytics'

  # ---------------------------------------------------------------------------
  # Glue Table: CloudFront Access Logs
  # ---------------------------------------------------------------------------
  CloudFrontLogsTable:
    Type: AWS::Glue::Table
    DependsOn: GlueDatabase
    Properties:
      CatalogId: !Ref AWS::AccountId
      DatabaseName: !Ref DatabaseName
      TableInput:
        Name: cloudfront_hls_logs
        Description: 'CloudFront standard access logs for HLS rendition analysis'
        TableType: EXTERNAL_TABLE
        Parameters:
          'skip.header.line.count': '2'
          classification: csv
        StorageDescriptor:
          Location: !If
            - UseExistingBucket
            - !Sub 's3://${ExistingLogBucket}/${ExistingLogPrefix}'
            - !Sub 's3://${CloudFrontLogsBucket}/${ChannelName}/'
          InputFormat: org.apache.hadoop.mapred.TextInputFormat
          OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat
          SerdeInfo:
            SerializationLibrary: org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe
            Parameters:
              'field.delim': "\t"
              'serialization.format': "\t"
          Columns:
            - Name: date
              Type: date
            - Name: time
              Type: string
            - Name: x_edge_location
              Type: string
            - Name: sc_bytes
              Type: bigint
            - Name: c_ip
              Type: string
            - Name: cs_method
              Type: string
            - Name: cs_host
              Type: string
            - Name: cs_uri_stem
              Type: string
            - Name: sc_status
              Type: int
            - Name: cs_referer
              Type: string
            - Name: cs_user_agent
              Type: string
            - Name: cs_uri_query
              Type: string
            - Name: cs_cookie
              Type: string
            - Name: x_edge_result_type
              Type: string
            - Name: x_edge_request_id
              Type: string
            - Name: x_host_header
              Type: string
            - Name: cs_protocol
              Type: string
            - Name: cs_bytes
              Type: bigint
            - Name: time_taken
              Type: float
            - Name: x_forwarded_for
              Type: string
            - Name: ssl_protocol
              Type: string
            - Name: ssl_cipher
              Type: string
            - Name: x_edge_response_result_type
              Type: string
            - Name: cs_protocol_version
              Type: string
            - Name: fle_status
              Type: string
            - Name: fle_encrypted_fields
              Type: int
            - Name: c_port
              Type: int
            - Name: time_to_first_byte
              Type: float
            - Name: x_edge_detailed_result_type
              Type: string
            - Name: sc_content_type
              Type: string
            - Name: sc_content_len
              Type: bigint
            - Name: sc_range_start
              Type: bigint
            - Name: sc_range_end
              Type: bigint


  # ---------------------------------------------------------------------------
  # Glue Table: CloudFront Real-Time Logs v2 (conditional — only if LogType=realtime-v2)
  # ---------------------------------------------------------------------------
  CloudFrontRealtimeLogsTable:
    Type: AWS::Glue::Table
    Condition: IsRealtimeLogs
    DependsOn: GlueDatabase
    Properties:
      CatalogId: !Ref AWS::AccountId
      DatabaseName: !Ref DatabaseName
      TableInput:
        Name: cloudfront_realtime_logs
        Description: 'CloudFront real-time logs (v2) delivered via Kinesis Data Firehose'
        TableType: EXTERNAL_TABLE
        Parameters:
          classification: json
        StorageDescriptor:
          Location: !If
            - UseExistingBucket
            - !Sub 's3://${ExistingLogBucket}/${ExistingLogPrefix}'
            - !Sub 's3://${CloudFrontLogsBucket}/${ChannelName}/'
          InputFormat: org.apache.hadoop.mapred.TextInputFormat
          OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat
          SerdeInfo:
            SerializationLibrary: org.openx.data.jsonserde.JsonSerDe
          Columns:
            - Name: timestamp
              Type: bigint
              Comment: 'Unix epoch in milliseconds'
            - Name: c-ip
              Type: string
            - Name: sc-status
              Type: int
            - Name: cs-uri-stem
              Type: string
            - Name: sc-bytes
              Type: bigint
            - Name: cs-user-agent
              Type: string
            - Name: x-edge-location
              Type: string
            - Name: x-edge-result-type
              Type: string
            - Name: cs-host
              Type: string
            - Name: time-taken
              Type: float
            - Name: sc-content-type
              Type: string
            - Name: sc-range-start
              Type: bigint
            - Name: sc-range-end
              Type: bigint

  # ---------------------------------------------------------------------------
  # Glue Table: Rendition Lookup (from HLS manifest parsing)
  # ---------------------------------------------------------------------------
  RenditionLookupTable:
    Type: AWS::Glue::Table
    DependsOn: GlueDatabase
    Properties:
      CatalogId: !Ref AWS::AccountId
      DatabaseName: !Ref DatabaseName
      TableInput:
        Name: rendition_lookup
        Description: 'Rendition mapping from HLS parent manifest parsing'
        TableType: EXTERNAL_TABLE
        Parameters:
          classification: json
        StorageDescriptor:
          Location: !Sub 's3://${RenditionLookupBucket}/lookup/'
          InputFormat: org.apache.hadoop.mapred.TextInputFormat
          OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat
          SerdeInfo:
            SerializationLibrary: org.openx.data.jsonserde.JsonSerDe
          Columns:
            - Name: uri_prefix
              Type: string
            - Name: resolution
              Type: string
            - Name: bandwidth
              Type: bigint
            - Name: label
              Type: string

  # ---------------------------------------------------------------------------
  # Athena Workgroup
  # ---------------------------------------------------------------------------
  AthenaWorkgroup:
    Type: AWS::Athena::WorkGroup
    Properties:
      Name: !Sub '${AWS::StackName}-workgroup'
      Description: 'HLS Rendition Usage Advisor queries'
      WorkGroupConfiguration:
        ResultConfiguration:
          OutputLocation: !Sub 's3://${AthenaResultsBucket}/results/'
        EnforceWorkGroupConfiguration: true
        PublishCloudWatchMetricsEnabled: true

  # ---------------------------------------------------------------------------
  # Athena Named Queries (ready to run from the console)
  # ---------------------------------------------------------------------------
  QueryRenditionDistribution:
    Type: AWS::Athena::NamedQuery
    Properties:
      Name: 'Rendition Usage Distribution'
      Description: 'Shows request count, bandwidth, and viewer percentage per rendition'
      Database: !Ref DatabaseName
      WorkGroup: !Ref AthenaWorkgroup
      QueryString: !Sub |
        -- HLS Rendition Usage Distribution (last 7 days)
        SELECT
          COALESCE(
                REGEXP_EXTRACT(cs_uri_stem, '/([0-9]+p[^/]*|[0-9]+k[^/]*)/', 1),
                REGEXP_EXTRACT(cs_uri_stem, '(index_[0-9]+)', 1)
              ) AS rendition,
          COUNT(*) AS segment_requests,
          ROUND(SUM(sc_bytes) / POWER(1024, 3), 2) AS total_gb,
          COUNT(DISTINCT c_ip) AS unique_viewers,
          ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) AS request_pct
        FROM cloudfront_hls_logs
        WHERE cs_uri_stem LIKE '%.ts'
          AND sc_status = 200
          AND "date" >= CURRENT_DATE - INTERVAL '7' DAY
        GROUP BY COALESCE(
                REGEXP_EXTRACT(cs_uri_stem, '/([0-9]+p[^/]*|[0-9]+k[^/]*)/', 1),
                REGEXP_EXTRACT(cs_uri_stem, '(index_[0-9]+)', 1)
              )
        ORDER BY segment_requests DESC;

  QueryRenditionWithLookup:
    Type: AWS::Athena::NamedQuery
    Properties:
      Name: 'Rendition Usage (with Manifest Lookup)'
      Description: 'For channels with generic output names — joins with rendition_lookup table'
      Database: !Ref DatabaseName
      WorkGroup: !Ref AthenaWorkgroup
      QueryString: !Sub |
        -- HLS Rendition Usage with Manifest-Based Lookup (last 7 days)
        SELECT
          rl.label AS rendition,
          rl.resolution,
          ROUND(rl.bandwidth / 1000000.0, 1) AS bitrate_mbps,
          COUNT(*) AS segment_requests,
          ROUND(SUM(cf.sc_bytes) / POWER(1024, 3), 2) AS total_gb,
          COUNT(DISTINCT cf.c_ip) AS unique_viewers,
          ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) AS request_pct
        FROM cloudfront_hls_logs cf
        JOIN rendition_lookup rl
          ON REGEXP_EXTRACT(cf.cs_uri_stem, '(index_[0-9]+|[^/]+)(?=_[0-9]+\.ts|\.ts)', 1) = rl.uri_prefix
        WHERE cf.cs_uri_stem LIKE '%.ts'
          AND cf.sc_status = 200
          AND cf."date" >= CURRENT_DATE - INTERVAL '7' DAY
        GROUP BY rl.label, rl.resolution, rl.bandwidth
        ORDER BY segment_requests DESC;

  QueryHourlyBreakdown:
    Type: AWS::Athena::NamedQuery
    Properties:
      Name: 'Rendition Usage by Hour'
      Description: 'Identifies peak and off-peak rendition consumption patterns'
      Database: !Ref DatabaseName
      WorkGroup: !Ref AthenaWorkgroup
      QueryString: !Sub |
        -- Hourly Rendition Usage Pattern
        SELECT
          REGEXP_EXTRACT(cs_uri_stem, '/([0-9]+p[^/]*)/', 1) AS rendition,
          SUBSTR("time", 1, 2) AS hour_of_day,
          COUNT(*) AS requests,
          ROUND(SUM(sc_bytes) / POWER(1024, 3), 3) AS gb
        FROM cloudfront_hls_logs
        WHERE cs_uri_stem LIKE '%.ts'
          AND sc_status = 200
          AND "date" >= CURRENT_DATE - INTERVAL '7' DAY
        GROUP BY 1, 2
        ORDER BY rendition, hour_of_day;

  QueryGeographicDistribution:
    Type: AWS::Athena::NamedQuery
    Properties:
      Name: 'Rendition Usage by Edge Location'
      Description: 'Geographic distribution of rendition requests'
      Database: !Ref DatabaseName
      WorkGroup: !Ref AthenaWorkgroup
      QueryString: !Sub |
        -- Geographic Distribution of Rendition Requests
        SELECT
          REGEXP_EXTRACT(cs_uri_stem, '/([0-9]+p[^/]*)/', 1) AS rendition,
          x_edge_location,
          COUNT(*) AS requests,
          ROUND(SUM(sc_bytes) / POWER(1024, 3), 3) AS total_gb
        FROM cloudfront_hls_logs
        WHERE cs_uri_stem LIKE '%.ts'
          AND sc_status = 200
          AND "date" >= CURRENT_DATE - INTERVAL '7' DAY
        GROUP BY 1, 2
        HAVING COUNT(*) > 100
        ORDER BY rendition, requests DESC;

  # ---------------------------------------------------------------------------
  # SNS Topic for Notifications (conditional)
  # ---------------------------------------------------------------------------
  NotificationTopic:
    Type: AWS::SNS::Topic
    Condition: HasNotificationEmail
    Properties:
      TopicName: !Sub '${AWS::StackName}-notifications'
      Tags:
        - Key: Project
          Value: HLS-Rendition-Advisor

  NotificationSubscription:
    Type: AWS::SNS::Subscription
    Condition: HasNotificationEmail
    Properties:
      TopicArn: !Ref NotificationTopic
      Protocol: email
      Endpoint: !Ref NotificationEmail

  # ---------------------------------------------------------------------------
  # Lambda: Weekly Rendition Analysis (conditional)
  # ---------------------------------------------------------------------------
  AnalysisLambdaRole:
    Type: AWS::IAM::Role
    Condition: DeployAutomation
    Properties:
      RoleName: !Sub '${AWS::StackName}-lambda-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: RenditionAnalysisPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - athena:StartQueryExecution
                  - athena:GetQueryExecution
                  - athena:GetQueryResults
                Resource: '*'
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                  - s3:GetBucketLocation
                  - s3:ListBucket
                Resource:
                  - !GetAtt AthenaResultsBucket.Arn
                  - !Sub '${AthenaResultsBucket.Arn}/*'
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:ListBucket
                Resource:
                  - !If
                    - UseExistingBucket
                    - !Sub 'arn:aws:s3:::${ExistingLogBucket}'
                    - !GetAtt CloudFrontLogsBucket.Arn
                  - !If
                    - UseExistingBucket
                    - !Sub 'arn:aws:s3:::${ExistingLogBucket}/*'
                    - !Sub '${CloudFrontLogsBucket.Arn}/*'
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:ListBucket
                  - s3:GetBucketLocation
                Resource:
                  - !GetAtt RenditionLookupBucket.Arn
                  - !Sub '${RenditionLookupBucket.Arn}/*'
              - Effect: Allow
                Action:
                  - glue:GetTable
                  - glue:GetDatabase
                Resource: '*'
              - !If
                - HasNotificationEmail
                - Effect: Allow
                  Action: sns:Publish
                  Resource: !Ref NotificationTopic
                - !Ref AWS::NoValue
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: 'arn:aws:logs:*:*:*'

  AnalysisLambdaFunction:
    Type: AWS::Lambda::Function
    Condition: DeployAutomation
    Properties:
      FunctionName: !Sub '${AWS::StackName}-weekly-analysis'
      Runtime: python3.12
      Handler: index.handler
      Role: !GetAtt AnalysisLambdaRole.Arn
      Timeout: 300
      MemorySize: 256
      Environment:
        Variables:
          DATABASE_NAME: !Ref DatabaseName
          RESULTS_BUCKET: !Ref AthenaResultsBucket
          WORKGROUP: !Ref AthenaWorkgroup
          SNS_TOPIC_ARN: !If [HasNotificationEmail, !Ref NotificationTopic, '']
      Code:
        ZipFile: |
          import boto3
          import json
          import os
          import time
          from datetime import datetime, timedelta

          athena = boto3.client('athena')
          sns = boto3.client('sns')

          def handler(event, context):
              database = os.environ['DATABASE_NAME']
              workgroup = os.environ['WORKGROUP']
              results_bucket = os.environ['RESULTS_BUCKET']
              sns_topic = os.environ.get('SNS_TOPIC_ARN', '')

              # Query: rendition distribution for last 7 days
              query = """
              SELECT
                COALESCE(
                REGEXP_EXTRACT(cs_uri_stem, '/([0-9]+p[^/]*|[0-9]+k[^/]*)/', 1),
                REGEXP_EXTRACT(cs_uri_stem, '(index_[0-9]+)', 1)
              ) AS rendition,
                COUNT(*) AS segment_requests,
                ROUND(SUM(sc_bytes) / POWER(1024, 3), 2) AS total_gb,
                COUNT(DISTINCT c_ip) AS unique_viewers,
                ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) AS request_pct
              FROM cloudfront_hls_logs
              WHERE cs_uri_stem LIKE '%.ts'
                AND sc_status = 200
                AND "date" >= CURRENT_DATE - INTERVAL '7' DAY
              GROUP BY COALESCE(
                REGEXP_EXTRACT(cs_uri_stem, '/([0-9]+p[^/]*|[0-9]+k[^/]*)/', 1),
                REGEXP_EXTRACT(cs_uri_stem, '(index_[0-9]+)', 1)
              )
              ORDER BY segment_requests DESC
              """

              # Execute query
              response = athena.start_query_execution(
                  QueryString=query,
                  QueryExecutionContext={'Database': database},
                  WorkGroup=workgroup
              )
              query_id = response['QueryExecutionId']

              # Poll for completion
              while True:
                  status = athena.get_query_execution(QueryExecutionId=query_id)
                  state = status['QueryExecution']['Status']['State']
                  if state in ('SUCCEEDED', 'FAILED', 'CANCELLED'):
                      break
                  time.sleep(2)

              if state != 'SUCCEEDED':
                  raise Exception(f"Query failed: {state}")

              # Get results
              results = athena.get_query_results(QueryExecutionId=query_id)
              rows = results['ResultSet']['Rows'][1:]  # skip header

              # Build report
              report_lines = ["HLS Rendition Usage Report", "=" * 40, ""]
              report_lines.append(f"Period: Last 7 days (ending {datetime.now().strftime('%Y-%m-%d')})")
              report_lines.append("")
              report_lines.append(f"{'Rendition':<12} {'Requests':>10} {'GB':>8} {'Viewers':>8} {'%':>7}")
              report_lines.append("-" * 50)

              low_usage = []
              for row in rows:
                  data = [col.get('VarCharValue', '') for col in row['Data']]
                  rendition, requests, gb, viewers, pct = data
                  report_lines.append(f"{rendition:<12} {requests:>10} {gb:>8} {viewers:>8} {pct:>6}%")
                  if float(pct) < 3.0:
                      low_usage.append(rendition)

              report_lines.append("")
              if low_usage:
                  report_lines.append("⚠️  RECOMMENDATIONS:")
                  for r in low_usage:
                      report_lines.append(f"  - ELIMINATE '{r}' (< 3% usage)")
              else:
                  report_lines.append("✅ All renditions have healthy usage (> 3%)")

              report = "\n".join(report_lines)
              print(report)

              # Send notification if configured
              if sns_topic:
                  sns.publish(
                      TopicArn=sns_topic,
                      Subject='Weekly HLS Rendition Usage Report',
                      Message=report
                  )

              return {'statusCode': 200, 'report': report}

  # ---------------------------------------------------------------------------
  # EventBridge: Weekly Schedule (conditional)
  # ---------------------------------------------------------------------------
  WeeklyScheduleRule:
    Type: AWS::Events::Rule
    Condition: DeployAutomation
    Properties:
      Name: !Sub '${AWS::StackName}-weekly-analysis'
      Description: 'Triggers weekly rendition usage analysis every Monday at 06:00 UTC'
      ScheduleExpression: 'cron(0 6 ? * MON *)'
      State: ENABLED
      Targets:
        - Arn: !GetAtt AnalysisLambdaFunction.Arn
          Id: WeeklyRenditionAnalysis

  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Condition: DeployAutomation
    Properties:
      FunctionName: !Ref AnalysisLambdaFunction
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt WeeklyScheduleRule.Arn

# =============================================================================
# OUTPUTS
# =============================================================================
Outputs:
  LogBucketName:
    Description: 'S3 bucket for CloudFront access logs. If you provided an existing bucket, this shows that. Otherwise, enable logging on your distribution pointing to the new bucket.'
    Value: !If
      - UseExistingBucket
      - !Sub '${ExistingLogBucket} (existing — already configured)'
      - !Ref CloudFrontLogsBucket

  LogPrefix:
    Description: 'Use this prefix when configuring CloudFront logging'
    Value: !If
      - UseExistingBucket
      - !Sub '${ExistingLogPrefix} (existing prefix — no change needed)'
      - !Sub '${ChannelName}/'

  AthenaDatabase:
    Description: 'Athena database name — use this in the Athena console'
    Value: !Ref DatabaseName

  AthenaWorkgroup:
    Description: 'Athena workgroup — select this when running queries'
    Value: !Ref AthenaWorkgroup

  RenditionLookupBucket:
    Description: 'Upload your manifest-parsed rendition lookup JSON here (at /lookup/ prefix)'
    Value: !Sub 's3://${RenditionLookupBucket}/lookup/'

  NextStep:
    Condition: CreateNewLogBucket
    Description: 'Post-deployment action (new bucket created)'
    Value: !Sub >
      1. Enable Standard Logging on CloudFront distribution ${CloudFrontDistributionId}
         pointing to bucket ${BucketPrefix}-cf-logs-${AWS::AccountId} with prefix ${ChannelName}/
      2. Wait 30 min for logs to appear
      3. Go to Athena Console → select workgroup ${AWS::StackName}-workgroup
      4. Run the pre-loaded "Rendition Usage Distribution" named query
      5. (Optional) Upload rendition_lookup.json to s3://${RenditionLookupBucket}/lookup/

  NextStepExisting:
    Condition: UseExistingBucket
    Description: 'Post-deployment action (using existing log bucket)'
    Value: !Sub >
      Your existing bucket "${ExistingLogBucket}" with prefix "${ExistingLogPrefix}" is already
      connected to Athena. No CloudFront logging changes needed!
      1. Go to Athena Console → select workgroup ${AWS::StackName}-workgroup
      2. Run the pre-loaded "Rendition Usage Distribution" named query
      3. Historical data is immediately available for analysis
      4. (Optional) Upload rendition_lookup.json to s3://${RenditionLookupBucket}/lookup/
      ```
2 Comments

I think it's not so straight, as some streams in abr ladder exist to compensate network network congestion and used really rare but still useful

AWS

replied 11 days ago

Hi Roman, The question I keep asking it is: "Do I know what my viewers are actually consuming, or am I just guessing?"

Once you have the data, you can make informed calls — maybe you keep that 240p fallback because 6 unique viewers needed it during peak congestion, or maybe you discover that a redundant 720p30 variant is costing you encoding budget that could go toward adding a 1080p rendition your viewers clearly have bandwidth for.

The goal is to review your ladder with data so you can give viewers better quality where it matters, not just cut costs. It's an example of how you can use the approach to evaluate and improve — each customer's decision will look different based on their audience. 📊

AWS
EXPERT

replied 11 days ago