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
- 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.
- 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.
- 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)
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::Consumeron its source forTRACK_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:
origin.request_broadcast. The warm segment therefore retains a consumer on the publisher's track, delayingDemand::unused()by one linger period after the last subscriber leaves.run_subscribecancels 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.TrackServecancels 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/supportwith paused time. The table shows the elapsed time from dropping the last subscriber toDemand::unused()on the publisher's track:The delay scales with
TRACK_IDLE_LINGERin every tested path, identifying the linger as the cause in these configurations. I also reproduced the publisher-side delay through libmoq'smoq_origin_request: 30.0 s in real time.Could the subscription aggregate be used instead?
Producer::subscription_changedis what the moq-lite relay uses, so I checked it as a possible alternative in the same harness:Somewhile subscribed and changed toNone30 s after the subscriber left.Nonewhile a subscriber was attached, withmax_ageset 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/unusedand 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
Found while implementing #3678.
(Written by Claude Opus 5)