DEV Community

Cover image for Building a Twitch Clone with React and Tailwind CSS
Nilay Jayswal
Nilay Jayswal

Posted on • Originally published at 100ms.live

Building a Twitch Clone with React and Tailwind CSS

Twitch, owned by Amazon, is a streaming platform‌‌ via which people can go live playing games, singing songs or doing anything that can be filmed.

In this step-by-step guide, we will be exploring building the popular streaming platform known as Twitch and also grasp the basics of a video conferencing app in general with the aid of 100ms.

This content was originally published - HERE

Our Twitch clone will consist of

  1. A video display - this is where the viewers see the stream
  2. Realtime chat
  3. Screen share option - to share screen for streaming the game
  4. Multiple hosts

To follow this tutorial you should have basic knowledge of JavaScript, React, Tailwind CSS, or a CSS library.

Also, have the following installed on your PC :

  1. VS Code code editor or any other preferred IDE code editor
  2. Node.js for npm or yarn packages

and an account with 100ms.

100ms is a cloud platform that allows developers to add video and audio features to all types of applications. It provides a dashboard that makes it quite easy to manipulate descriptive features of a video/audio platform from in-app interaction to the session recording. The 100ms SDK and packages is one of those very easy-to-use tools that save time without losing on effectiveness.

Setting up the Project

I have created a starter project based on create-react-app and tailwind. Folders include utils, styles, assets, and components for utilities like the token call, styles, images, and js files respectively. You will also find some imports and hooks already done to make the starter project easy to use. Just follow the steps and you should get a working app.

Clone the repository and run npm install to install all dependencies and npm start to start the server in your localhost.

When this is done, we get this:

Image description

This will not navigate forward because some important credentials are needed to operate a room.

Now, let's go into getting our 100ms SDK setup and get the necessary credentials.

Retrieving Credentials

To activate and link the 100ms SDK to our app, a token_endpoint and room_id are needed.

Register on the 100ms app, after registration, you would redirect to your dashboard to create an app.

Add a unique subdomain, (in this case the name is twitchclone) and from the template, options choose the “Virtual Event” option to create a template as seen below:

Image description

Image description

Note: It is not compulsory to use the virtual event template, you can create a custom app as long as the roles are well defined.

Create Roles

A role defines who a peer sees or hears, the quality at which they publish their video, whether they have permission to publish video/screen share, mute someone, change someone's role.

Our twitch clone would have the following roles:

  1. Viewer — can only listen to the stage. To create this role, turn off all publish strategies.
  2. Stage — can speak, mute and unmute himself as well as share screen. To create this role, turn on all publish strategies, then turn off all permissions except can end current session of the room and remove all participants.

Image description

Image description

Image description

Image description

Now we can create our room.

Create Room

Once a user joins a call, they are said to have joined a room. They are referred to as peers (local and peers).

To create a room, click on Rooms from the dashboard then Create Room as seen in the images below:

Image description

Image description

Image description

You can then get your room_id from the image above. To retrieve your token_endpoint, go to the developer section and copy the token_endpoint.

Image description

Now that we are done setting up our 100ms app, we can proceed with building our application.

Building our Clone

To use the SDK we installed previously, there are two hooks we need to be familiar with:

  1. hmsActions - this is used to perform any action such as sending a message or joining a room.
  2. hmsStore - this contains the complete state of the room at any given time such as the participant details etc.

Having said that, let's get coding.......

We will start with the getToken.js file in the utils folder:

getToken.js

const endPoint = "<token_endpoint>";
export default async function getToken(role) {
    const response = await fetch(`${endPoint}api/token`, {
        method: 'POST',
        body: JSON.stringify({
            user_id: '1234', // a reference user id for the user 
            role: role, // stage, viewer 
            room_id: "<room_id>" // as present on the dashboard
        }),
    });
    const { token } = await response.json();
}
Enter fullscreen mode Exit fullscreen mode

Fill in your token_endpoint and room_id following the steps outlined in the previous section.

To call the join function, add the following code to your joinRoom function in the JoinForm.js file

getToken(role).then((token) => {
    hmsActions.join({
        userName: userName || 'Anonymous',
        authToken: token,
        settings: {
            isAudioMuted: true,
        },
    });
}).catch((error) => {
    console.log('Token API Error', error);
});

Enter fullscreen mode Exit fullscreen mode

So as to publish our roles, in the Select.js file add the options to the select tag

<option id="only" value='stage'>stage</option> 
<option value='viewer'>viewer</option>
Enter fullscreen mode Exit fullscreen mode

Once this is done, you should get this

Image description

When a user joins a room, we want to hide this form and display the room which the user joined whether as a host(stage) or a viewer(listener).For this add the code below to your App.js file

{isConnected ? <Room /> : <JoinForm /> } 
Enter fullscreen mode Exit fullscreen mode

Pay attention here as we will be displaying different room features depending on the role a user joined as. For the stage/host, we want to display a mirror video, a chat section, and a control bar. For the viewer, we want to display the host's video, the screen shared, the chat section, and a leave button. To do this, we will use the ternary operator to select the appropriate component based on user's role.

In this case, we are using the stage role i.e. stage role === localPeer.roleName === "stage" here let's refer to it as the isModerator keyword. Therefore isModerator === localPeer.roleName === "stage". And now we'll conditional render the right component based on this flag.

In the Room.js, add these:

//for video display section
{
  isModerator
    ? localPeer && <VideoTile peer={localPeer} isLocal={true} />
    : peers &&
      peers
        .filter(peer => !peer.isLocal)
        .map(peer => {
          return (
            <>
              {' '}
              <VideoTile isLocal={false} peer={peer} />{' '}
            </>
          );
        });
}

//For screen share display section
{
  isModerator
    ? null
    : peers &&
      peers
        .filter(peer => !peer.isLocal)
        .map(peer => {
          return (
            <>
              {' '}
              <Screen isLocal={false} peer={peer} />{' '}
            </>
          );
        });
}
Enter fullscreen mode Exit fullscreen mode

In the above, we are looping through each member of the room i.e the localPeer and other peers.

Now that we have our room set up, let's work on the control bar, remember we have to render different features based on the role - ControlBar.js.

Let's add the toggle functions

const isLocalAudioEnabled = useHMSStore(selectIsLocalAudioEnabled);
const isLocalVideoEnabled = useHMSStore(selectIsLocalVideoEnabled);
const isLocalScreenShared = useHMSStore(selectIsLocalScreenShared);

const toggleAudio = async () => {
    await hmsActions.setLocalAudioEnabled(!isLocalAudioEnabled);
};
const toggleVideo = async () => {
    await hmsActions.setLocalVideoEnabled(!isLocalVideoEnabled);
};
const toggleScreen = async () => {
    await hmsActions.setScreenShareEnabled(!isLocalScreenShared);
}
Enter fullscreen mode Exit fullscreen mode

Then, we'll proceed to render buttons to see the current state and toggle it.

For audio controls

{
  isLocalAudioEnabled ? (
    <img src={Unmute} alt="mute" className="image h-5 w-5 rounded-lg" />
  ) : (
    <img src={Mute} alt="unmute" className="image h-5 w-5 bg-gray-900 rounded-lg" />
  );
}
Enter fullscreen mode Exit fullscreen mode

For video display controls

{isLocalVideoEnabled ? "Hide" : "Unhide"}
Enter fullscreen mode Exit fullscreen mode

For screen share controls

{isLocalScreenShared ? "Unshare" : "Share"}
To leave a room (viewer) and end session(host) control

<button
  className="text-xs uppercase tracking-wider bg-white py-1 px-2 rounded-lg shadow-lg text-iwhite ml-2 bg-red-600"
  onClick={() => {
      hmsActions.endRoom(false, "reason") && hmsActions.leave();
  }}
>
    End
</button>;
Enter fullscreen mode Exit fullscreen mode

In the above, we added video and screen share toggle buttons. Let's proceed to add the video and screen tile to effectively display the video and screen when we click on these buttons. In the Video Folder, you'll find two files, the VideoTile.js and Screen.js. Both files are merely replicas of themselves except they share a different part of the stage while VideoTile.js shares the video, Screen.js shares the screen.

VideoTile.js

React.useEffect(() => {
  (async () => {
    console.log(videoRef.current);
    console.log(videoTrack);
    if (videoRef.current && videoTrack) {
      if (videoTrack.enabled) {
        await hmsActions.attachVideo(videoTrack.id, videoRef.current);
      } else {
        await hmsActions.detachVideo(videoTrack.id, videoRef.current);
      }
    }
  })();
}, [hmsActions, videoTrack]);
Enter fullscreen mode Exit fullscreen mode

To display different screen sizes depending on the roles add

{
  isModerator ? (
    <video
      ref={videoRef}
      autoPlay={true}
      playsInline
      muted={false}
      style={{ width: 'calc(85vw - 100px)' }}
      className={`object-cover h-70 -ml-3 mt-10-h h-auto w-24 shadow-lg" ${isLocal ? 'mirror' : ''}`}
    ></video>
  ) : (
    <video
      ref={videoRef}
      autoPlay={true}
      playsInline
      muted={false}
      className={`object-cover h-40 w-40 rounded-lg mt-12 shadow-lg ${isLocal ? 'mirror' : ''}`}
    ></video>
  );
}
Enter fullscreen mode Exit fullscreen mode

