in src/devicecontroller/DefaultDeviceController.ts [1083:1193]
private async chooseInputIntrinsicDevice(
kind: 'audio' | 'video',
device: Device | null
): Promise<void> {
// N.B.,: the input device might already have augmented constraints supplied
// by an `AudioTransformDevice`. `getMediaStreamConstraints` will respect
// settings supplied by the device.
const proposedConstraints = this.getMediaStreamConstraints(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) {
this.stopTracksAndRemoveCallbacks(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;
}
await this.handleNewInputDevice(kind, newDevice);
} catch (error) {
const errorMessage = this.getErrorMessage(error);
if (kind === 'audio') {
this.eventController?.publishEvent('audioInputFailed', {
audioInputErrorMessage: errorMessage,
});
} else {
this.eventController?.publishEvent('videoInputFailed', {
videoInputErrorMessage: errorMessage,
});
}
this.logger.error(
`failed to get ${kind} device for constraints ${JSON.stringify(
proposedConstraints
)}: ${errorMessage}`
);
let hasError = true;
// This is effectively `error instanceof OverconstrainedError` but works in Node.
if (error && 'constraint' in error) {
this.logger.error(`Over-constrained by constraint: ${error.constraint}`);
// Try to reduce the constraints if over-constraints
if (this.useMediaConstraintsFallback) {
const fallbackConstraints = this.getMediaStreamConstraints(kind, device, true);
const fallbackConstraintsJSON = JSON.stringify(fallbackConstraints);
if (fallbackConstraintsJSON !== JSON.stringify(proposedConstraints)) {
this.logger.info(
`retry requesting new ${kind} device with minimal constraint ${fallbackConstraintsJSON}`
);
try {
newDevice.stream = await navigator.mediaDevices.getUserMedia(fallbackConstraints);
newDevice.constraints = fallbackConstraints;
await this.handleNewInputDevice(kind, newDevice);
hasError = false;
} catch (e) {
this.logger.error(
`failed to get ${kind} device for constraints ${fallbackConstraintsJSON}: ${this.getErrorMessage(
e
)}`
);
}
}
}
}
if (hasError) {
/*
* 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();
newDevice.constraints = null;
await this.handleNewInputDevice(kind, newDevice);
} catch (error) {
this.logger.error(
`failed to choose null ${kind} device. ${error.name}: ${error.message}`
);
}
}
this.handleGetUserMediaError(error, Date.now() - startTimeMs);
}
} finally {
this.watchForDeviceChangesIfNecessary();
}
}