Troubleshooting · MPEG-DASH
Streamwake reliability agents

Recover failing MPEG-DASH sessions
without guessing.

A working guide to the failure patterns Streamwake catches on real MPEG-DASH sessions — from manifest parse and profiles/type coherence through SegmentTemplate $Number$ numbering, SegmentTimeline t/d access-unit consistency, sidx parse failures, and the availabilityTimeOffset misuse that breaks low-latency DASH — written so an on-call engineer can read it next to a recent MPD capture and act on it.

Protocol: MPEG-DASH
Format: .mpd + CMAF fMP4 + sidx
Streamwake probes: manifest parse · SegmentTemplate + SegmentTimeline · sidx parse · availabilityTimeOffset coherence · ladder ordering · numbering drift.
Probes

What Streamwake checks

Five families of probes, each with a small, deterministic pass / fail / warn 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
  • Root MPD returns HTTP 200 within the probe budget (default 5s); well-formed application/dash+xml.
  • @profiles and @type coherent — isoff-live:2011 with type="dynamic"; no BOM.
  • Single-root MPD + xlink resolved before the agent emits a verdict — no half-parsed manifests in the timeline.
Segment timeline
segment.head_window + segment.tail_window
head · tail
  • SegmentTemplate + SegmentList consistency inside a Period — never both.
  • $Number$ resolved monotonically; startNumber drifts flagged via segment.numbering_drift.
  • SegmentTimeline S@t/r/d access-unit consistency + availabilityTimeOffset respected across Representations.
Representation ladder
ladder.bandwidth_order
ladder · baseurl
  • @bandwidth ascending per AdaptationSet — descending ladders resist ABR descent when the cohort buffers.
  • BaseURL resolve-rate across every Period — broken BaseURLs raise a global 404 probe before any SegmentTemplate URI ships.
  • Catches the case where a stealth republisher swaps a Representation in late with a different @codecs / @bandwidth than the original ladder.
Numbering drift
segment.numbering_drift + window_anchor_advanced
numbering
  • $Number$ jumps in the trailing window — gap > 1 fails the probe even when the player keeps streaming.
  • startNumber reset against a CDN that still hosts the prior counter — catches the startNumber drift that segment.tail_window misses.
  • Pairs with window_anchor_advanced on the publishTime / timeShiftBufferDepth axis — a number reset without a window advance is a rebase the cohort cannot recover from.
sidx parsing
sidx.parse_ok + sidx.references_inline
sidx · iso box
  • sidx box present when SegmentBase@indexRange declares it — its references must resolve inside the init URL byte range.
  • Cross-checks the sidx ref_size durations against SegmentTimeline S@duration on the parent Representation.
  • Most common cause of “first segment has no moof”: sidx references resolve out-of-line against a subsegment that the republisher is mid-rewriting.
Anatomy

Anatomy of an MPD

A live DASH MPD with three video Representations (360p / 720p / 1080p) plus an audio AdaptationSet, exact SegmentTimeline S@t/d bounds, an explicit startNumber on every Representation, an AvailabilityTimeOffset on the audio set, and a sidx box trace from the 720p indexRange. The annotations below name the probe that reads each element.

Live MPD (3 video reps + audio + SegmentTimeline + ATO)
<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011"
     type="dynamic"
     minimumUpdatePeriod="PT2S"
     timeShiftBufferDepth="PT12S"
     availabilityStartTime="2026-08-20T10:00:00.000Z"
     publishTime="2026-08-20T11:42:13.000Z"
     profiles="urn:mpeg:dash:profile:isoff-live:2011">
  <BaseURL>https://cdn.example.com/live/event/</BaseURL>

  <Period id="P0" start="PT0S">
    <AdaptationSet contentType="video" mimeType="video/mp4" segmentAlignment="true" startWithSAP="1">

      <Representation id="v1" bandwidth="628000"  width="640"  height="360"  codecs="avc1.640028">
        <SegmentTemplate timescale="1000"
                          initialization="video/360p/init.mp4"
                          media="video/360p/seg-$Number$.m4s"
                          startNumber="7421"
                          presentationTimeOffset="0">
          <SegmentTimeline>
            <S t="3043812000" d="6000" r="2"/>
            <S t="3043824000" d="6000"/>
            <S t="3043830000" d="6000"/>
            <S t="3043836000" d="6000"/>
          </SegmentTimeline>
        </SegmentTemplate>
      </Representation>

      <Representation id="v2" bandwidth="2428000" width="1280" height="720"  codecs="avc1.640028">
        <!-- sidx-shaped Representation: indexRange returns bytes from
             inside an sidx box that lives in a subsegment the packager
             is also rewriting for low-latency delivery. -->
        <SegmentTemplate timescale="1000"
                          initialization="video/720p/init.mp4"
                          media="video/720p/seg-$Number$.m4s"
                          startNumber="7421">
          <SegmentTimeline>
            <S t="3043812000" d="6000"/>
            <S t="3043818000" d="6000"/>
            <S t="3043824000" d="6000"/>
            <S t="3043830000" d="6000"/>
            <S t="3043836000" d="6000"/>
          </SegmentTimeline>
        </SegmentTemplate>
        <!-- SegmentBase indexRange is OPTIONAL in DASH; when present it
             MUST reference an sidx box that lives inside the
             initialization URL’s byte range, not inside a media
             subsegment that can be rewritten. -->
      </Representation>

      <Representation id="v3" bandwidth="4828000" width="1920" height="1080" codecs="avc1.640028">
        <SegmentTemplate timescale="1000"
                          initialization="video/1080p/init.mp4"
                          media="video/1080p/seg-$Number$.m4s"
                          startNumber="7421">
          <SegmentTimeline>
            <S t="3043812000" d="6000"/>
            <S t="3043818000" d="6000"/>
            <S t="3043824000" d="6000"/>
            <S t="3043830000" d="6000"/>
            <S t="3043836000" d="6000"/>
          </SegmentTimeline>
        </SegmentTemplate>
      </Representation>
    </AdaptationSet>

    <AdaptationSet contentType="audio" mimeType="audio/mp4" lang="en">
      <Representation id="a1" bandwidth="128000" codecs="mp4a.40.2">
        <SegmentTemplate timescale="1000"
                          initialization="audio/init.mp4"
                          media="audio/seg-$Number$.m4s"
                          startNumber="7421"
                          presentationTimeOffset="0">
          <SegmentTimeline>
            <S t="3043812000" d="6000" r="4"/>
          </SegmentTimeline>
        </SegmentTemplate>
        <AvailabilityTimeOffset value="2.0"/>
      </Representation>
    </AdaptationSet>

    <UTCTiming schemeIdUri="urn:mpeg:dash:utc:http-xsdate:2014" value="https://time.example.com/utc"/>
  </Period>
</MPD>
720p sidx byte trace (indexRange 0x10C0–0x18FF)
GET https://cdn.example.com/live/event/video/720p/seg-7423.m4s
HTTP/1.1 206 Partial Content
content-type: video/iso.segment
content-range: bytes 0-98304/412800
range: bytes=0-98304

ftyp + moov + sidx (offset 0x10C0–0x18FF, 4 references)
  ref[0]  type=1  size=98304   SAP=1  starts_at=0
  ref[1]  type=1  size=96608   SAP=1  starts_at=98304
  ref[2]  type=1  size=98304   SAP=1  starts_at=194912
  ref[3]  type=1  size=98560   SAP=1  starts_at=293216

moof (mdat starts at offset 0x20FC) but the sidx references resolve
to byte ranges that overlap the rewritten subsegment body that the
low-latency republisher is mid-flight on. sidx.parse_ok fails on
the box-cross-check; first-segment .m4s has no moof at the
declared offset.
Probes → MPD fields
Quick map from probe verdict to the line you should pull.
  • manifest.well_formed → root MPD@type + MPD@profiles
  • segment.tail_window SegmentTimeline S@t/d
  • segment.numbering_drift SegmentTemplate@startNumber + $Number$ sequence
  • sidx.parse_ok SegmentBase@indexRange + sidx box bytes
  • availability_offset.coherent AvailabilityTimeOffset@value on every AdaptationSet
  • ladder.bandwidth_order Representation@bandwidth ascending inside AdaptationSet

The MPD declares a single Period with two AdaptationSets — video (three ordered Representations) and audio. Every Representation uses SegmentTemplate with $Number$ + an explicit startNumber. The probe segment.numbering_drift reads startNumber + the resolved $Number$ sequence on every reload.

