Skip to content

Refactor to use track-started and track-stopped #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
316 changes: 182 additions & 134 deletions src/components/Call/Call.js
Original file line number Diff line number Diff line change
@@ -1,123 +1,211 @@
import React, { useEffect, useContext, useReducer, useCallback } from 'react';
import React, {
useEffect,
useContext,
useState,
useCallback,
useMemo,
} from 'react';
import './Call.css';
import Tile from '../Tile/Tile';
import CallObjectContext from '../../CallObjectContext';
import CallMessage from '../CallMessage/CallMessage';
import {
initialCallState,
CLICK_ALLOW_TIMEOUT,
PARTICIPANTS_CHANGE,
CAM_OR_MIC_ERROR,
FATAL_ERROR,
callReducer,
isLocal,
isScreenShare,
containsScreenShare,
getMessage,
} from './callState';
import { logDailyEvent } from '../../logUtils';

export default function Call() {
const callObject = useContext(CallObjectContext);
const [callState, dispatch] = useReducer(callReducer, initialCallState);

/**
* Start listening for participant changes, when the callObject is set.
*/
useEffect(() => {
if (!callObject) return;
const [participantUpdated, setParticipantUpdated] = useState(null);
const [participants, setParticipants] = useState([]);

const handleTrackStarted = useCallback((e) => {
logDailyEvent(e);
setParticipantUpdated(
`track-started-${e?.participant?.user_id}-${Date.now()}`
);
}, []);

const events = [
'participant-joined',
'participant-updated',
'participant-left',
];
const handleTrackStopped = useCallback((e) => {
logDailyEvent(e);
setParticipantUpdated(
`track-stopped-${e?.participant?.user_id}-${Date.now()}`
);
}, []);

function handleNewParticipantsState(event) {
event && logDailyEvent(event);
dispatch({
type: PARTICIPANTS_CHANGE,
participants: callObject.participants(),
});
}
const handleParticipantUpdate = useCallback((e) => {
logDailyEvent(e);
setParticipantUpdated(
`participant-updated-${e?.participant?.user_id}-${Date.now()}`
);
}, []);

// Use initial state
handleNewParticipantsState();
const handleErrorEvent = useCallback((e) => {
logDailyEvent(e);
getMessage(e);
}, []);

// Listen for changes in state
for (const event of events) {
callObject.on(event, handleNewParticipantsState);
}
const getMessage = (e) => {
let header = null;
let detail = null;
let isError = false;

// Stop listening for changes in state
return function cleanup() {
for (const event of events) {
callObject.off(event, handleNewParticipantsState);
if (!e) {
if (participants.length <= 1) {
header = "Copy and share this page's URL to invite others";
detail = window.location.href;
}
};
}, [callObject]);

/**
* Start listening for call errors, when the callObject is set.
*/
useEffect(() => {
if (!callObject) return;

function handleCameraErrorEvent(event) {
logDailyEvent(event);
dispatch({
type: CAM_OR_MIC_ERROR,
message:
(event && event.errorMsg && event.errorMsg.errorMsg) || 'Unknown',
});
} else if (e.action === 'error') {
header = `Fatal error ${(e && e.errorMsg) || 'Unknown'}`;
} else if (e.action === 'camera-error') {
header = `Camera or mic access error: ${
(e && e.errorMsg && e.errorMsg.errorMsg) || 'Unknown'
}`;
detail =
'See https://help.daily.co/en/articles/2528184-unblock-camera-mic-access-on-a-computer to troubleshoot.';
isError = true;
}

// We're making an assumption here: there is no camera error when callObject
// is first assigned.

callObject.on('camera-error', handleCameraErrorEvent);

return function cleanup() {
callObject.off('camera-error', handleCameraErrorEvent);
};
}, [callObject]);
return header || detail ? { header, detail, isError } : null;
};

/**
* Start listening for fatal errors, when the callObject is set.
* When the call object is set, listen and respond to events
*/
useEffect(() => {
if (!callObject) return;

function handleErrorEvent(e) {
logDailyEvent(e);
dispatch({
type: FATAL_ERROR,
message: (e && e.errorMsg) || 'Unknown',
});
}

// We're making an assumption here: there is no error when callObject is
// first assigned.

callObject.on('track-started', handleTrackStarted);
callObject.on('track-stopped', handleTrackStopped);
callObject.on('participant-updated', handleParticipantUpdate);
callObject.on('error', handleErrorEvent);
callObject.on('camera-error', handleErrorEvent);

return function cleanup() {
return () => {
callObject.off('track-started', handleTrackStarted);
callObject.off('track-stopped', handleTrackStopped);
callObject.off('participant-updated', handleParticipantUpdate);
callObject.off('error', handleErrorEvent);
callObject.off('camera-error', handleErrorEvent);
};
}, [callObject]);
}, [
callObject,
participants,
handleTrackStarted,
handleParticipantUpdate,
handleTrackStopped,
handleErrorEvent,
]);

/**
* Start a timer to show the "click allow" message, when the component mounts.
* Update participants for any event that happens to keep local participant list up to date.
* We grab the whole participant list to make sure everyone's status is most up to date.
*/
useEffect(() => {
const t = setTimeout(() => {
dispatch({ type: CLICK_ALLOW_TIMEOUT });
}, 2500);
if (participantUpdated) {
const list = Object.values(callObject?.participants());
setParticipants(list);
}
}, [participantUpdated, callObject]);

return function cleanup() {
clearTimeout(t);
};
}, []);
const isScreenShare = useMemo(() => {
return participants?.some((p) => p?.tracks?.screenVideo?.track);
}, [participants, callObject]);

