Session Timers
A session timer allows you to limit the maximum duration of a call. It's possible to configure a session timer for a single call, or every call of a certain type. When a session timer reaches zero, the call automatically ends, making it a great tool for managing paid appointments.
In this article, we'll integrate a session timer into a sample telemedicine application. We assume that two users are joining a call: a medical specialist and a patient. Each appointment lasts 1 hour, but the specialist can extend the appointment if necessary.
Prerequisites
Let's start by setting up an application. Here's what we need:
- Separate user roles for a medical specialist (
specialist
) and a patient (patient
) - An
appointment
call type with a maximum duration of 1 hour - Two test users, one for each call (we'll call them
dr-lecter
andbill
) - One test call of an
appointment
type
The quickest way to set up these requirements is to use the server-side Node.js SDK. Let us create a one-off Node.js script. So let's install it in a project:
- yarn
- npm
yarn add @stream-io/node-sdk
npm install @stream-io/node-sdk
Then create a one-off Node.js script with the following:
import { StreamClient, VideoOwnCapability } from '@stream-io/node-sdk';
const apiKey = 'REPLACE_WITH_API_KEY';
const secret = 'REPLACE_WITH_SECRET';
const client = new StreamClient(apiKey, secret);
async function main() {
// 1. Roles for a medical specialist (`specialist`) and a patient:
await client.createRole({ name: 'specialist' });
await client.createRole({ name: 'patient' });
// 2. Call type with the maximum duration of 1 hour:
await client.video.createCallType({
name: 'appointment',
grants: {
specialist: [
VideoOwnCapability.JOIN_CALL,
VideoOwnCapability.SEND_AUDIO,
VideoOwnCapability.SEND_VIDEO,
// These capabilities are required to change session duration:
VideoOwnCapability.UPDATE_CALL,
VideoOwnCapability.UPDATE_CALL_SETTINGS,
],
patient: [
VideoOwnCapability.JOIN_CALL,
VideoOwnCapability.SEND_AUDIO,
VideoOwnCapability.SEND_VIDEO,
],
},
settings: {
limits: {
// 3600 seconds = 1 hour
max_duration_seconds: 3600,
},
},
});
// 3. Two test users:
await client.upsertUsers({
users: {
'dr-lecter': {
id: 'dr-lecter',
name: 'Dr. Hannibal Lecter',
role: 'specialist',
},
'bill': {
id: 'bill',
name: 'Buffalo Bill',
role: 'patient',
},
},
});
// 4. Test call:
await client.video.call('appointment', 'test-call').create({
data: {
members: [{ user_id: 'dr-lecter' }, { user_id: 'bill' }],
created_by_id: 'dr-lecter',
},
});
}
main();
Now, run the script with the following:
npx ts-node script.ts
We can verify that the script ran successfully by checking the Call Types
and
the Roles & Permissions
sections in the application
dashboard.
Now we're ready to add a session timer to our application. If you haven't already bootstrapped a video calling application (our Video Calling Tutorial is a great place to start!), here's a very simple application that we'll use as a starting point:
import {
Call,
CallContent,
StreamCall,
StreamVideo,
StreamVideoClient,
} from '@stream-io/video-react-native-sdk';
import React, { useState, useEffect } from 'react';
import { ActivityIndicator, SafeAreaView, StyleSheet } from 'react-native';
const client = new StreamVideoClient({
apiKey: 'REPLACE_WITH_API_KEY',
user: {
/* one of the test users */
id: 'bill',
name: 'Buffalo Bill',
},
token: 'REPLACE_WITH_USER_TOKEN',
});
const RootContainer = (props: React.PropsWithChildren<{}>) => {
return <SafeAreaView style={styles.container}>{props.children}</SafeAreaView>;
};
const callId = 'test-call';
const App = () => {
const [call, setCall] = useState<Call>();
useEffect(() => {
const newCall = client.call('appointment', callId);
newCall
.join()
.then(() => setCall(newCall))
.catch(() => console.error('Failed to join the call'));
return () => {
newCall.leave().catch(() => console.error('Failed to leave the call'));
};
}, []);
if (!call) {
return (
<RootContainer>
<ActivityIndicator size={'large'} />
</RootContainer>
);
}
return (
<RootContainer>
<StreamVideo client={client}>
<StreamCall call={call}>
<CallContent />
</StreamCall>
</StreamVideo>
</RootContainer>
);
};
const styles = StyleSheet.create({
container: {
backgroundColor: 'black',
flex: 1,
justifyContent: 'center',
},
});
export default App;
At this point it's also possible to override the default call duration. The user
must have a permission to update call and call settings (in our case, the
specialist
role has these permissions):
newCall.join({
data: {
settings_override: {
limits: {
max_duration_seconds: 7200,
},
},
},
});
Session Timer Component
After joining the call, we can examine the session.timer_ends_at
property: if
the session timer has been set up, it contains the timestamp at which point the
call automatically ends.
Let's implement a component that displays a countdown to the end of the session:
import { useCallStateHooks } from '@stream-io/video-react-native-sdk';
import { StyleSheet, Text, View } from 'react-native';
const useSessionTimer = () => {
const { useCallSession } = useCallStateHooks();
const session = useCallSession();
const [remainingMs, setRemainingMs] = useState(Number.NaN);
useEffect(() => {
if (!session?.timer_ends_at) {
return;
}
const timeEndAtMillis = new Date(session.timer_ends_at).getTime();
const handle = setInterval(() => {
setRemainingMs(timeEndAtMillis - Date.now());
}, 500);
return () => clearInterval(handle);
}, [session?.timer_ends_at]);
return remainingMs;
};
function convertMillis(milliseconds: number) {
// Calculate the number of minutes and seconds
const minutes = Math.floor(milliseconds / 60000);
const seconds = ((milliseconds % 60000) / 1000).toFixed(0);
// Format the output
return `${minutes} mins:${seconds.padStart(2, '0')} secs`;
}
const SessionTimer = () => {
const remainingMs = useSessionTimer();
return (
<View style={styles.sessionTimer}>
<Text style={styles.sessionTimerText}>{convertMillis(remainingMs)}</Text>
</View>
);
};
const styles = StyleSheet.create({
sessionTimer: {
top: 0,
left: 0,
right: 0,
backgroundColor: 'white',
},
sessionTimerText: {
color: 'black',
fontSize: 24,
textAlign: 'center',
},
});
And now by adding this component inside of the StreamCall
, we get a ticking
countdown in the call UI:
<StreamVideo client={client}>
<StreamCall call={call}>
<SessionTimer />
<CallContent />
</StreamCall>
</StreamVideo>
Adding Alerts
It's easy to lose track of time during a meeting and then be surprised when time runs out. Let's add an alert that pops up on the page twenty minutes before the session timer reaches zero:
import { Alert } from 'react-native';
const useSessionTimerAlert = (remainingMs: number, thresholdMs: number) => {
const didAlert = useRef(false);
useEffect(() => {
if (!didAlert.current && remainingMs < thresholdMs) {
Alert.alert(
'Notice',
`Less than ${thresholdMs / 60000} minutes remaining`
);
didAlert.current = true;
}
}, [remainingMs, thresholdMs]);
};
const SessionTimer = () => {
const remainingMs = useSessionTimer();
useSessionTimerAlert(remainingMs, 20 * 60 * 1000);
return (
<View style={styles.sessionTimer}>
<Text style={styles.sessionTimerText}>{convertMillis(remainingMs)}</Text>
</View>
);
};
Similarly, we can show an alert when the call session is over (when time has elapsed):
const useSessionEndedAlert = (remainingMs: number) => {
const didAlert = useRef(false);
useEffect(() => {
if (!didAlert.current && remainingMs <= 0) {
Alert.alert('Call ended');
didAlert.current = true;
}
}, [remainingMs]);
};
Extending a Session
The specialist
user role that we created has the permission to update call
settings, granting it the change-max-duration
capability, which allows a user
to change the duration of a call. Let's add a component that updates the call
settings and extends a session by the specified number of seconds:
import {
OwnCapability,
useCall,
useCallStateHooks,
} from '@stream-io/video-react-native-sdk';
import { Button } from 'react-native';
const ExtendSessionButton = ({
durationSecsToExtend,
}: {
durationSecsToExtend: number;
}) => {
const call = useCall();
const { useCallSettings, useHasPermissions } = useCallStateHooks();
const settings = useCallSettings();
const canExtend = useHasPermissions(OwnCapability.CHANGE_MAX_DURATION);
if (!canExtend) {
return null;
}
const onPress = () => {
call?.update({
settings_override: {
limits: {
max_duration_seconds:
(settings?.limits?.max_duration_seconds ?? 0) +
durationSecsToExtend,
},
},
});
};
return (
<Button
onPress={onPress}
title={`Extend by ${Math.round(((durationSecsToExtend / 60) * 10) / 10)} minutes`}
/>
);
};
// Somewhere inside <StreamCall>:
<ExtendSessionButton durationSecsToExtend={10 * 60} />;
specialist
role. :::When the call settings are updated, all the states from the SDK are updated
automatically, so our SessionTimer
component always reflects the current
settings.