yarn add @stream-io/node-sdk
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:
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} />;
The button is only visible to the user with 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.