The SegmentTimeline under 360p repeats one S element twice with r="2"; the other video reps do the same. The audio AdaptationSet repeats one S element four times with r="4". Cross-rep S@t + presentationTimeOffset values must align — that is what segment.tail_window asserts.

The audio AdaptationSet emits <AvailabilityTimeOffset value="2.0"/> — the video reps omit it (defaults to 0). The probe availability_offset.coherent reads this delta and warns when audio + video disagree on the offset; window_anchor_advanced reads the matching publishTime / timeShiftBufferDepth advance.

The 720p sidx trace on the right shows the failure mode the budget mostly pays for: the sidx box lives inside a subsegment byte range (0x10C0–0x18FF) that the republisher is mid-rewriting. sidx.parse_ok catches the box-cross-check failure before MSE does, and sidx.references_inline fails because the references resolve out-of-line against the init URL. The probe order in the timeline puts sidx.verify right after segment.tail_window so the cross-element drift surfaces as a single, named verdict.

Why an MPD in 2026 still bites
DASH is mature — and that is the trap.

The probe shape is small because the spec is mature: parse, well-formed, segment-head, segment-tail, ladder order, availability offset, sidx parse, numbering drift. Eight probes cover most of what an on-call engineer needs for an attach in 2026. Every probe feeds a verdicts stream that the agentic-ops layer classifies under one of a small handful of heuristics below.

Failure modes

Ten ways a MPEG-DASH session breaks

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

01Segment-numbering drift after a packager restart
failure mode
Symptom

Running players — already on segment $Number$=7421 — see the next manifest relist with startNumber=1 and the join cohort sees a fresh timeline. Long-running sessions either drop silently to a 404 on the URI resolution the player built from the old Number, or the live-edge stalls because the news index has nothing the player trusts above $Number$=1.

Root cause

The packager restarted without draining the prior Number counter. MPD@7422 ships with startNumber="1", SegmentTemplate has no $Number$ inheritance guard, and the previous tail-window probes reference $Number$ values the packager no longer publishes. segment.numbering_drift catches it; window_anchor_advanced trips as a secondary because publishTime moves even though the timeShiftBufferDepth did not advance.

Fix

On any packager restart, emit a fresh Period (`<Period id="P1">`) instead of resetting startNumber; or, if you cannot change the Period boundary, preserve Number monotonicity by keeping the prior packager warm long enough to drain the trailing window. Streamwake coerces the agent to replay the prior window the moment segment.numbering_drift fires.

02`$Number$` jump after a graceful hot-swap
failure mode
Symptom

A graceful primary→secondary packager hot-swap lands cleanly. The new MPD skips from $Number$=8210 to $Number$=9100 — the secondary’s startNumber was tuned to “hundreds after the primary” instead of "the exact next Number". A cohort rendering bounds checking catches the gap and stalls on the seam; ABR locks the lowest rung for the cohort that joined across the swap.

Root cause

Secondary packager startNumber was set conservatively to “a few hundred above where the primary was last seen” — it adds headroom but breaks `$Number$` monotonicity from the player’s perspective. The probe segment.numbering_drift trips on any gap > 1; window_anchor_advanced trips if the gap opens a hole the manifest does not cover in the trailing 6 segments.

Fix

Read startNumber from the last successful MPD@tail on the primary side and seed it into the secondary before hot-swap; or, more robustly, use a shared counter (Redis / etcd) so the secondary computes startNumber from the same clock the primary writes. Streamwake asserts startNumber continuity across the trailing 6 segments and trips segment.numbering_drift the moment a gap > 1 appears.

03sidx parse failure when indexRange references a rewritten subsegment
failure mode
Symptom

MPD parses green. Players either reject the first segment with “no moof box at declared offset” or report QuotaExceededError on the first .m4s append. Logs show the byte range returned 200 but the box at the offset is not what the player expected. Republisher that emits low-latency subsegments is the suspect.

Root cause

SegmentBase@indexRange on the v2 Representation points at byte 0x10C0–0x18FF — the sidx box lives inside a subsegment that the republisher is also writing for low-latency delivery. Half the time the sidx is intact, half the time the republisher has overwritten the subsegment body behind the player. sidx.parse_ok fails on the box-cross-check; sidx.references_inline fails because the references resolve out-of-line relative to the init URL.

