private async chooseInputIntrinsicDevice()

in src/devicecontroller/DefaultDeviceController.ts [1415:1598]


  private async chooseInputIntrinsicDevice(
    kind: string,
    device: Device,
    fromAcquire: boolean,
    fromVideoTransformDevice: boolean = false
  ): Promise<void> {
    this.inputDeviceCount += 1;
    const callCount = this.inputDeviceCount;

    if (device === null && kind === 'video') {
      this.lastNoVideoInputDeviceCount = this.inputDeviceCount;
      const active = this.activeDevices[kind];
      if (active) {
        this.releaseActiveDevice(active);
        delete this.activeDevices[kind];
        this.watchForDeviceChangesIfNecessary();
      }
      return;
    }

    // N.B.,: the input device might already have augmented constraints supplied
    // by an `AudioTransformDevice`. `calculateMediaStreamConstraints` will respect
    // settings supplied by the device.
    const proposedConstraints = this.calculateMediaStreamConstraints(kind, device);

    // TODO: `matchesConstraints` should really return compatible/incompatible/exact --
    // `applyConstraints` can be used to reuse the active device while changing the
    // requested constraints.
    if (this.matchesDeviceSelection(kind, device, this.activeDevices[kind], proposedConstraints)) {
      this.logger.info(`reusing existing ${kind} input device`);
      return;
    }
    if (this.activeDevices[kind] && this.activeDevices[kind].stream) {
      /* istanbul ignore else */
      if (kind === 'audio') {
        this.releaseActiveDevice(this.activeDevices[kind]);
      } else if (kind === 'video') {
        this.stopTracksAndRemoveCallbacks('video');
        delete this.activeDevices[kind];
      }
    }

    const startTimeMs = Date.now();
    const newDevice: DeviceSelection = new DeviceSelection();
    try {
      this.logger.info(
        `requesting new ${kind} device with constraint ${JSON.stringify(proposedConstraints)}`
      );
      const stream = this.intrinsicDeviceAsMediaStream(device);
      if (kind === 'audio' && device === null) {
        newDevice.stream = DefaultDeviceController.createEmptyAudioDevice() as MediaStream;
        newDevice.constraints = null;
      } else if (stream) {
        this.logger.info(`using media stream ${stream.id} for ${kind} device`);
        newDevice.stream = stream;
        newDevice.constraints = proposedConstraints;
      } else {
        newDevice.stream = await navigator.mediaDevices.getUserMedia(proposedConstraints);
        newDevice.constraints = proposedConstraints;
        if (kind === 'video' && this.lastNoVideoInputDeviceCount > callCount) {
          this.logger.warn(
            `ignored to get video device for constraints ${JSON.stringify(
              proposedConstraints
            )} as no device was requested`
          );
          this.releaseMediaStream(newDevice.stream);
          return;
        }

        await this.handleDeviceChange();

        // We only monitor the first track, and use its device ID for observer notifications.
        const track = newDevice.stream.getTracks()[0];

        newDevice.endedCallback = (): void => {
          // Hard to test, but the safety check is worthwhile.
          /* istanbul ignore else */
          if (this.activeDevices[kind] && this.activeDevices[kind].stream === newDevice.stream) {
            this.logger.warn(
              `${kind} input device which was active is no longer available, resetting to null device`
            );
            this.handleDeviceStreamEnded(kind, this.getActiveDeviceId(kind));
            delete newDevice.endedCallback;
          }
        };
        track.addEventListener('ended', newDevice.endedCallback, { once: true });
      }

      if (device !== null) {
        const newDeviceId = this.getMediaTrackSettings(newDevice.stream)?.deviceId;
        newDevice.groupId = newDeviceId ? this.getGroupIdFromDeviceId(kind, newDeviceId) : '';
      } else {
        newDevice.groupId = '';
      }

      if (kind === 'audio') {
        // We only monitor the first track, and use its device ID for observer notifications.
        const track = newDevice.stream.getAudioTracks()[0];
        if (track) {
          const id = track.getSettings().deviceId || newDevice.stream;

          newDevice.trackMuteCallback = (): void => {
            this.mediaStreamMuteObserver(id, true);
          };
          newDevice.trackUnmuteCallback = (): void => {
            this.mediaStreamMuteObserver(id, false);
          };
          track.addEventListener('mute', newDevice.trackMuteCallback, { once: false });
          track.addEventListener('unmute', newDevice.trackUnmuteCallback, { once: false });
        }
      }
    } catch (error) {
      let errorMessage: string;
      if (error?.name && error.message) {
        errorMessage = `${error.name}: ${error.message}`;
      } else if (error?.name) {
        errorMessage = error.name;
      } else if (error?.message) {
        errorMessage = error.message;
      } else {
        errorMessage = 'UnknownError';
      }

      if (kind === 'audio') {
        this.boundAudioVideoController?.eventController?.publishEvent('audioInputFailed', {
          audioInputErrorMessage: errorMessage,
        });
      } else {
        this.boundAudioVideoController?.eventController?.publishEvent('videoInputFailed', {
          videoInputErrorMessage: errorMessage,
        });
      }

      this.logger.error(
        `failed to get ${kind} device for constraints ${JSON.stringify(
          proposedConstraints
        )}: ${errorMessage}`
      );

      // This is effectively `error instanceof OverconstrainedError` but works in Node.
      if (error && 'constraint' in error) {
        this.logger.error(`Over-constrained by constraint: ${error.constraint}`);
      }

      /*
       * If there is any error while acquiring the audio device, we fall back to null device.
       * Reason: If device selection fails (e.g. NotReadableError), the peer connection is left hanging
       * with no active audio track since we release the previously attached track.
       * If no audio packet has yet been sent to the server, the server will not emit the joined event.
       */
      if (kind === 'audio') {
        this.logger.info(`choosing null ${kind} device instead`);
        try {
          newDevice.stream = DefaultDeviceController.createEmptyAudioDevice() as MediaStream;
          newDevice.constraints = null;
          await this.handleNewInputDevice(kind, newDevice, fromAcquire);
        } catch (error) {
          this.logger.error(
            `failed to choose null ${kind} device. ${error.name}: ${error.message}`
          );
        }
      }

      this.handleGetUserMediaError(error, Date.now() - startTimeMs);
    } finally {
      this.watchForDeviceChangesIfNecessary();
    }

    this.logger.info(`got ${kind} device for constraints ${JSON.stringify(proposedConstraints)}`);
    await this.handleNewInputDevice(kind, newDevice, fromAcquire, fromVideoTransformDevice);

    // Notify the device mute state immediately after selection.
    if (kind === 'audio') {
      this.logger.debug('Notifying mute state after selection');
      for (const track of newDevice.stream.getAudioTracks()) {
        if (track.muted) {
          newDevice.trackMuteCallback();
        } else {
          newDevice.trackUnmuteCallback();
        }
      }
    }
    return;
  }