In this chapter, our goal is to implement a video calls. The plan for that is to start by obtaining stream API keys, adding the stream video and the node SDK, adding a few methods, procedures, and finally UI elements. Let's start by obtaining the stream API keys. So visit getstream.io and create an account. If this is your first time, you will have a prompt like this where you have to enter a unique organization name.
So I'm going to call this Meet-AI and I'm just gonna go ahead and give it a number to make it unique. Let's click save and there we go. Our organization was just created. And in here, we also have our production app. In case you don't have an app you can click create app.
So go ahead in your existing app or your newly created one and in here go ahead and select video and audio and click overview. And now inside of here make sure you are in video and audio and copy the key. Now let's go ahead make sure you are on your default branch go inside of dot environment and let's add next public stream video API key. And below that prepare the stream video secret key which will be this secret right here so copy that secret and add it here after we've obtained our keys let's install the SDK So I want to show you where you can find the documentation and it's quite simple. You can just click view documentation here, just make sure you have video and audio open.
And in here, go ahead and select the platform API and select JavaScript. Let's go ahead and install a StreamIO node SDK. So npm install and just add legacy peer depths so the installation doesn't fail. Now that it's installed let's go ahead back inside of our project source library and create stream dash video dot DS Let's go ahead and let's import stream client from at stream IO node SDK and export const stream video to the new stream client. And inside of here, we need to pass our new environment keys which are process.environment next public stream video API key and stream video secret key.
As always please double check copy and paste so you know you didn't add any typos. Great. For extra security, you can also import server only in the beginning. You already have this installed, server only, and let me just show you my stream IO node SDK version in case you're wondering. So this will basically prevent this from being imported anywhere on the client side.
Now let's divert a bit and let's go create the generate Avatar method. So we're going to do that inside of source lib and create avatar.tsx. And import createAvatar from Dice Bear Core. And let's add our two variants here. Now let's go ahead and create the props, seed, which is a string, and the variant.
They need to match the imports above. And then in here, create a very simple method, generateAvatarUri, accepting the seed and variant props. And what we are doing is calling the create avatar method depending on the variant. And we are passing some specific options if it's initials. If it's bots neutral, we are just passing the seed.
And we are returning to data URI. So this is a quick way to generate avatar URIs but not the entire component as we have with our generated avatar components. So it's pretty much the exact same function and we could probably reuse it here if we wanted to. Feel free to do that but this one doesn't render any JSX. Now let's go ahead inside of our meetings procedures located in modules meetings server procedures and let's go to the top here and let's create generate token procedure.
It's going to be a protected procedure go ahead and add a mutation here make it asynchronous extract the context from here And what you're going to do is await stream video from our newly added lib stream video and in here add absurd users. And in here, open an object and set the id for this new user to be the currently logged in user. Give it a name for the currently logged in user. A role will be admin and image will either be context out user image or our newly created generate avatar URI from lib avatar. And let's go ahead and pass it some options here.
So we are going to use seed to be context out, user name, and variant will be initials. There we go. So now we have prepared a user for the call. Now we have to create the actual token. So let's create the expiration time using this formula, which is equivalent to one hour.
And let's go ahead and create the issued ad using this formula right here. And then we can finally create the token using stream video, generate user token, as in the user underscore ID to be context out user ID. So this will now create a token for which user can join the call and is going to match the user ID of this user, which we just added to the list of members. So it's going to be able to read their image and their name, even though the token only knows the ID of that user. That's why we needed to upsert the user first.
Now let's add the expiration to be expiration time and the validity in seconds to be issued at. And since the generate user token does not return a promise, we don't need to await it. We can instead immediately return this token. Now we have to stay inside of the meetings procedures and go inside of the create protected procedure and in here we have a to-do to create stream call and upstart some more users. Let's see what that's all about.
So first let's create a new call using stream video dot video dot call. The type of the call will be default and the ID of the call will be created meeting ID. So we are going to associate the stream SDK call with our meeting ID. So that will be our unique identifier for this video call. And now let's await call create.
Let's go ahead and add some data inside. This will be created by underscore ID, context out user ID. And then we're going to add some custom fields here so we can render them in the call UI, such as meeting ID to be created meeting ID and meeting name to be created meeting dot name. Be careful about the spelling here because there is no type safety here. These are our custom fields so just make sure you don't misspell them.
And now let's add some settings override here. We are going to immediately enable the transcription of the call using English language. Mode will be auto on and closed caption mode will be auto on as well. And we are going to do the same thing with recording using mode auto on and quality 1080p. There we go.
So we just created a new call every time we create a new meeting. So we are ready to join that call whenever we want. But we are not done yet. What we have to do now is we have to fetch an existing agent that this newly created meeting uses. So let's go ahead and do that.
We can get the existing agent using await database, select from agents where equals agents ID matches the newly created meeting agent ID. And the first thing we are going to do is throw an error in case we were not able to found that because we cannot upsert this user and now we can finally go ahead and do stream video.upsert users that's right this agent will be treated as a normal user. So inside of here, let's add the ID and the name to be ExistingAgentId and ExistingAgentName. Let's go ahead and give it a role of user because we already set the admin and the image will be our generate avatar URI with the seed to be existing agent dot name and the variant will be bots neutral just like that perfect And now we can safely create our meeting. And that's all we need to do with Stream Video SDK in this procedure right here.
Now, let's go ahead and let's develop the following page, which we get access to in our upcoming state. If you remember, in the previous chapter, we created a lot of meeting variants. One of that was UpcomingState, which allows the user to either cancel the meeting or start the meeting. So it's basically this screen right here. So when we start the meeting, we are redirected to forward slash call meeting ID.
Let's go ahead and work on that page now. The crucial difference here will be that it's not going to be inside of a dashboard. So it's going to be in its own folder. Call. This means it's not going to have the sidebar because we don't want it to be anything other than full screen.
So let's go inside of call And let's go ahead and create a new folder, Meeting ID. And inside, page.tsx. Why Meeting ID? That's because in our procedure right now, when we use the create method, When we create a new call, we use the created Meeting ID as the unique identifier for that call, so we can safely rely on the Meeting ID to help us find the call from StreamVideo SDK. Perfect.
Now let's go ahead and let's create a props here with params, promise and meeting ID. Be mindful of spelling, don't make any typos or casing errors. Let's export const page here, make it an asynchronous page and destructure the params. Assign the props here and let's go ahead and destructure the meeting ID from the params like this. Let's also make sure that this page is protected.
We can do that by using our familiar session from Auth, which we can import from LiveAuth, getSession, and pass in the headers from nextHeaders and the redirect from nextNavigation if there is no session available. So now only authenticated users can visit this page. Now let's go ahead and let's grab our query client from getQueryClient. We can get it from trpcServer and let's void queryClient.prefetchQuery.trpc which you can also import from the server here and let's prefetch meetings get one pass in the query options and use the ID as meeting ID just like that Now let's go ahead and return our hydration boundary here from 10 stack react query and add the state as dehydrate from 10 stack react query and pass the query client inside. Just like that.
So we are doing the prefetching like we've always done it so far. So you can import dehydrate and hydration boundary from tanstack React query. And what I like to do is, for some reason, I like to destructure the params up here, even though we will redirect, it just feels right for it to be here. Now what we have to do is we have to develop the call view component. We are going to develop that instead of a new module.
So let's go inside of our modules and let's create a new folder called call. Add inside of here UI and then finally let's create views and inside call-view-tsx. It's going to have an interface props of the meeting ID. And let's export const call view here. And let's simply grab those props here.
So we are basically extracting the meeting ID here. So we can do trpc. And so that we can extract the data using useSuspenseQuery from trpc meetings get1 with the query options ID, meeting ID. So now we have that data prefetched here because we are prefetching it here so we can now import call view and pass in the meeting id as meeting id Make sure you have imported the call view from modules call UI views call view. The reason we still have an error here is because we don't return any JSX here.
So let's go ahead and let's return a div. Hello. Or maybe we can do json stringify data null 2. Just like that. And make sure to mark the call view as use client.
Now let's go ahead and test our app a bit just to see nothing's broken. So make sure that you go instead of a meeting which is in the state of upcoming. If you are unsure, you can go ahead and create a new meeting. I would actually suggest that you go ahead and create a new meeting so you can test the video SDK. So let's click Create here, and let's see if any errors pop up.
Maybe we did something incorrectly. In here, I cannot see any errors at all, which would mean that we have successfully up-sorted those users and created those calls. And I think that we might actually be able to track this inside of here. Let me go ahead and refresh here inside of my video and audio. And I think that somewhere here, I might be able to go maybe in the usage or explorer.
Let's click on calls here and there we go. I think that this is a new call and I think It might not, yes, it doesn't yet have any members. I think that in order to do that, we actually need to join the call, I think, right? But this is the new call which we just created. And you can see we have the custom information, test video SDK.
So we are successfully doing that. Perfect. Now let's go ahead and let's click on start meeting and in here we should now see a redirect to forward slash call and looks like we are missing something. So instead of this page here I'm doing export const page but that's a mistake because page needs to be export default. So let's just fix that.
And now in here you can see no sidebar and we have properly loaded the meeting ID for which call we want to connect to. Now let's go ahead and let's go inside of our app folder call meeting ID and instead of the entire call folder create a layout.dsx and it's going to be very simple. So interface props with the children and just wrap the children with a height screen and background color of black. So there we go. It's now just a black on black text here.
So I just wanted to change the background for everything relating the call to be black. Now Let's go back inside of our call view here. And first things first, let's check if data.status is completed. In that case, what I want to do here is I want to return a div with a class name FlexHideScreenItems Center and justify center. And I want to render the error state from components error state.
And in here, I just want to alert the user that this meeting has ended And you can no longer join this meeting. So now we can test this out more easily by changing this to Upcoming. So there we go. This is the error that the user will see, but only when it's completed. So make sure you change this to completed so the upcoming user actually sees the JSON stringify for now.
Now let's go ahead inside of call UI and let's create a new folder called components. Inside of here create a call-provider component. Now inside of here, go ahead and mark it as useClient. And go ahead and import loader2-icon from lucid-pref. Import auth-client from libauth-client.
Import generate-avatar-uri from lib-avatar. From Lucidprem, import ALT client from libaltclient, import generate avatar URI from libavatar, and let's quickly create an interface props with meeting ID and the meeting name. And now let's export const call provider here. Let's assign the props and let's destructure the meeting ID and the meeting name. Let me just fix the capitalization issue.
And inside of here, let's go ahead and let's get our current session using our client use session so now we have data and is pending here so in case we don't have data or in case is pending is active we are going to return a div with a loader2 icon inside. Give the div a class name of flex, height screen, items center, justified center, background radial, from sidebar accent to sidebar. I told you we're going to reuse that variables a few more times in the project. And give the icon size 6, animate spin, and text white. And now, down here, go ahead and simply return meeting ID inside of a div, whoops, and meeting name.
Just like this. And now let's go inside of the call view here and let's return call provider from components call provider and passing the meeting ID to be meeting ID and meeting name to be data name. So now when you refresh this, you should now see a loading. And then in here, you will see the name and the ID is just black text here. Now what I want to do is I just want to change the call provider from loader to icon to just the normal loader icon because I don't think we use loader to throughout the project as much.
So let me refresh here. Let's do refresh again. Yeah, that's the loader that I want. Now we're going to go ahead And we're going to build a new component, which we are going to render here, and where we are going to pass this to as well. And it's going to be inside of the same components folder.
So create call-connect.tsx. And create an interface props, accepting the meeting ID, meeting name, and the new user data information, which we await for here. So user ID, user name, and user image. Let's export const call connect here. And let's go ahead and assign the props here and let's destructure all of them.
There we go. Now inside of here, I want to go ahead and return a div called Connect. Like this. So let's go to the call provider here and instead of returning this let's return call connect component which is a self-closing tag make sure you have imported it and we are now going to pass the meeting ID and the meeting name, the user ID and the username which we get from data, which we use here in the use session. And finally, we are going to get the user image, which will look for a data user image if it exists.
Otherwise, it will generate one using our generate avatar using seed data user name and variant initials. Great. So now we can go inside of the call connect, and we can continue developing here. So now we should actually go ahead and go back instead of the stream documentation but this time go inside of react documentation here and we have to go ahead and install stream.io video react SDK. So let me go ahead and copy this.
Let me expand this a little bit. Clear. And let me add dash dash legacy here, depths, like this. Great. Once this is installed, let me immediately show you my package.json, so you can see the version.
The version is just in case you can't get your, your doesn't work, so you think it's the version. Well, this is the version I'm using so you can always install this directly and then try with that version. Now let's focus on the call connect components. So in here, we're going to import the following items from stream IO video react SDK call calling state stream call stream video and stream video client we are also going to add import loader icon from lucid react we are going to import use effect from react and use state from react. And we're also going to add use the RPC from the RPC client and we are also going to need to import the last import needs to be stream io video react SDK dist CSS style dot css so that's how you style the components of stream and we are later going to modify some of the variables to match our theme our project theme and also let's import use mutation from 10 stack react query and mark this as use client.
Now let's go ahead and use all of these props to establish our call. So let's add the TRPC from useTRPC here and let's extract mutate async and let's map it to generate token use mutation and pass in TRPC meetings generate token with mutation options here now open up the use state with the first argument client, second one setClient, and useState and give it a type of streamVideoClient. Now open up the useEffect here, Set the empty dependency array for now. And let's do the following. Create the underscore client variable, simply because client is already taken, using new stream video client.
Inside of API key go ahead and add our process environment next public stream video API key. Please be mindful and go inside of your .environment and just copy it from here make sure it has the next public prefix so it's accessible on the client copy it from here and paste it here and now let's add the user so id is user id name is username and image is user image and we have made it convenient for us so all of these are just props. Great. And let's add the token provider for the user training to join which will be this mutate asynchronous function. So let's just pass it here, and you will see it's completely compatible.
Perfect. And then let's go ahead and do set client to this newly created client, like that. And in the unmount method, we have to call this established underscore client and disconnect the user and set the client back to undefined. And now let's go ahead and let's add all the dependency array items, user ID, user name, user image, and generate token. So now we will have access to that generated client inside of this state and we will be able to pass it around and do whatever we want with it.
Now that we have established our stream video client, we have to establish the actual call. So we're going to do a pretty similar thing, starting with the useState call and setCall with the type of call which we imported from stream.io Video React SDK. And then let's go ahead and let's open a new use effect, which will check if we don't have the client. So if we don't have a client set, we cannot initialize the call at all so simply return at this point and then we're going to create underscore call using client dot call the type of call will be default and meeting id will be the identifier let's go ahead and by default disable the camera for this user and by default disable the microphone for this user and let's set call underscore call and now in the unmount function we have to disconnect from the call but only under certain conditions if underscore call state calling state is not identical to calling state dot left then you can do underscore call leave and underscore call and call and set call to undefined like this and inside of the dependency array add the client and meeting ID like that And now let's go ahead and let's check if at this point we still don't have client or we still don't have a call, we're just going to do the same thing we did in the call provider and return this type of loader here.
So let me just indent my items here a bit. There we go. Perfect. So this will be the loader while all of these is being set up here, the call and the client. And now finally in the return, instead of a div, we can render streamVideo with client being client.
Inside here stream call with call being call and now inside of here let's just see what we have left unused meeting name okay so what we have to add now is the call UI element let's go ahead and go inside of components and let's create call UI TSX instead of call UI let's create the props which are meeting ID and the meeting name. Actually, we don't need meeting ID, just the name. Let's export const call UI. Let's assign the props, the structure, the meeting name. And inside of here, we're gonna go ahead and do the following.
The first thing we can actually do is since the call UI is rendered inside of these two, we can use hooks to get the current state of the call. So let's go ahead and let's import the following. Let's import stream theme and use call from stream.io video react SDK. And let's also import the following use state from react like this now let's go ahead and let's obtain the call from use call and let's create the following use state. Show and set show.
And the types can be lobby, call, or ended, with the default being lobby. And now let's create two methods. One is handle join, which is going to be an asynchronous method. If there is no call, we are going to break this method. And let's just do a way call and join the call.
And after that, we can set show to an active call. And let's do handleLeave to check if there is no call and break the method as well and do call and call and set show to ended. So we now have methods to start the call from the lobby and to leave the call from the actual, well, call. Let's go ahead and return StreamTheme with a class name of IPool. And now, if show is equal to lobby, we should go ahead and render lobby.
If show is equal to call, we should render the call. And if it is ended, we should render ended. So let's go ahead now and let's use call UI inside of the call connect. So let's add it here. And meeting name is meeting name, like this.
So I'm going to go back to my app here, and I'm going to do a refresh. And let's just see what will happen. You can see I am in the lobby. It's very hard to see, you have to highlight it, right, because we set our background to be black, but you can see the text now says lobby because I'm in the lobby by default. So now let's go ahead and let's create the lobby component instead of components, add callLobby.tsx and in here we're going to go ahead and do the following props on join and let's go ahead and prepare the following imports here so that's going to be login icon from lucid react and we're going to have a bunch of imports from stream io video react sdk dream default video placeholder stream video participant, toggle audio preview button, toggle video preview button, use call state hooks, and video preview.
We are basically now building this page right here, the lobby, asking the user if they are ready to join and giving them an option to set up their video camera and microphone. Let's also go ahead and import link from next link. Let's go ahead and import alt client from lib alt client, button from components UI button, and generate avatar URI from our newly created lib. And let's also import the styles here as well. So stream.io, video react SDK, dist, CSS, styles, CSS.
Like that. Now let's go ahead and let's export const call lobby. In here let's destructure on join, props. And now we can go ahead and we can grab camera state and microphone state from use call state hooks. From these two hooks, the structure has browser permission for both of them, but since they are named the same, we will alias this one to hasMicrophonePermission, and this one to hasCameraPermission.
We're going to unify that into a single variable has browser media permission. So if we don't, we can then display to the user that they need to allow the browser to access the camera and the microphone. So now let's go ahead and return a div here with a class name flex, flex column, items center, justify center, height full, background radial from sidebar accent to sidebar inside open up a new div with a class name py of 4, px of 8, flex, flex1, items center and justify center. Inside of that div, let's open one more div with a class name of flex, flex column, items center, justify center, gap y six, bg background, rounded large, padding of 10, and shadow small. Inside of here, we are going to add one more div.
I know this is divception, so many divs, but we will finally write some text inside of this one I promise. So flex flex column, gap, y2 and text center. Let's add an h6 element ready to join. And let's add a paragraph, set up your call before joining. Now let's go ahead and give the heading a class name of text large and font medium and the paragraph a class name of text small.
Now Let's go ahead and go back instead of call UI and let's actually render call lobby here. And the only thing it accepts is the on join method. So let's pass in on join to be handle join. So there we go. This is what you will see now.
Let's refresh. Ready to join. Set up your call before joining. And now instead of call lobby, we actually have to go ahead and render the video preview. So in order to render the video preview, we have to create the following.
A const disabled video preview. And in here, the structure, the data from out client use session and return default video placeholder which is a self-closing tag and inside add the participant prop open a new object inside, set the name to data?userName or an empty string. Image will be data?userImage or generate avatarUri with seed data?userName or an empty string and let's just open an object here like this And we also need to pass the variant here to be initials, like this. And in order to get rid of the error use as stream video participant prop here, like this. So now we have the disabled video preview here and we need just one more thing, one more component besides disabled video preview and it's very simple one.
Allow browser permissions, just a simple paragraph, please grant your browser a permission to access your camera and microphone. And now, outside of this div, add a video preview component and pass in disabled video preview to be the following. If has browser media permission, you will use disabled video preview. Otherwise, allow browser permissions like this. And there we go.
Since by default, my camera is disabled, you can only see my current user's image, which is Google login. So Google image is loaded here. Perfect. So it correctly loads the currently logged in user here. Now, below the video preview, let's add a div with a class name of flex-gapx2.
And inside of here, we are going to render, toggle audio preview button, and toggle video preview button. There we go. And now let's go ahead below here, open up a div with a class name, flex-gap-axe-of-2, justify-between, and pool-width. In here, let's add first button to be cancel and the second button to be join call with login icon. This one will also be a link with an href going back to meetings and give this an as child prop and a variant of ghost the one below will simply have on click to be on join and just like that we've wrapped up our call lobby method.
So we can now cancel and go back to the meetings, or we can go to, make sure you choose the newer one, right? So delete any old ones you had, because there's a chance it might not work because we only in this chapter added the call creation in the create procedure. So use the newest one and click start meeting and there we go. And when you click join call, don't do it just yet because I'm not sure what's going to happen but what will happen is you will just see a paragraph call which will actually be invisible. So now let's actually develop the call active for this.
So inside of components, create call-active.tsx. Let's create the interface with onLeave and the meeting name and let's import the following things. Link from next link, image from next image, call controls and speaker layout from stream.io, video react SDK, export const call active, get on leave and meeting name, assign it to props here. And let's go ahead and return a div with a class name, Lex, Lex column, justify between, padding 4, height full, and text white. And inside open up a new give it a class name of flex items center justify center padding one background white forward slash 10 rounded full and W fit and render an image with the source of our app logo.
Width 22 and height 22 and alt logo. And then, after that, an h4 element with our meeting name and a class name of text base. And outside of that div, speaker layout, which is a self-closing tag, and below that a div with call controls. Go ahead and give it a background the same as above. So class name here and rounded pool and px of 4.
And let's pass our on leave method to call controls on leave. And let's add our own leave here. There we go. So now we can go back to the call UI and in here render a call active just like we rendered a call lobby and let's pass onLeave to be handleLeave. And we also need the meeting name and I think we have that we do meeting name Let's go ahead and try it out now.
So when I click join call, there we go. And I can go ahead and send emojis here. I can open my camera again. Here I am. And you can see that I have the meeting name rendered right here.
And you can see the meeting is actively being recorded. I can also share my screen, you can see my connection here, and I can also end the call. But before we end the call, let's actually implement the end screen so we can wrap up this chapter. So what we can actually do is we can copy call lobby and paste it and rename it to call ended. Go inside of call ended here.
You can remove all props. You can remove this component. You can remove allow browser permissions rename call lobby to call ended remove any props from here you can remove this you will move this basically remove everything besides link and button so just a return method here. And inside of this heading 6, you will use you have ended the call. And in the summary, in here, you will add summary will appear in a few minutes.
And then outside of this div, there will be no video preview, none of this. So let's go ahead and remove this. Let's remove this. And let's remove this as well. So all you're going to do after this is back to meetings button, because there will be no action to do here.
So now we can go back to call UI, and we can render call ended here, just like that. So now that we have all of these states, when I actually end the call, this appears. You have ended the call. So we have officially implemented the video calling. The problem is states are not being changed.
So I can now start this meeting as many times as I want and join it and end it, right? But we have two problems. Agent is not joining and no actual background jobs are being done and there is no communicating with our database. So that's what we're going to do in the next chapter to properly synchronize those states. But we did such an amazing job.
We obtained Stream API keys, Stream Video SDK, we've created these procedures, we added call page and call view, and now let's merge this. Let's go and create a new branch here, 22 video call. Let's go ahead and stage all of our changes just like that. 22 video call. Let's commit.
Let's publish our branch and let's review our pull request to see if there are any potential issues with our code. And here we have this chapter's summary. We introduced video call functionality, including joining, participating in, and ending calls with a streamlined user interface. This includes a pre-joined lobby, in-call controls, and a post-call summary screens for a complete meeting experience. We integrated video call service for secure token generation and user management.
We automatically created and managed video calls when scheduling meetings and we display user avatars and meeting details throughout the entire call experience. As always in here we have a sequence diagram containing every single component that we just created in this pull request. This is absolutely insanely detailed. In case you're interested I'm just going to try and target it, center it like this so you can take a look and review this entire diagram explaining exactly what's going on in our code. As per the code changes, most of these are refactor suggestions, which we have already seen but we decided that we like our current practice but there are a couple of potential issues that I want to discuss.
So this one is because it doesn't know yet that headers are now awaitable, they are a promise, same with the params, so we are okay with this, this is not an issue, This is a refactor suggestion and in here it shows some JVT payload fields are incorrect token may be invalid. As you can see in here it tells me that my tokens might be invalidly created and I'm not entirely sure but what I can tell you is that I went into video and audio platform here, user tokens, and I learned that validity and basically everything besides user ID is completely optional. So yeah, technically in our procedures in the meetings when we generate the token, We don't even have to use these two. We can just pass. You can see there are no errors here.
So if you want to, if it's causing you any problems, if it's expiring too soon, you can delete these two fields and this generation and just use the user ID. So that's the new thing I've learned here, thanks to Rabbit's potential issue. With the rest, I am satisfied. So I'm going to go ahead here and merge this whole request. And that marks the end of this chapter.
Let's go back to the main branch and let's go ahead and synchronize our changes. Let's go inside of the source control, open up the graph, and confirm we just merged the video call and we did. Great, Amazing job and see you in the next chapter.