I am trying to use WebRTC to implement video calling in an application. Connection is being established successfully between peers after sharing the offer, answer and ice-candidate.
Remote peer is sending the track (video, audio), but local peer (receiving peer) is not listing for the track of remote peer connection.
Adding track to the peer connection :
const localStream = await window.navigator.mediaDevices.getUserMedia({video: true, audio: true});
localStream.getTracks().forEach(track => {
this.peerConnection.addTrack(track, localStream);
});
Listening track event on other side :
this.peerConnection.addEventListener('track', async (event) => {
console.log("Remote video stream added to the video element...");
remoteStream.addTrack(event.track);
});
- How to receive and show the video of remote peer?
- Is there any way to inspect the stream/tracks of RTCPeerConnection?
We are using angular for our application. Here is the complete JS code :
- Triggering video call component to offer call
makeVideoCall(){
var videoCallDialog = this.matDialog.open(VideoCallComponent, {
height: '600px',
width: '700px',
data : {
'friendUser' : this.friendUser
}
});
}
- Listening video call offer and trigger video-call component
if (chatMessage.type == ChatType.VIDEO_CALL_OFFER){
this.userChatService.findFriend(chatMessage.textFrom).subscribe(user=>{
chatMessage.data = JSON.parse(chatMessage.data);
var videoCallDialog = this.matDialog.open(VideoCallComponent, {
height: '600px',
width: '700px',
data : {
'friendUser' : user,
'videoCallOfferChat' : chatMessage
}
});
});
}
import { Component, OnInit, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { User } from 'src/app/user/model/user';
import { RxStompService } from '@stomp/ng2-stompjs';
import { Subscription } from 'rxjs';
import { LoginService } from 'src/app/login/service/login.service';
import { Chat } from 'src/app/chats/model/chat';
import { ChatType } from 'src/app/chats/model/chat-type';
import { v4 as uuidv4 } from 'uuid';
import { CryptoService } from 'src/app/crypto/service/crypto.service';
@Component({
selector: 'app-video-call',
templateUrl: './video-call.component.html',
styleUrls: ['./video-call.component.css']
})
export class VideoCallComponent implements OnInit {
currentUser : User;
friendUser : User;
chatSubscription : Subscription;
peerConnection : RTCPeerConnection;
offer : Chat;
answer : Chat;
iceCandidate : Chat;
generatedIceCandidateCount = 0;
receivedIceCandidateCount = 0;
iceCandidates = new Array<any>();
constructor(
private loginService : LoginService,
@Inject(MAT_DIALOG_DATA) public data: any,
private stompService : RxStompService,
private cryptoService : CryptoService
) {
this.currentUser = this.loginService.getCurrentUser();
if(data.friendUser){
this.friendUser = data.friendUser;
const configuration = {'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]};
this.peerConnection = new RTCPeerConnection(configuration);
this.startReceivingStreams();
// Listen for local ICE candidates on the local RTCPeerConnection
this.peerConnection.addEventListener('icecandidate', event => {
if (event.candidate) {
// console.log('ICE Candidate generated', event.candidate);
console.log("Genrated Candidate ", ++this.generatedIceCandidateCount);
this.iceCandidates.push(event.candidate);
// this.sendToSocket(this.createChat(null, ChatType.ICE_CANDIDATE, {'iceCandidate' : event.candidate}));
}else{
console.log("Candidates in buffer : ", this.generatedIceCandidateCount);
this.iceCandidates.forEach((candidate) => {
this.sendToSocket(this.createChat(null, ChatType.ICE_CANDIDATE, {'iceCandidate' : candidate}))
});
if(this.iceCandidates.length>0)
this.startTransmittingStreams();
}
});
// Listen for connectionstatechange on the local RTCPeerConnection
this.peerConnection.addEventListener('connectionstatechange', event => {
if (this.peerConnection.connectionState === 'connected') {
console.log("Connection done...");
}
});
}
this.startListeningVideoCalls();
if (data.videoCallOfferChat){
this.startReceivingStreams();
this.answerVideoCall(data.videoCallOfferChat);
}
else{
this.offerVideoCall();
}
}
ngOnInit(): void {}
async startTransmittingStreams(){
const localStream = await window.navigator.mediaDevices.getUserMedia({video: true, audio: true});
localStream.getTracks().forEach(async track => {
console.log("Adding track...", track);
await this.peerConnection.addTrack(track, localStream);
});
}
startReceivingStreams(){
console.log("Start receiving...");
let remoteStream = new MediaStream();
this.peerConnection.ontrack = event => {
console.log("[addEventListener] Remote video stream added to the video element...", event);
let remoteVideo : any = document.getElementById('friendUserVideoTrack');
remoteVideo.srcObject = remoteStream;
remoteStream.addTrack(event.track);
}
}
private startListeningVideoCalls(){
if(this.stompService.connected()){
this.chatSubscription = this.stompService.watch('/text/'+this.currentUser.id).subscribe((data:any)=>{
let chat = JSON.parse(data.body);
if(chat.data)
chat.data = JSON.parse(chat.data);
if (chat.type == ChatType.VIDEO_CALL_ANSWER) {
console.log('Video Call Answer ...', chat);
if (chat.data.answer) {
// Contains RTCConnection answer then connect
console.log('Call accepted', chat.data.answer);
this.videoCallAnswered(chat);
}else{
// Doesn't contains RTCConnection answer : call rejected by recipient
console.log('Call rejected...');
}
}
if(chat.type == ChatType.ICE_CANDIDATE){
console.log('Chat with ICE Candidate ', chat);
this.iceCandidateReceived(chat.data.iceCandidate);
}
});
}
}
async offerVideoCall() {
var options = { offerToReceiveVideo: true, offerToReceiveAudio: true };
const offer = await this.peerConnection.createOffer(options);
await this.peerConnection.setLocalDescription(offer);
this.sendToSocket(this.createChat(null, ChatType.VIDEO_CALL_OFFER, {'offer' : offer}));
}
private async answerVideoCall(receivedChat : Chat){
if (receivedChat.data.offer) {
let remoteDescription = new RTCSessionDescription(receivedChat.data.offer);
await this.peerConnection.setRemoteDescription(remoteDescription);
var options = { offerToReceiveVideo: true, offerToReceiveAudio: true };
const answer = await this.peerConnection.createAnswer(options);
await this.peerConnection.setLocalDescription(answer);
this.sendToSocket(this.createChat(receivedChat.id, ChatType.VIDEO_CALL_ANSWER, {'answer' : answer}));
}
}
private async videoCallAnswered(chat : Chat){
const remoteDesc = new RTCSessionDescription(chat.data.answer);
await this.peerConnection.setRemoteDescription(remoteDesc);
}
private createChat(id : string, chatType : ChatType, data : any){
let chat : Chat = new Chat();
chat.id = uuidv4();
chat.textFrom = this.currentUser.id;
chat.textTo = this.friendUser.id;
chat.textFromName = this.currentUser.name;
chat.textToName = this.friendUser.name;
chat.date = new Date().getTime();
chat.type = chatType;
chat.message = this.cryptoService.encrypt(chat.message, this.friendUser.userChat.sk);
chat.data = JSON.stringify(data);
return chat;
}
private sendToSocket(chat : Chat){
if(this.stompService.connected()){
this.stompService.publish({
destination : "/app/text/"+this.friendUser.id,
body : JSON.stringify(chat)
});
}
}
private async iceCandidateReceived(iceCandidate : any){
if(this.peerConnection.remoteDescription){
try {
await this.peerConnection.addIceCandidate(new RTCIceCandidate(iceCandidate));
console.log("Received Count ", ++this.receivedIceCandidateCount);
} catch (e) {
console.error('Error adding received ice candidate', e);
}
}
}
}