Most tutorials on “protecting your video” stop at CloudFront signed URLs or S3 presigned links — sign one request, done. That works fine for a single MP4. It falls apart the moment you move to adaptive streaming, because HLS and DASH aren’t one file. They’re a manifest that points at many segment files, and a signed manifest URL says nothing about who’s allowed to fetch the segments it references.
This post covers the two approaches that actually hold up in production — manifest rewriting and edge-validated session tokens — the TTL tradeoff that comes with both, and a Safari quirk that will bite you if you don’t plan for it.
Signing just the .m3u8 or .mpd URL does not protect an HLS or DASH stream — it only protects the manifest request. The actual video lives in the segment files the manifest references, so you need to protect the whole session: either rewrite the manifest so every segment URL carries its own signature, or issue a short-lived session token the CDN edge validates on every segment request. Signed tokens stop casual link-sharing and hotlinking; they are not DRM and won’t stop screen recording.
The manifest is not the video
An HLS playlist (.m3u8) is a text file that lists other files — usually a master playlist pointing at one rendition playlist per bitrate, and each rendition playlist listing the actual .ts or .m4s segments, typically a few seconds each. DASH’s .mpd is the same idea in XML: a manifest describing segments per representation. A single video at a few bitrate renditions can easily reference hundreds of segment URLs over the course of playback.
If you sign only the top-level manifest URL, here’s what a player actually does: request the signed manifest, get back a 200 with a list of unsigned segment URLs, then spend the rest of playback requesting those segments directly. Anyone who opens dev tools and copies one segment URL can pull the file from there — the signature on the manifest request never travels with it. Worse, if segment paths are predictable, no interception is even required.
Two ways to protect the whole session
Manifest rewriting: sign every segment
The first real fix is to rewrite the manifest at serve time so every URL inside it — every rendition playlist, every segment — carries its own signature. AWS’s own writeup on the mechanics is a solid reference: CloudFront signed URLs for video streaming. This works, but your edge (or an origin function in front of it) has to parse and rewrite manifest text on every request, and you end up minting and validating hundreds of individual signatures per viewing session instead of one.
Session tokens: let the edge check every request
The more common approach for adaptive streaming is a single short-lived session token, issued once per viewer per playback, that the CDN edge checks on every request under that video’s path — manifest and segments alike. The manifest itself stays untouched; the token rides along as a cookie, header, or query param, and the edge either has it or it doesn’t. This is cheaper to reason about — one token, one TTL, one validation rule — and is what most production setups converge on once they’ve hit the manifest-rewriting problem once.
TTL: the tradeoff you can’t avoid
Both approaches need a token or signature lifetime, and there’s no free lunch here:
- Too short, and a viewer mid-video hits an expired token — the player either stalls or throws a fetch error on the next segment, in the middle of playback.
- Too long, and you’ve effectively handed out a shareable link for the length of the TTL — someone forwards the URL and it keeps playing for anyone until it expires.
The practical fix is renewal, not a longer TTL: keep the token short and have the player, or your backend, fetch a fresh one before the old one lapses, so the session is long-lived even though any single token isn’t.
Safari’s quiet failure mode
There’s a specific reason to lean toward session tokens with active renewal rather than “sign for a long time and hope”: Safari does not reliably fire an error event when a signed segment URL expires mid-playback. Chrome and Firefox surface a fetch or network error the player can catch and react to — retry, refresh the token, show a message. Safari can instead just stall, with no clean error to hook into. This behavior shows up in the wild; see this long-running video.js issue thread. It means you can’t rely on “catch the error, then refresh” as your only renewal strategy if Safari is in your traffic mix. Renew proactively, before expiry, rather than reactively after a failure you may never observe.
What this does not stop
Signed playback tokens are access control, not DRM. They stop someone from hotlinking your video into another site, sharing a raw URL that keeps working forever, or scraping segment paths off a public bucket. They do not stop someone who’s authorized to watch the video from recording their screen while it plays — no signed-URL scheme does, and any tutorial implying otherwise is overselling it. If the requirement is “must survive a fully authenticated viewer trying to rip it,” that’s Widevine/FairPlay-grade DRM territory — a different, much heavier system than what this post covers.
How rehelios handles this
rehelios gates playback with a short-lived signed playback token, issued per viewer and validated at the Cloudflare edge on every segment request, not just the manifest — so you don’t have to choose between rewriting manifests yourself and running your own token-issuing service. For content where the audience is a known set of pages rather than individual viewers — an embedded player on your own app, for example — a per-org domain allowlist is the simpler gate: the edge checks the referring origin instead of a token. Either way, GET /v1/videos/{id} hands back a playbackUrl that’s already scoped correctly, so your player never sees raw segment URLs to leak in the first place. Full endpoint details live in the docs. We walk through this pattern end to end, with actual code, in building a token-protected course video player.
A minimal example
const res = await fetch(`https://api.rehelios.com/v1/videos/${videoId}`, {
headers: { Authorization: `Bearer ${serverApiKey}` },
});
const { data } = await res.json();
// data.playbackUrl already carries a short-lived signed token scoped to
// this viewer and this video. Hand it straight to the player.
player.src = data.playbackUrl;
// Refresh before the token's TTL runs out so a long session never
// hits a mid-playback expiry — request a fresh playbackUrl and
// hot-swap the source.
setTimeout(refreshPlaybackUrl, ttlMs * 0.8);
The server never mints or stores tokens itself — it asks rehelios for the video and gets back a URL that’s already gated at the edge.
Closing
This matters most anywhere paid or gated video sits behind a login. Course platforms are the clearest case, where every module needs to survive being embedded on a page a logged-in student can view but not casually forward — see how that plays out in a full product context in protecting course video. The failure mode to avoid isn’t “no protection at all” — it’s the false confidence of a signed manifest URL that leaves every segment underneath it wide open.