diff --git a/src/state/CallViewModel/remoteMembers/Connection.test.ts b/src/state/CallViewModel/remoteMembers/Connection.test.ts index 9d098d445..248d66fbb 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.test.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.test.ts @@ -516,11 +516,12 @@ describe("remote track logging", () => { it("logs remote participant and track events on the connection logger", () => { setupTest(); const info = vi.fn(); + const warn = vi.fn(); const testLogger = { getChild: (): unknown => testLogger, info, debug: vi.fn(), - warn: vi.fn(), + warn, error: vi.fn(), } as unknown as Logger; new Connection( @@ -545,6 +546,7 @@ describe("remote track logging", () => { source: "microphone", trackSid: "TR_mic", isMuted: false, + isEncrypted: true, } as unknown as RemoteTrackPublication; const messages = (): string[] => info.mock.calls.map((c) => c[0] as string); @@ -564,11 +566,27 @@ describe("remote track logging", () => { ); expect(messages()).toEqual([ "Participant connected: @bob:example.org:DEV111 (PA_bob)", - "Subscribed: audio microphone TR_mic of @bob:example.org:DEV111 muted=false", - "Muted: audio microphone TR_mic of @bob:example.org:DEV111", - "Stream paused: audio microphone TR_mic of @bob:example.org:DEV111", + "Subscribed: audio microphone TR_mic of @bob:example.org:DEV111 encrypted=true muted=false", + "Muted: audio microphone TR_mic of @bob:example.org:DEV111 encrypted=true", + "Stream paused: audio microphone TR_mic of @bob:example.org:DEV111 encrypted=true", ]); + // Encryption status changes are logged; cryptor errors are warnings + fakeLivekitRoom.emit( + RoomEvent.ParticipantEncryptionStatusChanged, + false, + bob, + ); + expect(messages().at(-1)).toBe( + "Encryption status of @bob:example.org:DEV111: encrypted=false", + ); + const cryptorError = new Error("missing key at index 3"); + fakeLivekitRoom.emit(RoomEvent.EncryptionError, cryptorError, bob); + expect(warn).toHaveBeenCalledWith( + "Encryption error for @bob:example.org:DEV111:", + cryptorError, + ); + // Local mute events are already logged by the Publisher fakeLivekitRoom.emit(RoomEvent.TrackMuted, pub, { ...fakeLocalParticipant, diff --git a/src/state/CallViewModel/remoteMembers/Connection.ts b/src/state/CallViewModel/remoteMembers/Connection.ts index 9310c9b82..10478cb77 100644 --- a/src/state/CallViewModel/remoteMembers/Connection.ts +++ b/src/state/CallViewModel/remoteMembers/Connection.ts @@ -186,8 +186,11 @@ export class Connection { private logRemoteTrackEvents(): void { const room = this.livekitRoom; const log = this.logger.getChild("[RemoteTracks]"); + // The encryption flag matters: if the publisher encrypts but this client + // believes the track is unencrypted, the cryptor is bypassed and raw + // ciphertext reaches the decoder (audible as loud noise bursts). const track = (pub: TrackPublication, p: Participant): string => - `${pub.kind} ${pub.source} ${pub.trackSid} of ${p.identity}`; + `${pub.kind} ${pub.source} ${pub.trackSid} of ${p.identity} encrypted=${pub.isEncrypted}`; const onParticipantConnected = (p: RemoteParticipant): void => log.info(`Participant connected: ${p.identity} (${p.sid})`); @@ -232,6 +235,20 @@ export class Connection { state: Track.StreamState, p: RemoteParticipant, ): void => log.info(`Stream ${state}: ${track(pub, p)}`); + const onEncryptionStatusChanged = ( + encrypted: boolean, + p?: Participant, + ): void => + log.info( + `Encryption status of ${p?.identity ?? "unknown participant"}: encrypted=${encrypted}`, + ); + // livekit-client throttles these per cryptor; they indicate frames being + // dropped (missing/invalid key), which is the other half of the picture. + const onEncryptionError = (error: Error, p?: Participant): void => + log.warn( + `Encryption error for ${p?.identity ?? "unknown participant"}:`, + error, + ); room .on(RoomEvent.ParticipantConnected, onParticipantConnected) @@ -243,7 +260,12 @@ export class Connection { .on(RoomEvent.TrackSubscriptionFailed, onTrackSubscriptionFailed) .on(RoomEvent.TrackMuted, onTrackMuted) .on(RoomEvent.TrackUnmuted, onTrackUnmuted) - .on(RoomEvent.TrackStreamStateChanged, onTrackStreamStateChanged); + .on(RoomEvent.TrackStreamStateChanged, onTrackStreamStateChanged) + .on( + RoomEvent.ParticipantEncryptionStatusChanged, + onEncryptionStatusChanged, + ) + .on(RoomEvent.EncryptionError, onEncryptionError); this.scope.onEnd(() => { room .off(RoomEvent.ParticipantConnected, onParticipantConnected) @@ -255,7 +277,12 @@ export class Connection { .off(RoomEvent.TrackSubscriptionFailed, onTrackSubscriptionFailed) .off(RoomEvent.TrackMuted, onTrackMuted) .off(RoomEvent.TrackUnmuted, onTrackUnmuted) - .off(RoomEvent.TrackStreamStateChanged, onTrackStreamStateChanged); + .off(RoomEvent.TrackStreamStateChanged, onTrackStreamStateChanged) + .off( + RoomEvent.ParticipantEncryptionStatusChanged, + onEncryptionStatusChanged, + ) + .off(RoomEvent.EncryptionError, onEncryptionError); }); }