Verbal communication is an essential part of human life. Today, technology brings the possibility of much richer communication channel then standard text-chat to our browsers. With CometChat you can build your audio and Video chat application on your website. Today you will integrate CometChat with Angular application.

Angular video and voice chat with CometChat Pro

There is a companion repository created for this post, available on the GitHub.

Starting out

First things first. In the catalog where you want your project to be located, type in the following Angular CLI command, which creates Angular project boilerplate:

ng new comet-chat --style scss --routing true

And install @cometchat-pro/chat library from npm:

npm install @cometchat-pro/chat

Now navigate into just created file, and replace the content of the app.component.html file with following HTML syntax:

<router-outlet></router-outlet>

Now it’s time to store your application ID and API Key in the **environment.ts file:

export const environment = {
 production: false,
 appId: 'Your app ID',
 apiKey: 'Your API Key'
};

The application is going to have two main components: LoginComponent and CallComponent.
CallComponent is the place where a logged in user will be brought to display a contact list and perform video and voice calls to other active users. If the user is not authenticated he needs to provide login and password in the LoginComponent. Information about user authentication will be provided by the CometChatService. Generate them using the following commands:

ng g c Login
ng g c Call
ng g s CometChat

Let’s start from the implementation of the CometChatService basic functionalities. Replace the content of the src/app/comet-chat.service.ts file with the following code:

import { Injectable } from '@angular/core';
import { CometChat } from "@cometchat-pro/chat"
import { environment } from 'src/environments/environment';
import { Observable, ReplaySubject, Subject, from } from 'rxjs';
import { filter, flatMap, tap } from 'rxjs/operators';

@Injectable({
 providedIn: 'root'
})

export class CometChatService {
 private initialized: Subject<boolean> = new ReplaySubject<boolean>();
 private signedIn: string;

 constructor() {
   CometChat.init(environment.appId).then(_ => {
     console.log('Comet Chat initialized.');
     this.initialized.next(true);
   }, error => {
     console.log('Initialization error: ' + error);
   });
  }

 public login(uid: string): Observable<any> {
   uid = uid.toLowerCase();
   return this.initialized.pipe(filter(v => v), flatMap(() => {
     return from(CometChat.login(uid, environment.apiKey)).pipe(tap(() => {
       this.signedIn = uid;
     }));
   }));
  } 

 public getSignedIn(): string {
   return this.signedIn;
 }
}

With this basic functionality of the CometChatService (authenticating user and checking if he is authenticated) we can build up an AuthenticationGuard to protect the CallComponent against unauthorized access. Generate the AuthenticationGuard:

ng g g Auth --implements CanActivate

Now, you can implement the Authentication Guard which will check if user is authenticated every time he is trying to navigate into application. Replace the content of the src/app/auth.guard.ts file with following code,:

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { CometChatService } from './comet-chat.service';

@Injectable({
 providedIn: 'root'
})

export class AuthGuard implements CanActivate {
 constructor(private chat: CometChatService, private router: Router) {}

 canActivate(): boolean {
   if (!this.chat.getSignedIn()) {
     this.router.navigate(['login']);
   }
   
   return !!this.chat.getSignedIn();
  }
}

Now it’s more than ready to use it in the app navigation. Place following content in the src/app/app-routing.module.ts:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { AuthGuard } from './auth.guard';
import { CallComponent } from './call/call.component';

const routes: Routes = [
 { path: '', redirectTo: 'call', pathMatch: 'full' },
 { path: 'login', component: LoginComponent },
 { path: 'call', component: CallComponent, canActivate: [AuthGuard] }
];

@NgModule({
 imports: [RouterModule.forRoot(routes)],
 exports: [RouterModule]
})
export class AppRoutingModule { }

It’s time to make the first test of the application. Run it with the ng serve command and navigate to the localhost:4200, the application should redirect you to the /login path:

Redirection to the /login path

Ok, let’s give the user ability to login. Paste following code representing login form, to the src/login/login.component.ts file:

import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { CometChatService } from '../comet-chat.service';
import { Router } from '@angular/router';
@Component({
 selector: 'app-login',
 templateUrl: './login.component.html',
 styleUrls: ['./login.component.scss']
})

export class LoginComponent {
 public loginForm = new FormGroup({
   uid: new FormControl('', Validators.required)
 });

 constructor(private chat: CometChatService, private router: Router) {}
  public signIn(): void {
   this.chat.login(this.loginForm.value['uid']).subscribe(signedUser => {
     this.router.navigate(['call']);
   });
  }
}

And following HTML template in the src/app/login/login.component.html

<form [formGroup]="loginForm" (ngSubmit)="signIn()">
    <label>User ID:<input type="text" formControlName="uid" /></label>
    <button type="submit" [disabled]="!loginForm.valid">Sign In</button>
</form>

The last thing to do in this step is to add a new import to the src/app/app.module.ts, in the imports array add ReactiveFormsModule, imported from the @angular/forms library:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { CallComponent } from './call/call.component';
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [
    AppComponent,
    LoginComponent,
    CallComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Now, after navigating to the localhost:4200, the login page should be displayed:

Login form

After typing userID (ie.: superhero1) in the form, and clicking Sign In button, you should be brought to the CallComponent:

Logged in user

If you haven't been coding along and want to catch up to this step using the companion repository on GitHub, execute the following commands:

git clone https://github.com/maciejtreder/comet-chat-angular.git
cd comet-chat-angular
git checkout step1
npm install

Retrieving the list of active users

Let’s start the implementation of the main panel of the application with displaying a list of user, together with their statuses. First thing first, add a new field to the src/app/comet-chat.service.ts:

private users$: Subject<any> = new ReplaySubject();

Implement the method which will retrieve list of users from the CometChat service. For the purpose of that method you will use the UserRequestBuilder from the CometChat library:

private retrieveUsers(): void {
  new CometChat.UsersRequestBuilder().setLimit(20).build().fetchNext().then(response => {
    this.users$.next(response);
  });
}

Now you can bind this function to the userListener. Whenever some user will log in or out, this function would be fired.

It’s time to add some CometChat listeners. We will do that at the moment when user is logging into the application. Replace the login method with following new implementation:

public login(uid: string): Observable<any> {
  uid = uid.toLowerCase();
  return this.initialized.pipe(filter(v => v), flatMap(() => {
    return from(CometChat.login(uid, environment.apiKey)).pipe(tap(() => {
      this.signedIn = uid;
      CometChat.addUserListener(
        'USER_LISTENER_ID',
        //@ts-ignore
        new CometChat.UserListener({
          onUserOnline: _ => this.retrieveUsers(),
          onUserOffline: _ => this.retrieveUsers()
      }));
    }));
  }));
}

And publish a list of the users with the getUsers() method

public getUsers(): Observable<any> {
  return this.users$;
} 

Now you can consume the new implementation of the CometChatService in the CallComponent. Replace the code inside src/app/call/call.component.ts with the following code:

import { Component } from '@angular/core';
import { CometChatService } from '../comet-chat.service';
import { Observable } from 'rxjs';

@Component({
 selector: 'app-call',
 templateUrl: './call.component.html',
 styleUrls: ['./call.component.scss']
})
export class CallComponent {
 public contacts: Observable<any[]> = this.chat.getUsers();
 constructor(private chat: CometChatService) { }
}

Adjust the template, in the src/app/call/call.component.html:

<div>
    <ul class="contacts">
        <li *ngFor="let contact of (contacts | async)">
            {{contact.name}} ({{contact.status}}) <span *ngIf="contact.status=='online'" (click)="voiceCall(contact.uid)">voice call</span> <span *ngIf="contact.status=='online'"  (click)="videoCall(contact.uid)">vidoe call</span>
        </li>
    </ul>
</div>

And add some scss styles in the src/app/call/call.component.scss:

:host {
    display: flex;
}
:host > div {
    flex: 1;
}
#callScreen {
    height: 500px;
}
ul {
    list-style-type: none;
    span {
        margin: 0 15px;
        text-decoration: underline;
    }
    span:hover {
        text-decoration: none;
        cursor: pointer;
    }
}

After navigating to localhost:4200 and authentication with the userID superhero1 you should be able to see list of your buddies, together with their statuses:

List of users

Starting Audio & Video Calls

Next step to accomplish your application is the implementation of the Audio and Video calls functionality. Your application can have 4 states:

  • The call is incoming
  • The call is ongoing
  • The call is outgoing
  • No call is performed

The last one is already implemented, there is nothing new to display on the screen. Let’s go on and implement the outgoing call functionality.

Replace the content of the src/app/comet-chat.service.ts file with following code:

import { Injectable } from '@angular/core';
import { CometChat } from "@cometchat-pro/chat"
import { environment } from 'src/environments/environment';
import { Observable, ReplaySubject, Subject, from } from 'rxjs';
import { filter, flatMap, tap } from 'rxjs/operators';

