Hi everyone, in this post we will build a basic video call web using WebRTC,Angular and ASP.Net Core. Why? because this technologies are like read meat and malbec, their just pair well (I will write a post about this). This is going to be a very basic app with a lot of point for improvement but I will focus on basics.
WebRTC 'Supports video, voice, and generic data to be sent between peers', but as in any p2p system we need a signaling communication channel so users can discover each other, we will user singlaR for that.
The git repos are at the end of the post.
Lets begin with Backend
We only need a signaling server so create a .Net Core web api project and add singalR nuget package
dotnet new webapp -o signalRtc
cd signalRtc
dotnet add package Microsoft.AspNetCore.SignalR.Core
You could remove the controller folder that is created.
Now we need a hub, as docs saids, a hub 'allows a client and server to call methods on each other', I like to think that we invoke methods from frontend and listen to event from backend. So create a hubs folder and a class that extends Hub.
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using signalRtc.models;
namespace signalRtc.hubs
{
public class SignalRtcHub : Hub
{
}
}
There are 3 (or 4) basic actions that we need in order to make a video call app.
- Every new user need to notify they arrival.
- Existing users need to inform their presence to new user.
- Share signaling data between peers for WebRTC.
- Notify users when someone disconnect.
As I said, we are going to write task in the hub class that can be executed from the Angular project and send some data to frontend too.
First create a folder models with a UseInfo class to encapsulate user data
namespace signalRtc.models
{
public class UserInfo
{
public string userName { get; set; }
public string connectionId { get; set; }
}
}
The connectionId it's an unique identifier that signlarR give to each connected user so we can identify they, we will need it to send messages to that specific user.
Point #1
public async Task NewUser(string username)
{
var userInfo = new UserInfo() { userName = username, connectionId = Context.ConnectionId };
await Clients.Others.SendAsync("NewUserArrived", JsonSerializer.Serialize(userInfo));
}
In this short piece of code we use many of the signalR great features. The task is the action that we can invoke from frontend, there we create a User object with the current user id that we get from Context. We don't want to receive our "hello I'm a new user" so we send the message to "others", that means all users connected to the hub but not me, signalR keep the list of user for us. The "NewUserArrived" string is an identifier that we will use later in frontend to receive this messages, you could write any name you like.
Point #2 When we get the "NewUser" message, we need to inform of our existence to the new user with a hello.
public async Task HelloUser(string userName, string user)
{
var userInfo = new UserInfo() { userName = userName, connectionId = Context.ConnectionId };
await Clients.Client(user).SendAsync("UserSaidHello", JsonSerializer.Serialize(userInfo));
}
The task receive two parameters, the username of the user that is saying hello and the user that we are saluting. In order to send a message to one user, we use Clients.Client('connectionId')... In this case, we are sending a "UserSaidHello" event to frontend.
Point #3 is simple, we want to send our signal to a user to start a p2p videocall
public async Task SendSignal(string signal, string user)
{
await Clients.Client(user).SendAsync("SendSignal", Context.ConnectionId, signal);
}
Last but not essential, we want to inform to the group when a user disconnect, to do that we are going to override the onDisconnect task.
public override async Task OnDisconnectedAsync(System.Exception exception)
{
await Clients.All.SendAsync("UserDisconnect", Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
We are almost done with backend, we need to indicate to NetCore that we are using singlaR so in the Startup.cs add singalR in configureService (also cors for angular port) and an endpoint for the singalR communication
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy(MyAllowSpecificOrigins,
builder => builder.WithOrigins("http://localhost:4200", "https://localhost:4200")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
services.AddSignalR();
}
readonly string MyAllowSpecificOrigins = "AllowOrigins";
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseCors(MyAllowSpecificOrigins);
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<SignalRtcHub>("/signalrtc");
});
app.Run(async(context) =>
{
await context.Response.WriteAsync("Hello World!");
});
}
Lets move to Angular
First create a new Angular project and install few dependencies.
simple-peer (and its types, because we love static type checking) for WebRTC.
Bootrstap, because I know you could do everything with plain css and flex or grid, but I like bootstrap.
SignalR, to invoke the method we just wrote and receive the messages.
Remember to choose scss when cli ask.
ng new signalRtc
cd signalRtc
npm install simple-peer
npm install @types/simple-peer
npm install bootstrap
npm install @aspnet/signalr
To add sytles, open styles.scss and add "@import '~bootstrap/dist/css/bootstrap.min.css';"
First lets create a service to manage the singalR messages.
cd src/app
ng g service signalr
We need a private property to maintain the hub connection, we also need subjects and observables to encapsulate the events from signalR library. I like to have private subjects to emit values via methods and public observables to get data in components.
private hubConnection: signalR.HubConnection;
private newPeer = new Subject<UserInfo>();
public newPeer$ = this.newPeer.asObservable();
private helloAnswer = new Subject<UserInfo>();
public helloAnswer$ = this.helloAnswer.asObservable();
private disconnectedPeer = new Subject<UserInfo>();
public disconnectedPeer$ = this.disconnectedPeer.asObservable();
private signal = new Subject<SignalInfo>();
public signal$ = this.signal.asObservable();
As you can see, we have an observable for every task and some interfaces that you could write in a separate file
export interface PeerData {
id: string;
data: any;
}
export interface UserInfo {
userName: string;
connectionId: string;
}
export interface SignalInfo {
user: string;
signal: any;
}
Now we will write some methods, we need one to start a connection, one for response to the new user message and one to send the p2p signal
Start a connection
public async startConnection(currentUser: string): Promise<void> {
this.hubConnection = new signalR.HubConnectionBuilder()
.withUrl('https://localhost:5001/signalrtc')
.build();
await this.hubConnection.start();
console.log('Connection started');
this.hubConnection.on('NewUserArrived', (data) => {
this.newPeer.next(JSON.parse(data));
});
this.hubConnection.on('UserSaidHello', (data) => {
this.helloAnswer.next(JSON.parse(data));
});
this.hubConnection.on('UserDisconnect', (data) => {
this.disconnectedPeer.next(JSON.parse(data));
});
this.hubConnection.on('SendSignal', (user, signal) => {
this.signal.next({ user, signal });
});
this.hubConnection.invoke('NewUser', currentUser);
}
As you can see from code, we start the connection and encapsulate the message events in observables, also when we start a connection we send the 'Hello I'm a new user' message. That's the two way communication with backend, via invoke we execute task and receive data with 'on'.
Now to say hello new user and send signal data, we only need to invoke hub methods
public sendSignalToUser(signal: string, user: string) {
this.hubConnection.invoke('SendSignal', signal, user);
}
public sayHello(userName: string, user: string): void {
this.hubConnection.invoke('HelloUser', userName, user);
}
Now we are going to write a rtc service, similar to the signalR one, to encapsulate the simplee-peer events. We also need to maintain the list of connected users to start a conversation and the current peer we are talking to.
import { Injectable } from '@angular/core';
import { Observable, Subject, BehaviorSubject } from 'rxjs';
import { Instance } from 'simple-peer';
declare var SimplePeer: any;
@Injectable({
providedIn: 'root'
})
export class RtcService {
private users: BehaviorSubject<Array<UserInfo>>;
public users$: Observable<Array<UserInfo>>;
private onSignalToSend = new Subject<PeerData>();
public onSignalToSend$ = this.onSignalToSend.asObservable();
private onStream = new Subject<PeerData>();
public onStream$ = this.onStream.asObservable();
private onConnect = new Subject<PeerData>();
public onConnect$ = this.onConnect.asObservable();
private onData = new Subject<PeerData>();
public onData$ = this.onData.asObservable();
public currentPeer: Instance;
constructor() {
this.users = new BehaviorSubject([]);
this.users$ = this.users.asObservable();
}
}
When a user arrives, we add it to the list, and when they disconnect, we remove it (always in an immutable way), let’s add some methods in service for that
public newUser(user: UserInfo): void {
this.users.next([...this.users.getValue(), user]);
}
public disconnectedUser(user: UserInfo): void {
const filteredUsers = this.users.getValue().filter(x => x.connectionId === user.connectionId);
this.users.next(filteredUsers);
}
When we start a conversation, we need to create a peer instance. The simple peer library need and indication to know if we are initiating the p2p connection. It also provide some events that we will encapsulate (because we love observables). The most important event is 'signal', quoting the library description: 'Fired when the peer wants to send signaling data to the remote peer.
It is the responsibility of the application developer (that's you!) to get this data to the other peer' that's the whole point of signalR
public createPeer(stream, userId: string, initiator: boolean): Instance {
const peer = new SimplePeer({ initiator, stream });
peer.on('signal', data => {
const stringData = JSON.stringify(data);
this.onSignalToSend.next({ id: userId, data: stringData });
});
peer.on('stream', data => {
console.log('on stream', data);
this.onStream.next({ id: userId, data });
});
peer.on('connect', () => {
this.onConnect.next({ id: userId, data: null });
});
peer.on('data', data => {
this.onData.next({ id: userId, data });
});
return peer;
}
Finally, we have a method to signal a peer Instance, this actions is required by simple-peer. We have to check if exist a current peer, if it don't, it means that we are not the initiator and we have to create it because someone is staring a videocall with us
public signalPeer(userId: string, signal: string, stream: any) {
const signalObject = JSON.parse(signal);
if (this.currentPeer) {
this.currentPeer.signal(signalObject);
} else {
this.currentPeer = this.createPeer(stream, userId, false);
this.currentPeer.signal(signalObject);
}
}
public sendMessage(message: string) {
this.currentPeer.send(message);
}
We also create a sendMessage for the chat part, because WebRTC allow to send binary data between peers. We could have used signalR but we only want it as a signaling server.
Now let's change app.component.htm, we will add an input for our username and a place for a chat. Following good practice, we will make a special component to manage the connected users list to keep a place for that.
<div class="container-fluid">
<h1>SignalRTC</h1>
<div class="row">
<div class="col-5">
<div class="row">
<div class="col">
<input [(ngModel)]="currentUser" required placeholder="UserName" type="text">
</div>
<div class="col">
<div class="btn-group" role="group" aria-label="button group">
<button [disabled]="!currentUser" (click)="saveUsername()" type="button"
class="btn btn-sm btn-primary">Save</button>
</div>
</div>
</div>
<div class="row">
<div class="col">
<!-- user list component -->
</div>
</div>
</div>
<div class="col-7">
<div class="row">
<div class="col-8">
<input [(ngModel)]="dataString" required placeholder="Write a message" type="text">
</div>
<div class="col-4">
<button (click)="sendMessage()" type="button" class="btn btn-sm btn-secondary">Send</button>
</div>
</div>
</div>
</div>
</div>
This is the list component,just a simple list where we can click an user in order to start a videocall, the component will emit an event so we can react in the app component.
<ul class="list-group mt-4">
<li class="list-group-item" (click)="userClicked(user)" *ngFor="let user of users$ | async">
{{user.userName}}
</li>
</ul>
@Output() userSelected: EventEmitter<UserInfo> = new EventEmitter();
public users$: Observable<Array<UserInfo>>;
constructor(private rtcService: RtcService) { }
ngOnInit() {
this.users$ = this.rtcService.users$;
}
public userClicked(user: UserInfo) {
this.userSelected.emit(user);
}
Now you can change the app.component.html and replace the comment for the actual list component.
<app-user-list (userSelected)="onUserSelected($event)"></app-user-list>
In the component, we will use the OnInit method (in Angular we have to keep constructor clean) to suscribe to all the obserable we defined and disptach the actions we need.
ngOnInit() {
this.subscriptions.add(this.signalR.newPeer$.subscribe((user: UserInfo) => {
this.rtcService.newUser(user);
this.signalR.sayHello(this.currentUser, user.connectionId);
}));
this.subscriptions.add(this.signalR.helloAnswer$.subscribe((user: UserInfo) => {
this.rtcService.newUser(user);
}));
this.subscriptions.add(this.signalR.disconnectedPeer$.subscribe((user: UserInfo) => {
this.rtcService.disconnectedUser(user);
}));
this.subscriptions.add(this.signalR.signal$.subscribe((signalData: SignalInfo) => {
this.rtcService.signalPeer(signalData.user, signalData.signal, this.stream);
}));
this.subscriptions.add(this.rtcService.onSignalToSend$.subscribe((data: PeerData) => {
this.signalR.sendSignalToUser(data.data, data.id);
}));
this.subscriptions.add(this.rtcService.onData$.subscribe((data: PeerData) => {
console.log(`Data from user ${data.id}: ${data.data}`);
}));
this.subscriptions.add(this.rtcService.onStream$.subscribe((data: PeerData) => {
this.userVideo = data.id;
this.videoPlayer.nativeElement.srcObject = data.data;
this.videoPlayer.nativeElement.load();
this.videoPlayer.nativeElement.play();
}));
}
Brief explanation of each step:
- When a user arrives, we add create a new user and send our hell message.
- When we receive a hello, we add the user to the list.
- When a user disconnect, we remove it from the list.
- When when receive a signal we execute the function .signal that it's indicated in the simple-peer documentation.
- When simple-peer indicate that the WeRTC signal is ready, we send it to the user.
- When we receive a message, we show it.
- Finally, when the videostream is ready, we show it in a video tag.
Finally, when we click a user, we create a new peer, this will eventually fire the signal event!
We also have the function to save the username and send a message via the data channel. Also unsubscribe always on the OnDestroy lifecycle hook, in this case we have only one component but following this practice we will prevent memory leaks.
public onUserSelected(userInfo: UserInfo) {
const peer = this.rtcService.createPeer(this.stream, userInfo.connectionId, true);
this.rtcService.currentPeer = peer;
}
public async saveUsername(): Promise<void> {
try {
await this.signalR.startConnection(this.currentUser);
this.stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
} catch (error) {
console.error(`Can't join room, error ${error}`);
}
}
public sendMessage() {
this.rtcService.sendMessage(this.dataString);
this.messages = [...this.messages, { own: true, message: this.dataString }];
this.dataString = null;
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
Okey, that's all folks. As I told you at the beginning, this is a very simple starting point. We have a lot to improve, for example you could use signalR to ask to the other user if they want to accept the call! I wanted to keep it simple.
Frontend:
sebalr / signalrtc-frontend
WebRTC client example
Backend:
Oldest comments (86)
Very cool 😎!
A couple years ago I made something similar that encapsulates all the logic in a service. Could be made to handle AV pretty easily.
github.com/steveblue/ngfx/blob/mas...
Very nice, I will look in detail to get some ideas :)
HI! Do you have a repo for this code? Thank you!
Yes, sorry, I forget to put the links in the post. Already updated!
Nice work, I totally appreciate!
But please create a project for the Front-end Angular client in GitHub. I think this will make things a lot easier. The back-end already has a Visual Studio .csproj project.
Please also add some steps to installing the back-end server. The code is explained well enough.
thanks.
Hi, I use Vs Code so the frontend is a Angular CLI project.
You could run backend with dotnet CLI. As for a proper production setup, it's another tutoría and I'm not an expert
Nice work - thanks!
Does the code also work, when the user is behind a firewall/NAT?
Thanks for reading!
Behind a firewall or NAT the connection may not work, I'm not an expert in networks so I can't tell you exactly when and why, we make some test with friends behind a NAT and it work.
If it's not the case, and if you coluld not open ports, you need a TURN server, there are open source alternatives that you could host. When the server is running, you have to set it in the config section of the peer constructor.
Hi, I would like to implement this in . Netcore without angular... just C# on the server-side, and javascript and HTML on the client-side. I already have a fully implemented chatting system, both private and group conversations using WebSockets. I want to learn how to integrate video and audio calls.
Hi, both frontend libraries (simple-peer and signalR) are pure js. You could use them in your project. I don't know how to integrate them in one project because I've never used . Net for something else than rest APIs. However you could check the .Net docs on how to integrate signalR, I know it exists and is very simple to follow
Dear Sebastian,
I'm able to connect with other user using singalr and hello is working. When I choose an user the following error occurs in Angular. Please help me with this.
ReferenceError: SimplePeer is not defined
Maybe you are not importing the library
I did it in angular
npm install simple-peer
npm install @types/simple-peer
It only worked after i add the below script in index.html :(
Why is that so. Please help.
You have to import simple peer in component ts. Check the GitHub
Love you dear sebastian. You are a genius and a humble person. Never seen a great person like you to respond to these kind of simple error that are made by fellow dev's like me. Huge appreciation for your reply. God bless you.
import inside styles[] into angular.json file.
i used your repo to implement this with ionic.
everything is ok. two client can swap their sdp but onStream wil fire and has data but video element not showing any thing. also browser has permission for mic and camera
I have no experience with ionic. Are you seeing any console error?.
Maybe you need to access camera with some plugin
no errors. with a breakpoint (debugger) onstream has data and will be pass to html video element
but nothing happened ;(
Sorry I can't help. As I told you I've never use Ionic. I only used cordova and did not have any problem with video tag.
Hi.. Its a great post...
I m getting "currentpeer" (from rtc.service.ts ) object as undefined when i click on "send" button. so far my camera is activated but no video and chats are coming. any suggestion what i am missing? PFA also
You have to click a username to establish s connection before sending data
Hello Sebastian,
I clicked on user from userlist shown , currentpeer object is initialized though, but still video is not activated, PFA. I found that "connected" property of currentpeer object is set to "false". please suggest wat i m missing.
I am getting attached exception when click on send message. Please note i m the only user who is running this application on my local machine. or does it required more than 1 user to setup communication (video communication)
You need two users. You can open two browser windows
yup..it works for me when running via vs debug mode on two separate browsers but when i hosted the application on IIS, getting "simplepeer is not defined" error on console.....do I need to configure on IIS ??
also "SendSignal" hub method call is not going thru when hosted on IIS, if i select user
Sorry, I didn't understand. Why do you need IIS? I use dot net CLI to run the backend and Angular CLI to run frontend (or build and use a server like Apache or nginx) and open two browser to connect to Angular
I need IIS to host. I have to host my web api (signalr svc) and frontend on IIS
So if you open two browser and use two different user everything must work
No, its not working when hosted on IIS. I am wondering if i need to update anything on IIS related to support simplepeer. Anything you suggest ?
Hi Sebastian, Very nice project. I need the video call in my existing ASP.NET web application. Could you please share the front-end sample on ASP.NET? I appreciate your help.
Hi. The frontend project is in Angular, I believe you could embedded it in ASP.NET but I can't help you because I've never use it
Hi Sebastian,
This post is great.
I run it local is good but it's not working when I deployed it online. Front end and back end is deployed on the Ms azure.
Do you know why this issue happends?
Hello, I'm not an expert in networking but there could be two problems
One possibility (and easy to fix) it's that your Apache or nginx it's not correctly configured to allow web socket connection. If that is the case, you will see a signalR error in browser console.
If that is not the case and signalR is working ok, it's a webRTC problem, maybe some users are behind NAT or firewall, in that case you need to host and configure (config in peer constructor) a TURN server.
Edit. Another problem could be that outside localhost you need Https o video and audio request in browser it's not going to work
Hi. Please I am getting this error:
Access to XMLHttpRequest at 'localhost:5001/signalrtc/negotiate' from origin 'localhost:4200' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
can you help me please??
Hi. As the error show, you have to allow origin localhost:4200 to CORS.
But the fact is that I did it exactly as you did. But it stills showing that error. this is the startup file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using signalRtc.hubs;
namespace signalRtc
{
public class Startup
{
readonly string MyAllowSpecificOrigins = "AllowOrigins";
}
Hi. As the error shows, you have to allow origin localhost:4200 to CORS. Pleas read the tutorial carefully
How to add stun server url?
I added google stun server to simple-peer and it's not working on diffrent network.
But when i used peerjs with stun server it worked
Hello, how are you? I'm fine, thanks for asking. This is a nice community, no stack overflow, so please be nice.
Your questions make no sense, this to tutorial is about implementing your own signaling server, if you need something different you should read simple peer documentation
very nice man :D, i'm fine too :D
yes this is for signaling.
i read simple-peer documentation, but i could't run it. it has error on connection
why no screenshots?
hi,
i want to Video call integrate in asp.net c# core so how to please guid me
Hi, What did you do?
Where are you having problems?
I can help you if you have a particular problem but I'm not a tutor
my asp.net core project new Implement of Video call Option so how to do, can you help me ?
Well, this tutorial show how to integrate video call in Angular using Net Core as signaling service so that's how you do it, if you are not using Angular you could still use the rest of the code as signalR and rtc are pure js libraries.
I'm definitely not going to do your work but if you have a specific questions I will be happy to help.
Yes, But Video call option without Angular can be Integrate in asp.net core ? if yes, then how to do and all steps give me by code for Add Video call option in Project.
As I mentioned, signalR and simple-peer are js libraries and web-rtc is available in every modern web browser so yes, you could use them in any fronted technology
hey,
cd src/app
ng g service signalr
after this the src/app/signalr.service.spec.ts , src/app/signalr.service.ts
are created but where i can find this files not able to find it
Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more