Follow-up to #1151 / #1152, filed at @hiroshihorie's request in the approval review on #1152.
Summary
_publishAudioTrack runs entirely inside _publishRunner (the SerialRunner that serializes all publish/unpublish/mute/enable operations), and after the track is already fully published it does an unbounded await on third-party processor code:
- processor await (audio):
|
await track.processor?.onPublish(room); |
|
|
- same pattern (video):
|
info: trackInfo, |
|
track: track, |
- the lock:
|
final _publishRunner = SerialRunner<LocalTrackPublication?>(); |
with
|
while (_pending != null) { |
|
await _pending; |
|
} |
TrackProcessor.onPublish is an open extension point, so the SDK is awaiting arbitrary user/plugin code — in livekit_noise_filter's case, a server round-trip — inside its most contended critical section. If that future is slow or never resolves:
- The track is already published and live (
addTrackPublication has run, audio/video flows to remote participants).
- The
setMicrophoneEnabled(true) / publish*Track future doesn't resolve.
- The held
SerialRunner blocks every subsequent setMicrophoneEnabled / setCameraEnabled / setSourceEnabled / unpublish — the user cannot mute a live microphone for the rest of the session.
#1151 was one concrete trigger (the getSid() deadlock, now fixed); this issue is about the amplifier that turned it from "one hung future" into "an unmutable live mic". Any future slow onPublish reproduces the same failure.
Proposal
Detach the processor handoff from the publish critical section: the track is already fully published when onPublish runs, so treat it as asynchronous decoration — run track.processor?.onPublish(room) unawaited with errors logged (or await it after the runner block returns), instead of holding the lock on it. ~10-line diff at the two call sites, plus a regression test (fake processor whose onPublish never resolves; assert publish returns and a subsequent mute completes — same E2EContainer harness as #1152).
Alternative, if completion-before-return must be preserved: bound the await with a timeout + logged warning. Weaker — it converts "forever" into "N seconds of unmutable mic" and picks an arbitrary constant.
One semantics question I'd like your call on before opening the PR: is anything expected to rely on onPublish completing before track.start() / before the publish future resolves? If yes, the timeout variant (or detach-after-start()) is the shape.
Complementary (separate repo, happy to file there too): livekit_noise_filter.onPublish blocks on await room.getSid() just to build its context map — it could send the context immediately and update the sid when it resolves, making the plugin robust on old SDK versions as well.
I'm glad to open the PR for whichever direction you prefer.
Follow-up to #1151 / #1152, filed at @hiroshihorie's request in the approval review on #1152.
Summary
_publishAudioTrackruns entirely inside_publishRunner(theSerialRunnerthat serializes all publish/unpublish/mute/enable operations), and after the track is already fully published it does an unbounded await on third-party processor code:client-sdk-flutter/lib/src/participant/local.dart
Lines 239 to 240 in 9e05b79
client-sdk-flutter/lib/src/participant/local.dart
Lines 519 to 520 in 9e05b79
client-sdk-flutter/lib/src/participant/local.dart
Line 68 in 9e05b79
client-sdk-flutter/lib/src/support/serial_runner.dart
Lines 38 to 40 in 9e05b79
TrackProcessor.onPublishis an open extension point, so the SDK is awaiting arbitrary user/plugin code — inlivekit_noise_filter's case, a server round-trip — inside its most contended critical section. If that future is slow or never resolves:addTrackPublicationhas run, audio/video flows to remote participants).setMicrophoneEnabled(true)/publish*Trackfuture doesn't resolve.SerialRunnerblocks every subsequentsetMicrophoneEnabled/setCameraEnabled/setSourceEnabled/ unpublish — the user cannot mute a live microphone for the rest of the session.#1151 was one concrete trigger (the
getSid()deadlock, now fixed); this issue is about the amplifier that turned it from "one hung future" into "an unmutable live mic". Any future slowonPublishreproduces the same failure.Proposal
Detach the processor handoff from the publish critical section: the track is already fully published when
onPublishruns, so treat it as asynchronous decoration — runtrack.processor?.onPublish(room)unawaited with errors logged (or await it after the runner block returns), instead of holding the lock on it. ~10-line diff at the two call sites, plus a regression test (fake processor whoseonPublishnever resolves; assert publish returns and a subsequent mute completes — sameE2EContainerharness as #1152).Alternative, if completion-before-return must be preserved: bound the await with a timeout + logged warning. Weaker — it converts "forever" into "N seconds of unmutable mic" and picks an arbitrary constant.
One semantics question I'd like your call on before opening the PR: is anything expected to rely on
onPublishcompleting beforetrack.start()/ before the publish future resolves? If yes, the timeout variant (or detach-after-start()) is the shape.Complementary (separate repo, happy to file there too):
livekit_noise_filter.onPublishblocks onawait room.getSid()just to build its context map — it could send the context immediately and update the sid when it resolves, making the plugin robust on old SDK versions as well.I'm glad to open the PR for whichever direction you prefer.