Skip to content

moq export ts loses most of the feed under random packet loss since #4001 #4613

Description

@t0ms

Since #4001 (order TS export by media time), moq export ts loses most of a live feed when the path
has random packet loss. Before it, the same path delivers the full rate.

Setup: two network namespaces on one Linux host, relay + moq import ts on one side, moq export ts --max-age 2s on the other. netem adds 10% uniform loss and 25 ms delay on the relay → subscriber
leg, 25 ms delay on the return leg, and no rate limit. The source is a real ~10 Mb/s broadcast TS
(H.264 + MP2 + SI + several data PIDs), paced with tsp -P regulate. Each run lasts 120 s over QUIC
(noq).

I bisected with the relay and the importer held at an older build (84b34f54) and only the
subscriber's moq binary changed:

subscriber mean delivered second half of run
ffa5b81b 10.69 Mb/s 11.04
044ca571 (parent of #4001) 10.40 Mb/s 10.50
66440a6c (#4001) 0.67, 0.49 Mb/s 0.35, 0.25
2b689c24 0.54 Mb/s 0.40
9d2a4f6e (main) 0.73 Mb/s 0.26

It isn't the controller, the relay or the wire version:

  • With CUBIC in place of BBR, main still collapses: 0.80 Mb/s against 6.87 for the older build.
  • A main relay feeding an old subscriber delivers 10.15 Mb/s.
  • The old relay caps the session at moq-lite-05, and main's subscriber still collapses on it.

At 0% loss main and the old build both deliver about 9.8 Mb/s. Delivery doesn't settle; it decays
through the run.

Still there on 6f1a9e33, all roles on that build: 1.77 and 1.52 Mb/s at 10% loss against 9.84 at 0%.

My guess, from reading the change rather than measuring it: pick_next_track now holds the earliest
frame for up to max_age until every track has shown it can't precede it. Under loss some track is
nearly always behind, so the exporter spends the same budget that the consumer then enforces when it
evicts groups (current group evicted; skipping to next buffered group). Each hold becomes skipped
content. If that's right, the hold should be capped well below max_age, or charged against it.

Happy to rerun against a branch. The rig is a single shell script plus netem.

Activity

  1. t0ms commented on Sep 30, 2026

    @t0ms
    ContributorAuthor

    Found it. My guess above was half right. The hold does spend the sources' budget, but the collapse comes from rewind() renewing it.

    Under loss a source now and then skips a group, and skipping over a hole bumps its discontinuity(). The exporter takes that as a new generation, and rewind() resets stall, refreshes the held frame's arrived and clears every track's timeline. The sparse data PIDs, and video while it waits on a retransmit, then can't show, so the next frame waits the full max_age again. That's the 2 s the sources skip on. After the hold they're a budget behind the newest content, the next stall skips at once, and that's another rewind.

    I added logging to pick_next_track and the consumer's skip paths on main and reran the 10% arm. 56 holds in 120 s: 2 ended with the lagging track catching up, and a rewind restarted the rest at the full 2 s. The sources fell steadily behind the newest content, to 25 s after about 35 s and 105 s by the end.

    Same build and path, only the hold changed:

    mean second half
    hold as on main 1.13 Mb/s 0.36
    no hold 9.80 9.95
    hold capped at 200 ms 8.37 8.54
    hold kept across rewinds 9.70, 9.87 10.47, 10.13

    At 0% loss, main and the fix both give about 9.85 Mb/s, with one hold each.

    A cap only shrinks it, because each rewind still renews the hold. #4618 keeps the stall and the held frame's wait across a rewind instead, which reverses the two rewind_restarts_* tests from #4001.

  2. added
    questBeing tracked/planned in a quest. See `quest/`
    on Sep 30, 2026
  3. t0ms commented on Oct 12, 2026

    @t0ms
    ContributorAuthor

    A correction to my figures here and in #4618. The Mb/s were egress bytes, and the export pads to the
    mux rate, so they measured how much it wrote, not how much programme it carried.

    Graded on content (video access units against a 0 % run, same rig: two namespaces, 10 % netem loss
    and 25 ms each way on the relay → subscriber leg, the ~10 Mb/s broadcast clip, 120 s):

    Build Loss Egress Null packets Video access units
    main at 83ce47fe (with #4618) 0 % 9.85 Mb/s 4 % 4,262 (100 %)
    main at 83ce47fe (with #4618) 10 % 9.92, 10.49 Mb/s 88.5, 94.2 % 339, 109 (8.0, 2.6 %)
    84b34f54 10 % 7.21 Mb/s 95.9 % 74 (2.7 %)

    So #4618 fixed a real stall: the hold no longer compounds and the exporter keeps writing at the mux
    rate. It did not bring back the programme lost under loss, which goes before the exporter. The
    passthrough export (#5349) on the same rig, which has no interleave, keeps 6 % of the video, and its log shows
    the relay ending groups as Old while their streams wait on the transport. The title's symptom
    (most of the feed lost at 10 % loss) still holds on main. Happy to open a fresh issue for that part
    if you'd rather keep this one closed.

    (Written by Claude Opus 5.5)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    questBeing tracked/planned in a quest. See `quest/`

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions