Intro
This time, I try video chatting with WebRTC.
I use the ASP.NET Core application what was created last time as a server-side application.
And I referred these samples to create this WebRTC sample.
- samples-server/s/webrtc-from-chat at master · mdn/samples-server · GitHub
- Building a WebRTC video broadcast using Javascript
I didn't add any new packages into client-side or server-side.
Use Camera and Mic
To use Camera ans Mic from Web browsers, I use "getUserMedia".
Index.cshtml
<!DOCTYPE html>
<html lang="en">
<head>
<title>Hello WebRTC</title>
<meta charset="utf-8">
</head>
<body>
...
<div id="webrtc_sample_area">
<button id="offer_button" onclick="Page.sendOffer()">Offer</button>
<button id="hangup_button" onclick="Page.hangUp()">Hang Up</button>
<video id="local_video" muted>Video stream not available.</video>
<video id="received_video" autoplay>Video stream not available.</video>
</div>
<script src="js/main.js"></script>
</body>
</html>
main.page.ts
import { WebRtcController } from "./webrtc-controller";
let rtcSample = new WebRtcController();
function init(){
rtcSample = new WebRtcController();
rtcSample.initVideo();
}
init();
webrtc-controller.ts
export class WebRtcController {
private webcamStream: MediaStream|null = null;
public initVideo(){
const localVideo = document.getElementById('local_video') as HTMLVideoElement;
let streaming = false;
// after being UserMedia available, set Video element's size.
localVideo.addEventListener('canplay', ev => {
if (streaming === false) {
const width = 320;
const height = localVideo.videoHeight / (localVideo.videoWidth/width);
localVideo.setAttribute('width', width.toString());
localVideo.setAttribute('height', height.toString());
streaming = true;
}
}, false);
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
this.webcamStream = stream;
localVideo.srcObject = stream;
localVideo.play();
streaming = true;
})
.catch(err => console.error(`An error occurred: ${err}`));
}
}
- Getting started with media devices | WebRTC
- Media capture and constraints | WebRTC
- MediaDevices.getUserMedia() - Web APIs | MDN
RTCPeerConnection
After setting media devices enabled, I can connect with other clients through "RTCPeerConnection".
In this sample, the flow of operations like below.
webrtc-controller.ts
import { Candidate } from "./candidate";
import { VideoOffer } from "./video-offer";
export class WebRtcController {
private wsConnection: WebSocket|null = null;
private myPeerConnection: RTCPeerConnection|null = null;
private webcamStream: MediaStream|null = null;
public updateWebSocket(ws: WebSocket|null) {
this.wsConnection = ws;
}
public initVideo(){
const localVideo = document.getElementById('local_video') as HTMLVideoElement;
let streaming = false;
// after being UserMedia available, set Video element's size.
localVideo.addEventListener('canplay', ev => {
if (streaming === false) {
const width = 320;
const height = localVideo.videoHeight / (localVideo.videoWidth/width);
localVideo.setAttribute('width', width.toString());
localVideo.setAttribute('height', height.toString());
streaming = true;
}
}, false);
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
.then(stream => {
this.webcamStream = stream;
localVideo.srcObject = stream;
localVideo.play();
streaming = true;
})
.catch(err => console.error(`An error occurred: ${err}`));
}
public invite() {
// Create RTCPeerConnection and add local media stream.
this.myPeerConnection = this.createPeerConnection();
if (this.webcamStream == null) {
return;
}
this.webcamStream.getTracks().forEach(
track => {
if (this.myPeerConnection == null || this.webcamStream == null) {
return;
}
this.myPeerConnection.addTrack(track, this.webcamStream);
});
}
public async handleVideoOfferMsg(payload: VideoOffer) {
if (payload.sdp == null) {
return;
}
if (this.myPeerConnection == null) {
this.myPeerConnection = this.createPeerConnection();
if (this.myPeerConnection == null) {
return;
}
}
const remoteDescription = new RTCSessionDescription(payload.sdp);
if (this.myPeerConnection.signalingState != "stable") {
await Promise.all([
this.myPeerConnection.setLocalDescription({type: 'rollback'}),
this.myPeerConnection.setRemoteDescription(remoteDescription)
]);
return;
}
await this.myPeerConnection.setRemoteDescription(remoteDescription);
if (this.webcamStream == null) {
try {
this.webcamStream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
} catch(err) {
console.error(err);
return;
}
const localVideo = document.getElementById('local_video') as HTMLVideoElement;
localVideo.srcObject = this.webcamStream;
}
try {
this.webcamStream.getTracks().forEach(
track => {
if (this.myPeerConnection != null && this.webcamStream != null) {
this.myPeerConnection.addTrack(track, this.webcamStream);
}
});
} catch(err) {
console.error(err);
}
await this.myPeerConnection.setLocalDescription(await this.myPeerConnection.createAnswer());
this.sendToServer({
type: 'video-answer',
sdp: this.myPeerConnection.localDescription,
});
}
public async handleVideoAnswerMsg(msg: VideoOffer) {
if (msg.sdp == null || this.myPeerConnection == null) {
return;
}
const remoteDescription = new RTCSessionDescription(msg.sdp);
await this.myPeerConnection.setRemoteDescription(remoteDescription)
.catch(err => console.error(err));
}
public async handleNewICECandidateMsg(msg: Candidate) {
if (msg.candidate == null || this.myPeerConnection == null) {
return;
}
const candidate = new RTCIceCandidate(msg.candidate);
try {
await this.myPeerConnection.addIceCandidate(candidate)
} catch(err) {
console.error(err);
}
}
public closeVideoCall() {
const localVideo = document.getElementById('local_video') as HTMLVideoElement;
if (localVideo == null) {
return;
}
if (this.myPeerConnection) {
this.myPeerConnection.ontrack = null;
this.myPeerConnection.onicecandidate = null;
this.myPeerConnection.oniceconnectionstatechange = null;
this.myPeerConnection.onsignalingstatechange = null;
this.myPeerConnection.onnegotiationneeded = null;
if (localVideo.srcObject) {
localVideo.pause();
(localVideo.srcObject as MediaStream).getTracks().forEach(track => track.stop());
}
this.myPeerConnection.close();
this.myPeerConnection = null;
this.webcamStream = null;
}
}
private createPeerConnection(): RTCPeerConnection {
const newPeerConnection = new RTCPeerConnection({
iceServers: [{
urls: `stun:stun.l.google.com:19302`, // A STUN server
}]
});
newPeerConnection.onicecandidate = (ev) => this.handleICECandidateEvent(ev, this);
newPeerConnection.oniceconnectionstatechange = (ev) => this.handleICEConnectionStateChangeEvent(ev, newPeerConnection);
newPeerConnection.onsignalingstatechange = (ev) => this.handleSignalingStateChangeEvent(ev, newPeerConnection);
newPeerConnection.onnegotiationneeded = () => this.handleNegotiationNeededEvent(newPeerConnection);
newPeerConnection.ontrack = this.handleTrackEvent;
return newPeerConnection;
}
private sendToServer(msg: VideoOffer|Candidate) {
if (this.wsConnection == null) {
return;
}
this.wsConnection.send(JSON.stringify(msg));
}
private async handleNegotiationNeededEvent(connection: RTCPeerConnection) {
if (connection == null) {
return;
}
try {
const offer = await connection.createOffer();
if (connection.signalingState != 'stable') {
return;
}
await connection.setLocalDescription(offer);
this.sendToServer({
type: 'video-offer',
sdp: connection.localDescription,
});
} catch(err) {
console.error(err);
};
}
private handleICECandidateEvent(event: RTCPeerConnectionIceEvent, self: WebRtcController) {
if (event.candidate) {
self.sendToServer({
type: 'new-ice-candidate',
candidate: event.candidate
});
}
}
private handleICEConnectionStateChangeEvent(event: Event, connection: RTCPeerConnection) {
if (connection == null) {
return;
}
switch(connection.iceConnectionState) {
case 'closed':
case 'failed':
case 'disconnected':
this.closeVideoCall();
break;
}
}
private handleSignalingStateChangeEvent(event: Event, connection: RTCPeerConnection) {
if (connection == null) {
return;
}
switch(connection.signalingState) {
case 'closed':
this.closeVideoCall();
break;
}
}
private handleTrackEvent(event: RTCTrackEvent) {
const receivedVideo = document.getElementById('received_video') as HTMLVideoElement;
receivedVideo.srcObject = event.streams[0];
}
}
video-offer.ts
export type VideoOffer = {
type: 'video-offer'|'video-answer',
sdp: RTCSessionDescription|null,
};
candidate.ts
export type Candidate = {
type: 'new-ice-candidate',
candidate: RTCIceCandidateInit|null
};
main.page.ts
import { WebRtcController } from "./webrtc-controller";
import { WebsocketMessage } from "./websocket-message";
let ws: WebSocket|null = null;
let rtcSample = new WebRtcController();
export function connect() {
ws = new WebSocket(`wss://${window.location.hostname}:5001/ws`);
ws.onopen = () => sendMessage({
event: 'message',
type: 'text',
data: 'connected',
});
ws.onmessage = async data => {
const message = JSON.parse(data.data);
console.log(message);
switch(message.type) {
case 'text':
addReceivedMessage(message.data);
break;
case 'video-offer':
console.log("VideoOffer");
await rtcSample.handleVideoOfferMsg(message);
break;
case 'video-answer':
await rtcSample.handleVideoAnswerMsg(message);
break;
case 'new-ice-candidate':
await rtcSample.handleNewICECandidateMsg(message);
break;
default:
console.log('not found');
break;
}
};
rtcSample.updateWebSocket(ws);
}
export function send() {
const messageArea = document.getElementById('input_message') as HTMLTextAreaElement;
sendMessage({
event: 'message',
type: 'text',
data: messageArea.value,
});
}
export function close() {
if(ws == null) {
console.warn('WebSocket was null');
return;
}
rtcSample.updateWebSocket(null);
ws.close();
ws = null;
}
export function sendOffer() {
rtcSample.invite();
}
export function hangUp() {
rtcSample.closeVideoCall();
}
function addReceivedMessage(message: string) {
const receivedMessageArea = document.getElementById('received_text_area') as HTMLElement;
const child = document.createElement('div');
child.textContent = message;
receivedMessageArea.appendChild(child);
}
function sendMessage(message: WebsocketMessage) {
if (ws == null) {
console.warn('WebSocket was null');
return;
}
ws.send(JSON.stringify(message));
}
function init(){
rtcSample = new WebRtcController();
rtcSample.initVideo();
}
init();
DOMException
Because I didn't stop User1's own WebSocket message, I got an exception on connecting.
DOMException: Failed to execute 'setRemoteDescription' on 'RTCPeerConnection':
Failed to set remote answer sdp: Called in wrong state: have-remote-offer
...
So I stopped receiveing sender own WebSocket messages.
WebSocketHolder.cs
...
private async Task EchoAsync(WebSocket webSocket)
{
try
{
// for sending data
byte[] buffer = new byte[1024 * 4];
while(true)
{
WebSocketReceiveResult result = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), cancel);
if(result.CloseStatus.HasValue)
{
await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, cancel);
clients.TryRemove(clients.First(w => w.Value == webSocket));
webSocket.Dispose();
break;
}
// Send to all clients
foreach(var c in clients)
{
if(c.Value == webSocket) {
continue;
}
await c.Value.SendAsync(
new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, cancel);
}
}
}
Top comments (0)