Fix

Co-locate the sidx box inside the byte range of the init URL (SegmentBase@indexRange must NOT cross media-segment boundaries). When streaming live with low-latency rewriting, use SegmentTemplate + SegmentTimeline — sidx is unnecessary, and $Number$ gives you the deterministic address you need. Streamwake fails the attach on the next probe cycle once the sidx box-cross-check fails.

04`availabilityTimeOffset` is missing on low-latency DASH
failure mode
Symptom

On the first request the player thinks every segment in `timeShiftBufferDepth` is "available now" — even ones the packager has not actually landed yet. The player fetches segment 7423 before the packager publishes it, gets 404, and ABR descends the ladder. Latency reads as 9–12s because the player is chasing a manifest ahead of the byte stream.

Root cause

The packager declares a low-latency DASH workflow (small timeShiftBufferDepth, short minimumUpdatePeriod) but does not emit `<AvailabilityTimeOffset>` on the AdaptationSet. Default value is 0, which means "all segments referenced inside the available window are ready immediately." For a low-latency republisher that is wrong: the implementation needs a positive offset (typically equal to the manufacturing-server publish cadence) for the legal "available-after-this-much-time" semantics. window_anchor_advanced trips because the player is consuming window anchor faster than the bytes land; availability_offset.coherent flips to fail (or warn).

Fix

On any low-latency DASH AdaptationSet, emit `<AvailabilityTimeOffset value="X"/>` where X ≥ the manufacturing-server publish cadence; mirror the same value on every Representation inside the AdaptationSet so audio and video agree. Streamwake asserts availability_offset.coherent on $Number$ + SegmentTimeline t/d alignment.

05Wrong-sign `availabilityTimeOffset` (packager emits positive offset on a live-edge segment)
failure mode
Symptom

The player lets a segment sit marked "available" for X seconds longer than the packager has actually published it. Fetch logs show the player doing GETs that return 404 against freshly published media; ABR keeps the lowest rung because buffer math underflows. Latency appears to spike to 15+ seconds when in fact bytes are arriving on time.

Root cause

A misconfigured packager signs availabilityTimeOffset positive (the spec uses a floating-point offset where positive = "available in PAST relative to a wallclock anchor") by default on every segment, including the live-edge segment that is "available NOW." The probe availability_offset.coherent uses the MPD’s own UTC timing reference against SegmentTimeline starts to detect the sign flip. once the offset flips against a live-edge segment, the player treats it as not-yet-available when it is.

Fix

Compute `availabilityTimeOffset` from the live-edge SegmentTimeline start against publishTime, not against the wallclock; emit 0 on the live-edge segment and a strictly positive value only on segments the packager is not yet ready to commit. Verify with availability_offset.coherent on the next MPD reload — the probe should flip to pass within one cadence window of the fix.

06First segment after Period boundary 404s
failure mode
Symptom

A long-running session reaches the next Period boundary, the player tries to fetch the first `.m4s` of $Number$ from the new Period, gets HTTP 404. The previous Period’s tail had no segment with that URI on origin — the new Period has its own BaseURL and the player built the URI from MPD@old. ABR drops to the bottom rung while the seek range catches up.

Root cause

On Period transition, Representations did not preserve the address shape — the new Period’s $Number$ starts at 1 but the player seeks to $Number$=7421 and asks for `video/720p/seg-7421.m4s` relative to the new BaseURL. The packager has no such URI. segment.numbering_drift fires on the period hop; segment.tail_window fires on the trailing gap.

Fix

Either restart the new Period at the prior $Number$+1, or emit an xlink:href into the new Period that resolves against the prior adress. Streamwake asserts window_anchor_advanced on every period boundary and pre-rolls the trailing window so the cohort does not pay the cold-cache tax on the first segment.

07`$Number$` template collision with startNumber reset
failure mode
Symptom

Players joining mid-stream land on a manifest where SegmentTemplate @media is `video/720p/seg-$Number$.m4s` but startNumber has just been reset to 1 — yet the partner CDN still has catches named `seg-7421.m4s` on origin (the prior packager’s run). The player fetches `seg-1.m4s`, gets 404, ABR drops the rung.

