43
loading...
This website collects cookies to deliver better user experience
token_endpoint
and room_id
. We’ll be needing these credentials in the later stages of building the application.listener
, speaker
, handraise
, and moderator
. Let’s set up permissions for each of these roles, starting with the listener role.listener
role, we can turn off all the publish strategies as we don’t want listeners to share their audio, video, or screen. Listeners will still be able to listen to others’ audio.Can change any participant's role permission
.handraise
role, we can again turn off all the publish strategies and just keep the Can change any participant's role
permission turned on. This permission will allow us to switch the user from listener
role to handraise role, and vice-versa, and help us to to implement the hand-raise functionality.listener
wants to become a speaker
, they can click on the hand-raise button that will change their role to handraise
. When the user's role is handraise
, we'll display a small badge next to their avatar to notify the moderator.speaker
role, since we’re building an audio-only room, we can just check the Can share audio
publish strategy and leave the rest of them unchecked. We can leave all the permissions turned off for the speaker
role.moderator
role, we can check the Can share audio publish strategy and move on towards the permissions. In the permissions section, turn on the Can change any participant's role
permission and the Can mute any participant
permission.speaker
and moderator
. And with that, we’re ready to move on and get the required credentials from the 100ms Dashboard.template
branch of this repo with this command:git clone -b template https://github.com/itsnitinr/discord-stages-clone-100ms.git
@100mslive/hms-video
and @100mslive/hms-video-react
SDK have already been added to the package.json
file.npm install
or yarn install
to install these dependencies locally before moving forward..env.local.example
file.cp .env.local.example .env.local
.env.local
file:// .env.local
TOKEN_ENDPOINT = <YOUR-TOKEN-ENDPOINT-URL>
ROOM_ID = <YOUR-ROOM-ID>
dev
script in this manner:npm run dev
#or
yarn dev
<HMSRoomProvider />
component from the @100mslive/hms-video-react package
.// pages/index.js
import { HMSRoomProvider } from '@100mslive/hms-video-react';
import Head from 'next/head';
import Join from '../components/Join';
import Room from '../components/Room';
const StagesApp = () => {
const isConnected = false;
return isConnected ? <Room /> : <Join />;
};
const App = () => {
return (
<HMSRoomProvider>
<Head>
<title>Discord Stages Clone</title>
</Head>
<StagesApp />
</HMSRoomProvider>
);
};
export default App;
<Room />
component or the <Join />
component based on the isConnected variable. However, its value has been hardcoded to be false for now.selectIsConnectedToRoom
selector and useHMSStore
hook like this:// pages/index.js
import { HMSRoomProvider,
useHMSStore,
selectIsConnectedToRoom,
} from '@100mslive/hms-video-react';
import Head from 'next/head';
import Join from '../components/Join';
import Room from '../components/Room';
const StagesApp = () => {
const isConnected = useHMSStore(selectIsConnectedToRoom);
return isConnected ? <Room /> : <Join />;
};
const App = () => {
return (
<HMSRoomProvider>
<Head>
<title>Discord Stages Clone</title>
</Head>
<StagesApp />
</HMSRoomProvider>
);
};
export default App;
<Join />
component will be rendered. Let’s implement the functionality to join a room inside the components/Join.jsx file
.hmsActions
object returned by the useHMSActions() hook
. join(
) method takes an object containing the userName, authToken and an optional settings object as the parameter.userName
from the local name
state variable created using the useState()
hook from React. However, to obtain the authToken
, we will need to make a network request to our custom Next.js API route along with the role we want to join the room with.name
.pages/api/token.js
file. Here’s how it will look like:// pages/api/token.js
import { v4 } from 'uuid';
export default async function getAuthToken(req, res) {
try {
const { role } = JSON.parse(req.body);
const response = await fetch(`${process.env.TOKEN_ENDPOINT}api/token`, {
method: 'POST',
body: JSON.stringify({
user_id: v4(),
room_id: process.env.ROOM_ID,
role,
}),
});
const { token } = await response.json();
res.status(200).json({ token });
} catch (error) {
console.log('error', error);
res.status(500).json({ error });
}
}
POST
request to our 100ms token endpoint URL
, which is stored inside the environment variables, along with a unique user_id
, role
, and the room_id
, which is also stored inside the environment variables.authToken
. Using this authToken
, we can join the room
. Since we don’t want the user to join with their mic turned on, we can set isAudioMuted
to true
inside the optional settings object.// components/Join.jsx
import Image from 'next/image';
import { useState } from 'react';
import Avatar from 'boring-avatars';
import { useHMSActions } from '@100mslive/hms-video-react';
import NameInput from './Join/NameInput';
import RoleSelect from './Join/RoleSelect';
import JoinButton from './Join/JoinButton';
const Join = () => {
const hmsActions = useHMSActions();
const [name, setName] = useState('');
const [role, setRole] = useState('listener');
const joinRoom = async () => {
try {
const response = await fetch('/api/token', {
method: 'POST',
body: JSON.stringify({ role }),
});
const { token } = await response.json();
hmsActions.join({
userName: name || 'Anonymous',
authToken: token,
settings: {
isAudioMuted: true,
},
});
} catch (error) {
console.error(error);
}
};
return (
<>
<Image
src="https://imgur.com/27iLD4R.png"
alt="Login background"
className="w-screen h-screen object-cover relative"
layout="fill"
/>
<div className="bg-gray-800 rounded-lg w-11/12 md:w-1/2 lg:w-1/3 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 p-8 text-white shadow-lg space-y-4 flex flex-col items-center max-w-md">
<Avatar name={name} variant="marble" size="72" />
<NameInput name={name} setName={setName} />
<RoleSelect role={role} setRole={setRole} />
<JoinButton joinRoom={joinRoom} />
</div>
</>
);
};
export default Join;
<Room />
component. Now, let’s move forward and render the peers connected to our room.<Room />
component inside the components/Room.jsx
file, you can see that we have hardcoded the value of peers to an empty array.Let’s make this dynamic.selectPeers selector
combined with the useHMSStore()
hook to get an array of all the peers connected to the room in the form of objects. Each of these peer objects will contain information such as their name
and roleName
that we can use to render their tiles accordingly.filter()
JavaScript array method to separate them into listenersAndHandraised
and speakersAndModerators
using the roleName
property on each peer object. This will help us render the appropriate tile based on the user’s role.listener
or handraise
, we will render the <ListenerTile />
component. Else, we will render the <SpeakerTile />
component. While rendering these tiles, pass the peer
object as a prop in order to display the peer’s information inside the tiles.// components/Room.jsx
import { selectPeers, useHMSStore } from '@100mslive/hms-video-react';
import RoomInfo from './Room/RoomInfo';
import Controls from './Room/Controls';
import ListenerTile from './User/ListenerTile';
import SpeakerTile from './User/SpeakerTile';
const Room = () => {
const peers = useHMSStore(selectPeers);
const speakersAndModerators = peers.filter(
(peer) => peer.roleName === 'speaker' || peer.roleName === 'moderator'
);
const listenersAndHandraised = peers.filter(
(peer) => peer.roleName === 'listener' || peer.roleName === 'handraise'
);
return (
<div className="flex flex-col bg-main text-white min-h-screen p-6">
<RoomInfo count={peers.length} />
<div className="flex-1 py-8">
<h5 className="uppercase text-sm text-gray-300 font-bold mb-8">
Speakers - {speakersAndModerators.length}
</h5>
<div className="flex space-x-6 flex-wrap">
{speakersAndModerators.map((speaker) => (
<SpeakerTile key={speaker.id} peer={speaker} />
))}
</div>
<h5 className="uppercase text-sm text-gray-300 font-bold my-8">
Listeners - {listenersAndHandraised.length}
</h5>
<div className="flex space-x-8 flex-wrap">
{listenersAndHandraised.map((listener) => (
<ListenerTile key={listener.id} peer={listener} />
))}
</div>
</div>
<Controls />
</div>
);
};
export default Room;
<RoomInfo />
component takes a count prop with the total number of peers connected to the rooms as its value. For the speakers and listeners headings, we can access the length property of speakersAndModerators
and listenersAndHandraised
array, respectively, to get their count.<Controls />
component inside components/Room/Controls.jsx
. Essentially, we will be having three controls: one to toggle our mic on or off, one to toggle hand-raise, and lastly to leave the room. We'll cover the hand-raise functionality in the latter part of this blog post.<MicButton />
component responsible for the toggle mic functionality will only be displayed to the speakers and moderators whereas, the <ExitButton />
component will be displayed to all roles.selectIsLocalAudioEnabled
selector to get the status of our mic, and the selectLocalPeer
selector to get our local peer object.// components/Room/Controls.jsx
import {
useHMSStore,
selectIsLocalAudioEnabled,
selectLocalPeer,
} from '@100mslive/hms-video-react';
import MicButton from './MicButton';
import ExitButton from './ExitButton';
import HandRaiseButton from './HandRaiseButton';
const Controls = () => {
const isMicOn = useHMSStore(selectIsLocalAudioEnabled);
const peer = useHMSStore(selectLocalPeer);
const isListenerOrHandraised =
peer.roleName === 'listener' || peer.roleName === 'handraise';
return (
<div className="flex justify-center space-x-4">
{!isListenerOrHandraised && (
<MicButton isMicOn={isMicOn} toggleMic={() => {}} />
)}
{isListenerOrHandraised && (
<HandRaiseButton
isHandRaised={peer.roleName === 'handraise'}
toggleHandRaise={() => {}}
/>
)}
<ExitButton exitRoom={() => {}} />
</div>
);
};
export default Controls;
useHMSActions()
hook and store it inside hmsActions
.setLocalAudioEnabled()
method on the hmsActions
object inside the toggleMic
prop of the <MicButton />
component. true
for turning on the mic and false
for turning it off. Since we want to toggle, we can pass the opposite of current status using the ! operator.leave()
method on the hmsActions
object.// components/Room/Controls.jsx
import {
useHMSStore,
useHMSActions,
selectIsLocalAudioEnabled,
selectLocalPeer,
} from '@100mslive/hms-video-react';
import MicButton from './MicButton';
import ExitButton from './ExitButton';
import HandRaiseButton from './HandRaiseButton';
const Controls = () => {
const hmsActions = useHMSActions();
const isMicOn = useHMSStore(selectIsLocalAudioEnabled);
const peer = useHMSStore(selectLocalPeer);
const isListenerOrHandraised =
peer.roleName === 'listener' || peer.roleName === 'handraise';
return (
<div className="flex justify-center space-x-4">
{!isListenerOrHandraised && (
<MicButton
isMicOn={isMicOn}
toggleMic={() => hmsActions.setLocalAudioEnabled(!isMicOn)}
/>
)}
{isListenerOrHandraised && (
<HandRaiseButton
isHandRaised={peer.roleName === 'handraise'}
toggleHandRaise={() => {}}
/>
)}
<ExitButton exitRoom={() => hmsActions.leave()} />
</div>
);
};
export default Controls;
selectPeerAudioByID
selector.selectIsPeerAudioEnabled
selector, which also takes the peer’s ID as the parameter and returns a boolean value to indicate the mic status.<SpeakerTile />
component inside components/User/SpeakerTile.jsx
and make the following changes:// components/User/SpeakerTile.jsx
import Avatar from 'boring-avatars';
import { FiMic, FiMicOff } from 'react-icons/fi';
import {
useHMSStore,
selectPeerAudioByID,
selectIsPeerAudioEnabled,
} from '@100mslive/hms-video-react';
import PermissionsMenu from './PermissionsMenu';
const SpeakerTile = ({ peer }) => {
const isSpeaking = useHMSStore(selectPeerAudioByID(peer.id)) > 0;
const isMicOn = useHMSStore(selectIsPeerAudioEnabled(peer.id));
return (
<div className="relative bg-secondary px-12 py-6 rounded-lg border border-purple-500">
<PermissionsMenu id={peer.id} audioTrack={peer.audioTrack} />
<div className="flex flex-col gap-y-4 justify-center items-center">
<div
className={
isSpeaking
? 'ring rounded-full transition ring-3 ring-green-600 p-0.5'
: 'p-0.5'
}
>
<Avatar name={peer.name} size="60" />
</div>
<p className="flex items-center gap-x-2">
{peer.name}
{isMicOn ? (
<FiMic className="h-3 w-3" />
) : (
<FiMicOff className="h-3 w-3" />
)}
</p>
</div>
</div>
);
};
export default SpeakerTile;
<PermissionsMenu />
component inside the components/User/PermissionsMenu.jsx
file. We want to display this menu only if we have the moderator
role.localPeer
object, we can use the selectLocalPeer
selector function. This will return an object with the roleName
property that we can check to get our role. selectLocalPeerRole
selector and access the name
property of the returned object.moderator
, use the ===
equality operator to check if our roleName
equates to moderator
. moderator
.Mute Peer
, Make Listener
, and Make Speaker
. To achieve these functionalities, create a new instance of the useHMSActions()
hook to get access to all the required methods.setRemoteTrackEnabled()
method on hmsActions
with the peer’s audio track (that we’re getting from the props) and false
as parameters.changeRole()
method on hmsActions
along with the peer’s ID, new role, and a force boolean value to change their role without asking them or give them a chance to accept/reject.// components/User/PermissionsMenu.jsx
import { useState } from 'react';
import { AiOutlineMenu } from 'react-icons/ai';
import {
useHMSStore,
useHMSActions,
selectLocalPeer,
} from '@100mslive/hms-video-react';
const PermissionsMenu = ({ audioTrack, id }) => {
const hmsActions = useHMSActions();
const mutePeer = () => {
hmsActions.setRemoteTrackEnabled(audioTrack, false);
};
const changeRole = (role) => {
hmsActions.changeRole(id, role, true);
};
const localPeer = useHMSStore(selectLocalPeer);
const [showMenu, setShowMenu] = useState(false);
const btnClass = 'w-full text-sm font-semibold hover:text-purple-800 p-1.5';
const isModerator = localPeer.roleName === 'moderator';
if (isModerator) {
return (
<div className="absolute right-1 top-1 z-50">
<AiOutlineMenu
className="ml-auto"
onClick={() => setShowMenu(!showMenu)}
/>
{showMenu && (
<div className="mt-2 bg-white text-black py-2 rounded-md">
<button className={btnClass} onClick={() => mutePeer()}>
Mute
</button>
<button className={btnClass} onClick={() => changeRole('listener')}>
Make Listener
</button>
<button className={btnClass} onClick={() => changeRole('speaker')}>
Make Speaker
</button>
</div>
)}
</div>
);
} else {
return null;
}
};
export default PermissionsMenu;
hand-raise
. As a listener
, you might also want a chance to become a speaker at times. To notify the moderator
, we can build a simple hand-raise button that will display a badge next to your avatar to show that you're interested to speak.listener
to handraise
on clicking the <HandRaiseButton />
component. <Controls />
component inside components/Room/Controls.jsx
.<HandRaiseButton />
component with 2 props: a isHandRaised
boolean prop to check if you've raised hand currently and a toggleHandRaise
function to toggle it. Also, we'll display this button only if we are a listener
or have handraise
role.isHandRaised
prop, we simply need to get our role by accessing the roleName
property of our local peer and check if it equates to the handraise
role.changeRole()
method available on the hmsActions
object like we did for the <PermissionsMenu />
component.changeRole()
method takes our local peer's ID, the new role to set, and a force boolean prop. For the new role, if we are a listener
currently, we need to pass handraise
as the parameter. If we already have the role of handraise
, we need to set it back to listener.// components/Room/Controls.jsx
import {
useHMSStore,
useHMSActions,
selectIsLocalAudioEnabled,
selectLocalPeer,
} from '@100mslive/hms-video-react';
import MicButton from './MicButton';
import ExitButton from './ExitButton';
import HandRaiseButton from './HandRaiseButton';
const Controls = () => {
const hmsActions = useHMSActions();
const isMicOn = useHMSStore(selectIsLocalAudioEnabled);
const peer = useHMSStore(selectLocalPeer);
const isListenerOrHandraised =
peer.roleName === 'listener' || peer.roleName === 'handraise';
return (
<div className="flex justify-center space-x-4">
{!isListenerOrHandraised && (
<MicButton
isMicOn={isMicOn}
toggleMic={() => hmsActions.setLocalAudoEnabled(!isMicOn)}
/>
)}
{isListenerOrHandraised && (
<HandRaiseButton
isHandRaised={peer.roleName === 'handraise'}
toggleHandRaise={() =>
hmsActions.changeRole(
peer.id,
peer.roleName === 'listener' ? 'handraise' : 'listener',
true
)
}
/>
)}
<ExitButton exitRoom={() => hmsActions.leave()} />
</div>
);
};
export default Controls;
<ListenerTile />
component. Inside this component, we just need to check if the peer's role is set to handraise
and then conditionally render the <HandRaiseBadge />
accordingly.