Session Timers

Session timers limit maximum call duration. Configure session timers for individual calls or call types. When the timer reaches zero, the call ends automatically - ideal for paid appointments.

Best Practices

  • Display remaining time - Show countdown clearly to all participants
  • Warn before timeout - Alert users before the session ends
  • Allow extensions - Let authorized users extend if needed
  • Handle timeout gracefully - Clean up resources when session ends

This guide integrates a session timer into a telemedicine app with:

  • Two users: medical specialist and patient
  • 1-hour appointment duration
  • Specialist can extend if necessary

Prerequisites

Application setup requirements:

  1. User roles - specialist and patient roles
  2. Call type - appointment with 1-hour maximum duration
  3. Test users - dr-lecter and bill
  4. Test call - appointment type call

Use the server-side Node.js SDK for setup. Install it:

yarn add @stream-io/node-sdk

Create the setup script:

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();

Run the script:

npx ts-node script.ts

Verify in the dashboard under Call Types and Roles & Permissions.

For a video calling application starting point, see the Video Calling Tutorial.

Basic application setup:

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;

Override default call duration (requires update call and call settings permissions):

newCall.join({
  data: {
    settings_override: {
      limits: {
        max_duration_seconds: 7200,
      },
    },
  },
});

Session Timer Component

After joining, check session.timer_ends_at for the auto-end timestamp.

Countdown component implementation:

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",
  },
});

Add the component inside StreamCall for a countdown in the call UI:

<StreamVideo client={client}>
  <StreamCall call={call}>
    <SessionTimer />
    <CallContent />
  </StreamCall>
</StreamVideo>

SessionTimer component in use

Adding Alerts

Add an alert twenty minutes before session end:

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>
  );
};

Alert indicating that session is about to end

Show an alert when time elapses:

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 role has change-max-duration capability to modify call duration. Create an extension component:

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.

Alert with an option to extend the session by 30 minutes

SDK states update automatically when call settings change, keeping SessionTimer current.