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

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

AWS Security LIVE!

Hosted by security experts, AWS Security LIVE! showcases AWS Partners tackling real-world security challenges. Join live and get your security questions answered.

Tune in to the full event

DEV is partnering to bring live events to the community. Join us or dismiss this billboard if you're not interested. ❤️