/*Canopy - The next generation of stoner streaming software Copyright (C) 2024-2026 Rainbownapkin and the TTN Community This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see .*/ /** * Class for handling negotiation between peers during WebRTC Livestreams */ class webRTCHandler{ /** * Instantiates a new webRTCHandler object * @param {channel} client - Parent client mgmt object */ constructor(client){ /** * Parent Client Management object */ this.client = client /** * Flag thrown when this device is broadcasting via webRTC */ this.isBroadcasting = false; /** * Create variable to hold the peer connection object for the currently playing remote stream */ this.currentStream; /** * Create variable to hold currently broadcasting stream */ this.broadcastingStream; /** * Create map to hold objects representing viewers receiving our broadcasted stream, organized by username */ this.currentViewers = new Map(); /** * Create variable to hold Internet Connectivity Establishment configuration */ this.iceConfig; //Call setup functions this.setupInput(); this.defineListeners(); } /** * Defines input-related event listeners */ setupInput(){ } /** * Defines network-related event listeners */ defineListeners(){ this.client.socket.on("RTCData", this.handleRTCData.bind(this)); } /** * Handles and routes incoming RTC Messages * Normally I'd use dedicated socket.io message types, however throwing user input seems like bad security practice * I'll take ugly code over insecure code any day of the week */ handleRTCData(data){ //If we're brodcasting if(this.isBroadcasting){ switch(data.type){ case "request": this.handleViewer(data); break; case "candidate": this.handleIceCandidate(data); break; } //otherwise }else{ switch(data.type){ case "answer": this.handleAnswer(data); break; case "candidate": this.handleIceCandidate(data); break; } } } /** * Requests stream from streamer */ async requestStream(){ //If we're streaming if(this.client.webRTCHandler.isBroadcasting){ //fack off and die return; } //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"); this.currentStream.addEventListener('icecandidate', this.sendIceCandidate.bind(this)); //Create offer from peerConnection const offer = await this.currentStream.createOffer(); //Set offer as peerConnections local description await this.currentStream.setLocalDescription(offer); //Send RTC connection offer to server to be relayed to streamer's device this.client.socket.emit("relayRTCData", {type:"request",body:offer}); } /** * Handles request to connect to streamer from channel viewer * @param {data} data - Data from requesting socket */ async handleViewer(data){ //Check if user is already viewing stream const viewer = this.currentViewers.get(data.source.user); //If not if(data.user == null){ //Create a new connected viewer object from the received data, and store it in the currentViewers map by username this.currentViewers.set(data.source.user, new connectedViewer( data.source.user, data.source.socket, data.body, this.broadcastingStream, this.iceConfig, this.client)); //Otherwise, if they're already watching elsewhere via the same account }else{ //Hand over new socket ID and offer to the currentViewer object assigned to the current user await viewer.addSocket(data.source.socket, data.body); //Update the object within the currentViewers map this.currentViewers.set(data.source.user, viewer); } } async handleAnswer(data){ //Get answer from received data const answer = new RTCSessionDescription(data.body); //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 }); } } handleIceCandidate(data){ //If we're broadcasting if(this.isBroadcasting){ const user = this.currentViewers.get(data.source.user); user.handleIceCandidate(data); //If we're receiving a stream }else{ this.currentStream.addIceCandidate(data.body); } } } /** * Class representing a single connected viewer */ class connectedViewer{ constructor(user, socket, offer, broadcastingStream, iceConfig, client){ /** * Name of user */ this.user = user; /** * Map of user's connected peers by socket */ this.peers = new Map(); /** * Broadcasting Stream */ this.broadcastingStream = broadcastingStream; /** * ICE Config */ this.iceConfig = iceConfig; /** * Client channe object */ this.client = client; //Intake initial connection offer from user this.addSocket(socket, offer); } async addSocket(socket, offer){ //Create peer from iceConfig const peer = new RTCPeerConnection(this.iceConfig); for(const track of this.broadcastingStream.getTracks()){ 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)); //Generate answer to connection offer const answer = await peer.createAnswer(); //Set answer as local description await peer.setLocalDescription(answer); //Save peer by socket identifier this.peers.set(socket, peer); //Send answer to server to relay to peer this.client.socket.emit('relayRTCData',{ type:'answer', body:answer, recipient: socket }); } 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, recipient: this.socket }); } } 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 this.peers.set(data.source.socket, peer); } }