main ← fix/livekit-camera-quality
merged 05:56PM - 15 Oct 26 UTC
Reported on Meta: https://meta.discourse.org/t/413325. With LiveKit, camera vide…o was low resolution (8–20 fps) and stuttered, on both self-hosted LiveKit and LiveKit Cloud. The LiveKit server logged `adding up track failed: duplicate layer` and `unbound buffer overflowing, dropping packets`.
### Viewers never received the full-quality camera stream
`#applySubscriberQuality` asked for the MEDIUM simulcast layer whenever there were 6 or fewer camera publishers. No explicit layers were configured, so the SDK built MEDIUM from its `h360` preset (640×360, 450 kbps, 20 fps). The quality preference only changed the HIGH layer, which nobody subscribed to, and dynacast then paused HIGH at the publisher.
- Cameras publish `videoSimulcastLayers` taken from the mesh camera ladder's lower rungs (new `cameraSimulcastLayersFor`), sized from the capture track's dimensions.
- Subscribers pick HIGH up to 3 camera publishers, MEDIUM up to 6, and LOW above that, matching where the mesh ladder steps down.
### The camera could be published twice
`syncLocalVideo` had no serialization. Starting the camera broadcasts state, and the resulting roster update called `syncLocalVideo` again while the first `publishTrack` was still in flight. `#videoPublication` was still null, so the track was published a second time. The SDK's "already published" guard doesn't catch this because it wraps raw `MediaStreamTrack`s in a fresh `LocalTrack` on every call. That left two encoders on one webcam, with the first publication orphaned until leave. `syncLocalVideo` calls now run through a promise queue.
### Tests
- The camera publish test now asserts the simulcast layers.
- The quality test covers HIGH, then MEDIUM above 3 publishers, then LOW above 6.
- New regression test: a roster update during a slow camera publish leaves exactly one camera publication.