[bug] Stale and overlapping ParticipantInfo updates remove newer remote track publications (2.9.0)
I maintainer di solito rispondono entro 1 giorno
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 55/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Ambito
- backend-api-design
Direzione di ricerca
Start with lib/src/participant/remote.dart at RemoteParticipant.updateFromInfo() and _updateTracks(), then read the version check in lib/src/participant/participant.dart. Run the supplied stale_tracks_test.dart reproduction to confirm both stale and overlapping update failures. Done means obsolete snapshots cannot remove publications from a newer accepted snapshot, including across asynchronous reconciliation, and all three test cases pass.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Describe the bug
RemoteParticipant.updateFromInfo() can remove a track introduced by a newer ParticipantInfo snapshot. There are two reproducible cases on the unmodified livekit_client 2.9.0 pub.dev package:
- Stale update after a completed newer update: apply v2
[TR_old, TR_new], then v1[TR_old]. The base participant rejects v1, but the remote participant ignores that result, returnstrue, and deletesTR_new. - Overlapping updates delivered in increasing order: start v1
[TR_old], then v2[TR_old, TR_new]without awaiting the first call. Both updates returntrue, but the final publication map contains onlyTR_old. This case does not require out-of-order input delivery.
A sequential, increasing-version control preserves both publications.
To reproduce
This is a self-contained Flutter test against the actual published SDK. It uses SDK-internal APIs to isolate publication reconciliation, with an unconnected Room; it needs no LiveKit server, token, device, media capture, or application code. It reproduces the map mutation, not a complete network join or audible call.
Create pubspec.yaml:
name: livekit_stale_tracks_repro
publish_to: none
environment:
sdk: '>=3.6.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
livekit_client: 2.9.0
dev_dependencies:
flutter_test:
sdk: flutter
Create test/stale_tracks_test.dart:
// Intentionally exercise SDK internals to isolate snapshot reconciliation.
// ignore_for_file: implementation_imports, invalid_use_of_internal_member
import 'package:flutter_test/flutter_test.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:livekit_client/src/proto/livekit_models.pb.dart' as pb;
pb.ParticipantInfo snapshot(int version, List<String> tracks) =>
pb.ParticipantInfo(
sid: 'PA_repro',
identity: 'repro',
version: version,
state: pb.ParticipantInfo_State.ACTIVE,
tracks: tracks.map((sid) => pb.TrackInfo(
sid: sid,
name: sid,
type: pb.TrackType.AUDIO,
source: pb.TrackSource.MICROPHONE,
)),
);
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late Room room;
late RemoteParticipant participant;
setUp(() async {
room = Room();
participant = (await RemoteParticipant.createFromInfo(
room: room,
info: snapshot(0, []),
))
.participant;
});
tearDown(() async {
await participant.dispose();
await room.dispose();
});
test('control: sequential increasing versions preserve both tracks',
() async {
await participant.updateFromInfo(snapshot(1, ['TR_old']));
await participant.updateFromInfo(snapshot(2, ['TR_old', 'TR_new']));
expect(participant.trackPublications.keys,
unorderedEquals(['TR_old', 'TR_new']));
});
test('stale v1 must not remove a track already installed by v2', () async {
await participant.updateFromInfo(snapshot(2, ['TR_old', 'TR_new']));
final accepted = await participant.updateFromInfo(snapshot(1, ['TR_old']));
print(
'stale: accepted=$accepted tracks=${participant.trackPublications.keys.toList()}');
expect(participant.trackPublications.keys,
unorderedEquals(['TR_old', 'TR_new']));
expect(accepted, isFalse);
});
test('overlapping increasing updates must preserve the newest track',
() async {
final v1 = participant.updateFromInfo(snapshot(1, ['TR_old']));
final v2 = participant.updateFromInfo(snapshot(2, ['TR_old', 'TR_new']));
final accepted = await Future.wait([v1, v2]);
print(
'overlap: accepted=$accepted tracks=${participant.trackPublications.keys.toList()}');
expect(participant.trackPublications.keys,
unorderedEquals(['TR_old', 'TR_new']));
});
}
Run:
flutter pub get
flutter test --reporter expanded test/stale_tracks_test.dart
The assertions describe the expected correct behavior, so the two bug cases fail on 2.9.0.
Expected and actual behavior
| Case | Expected | Actual on published 2.9.0 |
|---|---|---|
| Sequential v1, then v2 | Both tracks remain | Pass |
| Completed v2, then stale v1 | accepted=false; both tracks remain |
accepted=true; only TR_old remains |
| Overlapping v1, then v2 | Newest snapshot wins; both tracks remain | accepted=[true, true]; only TR_old remains |
Observed output:
stale: accepted=true tracks=[TR_old]
Expected: equals ['TR_old', 'TR_new'] unordered
Actual: ['TR_old']
overlap: accepted=[true, true] tracks=[TR_old]
Expected: equals ['TR_old', 'TR_new'] unordered
Actual: ['TR_old']
1 passed, 2 failed
Restoring the early return alone is insufficient
As a diagnostic experiment, I copied the SDK into an isolated directory and changed only //return false; to return false; in RemoteParticipant.updateFromInfo(). With the same tests:
stale: accepted=false tracks=[TR_old, TR_new] # now passes
overlap: accepted=[true, true] tracks=[TR_old] # still fails
2 passed, 1 failed
Both overlapping updates can pass the version check before their asynchronous track reconciliation completes. The older _updateTracks() continuation can then remove the publication added by the newer update. Therefore merely uncommenting the early return does not cover both cases. A fix should also ensure per-participant reconciliation cannot apply obsolete mutations after a newer snapshot has been accepted, including across await boundaries.
SDK 2.9.0 provenance and source evidence
- Package:
livekit_client2.9.0, withflutter_webrtc1.5.2. - Re-downloaded the published 2.9.0 archive on 2026-10-21.
- Archive SHA-256, also matching the affected application's release lockfile:
8f8dd46fb9e58acedbc37efa890422f9e62d38a75d5937855126f81fc314b35b. - Verified that
participant/remote.dart,participant/participant.dart,core/room.dart, andmanagers/event.dartin the test's pub cache are byte-for-byte identical to the archive. - 2.9.0 base version check: returns
falsefor an older version of the same participant SID. - 2.9.0 remote update and reconciliation: the rejection is ignored, then
_updateTracks(info.tracks)removes publications absent from its input list. - The SDK's own room/event path can overlap asynchronous work: participant updates can wait for
RoomConnectedEvent, while the default event listener does not serialize asynchronous callbacks. This provides source context; the minimal test callsupdateFromInfo()directly.
Current upstream source
Checked main at d1ac905b819be096cdfde7fc4a7d688f6d1cb7c2 on 2026-10-21. The same relevant code remains:
- Current
main: remote participant - Pinned current source: ignored return and track reconciliation
- Pinned current source: base version check
The executable results above are for 2.9.0, not a runtime test of current main.
Production observation that prompted the test
In an Android call using 2.9.0, sanitized diagnostics showed this received/observed sequence for the same remote participant instance:
join snapshot v2: []
participant update v3: [track_A]
participant update v4: [track_A]
participant update v5: [track_A, track_B]
room connected
published(track_A): current map [track_A]
unpublished(track_B): current map [track_A]
published(track_B): current map [track_A]
track_B was the expected downlink audio publication. It remained published on the server, but was absent from subsequent client publication scans. The client reported zero subscribed remote audio publications and zero received audio bytes, while captions and upstream audio continued.
Evidence boundary: these are received snapshots and observer events, not traces of private SDK update-completion order. The production observation is consistent with this bug, but does not prove which exact interleaving occurred in that call. The two isolated SDK failures above are independently reproducible. All identifiers in this report/test are synthetic; no call recording, token, customer identity, or private application log is included.
Platform information
- Reproduction: Flutter 3.38.5, Dart 3.10.4, macOS host,
flutter test. - SDK under executable test: livekit_client 2.9.0, flutter_webrtc 1.5.2.
- Production observation: Android 16, physical device.
- No SDK modifications in the baseline test. The early-return-only diagnostic experiment is explicitly separate.
Related historical reports: livekit/livekit#162 describes a similar older-snapshot/new-track-loss pattern in a different SDK/server generation; #1051 concerns overlapping participant cleanup events; #937 concerns metadata arrival and rejoin subscription handling. None is being asserted to be the same defect or its fix.
- Lingua principale
- Dart
- Stelle
- 421
- Fork
- 260
- Merge medio
- 3g 18h
- PR unite (30g)
- 12
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di livekit/client-sdk-flutter
-
`jwtPayload.roomConfiguration` returns null fields for tokens from LiveKit server SDKsForse già presa @RaphaelFakhri l’ha presa 6 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
livekit/client-sdk-flutter#1243 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 57/100
livekit/client-sdk-flutter#1245 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 48/100
livekit/client-sdk-flutter#1227 ·
I maintainer di solito rispondono entro 1 giorno
-
Inbound data packets never delivered while media flows normally (2.13.0, LiveKit Cloud 1.13.7)Aperta
Difficoltà 4/5 3-5 giorni Idoneità per principianti 45/100
livekit/client-sdk-flutter#1221 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 30/100
livekit/client-sdk-flutter#1219 · 1 reazione ·
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di livekit/client-sdk-flutter
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
espresso3389/pdfrx#776 ·
I maintainer di solito rispondono entro 1 giorno
-
enhancement
Difficoltà 2/5 1-3 ore Idoneità per principianti 77/100
atsign-foundation/at_server#2896 ·
I maintainer di solito rispondono entro 1 giorno
-
backlog:low bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
simonoppowa/OpenNutriTracker#1445 ·
I maintainer di solito rispondono entro 2 giorni
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
lugassawan/swe-workbench#823 ·
I maintainer di solito rispondono entro 1 giorno
-
auto-implement
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
bannzai/kashakeibo#142 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno