fix(nests): T10 endGroup() on unmuted→muted transition
When the user mutes mid-broadcast, `NestMoqLiteBroadcaster`'s send loop just `continue`s past the mute check, leaving the current group's QUIC uni stream open with no FIN. The watcher's `Container.Consumer.#runGroup` parks on `await group.consumer.readFrame()` waiting for either another frame or a stream FIN — and gets neither. kixelated/hang's UI surfaces this as a "stalled" indicator on the speaker tile while we're muted, which is wrong: we're muted, not stalled. FIN the open uni stream once on the unmuted→muted edge via a `wasMuted` latch that the muted→unmuted edge clears. Doesn't change the timestamp counter — it still advances on muted frames so an unmute's first timestamp reflects the muted duration as a real wall- clock gap (not a collapse to zero). `runCatching` around endGroup because a transport-side close racing with the FIN is non-fatal — the broadcaster's terminal-failure path picks up real breakage on the next live frame. Also resets `framesInCurrentGroup` to 0 so the post-unmute send path opens a fresh group cleanly via the standard `currentGroup ?: openNextGroupLocked()` branch in `PublisherStateImpl.send`. https://claude.ai/code/session_014JfZJHSTvyYYWJbC9VbB47
This commit is contained in:
+36
-1
@@ -238,6 +238,21 @@ class NestMoqLiteBroadcaster(
|
|||||||
// wall-clock TimeMark did: timestamps are codec-payload
|
// wall-clock TimeMark did: timestamps are codec-payload
|
||||||
// metadata, not stream-position.
|
// metadata, not stream-position.
|
||||||
var nextFrameIndex = 0L
|
var nextFrameIndex = 0L
|
||||||
|
// Mute-edge tracking. On the unmuted→muted transition we
|
||||||
|
// FIN the open uni stream so the watcher sees a clean
|
||||||
|
// group boundary instead of a half-delivered group that
|
||||||
|
// never completes — kixelated/hang's
|
||||||
|
// `Container.Consumer.#runGroup` parks on
|
||||||
|
// `await group.consumer.readFrame()` until either a
|
||||||
|
// frame arrives or the group hits FIN, and renders a
|
||||||
|
// "stalled" indicator while it waits. Without the FIN,
|
||||||
|
// muting Amethyst lights up that indicator on every web
|
||||||
|
// watcher even though the broadcast is intentionally
|
||||||
|
// silent. The FIN clears it; unmuting opens a fresh
|
||||||
|
// group cleanly via the standard
|
||||||
|
// `currentGroup ?: openNextGroupLocked()` path inside
|
||||||
|
// `PublisherStateImpl.send`.
|
||||||
|
var wasMuted = false
|
||||||
try {
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
val pcm = capture.readFrame() ?: break
|
val pcm = capture.readFrame() ?: break
|
||||||
@@ -259,7 +274,27 @@ class NestMoqLiteBroadcaster(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (opus.isEmpty()) continue
|
if (opus.isEmpty()) continue
|
||||||
if (muted) continue
|
if (muted) {
|
||||||
|
// Unmuted → muted edge: FIN the current
|
||||||
|
// group's uni stream once. Doesn't reset
|
||||||
|
// [framesInCurrentGroup] since
|
||||||
|
// `endGroup` is a no-op when no group is
|
||||||
|
// open, and the next post-unmute send will
|
||||||
|
// open a fresh group anyway. `runCatching`
|
||||||
|
// because a transport-side close during
|
||||||
|
// mute is non-fatal — the broadcaster's
|
||||||
|
// terminal-failure path picks up real
|
||||||
|
// breakage on the next live frame.
|
||||||
|
if (!wasMuted) {
|
||||||
|
wasMuted = true
|
||||||
|
runCatching { publisher.endGroup() }
|
||||||
|
framesInCurrentGroup = 0
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Muted → unmuted edge: clear the latch so the
|
||||||
|
// next mute transition fires endGroup again.
|
||||||
|
wasMuted = false
|
||||||
// Snapshot the publisher reference once per frame.
|
// Snapshot the publisher reference once per frame.
|
||||||
// If [swapPublisher] mid-loop installed a new
|
// If [swapPublisher] mid-loop installed a new
|
||||||
// reference, pick it up here and reset the group
|
// reference, pick it up here and reset the group
|
||||||
|
|||||||
Reference in New Issue
Block a user