Permission Requests

Permission requests allow call participants to request permission to publish audio/video/screen-share. Participants with elevated permissions can accept or reject these requests. This guide shows you how you can build a UI for sending and receiving these requests.

The ToggleVideoPublishingButton, ToggleAudioPublishingButton and ScreenShareButton components can send permission requests, you only need to follow this guide if you want to build your own UI for this.

The PermissionRequests component can receive permission requests, you only need to follow this guide if you want to build your own UI for this.

Prerequisites

Before we can join a call, we need to connect to Stream’s edge infrastructure. To do that, we follow these steps:

Start point

Let’s start with a simple call UI where we create an audio_room call.

import {
  StreamVideo,
  StreamCall,
  SpeakerLayout,
  CallControls,
  useCall,
  StreamTheme,
} from '@stream-io/video-react-sdk';
import '@stream-io/video-react-sdk/dist/css/styles.css';

export default function App() {
  // create client and call

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

const MyCallUI = () => {
  const call = useCall();
  return (
    <div>
      <MyPermissionRequestNotifications />
      <SpeakerLayout />
      <CallControls />
      <MyPermissionRequests />
    </div>
  );
};

const MyPermissionRequests = () => {};

const MyPermissionRequestNotifications = () => {};

Permission requests are only relevant for call types that have roles where a participant can’t publish their audio/video/screen-share by default.

In this guide, we will implement the PermissionRequests component to send permission requests and the PermissionRequestNotifications to receive them.

To follow along with the guide, don’t forget to turn off backstage mode so others can join:

let call: Call;

await call.goLive();

Request permission

In this step we will implement the MyPermissionRequests and the MyPermissionRequestButton components to send permission requests.

For readability the code snippet only contains the MyPermissionRequests and the MyPermissionRequestButton implementation, the full example is available at the end of the guide.

import {
  useRequestPermission,
  useCallStateHooks,
  OwnCapability,
} from '@stream-io/video-react-sdk';

const MyPermissionRequestButton = ({ children, capability }) => {
  const {
    requestPermission,
    hasPermission,
    canRequestPermission,
    isAwaitingPermission,
  } = useRequestPermission(capability);

  if (hasPermission || !canRequestPermission) return null;

  return (
    <button disabled={isAwaitingPermission} onClick={() => requestPermission()}>
      {children}
    </button>
  );
};

const MyPermissionRequests = () => {
  const call = useCall();
  const { useHasPermissions } = useCallStateHooks();
  const canSendAudio = useHasPermissions(OwnCapability.SEND_AUDIO);
  const canSendVideo = useHasPermissions(OwnCapability.SEND_VIDEO);
  const canShareScreen = useHasPermissions(OwnCapability.SCREENSHARE);

  if (!call) {
    return null;
  }

  return (
    <div>
      <MyPermissionRequestButton capability={OwnCapability.SEND_AUDIO}>
        Request audio permission
      </MyPermissionRequestButton>
      <MyPermissionRequestButton capability={OwnCapability.SEND_VIDEO}>
        Request video permission
      </MyPermissionRequestButton>
      <MyPermissionRequestButton capability={OwnCapability.SCREENSHARE}>
        Request screen share permission
      </MyPermissionRequestButton>
      {canSendAudio ? 'Allowed to send audio' : 'Not allowed to send audio'}
      {canSendVideo ? 'Allowed to send video' : 'Not allowed to send video'}
      {canShareScreen
        ? 'Allowed to share screen'
        : 'Not allowed to share screen'}
    </div>
  );
};

Let’s unpack the above code snippet:

  • We only display the request button if the user is allowed to request the specific capability and doesn’t already have that permission. For more information, check out the permissions guide.
  • To send the request we are using the requestPermission function (wrapper around requestPermissions method of the Call instance) which comes from the useRequestPermission hook.
  • We use the useHasPermissions hook to be notified when the request was approved. Alternatively you can subscribe to the call.permissions_updated event.

Receive permission requests

In this step we will implement the MyPermissionRequestNotifications component to receive permission requests.

For readability, the code snippet only contains the MyPermissionRequestNotifications implementation, the full example is available at the end of the guide.

const MyPermissionRequestNotifications = () => {
  const call = useCall();
  const { useLocalParticipant, useHasPermissions } = useCallStateHooks();
  const localParticipant = useLocalParticipant();
  const canUpdateCallPermissions = useHasPermissions(
    OwnCapability.UPDATE_CALL_PERMISSIONS,
  );
  const [permissionRequests, setPermissionRequests] = useState([]);

  useEffect(() => {
    if (!call || !canUpdateCallPermissions) return;

    const unsubscribe = call.on(
      'call.permission_request',
      (event: StreamVideoEvent) => {
        // ignore own requests
        if (event.user.id !== localParticipant?.userId) {
          setPermissionRequests((requests) => [
            ...requests,
            event as PermissionRequestEvent,
          ]);
        }
      },
    );
    return () => {
      unsubscribe();
    };
  }, [call, canUpdateCallPermissions, localParticipant]);

  if (!call || permissionRequests.length === 0) {
    return null;
  }

  const answerRequest = async (answer, request) => {
    if (answer === 'accept') {
      await call.grantPermissions(request.user.id, request.permissions);
    } else {
      await call.revokePermissions(request.user.id, request.permissions);
    }
    setPermissionRequests((requests) => requests.filter((r) => r !== request));
  };

  return (
    <div>
      {permissionRequests.map((request) => (
        <div>
          New request from {request.user.id} to publish {request.permissions}
          <button onClick={() => answerRequest('accept', request)}>
            Accept
          </button>
          <button onClick={() => answerRequest('reject', request)}>
            Reject
          </button>
        </div>
      ))}
    </div>
  );
};

Let’s unpack the above code snippet:

  • We only show the permission requests to users that have the necessary capability to accept/reject: call.hasPermission(OwnCapability.UPDATE_CALL_PERMISSIONS)
  • We subscribe to listen to 'call.permission_request' WS event to update the list of requests
  • A permission request can be accepted with the grantPermissions method of the Call instance
  • A permission request can be rejected with the revokePermissions method of the Call instance. This method can also be used to revoke permissions later in the call.

Finished example

Here is the full example:

import {
  CallControls,
  CallParticipantsView,
  OwnCapability,
  PermissionRequestEvent,
  StreamCall,
  StreamTheme,
  StreamVideo,
  StreamVideoEvent,
  useCall,
  useCallStateHooks,
  useRequestPermission,
} from '@stream-io/video-react-sdk';
import '@stream-io/video-react-sdk/dist/css/styles.css';
import { useEffect, useState } from 'react';

export default function App() {
  // create client and call

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

const MyCallUI = () => {
  const call = useCall();

  return (
    <div>
      <MyPermissionRequestNotifications />
      <CallParticipantsView call={call} />
      <CallControls />
      <MyPermissionRequests />
    </div>
  );
};

const MyPermissionRequestButton = ({ children, capability }) => {
  const {
    requestPermission,
    hasPermission,
    canRequestPermission,
    isAwaitingPermission,
  } = useRequestPermission(capability);

  if (hasPermission || !canRequestPermission) return null;

  return (
    <button disabled={isAwaitingPermission} onClick={() => requestPermission()}>
      {children}
    </button>
  );
};

const MyPermissionRequests = () => {
  const call = useCall();
  const { useHasPermissions } = useCallStateHooks();
  const canSendAudio = useHasPermissions(OwnCapability.SEND_AUDIO);
  const canSendVideo = useHasPermissions(OwnCapability.SEND_VIDEO);
  const canShareScreen = useHasPermissions(OwnCapability.SCREENSHARE);

  if (!call) {
    return null;
  }

  return (
    <div>
      <MyPermissionRequestButton capability={OwnCapability.SEND_AUDIO}>
        Request audio permission
      </MyPermissionRequestButton>
      <MyPermissionRequestButton capability={OwnCapability.SEND_VIDEO}>
        Request video permission
      </MyPermissionRequestButton>
      <MyPermissionRequestButton capability={OwnCapability.SCREENSHARE}>
        Request screen share permission
      </MyPermissionRequestButton>
      {canSendAudio ? 'Allowed to send audio' : 'Not allowed to send audio'}
      {canSendVideo ? 'Allowed to send video' : 'Not allowed to send video'}
      {canShareScreen
        ? 'Allowed to share screen'
        : 'Not allowed to share screen'}
    </div>
  );
};

const MyPermissionRequestNotifications = () => {
  const call = useCall();
  const { useLocalParticipant, useHasPermissions } = useCallStateHooks();
  const localParticipant = useLocalParticipant();
  const canUpdateCallPermissions = useHasPermissions(
    OwnCapability.UPDATE_CALL_PERMISSIONS,
  );
  const [permissionRequests, setPermissionRequests] = useState([]);

  useEffect(() => {
    if (!call || !canUpdateCallPermissions) return;

    const unsubscribe = call.on(
      'call.permission_request',
      (event: StreamVideoEvent) => {
        // ignore own requests
        if (event.user.id !== localParticipant?.userId) {
          setPermissionRequests((requests) => [
            ...requests,
            event as PermissionRequestEvent,
          ]);
        }
      },
    );
    return () => {
      unsubscribe();
    };
  }, [call, canUpdateCallPermissions, localParticipant]);

  if (!call || permissionRequests.length === 0) {
    return null;
  }

  const answerRequest = async (answer, request) => {
    if (answer === 'accept') {
      await call.grantPermissions(request.user.id, request.permissions);
    } else {
      await call.revokePermissions(request.user.id, request.permissions);
    }
    setPermissionRequests((requests) => requests.filter((r) => r !== request));
  };

  return (
    <div>
      {permissionRequests.map((request) => (
        <div>
          New request from {request.user.id} to publish {request.permissions}
          <button onClick={() => answerRequest('accept', request)}>
            Accept
          </button>
          <button onClick={() => answerRequest('reject', request)}>
            Reject
          </button>
        </div>
      ))}
    </div>
  );
};
© Getstream.io, Inc. All Rights Reserved.