Implemented WebRTC peer-to-peer screen-roadcasting

This commit is contained in:
rainbow napkin 2026-09-28 03:25:56 -04:00
parent 62fc69489d
commit 96cdcfc840
5 changed files with 120 additions and 29 deletions

View file

@ -36,7 +36,7 @@
}, },
"iceServers":[ "iceServers":[
{ {
"url":"stun:stun.moonlight-stream.org:3478" "urls":["stun:stun.moonlight-stream.org:3478"]
} }
], ],
"links":{ "links":{

View file

@ -68,7 +68,7 @@
//Privdes STUN/TURN servers for WebRTC functionality //Privdes STUN/TURN servers for WebRTC functionality
"iceServers":[ "iceServers":[
{ {
"url":"stun:stun.moonlight-stream.org:3478" "urls":["stun:stun.moonlight-stream.org:3478"]
} }
], ],
//Provides customizable links for navbar //Provides customizable links for navbar

View file

@ -944,6 +944,7 @@ class WebRTCLivestreamHandler extends rawFileBase{
//Call derived constructor //Call derived constructor
super(client, player, media, "webrtc"); super(client, player, media, "webrtc");
//Define RTCHandler related listeners
this.defineListeners(); this.defineListeners();
} }
@ -968,18 +969,22 @@ class WebRTCLivestreamHandler extends rawFileBase{
}; };
//If we're not streaming //If we're not streaming
if(!this.client.webRTCHandler.isStreaming){ if(!this.client.webRTCHandler.isBroadcasting){
//Send stream request //Send stream request
await this.client.webRTCHandler.requestStream(); await this.client.webRTCHandler.requestStream();
//otherwise
}else{
//Load broadcasting stream into the player for local playback
this.startStream(this.client.webRTCHandler.broadcastingStream);
} }
} }
start(){ startStream(stream){
//call derived start function //Set video source object to stream
super.start(); this.video.srcObject = stream;
//Lock the player //Autoplay Stream
this.setPlayerLock(true); this.video.play();
} }
setVideoTitle(title){ setVideoTitle(title){
@ -996,4 +1001,12 @@ class WebRTCLivestreamHandler extends rawFileBase{
//Inject glowspan into title in a way that allows it to be easily replaced //Inject glowspan into title in a way that allows it to be easily replaced
this.player.title.prepend(glowSpan); this.player.title.prepend(glowSpan);
} }
end(){
//Let the webRTC handler know the stream ended
this.client.webRTCHandler.end();
//Call derived method
super.end();
}
} }

View file

@ -309,9 +309,11 @@ class queuePanel extends panelObj{
async goLive(event){ async goLive(event){
//If we're broadcastinv via webrtc //If we're broadcastinv via webrtc
if(this.client.streamType == "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 //Start a livestream

View file

@ -33,6 +33,16 @@ class webRTCHandler{
*/ */
this.isBroadcasting = false; 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 * Create variable to hold the peer connection object for the currently playing remote stream
*/ */
@ -49,9 +59,9 @@ class webRTCHandler{
this.currentViewers = new Map(); 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 //Call setup functions
this.setupInput(); this.setupInput();
@ -71,6 +81,29 @@ class webRTCHandler{
this.client.socket.on("RTCData", this.handleRTCData.bind(this)); 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 * Handles and routes incoming RTC Messages
* Normally I'd use dedicated socket.io message types, however throwing user input seems like bad security practice * 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 //Create new peer connection w/ provided ice config
this.currentStream = new RTCPeerConnection(this.iceConfig) 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 //Setup peer event handlers
//addeed to the requesting peer before they start pulling ICE candidates...
this.currentStream.createDataChannel("aux");
this.currentStream.addEventListener('icecandidate', this.sendIceCandidate.bind(this)); this.currentStream.addEventListener('icecandidate', this.sendIceCandidate.bind(this));
this.currentStream.addEventListener('track', this.handleVideoStream.bind(this));
//Create offer from peerConnection //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 //Set offer as peerConnections local description
await this.currentStream.setLocalDescription(offer); await this.currentStream.setLocalDescription(offer);
@ -161,18 +195,35 @@ class webRTCHandler{
//Get answer from received data //Get answer from received data
const answer = new RTCSessionDescription(data.body); 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 //Set remote desription
this.currentStream.setRemoteDescription(answer); this.currentStream.setRemoteDescription(answer);
} }
sendIceCandidate(event){ sendIceCandidate(event){
//If we have a candidate //If we have a candidate
if(event.candidate != null){ if(event.candidate != null){
//If our stream request was answered
if(this.answered){
//Send that shit off! //Send that shit off!
this.client.socket.emit('relayRTCData',{ this.client.socket.emit('relayRTCData',{
type: 'candidate', type: 'candidate',
body: event.candidate body: event.candidate
}); });
//Otherwise
}else{
//Cache candidate
this.cachedCandidates.push(event.candidate);
}
} }
} }
@ -186,10 +237,34 @@ class webRTCHandler{
this.currentStream.addIceCandidate(data.body); 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{ class connectedViewer{
constructor(user, socket, offer, broadcastingStream, iceConfig, client){ constructor(user, socket, offer, broadcastingStream, iceConfig, client){
@ -214,7 +289,7 @@ class connectedViewer{
this.iceConfig = iceConfig; this.iceConfig = iceConfig;
/** /**
* Client channe object * Client channel object
*/ */
this.client = client; this.client = client;
@ -226,12 +301,12 @@ class connectedViewer{
//Create peer from iceConfig //Create peer from iceConfig
const peer = new RTCPeerConnection(this.iceConfig); const peer = new RTCPeerConnection(this.iceConfig);
//For every track from the local stream
for(const track of this.broadcastingStream.getTracks()){ for(const track of this.broadcastingStream.getTracks()){
//Add it to the list of available tracks for our peer
peer.addTrack(track, this.broadcastingStream); peer.addTrack(track, this.broadcastingStream);
} }
peer.addEventListener('icecandidate', this.sendIceCandidate.bind(this));
//Set peer remote description to connection offer from viewer //Set peer remote description to connection offer from viewer
peer.setRemoteDescription(new RTCSessionDescription(offer)); peer.setRemoteDescription(new RTCSessionDescription(offer));
@ -241,6 +316,9 @@ class connectedViewer{
//Set answer as local description //Set answer as local description
await peer.setLocalDescription(answer); await peer.setLocalDescription(answer);
//Setup peer ICE Candidate event listner
peer.addEventListener('icecandidate', this.sendIceCandidate.bind(this));
//Save peer by socket identifier //Save peer by socket identifier
this.peers.set(socket, peer); this.peers.set(socket, peer);
@ -265,9 +343,7 @@ class connectedViewer{
} }
handleIceCandidate(data){ handleIceCandidate(data){
console.log(this);
const peer = this.peers.get(data.source.socket); const peer = this.peers.get(data.source.socket);
console.log(peer);
peer.addIceCandidate(data.body); peer.addIceCandidate(data.body);
//Save peer by socket identifier //Save peer by socket identifier