async chooseAudioInputDevice()

in src/devicecontroller/DefaultDeviceController.ts [306:419]


  async chooseAudioInputDevice(device: AudioInputDevice): Promise<void> {
    if (device === undefined) {
      this.logger.error('Audio input device cannot be undefined');
      return;
    }

    /*
     * This block of code is a workaround for a Chromium bug:
     * https://bugs.chromium.org/p/chromium/issues/detail?id=1173656
     *
     * In short: if we are about to select an audio device with a transform, which we assume for
     * safety's sake uses AudioWorklet, we recreate the audio context and the nodes that
     * are linked to it.
     *
     * This causes Chrome to rejig its buffers and the second context works correctly.
     *
     * This is theoretically worse for performance, but in practice it is fine.
     *
     * This is not safe in the general case: an application that already
     * retrieved the audio context in order to build an audio graph for some other purpose
     * will fail at this point as we pull the context out from under it.
     *
     * An application that always uses the supplied context in an
     * `AudioTransformDevice.createAudioNode` call should work correctly.
     *
     * If you are confident that your application does not use AudioWorklet, does not run in
     * an un-fixed Chromium version, or will never be used with sample-rate-switching Bluetooth
     * devices, you can disable this workaround by suppling a custom {@link ExtendedBrowserBehavior}
     * when you create your device controller.
     *
     * We can't tell in advance whether we need to give the device a different audio context,
     * because checking whether the resulting node is an AudioWorkletNode needs it to have been
     * created first.
     */

    // Ideally we would only do this work if we knew the device was going to change.

    // By definition, this is only needed for Web Audio.
    let recreateAudioContext = this.useWebAudio;

    if (!this.useWebAudio) {
      this.logger.debug('Not using Web Audio. No need to recreate audio context.');
    }

    // We have a suspended audio context. There's nothing we can do, and there's
    // certainly no point in recreating it. Choosing the transform device will try to resume.
    if (DefaultDeviceController.audioContext?.state === 'suspended') {
      recreateAudioContext = false;
    }

    // Only Chrome needs this fix.
    if (recreateAudioContext && !this.browserBehavior.requiresContextRecreationForAudioWorklet()) {
      this.logger.debug('Browser does not require audio context recreation hack.');
      recreateAudioContext = false;
    }

    // Only need to do this if either device has an audio worklet.
    if (recreateAudioContext && !this.transform && isAudioTransformDevice(device)) {
      this.logger.debug('Neither device is a transform. No need to recreate audio context.');
      recreateAudioContext = false;
    }

    if (recreateAudioContext) {
      this.logger.info('Recreating audio context when selecting new device.');

      /* istanbul ignore else */
      if (this.transform) {
        /* istanbul ignore else */
        if (this.transform.nodes) {
          this.transform.nodes.end.disconnect();
          this.transform.nodes = undefined;
        }
        this.transform = undefined;
      }

      /* istanbul ignore else */
      if (this.audioInputSourceNode) {
        this.audioInputSourceNode.disconnect();
        this.audioInputSourceNode = undefined;
      }

      /* istanbul ignore else */
      if (this.audioInputDestinationNode) {
        this.audioInputDestinationNode.disconnect();
        this.audioInputDestinationNode = undefined;
      }

      DefaultDeviceController.closeAudioContext();
    }

    if (isAudioTransformDevice(device)) {
      // N.B., do not JSON.stringify here — for some kinds of devices this
      // will cause a cyclic object reference error.
      this.logger.info(`Choosing transform input device ${device}`);

      await this.chooseAudioTransformInputDevice(device);
    } else {
      this.logger.info(`Choosing intrinsic audio input device ${device}`);
      this.removeTransform();
      await this.chooseInputIntrinsicDevice('audio', device, false);
      this.trace('chooseAudioInputDevice', device, `success`);
    }

    // Only recreate if there's a peer connection, otherwise `restartLocalAudio` will throw.
    // This hack is off by default, so tests don't cover it. We can remove this skip soon.
    /* istanbul ignore next */
    if (recreateAudioContext && this.boundAudioVideoController?.rtcPeerConnection) {
      this.boundAudioVideoController.restartLocalAudio(() => {
        this.logger.info('Local audio restarted.');
      });
    }

    this.pushAudioMeetingStateForPermissions(device);
  }