@Injectable({
 providedIn: 'root'
})
export class CometChatService {
 
  private initialized: Subject<boolean> = new ReplaySubject<boolean>();
  private incomingCall$: Subject<any> = new ReplaySubject();
  private outgoingCall$: Subject<any> = new ReplaySubject();
  private ongoingCall$: Subject<any> = new ReplaySubject();
  private users$: Subject<any> = new ReplaySubject();
  private signedIn: string;

You just introduced three new data streams incomingCall$, outgoingCall$ and ongoingCall$. Continue editing of this class, by adding constructor together subscription to the ongoingCall$ observable.

  constructor() {
    CometChat.init(environment.appId).then(_ => {
      console.log('Comet Chat initialized.');
      this.initialized.next(true);
    }, error => {
      console.log('Initialization error: ' + error);
    });
    
    this.ongoingCall$.pipe(filter(call => !!call)).subscribe(call => {
      CometChat.startCall(
        call.sessionId,
        document.getElementById('callScreen'),
        //@ts-ignore
        new CometChat.OngoingCallListener({
          onCallEnded: call => {
            this.ongoingCall$.next(null);
          }
        })
      );
    });
  }

Pay attention to the pipe used on the ongoingCall$ observable. Whenever a new value is pushed to this stream, you are performing the startCall method from the CometChat library, apart of call.sessionId you are passing the reference to the HTML element with id callScreen - you will introduce it later in one of your application components.

Later on, implement function which retrieves list of users, and login function.

  private retrieveUsers(): void {
    new CometChat.UsersRequestBuilder().setLimit(20).build().fetchNext().then(response => {
      this.users$.next(response);
    });
  }
 
  public login(uid: string): Observable<any> {
    uid = uid.toLowerCase();
    return this.initialized.pipe(filter(v => v), flatMap(_ => {
      return from(CometChat.login(uid, environment.apiKey)).pipe(tap(_ => {
        this.retrieveUsers();
        this.signedIn = uid;
        CometChat.addCallListener(
          'CALL_LISTENER_ID',
          //@ts-ignore
          new CometChat.CallListener({
            onIncomingCallReceived: call => {
              this.incomingCall$.next(call);
            },
            onOutgoingCallAccepted: call => {
              this.ongoingCall$.next(call);
              this.outgoingCall$.next(null);
            },
            onOutgoingCallRejected: _ => {
              this.outgoingCall$.next(null);
              this.incomingCall$.next(null);
            },
            onIncomingCallCancelled: call => {
              this.incomingCall$.next(null);
            }
          })
        );
  
        CometChat.addUserListener(
          'USER_LISTENER_ID',
          //@ts-ignore
          new CometChat.UserListener({
            onUserOnline: _ => this.retrieveUsers(),
            onUserOffline: _ => this.retrieveUsers()
          })
        );
      }));
    }));
  }

Now it’s time for functions responsible for starting video/voice calls and rejecting or accepting incoming calls.

  public startVoiceCall(receiverID: string): Observable<any> {
    if (!this.signedIn) {
      throw new Error('Not logged in.');
    }
    const call = new CometChat.Call(receiverID, CometChat.CALL_TYPE.AUDIO, CometChat.RECEIVER_TYPE.USER);
    return from(CometChat.initiateCall(call)).pipe(tap(call => this.outgoingCall$.next(call)));
  }
 
  public startVideoCall(receiverID: string): Observable<any> {
    if (!this.signedIn) {
     throw new Error('Not logged in.');
    }
    const call = new CometChat.Call(receiverID, CometChat.CALL_TYPE.VIDEO, CometChat.RECEIVER_TYPE.USER);
    return from(CometChat.initiateCall(call));
  }
  public accept(sessionId: string): Observable<any> {
    return from(CometChat.acceptCall(sessionId)).pipe(tap(call => {
      this.incomingCall$.next(null);
      this.ongoingCall$.next(call);
    }));
  }
  public reject(sessionId: string): Observable<any> {
    return from(CometChat.rejectCall(sessionId, CometChat.CALL_STATUS.REJECTED)).pipe(tap(_ => {
      this.incomingCall$.next(null);
    }));
  }

Finally bunch of getters, used by service consumers.

  public getSignedIn(): string {
    return this.signedIn;
  }
 
  public getIncomingCalls(): Observable<any> {
    return this.incomingCall$;
  }
 
