DEV Community

abdullah khrais
abdullah khrais

Posted on

ntegrating SignalR with Angular for Seamless Video Calls: A Step-by-Step Guide

Hi everyone! Today, we'll explore how to create a simple video call web app using WebRTC, Angular, and ASP.NET Core. This guide will walk you through the basics of setting up a functional application with these technologies. WebRTC enables peer-to-peer video, voice, and data communication, while SignalR will handle the signaling process needed for users to connect. We'll start with the backend by creating a .NET Core web API project and adding the SignalR NuGet package. Check out the repository links at the end for the complete code.

Backend Setup

  • *Step1: Create .NET Core API Project * First, create a .NET Core web API project and install the SignalR package:

dotnet add package Microsoft.AspNetCore.SignalR.Core

  • Step 2: Create the VideoCallHub Class Next, create a class VideoCallHub:
using Microsoft.AspNetCore.SignalR;
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

namespace Exam_Guardian.API
{
    public class VideoCallHub : Hub
    {
        private static readonly ConcurrentDictionary<string, string> userRooms = new ConcurrentDictionary<string, string>();

        public override async Task OnConnectedAsync()
        {
            await base.OnConnectedAsync();
            await Clients.Caller.SendAsync("Connected", Context.ConnectionId);
        }

        public override async Task OnDisconnectedAsync(Exception exception)
        {
            if (userRooms.TryRemove(Context.ConnectionId, out var roomName))
            {
                await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName);
            }
            await base.OnDisconnectedAsync(exception);
        }

        public async Task JoinRoom(string roomName)
        {
            await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
            userRooms.TryAdd(Context.ConnectionId, roomName);
            await Clients.Group(roomName).SendAsync("RoomJoined", Context.ConnectionId);
        }

        public async Task SendSDP(string roomName, string sdpMid, string sdp)
        {
            if (userRooms.ContainsKey(Context.ConnectionId))
            {
                await Clients.OthersInGroup(roomName).SendAsync("ReceiveSDP", Context.ConnectionId, sdpMid, sdp);
            }
            else
            {
                await Clients.Caller.SendAsync("Error", "You are not in a room");
            }
        }

        public async Task SendICE(string roomName, string candidate, string sdpMid, int sdpMLineIndex)
        {
            if (userRooms.ContainsKey(Context.ConnectionId))
            {
                await Clients.OthersInGroup(roomName).SendAsync("ReceiveICE", Context.ConnectionId, candidate, sdpMid, sdpMLineIndex);
            }
            else
            {
                await Clients.Caller.SendAsync("Error", "You are not in a room");
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

**- Step 3: Register the Hub in Program.cs
Register the SignalR hub and configure CORS in Program.cs:

builder.Services.AddSignalR();
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAngularDev", builder =>
    {
        builder.WithOrigins("http://localhost:4200", "http://[your_ip_address]:4200")
               .AllowAnyHeader()
               .AllowAnyMethod()
               .AllowCredentials();
    });
});

app.UseCors("AllowAngularDev");

app.UseEndpoints(endpoints =>
{
    endpoints.MapHub<VideoCallHub>("/videoCallHub");
    endpoints.MapControllers();
});

Enter fullscreen mode Exit fullscreen mode

**

With this, the backend setup for SignalR is complete.

Frontend Setup

- Step 1: Create Angular Project
Create an Angular project and install the required packages:

npm install @microsoft/signalr cors express rxjs simple-peer tslib webrtc-adapter zone.js
**- Step 2: Create Service Called SignalRService,
inside this service set this code,

inside this service set this code

import { Injectable } from '@angular/core';
import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr';
import { Subject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class SignalRService {
  private hubConnection: HubConnection;
  private sdpReceivedSource = new Subject<any>();
  private iceReceivedSource = new Subject<any>();
  private connectionPromise: Promise<void>;

  sdpReceived$ = this.sdpReceivedSource.asObservable();
  iceReceived$ = this.iceReceivedSource.asObservable();

  constructor() {
    this.hubConnection = new HubConnectionBuilder()
      .withUrl('http://[your_local_host]/videoCallHub')
      .build();

    this.connectionPromise = this.hubConnection.start()
      .then(() => console.log('SignalR connection started.'))
      .catch(err => console.error('Error starting SignalR connection:', err));

    this.hubConnection.on('ReceiveSDP', (connectionId: string, sdpMid: string, sdp: string) => {
      this.sdpReceivedSource.next({ connectionId, sdpMid, sdp });
    });

    this.hubConnection.on('ReceiveICE', (connectionId: string, candidate: string, sdpMid: string, sdpMLineIndex: number) => {
      this.iceReceivedSource.next({ connectionId, candidate, sdpMid, sdpMLineIndex });
    });
  }

  private async ensureConnection(): Promise<void> {
    if (this.hubConnection.state !== 'Connected') {
      await this.connectionPromise;
    }
  }

  async joinRoom(roomName: string): Promise<void> {
    await this.ensureConnection();
    return this.hubConnection.invoke('JoinRoom', roomName)
      .then(() => console.log(`Joined room ${roomName}`))
      .catch(err => console.error('Error joining room:', err));
  }

  async sendSDP(roomName: string, sdpMid: string, sdp: string): Promise<void> {
    await this.ensureConnection();
    return this.hubConnection.invoke('SendSDP', roomName, sdpMid, sdp)
      .catch(err => {
        console.error('Error sending SDP:', err);
        throw err;
      });
  }

  async sendICE(roomName: string, candidate: string, sdpMid: string, sdpMLineIndex: number): Promise<void> {
    await this.ensureConnection();
    return this.hubConnection.invoke('SendICE', roomName, candidate, sdpMid, sdpMLineIndex)
      .catch(err => {
        console.error('Error sending ICE candidate:', err);
        throw err;
      });
  }
}


Enter fullscreen mode Exit fullscreen mode

**- Step 3: create your component called VideoCallComponent
inside VideoCallComponent.ts
set this code

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { SignalRService } from '../../../core/services/video-call-signal-r.service';

@Component({
  selector: 'app-video-call',
  templateUrl: './video-call.component.html',
  styleUrls: ['./video-call.component.css']
})
export class VideoCallComponent implements OnInit, OnDestroy {
  roomName: string = 'room1'; // Change this as needed
  private sdpSubscription: Subscription;
  private iceSubscription: Subscription;
  private localStream!: MediaStream;
  private peerConnection!: RTCPeerConnection;

  constructor(private signalRService: SignalRService) {
    this.sdpSubscription = this.signalRService.sdpReceived$.subscribe(data => {
      console.log('Received SDP:', data);
      this.handleReceivedSDP(data);
    });

    this.iceSubscription = this.signalRService.iceReceived$.subscribe(data => {
      console.log('Received ICE Candidate:', data);
      this.handleReceivedICE(data);
    });
  }

  async ngOnInit(): Promise<void> {
    await this.signalRService.joinRoom(this.roomName);
    this.initializePeerConnection();
  }

  ngOnDestroy(): void {
    this.sdpSubscription.unsubscribe();
    this.iceSubscription.unsubscribe();
    this.endCall();
  }

  async startCall() {
    try {
      await this.getLocalStream();

      if (this.peerConnection.signalingState === 'stable') {
        const offer = await this.peerConnection.createOffer();
        await this.peerConnection.setLocalDescription(offer);
        await this.signalRService.sendSDP(this.roomName, 'offer', offer.sdp!);
        console.log('SDP offer sent successfully');
      } else {
        console.log('Peer connection not in stable state to create offer');
      }
    } catch (error) {
      console.error('Error starting call:', error);
    }
  }

  async getLocalStream() {
    this.localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
    const localVideo = document.getElementById('localVideo') as HTMLVideoElement;
    localVideo.srcObject = this.localStream;

    this.localStream.getTracks().forEach(track => this.peerConnection.addTrack(track, this.localStream));
  }

  initializePeerConnection() {
    this.peerConnection = new RTCPeerConnection();

    this.peerConnection.ontrack = (event) => {
      const remoteVideo = document.getElementById('remoteVideo') as HTMLVideoElement;
      if (remoteVideo.srcObject !== event.streams[0]) {
        remoteVideo.srcObject = event.streams[0];
        console.log('Received remote stream');
      }
    };

    this.peerConnection.onicecandidate = (event) => {
      if (event.candidate) {
        this.signalRService.sendICE(this.roomName, event.candidate.candidate, event.candidate.sdpMid!, event.candidate.sdpMLineIndex!)
          .then(() => console.log('ICE candidate sent successfully'))
          .catch(error => console.error('Error sending ICE candidate:', error));
      }
    };
  }

  async handleReceivedSDP(data: any) {
    const { connectionId, sdpMid, sdp } = data;

    try {
      const remoteDesc = new RTCSessionDescription({ type: sdpMid === 'offer' ? 'offer' : 'answer', sdp });
      await this.peerConnection.setRemoteDescription(remoteDesc);

      if (sdpMid === 'offer') {
        const answer = await this.peerConnection.createAnswer();
        await this.peerConnection.setLocalDescription(answer);
        await this.signalRService.sendSDP(this.roomName, 'answer', answer.sdp!);
        console.log('SDP answer sent successfully');
      }
    } catch (error) {
      console.error('Error handling received SDP:', error);
    }
  }

  async handleReceivedICE(data: any) {
    const { connectionId, candidate, sdpMid, sdpMLineIndex } = data;

    try {
      await this.peerConnection.addIceCandidate(new RTCIceCandidate({ candidate, sdpMid, sdpMLineIndex }));
      console.log('ICE candidate added successfully');
    } catch (error) {
      console.error('Error handling received ICE candidate:', error);
    }
  }

  endCall() {
    if (this.peerConnection) {
      this.peerConnection.close();
      console.log('Call ended');
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

**- Step 4: inside html
set this code



<div>
  <button (click)="startCall()">Start Call</button>
</div>
<video id="localVideo" autoplay muted></video>
<video id="remoteVideo" autoplay></video>




Enter fullscreen mode Exit fullscreen mode

Top comments (0)