Root cause

Same root as the package-restart case but a more aggressive form: the packager decides to "fresh-Number" on a reload but the CDN cache layer still hosts the prior counter. The byte stream at the new address is unrelated to what the byte stream at the old address was — yet the segment hash_drift probe has nothing to compare against because the URIs differ. segment.numbering_drift catches the discontinuity; window_anchor_advanced catches the resulting seek-range mismatch.

Fix

On any Number reset, invalidate the cache for every URI under the old counter (`PURGE` on the affected path globs) before relisting startNumber; or, never re-Number during a live event. Streamwake auto-fails the attach on the next probe cycle if the manifest declares startNumber=1 against a CDN that still hosts the previous counter. See the working postmortem at /incident-lab/origin-shield-saturation-correlated-cache-miss-storm — when segment-number-cohort cache-miss posture + origin-shield fan-in co-fire, the queue depth climbs and the cohort is the symptom, the upstream cache-tier is the cause.

08MSE rejects sidx box because track brands do not include `msdh`/`msix`
failure mode
Symptom

Player parses the .m4s bytes; MSE throws `SourceBuffer.appendBuffer` with a track-brand error: the sidx box exists but the major brand in the ftyp box excludes the `msdh`/`msix` track identifier that MSE requires to walk the sidx references. Player falls back to no-segmentation mode and ABR oscillates between rungs.

Root cause

The packager authored the sidx box but the ftyp brand set in the same media segment is `isom + dash + cmfv` — enough for plain playback but missing the Sidx-aware track identifier MSE looks for when the brand is declared. sidx.parse_ok catches the ftyp/sidx mismatch before MSE does; sidx.references_inline fails because the brand mismatch blocks the append.

Fix

Align the ftyp brand set on every media segment so it includes both `msdh` and `msix` whenever the same track publishes an sidx box; have the packager treat this as a packaging lint. Streamwake fires sidx.parse_ok on the next probe cycle the moment the brand set clears.

09sidx subsegment durations contradict `SegmentTimeline`
failure mode
Symptom

MPD parses green, but the player believes the segment is 6.0s while the actual bytes only carry 5.7s of media — ABR thinks the buffer underflows halfway through the segment, DESCENDS the ladder, ascends again, and reproduces that oscillation on every cadence. Tail viewers report a half-second rebuffer every 8–10 seconds.

Root cause

The sidx subsegment durations add up to a $Duration$ that disagrees with the SegmentTimeline `S@t/r/d` on the parent Representation. Cross-element drift — the player schedules the next fetch by $Duration$ but the byte stream only carries what the sidx boxes advertise. segment.tail_window fires because tail-window-bytes is wrong against the declared timeline; sidx.parse_ok fires as the secondary because the box-carve does not match the timeline.

Fix

Emit sidx subsegment durations that sum to the SegmentTimeline `S@duration` on the parent Representation; refuse to publish until sum(S@d) == Sum(sidx.ref_size) / $Timescale$. Streamwake pairs sidx.parse_ok with segment.tail_window so the cross-element drift surfaces as a single verdict.

10MPD parses green but `availabilityTimeOffset` shifts the live window past the player’s seek range
failure mode
Symptom

A cohort that joined the live edge starts seeing one of two regression patterns depending on player: native dash.js refetches every segment from the start because it interprets the offset as "the window moved"; shaka-player holds the tail and stops advancing, and latency creeps from 4s to 18s.

Root cause

`availabilityTimeOffset` was tuned too aggressively for the manufacturing-server cadence — segments the player expected to be ready are still not ready. The player compensates by seeking forward past the offset, which pulls the seek window into the past without ripping forward. window_anchor_advanced trips on the first mismatch; availability_offset.coherent flips as the secondary probe.

Fix

Tune offset to match the manufacturing-server’s actual publish cadence (median over the last N windows, not the worst case); re-tune at the next MPD reload. Streamwake asserts availability_offset.coherent + window_anchor_advanced on the same probe cycle so the loop closes on a single cadence.

Agentic-ops layer

Heuristics: how the agent loop classifies the incident

The probe families above produce verdicts. Three rules in the agentic-ops layer turn a stream of verdicts into an incident classification — without a human reading the timeline.

TTFB drift
fetch.ttfb_drift → egress saturation signal
heuristic
  • Rule: TTFB p95 across the last N probes vs the phased baseline for the same cohort + region-of-origin.
  • What it surfaces: origin egress saturation on sidx-bearing Representations and shipping-edge cold-cache fills on SegmentTemplate URI families — long before any segment actually 4xxs.
  • Agent does next:raise probe cadence for this stream, prepend to the alert feed, and open an incident tagged "egress_pressure" — actionable without waiting for a rebuffer report.
Fetch-concurrency exhaustion
fetch.concurrency → join-storm + pool-ceiling signal
heuristic
  • Rule:in-flight fetches on the outbound pool vs the cohort’s expected fanout; pool-utilization > 85% over more than one cadence.
  • What it surfaces: a DASH join storm where every player fetches init.mp4 + sidx + the head of SegmentTimeline in parallel — pins a CDN edge that was sized for steady-state.
  • Agent does next: auto-throttle probe fan-out for this stream, meta-classify as "join_storm vs regional_skew", and emit a routing suggestion rather than a rebuffer alarm.
Segment-numbering drift
segment.numbering_drift + window_anchor_advanced → packager / Period signal
heuristic
  • Rule:gap > 1 in the resolved $Number$ sequence on any Representation across the trailing 6 segments; packager restart + startNumber reset pairs with a non-advanced publishTime.
  • What it surfaces: a packager-restart Number drop, a hot-swap gap, or a Period boundary the representations did not inherit Number across — all of those break the trailing 6-segment invariant.
  • Agent does next:classify "packager_number_drift", mark the affected stream as affected-attached, and force a fresh MPD reload + replay of the trailing window on the next cadence — closes the loop that a passive monitor cannot.
Why this reduces MTTR
Active classify → remediate → verify vs humans-on-pager

The failure-mode rows above trace back to the three heuristics: sidx parse failures on a republishing stack surface as TTFB drift on the sidx-bearing representations before they surface as MSE append errors; availabilityTimeOffset misuse on low-latency DASH looks like fetch-concurrency exhaustion the moment the cohort joins at peak; packager restarts and Period boundaries surface as segment-numbering drift before any header-level probe notices. A passive-monitor logs the same verdicts and waits for a rebuffer report to fire — by which point you're already paying the cohort-trust cost and writing the postmortem. The agent loop flips the polarity: classify the verdict under one of the three heuristics, remediate by replaying the trailing window or rebalancing the sidx republisher, then verify that the next probe cycle clears with availability_offset.coherent in green. Each heuristic closes a loop that a passive monitoring pipeline cannot.

Diagnose

Diagnose with Streamwake