const displayLargeTiles = useMemo(() => {
const isLarge = true;
if (isScreenShare) {
const screenShare = participants?.find(
(p) => p?.tracks?.screenVideo?.track
);
return (
<div className="large-tiles">
{
<Tile
key={`screenshare`}
videoTrackState={screenShare?.tracks?.screenVideo}
audioTrackState={screenShare?.tracks?.audio}
isLarge={isLarge}
disableCornerMessage={isScreenShare}
onClick={
screenShare.local
? null
: () => {
sendHello(screenShare.id);
}
}
/>
}
</div>
);
} else {
const tiles = participants?.filter((p) => !p.local);
return (
<div className="large-tiles">
{tiles?.map((t, i) => (
<Tile
key={`large-${i}`}
videoTrackState={t?.tracks?.video}
audioTrackState={t?.tracks?.audio}
isLarge={isLarge}
disableCornerMessage={isScreenShare}
onClick={
t.local
? null
: () => {
sendHello(t.id);
}
}
/>
))}
</div>
);
}
}, [participants, isScreenShare]);

const displaySmallTiles = useMemo(() => {
const isLarge = false;
if (isScreenShare) {
return (
<div className="small-tiles">
{participants?.map((p, i) => (
<Tile
key={`small-${i}`}
videoTrackState={p.tracks.video}
audioTrackState={p.tracks.audio}
isLocalPerson={p.local}
isLarge={isLarge}
disableCornerMessage={false}
onClick={
p.local
? null
: () => {
sendHello(p.id);
}
}
/>
))}
</div>
);
} else {
const tiles = participants?.filter((p) => p.local);
return (
<div className="small-tiles">
{tiles?.map((t, i) => (
<Tile
key={`small-${i}`}
videoTrackState={t.tracks.video}
audioTrackState={t.tracks.audio}
isLarge={isLarge}
disableCornerMessage={false}
onClick={
t.local
? null
: () => {
sendHello(t.id);
}
}
/>
))}
</div>
);
}
}, [participants, isScreenShare]);

/**
* Send an app message to the remote participant whose tile was clicked on.
Expand All @@ -130,51 +218,11 @@ export default function Call() {
[callObject]
);

function getTiles() {
let largeTiles = [];
let smallTiles = [];
Object.entries(callState.callItems).forEach(([id, callItem]) => {
const isLarge =
isScreenShare(id) ||
(!isLocal(id) && !containsScreenShare(callState.callItems));
const tile = (
<Tile
key={id}
videoTrackState={callItem.videoTrackState}
audioTrackState={callItem.audioTrackState}
isLocalPerson={isLocal(id)}
isLarge={isLarge}
disableCornerMessage={isScreenShare(id)}
onClick={
isLocal(id)
? null
: () => {
sendHello(id);
}
}
/>
);
if (isLarge) {
largeTiles.push(tile);
} else {
smallTiles.push(tile);
}
});
return [largeTiles, smallTiles];
}

const [largeTiles, smallTiles] = getTiles();
const message = getMessage(callState);
const message = getMessage();
return (
<div className="call">
<div className="large-tiles">
{
!message
? largeTiles
: null /* Avoid showing large tiles to make room for the message */
}
</div>
<div className="small-tiles">{smallTiles}</div>
{displayLargeTiles}
{displaySmallTiles}
{message && (
<CallMessage
header={message.header}
Expand Down
Loading