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

@ -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