fix: don't skip receiver frame cryptor when track metadata arrives late (+ sync upstream, drop fork-only changes) - #3
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Unencrypted remote tracks wait publish timeout
- Changed the wait-loop condition from
encryptionType == kNone(which is also true for confirmed-unencrypted tracks) tolatestInfo == null, so plaintext remotes with a valid mimeType exit the loop immediately instead of polling until the publish timeout.
- Changed the wait-loop condition from
Or push these changes by commenting:
@cursor push af34777981
Preview (af34777981)
diff --git a/lib/src/e2ee/e2ee_manager.dart b/lib/src/e2ee/e2ee_manager.dart
--- a/lib/src/e2ee/e2ee_manager.dart
+++ b/lib/src/e2ee/e2ee_manager.dart
@@ -132,7 +132,7 @@
final timeout = participant.room.connectOptions.timeouts.publish;
final deadline = DateTime.now().add(timeout);
var codec = _codecFromMimeType(publication.mimeType);
- while ((codec.isEmpty || publication.encryptionType == EncryptionType.kNone) && DateTime.now().isBefore(deadline)) {
+ while ((codec.isEmpty || publication.latestInfo == null) && DateTime.now().isBefore(deadline)) {
await Future.delayed(const Duration(milliseconds: 200));
// Manager was cleaned up, or the track was unsubscribed / replaced while
// waiting; a newer subscription will run its own setup.You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit e52bdb8. Configure here.
| return; | ||
| } | ||
| codec = _codecFromMimeType(publication.mimeType); | ||
| } |
There was a problem hiding this comment.
Unencrypted remote tracks wait publish timeout
Medium Severity
The remote frame-cryptor wait loop keeps running while publication.encryptionType == EncryptionType.kNone, but that value also means a confirmed unencrypted track once TrackInfo is applied. Plaintext remotes with a valid mimeType therefore poll until connectOptions.timeouts.publish (often 10s) before the handler can skip setup, delaying every such subscribe in E2EE-enabled rooms.
Reviewed by Cursor Bugbot for commit e52bdb8. Configure here.
A remote track can be subscribed before its TrackInfo metadata (mimeType /
encryption) has been applied to the publication. The TrackSubscribedEvent
handler decided on that incomplete metadata: 'mimeType.split('/')[1]' threw
a RangeError on an empty mime type (killing the handler) and an unset
encryption read as kNone skipped the cryptor. Either way no frame cryptor
was ever created for the track, so with insertable streams enabled the
frames stayed encrypted forever - black video / silent audio that only a
renegotiation (e.g. toggling a screenshare) could unstick.
- wait (bounded by connectOptions.timeouts.publish) for mimeType/encryption
to be populated before deciding whether a frame cryptor is needed, and
abort cleanly when the track is unsubscribed or replaced while waiting
- parse the codec from the mime type safely instead of throwing on
malformed values, and still create the cryptor (without updateCodec) if
the mime type never arrives for an encrypted track
- attach onFrameCryptorStateChanged right after creating a cryptor (before
updateCodec) so E2EE state changes are always surfaced
- wrap both cryptor setup paths in try/catch with a warning log so failures
are visible instead of silently dropped
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Restore lib/src/core/room.dart and lib/src/track/web/_audio_html.dart to their upstream (livekit/client-sdk-flutter) versions, dropping the fork-only audioElementLogs debug logging and the 5x1s retry loop for _getRemoteParticipantBySid. The retry loop delayed media track event handling and made the room_e2e_test 'tracks arriving before participant metadata' test time out. Co-authored-by: td <td-famedly@users.noreply.github.com>
5e7b04b to
13d6085
Compare
EncryptionType.kNone is both 'metadata not applied yet' and 'confirmed unencrypted', so plaintext remote tracks in E2EE rooms polled the wait loop for the full publish timeout before skipping cryptor setup. Loop on publication.latestInfo == null instead, which only covers the metadata-not-yet-applied case; incomplete metadata is still awaited via the empty-codec check. Addresses Bugbot finding on PR #3. Co-authored-by: td <td-famedly@users.noreply.github.com>
Cover the three E2EEManager remote-track behaviors: - an encrypted track subscribed before its mimeType/encryption metadata arrives gets a frame cryptor once the metadata is applied, and cryptor state changes surface as TrackE2EEStateEvent (previously the handler died on a RangeError and no cryptor was ever created) - an encrypted track whose mimeType never arrives still gets a frame cryptor (without updateCodec) after the publish timeout - a confirmed-unencrypted track skips cryptor setup promptly instead of polling until the publish timeout To make E2EEManager testable without platform channels, expose the frame/data-packet cryptor factories as @VisibleForTesting fields. Test-infra changes: fake FrameCryptor/DataPacketCryptor factories, the media stream fakes from room_e2e_test moved to a shared mock (plus a fake RTCRtpReceiver), and E2EContainer accepts custom ConnectOptions. All three tests were verified to fail against the pre-fix handler (RangeError on the first two, delayed-skip timeout on the third). Co-authored-by: td <td-famedly@users.noreply.github.com>



Problem
A remote track can be subscribed before its
TrackInfometadata (mimeType/encryption) has been applied to the publication. TheTrackSubscribedEventhandler inE2EEManagerdecided on that incomplete metadata:mimeType.split('/')[1]threw aRangeErroron an empty mime type, killing the handler.kNone, so the frame cryptor was skipped.Either way no frame cryptor was ever created for the track, so with insertable streams enabled the frames stayed encrypted forever — black video / silent audio that only a renegotiation (e.g. toggling a screenshare) could unstick.
Changes
E2EE fix (
lib/src/e2ee/e2ee_manager.dart):connectOptions.timeouts.publish) for the publication'sTrackInfo(latestInfo) /mimeTypeto be populated before deciding whether a frame cryptor is needed, and abort cleanly when the track is unsubscribed or replaced while waiting. Confirmed-unencrypted tracks exit the wait immediately (Bugbot finding).updateCodec) if the mime type never arrives for an encrypted track.onFrameCryptorStateChangedright after creating a cryptor (beforeupdateCodec) so E2EE state changes are always surfaced.@visibleForTestingfields so tests can inject fakes instead of hitting platform channels.Regression tests (
test/e2ee/e2ee_manager_test.dart):TrackE2EEStateEvent.All three were verified to fail against the pre-fix handler (
RangeErroron the first two, delayed-skip timeout on the third). Test infra: fakeFrameCryptor/DataPacketCryptorfactories, media stream fakes fromroom_e2e_testmoved to a shared mock (plus a fakeRTCRtpReceiver), andE2EContaineraccepts customConnectOptions.Removed fork-only changes (separate commit):
lib/src/core/room.dartandlib/src/track/web/_audio_html.dartto their upstream (livekit/client-sdk-flutter) versions, dropping the[audioElementLogs]debug logging and the 5×1s retry loop for_getRemoteParticipantBySid. That retry loop delayed media track event handling and made theroom_e2e_test"tracks arriving before participant metadata" test time out.The branch is rebased onto the fork's
main(which now includes upstream's "Certificate pinning (livekit#1065)"), so after this PR the fork differs from upstreammainonly by the E2EE fix and its tests.Verification
dart format . --set-exit-if-changed— no changes.dart run import_sorter:main --no-comments --exit-if-changed— no changes.dart run scripts/check_version.dart— all version checks passed.flutter analyze— no issues.flutter test— all 344 tests pass (341 + the 3 new E2EE regression tests).