@daily-co/daily-js
Version:
**🚨Our docs have moved! 🚨**
3,399 lines • 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.properties.cssText) {
this.loadCss(this.properties);
}
for (let eventName in this._inputEventsOn) {
this.sendMessageToCallMachine({
action: DAILY_METHOD_REGISTER_INPUT_HANDLER,
on: eventName,
});
}
resolve();
};
});
}
}
async join(properties = {}) {
let newCss = false;
if (this.needsLoad()) {
this.updateIsPreparingToJoin(true);
try {
await this.load(properties);
} catch (e) {
this.updateIsPreparingToJoin(false);
return Promise.reject(e);
}
} else {
newCss = !!(this.properties.cssFile || this.properties.cssText);
// Validate that any provided url or token doesn't conflict with url or
// token already used to preAuth()
if (this._didPreAuth) {
if (properties.url && properties.url !== this.properties.url) {
console.error(
`url in join() is different than the one used in preAuth()`
);
this.updateIsPreparingToJoin(false);
return Promise.reject();
}
if (properties.token && properties.token !== this.properties.token) {
console.error(
`token in join() is different than the one used in preAuth()`
);
this.updateIsPreparingToJoin(false);
return Promise.reject();
}
}
// Validate that url we're using to join() doesn't conflict with the url
// we used to load()
if (properties.url) {
if (this._callObjectMode) {
const newBundleUrl = callObjectBundleUrl(properties.url);
const loadedBundleUrl = callObjectBundleUrl(
this.properties.url || this.properties.baseUrl
);
if (newBundleUrl !== loadedBundleUrl) {
console.error(
`url in join() has a different bundle url than the one loaded (${loadedBundleUrl} -> ${newBundleUrl})`
);
this.updateIsPreparingToJoin(false);
return Promise.reject();
}
this.properties.url = properties.url;
} else {
// iframe mode
if (properties.url && properties.url !== this.properties.url) {
console.error(
`url in join() is different than the one used in load() (${this.properties.url} -> ${properties.url})`
);
this.updateIsPreparingToJoin(false);
return Promise.reject();
}
}
}
// Validate and assign properties to this.properties, for use by call
// machine
this.validateProperties(properties);
this.properties = { ...this.properties, ...properties };
}
// only update if showLocalVideo/showParticipantsBar are being explicitly set
if (properties.showLocalVideo !== undefined) {
if (this._callObjectMode) {
console.error('showLocalVideo is not available in callObject mode');
} else {
this._showLocalVideo = !!properties.showLocalVideo;
}
}
if (properties.showParticipantsBar !== undefined) {
if (this._callObjectMode) {
console.error(
'showParticipantsBar is not available in callObject mode'
);
} else {
this._showParticipantsBar = !!properties.showParticipantsBar;
}
}
if (
this._meetingState === DAILY_STATE_JOINED ||
this._meetingState === DAILY_STATE_JOINING
) {
console.warn('already joined meeting, call leave() before joining again');
this.updateIsPreparingToJoin(false);
return;
}
this.updateMeetingState(DAILY_STATE_JOINING, false);
try {
this.emit(DAILY_EVENT_JOINING_MEETING, {
action: DAILY_EVENT_JOINING_MEETING,
});
} catch (e) {
console.log("could not emit 'joining-meeting'", e);
}
this.sendMessageToCallMachine({
action: DAILY_METHOD_JOIN,
properties: makeSafeForPostMessage(this.properties),
preloadCache: makeSafeForPostMessage(this._preloadCache),
});
return new Promise((resolve, reject) => {
this._joinedCallback = (participants, error) => {
if (this._meetingState === DAILY_STATE_ERROR) {
reject(error);
return;
}
this.updateMeetingState(DAILY_STATE_JOINED);
if (participants) {
for (var id in participants) {
if (this._callObjectMode) {
Participant.addTracks(participants[id]);
Participant.addCustomTracks(participants[id]);
Participant.addLegacyTracks(
participants[id],
this._participants[id]
);
}
this._participants[id] = { ...participants[id] };
this.toggleParticipantAudioBasedOnNativeAudioFocus();
}
}
if (newCss) {
this.loadCss(this.properties);
}
resolve(participants);
};
});
}
async leave() {
return new Promise((resolve, _) => {
if (this._callObjectLoader && !this._callObjectLoader.loaded) {
// If call object bundle never successfully loaded, cancel load if
// needed and clean up state immediately (without waiting for call
// machine to clean up its state).
this._callObjectLoader.cancel();
this.updateMeetingState(DAILY_STATE_LEFT);
this.resetMeetingDependentVars();
try {
this.emit(DAILY_STATE_LEFT, { action: DAILY_STATE_LEFT });
} catch (e) {
console.log("could not emit 'left-meeting'", e);
}
resolve();
} else if (
this._meetingState === DAILY_STATE_LEFT ||
this._meetingState === DAILY_STATE_ERROR
) {
// nothing to do, here, just resolve
resolve();
} else {
// TODO: the possibility that the iframe call machine is not yet loaded
// is never handled here...
this.sendMessageToCallMachine({ action: DAILY_METHOD_LEAVE }, () => {
resolve();
});
}
});
}
startScreenShare(captureOptions = {}) {
methodNotSupportedInReactNative();
if (captureOptions.mediaStream) {
this._preloadCache.screenMediaStream = captureOptions.mediaStream;
captureOptions.mediaStream = DAILY_CUSTOM_TRACK;
}
this.sendMessageToCallMachine({
action: DAILY_METHOD_START_SCREENSHARE,
captureOptions,
});
}
stopScreenShare() {
methodNotSupportedInReactNative();
this.sendMessageToCallMachine({ action: DAILY_METHOD_STOP_SCREENSHARE });
}
startRecording(args = {}) {
// TODO: Should we allow starting a cloud-mp4 recording in RN?
methodNotSupportedInReactNative();
this.sendMessageToCallMachine({
action: DAILY_METHOD_START_RECORDING,
...args,
});
}
updateRecording({ layout = { preset: 'default' } }) {
this.sendMessageToCallMachine({
action: DAILY_METHOD_UPDATE_RECORDING,
layout,
});
}
stopRecording() {
methodNotSupportedInReactNative();
this.sendMessageToCallMachine({ action: DAILY_METHOD_STOP_RECORDING });
}
startLiveStreaming(args = {}) {
this.sendMessageToCallMachine({
action: DAILY_METHOD_START_LIVE_STREAMING,
...args,
});
}
updateLiveStreaming({ layout = { preset: 'default' } }) {
this.sendMessageToCallMachine({
action: DAILY_METHOD_UPDATE_LIVE_STREAMING,
layout,
});
}
stopLiveStreaming() {
this.sendMessageToCallMachine({ action: DAILY_METHOD_STOP_LIVE_STREAMING });
}
startTranscription() {
this.sendMessageToCallMachine({ action: DAILY_METHOD_START_TRANSCRIPTION });
}
stopTranscription() {
this.sendMessageToCallMachine({ action: DAILY_METHOD_STOP_TRANSCRIPTION });
}
getNetworkStats() {
if (this._meetingState !== DAILY_STATE_JOINED) {
let stats = { latest: {} };
return { stats };
}
return new Promise((resolve, _) => {
let k = (msg) => {
resolve({ stats: msg.stats, ...this._network });
};
this.sendMessageToCallMachine({ action: DAILY_METHOD_GET_CALC_STATS }, k);
});
}
getActiveSpeaker() {
methodNotSupportedInReactNative();
return this._activeSpeaker;
}
setActiveSpeakerMode(enabled) {
methodNotSupportedInReactNative();
this.sendMessageToCallMachine({
action: DAILY_METHOD_SET_ACTIVE_SPEAKER_MODE,
enabled,
});
return this;
}
activeSpeakerMode() {
methodNotSupportedInReactNative();
return this._activeSpeakerMode;
}
subscribeToTracksAutomatically() {
return this._preloadCache.subscribeToTracksAutomatically;
}
setSubscribeToTracksAutomatically(enabled) {
if (this._meetingState !== DAILY_STATE_JOINED) {
throw new Error(
'setSubscribeToTracksAutomatically() is only allowed when joined'
);
}
this._preloadCache.subscribeToTracksAutomatically = enabled;
this.sendMessageToCallMachine({
action: DAILY_METHOD_SET_SUBSCRIBE_TO_TRACKS_AUTOMATICALLY,
enabled,
});
return this;
}
async enumerateDevices() {
methodNotSupportedInReactNative();
if (this._callObjectMode) {
let raw = await navigator.mediaDevices.enumerateDevices();
return { devices: raw.map((d) => JSON.parse(JSON.stringify(d))) };
}
return new Promise((resolve, _) => {
let k = (msg) => {
resolve({ devices: msg.devices });
};
this.sendMessageToCallMachine(
{ action: DAILY_METHOD_ENUMERATE_DEVICES },
k
);
});
}
sendAppMessage(data, to = '*') {
if (JSON.stringify(data).length > MAX_APP_MSG_SIZE) {
throw new Error(
'Message data too large. Max size is ' + MAX_APP_MSG_SIZE
);
}
this.sendMessageToCallMachine({ action: DAILY_METHOD_APP_MSG, data, to });
return this;
}
addFakeParticipant(args) {
methodNotSupportedInReactNative();
this.sendMessageToCallMachine({
action: DAILY_METHOD_ADD_FAKE_PARTICIPANT,
...args,
});
return this;
}
setShowNamesMode(mode) {
methodNotSupportedInReactNative();
if (mode && !(mode === 'always' || mode === 'never')) {
console.error(
'setShowNamesMode argument should be "always", "never", or false'
);
return this;
}
this.sendMessageToCallMachine({
action: DAILY_METHOD_SET_SHOW_NAMES,
mode: mode,
});
return this;
}
setShowLocalVideo(show = true) {
methodNotSupportedInReactNative();
if (typeof show !== 'boolean') {
console.error('setShowLocalVideo only accepts a boolean value');
return this;
}
if (this._callObjectMode) {
console.error('setShowLocalVideo is not available in callObject mode');
return this;
}
if (this._meetingState !== DAILY_STATE_JOINED) {
console.error(
'the meeting must be joined before calling setShowLocalVideo'
);
return this;
}
this.sendMessageToCallMachine({
action: DAILY_METHOD_SET_SHOW_LOCAL_VIDEO,
show,
});
this._showLocalVideo = show;
return this;
}
showLocalVideo() {
methodNotSupportedInReactNative();
if (this._callObjectMode) {
console.error('showLocalVideo is not available in callObject mode');
return this;
}
return this._showLocalVideo;
}
setShowParticipantsBar(show = true) {
methodNotSupportedInReactNative();
if (typeof show !== 'boolean') {
console.error('setShowParticipantsBar only accepts a boolean value');
return this;
}
if (this._callObjectMode) {
console.error(
'setShowParticipantsBar is not available in callObject mode'
);
return this;
}
if (this._meetingState !== DAILY_STATE_JOINED) {
console.error(
'the meeting must be joined before calling setShowParticipantsBar'
);
return this;
}
this.sendMessageToCallMachine({
action: DAILY_METHOD_SET_SHOW_PARTICIPANTS_BAR,
show,
});
this._showParticipantsBar = show;
return this;
}
showParticipantsBar() {
methodNotSupportedInReactNative();
if (this._callObjectMode) {
console.error('showParticipantsBar is not available in callObject mode');
return this;
}
return this._showParticipantsBar;
}
theme() {
if (this._callObjectMode) {
console.error('theme is not available in callObject mode');
return this;
}
return this.properties.theme;
}
setTheme(theme) {
return new Promise((resolve, reject) => {
if (this._callObjectMode) {
reject('setTheme is not available in callObject mode');
return;
}
try {
this.validateProperties({
theme,
});
this.properties.theme = {
...theme,
};
// Send message to Prebuilt UI Iframe driver
this.sendMessageToCallMachine({
action: DAILY_METHOD_SET_THEME,
theme: this.properties.theme,
});
/**
* For simplicity, emitting theme-updated here rather than
* listening for it from Prebuilt & re-emitting it, since:
* - we've fully validated the theme, so there's no risk of it not being applied
* - we set `this.properties.theme` first, so in a customer's `theme-updated`
* handler, a call to `theme()` will return the latest value
* - this method is the only way `theme-updated` can change
*/
try {
this.emit(DAILY_EVENT_THEME_UPDATED, {
action: DAILY_EVENT_THEME_UPDATED,
theme: this.properties.theme,
});
} catch (e) {
console.log("could not emit 'theme-updated'", e);
}
resolve(this.properties.theme);
} catch (e) {
reject(e);
}
});
}
detectAllFaces() {
methodNotSupportedInReactNative();
return new Promise((resolve, _) => {
let k = (msg) => {
delete msg.action;
delete msg.callbackStamp;
resolve(msg);
};
this.sendMessageToCallMachine(
{ action: DAILY_METHOD_DETECT_ALL_FACES },
k
);
});
}
async requestFullscreen() {
methodNotSupportedInReactNative();
if (!this._iframe || document.fullscreenElement) {
return;
}
try {
(await this._iframe.requestFullscreen)
? this._iframe.requestFullscreen()
: this._iframe.webkitRequestFullscreen();
} catch (e) {
console.log('could not make video call fullscreen', e);
}
}
exitFullscreen() {
methodNotSupportedInReactNative();
if (document.fullscreenElement) {
document.exitFullscreen();
} else if (document.webkitFullscreenElement) {
document.webkitExitFullscreen();
}
}
async room({ includeRoomConfigDefaults = true } = {}) {
if (this._meetingState === DAILY_STATE_JOINED || this._didPreAuth) {
// We've succesfully join()ed or preAuth()ed, so we should have room info.
return new Promise((resolve, _) => {
let k = (msg) => {
delete msg.action;
delete msg.callbackStamp;
resolve(msg);
};
this.sendMessageToCallMachine(
{ action: DAILY_METHOD_ROOM, includeRoomConfigDefaults },
k
);
});
} else {
// Return the URL of the room we'll be in if/when we successfully join(),
// since we have no other room info to show yet.
if (this.properties.url) {
// NOTE: technically this should be called "roomUrlPendingJoinOrPreauth"
// to indicate that *either* a join() or a preAuth() will allow you to
// access room info, but preAuth() was added later and this name was
// preserved to maintain backward compatibility: if a consumer hasn't
// updated their app to use preAuth(), they'll be none the wiser.
return { roomUrlPendingJoin: this.properties.url };
}
return null;
}
}
async geo() {
return new Promise(async (resolve, _) => {
try {
let url = 'https://gs.daily.co/_ks_/x-swsl/:';
let res = await fetch(url);
let data = await res.json();
resolve({ current: data.geo });
} catch (e) {
console.error('geo lookup failed', e);
resolve({ current: '' });
}
});
}
async setNetworkTopology(opts) {
methodNotSupportedInReactNative();
return new Promise(async (resolve, reject) => {
let k = (msg) => {
if (msg.error) {
reject({ error: msg.error });
} else {
resolve({ workerId: msg.workerId });
}
};
this.sendMessageToCallMachine(
{ action: DAILY_METHOD_SET_NETWORK_TOPOLOGY, opts },
k
);
});
}
async getNetworkTopology() {
return new Promise(async (resolve, reject) => {
let k = (msg) => {
if (msg.error) {
reject({ error: msg.error });
} else {
resolve({ topology: msg.topology });
}
};
this.sendMessageToCallMachine(
{ action: DAILY_METHOD_GET_NETWORK_TOPOLOGY },
k
);
});
}
setPlayNewParticipantSound(arg) {
methodNotSupportedInReactNative();
if (!(typeof arg === 'number' || arg === true || arg === false)) {
throw new Error(
`argument to setShouldPlayNewParticipantSound should be true, false, or a number, but is ${arg}`
);
}
this.sendMessageToCallMachine({ action: DAILY_METHOD_SET_PLAY_DING, arg });
}
on(eventName, k) {
this._inputEventsOn[eventName] = {};
this.sendMessageToCallMachine({
action: DAILY_METHOD_REGISTER_INPUT_HANDLER,
on: eventName,
});
return EventEmitter.prototype.on.call(this, eventName, k);
}
// todo: once is almost certainly implemented incorrectly. read the
// EventEmitter source to figure out how to do this properly. since
// overriding on/off/once are optimizations, anyway, we won't worry
// about it right now.
once(eventName, k) {
this._inputEventsOn[eventName] = {};
this.sendMessageToCallMachine({
action: DAILY_METHOD_REGISTER_INPUT_HANDLER,
on: eventName,
});
return EventEmitter.prototype.once.call(this, eventName, k);
}
off(eventName, k) {
delete this._inputEventsOn[eventName];
this.sendMessageToCallMachine({
action: DAILY_METHOD_REGISTER_INPUT_HANDLER,
off: eventName,
});
return EventEmitter.prototype.off.call(this, eventName, k);
}
//
// internal methods
//
validateProperties(properties) {
for (var k in properties) {
if (!FRAME_PROPS[k]) {
throw new Error(`unrecognized property '${k}'`);
}
if (
FRAME_PROPS[k].validate &&
!FRAME_PROPS[k].validate(properties[k], this)
) {
throw new Error(`property '${k}': ${FRAME_PROPS[k].help}`);
}
}
}
assembleMeetingUrl() {
// handle case of url with query string and without
let props = {
...this.properties,
emb: this._callFrameId,
embHref: encodeURIComponent(window.location.href),
},
firstSep = props.url.match(/\?/) ? '&' : '?',
url = props.url,
urlProps = Object.keys(FRAME_PROPS).filter(
(p) => FRAME_PROPS[p].queryString && props[p] !== undefined
);
let newQueryString = urlProps
.map((p) => `${FRAME_PROPS[p].queryString}=${props[p]}`)
.join('&');
return url + firstSep + newQueryString;
}
// Note that even if the below method returns true, load() may decide that
// there's nothing more to do (e.g. in the case that the call object has
// already been loaded once) and simply carry out the appropriate meeting
// state transition.
needsLoad() {
// NOTE: The *only* reason DAILY_STATE_LOADING is here is to preserve a bug
// that I (@kompfner) am a bit hesitant to fix until more time can be
// dedicated to doing the *right* fix. If we're in DAILY_STATE_LOADING, we
// probably *shouldn't* let you trigger another load() and get into a weird
// state, but this has been long-standing behavior. The alternative would mean
// that, if load() failed silently for some reason, you couldn't re-trigger it
// since we'd be stuck in the DAILY_STATE_LOADING state.
return [
DAILY_STATE_NEW,
DAILY_STATE_LOADING,
DAILY_STATE_LEFT,
DAILY_STATE_ERROR,
].includes(this._meetingState);
}
sendMessageToCallMachine(message, callback) {
this._messageChannel.sendMessageToCallMachine(
message,
callback,
this._iframe,
this._callFrameId
);
}
///
/// The below *packagedMessage* methods facilitate wiring up a DailyIframe
/// instance as a remote driver of another DailyIframe instance, like in the
/// new prebuilt UI case, where an "outer" callFrame controls an "inner"
/// callObject through an intermediate iframed app.
///
forwardPackagedMessageToCallMachine(msg) {
this._messageChannel.forwardPackagedMessageToCallMachine(
msg,
this._iframe,
this._callFrameId
);
}
addListenerForPackagedMessagesFromCallMachine(listener) {
return this._messageChannel.addListenerForPackagedMessagesFromCallMachine(
listener,
this._callFrameId
);
}
removeListenerForPackagedMessagesFromCallMachine(listenerId) {
this._messageChannel.removeListenerForPackagedMessagesFromCallMachine(
listenerId
);
}
handleMessageFromCallMachine(msg) {
switch (msg.action) {
case DAILY_EVENT_IFRAME_READY_FOR_LAUNCH_CONFIG:
this.sendMessageToCallMachine({
action: DAILY_EVENT_IFRAME_LAUNCH_CONFIG,
...this.properties,
});
break;
case DAILY_EVENT_LOADED:
if (this._loadedCallback) {
this._loadedCallback();
this._loadedCallback = null;
}
try {
this.emit(msg.action, msg);
} catch (e) {
console.log('could not emit', msg, e);
}
break;
case DAILY_EVENT_JOINED_MEETING:
if (this._joinedCallback) {
this._joinedCallback(msg.participants);
this._joinedCallback = null;
}
try {
this.emit(msg.action, msg);
} catch (e) {
console.log('could not emit', msg, e);
}
break;
case DAILY_EVENT_PARTICIPANT_JOINED:
case DAILY_EVENT_PARTICIPANT_UPDATED:
if (this._meetingState === DAILY_STATE_LEFT) {
return;
}
if (msg.participant && msg.participant.session_id) {
let id = msg.participant.local ? 'local' : msg.participant.session_id;
if (this._callObjectMode) {
Participant.addTracks(msg.participant);
Participant.addCustomTracks(msg.participant);
Participant.addLegacyTracks(
msg.participant,
this._participants[id]
);
}
try {
// track events
this.maybeEventTrackStopped(
this._participants[id],
msg.participant,
'audioTrack'
);
this.maybeEventTrackStopped(
this._participants[id],
msg.participant,
'videoTrack'
);
this.maybeEventTrackStopped(
this._participants[id],
msg.participant,
'screenVideoTrack'
);
this.maybeEventTrackStopped(
this._participants[id],
msg.participant,
'screenAudioTrack'
);
this.maybeEventTrackStarted(
this._participants[id],
msg.participant,
'audioTrack'
);
this.maybeEventTrackStarted(
this._participants[id],
msg.participant,
'videoTrack'
);
this.maybeEventTrackStarted(
this._participants[id],
msg.participant,
'screenVideoTrack'
);
this.maybeEventTrackStarted(
this._participants[id],
msg.participant,
'screenAudioTrack'
);
// custom tracks (presumably we'll do all tracks consistently in the
// future, refactoring the above maybeEventTrack* events)
this.maybeEventTrackStoppedForCustomTracks(
this._participants[id],
msg.participant
);
this.maybeEventTrackStartedForCustomTracks(
this._participants[id],
msg.participant
);
// recording events
this.maybeEventRecordingStopped(
this._participants[id],
msg.participant
);
this.maybeEventRecordingStarted(
this._participants[id],
msg.participant
);
} catch (e) {
console.error('track events error', e);
}
// participant joined/updated events
if (
!this.compareEqualForParticipantUpdateEvent(
msg.participant,
this._participants[id]
)
) {
this._participants[id] = { ...msg.participant };
this.toggleParticipantAudioBasedOnNativeAudioFocus();
try {
this.emit(msg.action, msg);
} catch (e) {
console.log('could not emit', msg, e);
}
}
}
break;
case DAILY_EVENT_PARTICIPANT_LEFT:
if (msg.participant && msg.participant.session_id) {
// track events
let prevP = this._participants[msg.participant.session_id];
if (prevP) {
this.maybeEventTrackStopped(prevP, null, 'audioTrack');
this.maybeEventTrackStopped(prevP, null, 'videoTrack');
this.maybeEventTrackStopped(prevP, null, 'screenVideoTrack');
this.maybeEventTrackStopped(prevP, null, 'screenAudioTrack');
this.maybeEventTrackStoppedForCustomTracks(prevP, null);
}
// delete from local cach
delete this._participants[msg.participant.session_id];
try {
this.emit(msg.action, msg);
} catch (e) {
console.log('could not emit', msg, e);
}
}
break;
case DAILY_EVENT_ACCESS_STATE_UPDATED:
let newAccessState = {
access: msg.access,
};
if (msg.awaitingAccess) {
newAccessState.awaitingAccess = msg.awaitingAccess;
}
if (!deepEqual(this._accessState, newAccessState)) {
this._accessState = newAccessState;
try {
this.emit(msg.action, msg);
} catch (e) {
console.log('could not emit', msg, e);
}
}
break;
case DAILY_EVENT_MEETING_SESSION_UPDATED:
if (msg.meetingSession) {
try {
delete msg.callFrameId;
this.emit(msg.action, msg);
} catch (e) {
console.log('could not emit', msg, e);
}
}
break;
case DAILY_EVENT_ERROR:
if (this._iframe && !msg.preserveIframe) {
this._iframe.src = '';
}
this.updateMeetingState(DAILY_STATE_ERROR);
this.resetMeetingDependentVars();
if (this._loadedCallback) {
this._loadedCallback(msg.errorMsg);
this._loadedCallback = null;
}
if (this._joinedCallback) {
this._joinedCallback(null, msg.errorMsg);
this._joinedCallback = null;
}
try {
let { preserveIframe, ...event } = msg;
this.emit(msg.action, event);
} catch (e) {
console.log('could not emit', msg, e);
}
break;
case DAILY_EVENT_LEFT_MEETING:
if (this._meetingState !== DAILY_STATE_ERROR) {
this.updateMeetingState(DAILY_STATE_LEFT);
}
this.resetMeetingDependentVars();
try {
this.emit(msg.action, msg);
} catch (e) {
console.log('could not emit', msg, e);
}
break;
case DAILY_EVENT_INPUT_EVENT:
let p = this._participants[msg.session_id];
if (!p) {
if (msg.session_id === this._participants.local.session_id) {
p = this._participants.local;
} else {
p = {};
}
}
try {
this.emit(msg.event.type, {
action: msg.event.type,
event: msg.event,
participant: { ...p },
});
} catch (e) {
console.log('could not emit', msg, e);
}
break;
case DAILY_EVENT_NETWORK_QUALITY_CHANGE:
let { threshold, quality } = msg;
if (
threshold !== this._network.threshold ||
quality !== this._network.quality
) {
this._network.quality = quality;
this._network.threshold = threshold;
try {
this.emit(msg.action, msg);
} catch (e) {
console.log('could not emit', msg, e);
}
}
break;
case DAILY_EVENT_ACTIVE_SPEAKER_CHANGE:
let { activeSpeaker } = msg;
if (this._activeSpeaker.peerId !== activeSpeaker.peerId) {
this._activeSpeaker.peerId = activeSpeaker.peerId;
try {
this.emit(msg.action, {
action: msg.action,
activeSpeaker: this._activeSpeaker,
});
} catch (e) {
console.log('could not emit', msg, e);
}
}
break;
case DAILY_EVENT_SHOW_LOCAL_VIDEO_CHANGED:
if (this._callObjectMode) return;
const { show } = msg;
this._showLocalVideo = show;
try {
this.emit(msg.action, {
action: msg.action,
show,
});
} catch (e) {
console.log('could not emit', msg, e);
}
break;
case DAILY_EVENT_ACTIVE_SPEAKER_MODE_CHANGE:
const { enabled } = msg;
if (this._activeSpeakerMode !== enabled) {
this._activeSpeakerMode = enabled;
try {
this.emit(msg.action, {
action: msg.action,
enabled: this._activeSpeakerMode,
});
} catch (e) {
console.log('could not emit', msg, e);
}
}
break;
case DAILY_EVENT_WAITING_PARTICIPANT_ADDED:
case DAILY_EVENT_WAITING_PARTICIPANT_UPDATED:
case DAILY_EVENT_WAITING_PARTICIPANT_REMOVED:
this._waitingParticipants = msg.allWaitingParticipants;
try {
this.emit(msg.action, {
action: msg.action,
participant: msg.participant,
});
} catch (e) {
console.log('could not emit', msg, e);
}
break;
case DAILY_EVENT_RECEIVE_SETTINGS_UPDATED:
// NOTE: doing equality check here rather than before sending message in
// the first place from call machine, to simplify handling initial
// receive settings
if (!deepEqual(this._receiveSettings, msg.receiveSettings)) {
this._receiveSettings = msg.receiveSettings;
try {
this.emit(msg.action, {
action: msg.action,
receiveSettings: msg.receiveSettings,
});
} catch (e) {
console.log('could not emit', msg, e);
}
}
break;
case DAILY_EVENT_INPUT_SETTINGS_UPDATED:
// NOTE: doing equality check here rather than before sending message in
// the first place from call machine, to simplify handling initial
// input settings
if (!deepEqual(this._inputSettings, msg.inputSettings)) {
this._inputSettings = msg.inputSettings;
try {
this.emit(msg.action, {
action: msg.action,
inputSettings: msg.inputSettings,
});
} catch (e) {
console.log('could not emit', msg, e);
}
}
break;
case DAILY_EVENT_RECORDING_STARTED:
case DAILY_EVENT_RECORDING_STOPPED:
case DAILY_EVENT_RECORDING_STATS:
case DAILY_EVENT_RECORDING_ERROR:
case DAILY_EVENT_RECORDING_UPLOAD_COMPLETED:
case DAILY_EVENT_TRANSCRIPTION_STARTED:
case DAILY_EVENT_TRANSCRIPTION_STOPPED:
case DAILY_EVENT_TRANSCRIPTION_ERROR:
case DAILY_EVENT_STARTED_CAMERA:
case DAILY_EVENT_CAMERA_ERROR:
case DAILY_EVENT_APP_MSG:
case DAILY_EVENT_LOCAL_SCREEN_SHARE_STARTED:
case DAILY_EVENT_LOCAL_SCREEN_SHARE_STOPPED:
case DAILY_EVENT_NETWORK_CONNECTION:
case DAILY_EVENT_RECORDING_DATA:
case DAILY_EVENT_LIVE_STREAMING_STARTED:
case DAILY_EVENT_LIVE_STREAMING_STOPPED:
case DAILY_EVENT_LIVE_STREAMING_ERROR:
case DAILY_EVENT_NONFATAL_ERROR:
case DAILY_EVENT_LANG_UPDATED:
case DAILY_EVENT_MEDIA_INGEST_ERROR:
try {
this.emit(msg.action, msg);
} catch (e) {
console.log('could not emit', msg, e);
}
break;
case DAILY_UI_REQUEST_FULLSCREEN:
this.requestFullscreen();
break;
case DAILY_UI_EXIT_FULLSCREEN:
this.exitFullscreen();
break;
default: // no op
}
}
maybeEventRecordingStopped(prevP, thisP) {
const key = 'record';
if (!prevP) {
return;
}
if (!thisP.local && thisP[key] === false && prevP[key] !== thisP[key]) {
try {
this.emit(DAILY_EVENT_RECORDING_STOPPED, {
action: DAILY_EVENT_RECORDING_STOPPED,
});
} catch (e) {
console.log('could not emit', e);
}
}
}
maybeEventRecordingStarted(prevP, thisP) {
const key = 'record';
if (!prevP) {
return;
}
if (!thisP.local && thisP[key] === true && prevP[key] !== thisP[key]) {
try {
this.emit(DAILY_EVENT_RECORDING_STARTED, {
action: DAILY_EVENT_RECORDING_STARTED,
});
} catch (e) {
console.log('could not emit', e);
}
}
}
maybeEventTrackStopped(prevP, thisP, key) {
if (!prevP) {
return;
}
if (
(prevP[key] && prevP[key].readyState === 'ended') ||
(prevP[key] && !(thisP && thisP[key])) ||
(prevP[key] && prevP[key].id !== thisP[key].id)
) {
try {
this.emit(DAILY_EVENT_TRACK_STOPPED, {
action: DAILY_EVENT_TRACK_STOPPED,
track: prevP[key],
participant: thisP,
});
} catch (e) {
console.log('could not emit', e);
}
}
}
maybeEventTrackStarted(prevP, thisP, key) {
if (
(thisP[key] && !(prevP && prevP[key])) ||
(thisP[key] && prevP[key].readyState === 'ended') ||
(thisP[key] && thisP[key].id !== prevP[key].id)
) {
try {
this.emit(DAILY_EVENT_TRACK_STARTED, {
action: DAILY_EVENT_TRACK_STARTED,
track: thisP[key],
participant: thisP,
});
} catch (e) {
console.log('could not emit', e);
}
}
}
maybeEventTrackStoppedForCustomTracks(prevP, thisP) {
if (!prevP) {
return;
}
for (const trackKey in prevP.tracks) {
// we might be able to use this logic for all tracks, not just additional,
// non-standard tracks. but for now, we'll only handle the non-standard
// tracks
if (Participant.isPredefinedTrack(trackKey)) {
continue;
}
this.maybeEventTrackStopped(
prevP.tracks[trackKey],
thisP ? thisP.tracks[trackKey] : null,
'track'
);
}
}
maybeEventTrackStartedForCustomTracks(prevP, thisP) {
if (!thisP) {
return;
}
for (const trackKey in thisP.tracks) {
// we might be able to use this logic for all tracks, not just additional,
// non-standard tracks. but for now, we'll only handle the non-standard
// tracks
if (Participant.isPredefinedTrack(trackKey)) {
continue;
}
this.maybeEventTrackStarted(
prevP ? prevP.tracks[trackKey] : null,
thisP.tracks[trackKey],
'track'
);
}
}
compareEqualForParticipantUpdateEvent(a, b) {
if (!deepEqual(a, b)) {
return false;
}
if (
a.videoTrack &&
b.videoTrack &&
(a.videoTrack.id !== b.videoTrack.id ||
a.videoTrack.muted !== b.videoTrack.muted ||
a.videoTrack.enabled !== b.videoTrack.enabled)
) {
return false;
}
if (
a.audioTrack &&
b.audioTrack &&
(a.audioTrack.id !== b.audioTrack.id ||
a.audioTrack.muted !== b.audioTrack.muted ||
a.audioTrack.enabled !== b.audioTrack.enabled)
) {
return false;
}
return true;
}
nativeUtils() {
if (!isReactNative()) {
return null;
}
if (typeof DailyNativeUtils === 'undefined') {
console.warn(
'in React Native, DailyNativeUtils is expected to be available'
);
return null;
}
return DailyNativeUtils;
}
updateIsPreparingToJoin(isPreparingToJoin) {
this.updateMeetingState(this._meetingState, isPreparingToJoin);
}
updateMeetingState(
meetingState,
isPreparingToJoin = this._isPreparingToJoin
) {
// If state hasn't changed, bail
if (
meetingState === this._meetingState &&
isPreparingToJoin === this._isPreparingToJoin
) {
return;
}
// Update state
const oldMeetingState = this._meetingState;
const oldIsPreparingToJoin = this._isPreparingToJoin;
this._meetingState = meetingState;
this._isPreparingToJoin = isPreparingToJoin;
// Update state side-effects (which, for now, all depend on whether
// isMeetingPendingOrOngoing)
const oldIsMeetingPendingOrOngoing = this.isMeetingPendingOrOngoing(
oldMeetingState,
oldIsPreparingToJoin
);
const isMeetingPendingOrOngoing = this.isMeetingPendingOrOngoing(
this._meetingState,
this._isPreparingToJoin
);
if (oldIsMeetingPendingOrOngoing === isMeetingPendingOrOngoing) {
return;
}
this.updateKeepDeviceAwake(isMeetingPendingOrOngoing);
this.updateDeviceAudioMode(isMeetingPendingOrOngoing);
this.updateShowAndroidOngoingMeetingNotification(isMeetingPendingOrOngoing);
this.updateNoOpRecordingEnsuringBackgroundContinuity(
isMeetingPendingOrOngoing
);
}
// To be invoked this when leaving or erroring out of a meeting.
// NOTE (Paul, 2021-01-07): this could probably be expanded to reset *all*
// meeting-dependent vars, but starting with this targeted small set which
// were being reset properly on leave() but not when leaving via prebuilt ui.
resetMeetingDependentVars() {
this._participants = {};
this._waitingParticipants = {};
this._activeSpeaker = {};
this._activeSpeakerMode = false;
this._didPreAuth = false;
this._accessState = { access: DAILY_ACCESS_UNKNOWN };
this._receiveSettings = {};
this._inputSettings = {};
resetPreloadCache(this._preloadCache);
}
updateKeepDeviceAwake(keepAwake) {
if (!isReactNative()) {
return;
}
this.nativeUtils().setKeepDeviceAwake(keepAwake, this._callFrameId);
}
updateDeviceAudioMode(useInCallAudioMode) {
if (
!isReactNative() ||
this.disableReactNativeAutoDeviceManagement('audio')
) {
return;
}
const audioMode = useInCallAudioMode
? this._nativeInCallAudioMode
: NATIVE_AUDIO_MODE_IDLE;
this.nativeUtils().setAudioMode(audioMode);
}
// Note: notification properties can't be changed while it is ongoing
updateShowAndroidOngoingMeetingNotification(showNotification) {
// Check that we're React Native and that the Android-only method exists
if (
!(isReactNative() && this.nativeUtils().setShowOngoingMeetingNotification)
) {
return;
}
// Use current this.properties to customize notification behavior
let title, subtitle, iconName, disableForCustomOverride;
if (
this.properties.reactNativeConfig &&
this.properties.reactNativeConfig.androidInCallNotification
) {
({
title,
subtitle,
iconName,
disableForCustomOverride,
} = this.properties.reactNativeConfig.androidInCallNotification);
}
if (disableForCustomOverride) {
showNotification = false;
}
this.nativeUtils().setShowOngoingMeetingNotification(
showNotification,
title,
subtitle,
iconName,
this._callFrameId
);
}
// Whether to enable no-op audio recording to ensure continuity of the app
// when backgrounded. Required in iOS to ensure we can finish joining when the
// app is backgrounded before gUM is called, and to ensure that signaling
// remains connected when we're in an empty room and our own cam and mic are
// off.
updateNoOpRecordingEnsuringBackgroundContinuity(enableNoOpRecording) {
if (
!(
isReactNative() &&
this.nativeUtils().enableNoOpRecordingEnsuringBackgroundContinuity
)
) {
return;
}
this.nativeUtils().enableNoOpRecordingEnsuringBackgroundContinuity(
enableNoOpRecording
);
}
isMeetingPendingOrOngoing(meetingState, isPreparingToJoin) {
return (
[DAILY_STATE_JOINING, DAILY_STATE_JOINED].includes(meetingState) ||
isPreparingToJoin
);
}
handleNativeAppActiveStateChange = (isActive) => {
// If automatic video device management is disabled, bail
if (this.disableReactNativeAutoDeviceManagement('video')) {
return;
}
if (isActive) {
// If cam was unmuted before losing focus, unmute
// (Note this is assumption is not perfect, since theoretically an app
// could unmute while in the background, but it's decent for now)
if (this.camUnmutedBeforeLosingNativeActiveState) {
this.setLocalVideo(true);
}
} else {
this.camUnmutedBeforeLosingNativeActiveState = this.localVideo();
// Mute cam, but check first whether we have local video in the first
// place: if we don't, we may still be in the gUM process, with the app
// "inactive" simply because it's behind the permissions dialogs.
if (this.camUnmutedBeforeLosingNativeActiveState) {
this.setLocalVideo(false);
}
}
};
handleNativeAudioFocusChange = (hasFocus) => {
// If automatic audio device management is disabled, bail
if (this.disableReactNativeAutoDeviceManagement('audio')) {
return;
}
this._hasNativeAudioFocus = hasFocus;
// toggle participant audio if needed
this.toggleParticipantAudioBasedOnNativeAudioFocus();
// toggle mic mute if needed
if (this._hasNativeAudioFocus) {
// If mic was unmuted before losing focus, unmute
// (Note this is assumption is not perfect, since theoretically an app
// could unmute while in the background, but it's decent for now)
if (this.micUnmutedBeforeLosingNativeAudioFocus) {
this.setLocalAudio(true);
}
} else {
this.micUnmutedBeforeLosingNativeAudioFocus = this.localAudio();
this.setLocalAudio(false);
}
};
toggleParticipantAudioBasedOnNativeAudioFocus() {
if (!isReactNative()) {
return;
}
// Need to access store directly since when participant muted their audio we
// don't have access to their audio tracks in this._participants
const state = store.getState();
for (const streamId in state.streams) {
const streamData = state.streams[streamId];
if (
streamData &&
streamData.pendingTrack &&
streamData.pendingTrack.kind === 'audio'
) {
streamData.pendingTrack.enabled = this._hasNativeAudioFocus;
}
}
}
// type must be either 'audio' or 'video'
disableReactNativeAutoDeviceManagement(type) {
return (
this.properties.reactNativeConfig &&
this.properties.reactNativeConfig.disableAutoDeviceManagement &&
this.properties.reactNativeConfig.disableAutoDeviceManagement[type]
);
}
absoluteUrl(url) {
if ('undefined' === typeof url) {
return undefined;
}
let a = document.createElement('a');
a.href = url;
return a.href;
}
sayHello() {
const str = 'hello, world.';
console.log(str);
return str;
}
}
function initializePreloadCache(callObject, properties) {
return {
subscribeToTracksAutomatically: true,
audioDeviceId: null,
videoDeviceId: null,
outputDeviceId: null,
};
}
function resetPreloadCache(c) {
// don't need to do anything, until we add stuff to the preload
// cache that should not persist
}
function makeSafeForPostMessage(props) {
const safe = {};
for (let p in props) {
if (props[p] instanceof MediaStreamTrack) {
// note: could store the track in a global variable for accessing
// on the other side of the postMessage, here, instead of as we
// currently do in the validate-properties routines, which definitely
// is a spooky-action-at-a-distance code anti-pattern
safe[p] = DAILY_CUSTOM_TRACK;
} else if (p === 'dailyConfig') {
if (props[p].modifyLocalSdpHook) {
if (window._dailyConfig) {
window._dailyConfig.modifyLocalSdpHook = props[p].modifyLocalSdpHook;
}
delete props[p].modifyLocalSdpHook;
}
if (props[p].modifyRemoteSdpHook) {
if (window._dailyConfig) {
window._dailyConfig.modifyRemoteSdpHook =
props[p].modifyRemoteSdpHook;
}
delete props[p].modifyRemoteSdpHook;
}
safe[p] = props[p];
} else {
safe[p] = props[p];
}
}
return safe;
}
function methodNotSupportedInReactNative() {
if (isReactNative()) {
throw new Error(
'This daily-js method is not currently supported in React Native'
);
}
}
function methodOnlySupportedInReactNative() {
if (!isReactNative()) {
throw new Error('This daily-js method is only supported in React Native');
}
}
function validateReceiveSettings(
receiveSettingsParam,
{ allowAllParticipantsKey }
) {
const isParticipantIdValid = (participantId) => {
const disallowedKeys = ['local'];
if (!allowAllParticipantsKey) disallowedKeys.push('*');
return participantId && !disallowedKeys.includes(participantId);
};
const areVideoReceiveSettingsValid = (videoReceiveSettings) => {
if (videoReceiveSettings.layer !== undefined) {
if (
!(
(Number.isInteger(videoReceiveSettings.layer) &&
videoReceiveSettings.layer >= 0) ||
videoReceiveSettings.layer === 'inherit'
)
) {
return false;
}
}
return true;
};
// NOTE: partial receive settings *are* allowed, in both senses:
// - only a subset of media types (e.g. only "video")
// - only a subset of settings per media type (e.g. only "layer")
const areParticipantReceiveSettingsValid = (receiveSettings) => {
if (!receiveSettings) return false;
if (receiveSettings.video) {
if (!areVideoReceiveSettingsValid(receiveSettings.video)) {
return false;
}
}
if (receiveSettings.screenVideo) {
if (!areVideoReceiveSettingsValid(receiveSettings.screenVideo)) {
return false;
}
}
return true;
};
for (const [participantId, receiveSettings] of Object.entries(
receiveSettingsParam
)) {
if (
!(
isParticipantIdValid(participantId) &&
areParticipantReceiveSettingsValid(receiveSettings)
)
) {
return false;
}
}
return true;
}
// Since currently videoProcessor is the only inputSetting. I wrote this code to reject
// everything else. I feel it is the safe approach. This will need changes as more
// functionality is added to inputSettings in the future.
function validateInputSettings(settings) {
if (typeof settings !== 'object') return false;
if (!(settings.video && typeof settings.video === 'object')) return false;
if (!validateVideoProcessor(settings.video.processor)) return false;
return true;
}
function validateVideoProcessor(p) {
if (!p) return false;
if (typeof p !== 'object') return false;
if (Object.keys(p).length === 0) return false; // lodash isEmpty did not work well with github workflow for some reason
if (p.type && !validateVideoProcessorType(p.type)) return false;
if (p.publish !== undefined && typeof p.publish !== 'boolean') return false;
if (p.config) {
if (typeof p.config !== 'object') return false;
if (!validateVideoProcessorConfig(p.type, p.config)) return false;
}
return true;
}
function validateVideoProcessorConfig(type, config) {
let keys = Object.keys(config);
if (keys.length === 0) return true;
const configErrMsg =
'invalid object in inputSettings -> video -> processor -> config';
switch (type) {
case VIDEO_PROCESSOR_TYPES.BGBLUR:
if (keys.length > 1 || keys[0] !== 'strength') {
throw new Error(configErrMsg);
}
if (
typeof config.strength !== 'number' ||
config.strength <= 0 ||
config.strength > 1 ||
isNaN(config.strength)
) {
throw new Error(
`${configErrMsg}; expected: {0 < strength <= 1}, got: ${config.strength}`
);
}
default:
return true;
}
}
function validateVideoProcessorType(type) {
if (typeof type !== 'string') return false;
return Object.values(VIDEO_PROCESSOR_TYPES).includes(type);
}
function inputSettingsValidationHelpMsg() {
let processorOpts = Object.values(VIDEO_PROCESSOR_TYPES).join(' | ');
return `inputSettings must be of the form: { video: { processor: [ ${processorOpts} ] }, publish?: boolean, config?: {} }`;
}
function receiveSettingsValidationHelpMsg({ allowAllParticipantsKey }) {
return (
`receiveSettings must be of the form { [<remote participant id> | ${DAILY_RECEIVE_SETTINGS_BASE_KEY}${
allowAllParticipantsKey
? ` | "${DAILY_RECEIVE_SETTINGS_ALL_PARTICIPANTS_KEY}"`
: ''
}]: ` +
'{ ' +
'[video: [{ layer: [<non-negative integer> | "inherit"] } | "inherit"]], ' +
'[screenVideo: [{ layer: [<non-negative integer> | "inherit"] } | "inherit"]] ' +
'}}}'
);
}
function validateReactNativeConfig(config) {
return validateConfigPropType(config, reactNativeConfigType);
}
function validateConfigPropType(prop, propType) {
if (propType === undefined) {
return false;
}
switch (typeof propType) {
case 'string':
return typeof prop === propType;
case 'object':
if (typeof prop !== 'object') {
return false;
}
for (const key in prop) {
if (!validateConfigPropType(prop[key], propType[key])) {
return false;
}
}
return true;
default:
// console.error(
// "Internal programming error: we've defined our config prop types wrong"
// );
return false;
}
}