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 for 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 an appointment if necessary.

Prerequisites

Let’s start by setting up an application. Here’s what we need:

  1. Separate user roles for a medical specialist (specialist) and a patient (patient)
  2. An appointment call type with a maximum duration of 1 hour
  3. Two test users, one for each call (we’ll call them dr-lecter and bill)
  4. One test call of an appointment type

The quickest way to set up these requirements is to use the server-side Node.js SDK. So let’s install it:

npm install @stream-io/node-sdk
# or using yarn:
yarn add @stream-io/node-sdk

And then run a one-off 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);

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

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 {
  SpeakerLayout,
  StreamCall,
  StreamTheme,
  StreamVideo,
  StreamVideoClient,
} from "@stream-io/video-react-sdk";
import "@stream-io/video-react-sdk/dist/css/styles.css";

const client = new StreamVideoClient({
  apiKey: "REPLACE_WITH_API_KEY",
  user: {
    /* one of the test users */
  },
  token: "REPLACE_WITH_TOKEN",
});

const App = () => {
  const [call, setCall] = useState(null);

  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 <>Loading...</>;
  }

  return (
    <StreamTheme>
      <StreamVideo client={client}>
        <StreamCall call={call}>
          <SpeakerLayout />
        </StreamCall>
      </StreamVideo>
    </StreamTheme>
  );
};

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-sdk";
import { formatDuration, intervalToDuration } from "date-fns";

const useSessionTimer = () => {
  const { useCallSession } = useCallStateHooks();
  const session = useCallSession();
  const [remainingMs, setRemainingMs] = useState(Number.NaN);

  useEffect(() => {
    if (!session?.timer_ends_at) return;
    const timerEndAt = new Date(session.timer_ends_at);
    const handle = setInterval(() => {
      const now = new Date();
      const remainingMs = +timerEndAt - +now;
      setRemainingMs(remainingMs);
    }, 500);
    return () => clearInterval(handle);
  }, [session]);

  return remainingMs;
};

const SessionTimer = () => {
  const remainingMs = useSessionTimer();
  return (
    <div className="session-timer">
      {formatDuration(
        intervalToDuration({
          start: Date.now(),
          end: Date.now() + remainingMs,
        }),
      )}
    </div>
  );
};

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 />
    <SpeakerLayout />
  </StreamCall>
</StreamVideo>

SessionTimer component in use

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 five minutes before the session timer reaches zero:

const useSessionTimerAlert = (remainingMs, threshold, onAlert) => {
  const didAlert = useRef(false);

  useEffect(() => {
    if (!didAlert.current && remainingMs < threshold) {
      onAlert();
    }
  }, [onAlert, remainingMs, threshold]);
};

const SessionTimer = () => {
  const remainingMs = useSessionTimer();
  const [showAlert, setShowAlert] = useState(false);
  useSessionTimerAlert(remainingMs, 5 * 60 * 1000, () => setShowAlert(true));
  return (
    <>
      <div className="session-timer">
        ⏱️{" "}
        {formatDuration(
          intervalToDuration({
            start: Date.now(),
            end: Date.now() + remainingMs,
          }),
        )}
      </div>
      {showAlert && (
        <div className="session-timer-alert">
          Less than 5 minutes remaining
          <button type="button" onClick={() => setShowAlert(false)}>
            Dismiss
          </button>
        </div>
      )}
    </>
  );
};

Alert indicating that session is about to end

Similarly, we can add an alert for when the timer reaches zero:

const remainingMs = useSessionTimer();
const [hasReachedZero, setHasReachedZero] = useState(false);
useSessionTimerAlert(remainingMs, 0, () => setHasReachedZero(true));

return (
  <>
    {hasReachedZero && (
      <div className="session-timer-alert">The time has ran out</div>
    )}
  </>
);

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-sdk";

const ExtendSessionButton = ({ duration }) => {
  const call = useCall();
  const { useCallSettings, useHasPermissions } = useCallStateHooks();
  const settings = useCallSettings();
  const canExtend = useHasPermissions(OwnCapability.CHANGE_MAX_DURATION);

  if (!canExtend) {
    return null;
  }

  return (
    <button
      type="button"
      className="extend-session-button"
      onClick={() => {
        call.update({
          settings_override: {
            limits: {
              max_duration_seconds:
                settings.limits.max_duration_seconds + duration,
            },
          },
        });
      }}
    >
      + {Math.round((duration / 60) * 10) / 10} minutes
    </button>
  );
};

// Somewhere inside <StreamCall>:
<ExtendSessionButton duration={1800} />;

Note that the button is only visible to the specialist role.

Alert with an option to extend the sesion by 30 minutes

When the call settings are updated, all call components react automatically, so our SessionTimer component always reflects the current settings.

© Getstream.io, Inc. All Rights Reserved.