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");
}
}
}
}
**- 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();
});
**
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;
});
}
}
**- 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');
}
}
}
**- 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>
Top comments (0)