@daily-co/daily-js
Version:
**🚨Our docs have moved! 🚨**
1,736 lines (1,615 loc) • 101 kB
JavaScript
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