HLS streaming with Ittybit and CloudFront

View Markdown

AWS MediaPackage is heavy machinery for what most teams actually need: take a video, produce an adaptive bitrate HLS stream, serve it fast. Ittybit’s adaptive_video task generates the .m3u8 manifest and .ts segments, writes them straight to your S3 bucket, and CloudFront handles the rest.

Prerequisites

  • An S3 bucket for your HLS output (e.g. my-stream-bucket)
  • An Ittybit connection configured for that bucket
  • An Ittybit API key

Create the adaptive HLS task

Point Ittybit at your source video, set the output to an s3:// path in your bucket, and let the adaptive_video kind handle the multi-bitrate packaging.

const task = {
  input: "s3://my-media-bucket/uploads/lecture.mov",
  kind: "adaptive_video",
  connection_id: "conn_abc123",
  output: "s3://my-stream-bucket/streams/lecture/",
  options: {
    format: "hls",
    quality: "high",
  },
};

const res = await fetch("https://api.ittybit.com/jobs", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ITTYBIT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(task),
});
const data = await res.json();
// data.id -> "task_xyz789"
TASK='{
  "input": "s3://my-media-bucket/uploads/lecture.mov",
  "kind": "adaptive_video",
  "connection_id": "conn_abc123",
  "output": "s3://my-stream-bucket/streams/lecture/",
  "options": {
    "format": "hls",
    "quality": "high"
  }
}'

curl -X POST https://api.ittybit.com/jobs \
  -H "Authorization: Bearer $ITTYBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$TASK"

When the task completes, your bucket contains the full HLS package:

s3://my-stream-bucket/streams/lecture/
  master.m3u8          # top-level manifest
  720p/stream.m3u8     # variant playlist
  720p/segment-0.ts
  720p/segment-1.ts
  1080p/stream.m3u8
  1080p/segment-0.ts
  1080p/segment-1.ts
  ...

Create a CloudFront distribution

Point CloudFront at your S3 bucket. Use Origin Access Control (OAC) so the bucket stays private.

# Create an OAC
aws cloudfront create-origin-access-control \
  --origin-access-control-config '{
    "Name": "hls-oac",
    "OriginAccessControlOriginType": "s3",
    "SigningBehavior": "always",
    "SigningProtocol": "sigv4"
  }'

Then create the distribution:

{
  "Origins": {
    "Items": [
      {
        "Id": "hls-s3",
        "DomainName": "my-stream-bucket.s3.us-east-1.amazonaws.com",
        "S3OriginConfig": {
          "OriginAccessIdentity": ""
        },
        "OriginAccessControlId": "<oac-id>"
      }
    ],
    "Quantity": 1
  },
  "DefaultCacheBehavior": {
    "TargetOriginId": "hls-s3",
    "ViewerProtocolPolicy": "redirect-to-https",
    "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
    "AllowedMethods": {
      "Quantity": 2,
      "Items": ["GET", "HEAD"]
    }
  },
  "Enabled": true
}

Update your bucket policy to allow CloudFront access:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudfront.amazonaws.com"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-stream-bucket/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::<account-id>:distribution/<distribution-id>"
        }
      }
    }
  ]
}

Cache behaviors for HLS

HLS manifests and segments have different caching needs. Manifests are small and may update if you reprocess content. Segments are immutable once written.

PatternTTLReason
*.m3u810 secondsManifests should refresh quickly
*.ts1 yearSegments never change

Create a cache policy for manifests with a short TTL and assign it to a behavior matching *.m3u8. Leave the default behavior (which catches .ts files) with a long TTL.

# Create a cache policy for manifests
aws cloudfront create-cache-policy \
  --cache-policy-config '{
    "Name": "hls-manifests",
    "DefaultTTL": 10,
    "MaxTTL": 30,
    "MinTTL": 0,
    "ParametersInCacheKeyAndForwardedToOrigin": {
      "EnableAcceptEncodingGzip": true,
      "HeadersConfig": { "HeaderBehavior": "none" },
      "CookiesConfig": { "CookieBehavior": "none" },
      "QueryStringsConfig": { "QueryStringBehavior": "none" }
    }
  }'

Add the behavior to your distribution config:

{
  "CacheBehaviors": {
    "Items": [
      {
        "PathPattern": "*.m3u8",
        "TargetOriginId": "hls-s3",
        "ViewerProtocolPolicy": "redirect-to-https",
        "CachePolicyId": "<hls-manifests-policy-id>",
        "AllowedMethods": {
          "Quantity": 2,
          "Items": ["GET", "HEAD"]
        }
      }
    ],
    "Quantity": 1
  }
}

Set CORS headers

Players loading .m3u8 and .ts files cross-origin need CORS. Add a response headers policy to your CloudFront behaviors:

aws cloudfront create-response-headers-policy \
  --response-headers-policy-config '{
    "Name": "hls-cors",
    "CorsConfig": {
      "AccessControlAllowOrigins": { "Items": ["*"], "Quantity": 1 },
      "AccessControlAllowMethods": { "Items": ["GET", "HEAD"], "Quantity": 2 },
      "AccessControlAllowHeaders": { "Items": ["*"], "Quantity": 1 },
      "AccessControlMaxAgeSec": 86400,
      "OriginOverride": true
    }
  }'

Play the stream

Once CloudFront is deployed, your master manifest is available at:

https://<distribution-id>.cloudfront.net/streams/lecture/master.m3u8

Point any HLS-compatible player at that URL — hls.js, Video.js, Safari native, or a mobile SDK.

<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<video id="player" controls></video>
<script>
  const video = document.getElementById('player');
  const src = 'https://d1234abcd.cloudfront.net/streams/lecture/master.m3u8';

  if (Hls.isSupported()) {
    const hls = new Hls();
    hls.loadSource(src);
    hls.attachMedia(video);
  } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
    video.src = src;
  }
</script>

See also