Continued work expirementing with WebRTC

This commit is contained in:
rainbow napkin 2026-09-24 17:16:29 -04:00
parent 3517a1499a
commit 86e9c586b6
7 changed files with 341 additions and 15 deletions

View file

@ -56,6 +56,8 @@ class channel{
* Child PM Handler
*/
this.pmHandler = new pmHandler(this);
this.webRTCHandler = new webRTCHandler(this);
/**
* Child Canopy Panel Object

View file

@ -944,11 +944,6 @@ class WebRTCLivestreamHandler extends rawFileBase{
//Call derived constructor
super(client, player, media, "webrtc");
/**
* Internet Connectivity Establishment configuration
*/
this.iceConfig = {iceServers: new Map(media.rawLink.combo).get('default')};
this.defineListeners();
}
@ -960,6 +955,25 @@ class WebRTCLivestreamHandler extends rawFileBase{
this.video.addEventListener('contextmenu', (e)=>{e.preventDefault()});
}
async buildPlayer(){
//Call derived buildPlayer function
super.buildPlayer();
/**
* set Internet Connectivity Establishment configuration
*/
this.client.webRTCHandler.iceConfig = {
iceServers: new Map(this.nowPlaying.rawLink.combo).get('default')
};
//If we're not streaming
if(!this.client.webRTCHandler.isStreaming){
//Send stream request
await this.client.webRTCHandler.requestStream();
}
}
start(){
//call derived start function
super.start();

View file

@ -307,6 +307,13 @@ class queuePanel extends panelObj{
* @param {Event} event - Event passed down from Event Listener
*/
async goLive(event){
//If we're broadcastinv via webrtc
if(this.client.streamType == "webrtc"){
this.client.webRTCHandler.isBroadcasting = true;
this.client.webRTCHandler.broadcastingStream = await navigator.mediaDevices.getDisplayMedia();
}
//Start a livestream
client.socket.emit('goLive',{title: this.goLiveNamePrompt.value, mode: event.target.dataset['mode']});
}

View file

@ -0,0 +1,276 @@
/*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 <https://www.gnu.org/licenses/>.*/
/**
* 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);
}
}