UNPKG

@daily-co/daily-js

Version:

**🚨Our docs have moved! 🚨**

1,736 lines (1,615 loc) • 101 kB
import EventEmitter from 'events'; import { deepEqual } from 'fast-equals'; import Bowser from 'bowser'; import { // re-export // // meeting states DAILY_STATE_NEW, DAILY_STATE_LOADING, DAILY_STATE_LOADED, DAILY_STATE_JOINING, DAILY_STATE_JOINED, DAILY_STATE_LEFT, DAILY_STATE_ERROR, // track states DAILY_TRACK_STATE_BLOCKED, DAILY_TRACK_STATE_OFF, DAILY_TRACK_STATE_SENDABLE, DAILY_TRACK_STATE_LOADING, DAILY_TRACK_STATE_INTERRUPTED, DAILY_TRACK_STATE_PLAYABLE, // meeting access DAILY_ACCESS_UNKNOWN, DAILY_ACCESS_LEVEL_FULL, DAILY_ACCESS_LEVEL_LOBBY, DAILY_ACCESS_LEVEL_NONE, // receive settings DAILY_RECEIVE_SETTINGS_BASE_KEY, DAILY_RECEIVE_SETTINGS_ALL_PARTICIPANTS_KEY, // error types DAILY_FATAL_ERROR_EJECTED, DAILY_FATAL_ERROR_NBF_ROOM, DAILY_FATAL_ERROR_NBF_TOKEN, DAILY_FATAL_ERROR_EXP_ROOM, DAILY_FATAL_ERROR_EXP_TOKEN, DAILY_CAMERA_ERROR_CAM_IN_USE, DAILY_CAMERA_ERROR_MIC_IN_USE, DAILY_CAMERA_ERROR_CAM_AND_MIC_IN_USE, // events DAILY_EVENT_IFRAME_READY_FOR_LAUNCH_CONFIG, DAILY_EVENT_IFRAME_LAUNCH_CONFIG, DAILY_EVENT_THEME_UPDATED, DAILY_EVENT_LOADING, DAILY_EVENT_LOADED, DAILY_EVENT_LOAD_ATTEMPT_FAILED, DAILY_EVENT_STARTED_CAMERA, DAILY_EVENT_CAMERA_ERROR, DAILY_EVENT_JOINING_MEETING, DAILY_EVENT_JOINED_MEETING, DAILY_EVENT_LEFT_MEETING, DAILY_EVENT_PARTICIPANT_JOINED, DAILY_EVENT_PARTICIPANT_UPDATED, DAILY_EVENT_PARTICIPANT_LEFT, DAILY_EVENT_TRACK_STARTED, DAILY_EVENT_TRACK_STOPPED, DAILY_EVENT_RECORDING_STARTED, DAILY_EVENT_RECORDING_STOPPED, DAILY_EVENT_TRANSCRIPTION_STARTED, DAILY_EVENT_TRANSCRIPTION_STOPPED, DAILY_EVENT_TRANSCRIPTION_ERROR, DAILY_EVENT_RECORDING_STATS, DAILY_EVENT_RECORDING_ERROR, DAILY_EVENT_RECORDING_UPLOAD_COMPLETED, DAILY_EVENT_ERROR, DAILY_EVENT_APP_MSG, DAILY_EVENT_INPUT_EVENT, DAILY_EVENT_LOCAL_SCREEN_SHARE_STARTED, DAILY_EVENT_LOCAL_SCREEN_SHARE_STOPPED, DAILY_EVENT_NETWORK_QUALITY_CHANGE, DAILY_EVENT_ACTIVE_SPEAKER_CHANGE, DAILY_EVENT_ACTIVE_SPEAKER_MODE_CHANGE, DAILY_EVENT_FULLSCREEN, DAILY_EVENT_EXIT_FULLSCREEN, DAILY_EVENT_NETWORK_CONNECTION, DAILY_EVENT_RECORDING_DATA, DAILY_EVENT_LIVE_STREAMING_STARTED, DAILY_EVENT_LIVE_STREAMING_STOPPED, DAILY_EVENT_LIVE_STREAMING_ERROR, DAILY_EVENT_LANG_UPDATED, DAILY_EVENT_SHOW_LOCAL_VIDEO_CHANGED, DAILY_EVENT_ACCESS_STATE_UPDATED, DAILY_EVENT_MEETING_SESSION_UPDATED, DAILY_EVENT_WAITING_PARTICIPANT_ADDED, DAILY_EVENT_WAITING_PARTICIPANT_REMOVED, DAILY_EVENT_WAITING_PARTICIPANT_UPDATED, DAILY_EVENT_RECEIVE_SETTINGS_UPDATED, DAILY_EVENT_MEDIA_INGEST_ERROR, DAILY_EVENT_INPUT_SETTINGS_UPDATED, DAILY_EVENT_NONFATAL_ERROR, // internals // DAILY_METHOD_SET_THEME, DAILY_METHOD_START_CAMERA, DAILY_METHOD_SET_INPUT_DEVICES, DAILY_METHOD_SET_OUTPUT_DEVICE, DAILY_METHOD_GET_INPUT_DEVICES, DAILY_METHOD_JOIN, DAILY_METHOD_LEAVE, DAILY_METHOD_UPDATE_PARTICIPANT, DAILY_METHOD_UPDATE_PARTICIPANTS, DAILY_METHOD_LOCAL_AUDIO, DAILY_METHOD_LOCAL_VIDEO, DAILY_METHOD_START_SCREENSHARE, DAILY_METHOD_STOP_SCREENSHARE, DAILY_METHOD_START_RECORDING, DAILY_METHOD_UPDATE_RECORDING, DAILY_METHOD_STOP_RECORDING, DAILY_METHOD_LOAD_CSS, DAILY_METHOD_SET_BANDWIDTH, DAILY_METHOD_GET_CALC_STATS, DAILY_METHOD_ENUMERATE_DEVICES, DAILY_METHOD_CYCLE_CAMERA, DAILY_METHOD_CYCLE_MIC, DAILY_METHOD_APP_MSG, DAILY_METHOD_ADD_FAKE_PARTICIPANT, DAILY_METHOD_SET_SHOW_NAMES, DAILY_METHOD_SET_SHOW_LOCAL_VIDEO, DAILY_METHOD_SET_SHOW_PARTICIPANTS_BAR, DAILY_METHOD_SET_ACTIVE_SPEAKER_MODE, DAILY_METHOD_GET_LANG, DAILY_METHOD_SET_LANG, DAILY_METHOD_GET_MEETING_SESSION, MAX_APP_MSG_SIZE, DAILY_METHOD_REGISTER_INPUT_HANDLER, DAILY_METHOD_DETECT_ALL_FACES, DAILY_METHOD_ROOM, DAILY_METHOD_GET_NETWORK_TOPOLOGY, DAILY_METHOD_SET_NETWORK_TOPOLOGY, DAILY_METHOD_SET_PLAY_DING, DAILY_METHOD_SET_SUBSCRIBE_TO_TRACKS_AUTOMATICALLY, DAILY_METHOD_START_LIVE_STREAMING, DAILY_METHOD_UPDATE_LIVE_STREAMING, DAILY_METHOD_STOP_LIVE_STREAMING, DAILY_METHOD_START_TRANSCRIPTION, DAILY_METHOD_STOP_TRANSCRIPTION, DAILY_CUSTOM_TRACK, DAILY_UI_REQUEST_FULLSCREEN, DAILY_UI_EXIT_FULLSCREEN, DAILY_METHOD_GET_CAMERA_FACING_MODE, DAILY_METHOD_SET_USER_NAME, DAILY_METHOD_PREAUTH, DAILY_METHOD_REQUEST_ACCESS, DAILY_METHOD_UPDATE_WAITING_PARTICIPANT, DAILY_METHOD_UPDATE_WAITING_PARTICIPANTS, DAILY_METHOD_GET_SINGLE_PARTICIPANT_RECEIVE_SETTINGS, DAILY_METHOD_UPDATE_RECEIVE_SETTINGS, DAILY_JS_VIDEO_PROCESSOR_TYPES as VIDEO_PROCESSOR_TYPES, DAILY_METHOD_UPDATE_INPUT_SETTINGS, } from './shared-with-pluot-core/CommonIncludes.js'; import { isReactNative, browserVideoSupported_p, getUserAgent, isScreenSharingSupported, isSfuSupported, isVideoProcessingSupported, } from './shared-with-pluot-core/Environment.js'; import WebMessageChannel from './shared-with-pluot-core/script-message-channels/WebMessageChannel'; import ReactNativeMessageChannel from './shared-with-pluot-core/script-message-channels/ReactNativeMessageChannel'; import CallObjectLoader from './CallObjectLoader'; import { callObjectBundleUrl, randomStringId } from './utils.js'; import * as Participant from './Participant'; // meeting states export { DAILY_STATE_NEW, DAILY_STATE_JOINING, DAILY_STATE_JOINED, DAILY_STATE_LEFT, DAILY_STATE_ERROR, }; // track states export { DAILY_TRACK_STATE_BLOCKED, DAILY_TRACK_STATE_OFF, DAILY_TRACK_STATE_SENDABLE, DAILY_TRACK_STATE_LOADING, DAILY_TRACK_STATE_INTERRUPTED, DAILY_TRACK_STATE_PLAYABLE, }; // meeting access export { DAILY_ACCESS_UNKNOWN, DAILY_ACCESS_LEVEL_FULL, DAILY_ACCESS_LEVEL_LOBBY, DAILY_ACCESS_LEVEL_NONE, }; // receive settings export { DAILY_RECEIVE_SETTINGS_BASE_KEY, DAILY_RECEIVE_SETTINGS_ALL_PARTICIPANTS_KEY, }; // error types export { DAILY_FATAL_ERROR_EJECTED, DAILY_FATAL_ERROR_NBF_ROOM, DAILY_FATAL_ERROR_NBF_TOKEN, DAILY_FATAL_ERROR_EXP_ROOM, DAILY_FATAL_ERROR_EXP_TOKEN, DAILY_CAMERA_ERROR_CAM_IN_USE, DAILY_CAMERA_ERROR_MIC_IN_USE, DAILY_CAMERA_ERROR_CAM_AND_MIC_IN_USE, }; // events export { DAILY_EVENT_IFRAME_READY_FOR_LAUNCH_CONFIG, DAILY_EVENT_IFRAME_LAUNCH_CONFIG, DAILY_EVENT_THEME_UPDATED, DAILY_EVENT_LOADING, DAILY_EVENT_LOADED, DAILY_EVENT_LOAD_ATTEMPT_FAILED, DAILY_EVENT_STARTED_CAMERA, DAILY_EVENT_CAMERA_ERROR, DAILY_EVENT_JOINING_MEETING, DAILY_EVENT_JOINED_MEETING, DAILY_EVENT_LEFT_MEETING, DAILY_EVENT_PARTICIPANT_JOINED, DAILY_EVENT_PARTICIPANT_UPDATED, DAILY_EVENT_PARTICIPANT_LEFT, DAILY_EVENT_TRACK_STARTED, DAILY_EVENT_TRACK_STOPPED, DAILY_EVENT_RECORDING_STARTED, DAILY_EVENT_RECORDING_STOPPED, DAILY_EVENT_RECORDING_STATS, DAILY_EVENT_RECORDING_ERROR, DAILY_EVENT_RECORDING_UPLOAD_COMPLETED, DAILY_EVENT_TRANSCRIPTION_STARTED, DAILY_EVENT_TRANSCRIPTION_STOPPED, DAILY_EVENT_TRANSCRIPTION_ERROR, DAILY_EVENT_ERROR, DAILY_EVENT_APP_MSG, DAILY_EVENT_INPUT_EVENT, DAILY_EVENT_LOCAL_SCREEN_SHARE_STARTED, DAILY_EVENT_LOCAL_SCREEN_SHARE_STOPPED, DAILY_EVENT_NETWORK_QUALITY_CHANGE, DAILY_EVENT_ACTIVE_SPEAKER_CHANGE, DAILY_EVENT_ACTIVE_SPEAKER_MODE_CHANGE, DAILY_EVENT_FULLSCREEN, DAILY_EVENT_EXIT_FULLSCREEN, DAILY_EVENT_NETWORK_CONNECTION, DAILY_EVENT_RECORDING_DATA, DAILY_EVENT_LIVE_STREAMING_STARTED, DAILY_EVENT_LIVE_STREAMING_STOPPED, DAILY_EVENT_LIVE_STREAMING_ERROR, DAILY_EVENT_LANG_UPDATED, DAILY_EVENT_ACCESS_STATE_UPDATED, DAILY_EVENT_MEETING_SESSION_UPDATED, DAILY_EVENT_WAITING_PARTICIPANT_ADDED, DAILY_EVENT_WAITING_PARTICIPANT_REMOVED, DAILY_EVENT_WAITING_PARTICIPANT_UPDATED, DAILY_EVENT_RECEIVE_SETTINGS_UPDATED, DAILY_EVENT_INPUT_SETTINGS_UPDATED, DAILY_EVENT_NONFATAL_ERROR, }; // Audio modes for React Native: whether we should configure audio for video // calls or audio calls (i.e. whether we should use speakerphone). const NATIVE_AUDIO_MODE_VIDEO_CALL = 'video'; const NATIVE_AUDIO_MODE_VOICE_CALL = 'voice'; const NATIVE_AUDIO_MODE_IDLE = 'idle'; // // // const reactNativeConfigType = { androidInCallNotification: { title: 'string', subtitle: 'string', iconName: 'string', disableForCustomOverride: 'boolean', }, disableAutoDeviceManagement: { audio: 'boolean', video: 'boolean', }, }; const FRAME_PROPS = { url: { validate: (url) => typeof url === 'string', help: 'url should be a string', }, baseUrl: { validate: (url) => typeof url === 'string', help: 'baseUrl should be a string', }, token: { validate: (token) => typeof token === 'string', help: 'token should be a string', queryString: 't', }, dailyConfig: { // only for call object mode, for now validate: (config) => { if (!window._dailyConfig) { window._dailyConfig = {}; } window._dailyConfig.experimentalGetUserMediaConstraintsModify = config.experimentalGetUserMediaConstraintsModify; delete config.experimentalGetUserMediaConstraintsModify; return true; }, }, reactNativeConfig: { validate: validateReactNativeConfig, help: `reactNativeConfig should look like ${JSON.stringify( reactNativeConfigType )}, all fields optional`, }, lang: { validate: (lang) => { return [ 'de', 'en-us', // Here for backwards compatibility, but not encouraged (just maps to 'en' anyway) 'en', 'es', 'fi', 'fr', 'it', 'jp', 'ka', 'nl', 'no', 'pl', 'pt', 'ru', 'sv', 'tr', 'user', ].includes(lang); }, help: 'language not supported. Options are: de, en-us, en, es, fi, fr, it, jp, ka, nl, no, pl, pt, ru, sv, tr, user', }, userName: true, // ignored if there's a token activeSpeakerMode: true, showLeaveButton: true, showLocalVideo: true, showParticipantsBar: true, showFullscreenButton: true, // style to apply to iframe in createFrame factory method iframeStyle: true, // styles passed through to video calls inside the iframe customLayout: true, cssFile: true, cssText: true, bodyClass: true, videoSource: { validate: (s, callObject) => { callObject._preloadCache.videoDeviceId = s; return true; }, }, audioSource: { validate: (s, callObject) => { callObject._preloadCache.audioDeviceId = s; return true; }, }, subscribeToTracksAutomatically: { validate: (s, callObject) => { callObject._preloadCache.subscribeToTracksAutomatically = s; return true; }, }, theme: { validate: (o) => { const validColors = [ 'accent', 'accentText', 'background', 'backgroundAccent', 'baseText', 'border', 'mainAreaBg', 'mainAreaBgAccent', 'mainAreaText', 'supportiveText', ]; const containsValidColors = (colors) => { for (const key of Object.keys(colors)) { if (!validColors.includes(key)) { // Key is not a supported theme color console.error( `unsupported color "${key}". Valid colors: ${validColors.join( ', ' )}` ); return false; } if (!colors[key].match(/^#[0-9a-f]{6}|#[0-9a-f]{3}$/i)) { // Color is not in hex format console.error( `${key} theme color should be provided in valid hex color format. Received: "${colors[key]}"` ); return false; } } return true; }; if ( typeof o !== 'object' || !(('light' in o && 'dark' in o) || 'colors' in o) ) { // Must define either both themes or colors console.error( 'Theme must contain either both "light" and "dark" properties, or "colors".', o ); return false; } if ('light' in o && 'dark' in o) { if (!('colors' in o.light)) { console.error('Light theme is missing "colors" property.', o); return false; } if (!('colors' in o.dark)) { console.error('Dark theme is missing "colors" property.', o); return false; } return ( containsValidColors(o.light.colors) && containsValidColors(o.dark.colors) ); } return containsValidColors(o.colors); }, help: 'unsupported theme configuration. Check error logs for detailed info.', }, layoutConfig: { validate: (layoutConfig) => { if ('grid' in layoutConfig) { const gridConfig = layoutConfig.grid; if ('maxTilesPerPage' in gridConfig) { if (!Number.isInteger(gridConfig.maxTilesPerPage)) { console.error( `grid.maxTilesPerPage should be an integer. You passed ${gridConfig.maxTilesPerPage}.` ); return false; } if (gridConfig.maxTilesPerPage > 49) { console.error( `grid.maxTilesPerPage can't be larger than 49 without sacrificing browser performance. Please contact us at https://www.daily.co/contact to talk about your use case.` ); return false; } } if ('minTilesPerPage' in gridConfig) { if (!Number.isInteger(gridConfig.minTilesPerPage)) { console.error( `grid.minTilesPerPage should be an integer. You passed ${gridConfig.minTilesPerPage}.` ); return false; } if (gridConfig.minTilesPerPage < 1) { console.error(`grid.minTilesPerPage can't be lower than 1.`); return false; } if ( 'maxTilesPerPage' in gridConfig && gridConfig.minTilesPerPage > gridConfig.maxTilesPerPage ) { console.error( `grid.minTilesPerPage can't be higher than grid.maxTilesPerPage.` ); return false; } } } return true; }, help: 'unsupported layoutConfig. Check error logs for detailed info.', }, receiveSettings: { // Disallow "*" shorthand key since it's a shorthand for participants // currently connected *to you* (i.e. participants already in // participants()), which is necessarily empty at join time. Allowing this // key might only sow confusion: it might lead people to think it's a // shorthand for participants currently connected *to the room*. validate: (receiveSettings) => validateReceiveSettings(receiveSettings, { allowAllParticipantsKey: false, }), help: receiveSettingsValidationHelpMsg({ allowAllParticipantsKey: false, }), }, inputSettings: { validate: (inputSettings) => validateInputSettings(inputSettings), help: inputSettingsValidationHelpMsg(), }, // used internally layout: { validate: (layout) => layout === 'custom-v1' || layout === 'browser' || layout === 'none', help: 'layout may only be set to "custom-v1"', queryString: 'layout', }, emb: { queryString: 'emb', }, embHref: { queryString: 'embHref', }, dailyJsVersion: { queryString: 'dailyJsVersion', }, }; // todo: more validation? const PARTICIPANT_PROPS = { styles: { validate: (styles) => { for (var k in styles) { if (k !== 'cam' && k !== 'screen') { return false; } } if (styles.cam) { for (var k in styles.cam) { if (k !== 'div' && k !== 'video') { return false; } } } if (styles.screen) { for (var k in styles.screen) { if (k !== 'div' && k !== 'video') { return false; } } } return true; }, help: 'styles format should be a subset of: ' + '{ cam: {div: {}, video: {}}, screen: {div: {}, video: {}} }', }, setSubscribedTracks: { validate: (subs, callObject, participant) => { if (callObject._preloadCache.subscribeToTracksAutomatically) { return false; } const validPrimitiveValues = [true, false, 'staged']; if ( validPrimitiveValues.includes(subs) || (!isReactNative() && subs === 'avatar') ) { return true; } for (const s in subs) { if ( !( ['audio', 'video', 'screenAudio', 'screenVideo'].includes(s) && validPrimitiveValues.includes(subs[s]) ) ) { return false; } } return true; }, help: 'setSubscribedTracks cannot be used when setSubscribeToTracksAutomatically is enabled, and should be of the form: ' + `true${ !isReactNative() ? " | 'avatar'" : '' } | false | 'staged' | { [audio: true|false|'staged'], [video: true|false|'staged'], [screenAudio: true|false|'staged'], [screenVideo: true|false|'staged'] }`, }, setAudio: true, setVideo: true, eject: true, }; // // // export default class DailyIframe extends EventEmitter { // // static methods // static supportedBrowser() { if (isReactNative()) { return { supported: true, mobile: true, name: 'React Native', version: null, supportsScreenShare: false, supportsSfu: true, supportsVideoProcessing: false, }; } const browser = Bowser.getParser(getUserAgent()); return { supported: !!browserVideoSupported_p(), mobile: browser.getPlatformType() === 'mobile', name: browser.getBrowserName(), version: browser.getBrowserVersion(), supportsScreenShare: !!isScreenSharingSupported(), supportsSfu: !!isSfuSupported(), supportsVideoProcessing: isVideoProcessingSupported(), }; } static version() { return __dailyJsVersion__; } // // constructors // static createCallObject(properties = {}) { properties.layout = 'none'; return new DailyIframe(null, properties); } static wrap(iframeish, properties = {}) { methodNotSupportedInReactNative(); if ( !iframeish || !iframeish.contentWindow || 'string' !== typeof iframeish.src ) { throw new Error('DailyIframe::Wrap needs an iframe-like first argument'); } if (!properties.layout) { if (properties.customLayout) { properties.layout = 'custom-v1'; } else { properties.layout = 'browser'; } } return new DailyIframe(iframeish, properties); } static createFrame(arg1, arg2) { methodNotSupportedInReactNative(); let parentEl, properties; if (arg1 && arg2) { parentEl = arg1; properties = arg2; } else if (arg1 && arg1.append) { parentEl = arg1; properties = {}; } else { parentEl = document.body; properties = arg1 || {}; } let iframeStyle = properties.iframeStyle; if (!iframeStyle) { if (parentEl === document.body) { iframeStyle = { position: 'fixed', border: '1px solid black', backgroundColor: 'white', width: '375px', height: '450px', right: '1em', bottom: '1em', }; } else { iframeStyle = { border: 0, width: '100%', height: '100%', }; } } let iframeEl = document.createElement('iframe'); // special-case for old Electron for Figma if (window.navigator && window.navigator.userAgent.match(/Chrome\/61\./)) { iframeEl.allow = 'microphone, camera'; } else { iframeEl.allow = 'microphone; camera; autoplay; display-capture'; } iframeEl.style.visibility = 'hidden'; parentEl.appendChild(iframeEl); iframeEl.style.visibility = null; Object.keys(iframeStyle).forEach( (k) => (iframeEl.style[k] = iframeStyle[k]) ); if (!properties.layout) { if (properties.customLayout) { properties.layout = 'custom-v1'; } else { properties.layout = 'browser'; } } try { let callFrame = new DailyIframe(iframeEl, properties); return callFrame; } catch (e) { // something when wrong while constructing the object. so let's clean // up by removing ourselves from the page, then rethrow the error. parentEl.removeChild(iframeEl); throw e; } } static createTransparentFrame(properties = {}) { methodNotSupportedInReactNative(); let iframeEl = document.createElement('iframe'); iframeEl.allow = 'microphone; camera; autoplay'; iframeEl.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; border: 0; pointer-events: none; `; document.body.appendChild(iframeEl); if (!properties.layout) { properties.layout = 'custom-v1'; } return DailyIframe.wrap(iframeEl, properties); } constructor(iframeish, properties = {}) { super(); properties.dailyJsVersion = __dailyJsVersion__; this._iframe = iframeish; this._callObjectMode = properties.layout === 'none' && !this._iframe; this._preloadCache = initializePreloadCache(); if (this._callObjectMode) { window._dailyPreloadCache = this._preloadCache; } if (properties.showLocalVideo !== undefined) { if (this._callObjectMode) { console.error('showLocalVideo is not available in call object mode'); } else { this._showLocalVideo = !!properties.showLocalVideo; } } else { this._showLocalVideo = true; } if (properties.showParticipantsBar !== undefined) { if (this._callObjectMode) { console.error( 'showParticipantsBar is not available in call object mode' ); } else { this._showParticipantsBar = !!properties.showParticipantsBar; } } else { this._showParticipantsBar = true; } if (properties.activeSpeakerMode !== undefined) { if (this._callObjectMode) { console.error('activeSpeakerMode is not available in call object mode'); } else { this._activeSpeakerMode = !!properties.activeSpeakerMode; } } else { this._activeSpeakerMode = false; } if (properties.receiveSettings) { if (this._callObjectMode) { this._receiveSettings = properties.receiveSettings; } else { console.error('receiveSettings is only available in call object mode'); } } else { // Here we avoid falling back to defaults, instead letting the call // machine decide on defaults when its loaded and telling us about them // via a DAILY_EVENT_RECEIVE_SETTINGS_UPDATED event. This will make it // easier to update defaults in the future, eliminating the worry of // daily-js getting out of sync with the call machine. this._receiveSettings = {}; } this._inputSettings = {}; if (properties.inputSettings) { // #Question: Do I need the call-object check here? this._inputSettings = properties.inputSettings; } this.validateProperties(properties); this.properties = { ...properties }; this._callObjectLoader = this._callObjectMode ? new CallObjectLoader() : null; this._meetingState = DAILY_STATE_NEW; // only update via updateIsPreparingToJoin() or updateMeetingState() this._isPreparingToJoin = false; // only update via updateMeetingState() this._accessState = { access: DAILY_ACCESS_UNKNOWN }; this._nativeInCallAudioMode = NATIVE_AUDIO_MODE_VIDEO_CALL; this._participants = {}; this._waitingParticipants = {}; this._inputEventsOn = {}; // need to cache these until loaded this._network = { threshold: 'good', quality: 100 }; this._activeSpeaker = {}; this._callFrameId = randomStringId(); this._messageChannel = isReactNative() ? new ReactNativeMessageChannel() : new WebMessageChannel(); // fullscreen event listener if (this._iframe) { if (this._iframe.requestFullscreen) { // chrome (not safari) this._iframe.addEventListener('fullscreenchange', (e) => { if (document.fullscreenElement === this._iframe) { this.emit(DAILY_EVENT_FULLSCREEN, { action: DAILY_EVENT_FULLSCREEN, }); this.sendMessageToCallMachine({ action: DAILY_EVENT_FULLSCREEN }); } else { this.emit(DAILY_EVENT_EXIT_FULLSCREEN, { action: DAILY_EVENT_EXIT_FULLSCREEN, }); this.sendMessageToCallMachine({ action: DAILY_EVENT_EXIT_FULLSCREEN, }); } }); } else if (this._iframe.webkitRequestFullscreen) { // safari this._iframe.addEventListener('webkitfullscreenchange', (e) => { if (document.webkitFullscreenElement === this._iframe) { this.emit(DAILY_EVENT_FULLSCREEN, { action: DAILY_EVENT_FULLSCREEN, }); this.sendMessageToCallMachine({ action: DAILY_EVENT_FULLSCREEN }); } else { this.emit(DAILY_EVENT_EXIT_FULLSCREEN, { action: DAILY_EVENT_EXIT_FULLSCREEN, }); this.sendMessageToCallMachine({ action: DAILY_EVENT_EXIT_FULLSCREEN, }); } }); } } // add native event listeners if (isReactNative()) { const nativeUtils = this.nativeUtils(); if ( !( nativeUtils.addAudioFocusChangeListener && nativeUtils.removeAudioFocusChangeListener && nativeUtils.addAppActiveStateChangeListener && nativeUtils.removeAppActiveStateChangeListener ) ) { console.warn( 'expected (add|remove)(AudioFocus|AppActiveState)ChangeListener to be available in React Native' ); } // audio focus event, used for auto-muting mic this._hasNativeAudioFocus = true; nativeUtils.addAudioFocusChangeListener( this.handleNativeAudioFocusChange ); // app active state event, used for auto-muting cam nativeUtils.addAppActiveStateChangeListener( this.handleNativeAppActiveStateChange ); } this._messageChannel.addListenerForMessagesFromCallMachine( this.handleMessageFromCallMachine, this._callFrameId, this ); } // // instance methods // async destroy() { try { if ( [DAILY_STATE_JOINED, DAILY_STATE_LOADING].includes(this._meetingState) ) { await this.leave(); } } catch (e) {} let iframe = this._iframe; if (iframe) { let parent = iframe.parentElement; if (parent) { parent.removeChild(iframe); } } this._messageChannel.removeListener(this.handleMessageFromCallMachine); // tear down native event listeners if (isReactNative()) { const nativeUtils = this.nativeUtils(); nativeUtils.removeAudioFocusChangeListener( this.handleNativeAudioFocusChange ); nativeUtils.removeAppActiveStateChangeListener( this.handleNativeAppActiveStateChange ); } this.resetMeetingDependentVars(); } loadCss({ bodyClass, cssFile, cssText }) { methodNotSupportedInReactNative(); this.sendMessageToCallMachine({ action: DAILY_METHOD_LOAD_CSS, cssFile: this.absoluteUrl(cssFile), bodyClass, cssText, }); return this; } iframe() { methodNotSupportedInReactNative(); return this._iframe; } meetingState() { return this._meetingState; } accessState() { if (!this._callObjectMode) { throw new Error( 'accessState() currently only supported in call object mode' ); } return this._accessState; } participants() { return this._participants; } waitingParticipants() { if (!this._callObjectMode) { throw new Error( 'waitingParticipants() currently only supported in call object mode' ); } return this._waitingParticipants; } validateParticipantProperties(sessionId, properties) { for (var prop in properties) { if (!PARTICIPANT_PROPS[prop]) { throw new Error(`unrecognized updateParticipant property ${prop}`); } if (PARTICIPANT_PROPS[prop].validate) { if ( !PARTICIPANT_PROPS[prop].validate( properties[prop], this, this._participants[sessionId] ) ) { throw new Error(PARTICIPANT_PROPS[prop].help); } } } } updateParticipant(sessionId, properties) { if ( this._participants.local && this._participants.local.session_id === sessionId ) { sessionId = 'local'; } if (sessionId && properties && this._participants[sessionId]) { this.validateParticipantProperties(sessionId, properties); this.sendMessageToCallMachine({ action: DAILY_METHOD_UPDATE_PARTICIPANT, id: sessionId, properties, }); } return this; } updateParticipants(properties) { const localId = this._participants.local && this._participants.local.session_id; for (var sessionId in properties) { if (sessionId === localId) { sessionId = 'local'; } if ( sessionId && properties[sessionId] && (this._participants[sessionId] || sessionId === '*') ) { this.validateParticipantProperties(sessionId, properties[sessionId]); } else { console.warn( `unrecognized participant in updateParticipants: ${sessionId}` ); delete properties[sessionId]; } } this.sendMessageToCallMachine({ action: DAILY_METHOD_UPDATE_PARTICIPANTS, participants: properties, }); return this; } async updateWaitingParticipant(id = '', updates = {}) { // Validate mode. if (!this._callObjectMode) { throw new Error( 'updateWaitingParticipant() currently only supported in call object mode' ); } // Validate meeting state: only allowed once you've joined. if (this._meetingState !== DAILY_STATE_JOINED) { throw new Error( 'updateWaitingParticipant() only supported for joined meetings' ); } // Validate argument presence. if (!(typeof id === 'string' && typeof updates === 'object')) { throw new Error( 'updateWaitingParticipant() must take an id string and a updates object' ); } return new Promise((resolve, reject) => { const k = (msg) => { if (msg.error) { reject(msg.error); } if (!msg.id) { reject(new Error('unknown error in updateWaitingParticipant()')); } resolve({ id: msg.id }); }; this.sendMessageToCallMachine( { action: DAILY_METHOD_UPDATE_WAITING_PARTICIPANT, id, updates, }, k ); }); } async updateWaitingParticipants(updatesById = {}) { // Validate mode. if (!this._callObjectMode) { throw new Error( 'updateWaitingParticipants() currently only supported in call object mode' ); } // Validate meeting state: only allowed once you've joined. if (this._meetingState !== DAILY_STATE_JOINED) { throw new Error( 'updateWaitingParticipants() only supported for joined meetings' ); } // Validate argument presence. if (typeof updatesById !== 'object') { throw new Error( 'updateWaitingParticipants() must take a mapping between ids and update objects' ); } return new Promise((resolve, reject) => { const k = (msg) => { if (msg.error) { reject(msg.error); } if (!msg.ids) { reject(new Error('unknown error in updateWaitingParticipants()')); } resolve({ ids: msg.ids }); }; this.sendMessageToCallMachine( { action: DAILY_METHOD_UPDATE_WAITING_PARTICIPANTS, updatesById, }, k ); }); } async requestAccess({ access = { level: DAILY_ACCESS_LEVEL_FULL }, name = '', } = {}) { // Validate mode. if (!this._callObjectMode) { throw new Error( 'requestAccess() currently only supported in call object mode' ); } // Validate meeting state: access requesting is only allowed once you've // joined. if (this._meetingState !== DAILY_STATE_JOINED) { throw new Error('requestAccess() only supported for joined meetings'); } return new Promise((resolve, reject) => { const k = (msg) => { if (msg.error) { reject(msg.error); } if (!msg.access) { reject(new Error('unknown error in requestAccess()')); } resolve({ access: msg.access, granted: msg.granted }); }; this.sendMessageToCallMachine( { action: DAILY_METHOD_REQUEST_ACCESS, access, name, }, k ); }); } localAudio() { if (this._participants.local) { return this._participants.local.audio; } return null; } localVideo() { if (this._participants.local) { return this._participants.local.video; } return null; } setLocalAudio(bool) { this.sendMessageToCallMachine({ action: DAILY_METHOD_LOCAL_AUDIO, state: bool, }); return this; } setLocalVideo(bool) { this.sendMessageToCallMachine({ action: DAILY_METHOD_LOCAL_VIDEO, state: bool, }); return this; } // NOTE: "base" receive settings will not appear until the call machine bundle // is initialized (e.g. after a call to join()). // Listen for the receive-settings-updated to be notified when those come in. async getReceiveSettings(id, { showInheritedValues = false } = {}) { // Validate mode. if (!this._callObjectMode) { throw new Error( 'getReceiveSettings() only supported in call object mode' ); } // This method can be called in two main ways: // - it can get receive settings for a specific participant (or "base") // - it can get *all* receive settings switch (typeof id) { // Case: getting receive settings for a single participant case 'string': // Ask call machine to get receive settings for the participant. // Centralizing this nontrivial fetching logic in the call machine, // rather than attempting to duplicate it here, avoids the problem of // daily-js and the call machine getting out of sync. return new Promise((resolve) => { const k = (msg) => { resolve(msg.receiveSettings); }; this.sendMessageToCallMachine( { action: DAILY_METHOD_GET_SINGLE_PARTICIPANT_RECEIVE_SETTINGS, id, showInheritedValues, }, k ); }); // Case: getting all receive settings case 'undefined': return this._receiveSettings; default: throw new Error( 'first argument to getReceiveSettings() must be a participant id (or "base"), or there should be no arguments' ); } } async updateReceiveSettings(receiveSettings) { // Validate mode. if (!this._callObjectMode) { throw new Error( 'updateReceiveSettings() only supported in call object mode' ); } // Validate receive settings. if ( !validateReceiveSettings(receiveSettings, { allowAllParticipantsKey: true, }) ) { throw new Error( receiveSettingsValidationHelpMsg({ allowAllParticipantsKey: true }) ); } // Validate that call machine is joined. // (We need the Redux state to be set up first; technically, we could // proceed if we've either join()ed *or* preAuth()ed *or* startCamera()ed // but since there's an easy alternative way to specify initial receive // settings until join(), for simplicity let's just require that we be // joined). if (this._meetingState !== DAILY_STATE_JOINED) { throw new Error( 'updateReceiveSettings() is only allowed when joined. To specify receive settings earlier, use the receiveSettings config property.' ); } // Ask call machine to update receive settings, then await callback. return new Promise((resolve) => { const k = (msg) => { resolve({ receiveSettings: msg.receiveSettings }); }; this.sendMessageToCallMachine( { action: DAILY_METHOD_UPDATE_RECEIVE_SETTINGS, receiveSettings, }, k ); }); } // Input Settings Getter // { video: { processor } } // In the future: // { video: {...}, audio: {...}, screenVideo: {...}, screenAudio: {...} } getInputSettings() { return this._inputSettings; } async updateInputSettings(inputSettings) { //#Question: Do I need the call-object mode check for input-settings? if (!validateInputSettings(inputSettings)) { throw new Error(inputSettingsValidationHelpMsg()); } // Ask call machine to update input settings, then await callback. return new Promise((resolve) => { const k = (msg) => { resolve({ inputSettings: msg.inputSettings }); }; this.sendMessageToCallMachine( { action: DAILY_METHOD_UPDATE_INPUT_SETTINGS, inputSettings, }, k ); }); } setBandwidth({ kbs, trackConstraints }) { methodNotSupportedInReactNative(); this.sendMessageToCallMachine({ action: DAILY_METHOD_SET_BANDWIDTH, kbs, trackConstraints, }); return this; } getDailyLang() { methodNotSupportedInReactNative(); return new Promise(async (resolve) => { const k = (msg) => { delete msg.action; delete msg.callbackStamp; resolve(msg); }; this.sendMessageToCallMachine({ action: DAILY_METHOD_GET_LANG }, k); }); } setDailyLang(lang) { methodNotSupportedInReactNative(); this.sendMessageToCallMachine({ action: DAILY_METHOD_SET_LANG, lang }); return this; } async getMeetingSession() { // Validate meeting state: meeting session details are only available // once you have joined the meeting if (this._meetingState !== DAILY_STATE_JOINED) { throw new Error('getMeetingSession() is only allowed when joined'); } return new Promise(async (resolve) => { const k = (msg) => { delete msg.action; delete msg.callbackStamp; delete msg.callFrameId; resolve(msg); }; this.sendMessageToCallMachine( { action: DAILY_METHOD_GET_MEETING_SESSION }, k ); }); } setUserName(name, options) { this.properties.userName = name; return new Promise(async (resolve) => { const k = (msg) => { delete msg.action; delete msg.callbackStamp; resolve(msg); }; this.sendMessageToCallMachine( { action: DAILY_METHOD_SET_USER_NAME, name: name ?? '', thisMeetingOnly: isReactNative() || (options ? !!options.thisMeetingOnly : false), }, k ); }); } startCamera(properties = {}) { return new Promise(async (resolve, reject) => { let k = (msg) => { delete msg.action; delete msg.callbackStamp; resolve(msg); }; if (this.needsLoad()) { try { await this.load(properties); } catch (e) { reject(e); } } this.sendMessageToCallMachine( { action: DAILY_METHOD_START_CAMERA, properties: makeSafeForPostMessage(this.properties), preloadCache: makeSafeForPostMessage(this._preloadCache), }, k ); }); } cycleCamera() { return new Promise((resolve, _) => { let k = (msg) => { resolve({ device: msg.device }); }; this.sendMessageToCallMachine({ action: DAILY_METHOD_CYCLE_CAMERA }, k); }); } cycleMic() { methodNotSupportedInReactNative(); return new Promise((resolve, _) => { let k = (msg) => { resolve({ device: msg.device }); }; this.sendMessageToCallMachine({ action: DAILY_METHOD_CYCLE_MIC }, k); }); } getCameraFacingMode() { methodOnlySupportedInReactNative(); return new Promise((resolve, _) => { let k = (msg) => { resolve(msg.facingMode); }; this.sendMessageToCallMachine( { action: DAILY_METHOD_GET_CAMERA_FACING_MODE }, k ); }); } setInputDevices({ audioDeviceId, videoDeviceId, audioSource, videoSource }) { console.warn( 'setInputDevices() is deprecated: instead use setInputDevicesAsync(), which returns a Promise' ); this.setInputDevicesAsync({ audioDeviceId, videoDeviceId, audioSource, videoSource, }); return this; } async setInputDevicesAsync({ audioDeviceId, videoDeviceId, audioSource, videoSource, }) { methodNotSupportedInReactNative(); // use audioDeviceId and videoDeviceId internally if (audioSource !== undefined) { audioDeviceId = audioSource; } if (videoSource !== undefined) { videoDeviceId = videoSource; } // cache these for use in subsequent calls if (audioDeviceId) { this._preloadCache.audioDeviceId = audioDeviceId; } if (videoDeviceId) { this._preloadCache.videoDeviceId = videoDeviceId; } // if we're in callObject mode and not loaded yet, don't do anything if (this._callObjectMode && this.needsLoad()) { return { camera: { deviceId: this._preloadCache.videoDeviceId }, mic: { deviceId: this._preloadCache.audioDeviceId }, speaker: { deviceId: this._preloadCache.outputDeviceId }, }; } if (audioDeviceId instanceof MediaStreamTrack) { audioDeviceId = DAILY_CUSTOM_TRACK; } if (videoDeviceId instanceof MediaStreamTrack) { videoDeviceId = DAILY_CUSTOM_TRACK; } return new Promise((resolve) => { let k = (msg) => { delete msg.action; delete msg.callbackStamp; if (msg.returnPreloadCache) { resolve({ camera: { deviceId: this._preloadCache.videoDeviceId }, mic: { deviceId: this._preloadCache.audioDeviceId }, speaker: { deviceId: this._preloadCache.outputDeviceId }, }); return; } resolve(msg); }; this.sendMessageToCallMachine( { action: DAILY_METHOD_SET_INPUT_DEVICES, audioDeviceId, videoDeviceId, }, k ); }); } setOutputDevice({ outputDeviceId }) { methodNotSupportedInReactNative(); // cache this for use later if (outputDeviceId) { this._preloadCache.outputDeviceId = outputDeviceId; } // if we're in callObject mode and not joined yet, don't do anything if (this._callObjectMode && this._meetingState !== DAILY_STATE_JOINED) { return this; } this.sendMessageToCallMachine({ action: DAILY_METHOD_SET_OUTPUT_DEVICE, outputDeviceId, }); return this; } async getInputDevices() { methodNotSupportedInReactNative(); if (this._callObjectMode && this.needsLoad()) { return { camera: { deviceId: this._preloadCache.videoDeviceId }, mic: { deviceId: this._preloadCache.audioDeviceId }, speaker: { deviceId: this._preloadCache.outputDeviceId }, }; } return new Promise((resolve, reject) => { let k = (msg) => { delete msg.action; delete msg.callbackStamp; if (msg.returnPreloadCache) { resolve({ camera: { deviceId: this._preloadCache.videoDeviceId }, mic: { deviceId: this._preloadCache.audioDeviceId }, speaker: { deviceId: this._preloadCache.outputDeviceId }, }); return; } resolve(msg); }; this.sendMessageToCallMachine( { action: DAILY_METHOD_GET_INPUT_DEVICES }, k ); }); } nativeInCallAudioMode() { methodOnlySupportedInReactNative(); return this._nativeInCallAudioMode; } setNativeInCallAudioMode(inCallAudioMode) { methodOnlySupportedInReactNative(); if ( ![NATIVE_AUDIO_MODE_VIDEO_CALL, NATIVE_AUDIO_MODE_VOICE_CALL].includes( inCallAudioMode ) ) { console.error('invalid in-call audio mode specified: ', inCallAudioMode); return; } if (inCallAudioMode === this._nativeInCallAudioMode) { return; } // Set new audio mode (video call, audio call) to use when we're in a call this._nativeInCallAudioMode = inCallAudioMode; // If we're in a call now, apply the new audio mode // (assuming automatic audio device management isn't disabled) if ( !this.disableReactNativeAutoDeviceManagement('audio') && this.isMeetingPendingOrOngoing( this._meetingState, this._isPreparingToJoin ) ) { this.nativeUtils().setAudioMode(this._nativeInCallAudioMode); } return this; } async preAuth(properties = {}) { // Validate mode. if (!this._callObjectMode) { throw new Error('preAuth() currently only supported in call object mode'); } // Validate meeting state: pre-auth is only allowed if you haven't already // joined (or aren't in the process of joining). if ( [DAILY_STATE_JOINING, DAILY_STATE_JOINED].includes(this._meetingState) ) { throw new Error('preAuth() not supported after joining a meeting'); } // Load call machine bundle, if needed. if (this.needsLoad()) { await this.load(properties); } // Assign properties, ensuring that at a minimum url is set. // Disallow changing to a url with a different bundle url than the one used // for load(). if (!properties.url) { throw new Error('preAuth() requires at least a url to be provided'); } const newBundleUrl = callObjectBundleUrl(properties.url); const loadedBundleUrl = callObjectBundleUrl( this.properties.url || this.properties.baseUrl ); if (newBundleUrl !== loadedBundleUrl) { throw new Error( `url in preAuth() has a different bundle url than the one loaded (${loadedBundleUrl} -> ${newBundleUrl})` ); } this.validateProperties(properties); this.properties = { ...this.properties, ...properties }; // Pre-auth with the server. return new Promise((resolve, reject) => { const k = (msg) => { if (msg.error) { return reject(msg.error); } if (!msg.access) { return reject(new Error('unknown error in preAuth()')); } // Set a flag indicating that we've pre-authed. // This flag has the effect of "locking in" url and token, so that they // can't be changed subsequently on join(), which would invalidate this // pre-auth. this._didPreAuth = true; resolve({ access: msg.access }); }; this.sendMessageToCallMachine( { action: DAILY_METHOD_PREAUTH, properties: makeSafeForPostMessage(this.properties), }, k ); }); } async load(properties) { if (!this.needsLoad()) { return; } if (properties) { this.validateProperties(properties); this.properties = { ...this.properties, ...properties }; } // In iframe mode, we *must* have a meeting url // (As opposed to call object mode, where a meeting url, a base url, or no // url at all are all valid here) if (!this._callObjectMode && !this.properties.url) { throw new Error( "can't load iframe meeting because url property isn't set" ); } this.updateMeetingState(DAILY_STATE_LOADING); try { this.emit(DAILY_EVENT_LOADING, { action: DAILY_EVENT_LOADING }); } catch (e) { console.log("could not emit 'loading'", e); } if (this._callObjectMode) { // non-iframe, callObjectMode return new Promise((resolve, reject) => { this._callObjectLoader.cancel(); this._callObjectLoader.load( this.properties.url || this.properties.baseUrl, this._callFrameId, (wasNoOp) => { this.updateMeetingState(DAILY_STATE_LOADED); // Only need to emit event if load was a no-op, since the loaded // bundle won't be emitting it if it's not executed again wasNoOp && this.emit(DAILY_EVENT_LOADED, { action: DAILY_EVENT_LOADED }); resolve(); }, (errorMsg, willRetry) => { this.emit(DAILY_EVENT_LOAD_ATTEMPT_FAILED, { action: DAILY_EVENT_LOAD_ATTEMPT_FAILED, errorMsg, }); if (!willRetry) { this.updateMeetingState(DAILY_STATE_ERROR); this.resetMeetingDependentVars(); this.emit(DAILY_EVENT_ERROR, { action: DAILY_EVENT_ERROR, errorMsg, }); reject(errorMsg); } } ); }); } else { // iframe this._iframe.src = this.assembleMeetingUrl(); return new Promise((resolve, reject) => { this._loadedCallback = (error) => { if (this._meetingState === DAILY_STATE_ERROR) { reject(error); return; } this.updateMeetingState(DAILY_STATE_LOADED); if (this.properties.cssFile || this.properti