Skip to content

Commit 6bfc91c

Browse files
authored
fix: rtpMap event listener leak on multiple negotiations without video tracks (#1961)
1 parent 59d29ab commit 6bfc91c

3 files changed

Lines changed: 60 additions & 21 deletions

File tree

.changeset/famous-ends-guess.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"livekit-client": patch
3+
---
4+
5+
fix: rtpMap event leak on multiple negotiations without video tracks

src/e2ee/worker/FrameCryptor.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
} from '../../packetTrailer/packetTrailer';
99
import type { PacketTrailerPublishOptions } from '../../packetTrailer/types';
1010
import { hasPacketTrailerPublishOptions } from '../../packetTrailer/utils';
11-
import type { VideoCodec } from '../../room/track/options';
11+
import { type VideoCodec, videoCodecs } from '../../room/track/options';
12+
import { mimeTypeToVideoCodecString } from '../../room/track/utils';
1213
import { ENCRYPTION_ALGORITHM, IV_LENGTH, UNENCRYPTED_BYTES } from '../constants';
1314
import { CryptorError, CryptorErrorReason } from '../errors';
1415
import { type CryptorCallbacks, CryptorEvent } from '../events';
@@ -106,6 +107,12 @@ export class FrameCryptor extends BaseFrameCryptor {
106107

107108
private readonly ERROR_WINDOW_MS = 60000; // 1 minute window
108109

110+
/**
111+
* Tracks (participant, trackId, payloadType) tuples for which we've already logged a NALU
112+
* fallback, so a persistent bad state doesn't flood the console (Firefox doesn't filter debug).
113+
*/
114+
private loggedNALUFallbacks: Set<string> = new Set();
115+
109116
constructor(opts: {
110117
keys: ParticipantKeyHandler;
111118
participantIdentity: string;
@@ -795,36 +802,64 @@ export class FrameCryptor extends BaseFrameCryptor {
795802
}
796803

797804
// Try NALU processing for H.264/H.265 codecs
805+
const payloadType = frame.getMetadata().payloadType;
806+
const fallbackKey = `${this.participantIdentity}-${this.trackId}-${payloadType}`;
798807
try {
799808
const knownCodec =
800809
detectedCodec === 'h264' || detectedCodec === 'h265' ? detectedCodec : undefined;
801810
const naluResult = processNALUsForEncryption(new Uint8Array(frame.data), knownCodec);
802811

803812
if (naluResult.requiresNALUProcessing) {
813+
// Recovered for this tuple, allow a future failure to log again.
814+
this.loggedNALUFallbacks.delete(fallbackKey);
804815
return {
805816
unencryptedBytes: naluResult.unencryptedBytes,
806817
requiresNALUProcessing: true,
807818
};
808819
}
809820
} catch (e) {
810-
workerLogger.debug('NALU processing failed, falling back to VP8 handling', {
811-
error: e,
812-
...this.logContext,
813-
});
821+
this.logNALUFallbackOnce(fallbackKey, payloadType, e);
814822
}
815823

816824
// Fallback to VP8 handling
817825
return { unencryptedBytes: UNENCRYPTED_BYTES[frame.type], requiresNALUProcessing: false };
818826
}
819827

820828
/**
821-
* inspects frame payloadtype if available and maps it to the codec specified in rtpMap
829+
* Logs a NALU processing fallback at most once per (participant, trackId, payloadType) tuple,
830+
* so a persistent bad state doesn't flood the console (Firefox doesn't filter debug).
831+
*/
832+
private logNALUFallbackOnce(
833+
fallbackKey: string,
834+
payloadType: number | undefined,
835+
error: unknown,
836+
) {
837+
if (this.loggedNALUFallbacks.has(fallbackKey)) {
838+
return;
839+
}
840+
this.loggedNALUFallbacks.add(fallbackKey);
841+
workerLogger.warn('NALU processing failed, falling back to VP8 handling', {
842+
error,
843+
payloadType,
844+
...this.logContext,
845+
});
846+
}
847+
848+
/**
849+
* inspects frame mimetype if available. falls back to payloadtype and maps it to the codec specified in rtpMap
822850
*/
823851
private getVideoCodec(frame: RTCEncodedVideoFrame): VideoCodec | undefined {
852+
const metadata = frame.getMetadata();
853+
if (metadata.mimeType) {
854+
const maybeKnownCodec = mimeTypeToVideoCodecString(metadata.mimeType);
855+
if (videoCodecs.includes(maybeKnownCodec)) {
856+
return maybeKnownCodec;
857+
}
858+
}
824859
if (this.rtpMap.size === 0) {
825860
return undefined;
826861
}
827-
const payloadType = frame.getMetadata().payloadType;
862+
const payloadType = metadata.payloadType;
828863
const codec = payloadType ? this.rtpMap.get(payloadType) : undefined;
829864
return codec;
830865
}

src/room/RTCEngine.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1746,20 +1746,8 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
17461746
}
17471747
this.on(EngineEvent.Closing, handleClosed);
17481748
this.on(EngineEvent.Restarting, handleClosed);
1749-
1750-
this.pcManager.publisher.once(
1751-
PCEvents.RTPVideoPayloadTypes,
1752-
(rtpTypes: MediaAttributes['rtp']) => {
1753-
const rtpMap = new Map<number, VideoCodec>();
1754-
rtpTypes.forEach((rtp) => {
1755-
const codec = rtp.codec.toLowerCase();
1756-
if (isVideoCodec(codec)) {
1757-
rtpMap.set(rtp.payload, codec);
1758-
}
1759-
});
1760-
this.emit(EngineEvent.RTPVideoMapUpdate, rtpMap);
1761-
},
1762-
);
1749+
this.pcManager.publisher.off(PCEvents.RTPVideoPayloadTypes, this.onRtpMapAvailable);
1750+
this.pcManager.publisher.once(PCEvents.RTPVideoPayloadTypes, this.onRtpMapAvailable);
17631751

17641752
try {
17651753
await this.pcManager.negotiate(abortController);
@@ -1906,6 +1894,17 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
19061894
this.shouldFailOnV1Path = true;
19071895
}
19081896

1897+
private onRtpMapAvailable = (rtpTypes: MediaAttributes['rtp']) => {
1898+
const rtpMap = new Map<number, VideoCodec>();
1899+
rtpTypes.forEach((rtp) => {
1900+
const codec = rtp.codec.toLowerCase();
1901+
if (isVideoCodec(codec)) {
1902+
rtpMap.set(rtp.payload, codec);
1903+
}
1904+
});
1905+
this.emit(EngineEvent.RTPVideoMapUpdate, rtpMap);
1906+
};
1907+
19091908
private dataChannelsInfo(): DataChannelInfo[] {
19101909
const infos: DataChannelInfo[] = [];
19111910
const getInfo = (dc: RTCDataChannel | undefined, target: SignalTarget) => {

0 commit comments

Comments
 (0)