Why is RTCUtils missing enumerateDevices?

I’m getting the following error in a custom client using lib-jitsi-meet hosted at meet.jit.si:

RTC.js:861 Uncaught (in promise) TypeError: v.a.enumerateDevices is not a function
    at Function.enumerateDevices (RTC.js:861)
    at Object.enumerateDevices (JitsiMediaDevices.js:94)
    at avutil.js:58

avutil.js is my code. I’m asking Jitsi to enumerate devices. But for some reason, when RTC.enumerateDevices is called, it internally calls RTCUtils.enumerateDevices and the function is not found:

In JitsiMediaDevices.js:

    /**
     * Executes callback with list of media devices connected.
     * @param {function} callback
     */
    enumerateDevices(callback) {
        RTC.enumerateDevices(callback);
    }

Subsequently, in RTC.js:

    /**
     * Allows to receive list of available cameras/microphones.
     * @param {function} callback Would receive array of devices as an
     *      argument.
     */
    static enumerateDevices(callback) {
        RTCUtils.enumerateDevices(callback);
    }

When I inspect the RTCUtils object in the Chrome debugger, RTCUtils looks like it is set up for events, but it doesn’t have the enumerateDevices function:

RTCUtils:
  addEventListener: addListener(e,t){return this.eventEmitter.addListener(e,t),()=> {…}
  eventEmitter: a {_events: {…}, _eventsCount: 3, _maxListeners: undefined}
  off: ƒ removeListener(e,t)
  on: addListener(e,t){return this.eventEmitter.addListener(e,t),()=> {…}
  removeEventListener: ƒ removeListener(e,t)
  __proto__: s
    constructor: class z
    getAudioOutputDevice: ƒ getAudioOutputDevice()
    getCurrentlyAvailableMediaDevices: ƒ getCurrentlyAvailableMediaDevices()
    getEventDataForActiveDevice: ƒ getEventDataForActiveDevice(e)
    getUserMediaWithConstraints: getUserMediaWithConstraints(e,t={}){const n=V(e,t);return I.info("Get media constraints",n),new Promise((t,i)=> {…}
    init: init(e={}){if("boolean"==typeof e.disableAEC&&(x=e.disableAEC,I.info("Disable AEC: "+x)),"boolean"==typeof e.disableNS&&(F=e.disableNS,I.info("Disable NS: "+F)),"boolean"==typeof e.disableAP&&(k=e.disableAP,I.info("Disable AP: "+k)),"boolean"==typeof e.disableAGC&&(j=e.disableAGC,I.info("Disable AGC: "+j)),"boolean"==typeof e.disableHPF&&(U=e.disableHPF,I.info("Disable HPF: "+U)),J=void 0,window.clearInterval(G),G=void 0,this.enumerateDevices=function(){if(navigator.mediaDevices&&navigator.mediaDevices.enumerateDevices)return e=> {…}
    isDesktopSharingEnabled: ƒ isDesktopSharingEnabled()
    isDeviceChangeAvailable: ƒ isDeviceChangeAvailable(e)
    isDeviceListAvailable: ƒ isDeviceListAvailable()
    newObtainAudioAndVideoPermissions: newObtainAudioAndVideoPermissions(e){I.info("Using the new gUM flow");const t=[],n=function(){if(!(-1!==(e.devices||[]).indexOf("desktop")))return Promise.resolve();const{desktopSharingSourceDevice:t,desktopSharingSources:n,desktopSharingFrameRate:i}=e;if(t){const n=J&&J.find(e=> {…}
    obtainAudioAndVideoPermissions: obtainAudioAndVideoPermissions(e={}){e.devices=e.devices||[...D],e.resolution=e.resolution||720;return e.devices.includes("desktop")&&!R.a.isSupported()?Promise.reject(new Error("Desktop sharing is not supported!")):this._getAudioAndVideoStreams(e).then(t=> {…}
    setAudioOutputDevice: setAudioOutputDevice(e){return     this.isDeviceChangeAvailable("output")?H.setSinkId(e).then(()=> {…}
    setSuspendVideo: setSuspendVideo(e,t){e.optional||(e.optional=[]),e.optional=e.optional.filter(e=> {…}
    stopMediaStream: stopMediaStream(e){if(!e)return;e.getTracks().forEach(e=> {…}
    _getAudioAndVideoStreams: _getAudioAndVideoStreams(e){const t=e.devices.includes("desktop");e.devices=e.devices.filter(e=> {…}
    _getMissingTracks: ƒ _getMissingTracks(e=[],t)
    _initPCConstraints: ƒ _initPCConstraints()
    _newGetDesktopMedia: _newGetDesktopMedia(e){return R.a.isSupported()&&S.a.supportsVideo()?new Promise((t,n)=> {…}
    _newGetUserMediaWithConstraints: _newGetUserMediaWithConstraints(e,t={}){return new Promise((n,i)=> {…}
    _parseDesktopSharingOptions: ƒ _parseDesktopSharingOptions(e)
    __proto__: Object

What’s going on here? It looks to me like some kind of async code is expecting some functions to be dynamically created on RTCUtils, but that isn’t happening. Can anyone offer insight?

Thanks!

Ahhhh, I think I understand a bit more about what’s going on:

It looks like enumerateDevices is, for some reason, initialized later. Still trying to figure out when it’s “safe” to call this function.

Problem solved:

I wasn’t calling JitsiMeetJS.init() in certain cases. It must be called before enumerateDevices().