Skip to content

moq-net: spliced track idle linger retains production demand along with cached state #3704

Description

@pwrwpw

Problem

The idle linger introduced in #2505 keeps a spliced segment warm so that a returning viewer or the next HLS fetch can reuse it. After the last reader leaves, the segment retains a track::Consumer on its source for TRACK_IDLE_LINGER (30 s).

That consumer also counts toward track::Demand, which is documented as a way to gate on-demand capture and encoding on “whether anyone is subscribed.” As a result, retaining cached state also keeps production demand active.

The linger documentation says a warm copy “holds no upstream subscription.” Whether that is true depends on how the subscription is canceled:

  • Publisher: the session resolves each SUBSCRIBE through its own origin.request_broadcast. The warm segment therefore retains a consumer on the publisher's track, delaying Demand::unused() by one linger period after the last subscriber leaves.
  • moq-transport hop: run_subscribe cancels the upstream SUBSCRIBE only when its accepted copy goes unused. The hop's warm segment adds another linger period before the publisher receives the cancellation.
  • moq-lite hop: upstream cancellation is unaffected. TrackServe cancels based on the subscription aggregate without waiting for its copy to go unused.

A grace period before stopping an encoder can be reasonable. The issue is that this period is currently determined by a cache retention policy intended for HLS fetch cadence, and the delays accumulate across the tested moq-transport path.

Measured behavior

I used a mock session pair from moq-net/tests/support with paused time. The table shows the elapsed time from dropping the last subscriber to Demand::unused() on the publisher's track:

Path Linger: 30 s Linger: 7 s
Subscriber on the publisher's own origin 30 s 7 s
moq-lite-05 session 30 s 7 s
moq-transport-14 / 17 / 19 session 60 s 14 s

The delay scales with TRACK_IDLE_LINGER in every tested path, identifying the linger as the cause in these configurations. I also reproduced the publisher-side delay through libmoq's moq_origin_request: 30.0 s in real time.

Could the subscription aggregate be used instead?

Producer::subscription_changed is what the moq-lite relay uses, so I checked it as a possible alternative in the same harness:

  • moq-transport-14 / 19: the aggregate was Some while subscribed and changed to None 30 s after the subscriber left.
  • Publisher's own origin and moq-lite-05: the aggregate remained None while a subscriber was attached, with max_age set to both 0 and 5 s. The subscriber did not read any groups in this test, so the result may differ when groups flow.

It was therefore not a usable substitute in these tests: it lagged by the same linger on the moq-transport paths and did not indicate demand on the other two.

Impact

This affects demand-based control on tracks served through an origin, including moq-ffi used/unused and the libmoq demand watcher being implemented for #3678.

In the tested configurations, the publisher's track remained demanded for 30 to 60 s after the last viewer left. An encoder gated on that demand would continue encoding for the same period.

Possible approaches

  1. Skip the linger when the source is a local producer that already retains the cache. This is expected to remove the publisher-side delay, but a moq-transport hop would still add its own linger.
  2. Retain the warm segment without counting it toward demand, for example through a handle that does not count as a consumer. This is expected to address both cases, but has not been implemented or verified.
  3. Keep the behavior and document that demand includes the cache linger and can accumulate delay across the tested moq-transport path.

Found while implementing #3678.
(Written by Claude Opus 5)

Activity

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