  public getOutgoingCalls(): Observable<any> {
    return this.outgoingCall$;
  }
 
  public getOngoingCalls(): Observable<any> {
    return this.ongoingCall$;
  }

  public getUsers(): Observable<any> {
    return this.users$;
  }
}

Now you are ready to generate new component, it will be dealing with outgoing calls:

ng g c outgoingCall

Replace content in the src/app/outgoing-call/outgoing-call.componenet.ts file with following TypeScript code:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-outgoing-call',
  templateUrl: './outgoing-call.component.html',
  styleUrls: ['./outgoing-call.component.scss']
})
export class OutgoingCallComponent {
  @Input()
  public call: any;
}

And adjust the template in the src/app/outgoing-call/outgoing-call.component.html with:

<h1>Calling to {{call.data.entities.for.entity.name}}</h1>
<div><img [src]="call.data.entities.for.entity.avatar" /></div>

Add some styling to the src/app/outgoing-call/outgoing-call.componenet.scss:

img {
    max-width: 50px;
    max-height: 50px;
}
:host {
    display: flex;
}

For now, the last change would be in the src/app/call/call.component.ts, implement two new methods, which are already in the view:

public voiceCall(userId: string):void {
  this.chat.startVoiceCall(userId).subscribe();
}
public videoCall(userId: string):void {
  this.chat.startVideoCall(userId).subscribe();
}

Expose one of the new streams from the CometChatService:

public outgoingCall$: Observable<any> = this.chat.getOutgoingCalls();

And adjust the src/app/call/call.component.html file:

<div>
    <ul class="contacts">
        <li *ngFor="let contact of (contacts | async)">
            {{contact.name}} ({{contact.status}}) <span *ngIf="contact.status=='online'" (click)="voiceCall(contact.uid)">voice call</span> <span *ngIf="contact.status=='online'"  (click)="videoCall(contact.uid)">vidoe call</span>
        </li>
    </ul>
</div>
<div>
    <app-outgoing-call *ngIf = "outgoingCall$ | async as outgoingCall" [call]="outgoingCall"></app-outgoing-call>
</div>

Now, after navigating to localhost:4200 in two separate windows, authorizing into two different accounts, you should be able to perform a voice call from one account to another and see the following screen:

Outgoing call

If you haven't been coding along and want to catch up to this step using the companion repository on GitHub, execute the following commands in the directory where you’d like to create the project:

git clone https://github.com/maciejtreder/comet-chat-angular.git
cd comet-chat-angular
git checkout step2
npm install

Receiving calls

You are close to the end. You can perform the call, but it’s not displayed on the second user account that someone is trying to reach him. Now you are going to implement the incoming call view and ongoing call view.

All necessary methods are already in the CometChat service, so the only thing on our end is to generate an incoming call component:

ng g c incomingCall

Replace the content of the src/app/incoming-call/incoming-call.component.ts with the following TypeScript code:

import { Component, Input } from '@angular/core';
import { CometChatService } from '../comet-chat.service';

@Component({
  selector: 'app-incoming-call',
  templateUrl: './incoming-call.component.html',
  styleUrls: ['./incoming-call.component.scss']
})
export class IncomingCallComponent {
  @Input()
  private call: any;

  constructor(private chat: CometChatService) { }

  public accept(): void {
    this.chat.accept(this.call.sessionId).subscribe();
  }

  public reject(): void {
    this.chat.reject(this.call.sessionId).subscribe();
  }
}

Adjust the HTML template in the src/app/incoming-call/incoming-call.component.html

<h1>Call from {{call.callInitiator.name}}</h1>
<div><img [src]="call.callInitiator.avatar" /></div>
<div>
    <span class="positive" (click)="accept()">Accept</span><span class="negative" (click)="reject()">Reject</span>
</div>

And add some styling to the src/app/incoming-call/incoming-call.component.scss file:

.positive {
    color: green;
}
.negative {
    color: red;
}
img {
    max-width: 200px;
    max-height: 200px;
}
:host {
    display: flex;
    flex-direction: column;
}
:host > * {
    display: flex;
    justify-content: center;
}
span {
    margin: 0 15px;
    text-decoration: underline;
}
span:hover {
    text-decoration: none;
    cursor: pointer;
}

Last step for now, is adjusting the CallComponent, place the new component handle in the src/app/call/call.component.html file next to the :

