UNPKG

@fakes/media-devices

Version:

A interactive fake implementation of MediaDevices interface in the browser for testing

216 lines (215 loc) 9.27 kB
"use strict"; 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;