DEV Community

abdullah khrais
abdullah khrais

Posted on

2 1 1 1 1

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

Dynatrace image

Frictionless debugging for developers

Debugging in production doesn't have to be a nightmare.

Dynatrace reimagines the developer experience with runtime debugging, native OpenTelemetry support, and IDE integration allowing developers to stay in the flow and focus on building instead of fixing.

Learn more

Top comments (0)

Tiger Data image

🐯 🚀 Timescale is now TigerData: Building the Modern PostgreSQL for the Analytical and Agentic Era

We’ve quietly evolved from a time-series database into the modern PostgreSQL for today’s and tomorrow’s computing, built for performance, scale, and the agentic future.

So we’re changing our name: from Timescale to TigerData. Not to change who we are, but to reflect who we’ve become. TigerData is bold, fast, and built to power the next era of software.

Read more

👋 Kindness is contagious

Sign in to DEV to enjoy its full potential—unlock a customized interface with dark mode, personal reading preferences, and more.

Okay