UNPKG

@huddle01/web-core

Version:

The Huddle01 Javascript SDK offers a comprehensive suite of methods and event listeners that allow for seamless real-time audio and video communication with minimal coding required.

429 lines (426 loc) 14.8 kB
import { deviceConstraints_default } from './chunk-FIM2WZLS.js'; import { isReactNative } from './chunk-WA3QABYS.js'; import { mainLogger } from './chunk-TOCFOGTC.js'; import { EnhancedEventEmitter } from './chunk-BW2DGP4D.js'; // src/DeviceHandler.ts var logger = mainLogger.createSubLogger("DeviceHandler"); var CustomMediaKindToSystemKind = { cam: "videoinput", mic: "audioinput", speaker: "audiooutput" }; var DeviceHandler = class extends EnhancedEventEmitter { SCREEN_DEFAULT_DEVICE = "monitor"; /** * User Selected Devices, If no device is selected, it will use the default device of the system * * is preffered device is null, it will use the default device of the system * * `NOTE: User has the ability to select a preferred device for each media kind` */ __preferredDevices = /* @__PURE__ */ new Map([ ["cam", null], ["mic", null], ["speaker", null] ]); /** * Map the media devices currently present in the system */ __mediaDevicesInfo = /* @__PURE__ */ new Map([ ["cam", []], ["mic", []], ["speaker", []] ]); /** * Get all the devices which are currently available in the system */ get devices() { return this.__mediaDevicesInfo; } get preferredDevices() { return this.__preferredDevices; } /** * Get all the devices which are currently available in the system, also updates the `__mediaDevicesInfo` record * * Can also query for a specific device kind `audioinput` | `videoinput` | `audiooutput` * * @param deviceKind `cam` | `mic` | `speaker` | `undefined` * @returns - MediaDeviceInfo[] | null * * `NOTE`: Ask for MediaDevice Permission to get the right result for that device else it will return `null` */ getMediaDevices = async (filterByDeviceKind) => { logger.debug("\u{1F4F9} Fetching Media Devices"); const devices = await navigator.mediaDevices.enumerateDevices(); if (!filterByDeviceKind || filterByDeviceKind === "device-change") { this.__setMediaDeviceInfo({ devices, update: "all" }); } if (filterByDeviceKind === "cam" || filterByDeviceKind === "mic") { this.__setMediaDeviceInfo({ devices, update: filterByDeviceKind }); } const mediaDevices = devices.filter((device) => { if (device.deviceId === "" || device.label === "") { return false; } if (filterByDeviceKind && filterByDeviceKind !== "device-change") { const systemDeviceKind = CustomMediaKindToSystemKind[filterByDeviceKind]; return device.kind === systemDeviceKind; } return true; }); return mediaDevices; }; /** * Get the device from the given facing type of device * * This function is used for only RN * * @param facing - facing of the device { 'environment' | 'front' | 'undefined' } * @param mediaDeviceKind - mediaDeviceKind for the device { 'audioinput' | 'videoinput' } * @returns - deviceId: string | null * * `NOTE`: Ask for MediaDevice Permission to get the right result for that device else it will return `null` */ getDeviceFromFacingMode = (facing, mediaDeviceKind) => { const allDevices = this.__mediaDevicesInfo.get(mediaDeviceKind); if (allDevices) { const d = allDevices.find((device) => device.facing === facing); if (d) { if (mediaDeviceKind === "cam") { return facing || d.deviceId; } return d.deviceId; } } return null; }; setPreferredDevice = (data) => { const { deviceId, deviceKind } = data; this.__preferredDevices.set(deviceKind, deviceId); this.emit("preferred-device-change", { deviceId, deviceKind }); }; /** * Fetches a stream of the screen of the device i.e the screen sharing stream * based on the selected choice from the pop up returns the audio and video stream * in one stream. * * `NOTE: This stream is not managed by the Huddle01 SDK, i.e. it will not be closed by the SDK` * @returns */ fetchScreen = async () => { const constraints = deviceConstraints_default.screen; try { const stream = await navigator.mediaDevices.getDisplayMedia(constraints); return { stream }; } catch (err) { logger.error(err); let error = { message: "Unknown Error", errorStack: err }; if (!isReactNative() && err instanceof DOMException) { error = { blocked: { byDeviceMissing: err.name === "NotFoundError", byDeviceInUse: err.name === "OverconstrainedError", byPermissions: err.name === "NotAllowedError" }, message: err.message }; } else { error = { message: "Screen Sharing Permission Denied", blocked: { byPermissions: true, byDeviceInUse: false, byDeviceMissing: false } }; } return { stream: null, error }; } }; /** * Fetch the stream from the device for the given media kind, if no preferred device is found it will throw an error. * by default the preferred device is the system default device * * `NOTE: If Preffered device is not found, it will use the system default device, if no default device is found it will throw an error` * `Set the preferred device using setPreferredDevice()` * */ fetchStream = async (data) => { const preferredDeviceId = this.__preferredDevices.get(data.mediaDeviceKind); logger.info("\u{1F4F9} Fetching Stream", { mediaDeviceKind: data.mediaDeviceKind, preferredDeviceId }); navigator.mediaDevices.ondevicechange = async () => { const newMediaDevices = await this.getMediaDevices("device-change"); for (const [deviceKind, deviceId] of this.__preferredDevices) { const device = newMediaDevices.find((d) => d.deviceId === deviceId); if (!device) { this.setPreferredDevice({ deviceId: null, deviceKind }); } } this.emit("device-change"); }; try { let fetchStreamFunc; if (typeof navigator === "object" && navigator.product === "ReactNative") { fetchStreamFunc = this.__fetchStreamFromDeviceForRN; } else { fetchStreamFunc = this.__fetchStreamFromDeviceForWeb; } const { stream, deviceId } = await fetchStreamFunc({ deviceId: preferredDeviceId ?? void 0, mediaKind: data.mediaDeviceKind === "mic" ? "mic" : "cam" }); const track = data.mediaDeviceKind === "mic" ? stream.getAudioTracks()[0] : stream.getVideoTracks()[0]; if (!this.__preferredDevices.get(data.mediaDeviceKind)) { this.setPreferredDevice({ deviceId, deviceKind: data.mediaDeviceKind }); } return { stream, track, deviceId }; } catch (err) { logger.error(err); let error = { message: "Unknown Error", errorStack: err }; if (!isReactNative() && err instanceof DOMException) { error = { blocked: { byDeviceMissing: err.name === "NotFoundError", byDeviceInUse: err.name === "OverconstrainedError", byPermissions: err.name === "NotAllowedError" }, message: err.message }; } else { error = { message: "Media Permission Denied", blocked: { byPermissions: true, byDeviceInUse: false, byDeviceMissing: false } }; } return { stream: null, track: null, deviceId: null, error }; } }; fetchStreamByGroupId = async (data) => { let constraints; if (data.mediaDeviceKind === "mic") { constraints = { audio: { groupId: data.groupId } }; } else { constraints = { video: { groupId: data.groupId } }; } const stream = await navigator.mediaDevices.getUserMedia(constraints); return stream; }; /** * Fetch the stream from the device for the React Native Based Application * * `This stream is not managed by the Huddle01 SDK, i.e. it will not be closed by the SDK * the user has to close it manually by calling {stream.getTracks().forEach(track => track.stop())}` * * NOTE: `using stopTrackOnClose = true` while producing will stop the track when producing is stopped * * @param data - { deviceId: "front" | "back" | "audio" | string; kind: "audioinput" | "videoinput" } * @returns - { stream: MediaStream, deviceId: string } */ __fetchStreamFromDeviceForRN = async (data) => { const constraints = deviceConstraints_default[data.mediaKind]; let facingMode; if (data.mediaKind === "cam") { facingMode = data.deviceId === "environment" ? "environment" : "front"; constraints.video = Object.assign({}, constraints.video, { facingMode }); } if (data.mediaKind === "mic") { constraints.audio = Object.assign({}, constraints.audio, { deviceId: data.deviceId }); } const stream = await navigator.mediaDevices.getUserMedia(constraints); const streamDeviceId = this.getDeviceFromFacingMode( facingMode, data.mediaKind === "mic" ? "mic" : "cam" ); const devices = await navigator.mediaDevices.enumerateDevices(); if (data.mediaKind === "cam" || data.mediaKind === "mic") { this.emit("permission-granted", { deviceKind: data.mediaKind }); this.__setMediaDeviceInfo({ devices, update: data.mediaKind }); } if (!streamDeviceId) { const tracks = stream.getTracks(); for (const track of tracks) { track.stop(); } throw new Error( "\u274C No DeviceId found for this stream, this is a bug in the SDK, please report it to the developers" ); } return { stream, deviceId: streamDeviceId }; }; /** * Fetch the stream from the device for the web * * `This stream is not managed by the Huddle01 SDK, i.e. it will not be closed by the SDK * the user has to close it manually by calling {stream.getTracks().forEach(track => track.stop())}` * * NOTE: `using stopTrackOnClose = true` while producing will stop the track when producing is stopped * * @param data - { deviceId: string; kind: 'audio' | 'video' } * @returns - { stream: MediaStream, deviceId: string } */ __fetchStreamFromDeviceForWeb = async (data) => { const constraints = Object.assign( {}, deviceConstraints_default[data.mediaKind] ); if (data.mediaKind === "cam" && data.deviceId) { constraints.video = Object.assign({}, constraints.video, { deviceId: data.deviceId }); } if (data.mediaKind === "mic" && data.deviceId) { const micConstraint = {}; const devices2 = this.__mediaDevicesInfo.get("mic"); if (data.deviceId === "default" && devices2 && devices2.length > 0) { const device = devices2.find((d) => d.deviceId === data.deviceId); if (device) { micConstraint.groupId = device.groupId; } } else if (data?.deviceId?.length > 0) { micConstraint.deviceId = { exact: data.deviceId }; } constraints.audio = Object.assign({}, constraints.audio, micConstraint); } const stream = await navigator.mediaDevices.getUserMedia(constraints); const streamDeviceId = stream.getTracks()[0].getSettings().deviceId; const devices = await navigator.mediaDevices.enumerateDevices(); if (data.mediaKind === "cam" || data.mediaKind === "mic") { this.emit("permission-granted", { deviceKind: data.mediaKind }); this.__setMediaDeviceInfo({ devices, update: data.mediaKind }); } if (!streamDeviceId) { const tracks = stream.getTracks(); for (const track of tracks) { track.stop(); } throw new Error( "\u274C No DeviceId found for this stream, this is a bug in the browser, please report it to the developers" ); } return { stream, deviceId: data?.deviceId ?? streamDeviceId }; }; /** * @description Get the media permission for the given type * @param data { type: 'video' | 'audio' } * @throws error { StreamPermissionsError } * @example await getMediaPermission({ type: 'video' }) */ getMediaPermission = async (data) => { const { mediaDeviceKind } = data; try { await this.getMediaDevices(mediaDeviceKind); this.emit("permission-granted", { deviceKind: mediaDeviceKind }); return { permission: "granted" }; } catch (err) { let error = { message: "Unknown Error", errorStack: err }; if (!isReactNative() && err instanceof DOMException) { error = { blocked: { byDeviceMissing: err.name === "NotFoundError", byDeviceInUse: err.name === "OverconstrainedError", byPermissions: err.name === "NotAllowedError" }, message: err.message }; } else { error = { message: "Media Permission Denied", blocked: { byPermissions: true, byDeviceInUse: false, byDeviceMissing: false } }; } this.emit("permission-denied", { deviceKind: mediaDeviceKind, error }); return { permission: "denied", error }; } }; stopStream = (stream) => { if (!stream) return; for (const track of stream.getTracks()) { track.stop(); } }; destroy = () => { this.__preferredDevices.clear(); this.__mediaDevicesInfo.clear(); logger.info("\u2705 Destroyed StreamHandler"); }; /** * Set the Media devices info based on the latest devices available in the system */ __setMediaDeviceInfo = (data) => { const { devices, update } = data; const camDevices = []; const micDevices = []; const speakerDevices = []; for (const device of devices) { if (device.label === "" || device.deviceId === "") continue; if (device.kind === "videoinput") camDevices.push(device); if (device.kind === "audioinput") micDevices.push(device); if (device.kind === "audiooutput") speakerDevices.push(device); } if (update === "all") { this.__mediaDevicesInfo.set("cam", camDevices); this.__mediaDevicesInfo.set("mic", micDevices); this.__mediaDevicesInfo.set("speaker", speakerDevices); } if (update === "cam") this.__mediaDevicesInfo.set("cam", camDevices); if (update === "mic") { this.__mediaDevicesInfo.set("mic", micDevices); this.__mediaDevicesInfo.set("speaker", speakerDevices); } }; }; var DeviceHandler_default = DeviceHandler; export { DeviceHandler_default };