Troubleshooting · HLS
Streamwake reliability agents

Recover failing HLS sessions
without guessing.

A working guide to the failure patterns Streamwake catches on real HLS sessions — from master playlist fetch through EXT-X tag continuity and AES key delivery — written so an on-call engineer can read it next to a recent chunklist capture and act on it.

Protocol: HLS
Format: .m3u8 + TS/fMP4
Streamwake probes: playlist fetch · variant continuity · segment drift.
Probes

What Streamwake checks

Five families of probes, each with a small, deterministic pass/fail verdict that feeds the timeline. Every check has a name — that's the name you'll see on the agents feed.

Master playlist reachable
playlist.reachable + playlist.well_formed
fetch · parse
  • Master playlist returns HTTP 200 within the probe budget (default 5s).
  • Content-Type reads as application/vnd.apple.mpegurl — not octet-stream.
  • #EXTM3U on line 1; EXT-X-VERSION matches features used; no BOM.
Variant + media continuity
variant.consistency + media.discontinuity_resolution
head · tail
  • Every STREAM-INF variant URI resolves to a media playlist.
  • Each variant playlist has a coherent EXT-X-TARGETDURATION / MEDIA-SEQUENCE pair.
  • #EXT-X-DISCONTINUITY only at genuine PTS / codec / format transitions.
Segment timeline
segment.head_window + segment.tail_window
window
  • MEDIA-SEQUENCE advances window stride of EXTINF.
  • EXT-X-PROGRAM-DATE-TIME monotonic across the last 6 segments.
  • Segment duration drift: declared EXTINF vs actual bytes.
Bandwidth ladder
ladder.bandwidth_order + ladder.codec_consistent
ladder
  • STREAM-INF BANDWIDTH ascending across every variant.
  • CODECS strings cover every audio group referenced via AUDIO=.
  • RESOLUTION matches rung ordering on the CDN edge.
DRM / AES key envelope
drm.key_reachable + drm.pssh_present
DRM
  • EXT-X-KEY URI reachable for the current MEDIA-SEQUENCE (AES-128 and SAMPLE-AES).
  • For fMP4: PSSH box present in every #EXT-X-MAP init segment.
  • Key rotation: new keyid live before the rolling segment references it.
Anatomy

A minimal-but-real pair of playlists

A master with three video variants and one audio rendition group, plus a media playlist for the 720p variant. Annotations below name the probe that reads from each line — so when an alert fires, you know which leaf of the playlists to look at first.

The master playlist #EXTM3U declares one #EXT-X-MEDIA audio rendition group, then three #EXT-X-STREAM-INF lines — bandwidth ascending, RESOLUTION ascending, CODECS spanning audio + video.

The variant playlist carries its own #EXTM3U and #EXT-X-VERSION:6. #EXT-X-MAP points at the fMP4 init segment (the proxy for the drm.pssh_present probe); each segment has a matching #EXT-X-PROGRAM-DATE-TIME so the segment_tail_window probe can check monotonicity.

Probes → playlist lines
Quick map from probe verdict to the tag you should pull.
  • playlist.reachable → root #EXTM3U
  • variant.consistency #EXT-X-STREAM-INF URI per variant
  • segment.tail_window → trailing #EXTINF + #EXT-X-PROGRAM-DATE-TIME
  • media.discontinuity_resolution #EXT-X-DISCONTINUITY placement
  • drm.key_reachable #EXT-X-KEY URI= for current MEDIA-SEQUENCE
Sample master playlist
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-aac",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,URI="audio-aac/playlist.m3u8"
#EXT-X-STREAM-INF:BANDWIDTH=628000,CODECS="mp4a.40.2,avc1.640028",RESOLUTION=640x360,AUDIO="audio-aac"
video-360p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2428000,CODECS="mp4a.40.2,avc1.640028",RESOLUTION=1280x720,AUDIO="audio-aac"
video-720p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=4828000,CODECS="mp4a.40.2,avc1.640028",RESOLUTION=1920x1080,AUDIO="audio-aac"
video-1080p/playlist.m3u8
Sample 720p variant playlist
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:1470
#EXT-X-MAP:URI="video-720p/init.mp4"
#EXT-X-PROGRAM-DATE-TIME:2026-08-06T10:42:13.000Z
#EXTINF:6.0,
video-720p/seg-1470.m4s
#EXT-X-PROGRAM-DATE-TIME:2026-08-06T10:42:19.000Z
#EXTINF:6.0,
video-720p/seg-1471.m4s
#EXT-X-PROGRAM-DATE-TIME:2026-08-06T10:42:25.000Z
#EXTINF:6.0,
video-720p/seg-1472.m4s
#EXT-X-PROGRAM-DATE-TIME:2026-08-06T10:42:31.000Z
#EXTINF:6.0,
video-720p/seg-1473.m4s
#EXT-X-PROGRAM-DATE-TIME:2026-08-06T10:42:37.000Z
#EXTINF:6.0,
video-720p/seg-1474.m4s
#EXT-X-PROGRAM-DATE-TIME:2026-08-06T10:42:43.000Z
#EXTINF:6.0,
video-720p/seg-1475.m4s
Failure modes

