So now I want to go ahead and I want to create the first component in this allowed to watch stream which is going to be the video component. So in here we're gonna have a loading state, we're gonna have an offline state if the user is not streaming and finally we're going to have the actual live stream. So in order to do that I want to do the following. First thing I want to replace this with an empty fragment like this and then I want to go ahead and I want to import LiveKit room from at livekit slash components dash react so let me show you my package json this is my version if you're wondering if you have a higher version It doesn't mean that it will not work. It can definitely work, right?
But in case you run into some issues, just so you know, this is the exact version I have. So you can always use that one if you're having any problems. Great, so now what we have to do is we have to render that LiveKit room. So let's do LiveKit room like this and let's open it up like this and inside I'm going to write stream like this. And now we have to go ahead and pass in some props here.
So let's go ahead and let's pass in the token to be token which we get from our use viewer token hook right here. That's why we need to check if we have it. Then let's go ahead and pass in the server URL which is going to be process.environment.next underscore public underscore livekit underscore vs underscore url and as always just double check inside of your dot environment file that you have this property right here you can go ahead and find it well it's it's actually exactly the same as your LiveKit URL as you can see it's exactly the same. The only difference is that instead of HTTPS you use VSS like this. So just make sure you have next public LiveKit VS URL you can also find it in the LiveKit config page.
Just go into your settings, into API keys, and paste that here. So make sure this is correct like that. And now this is what I have. I have a little error here, which says that attempted import error, local participant is not exported from LiveKit client. So they have an open issue about this error and it's actually not a LiveKit issue.
It's a Next.js issue when it comes to this .mjs extensions. And thankfully there is a fix for it. So if you're not having this error, in that case, you know, Next.js has updated and they fixed that. But in case you are having this error, this is what you can do. So go ahead inside of your .next.config.js inside.
And let's go ahead and let's add webpack option let's get the current config and let's go ahead and do config.module.rules.push and inside let's write test and go ahead and add a forward slash then go ahead and add the backward slash I'm not sure where it is there we go the backward slash dot mjs add a little dollar sign and then a forward slash so an regex to recognize dot mjs and you can also copy this from my github directly if you're having trouble writing this and then let's do include forward slash node underscore modules forward slash as well and type let's go ahead and write JavaScript slash auto like this and very important is that you do return config in the end like that and what I recommend you do next is shut down your application So I'm gonna go ahead and shut down my npm run dev and do npm run dev again. And you can keep your ngrok running, right? So this is automatically going to get connected. Don't worry about that. And go ahead and refresh your page again.
And let's see if this resolved the issue. There we go. Now you can see the text stream and you can see how for a second it said cannot watch the stream meaning it's loading the token and now we have our text meaning that we successfully fixed that little issue. So go back inside of the components, stream player and now we can continue developing inside of here. So let's go ahead and give this LiveKit room a class name which is gonna go ahead and have a grid grid calls one, lg gap y zero, lg grid calls three, excel grid calls three as well, to excel lg gap y 0 lg grid calls 3 excel grid calls 3 as well to excel grid calls 6 and h full like this and let's just not misspell grid here so make sure you don't have any misspelling so whenever you hover on a class it should give you exactly what it's doing that way you know that you have correct names and that is the extension called Tailwind CSS IntelliSense which we installed in the beginning of the project.
And then let's add a little div here and let's create the class name for the first column inside of this grid which is going to hold our video column. So space y4, colspan1, on lg colspan2, on Excel colspan2 as well, call span 1, on LG call span 2, on Excel call span 2 as well, on 2xl call span 5, on LG overflow y auto hidden scrollbar. So this hidden scrollbar is what we got when we copied my global CSS if you remember from the beginning I told you to copy it from my github and in here we have hidden scrollbar so that's what we're going to use here and padding bottom of 10. Like that and then inside we can go ahead and render the video component which you don't have yet so don't import this from anywhere instead just pass the prop hostname to be user.username and pass in the host identity to be user.id like that. Great.
And now we have to go ahead and create the video component. So we're going to do that inside of the stream player. And what I actually should have done is created a folder for a stream player. So let's do the following. I'm going to shut down my app just so I don't have any cache errors here.
So I'm shutting down my npm run dev. And then what I'm gonna do is I'm gonna create a folder stream player. I'm gonna drag and drop the component StreamPlayer inside of that folder like this. I'm going to click OK if this pops up and then I should have one unsaved file here inside of my dashboard.u username home page. So in here now I have StreamPlayer from StreamPlayer StreamPlayer.
So you can save this for now, but we're going to fix that in a second. So go back inside of your components and simply rename this component to index like this. And if this shows again, you can click okay. And then again, you're gonna have an unsaved file here in dashboard u username home page dot vsx and you can see how now it changed back to that previous version because we're using the index so you can just click save and there we go. Great and now let me go ahead and just run my app again so npm run dev like this let's refresh the localhost and let's go back inside of here and now inside of this stream player folder create a new file video.tsx like that.
Let's go ahead and mark this as use client. Let's create an interface video props to accept a host name which is a string and a host identity which is also a string and let's do export const video and let's assign the props video props and let's return a div saying video like this. Now let's go back inside of the stream player index and import video from dot slash video as I did right here and when you save you should no longer be having any errors instead once this token loads from cannot watch the stream you should be seeing the video. Great! So now Let's go inside of the video and let's go ahead and start developing our loading states.
So in here I want to give this div a class name of AspectVideo, border-bottom, group and a relative like this. Alright and now you should have a little border right here in the bottom as you can see and when you expand to a larger screen you can see how it has a cut off because this space here is going to be for our chat but on mobile mode the entire space is going to be for the video. Now inside of the video component, we have to import a couple of hooks which are going to help us with the current status of the stream. So let's go ahead and import ConnectionState from LiveKitClient and let's also import track from LiveKit client. Then let's go ahead and import the following hooks from at LiveKit components react.
So we're going to need useConnectionState, we're going to use useRemoteParticipant and useTracks. Like that. And now inside of here let's extract the host name and the host identity and let's go ahead and first get the connection state which is going to be useConnectionState then let's go ahead and let's get the participant from useRemoteParticipant and pass in the host identity Then let's go ahead and get the tracks so audio and video tracks right from use tracks like that and go ahead and pass in an array inside the use track.source.camera and track.source.microphone like that so make sure you have imported track from livekit client here at the top and then what we're going to do is we're going to add .filter get the individual track and only use those which match the host identity so track.participant.identity is equal to host identity like this. Great! And now let's go ahead and dynamically render the content.
So let content and I'm gonna go ahead and replace this with content like this and let's go ahead and do the following if we don't have the participant from the hook and if the connection state is connection state dot connected so you can import connection state from livekit client So if we don't have the participant and we are connected in that case it means that we are offline. So I'm just going to write a paragraph host is offline like that. Now let's go ahead and write else if there is no participant or tracks.length is zero. In that case, the content is going to be a paragraph which says loading. And then let's do a last else content is going to be a paragraph live video, right?
Great. So now let's go ahead and try this out. So now it says loading, cannot watch the stream, loading and then host is offline. So now I'm gonna go inside of my OBS studio and I'm going to click start streaming. So do the same thing and just confirm that inside of your stream settings you have the matching server URL and stream key.
So now if I go ahead and click start the streaming I shouldn't even have to refresh instead there we go now it said loading for a second and then it said live video and if I go ahead and click stop streaming it should go back to offline. There we go so we successfully have the connection All we have to do now is change this from being paragraphs to actually being video components. So just one more time I want to show you how to do it in your OBS just in case you're confused. So you should have a big button which says start streaming and just confirm in your settings in your stream that you have the matching server and stream key from your keys right here. Right, so confirm that these two are matching, go back inside of here and go ahead and click the big start streaming button right here and then once you do that it says live video.
Once I go back and click stop streaming it's going to say host is offline. So the first state I want to create is the offline video so make sure you are not streaming make sure this says host is offline right and let's go ahead and create another component inside of our stream player folder called offlinevideo.tsx like that and let's go ahead and import wi-fi off from lucid react and let's create an interface offline video props here to accept the username, which is a string, and let's export const offline video like that to accept the props offline video props. And then we can destructure the username. And all we're going to do is simply return a div with a class name of hFullPlexFlexCall space y 4 justify center and items center. And then inside we're going to render the Wi-Fi off icon and we're going to give it a class name of height of 10, width of 10 and text muted foreground and then below that open up a paragraph and give it a class name of TextMutedForeground as well and inside render a span, my apologies, so a span like this, which is simply going to render the username.
Actually, we don't need a span at all. We can just do username is offline. As simple as this. Now go back inside of your video component and instead of rendering the paragraph host is offline, we can now render the offline video component. So make sure you add the import for offline video as I did here and go ahead and pass in the username property to be hostname like that and now you should have a nice text which says Antonio is offline.
Great! So now what I want to do is I want to create the loading video component so it's going to be very similar so you can actually copy offline video and paste it here and rename it to loading video like that. Go inside of the loading video and first let's rename the props from offline video to be loading video like that and let's change the prop to accept a label and let's go ahead and extract the label from the props here and now what I'm going to do is I'm not going to import Wi-Fi off instead I'm going to import Loader like this and let's go ahead and replace that icon here and alongside the height of 10, text muted foreground let's also give it a class name animate spin. So it's going to be spinning all the time. And inside of here, let's go ahead and simply render the label like this.
Great. So that's going to be our loading video here. And now let's go ahead inside of video component and let's replace the loading state with the loading video from .slash loading video so make sure you import this and we are simply going to pass the state label to be the connection state like that. And now if you go ahead and refresh here, you can see how we have a little spinner connecting, Antonio is offline. That's how we also have disconnected when you refresh.
And one thing that I want to do is go inside of the loading video and give this paragraph a class name of Capitalized. So it starts with a capital letter, you can see now it looks a little bit better with the connecting being capitalized. Great! And there is one last left and that is to create the live video component. So let's go back inside of our stream player and create a new file livevideo.tsx like that.
Go ahead and mark this as use client and go ahead and export const live video here and return a div live video and now let's go ahead and create an interface live video props to accept a participant which is going to be a type of participant from LiveKitClient like that and go ahead and assign the live video props here and extract the participant like this. Now go back inside of the video component and in here finally replace this with live video from .slash live video and we have to pass in the prop participant to be participant like this. There we go. Great and now we're going to do a very simple way to render this and show the video. So inside of this div right here we're going to add a class name relative age full and flex and then inside we're going to render a video element and let's go ahead and make this a self-closing tag and what I want to give it is a ref which we don't have yet so let's just give it a width of 100% for now.
And then let's go ahead and let's create two refs here. So const videoRef is useRef from React. So make sure you add useRef from React as I did. And we're also gonna have the wrapper ref, which is also use ref. And now go ahead and give this a type.
So video is gonna be HTML video element and the wrapper is gonna be HTML div element and give them both default values of null. Like this. And then go ahead and assign those. So this div right here is going to have a ref of wrapper ref and the video is going to have a ref of video ref like that, so make sure you added this and what we have to do now is we have to use some hooks again to enable this from actually working, right? So let's go ahead and import everything we need from LiveKit components here so let's go ahead and import everything we need from LiveKit components here so let's go ahead and import use tracks from components from LiveKit components react and from LiveKit client let's import the track itself.
And now what we can do is go ahead and do the use track, use tracks, sorry, so make sure you imported use tracks and let's go ahead and write track.source.camera track.source.microphone like this, dot filter, get the individual track and confirm that track.participant.identity is equal to the prop that we passed. So participant.identity. And then let's do for each track If we have video ref.current go ahead and do track.publication.track?.attach video ref.current like this And if you start streaming this should be working. So I'm going inside of my OBS and I'm going to click start streaming again. And I believe that we should be seeing my screen here.
That we should be seeing... There we go. You can also hear me twice because our microphone is working as well. So you should be seeing exactly what you're seeing in your OBS. Let me go ahead and show you this.
So I just moved my OBS to the side here and this is what I'm seeing here. And you can see that it has a little bit of a delay because that's exactly what's rendering in this page right now. And you should also be hearing your voice if you added the microphone source, right? Great! So this is now working, right?
And if I click stop streaming in here in a couple of seconds, this turns back to offline. Great. So what I want to add now is I want to add controls for the volume and for the microphone. What I meant to say are controls for the microphone and for the full screen preview. So go inside of the stream player and create a new file fullscreen-control.tsx like this.
Let's go ahead and let's import maximize and minimize from Lucid React. Let's also go ahead and mark this as use client. And let's go ahead and import the hint from dot dot slash hint, but I'm going to change it to slash components so it's clearer. Let's create an interface full screen control props. Is full screen is going to be a Boolean and on toggle is simply going to be a void.
And then let's do export const fullscreen control. Let's assign the props, fullscreen control props. And let's go ahead and extract the isFullscreen and onToggle from here. And now let's go ahead and dynamically render the icon. So const icon with capital I is going to check if we are full screen.
In that case, we use the minimize icon, otherwise maximize icon. Maximize like this. And then let's render the label for the hint. So if is full screen, in that case, the hint is going to be exit full screen. Otherwise, it's going to be enter full screen like this and let me just expand this so you can see it in one line and then let's go ahead and return a div with a class name of FlexItemsCenter, JustifyCenter and Gap4 Let's go ahead and add our hint component.
Let's give it a label as label and as child property. And inside, we're gonna render a native button. So don't import button from anywhere, right? So this is a native button component. We are not importing this.
And inside we're going to go ahead and render the icon component and let's go ahead and give it a class name of h-5 and w-5 and give this button an onclick to be on toggle and let's give it a class name of TextWide B-1.5 hover BG-White-10 and rounded large like that. Great, So we have our volume control. Now we can go back inside of the live video here and below the video component let's add a div with a class name of AbsoluteTop0. So we have to make this absolute to our relative container so we can always put it at the bottom and we also want to add a little overlay which is darkened so the user can clearly see those buttons, right? So let's give it an absolute top 0, hFull, widthFull, opacity 0 by default but when we hover that's when we're gonna see that's what we're gonna see those actions and let's also add a transition all.
Then inside let's create a div and further position these elements so class name here is going to be absolute bottom zero flex h14 width pool items center justify between VG gradient to right from neutral 900 and PX4 and then inside put the full screen control like this and let's give it a value is fullscreen of false and let's give it on toggle to just be an empty arrow function like this and now I'm going to go ahead and click start streaming again so make sure you click start streaming and this should connect and there we go you can see how I have a nice button which says enter full screen and it's not visible now but when I hover that's when it's visible great So now what I want to do is create the actual functionality to toggle between full screen or not. So first thing that I want to do is create a state. So let's do use state here. And after this refs, let's add const isFullScreen, set isFullScreen, useState and by default it's going to be false and then what I want to do is I want to go ahead and create const toggleFullScreen to be a function which is going to check if we are full screen if isFullScreen right from this state in that case sorry isFullScreen so basically this one make sure you have no errors right so if it isFullScreen we're going to do document.exitFullScreen like that and let's go ahead and do else if we have wrapper ref current let's do wrapper ref.current.requestFullScreen like that and then let's go ahead and add the states here so we're gonna go ahead and do setIsFullScreen to be true and let's go ahead here and write set isFullScreen to be sorry the opposite this is true and this is false right so let's go ahead and do that now so inside of this onToggle I want to go ahead and give it a property of onToggle, sorry, toggleFullScreen and give this one a property of isFullScreen, right?
So let's see if that is enough for this to work. So make sure you start your stream. So I'm going to do that now. There we go. I'm connected.
And when I click on this, there we go. I am in full screen. And when I click again, I exit the full screen. Perfect. Exactly what we want.
And as you can see, there is a little issue that we have here so that happens if you go ahead and click on enter full screen and then use the escape button to exit the full screen right and then when you try again we have an error. So let's go ahead and resolve that. So I'm going to import useEventListener from useHooks. So let's import useEventListener from useHooks.ts and then what I'm going to create is another function here So just below the toggle fullscreen called const handle fullscreen change like this and I'm going to do const isCurrentlyFullScreen to be document.fullscreen element is not null like that so let me see if I can zoom out a bit more so you can see it in one line like that and then I'm gonna do set is full screen to do is currently full screen like that and then let's do use event listener which we imported to listen to full screen change and do handle full screen change and add the wrapper ref as the third argument like this. So I believe that now we should no longer be having that issue.
I'm going to refresh here just to confirm. So if we go ahead inside and escape and then try again and it is working. Great! And I believe that actually we can now remove this manual state changes because we have an event listener. So I think we can just remove this and let this handle the wrapper and let this handle the state.
So let's try it now. I'm gonna refresh again. Cannot watch the stream. Okay and I think this is still working. Yeah looks like this is an even better solution because we won't have any conflicting set states here.
Perfect. Great! So just don't spam this too much. I actually tested this on other streaming website as well and apparently there is a limit to how much you can toggle between fullscreen and minimized and apparently at some point it will break and you're just gonna keep getting errors until you clear the cache or something like that. So just don't spam it too much, just confirm that you can exit and enter and that you can click the escape button and then enter again.
Great, so we finished that. What I want to create now is the volume control. So let's go back inside of our stream player folder, right? And just as we created the full screen control, I want to create the volume control.tsx. Let's mark this as use client and let's import volume1 from lucidreact, volume2 from lucidreact and volumex from lucidreact.
So we're going to handle three states, right? If we are muted, if we turned on the volume and if we are at max volume so let's go ahead and import hint from dot dot slash hint so I'm just going to change this to use at slash components like that And we also need a slider so we can import that from chat.cnui. So go ahead and make sure that you have npm run dev running and that you have your ngrok running. And I'm just going to use a new terminal to do npx chat.cn-ui at latest add slider like this. Just wait a second for this to install and great.
Go back inside of the code. And now you can also import slider, not from Radix, but from dot dot slash UI slider or components UI slider. Let's create an interface volume control props here. On toggle is going to be a simple void function. On change is going to be if value is larger than 50.
So the icon by default is going to use the volume1 option, But if we are muted, then we're going to change the icon to use volume X. Else, if is above half, we're going to go ahead and do icon to be volume 2. So it indicates to the user that it is louder. Now let's create the label for our hint. So if is muted let's go ahead and write unmute otherwise mute and let's do const handle change to accept the value which is an array of numbers because that's how the slider component works and what we're gonna do is just do onChange and passing the first of that, like this and now let's go ahead and create our components so return a div with a class name of flex-item-center-and-gap-2 let's create a hint component let's pass in the label to be label and as child property inside render the button component and let's go ahead and give it an on click to be on toggle and let's give it a class name of text white hover bg white slash 10 padding 1.5 and rounded large and then let's render the icon component and give it a class name of h6 and width of 6 like that and then outside of hint render the slider component and go ahead and give it a class name of width 8rem like that and cursor pointer give it an on value change to be handle change and give it a value of array and just passing the current value and max is going to be 100 and step is going to be one like that perfect and now let's go back inside of our live video component and just as we render the full screen control above it render the volume control from .slash volume control so make sure you have both of those imported and now let's go ahead and pass in the onChange to just be an empty arrow function.
Let's pass in the value to be 0. Let's pass in the onToggle to be an empty arrow function as well. And go ahead and start streaming. And Now you should be seeing those two icons. Once you connect, there we go.
You can see how we can mute and unmute and we have this little slider. So what I do now is modify this slider so it looks a bit better. So for that, let's go inside of our components folder, UI slider and in here I want to change a couple of stuff here. So first I want to change this from not being h2 but instead I want it to be h0.5 so it looks just a little bit smaller. You can see how my line is very small and then I'm going to change this from BG secondary to BG white slash 10 and now you can see how my slider is kind of transparent.
What I want to do next is I want to go ahead and modify this thumb here. So I want to change the height and width of 5 to instead be 3.5 and width 3.5 as well. And I want to go ahead and change the from BG Background to be BG White like this and let's see if that's going to improve it and there we go, that looks better great and now let's go ahead and create the actual functions for this. So let's go inside of the StreamPlayer and inside of the index here. And let's go ahead and add the state for...
Not inside of StreamPlayer, inside of LiveVideo, sorry. Let's go ahead and let's duplicate this and change this one to be to volume and let's do set volume and let's go ahead and give it a default value of zero like this and now let's go ahead and let's write const on volume change to accept the value which is a number let's do set volume and let's go ahead and just do plus volume a value like this And then let's go ahead and do if videoRef?current let's do videoRef.current.muted is value is equal to zero and videoRef.current.volume is going to be plus value times 0.01 like that, so we have to modify both the state and the actual video element, right? And now let's go ahead and create const toggle mute like this and check const isMuted to be volume equals 0 and let's do setVolume isMuted 50 otherwise 0 so that's how we're going to toggle if we toggle from muted to unmuted we're gonna bring it back to 50 otherwise we bring it to 0 and now we have to do the same thing so if we have videoRef.current in that case let's do videoRef.current.muted to be the opposite of the current value and videoRef.current.volume is going to check if is muted it's gonna be 0.5 otherwise it's going to be 0.
Like that. Great. And one more thing that I want to do is I want to add the use effect. Like that. And I want to ensure that at the beginning of when this page loads the volume is muted so I'm gonna do use effect like this and I'm just gonna do on volume change and pass in zero manually like that because it's not enough that we just set the volume to 0 here because we have to fire these elements on the video ref so that's why I'm calling the useEffect which will only fire once because there is nothing in the dependency array and trigger that.
Great! And now let's go ahead and pass in all of the props here. So onChange in here is going to be onVolumeChange, value is going to be the volume and onToggle is going to be toggleMute. Like that. So make sure you are live streaming and you should now be able as you can see when I go above 50 I have one icon when I mute it's another icon so you should be able to mute, unmute and do all of those things.
Great, great job. You just finished the video component and now we're gonna go ahead and we're gonna create the chat component.