Skip to content

MediaStreamTrack crashes on Hermes: TypeError: Cannot read property 'get'/'delete' of undefined (dimension queue) #65

@RohanDhalpe05

Description

@RohanDhalpe05

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

  1. 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.

  1. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions