From 9d162b13edad5b891efda1e685f1706fd14271a8 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Wed, 21 Oct 2020 22:10:39 +0200 Subject: [PATCH] WebRTC JS module --- web/static/js/main.js | 13 +++- web/static/js/modules/webrtc.js | 98 ++++++++++++++++++++++++++++++ web/static/js/modules/websocket.js | 25 +++++--- web/static/js/viewer.js | 81 ------------------------ 4 files changed, 128 insertions(+), 89 deletions(-) create mode 100644 web/static/js/modules/webrtc.js delete mode 100644 web/static/js/viewer.js diff --git a/web/static/js/main.js b/web/static/js/main.js index 365731a..3e7ec0b 100644 --- a/web/static/js/main.js +++ b/web/static/js/main.js @@ -1,5 +1,6 @@ import { GsWebSocket } from "./modules/websocket.js"; import { ViewerCounter } from "./modules/viewerCounter.js"; +import { GsWebRTC } from "./modules/webrtc.js"; /** * Initialize viewer page @@ -17,7 +18,17 @@ export function initViewerPage(stream, stunServers, viewersCounterRefreshPeriod) s.open(); // Create WebRTC - // FIXME startPeerConnection() with stunServers + const c = new GsWebRTC( + stunServers, + document.getElementById("connectionIndicator"), + ); + c.createOffer(); + c.onICECandidate(localDescription => { + s.sendDescription(localDescription, stream, quality); + }); + s.onDescription(data => { + c.setRemoteDescription(data); + }); // Register keyboard events const viewer = document.getElementById("viewer"); diff --git a/web/static/js/modules/webrtc.js b/web/static/js/modules/webrtc.js new file mode 100644 index 0000000..b6e5366 --- /dev/null +++ b/web/static/js/modules/webrtc.js @@ -0,0 +1,98 @@ +/** + * GsWebRTC to connect to Ghostream + */ +export class GsWebRTC { + /** + * @param {list} stunServers + * @param {HTMLElement} connectionIndicator + */ + constructor(stunServers, connectionIndicator) { + this.connectionIndicator = connectionIndicator; + this.pc = new RTCPeerConnection({ + iceServers: [{ urls: stunServers }] + }); + + // We want to receive audio and video + this.pc.addTransceiver("video", { "direction": "sendrecv" }); + this.pc.addTransceiver("audio", { "direction": "sendrecv" }); + + // Configure events + this.pc.oniceconnectionstatechange = this._onConnectionStateChange; + this.pc.ontrack = this._onTrack; + } + + /** + * On connection change, log it and change indicator. + * If connection closed or failed, try to reconnect. + */ + _onConnectionStateChange() { + console.log("ICE connection state changed to " + this.pc.iceConnectionState); + switch (this.pc.iceConnectionState) { + case "disconnected": + this.connectionIndicator.style.fill = "#dc3545"; + break; + case "checking": + this.connectionIndicator.style.fill = "#ffc107"; + break; + case "connected": + this.connectionIndicator.style.fill = "#28a745"; + break; + case "closed": + case "failed": + console.log("Connection closed, restarting..."); + /*peerConnection.close(); + peerConnection = null; + setTimeout(startPeerConnection, 1000);*/ + break; + } + } + + /** + * On new track, add it to the player + * @param {Event} event + */ + _onTrack(event) { + console.log(`New ${event.track.kind} track`); + if (event.track.kind === "video") { + const viewer = document.getElementById("viewer"); + viewer.srcObject = event.streams[0]; + } + } + + /** + * Create an offer and set local description. + * After that the browser will fire onicecandidate events. + */ + createOffer() { + this.pc.createOffer().then(offer => { + this.pc.setLocalDescription(offer); + console.log("WebRTC offer created"); + }).catch(console.log); + } + + /** + * Register a function to call to send local descriptions + * @param {Function} sendFunction Called with a local description to send. + */ + onICECandidate(sendFunction) { + // When candidate is null, ICE layer has run out of potential configurations to suggest + // so let's send the offer to the server. + // FIXME: Send offers progressively to do Trickle ICE + this.pc.onicecandidate = event => { + if (event.candidate === null) { + // Send offer to server + console.log("Sending session description to server"); + sendFunction(this.pc.localDescription); + } + }; + } + + /** + * Set WebRTC remote description + * After that, the connection will be established and ontrack will be fired. + * @param {*} data Session description data + */ + setRemoteDescription(data) { + this.pc.setRemoteDescription(new RTCSessionDescription(data)); + } +} diff --git a/web/static/js/modules/websocket.js b/web/static/js/modules/websocket.js index 792d9f9..940cc96 100644 --- a/web/static/js/modules/websocket.js +++ b/web/static/js/modules/websocket.js @@ -13,7 +13,6 @@ export class GsWebSocket { /** * Open websocket. - * * @param {Function} openCallback Function called when connection is established. * @param {Function} closeCallback Function called when connection is lost. */ @@ -34,19 +33,31 @@ export class GsWebSocket { /** * Exchange WebRTC session description with server. - * - * @param {string} data JSON formated data - * @param {Function} receiveCallback Function called when data is received + * @param {SessionDescription} localDescription WebRTC local SDP + * @param {string} stream Name of the stream + * @param {string} quality Requested quality */ - exchangeDescription(data, receiveCallback) { + sendDescription(localDescription, stream, quality) { if (this.socket.readyState !== 1) { console.log("WebSocket not ready to send data"); return; } - this.socket.send(data); + this.socket.send(JSON.stringify({ + "webRtcSdp": localDescription, + "stream": stream, + "quality": quality + })); + } + + /** + * Set callback function on new session description. + * @param {Function} callback Function called when data is received + */ + onDescription(callback) { this.socket.addEventListener("message", (event) => { + // FIXME: json to session description console.log("Message from server ", event.data); - receiveCallback(event); + callback(event.data); }); } } diff --git a/web/static/js/viewer.js b/web/static/js/viewer.js deleted file mode 100644 index 4891dd8..0000000 --- a/web/static/js/viewer.js +++ /dev/null @@ -1,81 +0,0 @@ -let peerConnection; -let streamPath = window.location.href; -let stream = streamPath; -let quality = "source"; - - -const startPeerConnection = () => { - // Init peer connection - peerConnection = new RTCPeerConnection({ - iceServers: [{ urls: stunServers }] - }); - - // On connection change, change indicator color - // if connection failed, restart peer connection - peerConnection.oniceconnectionstatechange = e => { - console.log("ICE connection state changed, " + peerConnection.iceConnectionState); - switch (peerConnection.iceConnectionState) { - case "disconnected": - document.getElementById("connectionIndicator").style.fill = "#dc3545"; - break; - case "checking": - document.getElementById("connectionIndicator").style.fill = "#ffc107"; - break; - case "connected": - document.getElementById("connectionIndicator").style.fill = "#28a745"; - break; - case "closed": - case "failed": - console.log("Connection failed, restarting..."); - peerConnection.close(); - peerConnection = null; - setTimeout(startPeerConnection, 1000); - break; - } - }; - - // We want to receive audio and video - peerConnection.addTransceiver("video", { "direction": "sendrecv" }); - peerConnection.addTransceiver("audio", { "direction": "sendrecv" }); - - // Create offer and set local description - peerConnection.createOffer().then(offer => { - // After setLocalDescription, the browser will fire onicecandidate events - peerConnection.setLocalDescription(offer); - }).catch(console.log); - - // When candidate is null, ICE layer has run out of potential configurations to suggest - // so let's send the offer to the server - peerConnection.onicecandidate = event => { - if (event.candidate === null) { - // Send offer to server - // The server replies with its description - // After setRemoteDescription, the browser will fire ontrack events - console.log("Sending session description to server"); - fetch(streamPath, { - method: "POST", - headers: { - "Accept": "application/json", - "Content-Type": "application/json" - }, - body: JSON.stringify({ - "webRtcSdp": peerConnection.localDescription, - "stream": stream, - "quality": quality - }) - }) - .then(response => response.json()) - .then((data) => peerConnection.setRemoteDescription(new RTCSessionDescription(data))) - .catch(console.log); - } - }; - - // When video track is received, configure player - peerConnection.ontrack = function (event) { - console.log(`New ${event.track.kind} track`); - if (event.track.kind === "video") { - const viewer = document.getElementById("viewer"); - viewer.srcObject = event.streams[0]; - } - }; -};