<div>
    <ul class="contacts">
        <li *ngFor="let contact of (contacts | async)">
            {{contact.name}} ({{contact.status}}) <span *ngIf="contact.status=='online'" (click)="voiceCall(contact.uid)">voice call</span> <span *ngIf="contact.status=='online'"  (click)="videoCall(contact.uid)">video call</span>
        </li>
    </ul>
</div>
<div>
    <app-outgoing-call *ngIf = "outgoingCall$ | async as outgoingCall" [call]="outgoingCall"></app-outgoing-call>
    <app-incoming-call *ngIf = "incomingCall$ | async as incomingCall" [call]="incomingCall"></app-incoming-call>
</div>

And add new data stream in src/app/call/call.component.ts:

public incomingCall$: Observable<any> = this.chat.getIncomingCalls();

Try the application right now. After performing a call to one of the superheroes, they should be able to see incoming call component together with the accept and reject buttons:

Incoming call screen

The last step, is to handle the ongoing calls. You have one more component to generate:

ng g c ongoingCall

Implement its logic by pasting the following code into src/app/ongoing-call/ongoing-call.component.ts file:

import { Component, OnInit, Input } from '@angular/core';
import { CometChatService } from '../comet-chat.service';

@Component({
  selector: 'app-ongoing-call',
  templateUrl: './ongoing-call.component.html',
  styleUrls: ['../outgoing-call/outgoing-call.component.scss']
})
export class OngoingCallComponent implements OnInit {
  @Input()
  private call: any;
  public name: string;
  public avatar: string;

  constructor(private chat: CometChatService) {}

  public ngOnInit() {
    if (this.call.callInitiator.uid === this.chat.getSignedIn()) {
      this.name = this.call.callReceiver.name;
      this.avatar = this.call.callReceiver.avatar;
    } else {
      this.name = this.call.callInitiator.name;
      this.avatar = this.call.callInitiator.avatar;
    }
  }
}

Notice that you are using the same stylesheet as for outgoing-call component.

Prepare a HTML template in src/app/ongoing-call/ongoing-call.component.html:

<h1>Speaking with {{name}}</h1>
<div><img [src]="avatar" /></div>

You don’t need to implement the finishing call logic, it’s handled by CometChat library (sweet!).
The last step is to add this component into the CallComponent and place a placeholder for the CometChat call screen in the src/app/call/call.component.html file. Another addition is the placeholder for the CometChat library (you described it in the CometChatService); an HTML entity with id callScreen:

<div>
    <ul class="contacts">
        <li *ngFor="let contact of (contacts | async)">
            {{contact.name}} ({{contact.status}}) <span *ngIf="contact.status=='online'" (click)="voiceCall(contact.uid)">voice call</span> <span *ngIf="contact.status=='online'"  (click)="videoCall(contact.uid)">vidoe call</span>
        </li>
    </ul>
</div>
<div>
    <app-outgoing-call *ngIf = "outgoingCall$ | async as outgoingCall" [call]="outgoingCall"></app-outgoing-call>
    <app-incoming-call *ngIf = "incomingCall$ | async as incomingCall" [call]="incomingCall"></app-incoming-call>
    <app-ongoing-call *ngIf = "ongoingCall$ | async as ongoingCall" [call]="ongoingCall"></app-ongoing-call>
    <div id="callScreen"></div>
</div>

Notice that the id of the div must match the id specified in the src/app/comet-chat.service.ts, inside the constructor:

constructor() {
  CometChat.init(environment.appId).then(_ => {
    console.log('Comet Chat initialized.');
    this.initialized.next(true);
  }, error => {
    console.log('Initialization error: ' + error);
  });
  
  this.ongoingCall$.pipe(filter(call => !!call)).subscribe(call => {
    CometChat.startCall(
      call.sessionId,
      document.getElementById('callScreen'),
      //@ts-ignore
      new CometChat.OngoingCallListener({
        onCallEnded: call => {
          this.ongoingCall$.next(null);
        }
      })
    );
  });
}

You must also add the ongoingCall stream into the src/app/call/call.component.ts:

public ongoingCalls$: Observable<any> = this.chat.getOngoingCalls();

Now, whenever the call is accepted, the CometChat snippet is loaded into your application:

Ongoing call

If you haven't been coding along and want to catch to this step using the companion repository on GitHub, execute the following commands in the directory where you’d like to create the project directory:

git clone https://github.com/maciejtreder/comet-chat-angular.git
cd comet-chat-angular
git checkout step3
npm install

Conclusion

Today you brought one of the most important part of human communication - voice and view - to the web! You learned how to embed CometChat in an Angular application.