Troubleshooting · MPEG-DASH
Streamwake reliability agents

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.

Protocol: DASH
Format: MPD + ISO BMFF segments
Streamwake probes: manifest fetch · segment head · segment tail.
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.

Manifest reachable
manifest.reachable + manifest.well_formed
fetch · parse
  • 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.
Segment timeline
segment.head_window + segment.tail_window
head · tail
  • 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.
Representation ladder
ladder.bandwidth_order + ladder.baseurl_resolution
ladder
  • Bandwidth ordering across Representations in every AdaptationSet.
  • Resync points present where ABR expects them.
  • BaseURL resolves consistently across Periods, multi-CDN safe.
DRM envelope
drm.pssh_present + drm.license_reachable
DRM
  • 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).
CMAF track alignment
cmaf.ftyp_match + cmaf.styp_consistent
CMAF
  • 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).
Anatomy

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.

Probes → MPD elements
Quick map from probe verdict to the XML you should pull.
  • manifest.reachable → root <MPD>
  • segment.head_window → first SegmentTemplate.media URI
  • segment.tail_window $Number$ resolved against availabilityStartTime
  • ladder.bandwidth_order <Representation> sibling bandwidth attrs
  • drm.pssh_present initialization URI bytes
Sample MPD
<?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>
Failure modes

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.

01MPD parses as text/200 but the player fails
failure mode
Symptom

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

Root cause

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.

Fix

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.

02Late-join stutter on first segment
failure mode
Symptom

Players joining mid-stream see ~6s of buffer-then-spinner before playback starts.

Root cause

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.

Fix

Recompute availabilityStartTime from the packager clock, not the origin’s stat-time. Confirm publishTime advances within minimumUpdatePeriod.

03Segments 404 mid-session
failure mode
Symptom

Player logs "segment unavailable" at random positions; ABR oscillates to a different rendition.

Root cause

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.

Fix

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.

04ABR oscillates between rungs
failure mode
Symptom

Player flips between 360p and 720p every few seconds; logs show "DOWN_SWITCH / UP_SWITCH" noise.

Root cause

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.

Fix

Sort Representation rows ascending by bandwidth inside every AdaptationSet. Probe ladder.bandwidth_order will flip to pass.

05First segment after a Period boundary 404s
failure mode
Symptom

Smooth playback, then a hard error at the Period transition; only the new Period fails.

Root cause

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.

Fix

Pin BaseURL to an absolute origin per Period, or carry a BaseURL at MPD root and only override when the absolute URL genuinely changes.

06MSE throws "SourceBuffer.appendBuffer" on init
failure mode
Symptom

Player reaches "ready" state but appendBuffer throws a QuotaExceededError on the first init segment.

Root cause

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.

Fix

Align ftyp brand set across audio + video (isom + dash + cmfc typically), ensure styp type matches between segments.

07DRM license server returns 200 but the player stalls
failure mode
Symptom

Widevine/PlayReady/FairPlay license fetches succeed; player never gets a usable key.

Root cause

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.

Fix

Embed a PSSH box in every init segment; if cenc:default_KID is set on the AdaptationSet, propagate it into the init segment headers.

08Player stalls even though the tail is reachable
failure mode
Symptom

Streamwake segment.tail_window reports "pass" but viewers report a hard pause near the live edge.

Root cause

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.

Fix

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.

09Agent flags "no probe" repeatedly on a live source
failure mode
Symptom

The reliability feed shows zero new probes for minutes while the widget still says "watching".

Root cause

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.

Fix

Check that publishTime moves forward on every refresh; reset the live edge from the packager timeline rather than wall clock when bridging restarts.

10Multi-CDN 403s from one upstream under load
failure mode
Symptom

Multi-CDN routing pulls the heavy region onto a CDN whose WAF rejects the codec User-Agent string.

Root cause

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.

Fix

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

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.

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.mpd",
    "protocol": "DASH",
    "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": "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"
    }
  ]
}
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.