Ten ways an HLS session fails

Each row: symptom the agent reports → the underlying cause → a fix that holds under the next probe cycle. The named probes are what you'd grep for in the agents feed.

01Master playlist 200s but the player throws MANIFEST_PARSE_ERROR
failure mode
Symptom

curl returns 200 with the right bytes; the player throws "MANIFEST_PARSE_ERROR" before playback starts.

Root cause

Content-Type is wrong (text/plain or application/octet-stream) or the file has a UTF-8 BOM before #EXTM3U. The reachable probe marks the fetch green but the well-formed probe flags the parse.

Fix

Set Content-Type: application/vnd.apple.mpegurl on the origin response; emit the playlist as UTF-8 with no BOM and a leading #EXTM3U on line 1.

02EXT-X-VERSION mismatches the features you used
failure mode
Symptom

Player drops without error, or parser rejects live-only tags like #EXT-X-PROGRAM-DATE-TIME.

Root cause

You used EXT-X-VERSION:6 features in a playlist declared as VERSION:3. Older parsers silently drop tags; aggressive ones refuse to play.

Fix

Set EXT-X-VERSION to the highest version any tag in the playlist requires (3 + MAP, 4 + byte-range, 6 + fMP4 CMAF + PDT). The well-formed probe will flag underspec.

03Variants listed out of bandwidth order
failure mode
Symptom

ABR oscillates between rungs; logs show UP_SWITCH / DOWN_SWITCH within seconds of the first segment.

Root cause

#EXT-X-STREAM-INF BANDWIDTH values are not monotonically increasing — the 720p row sits after the 1080p row by accident. Safari/AVPlayer honors insertion order, not magnitude.

Fix

Sort variant lines ascending by BANDWIDTH; ladder.bandwidth_order will flip to pass at the next probe.

04CODECS in STREAM-INF does not cover the audio group
failure mode
Symptom

Video plays but audio falls back to silence on certain players; some render audio-only without video.

Root cause

CODECS="avc1.640028" on the video variant but the audio rendition group adds mp4a.40.2 elsewhere — STREAM-INF omits the audio codec. Players that build the source buffer from STREAM-INF alone will drop the audio track.

Fix

List the full codec set in CODECS="mp4a.40.2,avc1.640028" on every variant that references the audio group.

05EXT-X-DISCONTINUITY forced on every segment
failure mode
Symptom

Player stalls at every segment boundary; timeline jumps visibly in the seek bar; ABR locks to the lowest rung.

Root cause

Packager emits #EXT-X-DISCONTINUITY before every #EXTINF even though PTS is continuous. Players close and reopen the SourceBuffer on every discontinuity, blowing the buffer budget.

Fix

Emit #EXT-X-DISCONTINUITY only on actual PTS / codec / format transitions. media.discontinuity_resolution will stop flagging each segment.

06EXT-X-PROGRAM-DATE-TIME drifts or rolls over
failure mode
Symptom

Timestamps a viewer sees in the seek bar move backwards; CDN returns 304-not-modified for a chunklist that should have advanced.

Root cause

PDT regeneration lost the live anchor — the packager restarted its wall clock from origin time instead of the live edge. CDN sees the chunklist unchanged and serves a stale body.

Fix

Pin PDT regeneration to the live edge (not origin stat-time); have the agent tail_window probe assert PDT monotonicity across the last 6 segments.

07Packaged 6.2s segments with declared EXTINF:6.0
failure mode
Symptom

Player underestimates when the next segment lands; tail-window probe shows pass but viewers report a hard pause near the live edge.

Root cause

Packager is producing 6.2s segments because of GOP boundaries; playlist still declares #EXTINF:6.0. Player schedules the next fetch 200ms too early and stalls before the segment is on the wire.

Fix

Either coerce the packager to honor the declared duration, or surface real segment durations in EXTINF so the player uses the actual figure.

08.ts segments on one variant and fMP4 on another
failure mode
Symptom

Player picks a variant and MSE throws SourceBuffer.appendBuffer with a mimeType mismatch on a subsequent rendition switch.

Root cause

One rendition is packaged as .ts, another as fMP4 with #EXT-X-MAP. MSE was appended with the wrong codec string and the switch rejects the next segment.

Fix

Align the packaging across every variant — either all fMP4/CMAF, or all TS — and update the variant TYPE parameter consistently. cmaf.ftyp_match will start passing.

09AES-128 key URI 404s mid-stream
failure mode
Symptom

Playback works for the first key rotation, then one variant throws "KEY_LOAD_ERROR" and the player aborts that rendition.

Root cause

EXT-X-KEY URI references a per-region keyserver that rebalances the key id across the rotation; the new key id is unavailable on the new origin. Subsequent segments cannot be decrypted.

Fix

Surface the EXT-X-KEY URI as part of the probe — drm.key_reachable should hit the URI bound to the current MEDIA-SEQUENCE before each rotation.

10Master returns 304 while the live window advanced
failure mode
Symptom

Player keeps the same dead media sequence even though new segments have landed; ABR locks at the bottom rung.

Root cause

CDN honored If-Modified-Since with the prior manifest timestamp even though the live window advanced. Master playlist 304s; media sequence never moves forward.

Fix

Set Cache-Control: no-cache, must-revalidate on the master; pin Cache-Control: max-age=1 on the variant playlists. The reachable probe flags 304s over a moving window.

Diagnose

Diagnose with Streamwake

Register the HLS source against POST /api/v1/streams, then read the agent timeline back through GET /api/v1/agents. The probe verdicts in the timeline are exactly the rows above.

The curl below registers an HLS master playlist URL and asks for a 30-second probe cadence. The cookie is the same better-auth.session_token that gates every /api/v1/* call — see the auth guide for how to mint one.

Once the stream is registered, the agents endpoint returns the per-probe verdicts below. The order of checks mirrors the probe families in the section above — playlist.reachable first, variant continuity next, segment windows and ladder after, then CMAF and DRM.

POST /api/v1/streams
curl -X POST https://streamwake.polsia.io/api/v1/streams \
  -H "content-type: application/json" \
  -b "better-auth.session_token=<your-session-cookie>" \
  -d '{
    "sourceUrl": "https://cdn.example.com/live/event/manifest.m3u8",
    "protocol": "HLS",
    "probeIntervalSeconds": 30
  }'
GET /api/v1/agents?stream_id=…
curl https://streamwake.polsia.io/api/v1/agents?stream_id=<id> \
  -b "better-auth.session_token=<your-session-cookie>"
Agent timeline response (trimmed)
{
  "stream_id": "cklivehlsevent456",
  "source": "https://cdn.example.com/live/event/manifest.m3u8",
  "protocol": "HLS",
  "checks": [
    {
      "probe": "playlist.reachable",
      "result": "pass",
      "latency_ms": 118,
      "detail": "content-type application/vnd.apple.mpegurl"
    },
    {
      "probe": "playlist.well_formed",
      "result": "pass",
      "detail": "EXT-X-VERSION=6, MASTER with 3 variants + 1 audio group"
    },
    {
      "probe": "variant.consistency",
      "result": "pass",
      "variants_checked": 3
    },
    {
      "probe": "segment.head_window",
      "result": "pass",
      "latency_ms": 96,
      "segments_checked": 6
    },
    {
      "probe": "segment.tail_window",
      "result": "fail",
      "latency_ms": 4014,
      "detail": "EXT-X-PROGRAM-DATE-TIME drift: PDT moved backwards at seg-1474 (10:42:31Z -> 10:42:29Z), CDN served stale chunklist"
    },
    {
      "probe": "media.discontinuity_resolution",
      "result": "pass",
      "discontinuities_checked": 0
    },
    {
      "probe": "ladder.bandwidth_order",
      "result": "pass"
    },
    {
      "probe": "cmaf.ftyp_match",
      "result": "pass",
      "detail": "audio/video init segments share compatible ftyp brand set"
    },
    {
      "probe": "drm.key_reachable",
      "result": "pass",
      "detail": "AES-128 key URI returned 200, valid for current MEDIA-SEQUENCE"
    }
  ]
}
Next step

Want Streamwake to catch this on its own?

Sign up, register a stream, and the same probes that produced the timeline above run on every refresh — and surface in a Slack channel, a webhook, or the streams dashboard.

Open the streams dashboard
Auth-gated · reads the timeline the agent wrote on every probe.
  • Stream list reads from GET /api/v1/streams; per-stream timeline from GET /api/v1/agents.
  • Probe verdicts stream into the dashboard within one cadence interval — manual curl not required.
  • Self-serve signup at /sign-up — no sales call required for the first stream.