Recover failing DASH sessions
without guessing.
A working guide to the failure patterns Streamwake catches on real DASH sessions — from manifest fetch through CMAF packaging — written so an on-call engineer can read it next to a pcap of the last refresh and act on it.
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.
- MPD returns HTTP 200 within the probe budget (default 5s).
- Content-Type reads as application/dash+xml — not octet-stream.
- XML parses, profiles/types/availabilityStartTime are coherent.
- SegmentTemplate / SegmentList numSegments consistency vs declared duration.
- availabilityStartTime relative to wall clock and live edge.
- Segment duration drift: actual segment bytes vs declared timescale/duration.
- Bandwidth ordering across Representations in every AdaptationSet.
- Resync points present where ABR expects them.
- BaseURL resolves consistently across Periods, multi-CDN safe.
- Encryption scheme matches what the player expects (CAuth / CBCS / CENC).
- PSSH box present in init segments for every protected AdaptationSet.
- License URL reachable from the probe source (bounded to private ranges).
- moof/traf boxes parse cleanly off the wire (no torn segments).
- styp type consistent across audio and video segments.
- ftyp major brand matches across tracks (MSE needs them aligned).
A minimal-but-real MPD
Three video Representations and one audio AdaptationSet. The annotations below name the probe that reads from each element — so when an alert fires, you know which leaf of the MPD to look at first.
The MPD declares this manifest as type=dynamic, which is what tells the player (and the agent) to refresh on minimumUpdatePeriod. The availabilityStartTime is the live-edge anchor; the publishTime is the wall clock of the latest packager write.
Inside the Period, each Representation lists a SegmentTemplate. The $Number$ token is what the agent probes in the segment-window probes; the initialization URI is what the DRM envelope probe reads for PSSH.
- manifest.reachable → root
<MPD> - segment.head_window → first
SegmentTemplate.mediaURI - segment.tail_window →
$Number$resolved againstavailabilityStartTime - ladder.bandwidth_order →
<Representation>siblingbandwidthattrs - drm.pssh_present →
initializationURI bytes
<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011"
type="dynamic"
minimumUpdatePeriod="PT6S"
timeShiftBufferDepth="PT30S"
availabilityStartTime="2026-08-06T10:00:00Z"
publishTime="2026-08-06T10:42:13Z"
profiles="urn:mpeg:dash:profile:isoff-live:2011">
<Period id="0" start="PT0S">
<AdaptationSet contentType="video" mimeType="video/mp4" codecs="avc1.640028">
<Representation id="video-360p" bandwidth="600000" width="640" height="360">
<SegmentTemplate timescale="90000" duration="5400000"
initialization="video-360p/init.mp4"
media="video-360p/seg-$Number$.m4s" startNumber="1"/>
</Representation>
<Representation id="video-720p" bandwidth="2400000" width="1280" height="720">
<SegmentTemplate timescale="90000" duration="5400000"
initialization="video-720p/init.mp4"
media="video-720p/seg-$Number$.m4s" startNumber="1"/>
</Representation>
<Representation id="video-1080p" bandwidth="4800000" width="1920" height="1080">
<SegmentTemplate timescale="90000" duration="5400000"
initialization="video-1080p/init.mp4"
media="video-1080p/seg-$Number$.m4s" startNumber="1"/>
</Representation>
</AdaptationSet>
<AdaptationSet contentType="audio" mimeType="audio/mp4" codecs="mp4a.40.2">
<Representation id="audio-128k" bandwidth="128000">
<SegmentTemplate timescale="48000" duration="2880000"
initialization="audio-128k/init.mp4"
media="audio-128k/seg-$Number$.m4s" startNumber="1"/>
</Representation>
</AdaptationSet>
</Period>
</MPD>Ten ways a DASH 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.
curl returns 200 with the right bytes; the player throws "MANIFEST_PARSE_ERROR".
Content-Type is wrong (application/octet-stream), or the file has a UTF-16 BOM the parser does not expect. The probe marks the fetch green but the parse probe red.
Set Content-Type: application/dash+xml on the origin response; emit the MPD as UTF-8 with no BOM. Re-run a manifest-reachable probe after the CDN reload.
Players joining mid-stream see ~6s of buffer-then-spinner before playback starts.
availabilityStartTime lags the live edge by a minute or more — the player is fetching segments from before the live window. Probe flags window_anchor_advanced=false.
Recompute availabilityStartTime from the packager clock, not the origin’s stat-time. Confirm publishTime advances within minimumUpdatePeriod.
Player logs "segment unavailable" at random positions; ABR oscillates to a different rendition.
SegmentTemplate $Number$ drifted ahead of the packager — the CDN is now serving what the packager used to produce. Common after a hot-swap of a packager without draining.
Reboot packager, drain in-flight segments, then resume. While draining, force the agent to a 2s probe cadence so tail-window 404s are caught before the player hits them.
Player flips between 360p and 720p every few seconds; logs show "DOWN_SWITCH / UP_SWITCH" noise.
Representations are out of bandwidth order — the 720p row has bandwidth=2400000 listed BEFORE 1080p showing bandwidth=2300000. Browser parsers honor insertion order, not sorted magnitude.
Sort Representation rows ascending by bandwidth inside every AdaptationSet. Probe ladder.bandwidth_order will flip to pass.
Smooth playback, then a hard error at the Period transition; only the new Period fails.
BaseURL drifted between Periods — Period 2 uses a relative BaseURL whose parent path resolves off the wrong edge. Period 1 is fine; Period 2 is not.
Pin BaseURL to an absolute origin per Period, or carry a BaseURL at MPD root and only override when the absolute URL genuinely changes.
Player reaches "ready" state but appendBuffer throws a QuotaExceededError on the first init segment.
Audio and video tracks use incompatible CMAF brands — audio init has ftyp/isom, video has ftyp/cmfv with no shared styp type. MSE rejects the mismatch before assignment.
Align ftyp brand set across audio + video (isom + dash + cmfc typically), ensure styp type matches between segments.
Widevine/PlayReady/FairPlay license fetches succeed; player never gets a usable key.
No PSSH box in the init segment — the player gets a key but cannot bind it to a content ID. Common when packaging was done against a manifest without the explicitContentType / cenc:default_KID.
Embed a PSSH box in every init segment; if cenc:default_KID is set on the AdaptationSet, propagate it into the init segment headers.
Streamwake segment.tail_window reports "pass" but viewers report a hard pause near the live edge.
Segment duration is declared 6s but the packager is producing 6.2s segments — the player underestimates when the next segment becomes available and stalls before it lands.
Either fix the packager to honor the declared timescale/duration, or surface the actual segment duration through SegmentTimeline.n so the player uses real values.
The reliability feed shows zero new probes for minutes while the widget still says "watching".
The live window anchor is not advancing. The previousPeriodTime or PTS rollover is mis-set, so the player (and the agent) treat the stream as still.
Check that publishTime moves forward on every refresh; reset the live edge from the packager timeline rather than wall clock when bridging restarts.
Multi-CDN routing pulls the heavy region onto a CDN whose WAF rejects the codec User-Agent string.
BaseURL.host picks the wrong upstream — the BaseURL string is templated and resolves to a CDN hostname whose WAF is unfamiliar with the DASH codec hint UA.
Audit the multi-CDN routing rule; pin that segment tree to the proven CDN; have Streamwake flag segments 4xx >5% on a given BaseURL host.
Diagnose with Streamwake
Register the DASH 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 a DASH source 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 — manifest.reachable first, segment windows next, ladder and DRM after.
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.mpd",
"protocol": "DASH",
"probeIntervalSeconds": 30
}'curl https://streamwake.polsia.io/api/v1/agents?stream_id=<id> \
-b "better-auth.session_token=<your-session-cookie>"{
"stream_id": "cklivedashevent123",
"source": "https://cdn.example.com/live/event/manifest.mpd",
"protocol": "DASH",
"checks": [
{
"probe": "manifest.reachable",
"result": "pass",
"latency_ms": 142,
"detail": "content-type application/dash+xml"
},
{
"probe": "manifest.well_formed",
"result": "pass",
"detail": "type=dynamic, profiles=isoff-live:2011"
},
{
"probe": "segment.head_window",
"result": "pass",
"latency_ms": 88,
"segments_checked": 6
},
{
"probe": "segment.tail_window",
"result": "fail",
"latency_ms": 4128,
"detail": "SegmentTimeline drift: 404 on video-720p/seg-721.m4s"
},
{
"probe": "ladder.bandwidth_order",
"result": "pass"
},
{
"probe": "cmaf.ftyp_match",
"result": "pass",
"detail": "audio/video init segments share compatible brand set"
}
]
}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.
- Stream list reads from
GET /api/v1/streams; per-stream timeline fromGET /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.