331 lines
9.0 KiB
JavaScript
331 lines
9.0 KiB
JavaScript
/**
|
||
* Call Machine hook
|
||
* --
|
||
* Manages the overaching state of a Daily call, including
|
||
* error handling, preAuth, joining, leaving etc.
|
||
*/
|
||
|
||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||
import DailyIframe from '@daily-co/daily-js';
|
||
|
||
import {
|
||
ACCESS_STATE_LOBBY,
|
||
ACCESS_STATE_NONE,
|
||
ACCESS_STATE_UNKNOWN,
|
||
MEETING_STATE_JOINED,
|
||
} from '../constants';
|
||
|
||
export const CALL_STATE_READY = 'ready';
|
||
export const CALL_STATE_LOBBY = 'lobby';
|
||
export const CALL_STATE_JOINING = 'joining';
|
||
export const CALL_STATE_JOINED = 'joined';
|
||
export const CALL_STATE_ENDED = 'ended';
|
||
export const CALL_STATE_ERROR = 'error';
|
||
export const CALL_STATE_FULL = 'full';
|
||
export const CALL_STATE_EXPIRED = 'expired';
|
||
export const CALL_STATE_NOT_BEFORE = 'nbf';
|
||
export const CALL_STATE_REMOVED = 'removed-from-call';
|
||
export const CALL_STATE_REDIRECTING = 'redirecting';
|
||
export const CALL_STATE_NOT_FOUND = 'not-found';
|
||
export const CALL_STATE_NOT_ALLOWED = 'not-allowed';
|
||
export const CALL_STATE_AWAITING_ARGS = 'awaiting-args';
|
||
|
||
export const useCallMachine = ({
|
||
domain,
|
||
room,
|
||
token,
|
||
subscribeToTracksAutomatically = true,
|
||
}) => {
|
||
const [daily, setDaily] = useState(null);
|
||
const [state, setState] = useState(CALL_STATE_READY);
|
||
const [redirectOnLeave, setRedirectOnLeave] = useState(false);
|
||
|
||
const url = useMemo(
|
||
() => (domain && room ? `https://${domain}.daily.co/${room}` : null),
|
||
[domain, room]
|
||
);
|
||
|
||
/**
|
||
* Check whether we show the lobby screen, need to knock or
|
||
* can head straight to the call. These parameters are set using
|
||
* `enable_knocking` and `enable_prejoin_ui` when creating the room
|
||
* @param co – Daily call object
|
||
*/
|
||
const prejoinUIEnabled = async (co) => {
|
||
const dailyRoomInfo = await co.room();
|
||
const { access } = co.accessState();
|
||
|
||
const prejoinEnabled =
|
||
dailyRoomInfo?.config?.enable_prejoin_ui === null
|
||
? !!dailyRoomInfo?.domainConfig?.enable_prejoin_ui
|
||
: !!dailyRoomInfo?.config?.enable_prejoin_ui;
|
||
|
||
const knockingEnabled = !!dailyRoomInfo?.config?.enable_knocking;
|
||
|
||
return (
|
||
prejoinEnabled ||
|
||
(access !== ACCESS_STATE_UNKNOWN &&
|
||
access?.level === ACCESS_STATE_LOBBY &&
|
||
knockingEnabled)
|
||
);
|
||
};
|
||
|
||
// --- Callbacks ---
|
||
|
||
/**
|
||
* Joins call (with the token, if applicable)
|
||
*/
|
||
const join = useCallback(
|
||
async (callObject) => {
|
||
setState(CALL_STATE_JOINING);
|
||
|
||
// Force mute clients when joining a call with experimental_optimize_large_calls enabled.
|
||
if (room?.config?.experimental_optimize_large_calls) {
|
||
callObject.setLocalAudio(false);
|
||
}
|
||
|
||
await callObject.join({ subscribeToTracksAutomatically, token, url });
|
||
setState(CALL_STATE_JOINED);
|
||
},
|
||
[room, token, subscribeToTracksAutomatically, url]
|
||
);
|
||
|
||
/**
|
||
* PreAuth checks whether we have access or need to knock
|
||
*/
|
||
const preAuth = useCallback(
|
||
async (co) => {
|
||
const { access } = await co.preAuth({
|
||
subscribeToTracksAutomatically,
|
||
token,
|
||
url,
|
||
});
|
||
|
||
// Private room and no `token` was passed
|
||
if (
|
||
access === ACCESS_STATE_UNKNOWN ||
|
||
access?.level === ACCESS_STATE_NONE
|
||
) {
|
||
return;
|
||
}
|
||
|
||
// Either `enable_knocking_ui` or `enable_prejoin_ui` is set to `true`
|
||
if (
|
||
access?.level === ACCESS_STATE_LOBBY ||
|
||
(await prejoinUIEnabled(co))
|
||
) {
|
||
setState(CALL_STATE_LOBBY);
|
||
return;
|
||
}
|
||
|
||
// Public room or private room with passed `token` and `enable_prejoin_ui` is `false`
|
||
join(co);
|
||
},
|
||
[join, subscribeToTracksAutomatically, token, url]
|
||
);
|
||
|
||
/**
|
||
* Leave call
|
||
*/
|
||
const leave = useCallback(() => {
|
||
if (!daily) return;
|
||
// If we're in the error state, we've already "left", so just clean up
|
||
if (state === CALL_STATE_ERROR) {
|
||
daily.destroy();
|
||
} else {
|
||
daily.leave();
|
||
}
|
||
}, [daily, state]);
|
||
|
||
/**
|
||
* Listen for access state updates
|
||
*/
|
||
const handleAccessStateUpdated = useCallback(
|
||
async ({ access }) => {
|
||
console.log(`🔑 Access level: ${access?.level}`);
|
||
|
||
/**
|
||
* Ignore initial access-state-updated event
|
||
*/
|
||
if (
|
||
[CALL_STATE_ENDED, CALL_STATE_AWAITING_ARGS, CALL_STATE_READY].includes(
|
||
state
|
||
)
|
||
) {
|
||
return;
|
||
}
|
||
|
||
if (
|
||
access === ACCESS_STATE_UNKNOWN ||
|
||
access?.level === ACCESS_STATE_NONE
|
||
) {
|
||
setState(CALL_STATE_NOT_ALLOWED);
|
||
return;
|
||
}
|
||
|
||
const meetingState = daily.meetingState();
|
||
if (
|
||
access?.level === ACCESS_STATE_LOBBY &&
|
||
meetingState === MEETING_STATE_JOINED
|
||
) {
|
||
// Already joined, no need to call join(daily) again.
|
||
return;
|
||
}
|
||
|
||
/**
|
||
* 'full' access, we can now join the meeting.
|
||
*/
|
||
join(daily);
|
||
},
|
||
[daily, state, join]
|
||
);
|
||
|
||
// --- Effects ---
|
||
|
||
/**
|
||
* Instantiate the call object and preauthenticate
|
||
*/
|
||
useEffect(() => {
|
||
if (daily || !url || state !== CALL_STATE_READY) return;
|
||
|
||
console.log('🚀 Creating call object');
|
||
|
||
const co = DailyIframe.createCallObject({
|
||
url,
|
||
dailyConfig: {
|
||
experimentalChromeVideoMuteLightOff: true,
|
||
useDevicePreferenceCookies: true,
|
||
},
|
||
});
|
||
|
||
setDaily(co);
|
||
preAuth(co);
|
||
}, [daily, url, state, preAuth]);
|
||
|
||
/**
|
||
* Listen for changes in the participant's access state
|
||
*/
|
||
useEffect(() => {
|
||
if (!daily) return false;
|
||
|
||
daily.on('access-state-updated', handleAccessStateUpdated);
|
||
return () => daily.off('access-state-updated', handleAccessStateUpdated);
|
||
}, [daily, handleAccessStateUpdated]);
|
||
|
||
/**
|
||
* Listen for and manage call state
|
||
*/
|
||
useEffect(() => {
|
||
if (!daily) return false;
|
||
|
||
const events = [
|
||
'joined-meeting',
|
||
'joining-meeting',
|
||
'left-meeting',
|
||
'error',
|
||
];
|
||
|
||
const handleMeetingState = async (ev) => {
|
||
const { access } = daily.accessState();
|
||
|
||
switch (ev.action) {
|
||
/**
|
||
* Don't transition to 'joining' or 'joined' UI as long as access is not 'full'.
|
||
* This means a request to join a private room is not granted, yet.
|
||
* Technically in requesting for access, the participant is already known
|
||
* to the room, but not joined, yet.
|
||
*/
|
||
case 'joining-meeting':
|
||
if (
|
||
access === ACCESS_STATE_UNKNOWN ||
|
||
access.level === ACCESS_STATE_NONE ||
|
||
access.level === ACCESS_STATE_LOBBY
|
||
) {
|
||
return;
|
||
}
|
||
setState(CALL_STATE_JOINING);
|
||
break;
|
||
case 'joined-meeting':
|
||
if (
|
||
access === ACCESS_STATE_UNKNOWN ||
|
||
access.level === ACCESS_STATE_NONE ||
|
||
access.level === ACCESS_STATE_LOBBY
|
||
) {
|
||
return;
|
||
}
|
||
setState(CALL_STATE_JOINED);
|
||
break;
|
||
case 'left-meeting':
|
||
daily.destroy();
|
||
setState(
|
||
!redirectOnLeave ? CALL_STATE_ENDED : CALL_STATE_REDIRECTING
|
||
);
|
||
break;
|
||
case 'error':
|
||
switch (ev?.error?.type) {
|
||
case 'nbf-room':
|
||
case 'nbf-token':
|
||
daily.destroy();
|
||
setState(CALL_STATE_NOT_BEFORE);
|
||
break;
|
||
case 'exp-room':
|
||
case 'exp-token':
|
||
daily.destroy();
|
||
setState(CALL_STATE_EXPIRED);
|
||
break;
|
||
case 'ejected':
|
||
daily.destroy();
|
||
setState(CALL_STATE_REMOVED);
|
||
break;
|
||
default:
|
||
switch (ev?.errorMsg) {
|
||
case 'Join request rejected':
|
||
// Join request to a private room was denied. We can end here.
|
||
setState(CALL_STATE_LOBBY);
|
||
daily.leave();
|
||
break;
|
||
case 'Meeting has ended':
|
||
// Meeting has ended or participant was removed by an owner.
|
||
daily.destroy();
|
||
setState(CALL_STATE_ENDED);
|
||
break;
|
||
case 'Meeting is full':
|
||
daily.destroy();
|
||
setState(CALL_STATE_FULL);
|
||
break;
|
||
case "The meeting you're trying to join does not exist.":
|
||
daily.destroy();
|
||
setState(CALL_STATE_NOT_FOUND);
|
||
break;
|
||
case 'You are not allowed to join this meeting':
|
||
daily.destroy();
|
||
setState(CALL_STATE_NOT_ALLOWED);
|
||
break;
|
||
default:
|
||
setState(CALL_STATE_ERROR);
|
||
break;
|
||
}
|
||
break;
|
||
}
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
};
|
||
|
||
// Listen for changes in state
|
||
events.forEach((event) => daily.on(event, handleMeetingState));
|
||
|
||
// Stop listening for changes in state
|
||
return () =>
|
||
events.forEach((event) => daily.off(event, handleMeetingState));
|
||
}, [daily, domain, room, redirectOnLeave]);
|
||
|
||
return {
|
||
daily,
|
||
leave,
|
||
setRedirectOnLeave,
|
||
state: useMemo(() => state, [state]),
|
||
};
|
||
};
|