Continued work expirementing with WebRTC
This commit is contained in:
parent
3517a1499a
commit
86e9c586b6
7 changed files with 341 additions and 15 deletions
|
|
@ -369,11 +369,6 @@ class queue{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*if(chanDB.settings.streamType == "webrtc"){
|
|
||||||
console.log("WEBRTC ENABLED, BAILING!");
|
|
||||||
return;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
//If something is playing
|
//If something is playing
|
||||||
if(this.nowPlaying != null){
|
if(this.nowPlaying != null){
|
||||||
//Capture currently playing object
|
//Capture currently playing object
|
||||||
|
|
@ -410,6 +405,10 @@ class queue{
|
||||||
}
|
}
|
||||||
|
|
||||||
rawLink = {combo:[['default',streamURL]]};
|
rawLink = {combo:[['default',streamURL]]};
|
||||||
|
//Otherwise if we're starting a web RTC Stream
|
||||||
|
}else if(chanDB.settings.streamType == "webrtc"){
|
||||||
|
//Let our webRTCHandler know
|
||||||
|
this.channel.webRTCHandler.startStream(socket);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Create queued media object from stream URL and set it to nowPlaying
|
//Create queued media object from stream URL and set it to nowPlaying
|
||||||
|
|
@ -1351,6 +1350,11 @@ class queue{
|
||||||
//Set duration from start and end time
|
//Set duration from start and end time
|
||||||
wasPlaying.duration = (now - wasPlaying.startTime) / 1000;
|
wasPlaying.duration = (now - wasPlaying.startTime) / 1000;
|
||||||
|
|
||||||
|
//If we're ending a webrtc stream
|
||||||
|
if(wasPlaying.type == "webrtc"){
|
||||||
|
this.channel.webRTCHandler.endStream();
|
||||||
|
}
|
||||||
|
|
||||||
//If we're in pushback mode
|
//If we're in pushback mode
|
||||||
if(this.liveMode == "pushback"){
|
if(this.liveMode == "pushback"){
|
||||||
await this.livestreamPushbackSchedule(wasPlaying, chanDB);
|
await this.livestreamPushbackSchedule(wasPlaying, chanDB);
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,15 @@ class webRTCHandler{
|
||||||
defineListeners(socket){
|
defineListeners(socket){
|
||||||
//Queueing Functions
|
//Queueing Functions
|
||||||
socket.on("relayRTCData", (data) => {this.relayRTCData(socket, data)});
|
socket.on("relayRTCData", (data) => {this.relayRTCData(socket, data)});
|
||||||
|
socket.on("disconnect", (data) => {this.handleStreamerDisconnect(socket, data)});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleStreamerDisconnect(socket){
|
||||||
|
//If the disconnecting socket is the current streamer
|
||||||
|
if(socket.id == this.currentStreamer){
|
||||||
|
//Tell the queue to stop current stream
|
||||||
|
this.channel.queue.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startStream(socket){
|
startStream(socket){
|
||||||
|
|
@ -61,20 +70,33 @@ class webRTCHandler{
|
||||||
//Set the recipient to the current streamer by default, assuming that this isn't coming from the streaming socket
|
//Set the recipient to the current streamer by default, assuming that this isn't coming from the streaming socket
|
||||||
let recipientID = this.currentStreamer;
|
let recipientID = this.currentStreamer;
|
||||||
|
|
||||||
|
//Create object to hold data being relayed to client
|
||||||
|
let webRTCData = {
|
||||||
|
type: data.type,
|
||||||
|
body: data.body
|
||||||
|
};
|
||||||
|
|
||||||
//if data is being relayed from streaming socket to a viewer
|
//if data is being relayed from streaming socket to a viewer
|
||||||
if(socket.id == this.currentStreamer){
|
if(socket.id == this.currentStreamer){
|
||||||
//Change recipient to reflect streaming socket's request
|
//Change recipient to reflect streaming socket's request
|
||||||
recipientID = data.recipient;
|
recipientID = data.recipient;
|
||||||
|
//Otherwise if the data is being relayed from a viewer to the streaming socket
|
||||||
|
}else{
|
||||||
|
//Add viewers socket id
|
||||||
|
webRTCData.source = {
|
||||||
|
user: socket.user.user,
|
||||||
|
socket: socket.id
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//Get recipient socket
|
//Get recipient socket
|
||||||
const recipient = this.server.io.sockets.sockets.get(recipientID);
|
const recipient = this.server.io.sockets.sockets.get(recipientID);
|
||||||
|
|
||||||
//Relay RTC Data
|
//If we have a recipient
|
||||||
recipient.emit("RTCData", {
|
if(recipient != null){
|
||||||
type: data.type,
|
//Relay RTC Data
|
||||||
body: data.body
|
recipient.emit("RTCData", webRTCData);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
|
||||||
<script src="/js/channel/mediaHandler.js"></script>
|
<script src="/js/channel/mediaHandler.js"></script>
|
||||||
<script src="/js/channel/player.js"></script>
|
<script src="/js/channel/player.js"></script>
|
||||||
<script src="/js/channel/pmHandler.js"></script>
|
<script src="/js/channel/pmHandler.js"></script>
|
||||||
|
<script src="/js/channel/webRTCHandler.js"></script>
|
||||||
<script src="/js/channel/cpanel.js"></script>
|
<script src="/js/channel/cpanel.js"></script>
|
||||||
<%# panels %>
|
<%# panels %>
|
||||||
<script src="/js/channel/panels/emotePanel.js"></script>
|
<script src="/js/channel/panels/emotePanel.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,8 @@ class channel{
|
||||||
*/
|
*/
|
||||||
this.pmHandler = new pmHandler(this);
|
this.pmHandler = new pmHandler(this);
|
||||||
|
|
||||||
|
this.webRTCHandler = new webRTCHandler(this);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Child Canopy Panel Object
|
* Child Canopy Panel Object
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -944,11 +944,6 @@ class WebRTCLivestreamHandler extends rawFileBase{
|
||||||
//Call derived constructor
|
//Call derived constructor
|
||||||
super(client, player, media, "webrtc");
|
super(client, player, media, "webrtc");
|
||||||
|
|
||||||
/**
|
|
||||||
* Internet Connectivity Establishment configuration
|
|
||||||
*/
|
|
||||||
this.iceConfig = {iceServers: new Map(media.rawLink.combo).get('default')};
|
|
||||||
|
|
||||||
this.defineListeners();
|
this.defineListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -960,6 +955,25 @@ class WebRTCLivestreamHandler extends rawFileBase{
|
||||||
this.video.addEventListener('contextmenu', (e)=>{e.preventDefault()});
|
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(){
|
start(){
|
||||||
//call derived start function
|
//call derived start function
|
||||||
super.start();
|
super.start();
|
||||||
|
|
|
||||||
|
|
@ -307,6 +307,13 @@ class queuePanel extends panelObj{
|
||||||
* @param {Event} event - Event passed down from Event Listener
|
* @param {Event} event - Event passed down from Event Listener
|
||||||
*/
|
*/
|
||||||
async goLive(event){
|
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
|
//Start a livestream
|
||||||
client.socket.emit('goLive',{title: this.goLiveNamePrompt.value, mode: event.target.dataset['mode']});
|
client.socket.emit('goLive',{title: this.goLiveNamePrompt.value, mode: event.target.dataset['mode']});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
276
www/js/channel/webRTCHandler.js
Normal file
276
www/js/channel/webRTCHandler.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue