@fakes/media-devices
Version:
A interactive fake implementation of MediaDevices interface in the browser for testing
216 lines (215 loc) • 9.27 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MediaDevicesFake = void 0;
const Deferred_1 = require("./Deferred");
const LocalListenerPropertySync_1 = require("./LocalListenerPropertySync");
const MediaDeviceInfoFake_1 = require("./MediaDeviceInfoFake");
const MediaStreamFake_1 = require("./MediaStreamFake");
const MediaStreamTrackFake_1 = require("./MediaStreamTrackFake");
const descriptionMatching = (description) => (device) => device.deviceId === description.deviceId && device.groupId === description.groupId && device.kind === description.kind;
const fit2 = (actual, ideal) => (actual === ideal ? 0 : 1);
const fitExact = (actual, ideal) => (actual === ideal ? 0 : Infinity);
class ConstrainSet {
constructor(_context, requested) {
this._context = _context;
this._constraints = [];
if (typeof requested === 'boolean') {
return;
}
const deviceId = requested.deviceId;
if (deviceId === undefined) {
return;
}
if (typeof deviceId === 'string') {
this._constraints.push((device) => {
return fit2(device.deviceId, deviceId);
});
return;
}
if (typeof deviceId === 'object') {
if (Array.isArray(deviceId)) {
this._context.notImplemented.call('An array of deviceIds is not supported right now');
}
else {
const exactDeviceId = deviceId.exact;
if (exactDeviceId === undefined) {
return;
}
if (Array.isArray(exactDeviceId)) {
this._context.notImplemented.call('An array of exact deviceIds is not supported right now');
}
else {
this._constraints.push((device) => {
return fitExact(device.deviceId, exactDeviceId);
});
}
}
}
}
fitnessDistanceFor(device) {
return this._constraints.reduce((acc, curr) => acc + curr(device), 0);
}
}
class OverconstrainedError extends DOMException {
constructor(constraint, message) {
super(message, 'OverconstrainedError');
this.constraint = constraint;
}
}
const selectSettings = (context, mediaTrackConstraints, devices) => {
const constraintSet = new ConstrainSet(context, mediaTrackConstraints);
const viableDevices = devices
.map((device) => {
return {
device,
fitness: constraintSet.fitnessDistanceFor(device),
};
})
.filter((scoredDevice) => scoredDevice.fitness !== Infinity);
viableDevices.sort((a, b) => a.fitness - b.fitness);
if (viableDevices.length === 0) {
return undefined;
}
return viableDevices[0].device;
};
const trackConstraintsFrom = (constraints) => {
if (constraints.video) {
const mediaTrackConstraints = constraints.video;
const trackKind = 'video';
const deviceKind = 'videoinput';
return {
mediaTrackConstraints,
trackKind,
deviceKind,
};
}
if (constraints.audio) {
const mediaTrackConstraints = constraints.audio;
const trackKind = 'audio';
const deviceKind = 'audioinput';
return {
mediaTrackConstraints,
trackKind,
deviceKind,
};
}
throw new Error('with the current assumptions this should not happen');
};
const deepClone = (mediaTrackConstraints) => JSON.parse(JSON.stringify(mediaTrackConstraints));
const tryToOpenAStreamFor = (context, deferred, deviceKind, trackKind, mediaTrackConstraints, allDevices, openMediaTracks) => {
const devices = allDevices.filter((device) => device.kind === deviceKind);
if (devices.length === 0) {
deferred.reject(new DOMException('Requested device not found', 'NotFoundError'));
return;
}
const selectedDevice = selectSettings(context, mediaTrackConstraints, devices);
if (selectedDevice === undefined) {
//constraint name is hardcoded here, because right now we only check for deviceId
// Firefox also has a message and a stack
deferred.reject(new OverconstrainedError('deviceId', ''));
return;
}
const constraintObject = typeof mediaTrackConstraints === 'boolean' ? {} : deepClone(mediaTrackConstraints);
const mediaTrack = new MediaStreamTrackFake_1.MediaStreamTrackFake(context, MediaStreamTrackFake_1.initialMediaStreamTrackProperties(selectedDevice.label, trackKind, constraintObject));
openMediaTracks.track(selectedDevice, mediaTrack);
mediaTrack.onTerminated = (track) => openMediaTracks.remove(track);
const mediaTracks = [mediaTrack];
const mediaStream = new MediaStreamFake_1.MediaStreamFake(context, MediaStreamFake_1.mediaStreamId(), mediaTracks);
deferred.resolve(mediaStream);
};
// this looks interesting
// https://github.com/fippo/dynamic-getUserMedia/blob/master/content.js
class MediaDevicesFake extends EventTarget {
constructor(_context, _userConsentTracker, _openMediaTracks) {
super();
this._context = _context;
this._userConsentTracker = _userConsentTracker;
this._openMediaTracks = _openMediaTracks;
this._deviceDescriptions = [];
this._onDeviceChangeListener = new LocalListenerPropertySync_1.LocalListenerPropertySync(this, 'devicechange');
}
get devices() {
return this._deviceDescriptions
.map((description) => {
const { kind } = description;
const accessAllowed = this._userConsentTracker.accessAllowedFor(kind);
const deviceId = accessAllowed ? description.deviceId : '';
const label = accessAllowed ? description.label : '';
return {
...description,
deviceId,
label,
};
})
.map((description) => new MediaDeviceInfoFake_1.MediaDeviceInfoFake(this._context, description));
}
get ondevicechange() {
return this._onDeviceChangeListener.get();
}
set ondevicechange(listener) {
this._onDeviceChangeListener.set(listener);
}
enumerateDevices() {
try {
const devices = this.devices;
return Promise.resolve(devices);
}
catch (e) {
return Promise.reject(e);
}
}
getSupportedConstraints() {
this._context.notImplemented.call('MediaDevicesFake.getSupportedConstraints()');
}
// https://w3c.github.io/mediacapture-main/#methods-5
// https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
// https://blog.addpipe.com/common-getusermedia-errors/
getUserMedia(constraints) {
this._context.reporter.report(() => ['getUserMedia', 'constraints' + ':' + JSON.stringify(constraints, null, 2)]);
if (constraints === undefined ||
Object.keys(constraints).length === 0 ||
(constraints.video === false && constraints.audio === false)) {
return Promise.reject(new TypeError(`Failed to execute 'getUserMedia' on 'MediaDevices': At least one of audio and video must be requested`));
}
if (constraints.audio !== undefined && constraints.video !== undefined) {
this._context.notImplemented.call('at the moment there is no support to request audio and video at the same time');
}
const { mediaTrackConstraints, trackKind, deviceKind } = trackConstraintsFrom(constraints);
const deferred = new Deferred_1.Deferred();
this._userConsentTracker.requestPermissionFor({
deviceKind,
granted: () => {
tryToOpenAStreamFor(this._context, deferred, deviceKind, trackKind, mediaTrackConstraints, this.devices, this._openMediaTracks);
},
blocked: () => {
deferred.reject(new DOMException('Permission denied', 'NotAllowedError'));
},
});
return deferred.promise;
}
noDevicesAttached() {
const currentlyAttachedDevices = [...this._deviceDescriptions];
currentlyAttachedDevices.forEach((descriptor) => this.remove(descriptor));
}
attach(toAdd) {
if (this._deviceDescriptions.some(descriptionMatching(toAdd))) {
this._context.notImplemented.call(`device with this description already attached
${JSON.stringify(toAdd, null, 2)}`);
}
// make a defensive copy to stop manipulation after attaching the device
this._deviceDescriptions.push({ ...toAdd });
this.informDeviceChangeListener();
}
remove(toRemove) {
const index = this._deviceDescriptions.findIndex(descriptionMatching(toRemove));
if (index >= 0) {
this._deviceDescriptions.splice(index, 1);
this.informDeviceChangeListener();
}
this._openMediaTracks.allFor(toRemove).forEach((mediaStreamFake) => mediaStreamFake.deviceRemoved());
}
informDeviceChangeListener() {
this.dispatchEvent(new Event('devicechange'));
}
}
exports.MediaDevicesFake = MediaDevicesFake;