69
loading...
This website collects cookies to deliver better user experience
react-native-daily-js
daily-js
(web) apps, which we promise already have a bunch of existing tutorials.📚 App
component is the top-level parent component. It renders either the home screen or the in-call view.createRoom
, defined in App
:// App.tsx
<Button
type="secondary"
onPress={createRoom}
label={
appState === AppState.Creating
? 'Creating room...'
: 'Create demo room'
}
/>
// App.tsx
const createRoom = () => {
setRoomCreateError(false);
setAppState(AppState.Creating);
api
.createRoom()
.then((room) => {
setRoomUrlFieldValue(room.url);
setAppState(AppState.Idle);
})
.catch(() => {
setRoomCreateError(true);
setRoomUrlFieldValue(undefined);
setAppState(AppState.Idle);
});
};
appState
state value to be in a temporary “creating” state, call api.createRoom()
, and, if it’s successful, set our roomUrlFieldValue
value and appState
. (Both appState
and roomUrlFieldValue
are component state values initialized in App
.)api.createRoom()
method.roomUrlFieldValue
, set the roomUrl
state value with it, and kick off creating the Daily call object. // App.tsx
// “Join call” button will call startCall on press
<StartButton
onPress={startCall}
disabled={startButtonDisabled}
starting={appState === AppState.Joining}
/>
startCall
:// App.tsx
/**
* Join the room provided by the user or the
* temporary room created by createRoom
*/
const startCall = () => {
setRoomUrl(roomUrlFieldValue);
};
useEffect
hook is triggered by the roomURL
value getting updated, which creates our Daily call object (the brain of this operation!)// App.tsx
/**
* Create the callObject as soon as we have a roomUrl.
* This will trigger the call starting.
*/
useEffect(() => {
if (!roomUrl) {
return;
}
const newCallObject = Daily.createCallObject();
setCallObject(newCallObject);
}, [roomUrl]);
const newCallObject = Daily.createCallObject();
setCallObject(newCallObject);
// App.tsx
useEffect(() => {
if (!callObject || !roomUrl) {
return;
}
callObject.join({ url: roomUrl }).catch((_) => {
// Doing nothing here since we handle fatal join errors in another way,
// via our listener attached to the 'error' event
});
setAppState(AppState.Joining);
}, [callObject, roomUrl]);
useEffect
hook in App
, when the callObject
and roomUrl
state values are truthy, which they now are, we can actually join
our call by passing the roomUrl
to our call object instance. setAppState(AppState.Joining);
// App.tsx
const showCallPanel = [
AppState.Joining,
AppState.Joined,
AppState.Error,
].includes(appState);
showCallPanel
— shown above — is truthy, our in-call view will render instead of the home screen:// App.tsx
<View style={styles.container}>
{showCallPanel ? (
<View style={[
styles.callContainerBase,
orientation === Orientation.Landscape
? styles.callContainerLandscape
: null,
]}>
<CallPanel roomUrl={roomUrl || ''} />
<Tray
onClickLeaveCall={leaveCall}
disabled={!enableCallButtons}
/>
</View>
) : (
… //home screen
)
...
CallPanel
component — our in-call view — for the rest of this tutorial. If you have any questions about this section, please reach out! We’re happy to help. 🙌daily-js
, where screen sharing is permitted.Tray
component) has buttons to toggle the local participant’s audio, video, and to leave the call.react-native-daily-js
. CallPanel.tsx
, we render an array called largeTiles
, which represents the remote participants.// CallPanel.tsx
<ScrollView
alwaysBounceVertical={false}
alwaysBounceHorizontal={false}
horizontal={orientation === Orientation.Landscape}
>
<View
style={[
styles.largeTilesContainerInnerBase,
orientation === Orientation.Portrait
? styles.largeTilesContainerInnerPortrait
: styles.largeTilesContainerInnerLandscape,
]}
>
{largeTiles} // <- our remote participants
</View>
</ScrollView>
ScrollView
but you may prefer a FlatList
component if you know you will be having larger calls. (A FlatList
will only render the visible tiles, which should help with performance. It’s less of a concern in 1-on-1 video calls.)largeTiles
(remote participants) and thumbnailTiles
(the local participant or screen sharer) are determined by the same memoized function. The tiles in largeTiles
can be either full size or half size depending on the number of participants.// CallPanel.tsx
/**
* Get lists of large tiles and thumbnail tiles to render.
*/
const [largeTiles, thumbnailTiles] = useMemo(() => {
let larges: JSX.Element[] = [];
let thumbnails: JSX.Element[] = [];
Object.entries(callState.callItems).forEach(([id, callItem]) => {
let tileType: TileType;
if (isScreenShare(id)) {
tileType = TileType.Full;
} else if (isLocal(id) || containsScreenShare(callState.callItems)) {
tileType = TileType.Thumbnail;
} else if (participantCount(callState.callItems) <= 3) {
tileType = TileType.Full;
} else {
tileType = TileType.Half;
}
const tile = (
<Tile
key={id}
videoTrackState={callItem.videoTrackState}
audioTrackState={callItem.audioTrackState}
mirror={usingFrontCamera && isLocal(id)}
type={tileType}
disableAudioIndicators={isScreenShare(id)}
onPress={
isLocal(id)
? flipCamera
: () => {
sendHello(id);
}
}
/>
);
if (tileType === TileType.Thumbnail) {
thumbnails.push(tile);
} else {
larges.push(tile);
}
});
return [larges, thumbnails];
}, [callState.callItems, flipCamera, sendHello, usingFrontCamera]);
larges
and thumbnails
Object.entries(callState.callItems)
) and do the following for each (or forEach
, if you will):
tileType
can be TileType.Full
, TileType.Half
, or TileType.Thumbnail
. The latter is the local participant, and the first two options are for remote participants (our largeTiles
).Tile
component for each participant and update our larges
and thumbnails
arraysTile
component is the mediaComponent
, a memoized instance of the DailyMediaView
component imported from react-native-daily-js
:// Tile.tsx
import {
DailyMediaView,
} from '@daily-co/react-native-daily-js';
...
const mediaComponent = useMemo(() => {
return (
<DailyMediaView
videoTrack={videoTrack}
audioTrack={audioTrack}
mirror={props.mirror}
zOrder={props.type === TileType.Thumbnail ? 1 : 0}
style={styles.media}
objectFit="cover"
/>
);
}, [videoTrack, audioTrack, props.mirror, props.type]);
videoTrack
and audioTrack
are props passed to Tile
from CallPanel
but are actually set in callState.ts
:// callState.ts
function getCallItems(participants: { [id: string]: DailyParticipant }) {
// Ensure we *always* have a local participant
let callItems = { ...initialCallState.callItems };
for (const [id, participant] of Object.entries(participants)) {
callItems[id] = {
videoTrackState: participant.tracks.video,
audioTrackState: participant.tracks.audio,
};
if (shouldIncludeScreenCallItem(participant)) {
callItems[id + '-screen'] = {
videoTrackState: participant.tracks.screenVideo,
audioTrackState: participant.tracks.screenAudio,
};
}
}
return callItems;
}
callObject
provides our participant information (see: callObject.participants()
) and our participant information contains their media (video/audio) tracks. We can then pass those tracks to the DailyMediaView
component to actually play those tracks in the app. Tile
component, we get the videoTrack
and audioTrack
values from the videoTrackState
and audioTrackState
props.// Tile.tsx
const videoTrack = useMemo(() => {
return props.videoTrackState
&& props.videoTrackState.state === 'playable'
? props.videoTrackState.track!
: null;
}, [props.videoTrackState]);
const audioTrack = useMemo(() => {
return props.audioTrackState && props.audioTrackState.state === 'playable'
? props.audioTrackState.track!
: null;
}, [props.audioTrackState]);
null
. Both are valid types for the DailyMediaView
videoTrack
and audioTrack
props.Tile
also has an overlay with the audio and camera muted icons when they apply (i.e. when there’s no track to play), but we won’t review that code here. Again, let us know if you have any questions. 🙏Tray
component interacts with the Daily call object. As a reminder, it’s rendered in App.tsx
at the same time the CallPanel
component is rendered.setLocalAudio
on the call object instance.// Tray.tsx
const toggleCamera = useCallback(() => {
callObject?.setLocalVideo(isCameraMuted);
}, [callObject, isCameraMuted]);
setLocalAudio
.// Tray.tsx
const toggleMic = useCallback(() => {
callObject?.setLocalAudio(isMicMuted);
}, [callObject, isMicMuted]);
leaveCall
function call, a prop passed from App
.// App.tsx
/**
* Leave the current call.
* If we're in the error state (AppState.Error),
* we've already "left", so just
* clean up our state.
*/
const leaveCall = useCallback(() => {
if (!callObject) {
return;
}
if (appState === AppState.Error) {
callObject.destroy().then(() => {
setRoomUrl(undefined);
setRoomUrlFieldValue(undefined);
setCallObject(null);
setAppState(AppState.Idle);
});
} else {
setAppState(AppState.Leaving);
callObject.leave();
}
}, [callObject, appState]);
destroy
ing our call object instance and resetting the state in App
to get back to our initial values.