
In this series, we’re building a video call app with a fully customized UI usingAngular andDaily’s Client SDK for JavaScript. In thefirst post, we reviewed the app’s core features, as well as the general code structure and the role of each component. (Instructions for setting up the demo app locally are also included there.)
In today’s post, we’ll start digging into theAngular demo app’s source code available on GitHub. More specifically, we will:
- Build the
join-form
component to allow users to join a Daily room. - Show how the
app-daily-container
component works, including how it responds to thejoin-form
being submitted. - See how the
app-call
component sets up an instance of the Daily Client SDK’scall object. - Review how joining a Daily call works, and how to handle the events related tojoining andleaving a call.
The next post in this series will cover rendering the actual video tiles for each participant, so stay tuned for that.
Now that we have a plan, let’s get started!
app-daily-container:
The parent component
As a reminder from our first post,app-daily-container
is the parent component for the video call feature:
Component structure in the Angular demo app.
It includes thejoin-form
component, which allows users to submit an HTML form to join a Daily call. It also includes theapp-call
component, which represents the video call UI shown after the form is submitted.
Submitting the form injoin-for
m will causeapp-daily-container
to renderapp-call
instead.
In this sense,app-daily-container
is the bridge between the two views because we need to share the form values obtained viajoin-form
with the in-call components (app-call
and its children).
💡app-chat
is also a child ofapp-daily-container
, but we’ll cover that in a future post.
Looking at the class definition forapp-daily-container
, we see there are two class variables (dailyRoomUrl
anduserName
), as well as methods to set or reset those variables.
import { Component } from "@angular/core";@Component({ selector: "app-daily-container", templateUrl: "./daily-container.component.html", styleUrls: ["./daily-container.component.css"],})export class DailyContainerComponent { // Store callObject in this parent container. // Most callObject logic in CallComponent. userName: string; dailyRoomUrl: string; setUserName(name: string): void { // Event is emitted from JoinForm this.userName = name; } setUrl(url: string): void { // Event is emitted from JoinForm this.dailyRoomUrl = url; } callEnded(): void { // Truthy value will show the CallComponent; otherwise, the JoinFormComponent is shown. this.dailyRoomUrl = ""; this.userName = ""; }}
TheHTML forapp-daily-container
shows how these variables and methods get shared with the child components:
<join-form *ngIf="!dailyRoomUrl" (setUserName)="setUserName($event)" (setUrl)="setUrl($event)"></join-form><app-call *ngIf="dailyRoomUrl" [userName]="userName" [dailyRoomUrl]="dailyRoomUrl" (callEnded)="callEnded()"></app-call>
We can see that thejoin-form
andapp-call
components use the*ngIf
attribute to determine when they should be rendered. They have opposite conditions, meaning they will never both be rendered at the same time. If thedailyRoomUrl
variable is not set yet, it renders thejoin-form
(the default view); otherwise, it renders theapp-call
component. This is because thedailyRoomUrl
is set when the join form has been submitted, so if the value isn’t set, the form hasn’t been used yet.
join-form
has two output properties: thesetUserName()
andsetUrl()
event emitters, which are invoked when the join form is submitted.
app-call
receives theuserName
anddailyRoomUrl
variables as input props, which automatically update when Angular detects a change in their values. It also hascallEnded()
as an output prop.
Understanding input and output props in Angular
In Angular, props can either be aninput property – a value passed down to the child – or anoutput property – a method used to emit a value from the child to the parent component. (Data sharing is bidirectional between the parent and child when both types are used.)
You can tell which type of property it is based on whether the child component declares the prop as an input or output. For example, inapp-call
we see:
@Input() userName: string;@Output() callEnded: EventEmitter<null> = new EventEmitter();
TheuserName
prop is an input property, meaning the value will automatically update inapp-call
whenever the parent component updates it.
ThecallEnded
prop is an output property. Wheneverapp-call
wants to emit an update to the parent component (app-daily-container
), it can callthis.callEnded(value)
andapp-daily-container
will receive the event and event payload.
join-form
: Submitting user data for the call
The demo app’sjoin-form
component.
Thejoin-form
component renders an HTML form element. It includes two inputs for the user to provide the Daily room they want to join, as well as their name:
<form [formGroup]="joinForm" (ngSubmit)="onSubmit()"> <label for="name">Your name</label> <input type="text" formControlName="name" required /> <label for="url">Daily room URL</label> <input type="text" formControlName="url" required /> <input type="submit" value="Join call" [disabled]="!joinForm.valid" /></form>
💡You can create a Daily room and get its URL through theDaily dashboard orREST API.
TheonSubmit()
class method is invoked when the form is submitted (ngSubmited
).
The input values are bound to the component’sjoinForm
values, an instance of Angular’sFormBuilder
class:
// … See the source code for the complete componentexport class JoinFormComponent { @Output() setUserName: EventEmitter<string> = new EventEmitter(); @Output() setUrl: EventEmitter<string> = new EventEmitter(); joinForm = this.formBuilder.group({ name: "", url: "", }); constructor(private formBuilder: FormBuilder) {} onSubmit(): void { const { name, url } = this.joinForm.value; if (!name || !url) return; // Clear form inputs this.joinForm.reset(); // Emit event to update userName var in parent component this.setUserName.emit(name); // Emit event to update URL var in parent component this.setUrl.emit(url); }}
WhenonSubmit()
is invoked, the form input values are retrieved throughthis.joinForm.value
. Thethis.joinForm
values are then reset, which also resets the inputs in the form UI to visually indicate to the user that the form was successfully submitted.
Next, thesetUserName()
andsetURL()
output properties are used to emit form input values toapp-daily-container
. (Remember:app-daily-container
is listening for events already.)
Once these events are received byapp-daily-container
, the form values are assigned to theuserName
anddailyRoomUrl
variables inapp-daily-container
, the latter of which causes theapp-call
component to be rendered instead of thejoin-form
.
It’s a mouthful, but now we can move on to creating the actual video call!
app-call
: The brains of this video call operation
Let’s start withapp-call
’s HTML so we know what’s going to be rendered:
<error-message *ngIf="error" [error]="error" (reset)="leaveCall()"></error-message><p *ngIf="dailyRoomUrl">Daily room URL: {{ dailyRoomUrl }}</p><p *ngIf="!isPublic"> This room is private and you are now in the lobby. Please use a public room to join a call.</p><div *ngIf="!error"> <video-tile *ngFor="let participant of Object.values(participants)" (leaveCallClick)="leaveCall()" (toggleVideoClick)="toggleLocalVideo()" (toggleAudioClick)="toggleLocalAudio()" [joined]="joined" [videoReady]="participant.videoReady" [audioReady]="participant.audioReady" [userName]="participant.userName" [local]="participant.local" [videoTrack]="participant.videoTrack" [audioTrack]="participant.audioTrack"></video-tile></div>
There is anerror message that gets shown only if theerror
variable is true. (This gets set when Daily’s Client SDK’s”error”
event is emitted.)
ThedailyRoomUrl
value is rendered in the UI so the local participant can see which room they joined. We also let the participant know if the room is private since we haven’t added a feature yet to join a private call. (You could, for example, add aknocking feature).
Finally, the most important part: the video tiles. In Angular, you can use*ngFor
attribute like afor of
statement. For each value in the participants object (described in more detail below), avideo-tile
component will be rendered. We will look at how thevideo-tile
component works in the next post so, for now, know that there’s one for each participant.
Next, let’s look at how we build ourparticipants
object inapp-call
’s class definition.
Defining theCallComponent
class
Most of the logic related to interacting withDaily’s Client SDK for JavaScript is included in theapp-call
component. Thedaily-js
library is imported at the top of the file (import DailyIframe from "@daily-co/daily-js";
) which allows us to create the call for the local participant.
To build our call, we will need to:
- Create an instance of the call object (i.e., the
DailyIframe
class) using thecreateCallObject()
factory method. (ThegetCallInstance()
static method can be used to see if a call object already exists, too.) - Attachevent listeners to the call object.
daily-js
will emit events for any changes in the call so we’ll listen for the ones relevant to our demo use case. - Join the call using the
dailyRoomUrl
and theuserName
, which were provided in thejoin-form
. This is done via thejoin()
instance method.
Since the call component is rendered as soon as the join form is submitted, we need to set up the call logic as soon as the component is initialized. In Angular, we use thengOnInit()
lifecycle method to capture this:
// … See source code for more detailexport class CallComponent { Object = Object; // Allows us to use Object.values() in the HTML file @Input() dailyRoomUrl: string; @Input() userName: string; // .. See source code ngOnInit(): void { // Retrieve or create the call object this.callObject = DailyIframe.getCallInstance(); if (!this.callObject) { this.callObject = DailyIframe.createCallObject(); } // Add event listeners for Daily events this.callObject .on("joined-meeting", this.handleJoinedMeeting) .on("participant-joined", this.participantJoined) .on("track-started", this.handleTrackStartedStopped) .on("track-stopped", this.handleTrackStartedStopped) .on("participant-left", this.handleParticipantLeft) .on("left-meeting", this.handleLeftMeeting) .on("error", this.handleError); // Join Daily call this.callObject.join({ userName: this.userName, url: this.dailyRoomUrl, }); } // … See source code
All three of the steps mentioned above happen as soon as the component is initialized. The second step – attaching Daily event listeners – is where most of the heavy lifting happens for managing the call. For each event shown in the code block above, an event handler is attached that will be invoked when the associated event is received.
As an overview, each event used above represents the following:”joined-meeting”
: The local participant joined the call.”participant-joined”
: A remote participant joined the call.”track-started”
: A participant's audio or video track began.”track-stopped”
: A participant's audio or video track ended.”participant-left”
: A remote participant left the call.”left-meeting”
: The local participant left the call.”error”
: Something went wrong.
💡The next post in this series will cover the track-related events.
Once the call object is initialized andjoin()
-ed, we need to manage the call state related to which participants are in the call. Once we know who is in the call, we can rendervideo-tile
components for them.
Adding a new participant
Theapp-call
component uses theparticipants
variable (an empty object to start) to track all participants currently in the call.
export type Participant = { videoTrack?: MediaStreamTrack | undefined; audioTrack?: MediaStreamTrack | undefined; videoReady: boolean; audioReady: boolean; userName: string; local: boolean; id: string;};type Participants = {};export class CallComponent { // … See source code participants: Participants = {};
When the local participant or a new remote participant joins, we’ll respond the same way: by updatingparticipants
.
In boththis.handleJoinedMeeting()
andthis.participantJoined()
– the callbacks for”joined-meeting”
and”participant-joined”
– thethis.addParticipant()
method gets called to updateparticipants
. Let’s see how this works usingthis.participantJoined()
as an example:
participantJoined = (e: DailyEventObjectParticipant | undefined) => { if (!e) return; // Add remote participants to participants list used to display video tiles this.addParticipant(e.participant);};
With any Daily event, the event payload is available in the callback method (e
in this case). Theparticipant
information is available in the event payload, which then gets passed tothis.addParticipant()
.
addParticipant(participant: DailyParticipant) { const p = this.formatParticipantObj(participant); this.participants[participant.session_id] = p;}
Two steps happen here:
- A reformatted copy of the participant object is made.
- The reformatted participant object gets added to the
participants
object.
The participant object doesn’t need to get reformatted for this demo to work, but we do this to make the object easier to work with. The participant object that gets returned in the event payload contains a lot of nested information – much of which doesn’t affect our current feature set. To extract the information we do care about, we usethis.formatParticipantObj()
to format a copy of the participant object, like so:
formatParticipantObj(p: DailyParticipant): Participant { const { video, audio } = p.tracks; const vt = video?.persistentTrack; const at = audio?.persistentTrack; return { videoTrack: vt, audioTrack: at, videoReady: !!(vt && (video.state === PLAYABLE_STATE || video.state === LOADING_STATE)), audioReady: !!(at && (audio.state === PLAYABLE_STATE || audio.state === LOADING_STATE)), userName: p.user_name, local: p.local, id: p.session_id, };}
What we’re interested in here is:
- The video and audio tracks.
- Whether the tracks can be played in the demo’s UI, which is represented by
videoReady
andaudioReady
. - The user name of the participant, which we’ll display in the UI.
- Whether they’re local, since local participants will have device controls in their
video-tile
. - The participant ID, so we can have a unique way of identifying them.(Refer to our docs for an example ofall the other participant information returned in the event.)
Once the participant has been added to theparticipants
object, avideo-tile
can be rendered for them, but more on that in our next post!
Removing a remote participant from the call
A call participant can leave the call in different ways. For example, they can use the “Leave” button in thevideo-tile
component or they can simply exit their browser tab for the app. In any case, we need to know when a participant has left to properly update the app UI.
When any remote participant leaves the call, we need to updateparticipants
to remove that participant.
handleParticipantLeft = ( e: DailyEventObjectParticipantLeft | undefined): void => { if (!e) return; console.log(e.action); delete this.participants[e.participant.session_id];};
InhandleParticipantLeft()
(the handler for the”participant-left”
event), we simply delete the entry for that participant in the participants object. This will in turn remove their video tile from the call UI.
Removing a local participant from the call
When the local participant leaves, we need to reset the UI by unmounting theapp-call
component and going back to rendering thejoin-form
view instead, since the participant is no longer in the call. Instead of updatingparticipants
, we can justdestroy()
the call object and emitcallEnded()
, the output property passed fromapp-daily-container
.
handleLeftMeeting = (e: DailyEventObjectNoPayload | undefined): void => { if (!e) return; console.log(e); this.joined = false; // this wasn’t mentioned above but is used in the UI this.callObject.destroy(); this.callEnded.emit();};
WhencallEnded()
is emitted,app-daily-container
will reset thedailyRoomUrl
anduserName
class variables.
// In DailyContainerComponent:callEnded(): void { // Truthy value will show the CallComponent; otherwise, the JoinFormComponent is shown. this.dailyRoomUrl = ""; this.userName = ""; }
As a reminder, whendailyRoomUrl
is falsy, thejoin-form
component gets rendered instead of theapp-call
.
And with that, we have a Daily call that can be joined and left!
Conclusion
In today’s post, we covered how participants can submit an HTML form in an Angular app to join a Daily video call, as well as how to manage participant state when participants join or leave the call.
In the next post, we’ll discuss how to render video tiles for each participant while they’re in the call, including our recommendations for optimizing video performance in an app like this.
If you have any questions or thoughts about implementing your Daily-powered video app with Angular,reach out to our support team or head over to ourWebRTC community, peerConnection.
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse