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;
}