daily-examples/dailyjs/shared/contexts/useCallMachine.js

318 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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(true);
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 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 || knockingEnabled;
};
// --- Callbacks ---
/**
* Joins call (with the token, if applicable)
*/
const join = useCallback(
async (callObject) => {
setState(CALL_STATE_JOINING);
await callObject.join({ subscribeToTracksAutomatically, token, url });
setState(CALL_STATE_JOINED);
},
[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();
if (!redirectOnLeave) return;
setState(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]),
};
};