UNPKG

101 kBJavaScriptView Raw
1import EventEmitter from 'events';
2import { deepEqual } from 'fast-equals';
3import Bowser from 'bowser';
4
5import {
6 // re-export
7 //
8 // meeting states
9 DAILY_STATE_NEW,
10 DAILY_STATE_LOADING,
11 DAILY_STATE_LOADED,
12 DAILY_STATE_JOINING,
13 DAILY_STATE_JOINED,
14 DAILY_STATE_LEFT,
15 DAILY_STATE_ERROR,
16 // track states
17 DAILY_TRACK_STATE_BLOCKED,
18 DAILY_TRACK_STATE_OFF,
19 DAILY_TRACK_STATE_SENDABLE,
20 DAILY_TRACK_STATE_LOADING,
21 DAILY_TRACK_STATE_INTERRUPTED,
22 DAILY_TRACK_STATE_PLAYABLE,
23 // meeting access
24 DAILY_ACCESS_UNKNOWN,
25 DAILY_ACCESS_LEVEL_FULL,
26 DAILY_ACCESS_LEVEL_LOBBY,
27 DAILY_ACCESS_LEVEL_NONE,
28 // receive settings
29 DAILY_RECEIVE_SETTINGS_BASE_KEY,
30 DAILY_RECEIVE_SETTINGS_ALL_PARTICIPANTS_KEY,
31 // error types
32 DAILY_FATAL_ERROR_EJECTED,
33 DAILY_FATAL_ERROR_NBF_ROOM,
34 DAILY_FATAL_ERROR_NBF_TOKEN,
35 DAILY_FATAL_ERROR_EXP_ROOM,
36 DAILY_FATAL_ERROR_EXP_TOKEN,
37 DAILY_CAMERA_ERROR_CAM_IN_USE,
38 DAILY_CAMERA_ERROR_MIC_IN_USE,
39 DAILY_CAMERA_ERROR_CAM_AND_MIC_IN_USE,
40 // events
41 DAILY_EVENT_IFRAME_READY_FOR_LAUNCH_CONFIG,
42 DAILY_EVENT_IFRAME_LAUNCH_CONFIG,
43 DAILY_EVENT_THEME_UPDATED,
44 DAILY_EVENT_LOADING,
45 DAILY_EVENT_LOADED,
46 DAILY_EVENT_LOAD_ATTEMPT_FAILED,
47 DAILY_EVENT_STARTED_CAMERA,
48 DAILY_EVENT_CAMERA_ERROR,
49 DAILY_EVENT_JOINING_MEETING,
50 DAILY_EVENT_JOINED_MEETING,
51 DAILY_EVENT_LEFT_MEETING,
52 DAILY_EVENT_PARTICIPANT_JOINED,
53 DAILY_EVENT_PARTICIPANT_UPDATED,
54 DAILY_EVENT_PARTICIPANT_LEFT,
55 DAILY_EVENT_TRACK_STARTED,
56 DAILY_EVENT_TRACK_STOPPED,
57 DAILY_EVENT_RECORDING_STARTED,
58 DAILY_EVENT_RECORDING_STOPPED,
59 DAILY_EVENT_TRANSCRIPTION_STARTED,
60 DAILY_EVENT_TRANSCRIPTION_STOPPED,
61 DAILY_EVENT_TRANSCRIPTION_ERROR,
62 DAILY_EVENT_RECORDING_STATS,
63 DAILY_EVENT_RECORDING_ERROR,
64 DAILY_EVENT_RECORDING_UPLOAD_COMPLETED,
65 DAILY_EVENT_ERROR,
66 DAILY_EVENT_APP_MSG,
67 DAILY_EVENT_INPUT_EVENT,
68 DAILY_EVENT_LOCAL_SCREEN_SHARE_STARTED,
69 DAILY_EVENT_LOCAL_SCREEN_SHARE_STOPPED,
70 DAILY_EVENT_NETWORK_QUALITY_CHANGE,
71 DAILY_EVENT_ACTIVE_SPEAKER_CHANGE,
72 DAILY_EVENT_ACTIVE_SPEAKER_MODE_CHANGE,
73 DAILY_EVENT_FULLSCREEN,
74 DAILY_EVENT_EXIT_FULLSCREEN,
75 DAILY_EVENT_NETWORK_CONNECTION,
76 DAILY_EVENT_RECORDING_DATA,
77 DAILY_EVENT_LIVE_STREAMING_STARTED,
78 DAILY_EVENT_LIVE_STREAMING_STOPPED,
79 DAILY_EVENT_LIVE_STREAMING_ERROR,
80 DAILY_EVENT_LANG_UPDATED,
81 DAILY_EVENT_SHOW_LOCAL_VIDEO_CHANGED,
82 DAILY_EVENT_ACCESS_STATE_UPDATED,
83 DAILY_EVENT_MEETING_SESSION_UPDATED,
84 DAILY_EVENT_WAITING_PARTICIPANT_ADDED,
85 DAILY_EVENT_WAITING_PARTICIPANT_REMOVED,
86 DAILY_EVENT_WAITING_PARTICIPANT_UPDATED,
87 DAILY_EVENT_RECEIVE_SETTINGS_UPDATED,
88 DAILY_EVENT_MEDIA_INGEST_ERROR,
89 DAILY_EVENT_INPUT_SETTINGS_UPDATED,
90 DAILY_EVENT_NONFATAL_ERROR,
91
92 // internals
93 //
94 DAILY_METHOD_SET_THEME,
95 DAILY_METHOD_START_CAMERA,
96 DAILY_METHOD_SET_INPUT_DEVICES,
97 DAILY_METHOD_SET_OUTPUT_DEVICE,
98 DAILY_METHOD_GET_INPUT_DEVICES,
99 DAILY_METHOD_JOIN,
100 DAILY_METHOD_LEAVE,
101 DAILY_METHOD_UPDATE_PARTICIPANT,
102 DAILY_METHOD_UPDATE_PARTICIPANTS,
103 DAILY_METHOD_LOCAL_AUDIO,
104 DAILY_METHOD_LOCAL_VIDEO,
105 DAILY_METHOD_START_SCREENSHARE,
106 DAILY_METHOD_STOP_SCREENSHARE,
107 DAILY_METHOD_START_RECORDING,
108 DAILY_METHOD_UPDATE_RECORDING,
109 DAILY_METHOD_STOP_RECORDING,
110 DAILY_METHOD_LOAD_CSS,
111 DAILY_METHOD_SET_BANDWIDTH,
112 DAILY_METHOD_GET_CALC_STATS,
113 DAILY_METHOD_ENUMERATE_DEVICES,
114 DAILY_METHOD_CYCLE_CAMERA,
115 DAILY_METHOD_CYCLE_MIC,
116 DAILY_METHOD_APP_MSG,
117 DAILY_METHOD_ADD_FAKE_PARTICIPANT,
118 DAILY_METHOD_SET_SHOW_NAMES,
119 DAILY_METHOD_SET_SHOW_LOCAL_VIDEO,
120 DAILY_METHOD_SET_SHOW_PARTICIPANTS_BAR,
121 DAILY_METHOD_SET_ACTIVE_SPEAKER_MODE,
122 DAILY_METHOD_GET_LANG,
123 DAILY_METHOD_SET_LANG,
124 DAILY_METHOD_GET_MEETING_SESSION,
125 MAX_APP_MSG_SIZE,
126 DAILY_METHOD_REGISTER_INPUT_HANDLER,
127 DAILY_METHOD_DETECT_ALL_FACES,
128 DAILY_METHOD_ROOM,
129 DAILY_METHOD_GET_NETWORK_TOPOLOGY,
130 DAILY_METHOD_SET_NETWORK_TOPOLOGY,
131 DAILY_METHOD_SET_PLAY_DING,
132 DAILY_METHOD_SET_SUBSCRIBE_TO_TRACKS_AUTOMATICALLY,
133 DAILY_METHOD_START_LIVE_STREAMING,
134 DAILY_METHOD_UPDATE_LIVE_STREAMING,
135 DAILY_METHOD_STOP_LIVE_STREAMING,
136 DAILY_METHOD_START_TRANSCRIPTION,
137 DAILY_METHOD_STOP_TRANSCRIPTION,
138 DAILY_CUSTOM_TRACK,
139 DAILY_UI_REQUEST_FULLSCREEN,
140 DAILY_UI_EXIT_FULLSCREEN,
141 DAILY_METHOD_GET_CAMERA_FACING_MODE,
142 DAILY_METHOD_SET_USER_NAME,
143 DAILY_METHOD_PREAUTH,
144 DAILY_METHOD_REQUEST_ACCESS,
145 DAILY_METHOD_UPDATE_WAITING_PARTICIPANT,
146 DAILY_METHOD_UPDATE_WAITING_PARTICIPANTS,
147 DAILY_METHOD_GET_SINGLE_PARTICIPANT_RECEIVE_SETTINGS,
148 DAILY_METHOD_UPDATE_RECEIVE_SETTINGS,
149 DAILY_JS_VIDEO_PROCESSOR_TYPES as VIDEO_PROCESSOR_TYPES,
150 DAILY_METHOD_UPDATE_INPUT_SETTINGS,
151} from './shared-with-pluot-core/CommonIncludes.js';
152import {
153 isReactNative,
154 browserVideoSupported_p,
155 getUserAgent,
156 isScreenSharingSupported,
157 isSfuSupported,
158 isVideoProcessingSupported,
159} from './shared-with-pluot-core/Environment.js';
160import WebMessageChannel from './shared-with-pluot-core/script-message-channels/WebMessageChannel';
161import ReactNativeMessageChannel from './shared-with-pluot-core/script-message-channels/ReactNativeMessageChannel';
162import CallObjectLoader from './CallObjectLoader';
163import { callObjectBundleUrl, randomStringId } from './utils.js';
164import * as Participant from './Participant';
165
166// meeting states
167export {
168 DAILY_STATE_NEW,
169 DAILY_STATE_JOINING,
170 DAILY_STATE_JOINED,
171 DAILY_STATE_LEFT,
172 DAILY_STATE_ERROR,
173};
174
175// track states
176export {
177 DAILY_TRACK_STATE_BLOCKED,
178 DAILY_TRACK_STATE_OFF,
179 DAILY_TRACK_STATE_SENDABLE,
180 DAILY_TRACK_STATE_LOADING,
181 DAILY_TRACK_STATE_INTERRUPTED,
182 DAILY_TRACK_STATE_PLAYABLE,
183};
184
185// meeting access
186export {
187 DAILY_ACCESS_UNKNOWN,
188 DAILY_ACCESS_LEVEL_FULL,
189 DAILY_ACCESS_LEVEL_LOBBY,
190 DAILY_ACCESS_LEVEL_NONE,
191};
192
193// receive settings
194export {
195 DAILY_RECEIVE_SETTINGS_BASE_KEY,
196 DAILY_RECEIVE_SETTINGS_ALL_PARTICIPANTS_KEY,
197};
198
199// error types
200export {
201 DAILY_FATAL_ERROR_EJECTED,
202 DAILY_FATAL_ERROR_NBF_ROOM,
203 DAILY_FATAL_ERROR_NBF_TOKEN,
204 DAILY_FATAL_ERROR_EXP_ROOM,
205 DAILY_FATAL_ERROR_EXP_TOKEN,
206 DAILY_CAMERA_ERROR_CAM_IN_USE,
207 DAILY_CAMERA_ERROR_MIC_IN_USE,
208 DAILY_CAMERA_ERROR_CAM_AND_MIC_IN_USE,
209};
210
211// events
212export {
213 DAILY_EVENT_IFRAME_READY_FOR_LAUNCH_CONFIG,
214 DAILY_EVENT_IFRAME_LAUNCH_CONFIG,
215 DAILY_EVENT_THEME_UPDATED,
216 DAILY_EVENT_LOADING,
217 DAILY_EVENT_LOADED,
218 DAILY_EVENT_LOAD_ATTEMPT_FAILED,
219 DAILY_EVENT_STARTED_CAMERA,
220 DAILY_EVENT_CAMERA_ERROR,
221 DAILY_EVENT_JOINING_MEETING,
222 DAILY_EVENT_JOINED_MEETING,
223 DAILY_EVENT_LEFT_MEETING,
224 DAILY_EVENT_PARTICIPANT_JOINED,
225 DAILY_EVENT_PARTICIPANT_UPDATED,
226 DAILY_EVENT_PARTICIPANT_LEFT,
227 DAILY_EVENT_TRACK_STARTED,
228 DAILY_EVENT_TRACK_STOPPED,
229 DAILY_EVENT_RECORDING_STARTED,
230 DAILY_EVENT_RECORDING_STOPPED,
231 DAILY_EVENT_RECORDING_STATS,
232 DAILY_EVENT_RECORDING_ERROR,
233 DAILY_EVENT_RECORDING_UPLOAD_COMPLETED,
234 DAILY_EVENT_TRANSCRIPTION_STARTED,
235 DAILY_EVENT_TRANSCRIPTION_STOPPED,
236 DAILY_EVENT_TRANSCRIPTION_ERROR,
237 DAILY_EVENT_ERROR,
238 DAILY_EVENT_APP_MSG,
239 DAILY_EVENT_INPUT_EVENT,
240 DAILY_EVENT_LOCAL_SCREEN_SHARE_STARTED,
241 DAILY_EVENT_LOCAL_SCREEN_SHARE_STOPPED,
242 DAILY_EVENT_NETWORK_QUALITY_CHANGE,
243 DAILY_EVENT_ACTIVE_SPEAKER_CHANGE,
244 DAILY_EVENT_ACTIVE_SPEAKER_MODE_CHANGE,
245 DAILY_EVENT_FULLSCREEN,
246 DAILY_EVENT_EXIT_FULLSCREEN,
247 DAILY_EVENT_NETWORK_CONNECTION,
248 DAILY_EVENT_RECORDING_DATA,
249 DAILY_EVENT_LIVE_STREAMING_STARTED,
250 DAILY_EVENT_LIVE_STREAMING_STOPPED,
251 DAILY_EVENT_LIVE_STREAMING_ERROR,
252 DAILY_EVENT_LANG_UPDATED,
253 DAILY_EVENT_ACCESS_STATE_UPDATED,
254 DAILY_EVENT_MEETING_SESSION_UPDATED,
255 DAILY_EVENT_WAITING_PARTICIPANT_ADDED,
256 DAILY_EVENT_WAITING_PARTICIPANT_REMOVED,
257 DAILY_EVENT_WAITING_PARTICIPANT_UPDATED,
258 DAILY_EVENT_RECEIVE_SETTINGS_UPDATED,
259 DAILY_EVENT_INPUT_SETTINGS_UPDATED,
260 DAILY_EVENT_NONFATAL_ERROR,
261};
262
263// Audio modes for React Native: whether we should configure audio for video
264// calls or audio calls (i.e. whether we should use speakerphone).
265const NATIVE_AUDIO_MODE_VIDEO_CALL = 'video';
266const NATIVE_AUDIO_MODE_VOICE_CALL = 'voice';
267const NATIVE_AUDIO_MODE_IDLE = 'idle';
268
269//
270//
271//
272
273const reactNativeConfigType = {
274 androidInCallNotification: {
275 title: 'string',
276 subtitle: 'string',
277 iconName: 'string',
278 disableForCustomOverride: 'boolean',
279 },
280 disableAutoDeviceManagement: {
281 audio: 'boolean',
282 video: 'boolean',
283 },
284};
285
286const FRAME_PROPS = {
287 url: {
288 validate: (url) => typeof url === 'string',
289 help: 'url should be a string',
290 },
291 baseUrl: {
292 validate: (url) => typeof url === 'string',
293 help: 'baseUrl should be a string',
294 },
295 token: {
296 validate: (token) => typeof token === 'string',
297 help: 'token should be a string',
298 queryString: 't',
299 },
300 dailyConfig: {
301 // only for call object mode, for now
302 validate: (config) => {
303 if (!window._dailyConfig) {
304 window._dailyConfig = {};
305 }
306 window._dailyConfig.experimentalGetUserMediaConstraintsModify =
307 config.experimentalGetUserMediaConstraintsModify;
308 delete config.experimentalGetUserMediaConstraintsModify;
309 return true;
310 },
311 },
312 reactNativeConfig: {
313 validate: validateReactNativeConfig,
314 help: `reactNativeConfig should look like ${JSON.stringify(
315 reactNativeConfigType
316 )}, all fields optional`,
317 },
318 lang: {
319 validate: (lang) => {
320 return [
321 'de',
322 'en-us', // Here for backwards compatibility, but not encouraged (just maps to 'en' anyway)
323 'en',
324 'es',
325 'fi',
326 'fr',
327 'it',
328 'jp',
329 'ka',
330 'nl',
331 'no',
332 'pl',
333 'pt',
334 'ru',
335 'sv',
336 'tr',
337 'user',
338 ].includes(lang);
339 },
340 help:
341 'language not supported. Options are: de, en-us, en, es, fi, fr, it, jp, ka, nl, no, pl, pt, ru, sv, tr, user',
342 },
343 userName: true, // ignored if there's a token
344 activeSpeakerMode: true,
345 showLeaveButton: true,
346 showLocalVideo: true,
347 showParticipantsBar: true,
348 showFullscreenButton: true,
349 // style to apply to iframe in createFrame factory method
350 iframeStyle: true,
351 // styles passed through to video calls inside the iframe
352 customLayout: true,
353 cssFile: true,
354 cssText: true,
355 bodyClass: true,
356 videoSource: {
357 validate: (s, callObject) => {
358 callObject._preloadCache.videoDeviceId = s;
359 return true;
360 },
361 },
362 audioSource: {
363 validate: (s, callObject) => {
364 callObject._preloadCache.audioDeviceId = s;
365 return true;
366 },
367 },
368 subscribeToTracksAutomatically: {
369 validate: (s, callObject) => {
370 callObject._preloadCache.subscribeToTracksAutomatically = s;
371 return true;
372 },
373 },
374 theme: {
375 validate: (o) => {
376 const validColors = [
377 'accent',
378 'accentText',
379 'background',
380 'backgroundAccent',
381 'baseText',
382 'border',
383 'mainAreaBg',
384 'mainAreaBgAccent',
385 'mainAreaText',
386 'supportiveText',
387 ];
388 const containsValidColors = (colors) => {
389 for (const key of Object.keys(colors)) {
390 if (!validColors.includes(key)) {
391 // Key is not a supported theme color
392 console.error(
393 `unsupported color "${key}". Valid colors: ${validColors.join(
394 ', '
395 )}`
396 );
397 return false;
398 }
399 if (!colors[key].match(/^#[0-9a-f]{6}|#[0-9a-f]{3}$/i)) {
400 // Color is not in hex format
401 console.error(
402 `${key} theme color should be provided in valid hex color format. Received: "${colors[key]}"`
403 );
404 return false;
405 }
406 }
407 return true;
408 };
409 if (
410 typeof o !== 'object' ||
411 !(('light' in o && 'dark' in o) || 'colors' in o)
412 ) {
413 // Must define either both themes or colors
414 console.error(
415 'Theme must contain either both "light" and "dark" properties, or "colors".',
416 o
417 );
418 return false;
419 }
420 if ('light' in o && 'dark' in o) {
421 if (!('colors' in o.light)) {
422 console.error('Light theme is missing "colors" property.', o);
423 return false;
424 }
425 if (!('colors' in o.dark)) {
426 console.error('Dark theme is missing "colors" property.', o);
427 return false;
428 }
429 return (
430 containsValidColors(o.light.colors) &&
431 containsValidColors(o.dark.colors)
432 );
433 }
434 return containsValidColors(o.colors);
435 },
436 help:
437 'unsupported theme configuration. Check error logs for detailed info.',
438 },
439 layoutConfig: {
440 validate: (layoutConfig) => {
441 if ('grid' in layoutConfig) {
442 const gridConfig = layoutConfig.grid;
443 if ('maxTilesPerPage' in gridConfig) {
444 if (!Number.isInteger(gridConfig.maxTilesPerPage)) {
445 console.error(
446 `grid.maxTilesPerPage should be an integer. You passed ${gridConfig.maxTilesPerPage}.`
447 );
448 return false;
449 }
450 if (gridConfig.maxTilesPerPage > 49) {
451 console.error(
452 `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.`
453 );
454 return false;
455 }
456 }
457 if ('minTilesPerPage' in gridConfig) {
458 if (!Number.isInteger(gridConfig.minTilesPerPage)) {
459 console.error(
460 `grid.minTilesPerPage should be an integer. You passed ${gridConfig.minTilesPerPage}.`
461 );
462 return false;
463 }
464 if (gridConfig.minTilesPerPage < 1) {
465 console.error(`grid.minTilesPerPage can't be lower than 1.`);
466 return false;
467 }
468 if (
469 'maxTilesPerPage' in gridConfig &&
470 gridConfig.minTilesPerPage > gridConfig.maxTilesPerPage
471 ) {
472 console.error(
473 `grid.minTilesPerPage can't be higher than grid.maxTilesPerPage.`
474 );
475 return false;
476 }
477 }
478 }
479 return true;
480 },
481 help: 'unsupported layoutConfig. Check error logs for detailed info.',
482 },
483 receiveSettings: {
484 // Disallow "*" shorthand key since it's a shorthand for participants
485 // currently connected *to you* (i.e. participants already in
486 // participants()), which is necessarily empty at join time. Allowing this
487 // key might only sow confusion: it might lead people to think it's a
488 // shorthand for participants currently connected *to the room*.
489 validate: (receiveSettings) =>
490 validateReceiveSettings(receiveSettings, {
491 allowAllParticipantsKey: false,
492 }),
493 help: receiveSettingsValidationHelpMsg({
494 allowAllParticipantsKey: false,
495 }),
496 },
497 inputSettings: {
498 validate: (inputSettings) => validateInputSettings(inputSettings),
499 help: inputSettingsValidationHelpMsg(),
500 },
501 // used internally
502 layout: {
503 validate: (layout) =>
504 layout === 'custom-v1' || layout === 'browser' || layout === 'none',
505 help: 'layout may only be set to "custom-v1"',
506 queryString: 'layout',
507 },
508 emb: {
509 queryString: 'emb',
510 },
511 embHref: {
512 queryString: 'embHref',
513 },
514 dailyJsVersion: {
515 queryString: 'dailyJsVersion',
516 },
517};
518
519// todo: more validation?
520const PARTICIPANT_PROPS = {
521 styles: {
522 validate: (styles) => {
523 for (var k in styles) {
524 if (k !== 'cam' && k !== 'screen') {
525 return false;
526 }
527 }
528 if (styles.cam) {
529 for (var k in styles.cam) {
530 if (k !== 'div' && k !== 'video') {
531 return false;
532 }
533 }
534 }
535 if (styles.screen) {
536 for (var k in styles.screen) {
537 if (k !== 'div' && k !== 'video') {
538 return false;
539 }
540 }
541 }
542 return true;
543 },
544 help:
545 'styles format should be a subset of: ' +
546 '{ cam: {div: {}, video: {}}, screen: {div: {}, video: {}} }',
547 },
548 setSubscribedTracks: {
549 validate: (subs, callObject, participant) => {
550 if (callObject._preloadCache.subscribeToTracksAutomatically) {
551 return false;
552 }
553 const validPrimitiveValues = [true, false, 'staged'];
554 if (
555 validPrimitiveValues.includes(subs) ||
556 (!isReactNative() && subs === 'avatar')
557 ) {
558 return true;
559 }
560 for (const s in subs) {
561 if (
562 !(
563 ['audio', 'video', 'screenAudio', 'screenVideo'].includes(s) &&
564 validPrimitiveValues.includes(subs[s])
565 )
566 ) {
567 return false;
568 }
569 }
570 return true;
571 },
572 help:
573 'setSubscribedTracks cannot be used when setSubscribeToTracksAutomatically is enabled, and should be of the form: ' +
574 `true${
575 !isReactNative() ? " | 'avatar'" : ''
576 } | false | 'staged' | { [audio: true|false|'staged'], [video: true|false|'staged'], [screenAudio: true|false|'staged'], [screenVideo: true|false|'staged'] }`,
577 },
578 setAudio: true,
579 setVideo: true,
580 eject: true,
581};
582
583//
584//
585//
586
587export default class DailyIframe extends EventEmitter {
588 //
589 // static methods
590 //
591
592 static supportedBrowser() {
593 if (isReactNative()) {
594 return {
595 supported: true,
596 mobile: true,
597 name: 'React Native',
598 version: null,
599 supportsScreenShare: false,
600 supportsSfu: true,
601 supportsVideoProcessing: false,
602 };
603 }
604 const browser = Bowser.getParser(getUserAgent());
605 return {
606 supported: !!browserVideoSupported_p(),
607 mobile: browser.getPlatformType() === 'mobile',
608 name: browser.getBrowserName(),
609 version: browser.getBrowserVersion(),
610 supportsScreenShare: !!isScreenSharingSupported(),
611 supportsSfu: !!isSfuSupported(),
612 supportsVideoProcessing: isVideoProcessingSupported(),
613 };
614 }
615
616 static version() {
617 return __dailyJsVersion__;
618 }
619
620 //
621 // constructors
622 //
623
624 static createCallObject(properties = {}) {
625 properties.layout = 'none';
626 return new DailyIframe(null, properties);
627 }
628
629 static wrap(iframeish, properties = {}) {
630 methodNotSupportedInReactNative();
631 if (
632 !iframeish ||
633 !iframeish.contentWindow ||
634 'string' !== typeof iframeish.src
635 ) {
636 throw new Error('DailyIframe::Wrap needs an iframe-like first argument');
637 }
638 if (!properties.layout) {
639 if (properties.customLayout) {
640 properties.layout = 'custom-v1';
641 } else {
642 properties.layout = 'browser';
643 }
644 }
645 return new DailyIframe(iframeish, properties);
646 }
647
648 static createFrame(arg1, arg2) {
649 methodNotSupportedInReactNative();
650 let parentEl, properties;
651 if (arg1 && arg2) {
652 parentEl = arg1;
653 properties = arg2;
654 } else if (arg1 && arg1.append) {
655 parentEl = arg1;
656 properties = {};
657 } else {
658 parentEl = document.body;
659 properties = arg1 || {};
660 }
661 let iframeStyle = properties.iframeStyle;
662 if (!iframeStyle) {
663 if (parentEl === document.body) {
664 iframeStyle = {
665 position: 'fixed',
666 border: '1px solid black',
667 backgroundColor: 'white',
668 width: '375px',
669 height: '450px',
670 right: '1em',
671 bottom: '1em',
672 };
673 } else {
674 iframeStyle = {
675 border: 0,
676 width: '100%',
677 height: '100%',
678 };
679 }
680 }
681
682 let iframeEl = document.createElement('iframe');
683 // special-case for old Electron for Figma
684 if (window.navigator && window.navigator.userAgent.match(/Chrome\/61\./)) {
685 iframeEl.allow = 'microphone, camera';
686 } else {
687 iframeEl.allow = 'microphone; camera; autoplay; display-capture';
688 }
689 iframeEl.style.visibility = 'hidden';
690 parentEl.appendChild(iframeEl);
691 iframeEl.style.visibility = null;
692 Object.keys(iframeStyle).forEach(
693 (k) => (iframeEl.style[k] = iframeStyle[k])
694 );
695 if (!properties.layout) {
696 if (properties.customLayout) {
697 properties.layout = 'custom-v1';
698 } else {
699 properties.layout = 'browser';
700 }
701 }
702 try {
703 let callFrame = new DailyIframe(iframeEl, properties);
704 return callFrame;
705 } catch (e) {
706 // something when wrong while constructing the object. so let's clean
707 // up by removing ourselves from the page, then rethrow the error.
708 parentEl.removeChild(iframeEl);
709 throw e;
710 }
711 }
712
713 static createTransparentFrame(properties = {}) {
714 methodNotSupportedInReactNative();
715 let iframeEl = document.createElement('iframe');
716 iframeEl.allow = 'microphone; camera; autoplay';
717 iframeEl.style.cssText = `
718 position: fixed;
719 top: 0;
720 left: 0;
721 width: 100%;
722 height: 100%;
723 border: 0;
724 pointer-events: none;
725 `;
726 document.body.appendChild(iframeEl);
727 if (!properties.layout) {
728 properties.layout = 'custom-v1';
729 }
730 return DailyIframe.wrap(iframeEl, properties);
731 }
732
733 constructor(iframeish, properties = {}) {
734 super();
735 properties.dailyJsVersion = __dailyJsVersion__;
736 this._iframe = iframeish;
737 this._callObjectMode = properties.layout === 'none' && !this._iframe;
738 this._preloadCache = initializePreloadCache();
739 if (this._callObjectMode) {
740 window._dailyPreloadCache = this._preloadCache;
741 }
742
743 if (properties.showLocalVideo !== undefined) {
744 if (this._callObjectMode) {
745 console.error('showLocalVideo is not available in call object mode');
746 } else {
747 this._showLocalVideo = !!properties.showLocalVideo;
748 }
749 } else {
750 this._showLocalVideo = true;
751 }
752
753 if (properties.showParticipantsBar !== undefined) {
754 if (this._callObjectMode) {
755 console.error(
756 'showParticipantsBar is not available in call object mode'
757 );
758 } else {
759 this._showParticipantsBar = !!properties.showParticipantsBar;
760 }
761 } else {
762 this._showParticipantsBar = true;
763 }
764
765 if (properties.activeSpeakerMode !== undefined) {
766 if (this._callObjectMode) {
767 console.error('activeSpeakerMode is not available in call object mode');
768 } else {
769 this._activeSpeakerMode = !!properties.activeSpeakerMode;
770 }
771 } else {
772 this._activeSpeakerMode = false;
773 }
774
775 if (properties.receiveSettings) {
776 if (this._callObjectMode) {
777 this._receiveSettings = properties.receiveSettings;
778 } else {
779 console.error('receiveSettings is only available in call object mode');
780 }
781 } else {
782 // Here we avoid falling back to defaults, instead letting the call
783 // machine decide on defaults when its loaded and telling us about them
784 // via a DAILY_EVENT_RECEIVE_SETTINGS_UPDATED event. This will make it
785 // easier to update defaults in the future, eliminating the worry of
786 // daily-js getting out of sync with the call machine.
787 this._receiveSettings = {};
788 }
789
790 this._inputSettings = {};
791 if (properties.inputSettings) {
792 // #Question: Do I need the call-object check here?
793 this._inputSettings = properties.inputSettings;
794 }
795
796 this.validateProperties(properties);
797 this.properties = { ...properties };
798 this._callObjectLoader = this._callObjectMode
799 ? new CallObjectLoader()
800 : null;
801 this._meetingState = DAILY_STATE_NEW; // only update via updateIsPreparingToJoin() or updateMeetingState()
802 this._isPreparingToJoin = false; // only update via updateMeetingState()
803 this._accessState = { access: DAILY_ACCESS_UNKNOWN };
804 this._nativeInCallAudioMode = NATIVE_AUDIO_MODE_VIDEO_CALL;
805 this._participants = {};
806 this._waitingParticipants = {};
807 this._inputEventsOn = {}; // need to cache these until loaded
808 this._network = { threshold: 'good', quality: 100 };
809 this._activeSpeaker = {};
810 this._callFrameId = randomStringId();
811 this._messageChannel = isReactNative()
812 ? new ReactNativeMessageChannel()
813 : new WebMessageChannel();
814
815 // fullscreen event listener
816 if (this._iframe) {
817 if (this._iframe.requestFullscreen) {
818 // chrome (not safari)
819 this._iframe.addEventListener('fullscreenchange', (e) => {
820 if (document.fullscreenElement === this._iframe) {
821 this.emit(DAILY_EVENT_FULLSCREEN, {
822 action: DAILY_EVENT_FULLSCREEN,
823 });
824 this.sendMessageToCallMachine({ action: DAILY_EVENT_FULLSCREEN });
825 } else {
826 this.emit(DAILY_EVENT_EXIT_FULLSCREEN, {
827 action: DAILY_EVENT_EXIT_FULLSCREEN,
828 });
829 this.sendMessageToCallMachine({
830 action: DAILY_EVENT_EXIT_FULLSCREEN,
831 });
832 }
833 });
834 } else if (this._iframe.webkitRequestFullscreen) {
835 // safari
836 this._iframe.addEventListener('webkitfullscreenchange', (e) => {
837 if (document.webkitFullscreenElement === this._iframe) {
838 this.emit(DAILY_EVENT_FULLSCREEN, {
839 action: DAILY_EVENT_FULLSCREEN,
840 });
841 this.sendMessageToCallMachine({ action: DAILY_EVENT_FULLSCREEN });
842 } else {
843 this.emit(DAILY_EVENT_EXIT_FULLSCREEN, {
844 action: DAILY_EVENT_EXIT_FULLSCREEN,
845 });
846 this.sendMessageToCallMachine({
847 action: DAILY_EVENT_EXIT_FULLSCREEN,
848 });
849 }
850 });
851 }
852 }
853
854 // add native event listeners
855 if (isReactNative()) {
856 const nativeUtils = this.nativeUtils();
857 if (
858 !(
859 nativeUtils.addAudioFocusChangeListener &&
860 nativeUtils.removeAudioFocusChangeListener &&
861 nativeUtils.addAppActiveStateChangeListener &&
862 nativeUtils.removeAppActiveStateChangeListener
863 )
864 ) {
865 console.warn(
866 'expected (add|remove)(AudioFocus|AppActiveState)ChangeListener to be available in React Native'
867 );
868 }
869 // audio focus event, used for auto-muting mic
870 this._hasNativeAudioFocus = true;
871 nativeUtils.addAudioFocusChangeListener(
872 this.handleNativeAudioFocusChange
873 );
874 // app active state event, used for auto-muting cam
875 nativeUtils.addAppActiveStateChangeListener(
876 this.handleNativeAppActiveStateChange
877 );
878 }
879
880 this._messageChannel.addListenerForMessagesFromCallMachine(
881 this.handleMessageFromCallMachine,
882 this._callFrameId,
883 this
884 );
885 }
886
887 //
888 // instance methods
889 //
890
891 async destroy() {
892 try {
893 if (
894 [DAILY_STATE_JOINED, DAILY_STATE_LOADING].includes(this._meetingState)
895 ) {
896 await this.leave();
897 }
898 } catch (e) {}
899 let iframe = this._iframe;
900 if (iframe) {
901 let parent = iframe.parentElement;
902 if (parent) {
903 parent.removeChild(iframe);
904 }
905 }
906 this._messageChannel.removeListener(this.handleMessageFromCallMachine);
907
908 // tear down native event listeners
909 if (isReactNative()) {
910 const nativeUtils = this.nativeUtils();
911 nativeUtils.removeAudioFocusChangeListener(
912 this.handleNativeAudioFocusChange
913 );
914 nativeUtils.removeAppActiveStateChangeListener(
915 this.handleNativeAppActiveStateChange
916 );
917 }
918
919 this.resetMeetingDependentVars();
920 }
921
922 loadCss({ bodyClass, cssFile, cssText }) {
923 methodNotSupportedInReactNative();
924 this.sendMessageToCallMachine({
925 action: DAILY_METHOD_LOAD_CSS,
926 cssFile: this.absoluteUrl(cssFile),
927 bodyClass,
928 cssText,
929 });
930 return this;
931 }
932
933 iframe() {
934 methodNotSupportedInReactNative();
935 return this._iframe;
936 }
937
938 meetingState() {
939 return this._meetingState;
940 }
941
942 accessState() {
943 if (!this._callObjectMode) {
944 throw new Error(
945 'accessState() currently only supported in call object mode'
946 );
947 }
948
949 return this._accessState;
950 }
951
952 participants() {
953 return this._participants;
954 }
955
956 waitingParticipants() {
957 if (!this._callObjectMode) {
958 throw new Error(
959 'waitingParticipants() currently only supported in call object mode'
960 );
961 }
962
963 return this._waitingParticipants;
964 }
965
966 validateParticipantProperties(sessionId, properties) {
967 for (var prop in properties) {
968 if (!PARTICIPANT_PROPS[prop]) {
969 throw new Error(`unrecognized updateParticipant property ${prop}`);
970 }
971 if (PARTICIPANT_PROPS[prop].validate) {
972 if (
973 !PARTICIPANT_PROPS[prop].validate(
974 properties[prop],
975 this,
976 this._participants[sessionId]
977 )
978 ) {
979 throw new Error(PARTICIPANT_PROPS[prop].help);
980 }
981 }
982 }
983 }
984
985 updateParticipant(sessionId, properties) {
986 if (
987 this._participants.local &&
988 this._participants.local.session_id === sessionId
989 ) {
990 sessionId = 'local';
991 }
992 if (sessionId && properties && this._participants[sessionId]) {
993 this.validateParticipantProperties(sessionId, properties);
994 this.sendMessageToCallMachine({
995 action: DAILY_METHOD_UPDATE_PARTICIPANT,
996 id: sessionId,
997 properties,
998 });
999 }
1000 return this;
1001 }
1002
1003 updateParticipants(properties) {
1004 const localId =
1005 this._participants.local && this._participants.local.session_id;
1006 for (var sessionId in properties) {
1007 if (sessionId === localId) {
1008 sessionId = 'local';
1009 }
1010 if (
1011 sessionId &&
1012 properties[sessionId] &&
1013 (this._participants[sessionId] || sessionId === '*')
1014 ) {
1015 this.validateParticipantProperties(sessionId, properties[sessionId]);
1016 } else {
1017 console.warn(
1018 `unrecognized participant in updateParticipants: ${sessionId}`
1019 );
1020 delete properties[sessionId];
1021 }
1022 }
1023 this.sendMessageToCallMachine({
1024 action: DAILY_METHOD_UPDATE_PARTICIPANTS,
1025 participants: properties,
1026 });
1027 return this;
1028 }
1029
1030 async updateWaitingParticipant(id = '', updates = {}) {
1031 // Validate mode.
1032 if (!this._callObjectMode) {
1033 throw new Error(
1034 'updateWaitingParticipant() currently only supported in call object mode'
1035 );
1036 }
1037
1038 // Validate meeting state: only allowed once you've joined.
1039 if (this._meetingState !== DAILY_STATE_JOINED) {
1040 throw new Error(
1041 'updateWaitingParticipant() only supported for joined meetings'
1042 );
1043 }
1044
1045 // Validate argument presence.
1046 if (!(typeof id === 'string' && typeof updates === 'object')) {
1047 throw new Error(
1048 'updateWaitingParticipant() must take an id string and a updates object'
1049 );
1050 }
1051
1052 return new Promise((resolve, reject) => {
1053 const k = (msg) => {
1054 if (msg.error) {
1055 reject(msg.error);
1056 }
1057
1058 if (!msg.id) {
1059 reject(new Error('unknown error in updateWaitingParticipant()'));
1060 }
1061
1062 resolve({ id: msg.id });
1063 };
1064 this.sendMessageToCallMachine(
1065 {
1066 action: DAILY_METHOD_UPDATE_WAITING_PARTICIPANT,
1067 id,
1068 updates,
1069 },
1070 k
1071 );
1072 });
1073 }
1074
1075 async updateWaitingParticipants(updatesById = {}) {
1076 // Validate mode.
1077 if (!this._callObjectMode) {
1078 throw new Error(
1079 'updateWaitingParticipants() currently only supported in call object mode'
1080 );
1081 }
1082
1083 // Validate meeting state: only allowed once you've joined.
1084 if (this._meetingState !== DAILY_STATE_JOINED) {
1085 throw new Error(
1086 'updateWaitingParticipants() only supported for joined meetings'
1087 );
1088 }
1089
1090 // Validate argument presence.
1091 if (typeof updatesById !== 'object') {
1092 throw new Error(
1093 'updateWaitingParticipants() must take a mapping between ids and update objects'
1094 );
1095 }
1096
1097 return new Promise((resolve, reject) => {
1098 const k = (msg) => {
1099 if (msg.error) {
1100 reject(msg.error);
1101 }
1102
1103 if (!msg.ids) {
1104 reject(new Error('unknown error in updateWaitingParticipants()'));
1105 }
1106
1107 resolve({ ids: msg.ids });
1108 };
1109 this.sendMessageToCallMachine(
1110 {
1111 action: DAILY_METHOD_UPDATE_WAITING_PARTICIPANTS,
1112 updatesById,
1113 },
1114 k
1115 );
1116 });
1117 }
1118
1119 async requestAccess({
1120 access = { level: DAILY_ACCESS_LEVEL_FULL },
1121 name = '',
1122 } = {}) {
1123 // Validate mode.
1124 if (!this._callObjectMode) {
1125 throw new Error(
1126 'requestAccess() currently only supported in call object mode'
1127 );
1128 }
1129
1130 // Validate meeting state: access requesting is only allowed once you've
1131 // joined.
1132 if (this._meetingState !== DAILY_STATE_JOINED) {
1133 throw new Error('requestAccess() only supported for joined meetings');
1134 }
1135
1136 return new Promise((resolve, reject) => {
1137 const k = (msg) => {
1138 if (msg.error) {
1139 reject(msg.error);
1140 }
1141
1142 if (!msg.access) {
1143 reject(new Error('unknown error in requestAccess()'));
1144 }
1145
1146 resolve({ access: msg.access, granted: msg.granted });
1147 };
1148 this.sendMessageToCallMachine(
1149 {
1150 action: DAILY_METHOD_REQUEST_ACCESS,
1151 access,
1152 name,
1153 },
1154 k
1155 );
1156 });
1157 }
1158
1159 localAudio() {
1160 if (this._participants.local) {
1161 return this._participants.local.audio;
1162 }
1163 return null;
1164 }
1165
1166 localVideo() {
1167 if (this._participants.local) {
1168 return this._participants.local.video;
1169 }
1170 return null;
1171 }
1172
1173 setLocalAudio(bool) {
1174 this.sendMessageToCallMachine({
1175 action: DAILY_METHOD_LOCAL_AUDIO,
1176 state: bool,
1177 });
1178 return this;
1179 }
1180
1181 setLocalVideo(bool) {
1182 this.sendMessageToCallMachine({
1183 action: DAILY_METHOD_LOCAL_VIDEO,
1184 state: bool,
1185 });
1186 return this;
1187 }
1188
1189 // NOTE: "base" receive settings will not appear until the call machine bundle
1190 // is initialized (e.g. after a call to join()).
1191 // Listen for the receive-settings-updated to be notified when those come in.
1192 async getReceiveSettings(id, { showInheritedValues = false } = {}) {
1193 // Validate mode.
1194 if (!this._callObjectMode) {
1195 throw new Error(
1196 'getReceiveSettings() only supported in call object mode'
1197 );
1198 }
1199
1200 // This method can be called in two main ways:
1201 // - it can get receive settings for a specific participant (or "base")
1202 // - it can get *all* receive settings
1203 switch (typeof id) {
1204 // Case: getting receive settings for a single participant
1205 case 'string':
1206 // Ask call machine to get receive settings for the participant.
1207 // Centralizing this nontrivial fetching logic in the call machine,
1208 // rather than attempting to duplicate it here, avoids the problem of
1209 // daily-js and the call machine getting out of sync.
1210 return new Promise((resolve) => {
1211 const k = (msg) => {
1212 resolve(msg.receiveSettings);
1213 };
1214 this.sendMessageToCallMachine(
1215 {
1216 action: DAILY_METHOD_GET_SINGLE_PARTICIPANT_RECEIVE_SETTINGS,
1217 id,
1218 showInheritedValues,
1219 },
1220 k
1221 );
1222 });
1223 // Case: getting all receive settings
1224 case 'undefined':
1225 return this._receiveSettings;
1226 default:
1227 throw new Error(
1228 'first argument to getReceiveSettings() must be a participant id (or "base"), or there should be no arguments'
1229 );
1230 }
1231 }
1232
1233 async updateReceiveSettings(receiveSettings) {
1234 // Validate mode.
1235 if (!this._callObjectMode) {
1236 throw new Error(
1237 'updateReceiveSettings() only supported in call object mode'
1238 );
1239 }
1240
1241 // Validate receive settings.
1242 if (
1243 !validateReceiveSettings(receiveSettings, {
1244 allowAllParticipantsKey: true,
1245 })
1246 ) {
1247 throw new Error(
1248 receiveSettingsValidationHelpMsg({ allowAllParticipantsKey: true })
1249 );
1250 }
1251
1252 // Validate that call machine is joined.
1253 // (We need the Redux state to be set up first; technically, we could
1254 // proceed if we've either join()ed *or* preAuth()ed *or* startCamera()ed
1255 // but since there's an easy alternative way to specify initial receive
1256 // settings until join(), for simplicity let's just require that we be
1257 // joined).
1258 if (this._meetingState !== DAILY_STATE_JOINED) {
1259 throw new Error(
1260 'updateReceiveSettings() is only allowed when joined. To specify receive settings earlier, use the receiveSettings config property.'
1261 );
1262 }
1263
1264 // Ask call machine to update receive settings, then await callback.
1265 return new Promise((resolve) => {
1266 const k = (msg) => {
1267 resolve({ receiveSettings: msg.receiveSettings });
1268 };
1269 this.sendMessageToCallMachine(
1270 {
1271 action: DAILY_METHOD_UPDATE_RECEIVE_SETTINGS,
1272 receiveSettings,
1273 },
1274 k
1275 );
1276 });
1277 }
1278
1279 // Input Settings Getter
1280 // { video: { processor } }
1281 // In the future:
1282 // { video: {...}, audio: {...}, screenVideo: {...}, screenAudio: {...} }
1283 getInputSettings() {
1284 return this._inputSettings;
1285 }
1286
1287 async updateInputSettings(inputSettings) {
1288 //#Question: Do I need the call-object mode check for input-settings?
1289 if (!validateInputSettings(inputSettings)) {
1290 throw new Error(inputSettingsValidationHelpMsg());
1291 }
1292
1293 // Ask call machine to update input settings, then await callback.
1294 return new Promise((resolve) => {
1295 const k = (msg) => {
1296 resolve({ inputSettings: msg.inputSettings });
1297 };
1298 this.sendMessageToCallMachine(
1299 {
1300 action: DAILY_METHOD_UPDATE_INPUT_SETTINGS,
1301 inputSettings,
1302 },
1303 k
1304 );
1305 });
1306 }
1307
1308 setBandwidth({ kbs, trackConstraints }) {
1309 methodNotSupportedInReactNative();
1310 this.sendMessageToCallMachine({
1311 action: DAILY_METHOD_SET_BANDWIDTH,
1312 kbs,
1313 trackConstraints,
1314 });
1315 return this;
1316 }
1317
1318 getDailyLang() {
1319 methodNotSupportedInReactNative();
1320 return new Promise(async (resolve) => {
1321 const k = (msg) => {
1322 delete msg.action;
1323 delete msg.callbackStamp;
1324 resolve(msg);
1325 };
1326 this.sendMessageToCallMachine({ action: DAILY_METHOD_GET_LANG }, k);
1327 });
1328 }
1329
1330 setDailyLang(lang) {
1331 methodNotSupportedInReactNative();
1332 this.sendMessageToCallMachine({ action: DAILY_METHOD_SET_LANG, lang });
1333 return this;
1334 }
1335
1336 async getMeetingSession() {
1337 // Validate meeting state: meeting session details are only available
1338 // once you have joined the meeting
1339 if (this._meetingState !== DAILY_STATE_JOINED) {
1340 throw new Error('getMeetingSession() is only allowed when joined');
1341 }
1342 return new Promise(async (resolve) => {
1343 const k = (msg) => {
1344 delete msg.action;
1345 delete msg.callbackStamp;
1346 delete msg.callFrameId;
1347 resolve(msg);
1348 };
1349 this.sendMessageToCallMachine(
1350 { action: DAILY_METHOD_GET_MEETING_SESSION },
1351 k
1352 );
1353 });
1354 }
1355
1356 setUserName(name, options) {
1357 this.properties.userName = name;
1358 return new Promise(async (resolve) => {
1359 const k = (msg) => {
1360 delete msg.action;
1361 delete msg.callbackStamp;
1362 resolve(msg);
1363 };
1364 this.sendMessageToCallMachine(
1365 {
1366 action: DAILY_METHOD_SET_USER_NAME,
1367 name: name ?? '',
1368 thisMeetingOnly:
1369 isReactNative() || (options ? !!options.thisMeetingOnly : false),
1370 },
1371 k
1372 );
1373 });
1374 }
1375
1376 startCamera(properties = {}) {
1377 return new Promise(async (resolve, reject) => {
1378 let k = (msg) => {
1379 delete msg.action;
1380 delete msg.callbackStamp;
1381 resolve(msg);
1382 };
1383 if (this.needsLoad()) {
1384 try {
1385 await this.load(properties);
1386 } catch (e) {
1387 reject(e);
1388 }
1389 }
1390 this.sendMessageToCallMachine(
1391 {
1392 action: DAILY_METHOD_START_CAMERA,
1393 properties: makeSafeForPostMessage(this.properties),
1394 preloadCache: makeSafeForPostMessage(this._preloadCache),
1395 },
1396 k
1397 );
1398 });
1399 }
1400
1401 cycleCamera() {
1402 return new Promise((resolve, _) => {
1403 let k = (msg) => {
1404 resolve({ device: msg.device });
1405 };
1406 this.sendMessageToCallMachine({ action: DAILY_METHOD_CYCLE_CAMERA }, k);
1407 });
1408 }
1409
1410 cycleMic() {
1411 methodNotSupportedInReactNative();
1412 return new Promise((resolve, _) => {
1413 let k = (msg) => {
1414 resolve({ device: msg.device });
1415 };
1416 this.sendMessageToCallMachine({ action: DAILY_METHOD_CYCLE_MIC }, k);
1417 });
1418 }
1419
1420 getCameraFacingMode() {
1421 methodOnlySupportedInReactNative();
1422 return new Promise((resolve, _) => {
1423 let k = (msg) => {
1424 resolve(msg.facingMode);
1425 };
1426 this.sendMessageToCallMachine(
1427 { action: DAILY_METHOD_GET_CAMERA_FACING_MODE },
1428 k
1429 );
1430 });
1431 }
1432
1433 setInputDevices({ audioDeviceId, videoDeviceId, audioSource, videoSource }) {
1434 console.warn(
1435 'setInputDevices() is deprecated: instead use setInputDevicesAsync(), which returns a Promise'
1436 );
1437 this.setInputDevicesAsync({
1438 audioDeviceId,
1439 videoDeviceId,
1440 audioSource,
1441 videoSource,
1442 });
1443 return this;
1444 }
1445
1446 async setInputDevicesAsync({
1447 audioDeviceId,
1448 videoDeviceId,
1449 audioSource,
1450 videoSource,
1451 }) {
1452 methodNotSupportedInReactNative();
1453 // use audioDeviceId and videoDeviceId internally
1454 if (audioSource !== undefined) {
1455 audioDeviceId = audioSource;
1456 }
1457 if (videoSource !== undefined) {
1458 videoDeviceId = videoSource;
1459 }
1460
1461 // cache these for use in subsequent calls
1462 if (audioDeviceId) {
1463 this._preloadCache.audioDeviceId = audioDeviceId;
1464 }
1465 if (videoDeviceId) {
1466 this._preloadCache.videoDeviceId = videoDeviceId;
1467 }
1468
1469 // if we're in callObject mode and not loaded yet, don't do anything
1470 if (this._callObjectMode && this.needsLoad()) {
1471 return {
1472 camera: { deviceId: this._preloadCache.videoDeviceId },
1473 mic: { deviceId: this._preloadCache.audioDeviceId },
1474 speaker: { deviceId: this._preloadCache.outputDeviceId },
1475 };
1476 }
1477
1478 if (audioDeviceId instanceof MediaStreamTrack) {
1479 audioDeviceId = DAILY_CUSTOM_TRACK;
1480 }
1481 if (videoDeviceId instanceof MediaStreamTrack) {
1482 videoDeviceId = DAILY_CUSTOM_TRACK;
1483 }
1484
1485 return new Promise((resolve) => {
1486 let k = (msg) => {
1487 delete msg.action;
1488 delete msg.callbackStamp;
1489
1490 if (msg.returnPreloadCache) {
1491 resolve({
1492 camera: { deviceId: this._preloadCache.videoDeviceId },
1493 mic: { deviceId: this._preloadCache.audioDeviceId },
1494 speaker: { deviceId: this._preloadCache.outputDeviceId },
1495 });
1496 return;
1497 }
1498
1499 resolve(msg);
1500 };
1501 this.sendMessageToCallMachine(
1502 {
1503 action: DAILY_METHOD_SET_INPUT_DEVICES,
1504 audioDeviceId,
1505 videoDeviceId,
1506 },
1507 k
1508 );
1509 });
1510 }
1511
1512 setOutputDevice({ outputDeviceId }) {
1513 methodNotSupportedInReactNative();
1514 // cache this for use later
1515 if (outputDeviceId) {
1516 this._preloadCache.outputDeviceId = outputDeviceId;
1517 }
1518
1519 // if we're in callObject mode and not joined yet, don't do anything
1520 if (this._callObjectMode && this._meetingState !== DAILY_STATE_JOINED) {
1521 return this;
1522 }
1523
1524 this.sendMessageToCallMachine({
1525 action: DAILY_METHOD_SET_OUTPUT_DEVICE,
1526 outputDeviceId,
1527 });
1528 return this;
1529 }
1530
1531 async getInputDevices() {
1532 methodNotSupportedInReactNative();
1533 if (this._callObjectMode && this.needsLoad()) {
1534 return {
1535 camera: { deviceId: this._preloadCache.videoDeviceId },
1536 mic: { deviceId: this._preloadCache.audioDeviceId },
1537 speaker: { deviceId: this._preloadCache.outputDeviceId },
1538 };
1539 }
1540
1541 return new Promise((resolve, reject) => {
1542 let k = (msg) => {
1543 delete msg.action;
1544 delete msg.callbackStamp;
1545
1546 if (msg.returnPreloadCache) {
1547 resolve({
1548 camera: { deviceId: this._preloadCache.videoDeviceId },
1549 mic: { deviceId: this._preloadCache.audioDeviceId },
1550 speaker: { deviceId: this._preloadCache.outputDeviceId },
1551 });
1552 return;
1553 }
1554
1555 resolve(msg);
1556 };
1557 this.sendMessageToCallMachine(
1558 { action: DAILY_METHOD_GET_INPUT_DEVICES },
1559 k
1560 );
1561 });
1562 }
1563
1564 nativeInCallAudioMode() {
1565 methodOnlySupportedInReactNative();
1566 return this._nativeInCallAudioMode;
1567 }
1568
1569 setNativeInCallAudioMode(inCallAudioMode) {
1570 methodOnlySupportedInReactNative();
1571 if (
1572 ![NATIVE_AUDIO_MODE_VIDEO_CALL, NATIVE_AUDIO_MODE_VOICE_CALL].includes(
1573 inCallAudioMode
1574 )
1575 ) {
1576 console.error('invalid in-call audio mode specified: ', inCallAudioMode);
1577 return;
1578 }
1579
1580 if (inCallAudioMode === this._nativeInCallAudioMode) {
1581 return;
1582 }
1583
1584 // Set new audio mode (video call, audio call) to use when we're in a call
1585 this._nativeInCallAudioMode = inCallAudioMode;
1586
1587 // If we're in a call now, apply the new audio mode
1588 // (assuming automatic audio device management isn't disabled)
1589 if (
1590 !this.disableReactNativeAutoDeviceManagement('audio') &&
1591 this.isMeetingPendingOrOngoing(
1592 this._meetingState,
1593 this._isPreparingToJoin
1594 )
1595 ) {
1596 this.nativeUtils().setAudioMode(this._nativeInCallAudioMode);
1597 }
1598
1599 return this;
1600 }
1601
1602 async preAuth(properties = {}) {
1603 // Validate mode.
1604 if (!this._callObjectMode) {
1605 throw new Error('preAuth() currently only supported in call object mode');
1606 }
1607
1608 // Validate meeting state: pre-auth is only allowed if you haven't already
1609 // joined (or aren't in the process of joining).
1610 if (
1611 [DAILY_STATE_JOINING, DAILY_STATE_JOINED].includes(this._meetingState)
1612 ) {
1613 throw new Error('preAuth() not supported after joining a meeting');
1614 }
1615
1616 // Load call machine bundle, if needed.
1617 if (this.needsLoad()) {
1618 await this.load(properties);
1619 }
1620
1621 // Assign properties, ensuring that at a minimum url is set.
1622 // Disallow changing to a url with a different bundle url than the one used
1623 // for load().
1624 if (!properties.url) {
1625 throw new Error('preAuth() requires at least a url to be provided');
1626 }
1627 const newBundleUrl = callObjectBundleUrl(properties.url);
1628 const loadedBundleUrl = callObjectBundleUrl(
1629 this.properties.url || this.properties.baseUrl
1630 );
1631 if (newBundleUrl !== loadedBundleUrl) {
1632 throw new Error(
1633 `url in preAuth() has a different bundle url than the one loaded (${loadedBundleUrl} -> ${newBundleUrl})`
1634 );
1635 }
1636 this.validateProperties(properties);
1637 this.properties = { ...this.properties, ...properties };
1638
1639 // Pre-auth with the server.
1640 return new Promise((resolve, reject) => {
1641 const k = (msg) => {
1642 if (msg.error) {
1643 return reject(msg.error);
1644 }
1645
1646 if (!msg.access) {
1647 return reject(new Error('unknown error in preAuth()'));
1648 }
1649
1650 // Set a flag indicating that we've pre-authed.
1651 // This flag has the effect of "locking in" url and token, so that they
1652 // can't be changed subsequently on join(), which would invalidate this
1653 // pre-auth.
1654 this._didPreAuth = true;
1655
1656 resolve({ access: msg.access });
1657 };
1658 this.sendMessageToCallMachine(
1659 {
1660 action: DAILY_METHOD_PREAUTH,
1661 properties: makeSafeForPostMessage(this.properties),
1662 },
1663 k
1664 );
1665 });
1666 }
1667
1668 async load(properties) {
1669 if (!this.needsLoad()) {
1670 return;
1671 }
1672
1673 if (properties) {
1674 this.validateProperties(properties);
1675 this.properties = { ...this.properties, ...properties };
1676 }
1677
1678 // In iframe mode, we *must* have a meeting url
1679 // (As opposed to call object mode, where a meeting url, a base url, or no
1680 // url at all are all valid here)
1681 if (!this._callObjectMode && !this.properties.url) {
1682 throw new Error(
1683 "can't load iframe meeting because url property isn't set"
1684 );
1685 }
1686
1687 this.updateMeetingState(DAILY_STATE_LOADING);
1688 try {
1689 this.emit(DAILY_EVENT_LOADING, { action: DAILY_EVENT_LOADING });
1690 } catch (e) {
1691 console.log("could not emit 'loading'", e);
1692 }
1693
1694 if (this._callObjectMode) {
1695 // non-iframe, callObjectMode
1696 return new Promise((resolve, reject) => {
1697 this._callObjectLoader.cancel();
1698 this._callObjectLoader.load(
1699 this.properties.url || this.properties.baseUrl,
1700 this._callFrameId,
1701 (wasNoOp) => {
1702 this.updateMeetingState(DAILY_STATE_LOADED);
1703 // Only need to emit event if load was a no-op, since the loaded
1704 // bundle won't be emitting it if it's not executed again
1705 wasNoOp &&
1706 this.emit(DAILY_EVENT_LOADED, { action: DAILY_EVENT_LOADED });
1707 resolve();
1708 },
1709 (errorMsg, willRetry) => {
1710 this.emit(DAILY_EVENT_LOAD_ATTEMPT_FAILED, {
1711 action: DAILY_EVENT_LOAD_ATTEMPT_FAILED,
1712 errorMsg,
1713 });
1714 if (!willRetry) {
1715 this.updateMeetingState(DAILY_STATE_ERROR);
1716 this.resetMeetingDependentVars();
1717 this.emit(DAILY_EVENT_ERROR, {
1718 action: DAILY_EVENT_ERROR,
1719 errorMsg,
1720 });
1721 reject(errorMsg);
1722 }
1723 }
1724 );
1725 });
1726 } else {
1727 // iframe
1728 this._iframe.src = this.assembleMeetingUrl();
1729 return new Promise((resolve, reject) => {
1730 this._loadedCallback = (error) => {
1731 if (this._meetingState === DAILY_STATE_ERROR) {
1732 reject(error);
1733 return;
1734 }
1735 this.updateMeetingState(DAILY_STATE_LOADED);
1736 if (this.properties.cssFile || this.properties.cssText) {
1737 this.loadCss(this.properties);
1738 }
1739 for (let eventName in this._inputEventsOn) {
1740 this.sendMessageToCallMachine({
1741 action: DAILY_METHOD_REGISTER_INPUT_HANDLER,
1742 on: eventName,
1743 });
1744 }
1745 resolve();
1746 };
1747 });
1748 }
1749 }
1750
1751 async join(properties = {}) {
1752 let newCss = false;
1753 if (this.needsLoad()) {
1754 this.updateIsPreparingToJoin(true);
1755 try {
1756 await this.load(properties);
1757 } catch (e) {
1758 this.updateIsPreparingToJoin(false);
1759 return Promise.reject(e);
1760 }
1761 } else {
1762 newCss = !!(this.properties.cssFile || this.properties.cssText);
1763
1764 // Validate that any provided url or token doesn't conflict with url or
1765 // token already used to preAuth()
1766 if (this._didPreAuth) {
1767 if (properties.url && properties.url !== this.properties.url) {
1768 console.error(
1769 `url in join() is different than the one used in preAuth()`
1770 );
1771 this.updateIsPreparingToJoin(false);
1772 return Promise.reject();
1773 }
1774 if (properties.token && properties.token !== this.properties.token) {
1775 console.error(
1776 `token in join() is different than the one used in preAuth()`
1777 );
1778 this.updateIsPreparingToJoin(false);
1779 return Promise.reject();
1780 }
1781 }
1782
1783 // Validate that url we're using to join() doesn't conflict with the url
1784 // we used to load()
1785 if (properties.url) {
1786 if (this._callObjectMode) {
1787 const newBundleUrl = callObjectBundleUrl(properties.url);
1788 const loadedBundleUrl = callObjectBundleUrl(
1789 this.properties.url || this.properties.baseUrl
1790 );
1791 if (newBundleUrl !== loadedBundleUrl) {
1792 console.error(
1793 `url in join() has a different bundle url than the one loaded (${loadedBundleUrl} -> ${newBundleUrl})`
1794 );
1795 this.updateIsPreparingToJoin(false);
1796 return Promise.reject();
1797 }
1798 this.properties.url = properties.url;
1799 } else {
1800 // iframe mode
1801 if (properties.url && properties.url !== this.properties.url) {
1802 console.error(
1803 `url in join() is different than the one used in load() (${this.properties.url} -> ${properties.url})`
1804 );
1805 this.updateIsPreparingToJoin(false);
1806 return Promise.reject();
1807 }
1808 }
1809 }
1810
1811 // Validate and assign properties to this.properties, for use by call
1812 // machine
1813 this.validateProperties(properties);
1814 this.properties = { ...this.properties, ...properties };
1815 }
1816
1817 // only update if showLocalVideo/showParticipantsBar are being explicitly set
1818 if (properties.showLocalVideo !== undefined) {
1819 if (this._callObjectMode) {
1820 console.error('showLocalVideo is not available in callObject mode');
1821 } else {
1822 this._showLocalVideo = !!properties.showLocalVideo;
1823 }
1824 }
1825 if (properties.showParticipantsBar !== undefined) {
1826 if (this._callObjectMode) {
1827 console.error(
1828 'showParticipantsBar is not available in callObject mode'
1829 );
1830 } else {
1831 this._showParticipantsBar = !!properties.showParticipantsBar;
1832 }
1833 }
1834
1835 if (
1836 this._meetingState === DAILY_STATE_JOINED ||
1837 this._meetingState === DAILY_STATE_JOINING
1838 ) {
1839 console.warn('already joined meeting, call leave() before joining again');
1840 this.updateIsPreparingToJoin(false);
1841 return;
1842 }
1843 this.updateMeetingState(DAILY_STATE_JOINING, false);
1844 try {
1845 this.emit(DAILY_EVENT_JOINING_MEETING, {
1846 action: DAILY_EVENT_JOINING_MEETING,
1847 });
1848 } catch (e) {
1849 console.log("could not emit 'joining-meeting'", e);
1850 }
1851 this.sendMessageToCallMachine({
1852 action: DAILY_METHOD_JOIN,
1853 properties: makeSafeForPostMessage(this.properties),
1854 preloadCache: makeSafeForPostMessage(this._preloadCache),
1855 });
1856 return new Promise((resolve, reject) => {
1857 this._joinedCallback = (participants, error) => {
1858 if (this._meetingState === DAILY_STATE_ERROR) {
1859 reject(error);
1860 return;
1861 }
1862 this.updateMeetingState(DAILY_STATE_JOINED);
1863 if (participants) {
1864 for (var id in participants) {
1865 if (this._callObjectMode) {
1866 Participant.addTracks(participants[id]);
1867 Participant.addCustomTracks(participants[id]);
1868 Participant.addLegacyTracks(
1869 participants[id],
1870 this._participants[id]
1871 );
1872 }
1873 this._participants[id] = { ...participants[id] };
1874 this.toggleParticipantAudioBasedOnNativeAudioFocus();
1875 }
1876 }
1877 if (newCss) {
1878 this.loadCss(this.properties);
1879 }
1880 resolve(participants);
1881 };
1882 });
1883 }
1884
1885 async leave() {
1886 return new Promise((resolve, _) => {
1887 if (this._callObjectLoader && !this._callObjectLoader.loaded) {
1888 // If call object bundle never successfully loaded, cancel load if
1889 // needed and clean up state immediately (without waiting for call
1890 // machine to clean up its state).
1891 this._callObjectLoader.cancel();
1892 this.updateMeetingState(DAILY_STATE_LEFT);
1893 this.resetMeetingDependentVars();
1894 try {
1895 this.emit(DAILY_STATE_LEFT, { action: DAILY_STATE_LEFT });
1896 } catch (e) {
1897 console.log("could not emit 'left-meeting'", e);
1898 }
1899 resolve();
1900 } else if (
1901 this._meetingState === DAILY_STATE_LEFT ||
1902 this._meetingState === DAILY_STATE_ERROR
1903 ) {
1904 // nothing to do, here, just resolve
1905 resolve();
1906 } else {
1907 // TODO: the possibility that the iframe call machine is not yet loaded
1908 // is never handled here...
1909 this.sendMessageToCallMachine({ action: DAILY_METHOD_LEAVE }, () => {
1910 resolve();
1911 });
1912 }
1913 });
1914 }
1915
1916 startScreenShare(captureOptions = {}) {
1917 methodNotSupportedInReactNative();
1918 if (captureOptions.mediaStream) {
1919 this._preloadCache.screenMediaStream = captureOptions.mediaStream;
1920 captureOptions.mediaStream = DAILY_CUSTOM_TRACK;
1921 }
1922 this.sendMessageToCallMachine({
1923 action: DAILY_METHOD_START_SCREENSHARE,
1924 captureOptions,
1925 });
1926 }
1927
1928 stopScreenShare() {
1929 methodNotSupportedInReactNative();
1930 this.sendMessageToCallMachine({ action: DAILY_METHOD_STOP_SCREENSHARE });
1931 }
1932
1933 startRecording(args = {}) {
1934 // TODO: Should we allow starting a cloud-mp4 recording in RN?
1935 methodNotSupportedInReactNative();
1936 this.sendMessageToCallMachine({
1937 action: DAILY_METHOD_START_RECORDING,
1938 ...args,
1939 });
1940 }
1941
1942 updateRecording({ layout = { preset: 'default' } }) {
1943 this.sendMessageToCallMachine({
1944 action: DAILY_METHOD_UPDATE_RECORDING,
1945 layout,
1946 });
1947 }
1948
1949 stopRecording() {
1950 methodNotSupportedInReactNative();
1951 this.sendMessageToCallMachine({ action: DAILY_METHOD_STOP_RECORDING });
1952 }
1953
1954 startLiveStreaming(args = {}) {
1955 this.sendMessageToCallMachine({
1956 action: DAILY_METHOD_START_LIVE_STREAMING,
1957 ...args,
1958 });
1959 }
1960
1961 updateLiveStreaming({ layout = { preset: 'default' } }) {
1962 this.sendMessageToCallMachine({
1963 action: DAILY_METHOD_UPDATE_LIVE_STREAMING,
1964 layout,
1965 });
1966 }
1967
1968 stopLiveStreaming() {
1969 this.sendMessageToCallMachine({ action: DAILY_METHOD_STOP_LIVE_STREAMING });
1970 }
1971
1972 startTranscription() {
1973 this.sendMessageToCallMachine({ action: DAILY_METHOD_START_TRANSCRIPTION });
1974 }
1975
1976 stopTranscription() {
1977 this.sendMessageToCallMachine({ action: DAILY_METHOD_STOP_TRANSCRIPTION });
1978 }
1979
1980 getNetworkStats() {
1981 if (this._meetingState !== DAILY_STATE_JOINED) {
1982 let stats = { latest: {} };
1983 return { stats };
1984 }
1985 return new Promise((resolve, _) => {
1986 let k = (msg) => {
1987 resolve({ stats: msg.stats, ...this._network });
1988 };
1989 this.sendMessageToCallMachine({ action: DAILY_METHOD_GET_CALC_STATS }, k);
1990 });
1991 }
1992
1993 getActiveSpeaker() {
1994 methodNotSupportedInReactNative();
1995 return this._activeSpeaker;
1996 }
1997
1998 setActiveSpeakerMode(enabled) {
1999 methodNotSupportedInReactNative();
2000 this.sendMessageToCallMachine({
2001 action: DAILY_METHOD_SET_ACTIVE_SPEAKER_MODE,
2002 enabled,
2003 });
2004 return this;
2005 }
2006
2007 activeSpeakerMode() {
2008 methodNotSupportedInReactNative();
2009 return this._activeSpeakerMode;
2010 }
2011
2012 subscribeToTracksAutomatically() {
2013 return this._preloadCache.subscribeToTracksAutomatically;
2014 }
2015
2016 setSubscribeToTracksAutomatically(enabled) {
2017 if (this._meetingState !== DAILY_STATE_JOINED) {
2018 throw new Error(
2019 'setSubscribeToTracksAutomatically() is only allowed when joined'
2020 );
2021 }
2022 this._preloadCache.subscribeToTracksAutomatically = enabled;
2023 this.sendMessageToCallMachine({
2024 action: DAILY_METHOD_SET_SUBSCRIBE_TO_TRACKS_AUTOMATICALLY,
2025 enabled,
2026 });
2027 return this;
2028 }
2029
2030 async enumerateDevices() {
2031 methodNotSupportedInReactNative();
2032 if (this._callObjectMode) {
2033 let raw = await navigator.mediaDevices.enumerateDevices();
2034 return { devices: raw.map((d) => JSON.parse(JSON.stringify(d))) };
2035 }
2036
2037 return new Promise((resolve, _) => {
2038 let k = (msg) => {
2039 resolve({ devices: msg.devices });
2040 };
2041 this.sendMessageToCallMachine(
2042 { action: DAILY_METHOD_ENUMERATE_DEVICES },
2043 k
2044 );
2045 });
2046 }
2047
2048 sendAppMessage(data, to = '*') {
2049 if (JSON.stringify(data).length > MAX_APP_MSG_SIZE) {
2050 throw new Error(
2051 'Message data too large. Max size is ' + MAX_APP_MSG_SIZE
2052 );
2053 }
2054 this.sendMessageToCallMachine({ action: DAILY_METHOD_APP_MSG, data, to });
2055 return this;
2056 }
2057
2058 addFakeParticipant(args) {
2059 methodNotSupportedInReactNative();
2060 this.sendMessageToCallMachine({
2061 action: DAILY_METHOD_ADD_FAKE_PARTICIPANT,
2062 ...args,
2063 });
2064 return this;
2065 }
2066
2067 setShowNamesMode(mode) {
2068 methodNotSupportedInReactNative();
2069 if (mode && !(mode === 'always' || mode === 'never')) {
2070 console.error(
2071 'setShowNamesMode argument should be "always", "never", or false'
2072 );
2073 return this;
2074 }
2075 this.sendMessageToCallMachine({
2076 action: DAILY_METHOD_SET_SHOW_NAMES,
2077 mode: mode,
2078 });
2079 return this;
2080 }
2081
2082 setShowLocalVideo(show = true) {
2083 methodNotSupportedInReactNative();
2084 if (typeof show !== 'boolean') {
2085 console.error('setShowLocalVideo only accepts a boolean value');
2086 return this;
2087 }
2088 if (this._callObjectMode) {
2089 console.error('setShowLocalVideo is not available in callObject mode');
2090 return this;
2091 }
2092 if (this._meetingState !== DAILY_STATE_JOINED) {
2093 console.error(
2094 'the meeting must be joined before calling setShowLocalVideo'
2095 );
2096 return this;
2097 }
2098 this.sendMessageToCallMachine({
2099 action: DAILY_METHOD_SET_SHOW_LOCAL_VIDEO,
2100 show,
2101 });
2102 this._showLocalVideo = show;
2103 return this;
2104 }
2105
2106 showLocalVideo() {
2107 methodNotSupportedInReactNative();
2108 if (this._callObjectMode) {
2109 console.error('showLocalVideo is not available in callObject mode');
2110 return this;
2111 }
2112 return this._showLocalVideo;
2113 }
2114
2115 setShowParticipantsBar(show = true) {
2116 methodNotSupportedInReactNative();
2117 if (typeof show !== 'boolean') {
2118 console.error('setShowParticipantsBar only accepts a boolean value');
2119 return this;
2120 }
2121 if (this._callObjectMode) {
2122 console.error(
2123 'setShowParticipantsBar is not available in callObject mode'
2124 );
2125 return this;
2126 }
2127 if (this._meetingState !== DAILY_STATE_JOINED) {
2128 console.error(
2129 'the meeting must be joined before calling setShowParticipantsBar'
2130 );
2131 return this;
2132 }
2133 this.sendMessageToCallMachine({
2134 action: DAILY_METHOD_SET_SHOW_PARTICIPANTS_BAR,
2135 show,
2136 });
2137 this._showParticipantsBar = show;
2138 return this;
2139 }
2140
2141 showParticipantsBar() {
2142 methodNotSupportedInReactNative();
2143 if (this._callObjectMode) {
2144 console.error('showParticipantsBar is not available in callObject mode');
2145 return this;
2146 }
2147 return this._showParticipantsBar;
2148 }
2149
2150 theme() {
2151 if (this._callObjectMode) {
2152 console.error('theme is not available in callObject mode');
2153 return this;
2154 }
2155 return this.properties.theme;
2156 }
2157
2158 setTheme(theme) {
2159 return new Promise((resolve, reject) => {
2160 if (this._callObjectMode) {
2161 reject('setTheme is not available in callObject mode');
2162 return;
2163 }
2164 try {
2165 this.validateProperties({
2166 theme,
2167 });
2168 this.properties.theme = {
2169 ...theme,
2170 };
2171 // Send message to Prebuilt UI Iframe driver
2172 this.sendMessageToCallMachine({
2173 action: DAILY_METHOD_SET_THEME,
2174 theme: this.properties.theme,
2175 });
2176 /**
2177 * For simplicity, emitting theme-updated here rather than
2178 * listening for it from Prebuilt & re-emitting it, since:
2179 * - we've fully validated the theme, so there's no risk of it not being applied
2180 * - we set `this.properties.theme` first, so in a customer's `theme-updated`
2181 * handler, a call to `theme()` will return the latest value
2182 * - this method is the only way `theme-updated` can change
2183 */
2184 try {
2185 this.emit(DAILY_EVENT_THEME_UPDATED, {
2186 action: DAILY_EVENT_THEME_UPDATED,
2187 theme: this.properties.theme,
2188 });
2189 } catch (e) {
2190 console.log("could not emit 'theme-updated'", e);
2191 }
2192 resolve(this.properties.theme);
2193 } catch (e) {
2194 reject(e);
2195 }
2196 });
2197 }
2198
2199 detectAllFaces() {
2200 methodNotSupportedInReactNative();
2201 return new Promise((resolve, _) => {
2202 let k = (msg) => {
2203 delete msg.action;
2204 delete msg.callbackStamp;
2205 resolve(msg);
2206 };
2207 this.sendMessageToCallMachine(
2208 { action: DAILY_METHOD_DETECT_ALL_FACES },
2209 k
2210 );
2211 });
2212 }
2213
2214 async requestFullscreen() {
2215 methodNotSupportedInReactNative();
2216 if (!this._iframe || document.fullscreenElement) {
2217 return;
2218 }
2219 try {
2220 (await this._iframe.requestFullscreen)
2221 ? this._iframe.requestFullscreen()
2222 : this._iframe.webkitRequestFullscreen();
2223 } catch (e) {
2224 console.log('could not make video call fullscreen', e);
2225 }
2226 }
2227
2228 exitFullscreen() {
2229 methodNotSupportedInReactNative();
2230 if (document.fullscreenElement) {
2231 document.exitFullscreen();
2232 } else if (document.webkitFullscreenElement) {
2233 document.webkitExitFullscreen();
2234 }
2235 }
2236
2237 async room({ includeRoomConfigDefaults = true } = {}) {
2238 if (this._meetingState === DAILY_STATE_JOINED || this._didPreAuth) {
2239 // We've succesfully join()ed or preAuth()ed, so we should have room info.
2240 return new Promise((resolve, _) => {
2241 let k = (msg) => {
2242 delete msg.action;
2243 delete msg.callbackStamp;
2244 resolve(msg);
2245 };
2246 this.sendMessageToCallMachine(
2247 { action: DAILY_METHOD_ROOM, includeRoomConfigDefaults },
2248 k
2249 );
2250 });
2251 } else {
2252 // Return the URL of the room we'll be in if/when we successfully join(),
2253 // since we have no other room info to show yet.
2254 if (this.properties.url) {
2255 // NOTE: technically this should be called "roomUrlPendingJoinOrPreauth"
2256 // to indicate that *either* a join() or a preAuth() will allow you to
2257 // access room info, but preAuth() was added later and this name was
2258 // preserved to maintain backward compatibility: if a consumer hasn't
2259 // updated their app to use preAuth(), they'll be none the wiser.
2260 return { roomUrlPendingJoin: this.properties.url };
2261 }
2262 return null;
2263 }
2264 }
2265
2266 async geo() {
2267 return new Promise(async (resolve, _) => {
2268 try {
2269 let url = 'https://gs.daily.co/_ks_/x-swsl/:';
2270 let res = await fetch(url);
2271 let data = await res.json();
2272 resolve({ current: data.geo });
2273 } catch (e) {
2274 console.error('geo lookup failed', e);
2275 resolve({ current: '' });
2276 }
2277 });
2278 }
2279
2280 async setNetworkTopology(opts) {
2281 methodNotSupportedInReactNative();
2282 return new Promise(async (resolve, reject) => {
2283 let k = (msg) => {
2284 if (msg.error) {
2285 reject({ error: msg.error });
2286 } else {
2287 resolve({ workerId: msg.workerId });
2288 }
2289 };
2290 this.sendMessageToCallMachine(
2291 { action: DAILY_METHOD_SET_NETWORK_TOPOLOGY, opts },
2292 k
2293 );
2294 });
2295 }
2296
2297 async getNetworkTopology() {
2298 return new Promise(async (resolve, reject) => {
2299 let k = (msg) => {
2300 if (msg.error) {
2301 reject({ error: msg.error });
2302 } else {
2303 resolve({ topology: msg.topology });
2304 }
2305 };
2306 this.sendMessageToCallMachine(
2307 { action: DAILY_METHOD_GET_NETWORK_TOPOLOGY },
2308 k
2309 );
2310 });
2311 }
2312
2313 setPlayNewParticipantSound(arg) {
2314 methodNotSupportedInReactNative();
2315 if (!(typeof arg === 'number' || arg === true || arg === false)) {
2316 throw new Error(
2317 `argument to setShouldPlayNewParticipantSound should be true, false, or a number, but is ${arg}`
2318 );
2319 }
2320 this.sendMessageToCallMachine({ action: DAILY_METHOD_SET_PLAY_DING, arg });
2321 }
2322
2323 on(eventName, k) {
2324 this._inputEventsOn[eventName] = {};
2325 this.sendMessageToCallMachine({
2326 action: DAILY_METHOD_REGISTER_INPUT_HANDLER,
2327 on: eventName,
2328 });
2329 return EventEmitter.prototype.on.call(this, eventName, k);
2330 }
2331
2332 // todo: once is almost certainly implemented incorrectly. read the
2333 // EventEmitter source to figure out how to do this properly. since
2334 // overriding on/off/once are optimizations, anyway, we won't worry
2335 // about it right now.
2336 once(eventName, k) {
2337 this._inputEventsOn[eventName] = {};
2338 this.sendMessageToCallMachine({
2339 action: DAILY_METHOD_REGISTER_INPUT_HANDLER,
2340 on: eventName,
2341 });
2342 return EventEmitter.prototype.once.call(this, eventName, k);
2343 }
2344
2345 off(eventName, k) {
2346 delete this._inputEventsOn[eventName];
2347 this.sendMessageToCallMachine({
2348 action: DAILY_METHOD_REGISTER_INPUT_HANDLER,
2349 off: eventName,
2350 });
2351 return EventEmitter.prototype.off.call(this, eventName, k);
2352 }
2353
2354 //
2355 // internal methods
2356 //
2357
2358 validateProperties(properties) {
2359 for (var k in properties) {
2360 if (!FRAME_PROPS[k]) {
2361 throw new Error(`unrecognized property '${k}'`);
2362 }
2363 if (
2364 FRAME_PROPS[k].validate &&
2365 !FRAME_PROPS[k].validate(properties[k], this)
2366 ) {
2367 throw new Error(`property '${k}': ${FRAME_PROPS[k].help}`);
2368 }
2369 }
2370 }
2371
2372 assembleMeetingUrl() {
2373 // handle case of url with query string and without
2374 let props = {
2375 ...this.properties,
2376 emb: this._callFrameId,
2377 embHref: encodeURIComponent(window.location.href),
2378 },
2379 firstSep = props.url.match(/\?/) ? '&' : '?',
2380 url = props.url,
2381 urlProps = Object.keys(FRAME_PROPS).filter(
2382 (p) => FRAME_PROPS[p].queryString && props[p] !== undefined
2383 );
2384 let newQueryString = urlProps
2385 .map((p) => `${FRAME_PROPS[p].queryString}=${props[p]}`)
2386 .join('&');
2387 return url + firstSep + newQueryString;
2388 }
2389
2390 // Note that even if the below method returns true, load() may decide that
2391 // there's nothing more to do (e.g. in the case that the call object has
2392 // already been loaded once) and simply carry out the appropriate meeting
2393 // state transition.
2394 needsLoad() {
2395 // NOTE: The *only* reason DAILY_STATE_LOADING is here is to preserve a bug
2396 // that I (@kompfner) am a bit hesitant to fix until more time can be
2397 // dedicated to doing the *right* fix. If we're in DAILY_STATE_LOADING, we
2398 // probably *shouldn't* let you trigger another load() and get into a weird
2399 // state, but this has been long-standing behavior. The alternative would mean
2400 // that, if load() failed silently for some reason, you couldn't re-trigger it
2401 // since we'd be stuck in the DAILY_STATE_LOADING state.
2402 return [
2403 DAILY_STATE_NEW,
2404 DAILY_STATE_LOADING,
2405 DAILY_STATE_LEFT,
2406 DAILY_STATE_ERROR,
2407 ].includes(this._meetingState);
2408 }
2409
2410 sendMessageToCallMachine(message, callback) {
2411 this._messageChannel.sendMessageToCallMachine(
2412 message,
2413 callback,
2414 this._iframe,
2415 this._callFrameId
2416 );
2417 }
2418
2419 ///
2420 /// The below *packagedMessage* methods facilitate wiring up a DailyIframe
2421 /// instance as a remote driver of another DailyIframe instance, like in the
2422 /// new prebuilt UI case, where an "outer" callFrame controls an "inner"
2423 /// callObject through an intermediate iframed app.
2424 ///
2425
2426 forwardPackagedMessageToCallMachine(msg) {
2427 this._messageChannel.forwardPackagedMessageToCallMachine(
2428 msg,
2429 this._iframe,
2430 this._callFrameId
2431 );
2432 }
2433
2434 addListenerForPackagedMessagesFromCallMachine(listener) {
2435 return this._messageChannel.addListenerForPackagedMessagesFromCallMachine(
2436 listener,
2437 this._callFrameId
2438 );
2439 }
2440
2441 removeListenerForPackagedMessagesFromCallMachine(listenerId) {
2442 this._messageChannel.removeListenerForPackagedMessagesFromCallMachine(
2443 listenerId
2444 );
2445 }
2446
2447 handleMessageFromCallMachine(msg) {
2448 switch (msg.action) {
2449 case DAILY_EVENT_IFRAME_READY_FOR_LAUNCH_CONFIG:
2450 this.sendMessageToCallMachine({
2451 action: DAILY_EVENT_IFRAME_LAUNCH_CONFIG,
2452 ...this.properties,
2453 });
2454 break;
2455 case DAILY_EVENT_LOADED:
2456 if (this._loadedCallback) {
2457 this._loadedCallback();
2458 this._loadedCallback = null;
2459 }
2460 try {
2461 this.emit(msg.action, msg);
2462 } catch (e) {
2463 console.log('could not emit', msg, e);
2464 }
2465 break;
2466 case DAILY_EVENT_JOINED_MEETING:
2467 if (this._joinedCallback) {
2468 this._joinedCallback(msg.participants);
2469 this._joinedCallback = null;
2470 }
2471 try {
2472 this.emit(msg.action, msg);
2473 } catch (e) {
2474 console.log('could not emit', msg, e);
2475 }
2476 break;
2477 case DAILY_EVENT_PARTICIPANT_JOINED:
2478 case DAILY_EVENT_PARTICIPANT_UPDATED:
2479 if (this._meetingState === DAILY_STATE_LEFT) {
2480 return;
2481 }
2482 if (msg.participant && msg.participant.session_id) {
2483 let id = msg.participant.local ? 'local' : msg.participant.session_id;
2484 if (this._callObjectMode) {
2485 Participant.addTracks(msg.participant);
2486 Participant.addCustomTracks(msg.participant);
2487 Participant.addLegacyTracks(
2488 msg.participant,
2489 this._participants[id]
2490 );
2491 }
2492
2493 try {
2494 // track events
2495 this.maybeEventTrackStopped(
2496 this._participants[id],
2497 msg.participant,
2498 'audioTrack'
2499 );
2500 this.maybeEventTrackStopped(
2501 this._participants[id],
2502 msg.participant,
2503 'videoTrack'
2504 );
2505 this.maybeEventTrackStopped(
2506 this._participants[id],
2507 msg.participant,
2508 'screenVideoTrack'
2509 );
2510 this.maybeEventTrackStopped(
2511 this._participants[id],
2512 msg.participant,
2513 'screenAudioTrack'
2514 );
2515 this.maybeEventTrackStarted(
2516 this._participants[id],
2517 msg.participant,
2518 'audioTrack'
2519 );
2520 this.maybeEventTrackStarted(
2521 this._participants[id],
2522 msg.participant,
2523 'videoTrack'
2524 );
2525 this.maybeEventTrackStarted(
2526 this._participants[id],
2527 msg.participant,
2528 'screenVideoTrack'
2529 );
2530 this.maybeEventTrackStarted(
2531 this._participants[id],
2532 msg.participant,
2533 'screenAudioTrack'
2534 );
2535 // custom tracks (presumably we'll do all tracks consistently in the
2536 // future, refactoring the above maybeEventTrack* events)
2537 this.maybeEventTrackStoppedForCustomTracks(
2538 this._participants[id],
2539 msg.participant
2540 );
2541 this.maybeEventTrackStartedForCustomTracks(
2542 this._participants[id],
2543 msg.participant
2544 );
2545
2546 // recording events
2547 this.maybeEventRecordingStopped(
2548 this._participants[id],
2549 msg.participant
2550 );
2551 this.maybeEventRecordingStarted(
2552 this._participants[id],
2553 msg.participant
2554 );
2555 } catch (e) {
2556 console.error('track events error', e);
2557 }
2558 // participant joined/updated events
2559 if (
2560 !this.compareEqualForParticipantUpdateEvent(
2561 msg.participant,
2562 this._participants[id]
2563 )
2564 ) {
2565 this._participants[id] = { ...msg.participant };
2566 this.toggleParticipantAudioBasedOnNativeAudioFocus();
2567 try {
2568 this.emit(msg.action, msg);
2569 } catch (e) {
2570 console.log('could not emit', msg, e);
2571 }
2572 }
2573 }
2574 break;
2575 case DAILY_EVENT_PARTICIPANT_LEFT:
2576 if (msg.participant && msg.participant.session_id) {
2577 // track events
2578 let prevP = this._participants[msg.participant.session_id];
2579 if (prevP) {
2580 this.maybeEventTrackStopped(prevP, null, 'audioTrack');
2581 this.maybeEventTrackStopped(prevP, null, 'videoTrack');
2582 this.maybeEventTrackStopped(prevP, null, 'screenVideoTrack');
2583 this.maybeEventTrackStopped(prevP, null, 'screenAudioTrack');
2584 this.maybeEventTrackStoppedForCustomTracks(prevP, null);
2585 }
2586 // delete from local cach
2587 delete this._participants[msg.participant.session_id];
2588 try {
2589 this.emit(msg.action, msg);
2590 } catch (e) {
2591 console.log('could not emit', msg, e);
2592 }
2593 }
2594 break;
2595 case DAILY_EVENT_ACCESS_STATE_UPDATED:
2596 let newAccessState = {
2597 access: msg.access,
2598 };
2599 if (msg.awaitingAccess) {
2600 newAccessState.awaitingAccess = msg.awaitingAccess;
2601 }
2602 if (!deepEqual(this._accessState, newAccessState)) {
2603 this._accessState = newAccessState;
2604 try {
2605 this.emit(msg.action, msg);
2606 } catch (e) {
2607 console.log('could not emit', msg, e);
2608 }
2609 }
2610 break;
2611 case DAILY_EVENT_MEETING_SESSION_UPDATED:
2612 if (msg.meetingSession) {
2613 try {
2614 delete msg.callFrameId;
2615 this.emit(msg.action, msg);
2616 } catch (e) {
2617 console.log('could not emit', msg, e);
2618 }
2619 }
2620 break;
2621 case DAILY_EVENT_ERROR:
2622 if (this._iframe && !msg.preserveIframe) {
2623 this._iframe.src = '';
2624 }
2625 this.updateMeetingState(DAILY_STATE_ERROR);
2626 this.resetMeetingDependentVars();
2627 if (this._loadedCallback) {
2628 this._loadedCallback(msg.errorMsg);
2629 this._loadedCallback = null;
2630 }
2631 if (this._joinedCallback) {
2632 this._joinedCallback(null, msg.errorMsg);
2633 this._joinedCallback = null;
2634 }
2635 try {
2636 let { preserveIframe, ...event } = msg;
2637 this.emit(msg.action, event);
2638 } catch (e) {
2639 console.log('could not emit', msg, e);
2640 }
2641 break;
2642 case DAILY_EVENT_LEFT_MEETING:
2643 if (this._meetingState !== DAILY_STATE_ERROR) {
2644 this.updateMeetingState(DAILY_STATE_LEFT);
2645 }
2646 this.resetMeetingDependentVars();
2647 try {
2648 this.emit(msg.action, msg);
2649 } catch (e) {
2650 console.log('could not emit', msg, e);
2651 }
2652 break;
2653 case DAILY_EVENT_INPUT_EVENT:
2654 let p = this._participants[msg.session_id];
2655 if (!p) {
2656 if (msg.session_id === this._participants.local.session_id) {
2657 p = this._participants.local;
2658 } else {
2659 p = {};
2660 }
2661 }
2662 try {
2663 this.emit(msg.event.type, {
2664 action: msg.event.type,
2665 event: msg.event,
2666 participant: { ...p },
2667 });
2668 } catch (e) {
2669 console.log('could not emit', msg, e);
2670 }
2671 break;
2672 case DAILY_EVENT_NETWORK_QUALITY_CHANGE:
2673 let { threshold, quality } = msg;
2674 if (
2675 threshold !== this._network.threshold ||
2676 quality !== this._network.quality
2677 ) {
2678 this._network.quality = quality;
2679 this._network.threshold = threshold;
2680 try {
2681 this.emit(msg.action, msg);
2682 } catch (e) {
2683 console.log('could not emit', msg, e);
2684 }
2685 }
2686 break;
2687 case DAILY_EVENT_ACTIVE_SPEAKER_CHANGE:
2688 let { activeSpeaker } = msg;
2689 if (this._activeSpeaker.peerId !== activeSpeaker.peerId) {
2690 this._activeSpeaker.peerId = activeSpeaker.peerId;
2691 try {
2692 this.emit(msg.action, {
2693 action: msg.action,
2694 activeSpeaker: this._activeSpeaker,
2695 });
2696 } catch (e) {
2697 console.log('could not emit', msg, e);
2698 }
2699 }
2700 break;
2701 case DAILY_EVENT_SHOW_LOCAL_VIDEO_CHANGED:
2702 if (this._callObjectMode) return;
2703 const { show } = msg;
2704 this._showLocalVideo = show;
2705 try {
2706 this.emit(msg.action, {
2707 action: msg.action,
2708 show,
2709 });
2710 } catch (e) {
2711 console.log('could not emit', msg, e);
2712 }
2713 break;
2714 case DAILY_EVENT_ACTIVE_SPEAKER_MODE_CHANGE:
2715 const { enabled } = msg;
2716 if (this._activeSpeakerMode !== enabled) {
2717 this._activeSpeakerMode = enabled;
2718 try {
2719 this.emit(msg.action, {
2720 action: msg.action,
2721 enabled: this._activeSpeakerMode,
2722 });
2723 } catch (e) {
2724 console.log('could not emit', msg, e);
2725 }
2726 }
2727 break;
2728 case DAILY_EVENT_WAITING_PARTICIPANT_ADDED:
2729 case DAILY_EVENT_WAITING_PARTICIPANT_UPDATED:
2730 case DAILY_EVENT_WAITING_PARTICIPANT_REMOVED:
2731 this._waitingParticipants = msg.allWaitingParticipants;
2732 try {
2733 this.emit(msg.action, {
2734 action: msg.action,
2735 participant: msg.participant,
2736 });
2737 } catch (e) {
2738 console.log('could not emit', msg, e);
2739 }
2740 break;
2741 case DAILY_EVENT_RECEIVE_SETTINGS_UPDATED:
2742 // NOTE: doing equality check here rather than before sending message in
2743 // the first place from call machine, to simplify handling initial
2744 // receive settings
2745 if (!deepEqual(this._receiveSettings, msg.receiveSettings)) {
2746 this._receiveSettings = msg.receiveSettings;
2747 try {
2748 this.emit(msg.action, {
2749 action: msg.action,
2750 receiveSettings: msg.receiveSettings,
2751 });
2752 } catch (e) {
2753 console.log('could not emit', msg, e);
2754 }
2755 }
2756 break;
2757 case DAILY_EVENT_INPUT_SETTINGS_UPDATED:
2758 // NOTE: doing equality check here rather than before sending message in
2759 // the first place from call machine, to simplify handling initial
2760 // input settings
2761 if (!deepEqual(this._inputSettings, msg.inputSettings)) {
2762 this._inputSettings = msg.inputSettings;
2763 try {
2764 this.emit(msg.action, {
2765 action: msg.action,
2766 inputSettings: msg.inputSettings,
2767 });
2768 } catch (e) {
2769 console.log('could not emit', msg, e);
2770 }
2771 }
2772 break;
2773
2774 case DAILY_EVENT_RECORDING_STARTED:
2775 case DAILY_EVENT_RECORDING_STOPPED:
2776 case DAILY_EVENT_RECORDING_STATS:
2777 case DAILY_EVENT_RECORDING_ERROR:
2778 case DAILY_EVENT_RECORDING_UPLOAD_COMPLETED:
2779 case DAILY_EVENT_TRANSCRIPTION_STARTED:
2780 case DAILY_EVENT_TRANSCRIPTION_STOPPED:
2781 case DAILY_EVENT_TRANSCRIPTION_ERROR:
2782 case DAILY_EVENT_STARTED_CAMERA:
2783 case DAILY_EVENT_CAMERA_ERROR:
2784 case DAILY_EVENT_APP_MSG:
2785 case DAILY_EVENT_LOCAL_SCREEN_SHARE_STARTED:
2786 case DAILY_EVENT_LOCAL_SCREEN_SHARE_STOPPED:
2787 case DAILY_EVENT_NETWORK_CONNECTION:
2788 case DAILY_EVENT_RECORDING_DATA:
2789 case DAILY_EVENT_LIVE_STREAMING_STARTED:
2790 case DAILY_EVENT_LIVE_STREAMING_STOPPED:
2791 case DAILY_EVENT_LIVE_STREAMING_ERROR:
2792 case DAILY_EVENT_NONFATAL_ERROR:
2793 case DAILY_EVENT_LANG_UPDATED:
2794 case DAILY_EVENT_MEDIA_INGEST_ERROR:
2795 try {
2796 this.emit(msg.action, msg);
2797 } catch (e) {
2798 console.log('could not emit', msg, e);
2799 }
2800 break;
2801 case DAILY_UI_REQUEST_FULLSCREEN:
2802 this.requestFullscreen();
2803 break;
2804 case DAILY_UI_EXIT_FULLSCREEN:
2805 this.exitFullscreen();
2806 break;
2807 default: // no op
2808 }
2809 }
2810
2811 maybeEventRecordingStopped(prevP, thisP) {
2812 const key = 'record';
2813 if (!prevP) {
2814 return;
2815 }
2816 if (!thisP.local && thisP[key] === false && prevP[key] !== thisP[key]) {
2817 try {
2818 this.emit(DAILY_EVENT_RECORDING_STOPPED, {
2819 action: DAILY_EVENT_RECORDING_STOPPED,
2820 });
2821 } catch (e) {
2822 console.log('could not emit', e);
2823 }
2824 }
2825 }
2826
2827 maybeEventRecordingStarted(prevP, thisP) {
2828 const key = 'record';
2829 if (!prevP) {
2830 return;
2831 }
2832 if (!thisP.local && thisP[key] === true && prevP[key] !== thisP[key]) {
2833 try {
2834 this.emit(DAILY_EVENT_RECORDING_STARTED, {
2835 action: DAILY_EVENT_RECORDING_STARTED,
2836 });
2837 } catch (e) {
2838 console.log('could not emit', e);
2839 }
2840 }
2841 }
2842
2843 maybeEventTrackStopped(prevP, thisP, key) {
2844 if (!prevP) {
2845 return;
2846 }
2847 if (
2848 (prevP[key] && prevP[key].readyState === 'ended') ||
2849 (prevP[key] && !(thisP && thisP[key])) ||
2850 (prevP[key] && prevP[key].id !== thisP[key].id)
2851 ) {
2852 try {
2853 this.emit(DAILY_EVENT_TRACK_STOPPED, {
2854 action: DAILY_EVENT_TRACK_STOPPED,
2855 track: prevP[key],
2856 participant: thisP,
2857 });
2858 } catch (e) {
2859 console.log('could not emit', e);
2860 }
2861 }
2862 }
2863
2864 maybeEventTrackStarted(prevP, thisP, key) {
2865 if (
2866 (thisP[key] && !(prevP && prevP[key])) ||
2867 (thisP[key] && prevP[key].readyState === 'ended') ||
2868 (thisP[key] && thisP[key].id !== prevP[key].id)
2869 ) {
2870 try {
2871 this.emit(DAILY_EVENT_TRACK_STARTED, {
2872 action: DAILY_EVENT_TRACK_STARTED,
2873 track: thisP[key],
2874 participant: thisP,
2875 });
2876 } catch (e) {
2877 console.log('could not emit', e);
2878 }
2879 }
2880 }
2881
2882 maybeEventTrackStoppedForCustomTracks(prevP, thisP) {
2883 if (!prevP) {
2884 return;
2885 }
2886 for (const trackKey in prevP.tracks) {
2887 // we might be able to use this logic for all tracks, not just additional,
2888 // non-standard tracks. but for now, we'll only handle the non-standard
2889 // tracks
2890 if (Participant.isPredefinedTrack(trackKey)) {
2891 continue;
2892 }
2893 this.maybeEventTrackStopped(
2894 prevP.tracks[trackKey],
2895 thisP ? thisP.tracks[trackKey] : null,
2896 'track'
2897 );
2898 }
2899 }
2900
2901 maybeEventTrackStartedForCustomTracks(prevP, thisP) {
2902 if (!thisP) {
2903 return;
2904 }
2905 for (const trackKey in thisP.tracks) {
2906 // we might be able to use this logic for all tracks, not just additional,
2907 // non-standard tracks. but for now, we'll only handle the non-standard
2908 // tracks
2909 if (Participant.isPredefinedTrack(trackKey)) {
2910 continue;
2911 }
2912 this.maybeEventTrackStarted(
2913 prevP ? prevP.tracks[trackKey] : null,
2914 thisP.tracks[trackKey],
2915 'track'
2916 );
2917 }
2918 }
2919
2920 compareEqualForParticipantUpdateEvent(a, b) {
2921 if (!deepEqual(a, b)) {
2922 return false;
2923 }
2924 if (
2925 a.videoTrack &&
2926 b.videoTrack &&
2927 (a.videoTrack.id !== b.videoTrack.id ||
2928 a.videoTrack.muted !== b.videoTrack.muted ||
2929 a.videoTrack.enabled !== b.videoTrack.enabled)
2930 ) {
2931 return false;
2932 }
2933 if (
2934 a.audioTrack &&
2935 b.audioTrack &&
2936 (a.audioTrack.id !== b.audioTrack.id ||
2937 a.audioTrack.muted !== b.audioTrack.muted ||
2938 a.audioTrack.enabled !== b.audioTrack.enabled)
2939 ) {
2940 return false;
2941 }
2942 return true;
2943 }
2944
2945 nativeUtils() {
2946 if (!isReactNative()) {
2947 return null;
2948 }
2949 if (typeof DailyNativeUtils === 'undefined') {
2950 console.warn(
2951 'in React Native, DailyNativeUtils is expected to be available'
2952 );
2953 return null;
2954 }
2955 return DailyNativeUtils;
2956 }
2957
2958 updateIsPreparingToJoin(isPreparingToJoin) {
2959 this.updateMeetingState(this._meetingState, isPreparingToJoin);
2960 }
2961
2962 updateMeetingState(
2963 meetingState,
2964 isPreparingToJoin = this._isPreparingToJoin
2965 ) {
2966 // If state hasn't changed, bail
2967 if (
2968 meetingState === this._meetingState &&
2969 isPreparingToJoin === this._isPreparingToJoin
2970 ) {
2971 return;
2972 }
2973
2974 // Update state
2975 const oldMeetingState = this._meetingState;
2976 const oldIsPreparingToJoin = this._isPreparingToJoin;
2977 this._meetingState = meetingState;
2978 this._isPreparingToJoin = isPreparingToJoin;
2979
2980 // Update state side-effects (which, for now, all depend on whether
2981 // isMeetingPendingOrOngoing)
2982 const oldIsMeetingPendingOrOngoing = this.isMeetingPendingOrOngoing(
2983 oldMeetingState,
2984 oldIsPreparingToJoin
2985 );
2986 const isMeetingPendingOrOngoing = this.isMeetingPendingOrOngoing(
2987 this._meetingState,
2988 this._isPreparingToJoin
2989 );
2990 if (oldIsMeetingPendingOrOngoing === isMeetingPendingOrOngoing) {
2991 return;
2992 }
2993 this.updateKeepDeviceAwake(isMeetingPendingOrOngoing);
2994 this.updateDeviceAudioMode(isMeetingPendingOrOngoing);
2995 this.updateShowAndroidOngoingMeetingNotification(isMeetingPendingOrOngoing);
2996 this.updateNoOpRecordingEnsuringBackgroundContinuity(
2997 isMeetingPendingOrOngoing
2998 );
2999 }
3000
3001 // To be invoked this when leaving or erroring out of a meeting.
3002 // NOTE (Paul, 2021-01-07): this could probably be expanded to reset *all*
3003 // meeting-dependent vars, but starting with this targeted small set which
3004 // were being reset properly on leave() but not when leaving via prebuilt ui.
3005 resetMeetingDependentVars() {
3006 this._participants = {};
3007 this._waitingParticipants = {};
3008 this._activeSpeaker = {};
3009 this._activeSpeakerMode = false;
3010 this._didPreAuth = false;
3011 this._accessState = { access: DAILY_ACCESS_UNKNOWN };
3012 this._receiveSettings = {};
3013 this._inputSettings = {};
3014 resetPreloadCache(this._preloadCache);
3015 }
3016
3017 updateKeepDeviceAwake(keepAwake) {
3018 if (!isReactNative()) {
3019 return;
3020 }
3021 this.nativeUtils().setKeepDeviceAwake(keepAwake, this._callFrameId);
3022 }
3023
3024 updateDeviceAudioMode(useInCallAudioMode) {
3025 if (
3026 !isReactNative() ||
3027 this.disableReactNativeAutoDeviceManagement('audio')
3028 ) {
3029 return;
3030 }
3031 const audioMode = useInCallAudioMode
3032 ? this._nativeInCallAudioMode
3033 : NATIVE_AUDIO_MODE_IDLE;
3034 this.nativeUtils().setAudioMode(audioMode);
3035 }
3036
3037 // Note: notification properties can't be changed while it is ongoing
3038 updateShowAndroidOngoingMeetingNotification(showNotification) {
3039 // Check that we're React Native and that the Android-only method exists
3040 if (
3041 !(isReactNative() && this.nativeUtils().setShowOngoingMeetingNotification)
3042 ) {
3043 return;
3044 }
3045 // Use current this.properties to customize notification behavior
3046 let title, subtitle, iconName, disableForCustomOverride;
3047 if (
3048 this.properties.reactNativeConfig &&
3049 this.properties.reactNativeConfig.androidInCallNotification
3050 ) {
3051 ({
3052 title,
3053 subtitle,
3054 iconName,
3055 disableForCustomOverride,
3056 } = this.properties.reactNativeConfig.androidInCallNotification);
3057 }
3058 if (disableForCustomOverride) {
3059 showNotification = false;
3060 }
3061 this.nativeUtils().setShowOngoingMeetingNotification(
3062 showNotification,
3063 title,
3064 subtitle,
3065 iconName,
3066 this._callFrameId
3067 );
3068 }
3069
3070 // Whether to enable no-op audio recording to ensure continuity of the app
3071 // when backgrounded. Required in iOS to ensure we can finish joining when the
3072 // app is backgrounded before gUM is called, and to ensure that signaling
3073 // remains connected when we're in an empty room and our own cam and mic are
3074 // off.
3075 updateNoOpRecordingEnsuringBackgroundContinuity(enableNoOpRecording) {
3076 if (
3077 !(
3078 isReactNative() &&
3079 this.nativeUtils().enableNoOpRecordingEnsuringBackgroundContinuity
3080 )
3081 ) {
3082 return;
3083 }
3084 this.nativeUtils().enableNoOpRecordingEnsuringBackgroundContinuity(
3085 enableNoOpRecording
3086 );
3087 }
3088
3089 isMeetingPendingOrOngoing(meetingState, isPreparingToJoin) {
3090 return (
3091 [DAILY_STATE_JOINING, DAILY_STATE_JOINED].includes(meetingState) ||
3092 isPreparingToJoin
3093 );
3094 }
3095
3096 handleNativeAppActiveStateChange = (isActive) => {
3097 // If automatic video device management is disabled, bail
3098 if (this.disableReactNativeAutoDeviceManagement('video')) {
3099 return;
3100 }
3101 if (isActive) {
3102 // If cam was unmuted before losing focus, unmute
3103 // (Note this is assumption is not perfect, since theoretically an app
3104 // could unmute while in the background, but it's decent for now)
3105 if (this.camUnmutedBeforeLosingNativeActiveState) {
3106 this.setLocalVideo(true);
3107 }
3108 } else {
3109 this.camUnmutedBeforeLosingNativeActiveState = this.localVideo();
3110 // Mute cam, but check first whether we have local video in the first
3111 // place: if we don't, we may still be in the gUM process, with the app
3112 // "inactive" simply because it's behind the permissions dialogs.
3113 if (this.camUnmutedBeforeLosingNativeActiveState) {
3114 this.setLocalVideo(false);
3115 }
3116 }
3117 };
3118
3119 handleNativeAudioFocusChange = (hasFocus) => {
3120 // If automatic audio device management is disabled, bail
3121 if (this.disableReactNativeAutoDeviceManagement('audio')) {
3122 return;
3123 }
3124 this._hasNativeAudioFocus = hasFocus;
3125 // toggle participant audio if needed
3126 this.toggleParticipantAudioBasedOnNativeAudioFocus();
3127 // toggle mic mute if needed
3128 if (this._hasNativeAudioFocus) {
3129 // If mic was unmuted before losing focus, unmute
3130 // (Note this is assumption is not perfect, since theoretically an app
3131 // could unmute while in the background, but it's decent for now)
3132 if (this.micUnmutedBeforeLosingNativeAudioFocus) {
3133 this.setLocalAudio(true);
3134 }
3135 } else {
3136 this.micUnmutedBeforeLosingNativeAudioFocus = this.localAudio();
3137 this.setLocalAudio(false);
3138 }
3139 };
3140
3141 toggleParticipantAudioBasedOnNativeAudioFocus() {
3142 if (!isReactNative()) {
3143 return;
3144 }
3145 // Need to access store directly since when participant muted their audio we
3146 // don't have access to their audio tracks in this._participants
3147 const state = store.getState();
3148 for (const streamId in state.streams) {
3149 const streamData = state.streams[streamId];
3150 if (
3151 streamData &&
3152 streamData.pendingTrack &&
3153 streamData.pendingTrack.kind === 'audio'
3154 ) {
3155 streamData.pendingTrack.enabled = this._hasNativeAudioFocus;
3156 }
3157 }
3158 }
3159
3160 // type must be either 'audio' or 'video'
3161 disableReactNativeAutoDeviceManagement(type) {
3162 return (
3163 this.properties.reactNativeConfig &&
3164 this.properties.reactNativeConfig.disableAutoDeviceManagement &&
3165 this.properties.reactNativeConfig.disableAutoDeviceManagement[type]
3166 );
3167 }
3168
3169 absoluteUrl(url) {
3170 if ('undefined' === typeof url) {
3171 return undefined;
3172 }
3173 let a = document.createElement('a');
3174 a.href = url;
3175 return a.href;
3176 }
3177
3178 sayHello() {
3179 const str = 'hello, world.';
3180 console.log(str);
3181 return str;
3182 }
3183}
3184
3185function initializePreloadCache(callObject, properties) {
3186 return {
3187 subscribeToTracksAutomatically: true,
3188 audioDeviceId: null,
3189 videoDeviceId: null,
3190 outputDeviceId: null,
3191 };
3192}
3193
3194function resetPreloadCache(c) {
3195 // don't need to do anything, until we add stuff to the preload
3196 // cache that should not persist
3197}
3198
3199function makeSafeForPostMessage(props) {
3200 const safe = {};
3201 for (let p in props) {
3202 if (props[p] instanceof MediaStreamTrack) {
3203 // note: could store the track in a global variable for accessing
3204 // on the other side of the postMessage, here, instead of as we
3205 // currently do in the validate-properties routines, which definitely
3206 // is a spooky-action-at-a-distance code anti-pattern
3207 safe[p] = DAILY_CUSTOM_TRACK;
3208 } else if (p === 'dailyConfig') {
3209 if (props[p].modifyLocalSdpHook) {
3210 if (window._dailyConfig) {
3211 window._dailyConfig.modifyLocalSdpHook = props[p].modifyLocalSdpHook;
3212 }
3213 delete props[p].modifyLocalSdpHook;
3214 }
3215 if (props[p].modifyRemoteSdpHook) {
3216 if (window._dailyConfig) {
3217 window._dailyConfig.modifyRemoteSdpHook =
3218 props[p].modifyRemoteSdpHook;
3219 }
3220 delete props[p].modifyRemoteSdpHook;
3221 }
3222 safe[p] = props[p];
3223 } else {
3224 safe[p] = props[p];
3225 }
3226 }
3227 return safe;
3228}
3229
3230function methodNotSupportedInReactNative() {
3231 if (isReactNative()) {
3232 throw new Error(
3233 'This daily-js method is not currently supported in React Native'
3234 );
3235 }
3236}
3237
3238function methodOnlySupportedInReactNative() {
3239 if (!isReactNative()) {
3240 throw new Error('This daily-js method is only supported in React Native');
3241 }
3242}
3243
3244function validateReceiveSettings(
3245 receiveSettingsParam,
3246 { allowAllParticipantsKey }
3247) {
3248 const isParticipantIdValid = (participantId) => {
3249 const disallowedKeys = ['local'];
3250 if (!allowAllParticipantsKey) disallowedKeys.push('*');
3251 return participantId && !disallowedKeys.includes(participantId);
3252 };
3253 const areVideoReceiveSettingsValid = (videoReceiveSettings) => {
3254 if (videoReceiveSettings.layer !== undefined) {
3255 if (
3256 !(
3257 (Number.isInteger(videoReceiveSettings.layer) &&
3258 videoReceiveSettings.layer >= 0) ||
3259 videoReceiveSettings.layer === 'inherit'
3260 )
3261 ) {
3262 return false;
3263 }
3264 }
3265 return true;
3266 };
3267 // NOTE: partial receive settings *are* allowed, in both senses:
3268 // - only a subset of media types (e.g. only "video")
3269 // - only a subset of settings per media type (e.g. only "layer")
3270 const areParticipantReceiveSettingsValid = (receiveSettings) => {
3271 if (!receiveSettings) return false;
3272 if (receiveSettings.video) {
3273 if (!areVideoReceiveSettingsValid(receiveSettings.video)) {
3274 return false;
3275 }
3276 }
3277 if (receiveSettings.screenVideo) {
3278 if (!areVideoReceiveSettingsValid(receiveSettings.screenVideo)) {
3279 return false;
3280 }
3281 }
3282 return true;
3283 };
3284 for (const [participantId, receiveSettings] of Object.entries(
3285 receiveSettingsParam
3286 )) {
3287 if (
3288 !(
3289 isParticipantIdValid(participantId) &&
3290 areParticipantReceiveSettingsValid(receiveSettings)
3291 )
3292 ) {
3293 return false;
3294 }
3295 }
3296 return true;
3297}
3298
3299// Since currently videoProcessor is the only inputSetting. I wrote this code to reject
3300// everything else. I feel it is the safe approach. This will need changes as more
3301// functionality is added to inputSettings in the future.
3302function validateInputSettings(settings) {
3303 if (typeof settings !== 'object') return false;
3304 if (!(settings.video && typeof settings.video === 'object')) return false;
3305 if (!validateVideoProcessor(settings.video.processor)) return false;
3306 return true;
3307}
3308
3309function validateVideoProcessor(p) {
3310 if (!p) return false;
3311 if (typeof p !== 'object') return false;
3312 if (Object.keys(p).length === 0) return false; // lodash isEmpty did not work well with github workflow for some reason
3313 if (p.type && !validateVideoProcessorType(p.type)) return false;
3314 if (p.publish !== undefined && typeof p.publish !== 'boolean') return false;
3315 if (p.config) {
3316 if (typeof p.config !== 'object') return false;
3317 if (!validateVideoProcessorConfig(p.type, p.config)) return false;
3318 }
3319 return true;
3320}
3321
3322function validateVideoProcessorConfig(type, config) {
3323 let keys = Object.keys(config);
3324 if (keys.length === 0) return true;
3325 const configErrMsg =
3326 'invalid object in inputSettings -> video -> processor -> config';
3327 switch (type) {
3328 case VIDEO_PROCESSOR_TYPES.BGBLUR:
3329 if (keys.length > 1 || keys[0] !== 'strength') {
3330 throw new Error(configErrMsg);
3331 }
3332 if (
3333 typeof config.strength !== 'number' ||
3334 config.strength <= 0 ||
3335 config.strength > 1 ||
3336 isNaN(config.strength)
3337 ) {
3338 throw new Error(
3339 `${configErrMsg}; expected: {0 < strength <= 1}, got: ${config.strength}`
3340 );
3341 }
3342 default:
3343 return true;
3344 }
3345}
3346
3347function validateVideoProcessorType(type) {
3348 if (typeof type !== 'string') return false;
3349 return Object.values(VIDEO_PROCESSOR_TYPES).includes(type);
3350}
3351
3352function inputSettingsValidationHelpMsg() {
3353 let processorOpts = Object.values(VIDEO_PROCESSOR_TYPES).join(' | ');
3354 return `inputSettings must be of the form: { video: { processor: [ ${processorOpts} ] }, publish?: boolean, config?: {} }`;
3355}
3356
3357function receiveSettingsValidationHelpMsg({ allowAllParticipantsKey }) {
3358 return (
3359 `receiveSettings must be of the form { [<remote participant id> | ${DAILY_RECEIVE_SETTINGS_BASE_KEY}${
3360 allowAllParticipantsKey
3361 ? ` | "${DAILY_RECEIVE_SETTINGS_ALL_PARTICIPANTS_KEY}"`
3362 : ''
3363 }]: ` +
3364 '{ ' +
3365 '[video: [{ layer: [<non-negative integer> | "inherit"] } | "inherit"]], ' +
3366 '[screenVideo: [{ layer: [<non-negative integer> | "inherit"] } | "inherit"]] ' +
3367 '}}}'
3368 );
3369}
3370
3371function validateReactNativeConfig(config) {
3372 return validateConfigPropType(config, reactNativeConfigType);
3373}
3374
3375function validateConfigPropType(prop, propType) {
3376 if (propType === undefined) {
3377 return false;
3378 }
3379 switch (typeof propType) {
3380 case 'string':
3381 return typeof prop === propType;
3382 case 'object':
3383 if (typeof prop !== 'object') {
3384 return false;
3385 }
3386 for (const key in prop) {
3387 if (!validateConfigPropType(prop[key], propType[key])) {
3388 return false;
3389 }
3390 }
3391 return true;
3392 default:
3393 // console.error(
3394 // "Internal programming error: we've defined our config prop types wrong"
3395 // );
3396 return false;
3397 }
3398}