diff --git a/config.example.json b/config.example.json index 990e2a7..51388bb 100644 --- a/config.example.json +++ b/config.example.json @@ -36,7 +36,7 @@ }, "iceServers":[ { - "url":"stun:stun.moonlight-stream.org:3478" + "urls":["stun:stun.moonlight-stream.org:3478"] } ], "links":{ diff --git a/config.example.jsonc b/config.example.jsonc index c9dfb42..163517e 100644 --- a/config.example.jsonc +++ b/config.example.jsonc @@ -68,7 +68,7 @@ //Privdes STUN/TURN servers for WebRTC functionality "iceServers":[ { - "url":"stun:stun.moonlight-stream.org:3478" + "urls":["stun:stun.moonlight-stream.org:3478"] } ], //Provides customizable links for navbar diff --git a/www/js/channel/mediaHandler.js b/www/js/channel/mediaHandler.js index 97708c5..07e4d89 100644 --- a/www/js/channel/mediaHandler.js +++ b/www/js/channel/mediaHandler.js @@ -944,6 +944,7 @@ class WebRTCLivestreamHandler extends rawFileBase{ //Call derived constructor super(client, player, media, "webrtc"); + //Define RTCHandler related listeners this.defineListeners(); } @@ -968,18 +969,22 @@ class WebRTCLivestreamHandler extends rawFileBase{ }; //If we're not streaming - if(!this.client.webRTCHandler.isStreaming){ + if(!this.client.webRTCHandler.isBroadcasting){ //Send stream request await this.client.webRTCHandler.requestStream(); + //otherwise + }else{ + //Load broadcasting stream into the player for local playback + this.startStream(this.client.webRTCHandler.broadcastingStream); } } - start(){ - //call derived start function - super.start(); + startStream(stream){ + //Set video source object to stream + this.video.srcObject = stream; - //Lock the player - this.setPlayerLock(true); + //Autoplay Stream + this.video.play(); } setVideoTitle(title){ @@ -996,4 +1001,12 @@ class WebRTCLivestreamHandler extends rawFileBase{ //Inject glowspan into title in a way that allows it to be easily replaced this.player.title.prepend(glowSpan); } + + end(){ + //Let the webRTC handler know the stream ended + this.client.webRTCHandler.end(); + + //Call derived method + super.end(); + } } \ No newline at end of file diff --git a/www/js/channel/panels/queuePanel/queuePanel.js b/www/js/channel/panels/queuePanel/queuePanel.js index 4224c47..5f49000 100644 --- a/www/js/channel/panels/queuePanel/queuePanel.js +++ b/www/js/channel/panels/queuePanel/queuePanel.js @@ -309,9 +309,11 @@ class queuePanel extends panelObj{ async goLive(event){ //If we're broadcastinv via webrtc if(this.client.streamType == "webrtc"){ - this.client.webRTCHandler.isBroadcasting = true; + await this.client.webRTCHandler.startStream(); - this.client.webRTCHandler.broadcastingStream = await navigator.mediaDevices.getDisplayMedia(); + //this.client.webRTCHandler.isBroadcasting = true; + + //this.client.webRTCHandler.broadcastingStream = await navigator.mediaDevices.getDisplayMedia(); } //Start a livestream diff --git a/www/js/channel/webRTCHandler.js b/www/js/channel/webRTCHandler.js index d5a08f1..75f7b31 100644 --- a/www/js/channel/webRTCHandler.js +++ b/www/js/channel/webRTCHandler.js @@ -27,12 +27,22 @@ class webRTCHandler{ * Parent Client Management object */ this.client = client - + /** * Flag thrown when this device is broadcasting via webRTC */ this.isBroadcasting = false; + /** + * Flag thrown when a request to view a stream is answered + */ + this.answered = false; + + /** + * Create variable to hold Internet Connectivity Establishment configuration + */ + this.iceConfig; + /** * Create variable to hold the peer connection object for the currently playing remote stream */ @@ -49,9 +59,9 @@ class webRTCHandler{ this.currentViewers = new Map(); /** - * Create variable to hold Internet Connectivity Establishment configuration + * Create array to hold candidates cached before */ - this.iceConfig; + this.cachedCandidates = []; //Call setup functions this.setupInput(); @@ -71,6 +81,29 @@ class webRTCHandler{ this.client.socket.on("RTCData", this.handleRTCData.bind(this)); } + async startStream(){ + //Throw broadcasting flag + this.isBroadcasting = true; + + //Get media stream from browser input + this.broadcastingStream = await navigator.mediaDevices.getDisplayMedia({ + video: true, + audio: true + }); + + //If we didn't get any audio tracks from the DisplayMedia portion of the API + if(this.broadcastingStream == null || this.broadcastingStream.getAudioTracks().length == 0){ + //Bug user for an audio device + const tempStream = await navigator.mediaDevices.getUserMedia({video: false, audio: true}); + + //For each track received from audio device + for(const track of tempStream.getAudioTracks()){ + //Add to existing broadcast stream + this.broadcastingStream.addTrack(track); + } + } + } + /** * Handles and routes incoming RTC Messages * Normally I'd use dedicated socket.io message types, however throwing user input seems like bad security practice @@ -113,14 +146,15 @@ class webRTCHandler{ //Create new peer connection w/ provided ice config this.currentStream = new RTCPeerConnection(this.iceConfig) - //Not really used atm but it's necissary for most browsers to need either a data channel or media track - //addeed to the requesting peer before they start pulling ICE candidates... - this.currentStream.createDataChannel("aux"); - + //Setup peer event handlers this.currentStream.addEventListener('icecandidate', this.sendIceCandidate.bind(this)); + this.currentStream.addEventListener('track', this.handleVideoStream.bind(this)); //Create offer from peerConnection - const offer = await this.currentStream.createOffer(); + const offer = await this.currentStream.createOffer({ + offerToReceiveAudio: true, + offerToReceiveVideo: true + }); //Set offer as peerConnections local description await this.currentStream.setLocalDescription(offer); @@ -161,18 +195,35 @@ class webRTCHandler{ //Get answer from received data const answer = new RTCSessionDescription(data.body); + //Throw answered flag + this.answered = true; + + //for each cached ICE candidate + for(const candidate of this.cachedCandidates){ + //Send cached candidate, handing candidate to method packed in a phoney event object + this.sendIceCandidate({candidate}); + } + //Set remote desription this.currentStream.setRemoteDescription(answer); + } sendIceCandidate(event){ //If we have a candidate if(event.candidate != null){ - //Send that shit off! - this.client.socket.emit('relayRTCData',{ - type: 'candidate', - body: event.candidate - }); + //If our stream request was answered + if(this.answered){ + //Send that shit off! + this.client.socket.emit('relayRTCData',{ + type: 'candidate', + body: event.candidate + }); + //Otherwise + }else{ + //Cache candidate + this.cachedCandidates.push(event.candidate); + } } } @@ -186,10 +237,34 @@ class webRTCHandler{ this.currentStream.addIceCandidate(data.body); } } + + async handleVideoStream(event){ + //If we where given a media stream + if(event.streams != null && event.streams[0] != null){ + //hand it over to our player + this.client.player.mediaHandler.startStream(event.streams[0]); + } + } + + end(){ + //Drop flags + this.isBroadcasting = false; + this.answered = false; + + //clear peer/media stream objects + this.currentStream = null; + this.broadcastingStream = null; + + //clear currentViewers map + this.currentViewers.clear(); + + //Clear candidate cache + this.cachedCandidates = []; + } } /** - * Class representing a single connected viewer + * Class representing a single connected remote viewer */ class connectedViewer{ constructor(user, socket, offer, broadcastingStream, iceConfig, client){ @@ -214,7 +289,7 @@ class connectedViewer{ this.iceConfig = iceConfig; /** - * Client channe object + * Client channel object */ this.client = client; @@ -226,12 +301,12 @@ class connectedViewer{ //Create peer from iceConfig const peer = new RTCPeerConnection(this.iceConfig); + //For every track from the local stream for(const track of this.broadcastingStream.getTracks()){ + //Add it to the list of available tracks for our peer peer.addTrack(track, this.broadcastingStream); } - peer.addEventListener('icecandidate', this.sendIceCandidate.bind(this)); - //Set peer remote description to connection offer from viewer peer.setRemoteDescription(new RTCSessionDescription(offer)); @@ -241,6 +316,9 @@ class connectedViewer{ //Set answer as local description await peer.setLocalDescription(answer); + //Setup peer ICE Candidate event listner + peer.addEventListener('icecandidate', this.sendIceCandidate.bind(this)); + //Save peer by socket identifier this.peers.set(socket, peer); @@ -265,9 +343,7 @@ class connectedViewer{ } handleIceCandidate(data){ - console.log(this); const peer = this.peers.get(data.source.socket); - console.log(peer); peer.addIceCandidate(data.body); //Save peer by socket identifier