-
Notifications
You must be signed in to change notification settings - Fork 32
Description
Context
Library: @stream-io/react-native-webrtc
Version: 125.4.4
Environment: React Native 0.79.x, Hermes, Android and iOS
The library maintains a videoTrackDimensionChangedEventQueue (a Map) to track width/height changes for local video tracks. MediaStreamTrack reads from and writes to this queue in two places:
_processVideoTrackDimensionChangedQueue
release
Problems
- Un-guarded access to a shared Map
In MediaStreamTrack._processVideoTrackDimensionChangedQueue:
// src/MediaStreamTrack.ts
_processVideoTrackDimensionChangedQueue(): void {
const eventData = videoTrackDimensionChangedEventQueue.get(this.id);
if (!eventData) {
return;
}
this._setVideoTrackDimensions(eventData.width, eventData.height);
videoTrackDimensionChangedEventQueue.delete(this.id);
}
In MediaStreamTrack.release:
release(): void {
if (this.remote) {
return;
}
removeListener(this);
WebRTCModule.mediaStreamTrackRelease(this.id);
if (this.kind === 'video') {
videoTrackDimensionChangedEventQueue.delete(this.id);
}
}
Both methods assume videoTrackDimensionChangedEventQueue is always defined and a valid Map.
On Hermes (and under certain timing conditions), this assumption is false.
- Race with MediaDevices.ensureListeners
The queue is created and populated in MediaDevices:
// src/MediaDevices.ts
export const videoTrackDimensionChangedEventQueue = new Map<string, VideoTrackDimension>();
let listenersReady = false;
function ensureListeners() {
if (listenersReady) {
return;
}
addListener('MediaDevices', 'videoTrackDimensionChanged', (ev: any) => {
if (ev.pcId !== -1) {
return;
}
const { trackId, width, height } = ev;
videoTrackDimensionChangedEventQueue.set(trackId, { width, height });
});
listenersReady = true;
}
ensureListeners() is called from getUserMedia / getDisplayMedia, but the order in which:
MediaStreamTrack instances are created, and
ensureListeners() initializes the queue
is not guaranteed, especially with Hermes and JIT differences.
This can lead to situations where:
- MediaStreamTrack is constructed and _processVideoTrackDimensionChangedQueue is called,
- or release() is called,
before videoTrackDimensionChangedEventQueue is properly initialized.
In that case, videoTrackDimensionChangedEventQueue is undefined (or not yet the expected Map), making .get() and .delete() throw:
- TypeError: Cannot read property 'get' of undefined
- TypeError: Cannot read property 'delete' of undefined
These errors are thrown inside the library and bubble up as runtime crashes when accepting or ending calls.
Observable behaviour
When a local video track is created (e.g., when joining a call), _processVideoTrackDimensionChangedQueue() may run early and hit .get on undefined.
When a call ends and tracks are cleaned up, release() can hit .delete on undefined.
In both cases, the TypeError comes from MediaStreamTrack accessing videoTrackDimensionChangedEventQueue without checking its existence.
Suggested fix (library side)
Add defensive guards in MediaStreamTrack before using the queue:
_processVideoTrackDimensionChangedQueue(): void {
if (
!videoTrackDimensionChangedEventQueue ||
typeof videoTrackDimensionChangedEventQueue.get !== 'function'
) {
return;
}
const eventData = videoTrackDimensionChangedEventQueue.get(this.id);
if (!eventData) {
return;
}
this._setVideoTrackDimensions(eventData.width, eventData.height);
videoTrackDimensionChangedEventQueue.delete(this.id);
}
release(): void {
if (this.remote) {
return;
}
removeListener(this);
WebRTCModule.mediaStreamTrackRelease(this.id);
if (
this.kind === 'video' &&
videoTrackDimensionChangedEventQueue &&
typeof videoTrackDimensionChangedEventQueue.delete === 'function'
) {
videoTrackDimensionChangedEventQueue.delete(this.id);
}
}
In short
The crash is caused by MediaStreamTrack assuming videoTrackDimensionChangedEventQueue is always a valid Map.
On Hermes / certain timing patterns, that queue can be uninitialized when _processVideoTrackDimensionChangedQueue and release run.
Simple null checks around the queue (plus optional lazy initialization) would make this code path resilient and prevent TypeError: ... 'get'/'delete' of undefined crashes during video calls.