Screen.js

React.useEffect(() => {
  (async () => {
    console.log(screenRef.current);
    console.log(screenTrack);
    if (screenRef.current && screenTrack) {
      if (screenTrack.enabled) {
        await hmsActions.attachVideo(screenTrack.id, screenRef.current);
      } else {
        await hmsActions.detachVideo(screenTrack.id, screenRef.current);
      }
    }
  })();
}, [hmsActions, screenTrack]);
Enter fullscreen mode Exit fullscreen mode

‌To display screen shared for viewer

<video
  ref={screenRef}
  autoPlay={true}
  playsInline
  muted={false}
  className={`h-screen ${ isLocal ? "" : "" }`} >
</video>
Enter fullscreen mode Exit fullscreen mode

At this point, we are almost done building our app final part is adding the chat section. The 100ms SDK fully supports in-room chat and P2P chat. But, we'll be working with in-room chat where both host and viewers can all chat. It is important to note that the chats do not persist, meaning when a new peer joins they can't see the earlier chat .The starter project comes with a chat section that has been built for you. Import the files to the Room.js file and join room.

Finally, let's add the header and footer tab to the room to give a static twitch-like display.

Footer.js

<p className="text-red-700 flex mx-2">
    <img src={User} alt="avatar" className="w-5 h-5 mr-2" />
    {peers.length}
</p>
 <button className='text-xs uppercase tracking-wider bg-white py-1 px-2 rounded-lg shadow-lg text-iwhite ml-2 bg-red-600'
   onClick={() => {
          hmsActions.leave();
        }}
   >
     Leave
   </button>
Enter fullscreen mode Exit fullscreen mode

This is also where we add the count of persons in a room peers.length.

Room.js

Import the Header, ChatContainer, ControlBar, and Footer respectively. The final Room.js code base should look like this:

import React from "react";
import Screen from "../Video/Screen";
import VideoTile from "../Video/VideoTile";
import ControlBar from "../Control/ControlBar";
import ChatContainer from '../Chat/ChatContainer';
import Footer from "../Control/Footer";
import Header from "../Control/Header";
import {
  useHMSStore,
  selectLocalPeer,
  selectPeers
} from "@100mslive/hms-video-react";

const Room = () => {
  const localPeer = useHMSStore(selectLocalPeer);
  const isModerator = localPeer.roleName === "stage";
  const peers = useHMSStore(selectPeers);


   return (
    <div className="flex flex-row">
      <Header />
          <div className="flex flex-wrap">
              {
              isModerator ? 
                  (localPeer && < VideoTile peer={localPeer} isLocal={true}  /> )
                :
                (peers &&
                  peers
                    .filter((peer) => !peer.isLocal)
                    .map((peer) => {
                      return (
                        <>
                          <VideoTile isLocal={false} peer={peer} />
                        </>
                      );
                    }))
              } 

      </div>

      <div className="bg-gray-900 m-0 h-screen z10 self-center flex-wrap absolute top-0 left-0" style={{ width: 'calc(90vw - 100px)' }}>
      {
              isModerator ? 
                  null
                :
                (peers &&
                  peers
                    .filter((peer) => !peer.isLocal)
                    .map((peer) => {
                      return (
                        <>
                          <Screen isLocal={false} peer={peer}/>
                        </>
                      );
                    }))
              }     
      </div>
      <ChatContainer />


      {
        isModerator ? <ControlBar /> : <Footer />
      } 


    </div>
  );
};

export default Room;
Enter fullscreen mode Exit fullscreen mode

Now, let's get twitching.

The stage/host room display should look something like this

Image description

In the above, we can see that the host can see the number of persons in a room, mute/unmute self, shares screen, hide/unhide video tile and as well end the session.

The viewer/listener's page should look like this:

Image description

The viewer gets to see the video and screen displayed by the host and can also leave the room.

Note: This was made possible by conditional rendering and the fantastic 100ms SDK helping accomplish this with very few lines of code and in a short time. I bet it would take days to figure out how to build a streaming platform from scratch.

Check out the demo here.

Conclusion

100ms is that SDK tool that provides multiple solutions with just a few clicks and hooks. There are numerous more use cases of 100ms like recording, face time 1-1, and lots more.

Join the discord channel to learn more about 100ms, don't forget to give it a try in your next app for free. You can get the full source code here.‌‌

Latest comments (1)

Collapse
 
_yogeshsingh profile image
Yogesh Singh

Nice!