Register the MPEG-DASH source against POST /api/v1/streams with protocol: DASH, 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 live DASH source URL and asks for a 30-second probe cadence. The "agents" list is what makes the agent run the manifest-reachable, numbering-drift, sidx-parse, and availability-offset probes on every rebuild — those four are the brief-driven probes the timeline above calls out by name.

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, then well-formed + SegmentTemplate + SegmentTimeline, then the new brief-driven probes (segment.numbering_drift, sidx.parse_ok, availability_offset.coherent), with the heuristic layer fetch.ttfb_drift + fetch.concurrency last because those are the ones that close the loop with the cohort.

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,
    "agents": [
      "manifest.reachable",
      "manifest.well_formed",
      "segment.head_window",
      "segment.tail_window",
      "segment.numbering_drift",
      "window_anchor_advanced",
      "sidx.parse_ok",
      "sidx.references_inline",
      "availability_offset.coherent",
      "ladder.bandwidth_order",
      "fetch.ttfb_drift",
      "fetch.concurrency"
    ]
  }'
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": "cklivedashevent205",
  "source": "https://cdn.example.com/live/event/manifest.mpd",
  "protocol": "DASH",
  "profiles": "urn:mpeg:dash:profile:isoff-live:2011",
  "checks": [
    {
      "probe": "manifest.reachable",
      "result": "pass",
      "latency_ms": 138,
      "detail": "content-type application/dash+xml"
    },
    {
      "probe": "manifest.well_formed",
      "result": "pass",
      "detail": "MPD type=dynamic; profiles match isoff-live:2011; no BOM; SegmentTemplate + SegmentTimeline present"
    },
    {
      "probe": "segment.head_window",
      "result": "pass",
      "latency_ms": 92,
      "segments_checked": 6
    },
    {
      "probe": "segment.tail_window",
      "result": "pass",
      "detail": "SegmentTimeline S@t/d monotonic; presentationTimeOffset=0 on every Representation; PDT projection contiguous"
    },
    {
      "probe": "ladder.bandwidth_order",
      "result": "pass",
      "representations_checked": 3,
      "detail": "AdaptationSet v1/v2/v3 sorted ascending (628K / 2428K / 4828K); BaseURL resolve-rate 100% across 1 Period"
    },
    {
      "probe": "segment.numbering_drift",
      "result": "fail",
      "detail": "Numbering jumped 7421 → 1 across a packager restart; startNumber reset to 1 in MPD@7422; tail-window probe integrity broken until next reload + replay"
    },
    {
      "probe": "window_anchor_advanced",
      "result": "warn",
      "detail": "MPD publishTime advanced, but availabilityStartTime + timeShiftBufferDepth did not move forward; live edge stalled at 9.8s even though segments kept landing"
    },
    {
      "probe": "sidx.parse_ok",
      "result": "fail",
      "detail": "SegmentBase indexRange on v2 references 0x10C0–0x18FF; sidx box exists but 2/4 SAP=1 references overlap a subsegment that the packager is mid-rewriting for low-latency"
    },
    {
      "probe": "sidx.references_inline",
      "result": "fail",
      "detail": "4/4 sidx reference byte ranges resolved out-of-line (cross media segment, not inside init URL); MSE rejects first append — sidx inline-vs-out-of-line contract violated"
    },
    {
      "probe": "availability_offset.coherent",
      "result": "warn",
      "detail": "audio AdaptationSet declares AvailabilityTimeOffset value=2.0 (positive = 'available past now'); video AdaptationSet omits it (defaults to 0 = 'available now'); A/V drift on join"
    },
    {
      "probe": "fetch.ttfb_drift",
      "result": "warn",
      "detail": "TTFB p95 1180ms vs phased baseline 360ms — egress saturation suspected on sidx-bearing Representations"
    },
    {
      "probe": "fetch.concurrency",
      "result": "pass",
      "detail": "outbound fan-out 64% of pool ceiling — within tolerance"
    }
  ]
}
Pair

Read the protocol-by-protocol pair

The MPEG-DASH guide above covers the manifest-parse + SegmentTemplate + SegmentTimeline + sidx + availabilityTimeOffset probes that apply to every DASH workflow. The peer troubleshooting surface at /troubleshooting/mpeg-dash covers the broader catalog — late-join stalls, 404 storms, ABR oscillation, Period / MSE / DRM edges, duration drift, and multi-CDN signing-region issues. The HLS guide is the protocol-by-protocol sibling, and the LL-HLS guide is the low-latency HLS arm of the pair.

Next step

Want Streamwake to catch this on its own?

Sign up, register a DASH source with protocol: DASH, and the same manifest-parse + SegmentTimeline + numbering-drift + sidx-parse + availability-offset + ladder-order probes that produced the timeline above run on every cadence — 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 cadence.
  • 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.
Need Streamwake on one of your incidents?
Would you like Streamwake to analyze one of your historical incidents and show where AI could reduce investigation time? (Filed under: MPEG-DASH troubleshooting.)
Incident analysis
  • Pick a recent on-call incident — manifest stall, edge miss, player-side stall, or peer congestion.
  • We replay it through the same reliability-agent probe cascade used on the postmortem above.
  • You walk away with a written what-could-have-been-Automated readout, not a sales deck.
Read the next

Related writeups

The closest siblings cover the cache-tier / origin-shield fan-in failure-mode shapes — particularly the correlated upstream cache-miss storm where the cohort stays CALM while the upstream origin-shield fans into the tier past its pre-provisioned ceiling, classified at 84% with cdn_edge_pop_warmup_under_eviction ruled out by name on warm edge POP segment-leg cache-hit. Recovery verified cohort-side + shield-side across four staged gates T+30 s → T+15 m, NOT infrastructure-green.