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