d72ce1a24c
- Fix almost the entire video-provider component typyings. Leaving some errors because the component depends on a lot of code which is not in typescript yet. Any attempt to fix them may cause problems.
586 lines
19 KiB
TypeScript
Executable File
586 lines
19 KiB
TypeScript
Executable File
import { Session } from 'meteor/session';
|
|
import Settings from '/imports/ui/services/settings';
|
|
import Auth from '/imports/ui/services/auth';
|
|
import Meetings from '/imports/api/meetings';
|
|
import Users from '/imports/api/users';
|
|
import UserListService from '/imports/ui/components/user-list/service';
|
|
import { meetingIsBreakout } from '/imports/ui/components/app/service';
|
|
import { notify } from '/imports/ui/services/notification';
|
|
import deviceInfo from '/imports/utils/deviceInfo';
|
|
import browserInfo from '/imports/utils/browserInfo';
|
|
import getFromUserSettings from '/imports/ui/services/users-settings';
|
|
import VideoPreviewService from '/imports/ui/components/video-preview/service';
|
|
import Storage from '/imports/ui/services/storage/session';
|
|
import BBBStorage from '/imports/ui/services/storage';
|
|
import logger from '/imports/startup/client/logger';
|
|
import { debounce } from '/imports/utils/debounce';
|
|
import getFromMeetingSettings from '/imports/ui/services/meeting-settings';
|
|
import {
|
|
setVideoState,
|
|
setConnectingStream,
|
|
getVideoState,
|
|
Stream,
|
|
} from './state';
|
|
import WebRtcPeer from '/imports/ui/services/webrtc-base/peer';
|
|
import { Constraints2, DesktopPageSizes, MobilePageSizes } from '/imports/ui/Types/meetingClientSettings';
|
|
|
|
const CAMERA_PROFILES = window.meetingClientSettings.public.kurento.cameraProfiles;
|
|
const MULTIPLE_CAMERAS = window.meetingClientSettings.public.app.enableMultipleCameras;
|
|
|
|
const SFU_URL = window.meetingClientSettings.public.kurento.wsUrl;
|
|
const ROLE_MODERATOR = window.meetingClientSettings.public.user.role_moderator;
|
|
const ROLE_VIEWER = window.meetingClientSettings.public.user.role_viewer;
|
|
const MIRROR_WEBCAM = window.meetingClientSettings.public.app.mirrorOwnWebcam;
|
|
const PIN_WEBCAM = window.meetingClientSettings.public.kurento.enableVideoPin;
|
|
const {
|
|
thresholds: CAMERA_QUALITY_THRESHOLDS = [],
|
|
applyConstraints: CAMERA_QUALITY_THR_CONSTRAINTS = false,
|
|
debounceTime: CAMERA_QUALITY_THR_DEBOUNCE = 2500,
|
|
} = window.meetingClientSettings.public.kurento.cameraQualityThresholds;
|
|
const {
|
|
paginationToggleEnabled: PAGINATION_TOGGLE_ENABLED,
|
|
pageChangeDebounceTime: PAGE_CHANGE_DEBOUNCE_TIME,
|
|
desktopPageSizes: DESKTOP_PAGE_SIZES,
|
|
mobilePageSizes: MOBILE_PAGE_SIZES,
|
|
} = window.meetingClientSettings.public.kurento.pagination;
|
|
const PAGINATION_THRESHOLDS_CONF = window.meetingClientSettings.public.kurento.paginationThresholds;
|
|
const PAGINATION_THRESHOLDS = PAGINATION_THRESHOLDS_CONF.thresholds.sort((t1, t2) => t1.users - t2.users);
|
|
const PAGINATION_THRESHOLDS_ENABLED = PAGINATION_THRESHOLDS_CONF.enabled;
|
|
const DEFAULT_VIDEO_MEDIA_SERVER = window.meetingClientSettings.public.kurento.videoMediaServer;
|
|
|
|
const TOKEN = '_';
|
|
|
|
class VideoService {
|
|
public isMobile: boolean;
|
|
|
|
public webRtcPeersRef: Record<string, WebRtcPeer>;
|
|
|
|
private userParameterProfile: string | null;
|
|
|
|
private isSafari: boolean;
|
|
|
|
private numberOfDevices: number;
|
|
|
|
private record: boolean | null;
|
|
|
|
private hackRecordViewer: boolean | null;
|
|
|
|
private deviceId: string | null = null;
|
|
|
|
constructor() {
|
|
this.userParameterProfile = null;
|
|
this.isMobile = deviceInfo.isMobile;
|
|
this.isSafari = browserInfo.isSafari;
|
|
this.numberOfDevices = 0;
|
|
this.record = null;
|
|
this.hackRecordViewer = null;
|
|
|
|
if (navigator.mediaDevices) {
|
|
this.updateNumberOfDevices = this.updateNumberOfDevices.bind(this);
|
|
if (!this.isSafari) {
|
|
navigator.mediaDevices.ondevicechange = () => this.updateNumberOfDevices();
|
|
}
|
|
this.updateNumberOfDevices();
|
|
}
|
|
|
|
this.webRtcPeersRef = {};
|
|
}
|
|
|
|
static fetchNumberOfDevices(devices: MediaDeviceInfo[]) {
|
|
const deviceIds: string[] = [];
|
|
devices.forEach((d) => {
|
|
const validDeviceId = d.deviceId !== '' && !deviceIds.includes(d.deviceId);
|
|
if (d.kind === 'videoinput' && validDeviceId) {
|
|
deviceIds.push(d.deviceId);
|
|
}
|
|
});
|
|
|
|
return deviceIds.length;
|
|
}
|
|
|
|
updateNumberOfDevices(devices: MediaDeviceInfo[] | null = null) {
|
|
if (devices) {
|
|
this.numberOfDevices = VideoService.fetchNumberOfDevices(devices);
|
|
} else {
|
|
navigator.mediaDevices.enumerateDevices().then((devices) => {
|
|
this.numberOfDevices = VideoService.fetchNumberOfDevices(devices);
|
|
});
|
|
}
|
|
}
|
|
|
|
static disableCam() {
|
|
const m = Meetings.findOne({ meetingId: Auth.meetingID },
|
|
{ fields: { 'lockSettings.disableCam': 1 } });
|
|
return m.lockSettings ? m.lockSettings.disableCam : false;
|
|
}
|
|
|
|
static isUserLocked() {
|
|
return !!Users.findOne({
|
|
userId: Auth.userID,
|
|
locked: true,
|
|
role: { $ne: ROLE_MODERATOR },
|
|
}, { fields: {} }) && VideoService.disableCam();
|
|
}
|
|
|
|
joinVideo(deviceId: string) {
|
|
this.deviceId = deviceId;
|
|
Storage.setItem('isFirstJoin', false);
|
|
if (!VideoService.isUserLocked()) {
|
|
const streamName = VideoService.buildStreamName(Auth.userID ?? '', deviceId);
|
|
const stream = {
|
|
stream: streamName,
|
|
userId: Auth.userID ?? '',
|
|
name: Auth.fullname ?? '',
|
|
type: 'connecting' as const,
|
|
};
|
|
setConnectingStream(stream);
|
|
setVideoState((curr) => ({
|
|
...curr,
|
|
isConnecting: true,
|
|
}));
|
|
}
|
|
}
|
|
|
|
joinedVideo() {
|
|
setVideoState((curr) => ({
|
|
...curr,
|
|
isConnected: true,
|
|
isConnecting: false,
|
|
}));
|
|
this.stopConnectingStream();
|
|
}
|
|
|
|
static storeDeviceIds(streams: Stream[]) {
|
|
const deviceIds: string[] = [];
|
|
streams.filter((s) => s.userId === Auth.userID).forEach((s) => {
|
|
deviceIds.push(s.deviceId);
|
|
});
|
|
Session.set('deviceIds', deviceIds.join());
|
|
}
|
|
|
|
exitedVideo() {
|
|
this.stopConnectingStream();
|
|
setVideoState((curr) => ({
|
|
...curr,
|
|
isConnected: false,
|
|
}));
|
|
}
|
|
|
|
static getAuthenticatedURL() {
|
|
return Auth.authenticateURL(SFU_URL);
|
|
}
|
|
|
|
shouldRenderPaginationToggle() {
|
|
return PAGINATION_TOGGLE_ENABLED && (this.getMyPageSize() ?? 0) > 0;
|
|
}
|
|
|
|
static isPaginationEnabled() {
|
|
// @ts-expect-error -> Untyped object.
|
|
return Settings.application.paginationEnabled;
|
|
}
|
|
|
|
static setCurrentVideoPageIndex(newVideoPageIndex: number) {
|
|
const { currentVideoPageIndex } = getVideoState();
|
|
if (currentVideoPageIndex !== newVideoPageIndex) {
|
|
setVideoState((curr) => ({
|
|
...curr,
|
|
currentVideoPageIndex: newVideoPageIndex,
|
|
}));
|
|
}
|
|
}
|
|
|
|
static getCurrentVideoPageIndex() {
|
|
const { currentVideoPageIndex } = getVideoState();
|
|
return currentVideoPageIndex;
|
|
}
|
|
|
|
static calculateNextPage() {
|
|
const { numberOfPages, currentVideoPageIndex } = getVideoState();
|
|
if (numberOfPages === 0) {
|
|
return 0;
|
|
}
|
|
|
|
return (((currentVideoPageIndex + 1) % numberOfPages) + numberOfPages) % numberOfPages;
|
|
}
|
|
|
|
static calculatePreviousPage() {
|
|
const { numberOfPages, currentVideoPageIndex } = getVideoState();
|
|
if (numberOfPages === 0) {
|
|
return 0;
|
|
}
|
|
|
|
return (((currentVideoPageIndex - 1) % numberOfPages) + numberOfPages) % numberOfPages;
|
|
}
|
|
|
|
static getNextVideoPage() {
|
|
const nextPage = VideoService.calculateNextPage();
|
|
VideoService.setCurrentVideoPageIndex(nextPage);
|
|
}
|
|
|
|
static getPreviousVideoPage() {
|
|
const previousPage = VideoService.calculatePreviousPage();
|
|
VideoService.setCurrentVideoPageIndex(previousPage);
|
|
}
|
|
|
|
getPageSizeDictionary() {
|
|
if (!PAGINATION_THRESHOLDS_ENABLED || PAGINATION_THRESHOLDS.length <= 0) {
|
|
return !this.isMobile ? DESKTOP_PAGE_SIZES : MOBILE_PAGE_SIZES;
|
|
}
|
|
|
|
let targetThreshold;
|
|
const userCount = UserListService.getUserCount();
|
|
const processThreshold = (
|
|
threshold: {
|
|
desktopPageSizes?: DesktopPageSizes,
|
|
mobilePageSizes?: MobilePageSizes,
|
|
} = {
|
|
desktopPageSizes: DESKTOP_PAGE_SIZES,
|
|
mobilePageSizes: MOBILE_PAGE_SIZES,
|
|
},
|
|
) => {
|
|
if (!this.isMobile) {
|
|
return threshold.desktopPageSizes || DESKTOP_PAGE_SIZES;
|
|
}
|
|
return threshold.mobilePageSizes || MOBILE_PAGE_SIZES;
|
|
};
|
|
|
|
if (userCount < PAGINATION_THRESHOLDS[0].users) return processThreshold();
|
|
|
|
for (let mapIndex = PAGINATION_THRESHOLDS.length - 1; mapIndex >= 0; mapIndex -= 1) {
|
|
targetThreshold = PAGINATION_THRESHOLDS[mapIndex];
|
|
if (targetThreshold.users <= userCount) {
|
|
return processThreshold(targetThreshold);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
getMyPageSize() {
|
|
let size;
|
|
const myRole = VideoService.getMyRole();
|
|
const pageSizes = this.getPageSizeDictionary();
|
|
switch (myRole) {
|
|
case ROLE_MODERATOR:
|
|
size = pageSizes?.moderator;
|
|
break;
|
|
case ROLE_VIEWER:
|
|
default:
|
|
size = pageSizes?.viewer;
|
|
}
|
|
|
|
return size;
|
|
}
|
|
|
|
static isGridEnabled() {
|
|
return Session.get('isGridEnabled');
|
|
}
|
|
|
|
stopConnectingStream() {
|
|
this.deviceId = null;
|
|
setConnectingStream(null);
|
|
}
|
|
|
|
static buildStreamName(userId: string, deviceId: string) {
|
|
return `${userId}${TOKEN}${deviceId}`;
|
|
}
|
|
|
|
static getMediaServerAdapter() {
|
|
return getFromMeetingSettings('media-server-video', DEFAULT_VIDEO_MEDIA_SERVER);
|
|
}
|
|
|
|
static getMyRole() {
|
|
return Users.findOne({ userId: Auth.userID },
|
|
{ fields: { role: 1 } })?.role;
|
|
}
|
|
|
|
getRecord() {
|
|
if (this.record === null) {
|
|
this.record = getFromUserSettings('bbb_record_video', true);
|
|
}
|
|
|
|
if (this.hackRecordViewer === null) {
|
|
const value = getFromMeetingSettings('hack-record-viewer-video', null);
|
|
this.hackRecordViewer = value ? value.toLowerCase() === 'true' : true;
|
|
}
|
|
|
|
const hackRecord = VideoService.getMyRole() === ROLE_MODERATOR || this.hackRecordViewer;
|
|
|
|
return this.record && hackRecord;
|
|
}
|
|
|
|
static getInfo() {
|
|
const m = Meetings.findOne({ meetingId: Auth.meetingID },
|
|
{ fields: { 'voiceSettings.voiceConf': 1 } });
|
|
const voiceBridge = m.voiceSettings ? m.voiceSettings.voiceConf : null;
|
|
return {
|
|
userId: Auth.userID,
|
|
userName: Auth.fullname,
|
|
meetingId: Auth.meetingID,
|
|
sessionToken: Auth.sessionToken,
|
|
voiceBridge,
|
|
};
|
|
}
|
|
|
|
static mirrorOwnWebcam(userId: string | null = null) {
|
|
const isOwnWebcam = userId ? Auth.userID === userId : true;
|
|
const isEnabledMirroring = getFromUserSettings('bbb_mirror_own_webcam', MIRROR_WEBCAM);
|
|
return isOwnWebcam && isEnabledMirroring;
|
|
}
|
|
|
|
static isPinEnabled() {
|
|
return PIN_WEBCAM;
|
|
}
|
|
|
|
static isVideoPinEnabledForCurrentUser(isModerator: boolean) {
|
|
const isBreakout = meetingIsBreakout();
|
|
const isPinEnabled = VideoService.isPinEnabled();
|
|
|
|
return !!(isModerator
|
|
&& isPinEnabled
|
|
&& !isBreakout);
|
|
}
|
|
|
|
static getMyStreamId(deviceId: string, streams: Stream[]) {
|
|
const videoStream = streams.find((vs) => vs.userId === Auth.userID && vs.deviceId === deviceId);
|
|
return videoStream ? videoStream.stream : null;
|
|
}
|
|
|
|
static isLocalStream(cameraId: string) {
|
|
return !!Auth.userID && cameraId?.startsWith(Auth.userID);
|
|
}
|
|
|
|
static getCameraProfile() {
|
|
const profileId = BBBStorage.getItem('WebcamProfileId') || '';
|
|
const cameraProfile = CAMERA_PROFILES.find((profile) => profile.id === profileId)
|
|
|| CAMERA_PROFILES.find((profile) => profile.default)
|
|
|| CAMERA_PROFILES[0];
|
|
const deviceId = BBBStorage.getItem('WebcamDeviceId');
|
|
if (deviceId) {
|
|
// @ts-expect-error -> Untyped object.
|
|
cameraProfile.constraints = cameraProfile.constraints || {};
|
|
// @ts-expect-error -> Untyped object.
|
|
cameraProfile.constraints.deviceId = { exact: deviceId };
|
|
}
|
|
|
|
return cameraProfile;
|
|
}
|
|
|
|
static addCandidateToPeer(peer: WebRtcPeer, candidate: Record<string | symbol, unknown>, cameraId: string) {
|
|
peer.addIceCandidate(candidate).catch((error: Error) => {
|
|
if (error) {
|
|
logger.error({
|
|
logCode: 'video_provider_addicecandidate_error',
|
|
extraInfo: {
|
|
cameraId,
|
|
error,
|
|
},
|
|
}, `Adding ICE candidate failed for ${cameraId} due to ${error.message}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
static processInboundIceQueue(peer: WebRtcPeer, cameraId: string) {
|
|
// @ts-expect-error -> Untyped object.
|
|
while (peer.inboundIceQueue.length) {
|
|
// @ts-expect-error -> Untyped object.
|
|
const candidate = peer.inboundIceQueue.shift();
|
|
VideoService.addCandidateToPeer(peer, candidate, cameraId);
|
|
}
|
|
}
|
|
|
|
static getRole(isLocal: boolean) {
|
|
return isLocal ? 'share' : 'viewer';
|
|
}
|
|
|
|
getUserParameterProfile() {
|
|
if (this.userParameterProfile === null) {
|
|
this.userParameterProfile = getFromUserSettings(
|
|
'bbb_preferred_camera_profile',
|
|
(CAMERA_PROFILES.find((i) => i.default) || {}).id || null,
|
|
);
|
|
}
|
|
|
|
return this.userParameterProfile;
|
|
}
|
|
|
|
isMultipleCamerasEnabled() {
|
|
return MULTIPLE_CAMERAS
|
|
&& !VideoPreviewService.getSkipVideoPreview()
|
|
&& !this.isMobile
|
|
&& !this.isSafari
|
|
&& this.numberOfDevices > 1;
|
|
}
|
|
|
|
static isProfileBetter(newProfileId: string, originalProfileId: string) {
|
|
return CAMERA_PROFILES.findIndex(({ id }) => id === newProfileId)
|
|
> CAMERA_PROFILES.findIndex(({ id }) => id === originalProfileId);
|
|
}
|
|
|
|
static applyBitrate(peer: WebRtcPeer, bitrate: number) {
|
|
const { peerConnection } = peer;
|
|
if ('RTCRtpSender' in window
|
|
&& 'setParameters' in window.RTCRtpSender.prototype
|
|
&& 'getParameters' in window.RTCRtpSender.prototype) {
|
|
peerConnection.getSenders().forEach((sender: RTCRtpSender) => {
|
|
const { track } = sender;
|
|
if (track && track.kind === 'video') {
|
|
const parameters = sender.getParameters();
|
|
const normalizedBitrate = bitrate * 1000;
|
|
|
|
if (parameters.encodings == null || parameters.encodings.length === 0) {
|
|
parameters.encodings = [{}];
|
|
}
|
|
|
|
if (parameters.encodings[0].maxBitrate !== normalizedBitrate) {
|
|
parameters.encodings[0].maxBitrate = normalizedBitrate;
|
|
sender.setParameters(parameters)
|
|
.then(() => {
|
|
logger.info({
|
|
logCode: 'video_provider_bitratechange',
|
|
extraInfo: { bitrate },
|
|
}, `Bitrate changed: ${bitrate}`);
|
|
})
|
|
.catch((error) => {
|
|
logger.warn({
|
|
logCode: 'video_provider_bitratechange_failed',
|
|
extraInfo: { bitrate, errorMessage: error.message, errorCode: error.code },
|
|
}, 'Bitrate change failed.');
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
static reapplyResolutionIfNeeded(track: MediaStreamTrack, constraints?: Constraints2) {
|
|
if (typeof track.getSettings !== 'function') {
|
|
return constraints;
|
|
}
|
|
|
|
const trackSettings = track.getSettings();
|
|
|
|
if (trackSettings.width && trackSettings.height) {
|
|
return {
|
|
...constraints,
|
|
width: trackSettings.width,
|
|
height: trackSettings.height,
|
|
};
|
|
}
|
|
|
|
return constraints;
|
|
}
|
|
|
|
static applyCameraProfile(peer: WebRtcPeer, profileId: string) {
|
|
const profile = CAMERA_PROFILES.find((targetProfile) => targetProfile.id === profileId);
|
|
|
|
if (!profile
|
|
|| peer == null
|
|
|| peer.peerConnection == null
|
|
// @ts-expect-error -> Untyped object.
|
|
|| peer.currentProfileId === profileId
|
|
// @ts-expect-error -> Untyped object.
|
|
|| VideoService.isProfileBetter(profileId, peer.originalProfileId)) {
|
|
return;
|
|
}
|
|
|
|
const { bitrate, constraints } = profile;
|
|
|
|
if (bitrate) VideoService.applyBitrate(peer, bitrate);
|
|
|
|
if (CAMERA_QUALITY_THR_CONSTRAINTS
|
|
&& constraints
|
|
&& typeof constraints === 'object'
|
|
) {
|
|
peer.peerConnection.getSenders().forEach((sender: RTCRtpSender) => {
|
|
const { track } = sender;
|
|
if (track && track.kind === 'video' && typeof track.applyConstraints === 'function') {
|
|
const normalizedVideoConstraints = VideoService.reapplyResolutionIfNeeded(track, constraints);
|
|
track.applyConstraints(normalizedVideoConstraints)
|
|
.catch((error) => {
|
|
logger.warn({
|
|
logCode: 'video_provider_constraintchange_failed',
|
|
extraInfo: { errorName: error.name, errorCode: error.code },
|
|
}, 'Error applying camera profile');
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
logger.info({
|
|
logCode: 'video_provider_profile_applied',
|
|
extraInfo: { profileId },
|
|
}, `New camera profile applied: ${profileId}`);
|
|
|
|
// @ts-expect-error -> Untyped object.
|
|
// eslint-disable-next-line no-param-reassign
|
|
peer.currentProfileId = profileId;
|
|
}
|
|
|
|
static getThreshold(numberOfPublishers: number) {
|
|
let targetThreshold = { threshold: 0, profile: 'original' };
|
|
let finalThreshold = { threshold: 0, profile: 'original' };
|
|
|
|
for (let mapIndex = 0; mapIndex < CAMERA_QUALITY_THRESHOLDS.length; mapIndex += 1) {
|
|
targetThreshold = CAMERA_QUALITY_THRESHOLDS[mapIndex];
|
|
if (targetThreshold.threshold <= numberOfPublishers) {
|
|
finalThreshold = targetThreshold;
|
|
}
|
|
}
|
|
|
|
return finalThreshold;
|
|
}
|
|
|
|
getPreloadedStream() {
|
|
if (this.deviceId == null) return null;
|
|
return VideoPreviewService.getStream(this.deviceId);
|
|
}
|
|
|
|
updatePeerDictionaryReference(newRef: Record<string, WebRtcPeer>) {
|
|
this.webRtcPeersRef = newRef;
|
|
}
|
|
}
|
|
|
|
const videoService = new VideoService();
|
|
|
|
export default {
|
|
addCandidateToPeer: VideoService.addCandidateToPeer,
|
|
getInfo: VideoService.getInfo,
|
|
getMyStreamId: VideoService.getMyStreamId,
|
|
getAuthenticatedURL: VideoService.getAuthenticatedURL,
|
|
getRole: VideoService.getRole,
|
|
getMediaServerAdapter: VideoService.getMediaServerAdapter,
|
|
getCameraProfile: VideoService.getCameraProfile,
|
|
getThreshold: VideoService.getThreshold,
|
|
getPreviousVideoPage: VideoService.getPreviousVideoPage,
|
|
getNextVideoPage: VideoService.getNextVideoPage,
|
|
getCurrentVideoPageIndex: VideoService.getCurrentVideoPageIndex,
|
|
isVideoPinEnabledForCurrentUser: VideoService.isVideoPinEnabledForCurrentUser,
|
|
isLocalStream: VideoService.isLocalStream,
|
|
isPaginationEnabled: VideoService.isPaginationEnabled,
|
|
mirrorOwnWebcam: VideoService.mirrorOwnWebcam,
|
|
processInboundIceQueue: VideoService.processInboundIceQueue,
|
|
storeDeviceIds: VideoService.storeDeviceIds,
|
|
exitedVideo: () => videoService.exitedVideo(),
|
|
getPreloadedStream: () => videoService.getPreloadedStream(),
|
|
getRecord: () => videoService.getRecord(),
|
|
getPageChangeDebounceTime: () => { return PAGE_CHANGE_DEBOUNCE_TIME; },
|
|
getUserParameterProfile: () => videoService.getUserParameterProfile(),
|
|
isMultipleCamerasEnabled: () => videoService.isMultipleCamerasEnabled(),
|
|
joinVideo: (deviceId: string) => videoService.joinVideo(deviceId),
|
|
joinedVideo: () => videoService.joinedVideo(),
|
|
shouldRenderPaginationToggle: () => videoService.shouldRenderPaginationToggle(),
|
|
updateNumberOfDevices: (devices: MediaDeviceInfo[]) => videoService.updateNumberOfDevices(devices),
|
|
stopConnectingStream: videoService.stopConnectingStream,
|
|
updatePeerDictionaryReference: (
|
|
newRef: Record<string, WebRtcPeer>,
|
|
) => videoService.updatePeerDictionaryReference(newRef),
|
|
webRtcPeersRef: () => videoService.webRtcPeersRef,
|
|
isMobile: videoService.isMobile,
|
|
notify: (message: string) => notify(message, 'error', 'video'),
|
|
applyCameraProfile: debounce(
|
|
VideoService.applyCameraProfile,
|
|
CAMERA_QUALITY_THR_DEBOUNCE,
|
|
{ leading: false, trailing: true },
|
|
),
|
|
};
|