Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Manage Daily video call state in Angular (Part 2)
Daily profile imageTasha
Tasha forDaily

Posted on • Originally published atdaily.co

Manage Daily video call state in Angular (Part 2)

By Jess Mitchell

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:

  1. Build thejoin-form component to allow users to join a Daily room.
  2. Show how theapp-daily-container component works, including how it responds to thejoin-form being submitted.
  3. See how theapp-call component sets up an instance of the Daily Client SDK’scall object.
  4. 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.
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.

Gif of call participant submitting the join form and joining the video call
Submitting the form injoin-form 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 = "";  }}
Enter fullscreen modeExit fullscreen mode

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>
Enter fullscreen modeExit fullscreen mode

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();
Enter fullscreen modeExit fullscreen mode

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

Join form UI
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>
Enter fullscreen modeExit fullscreen mode

💡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);  }}
Enter fullscreen modeExit fullscreen mode

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>
Enter fullscreen modeExit fullscreen mode

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:

  1. Create an instance of the call object (i.e., theDailyIframe class) using thecreateCallObject() factory method. (ThegetCallInstance() static method can be used to see if a call object already exists, too.)
  2. 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.
  3. Join the call using thedailyRoomUrl 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
Enter fullscreen modeExit fullscreen mode

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 = {};
Enter fullscreen modeExit fullscreen mode

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);};
Enter fullscreen modeExit fullscreen mode

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;}
Enter fullscreen modeExit fullscreen mode

Two steps happen here:

  1. A reformatted copy of the participant object is made.
  2. The reformatted participant object gets added to theparticipants 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,  };}
Enter fullscreen modeExit fullscreen mode

What we’re interested in here is:

  1. The video and audio tracks.
  2. Whether the tracks can be played in the demo’s UI, which is represented byvideoReady andaudioReady.
  3. The user name of the participant, which we’ll display in the UI.
  4. Whether they’re local, since local participants will have device controls in theirvideo-tile.
  5. 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];};
Enter fullscreen modeExit fullscreen mode

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();};
Enter fullscreen modeExit fullscreen mode

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 = "";  }
Enter fullscreen modeExit fullscreen mode

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)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

More fromDaily

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp