Compare commits

...

5 commits

15 changed files with 527 additions and 34 deletions

View file

@ -34,6 +34,11 @@
"address": "toke@42069.weed", "address": "toke@42069.weed",
"pass": "CHANGE_ME" "pass": "CHANGE_ME"
}, },
"iceServers":[
{
"url":"stun:stun.moonlight-stream.org:3478"
}
],
"links":{ "links":{
"About": "/about", "About": "/about",
"Code": "https://git.ourfore.st/rainbownapkin/canopy", "Code": "https://git.ourfore.st/rainbownapkin/canopy",

View file

@ -65,6 +65,12 @@
"address": "toke@42069.weed", "address": "toke@42069.weed",
"pass": "CHANGE_ME" "pass": "CHANGE_ME"
}, },
//Privdes STUN/TURN servers for WebRTC functionality
"iceServers":[
{
"url":"stun:stun.moonlight-stream.org:3478"
}
],
//Provides customizable links for navbar //Provides customizable links for navbar
"links":{ "links":{
"About": "/about", "About": "/about",

View file

@ -19,7 +19,8 @@ const connectedUser = require('./connectedUser');
const chatBuffer = require('./chatBuffer'); const chatBuffer = require('./chatBuffer');
const queue = require('./media/queue'); const queue = require('./media/queue');
const channelModel = require('../../schemas/channel/channelSchema'); const channelModel = require('../../schemas/channel/channelSchema');
const playlistHandler = require('./media/playlistHandler') const playlistHandler = require('./media/playlistHandler');
const webRTCHandler = require('./media/webRTCHandler');
/** /**
* Class representing a single active channel * Class representing a single active channel
@ -67,6 +68,11 @@ class activeChannel{
*/ */
this.playlistHandler = new playlistHandler(server, this); this.playlistHandler = new playlistHandler(server, this);
/**
* Child WebRTC Handler Object
*/
this.webRTCHandler = new webRTCHandler(server, this);
/** /**
* Child Chat Buffer Object * Child Chat Buffer Object
*/ */
@ -108,6 +114,7 @@ class activeChannel{
//Define per-channel event listeners //Define per-channel event listeners
this.queue.defineListeners(socket); this.queue.defineListeners(socket);
this.playlistHandler.defineListeners(socket); this.playlistHandler.defineListeners(socket);
this.webRTCHandler.defineListeners(socket);
//Hand off the connection initiation to it's user object //Hand off the connection initiation to it's user object
const activeUser = await userObj.handleConnection(userDB, chanDB, socket) const activeUser = await userObj.handleConnection(userDB, chanDB, socket)

View file

@ -329,7 +329,11 @@ class queue{
*/ */
async goLive(socket, data){ async goLive(socket, data){
try{ try{
//Define and/or declare required variables
let title = "Livestream"; let title = "Livestream";
let filename = '/';
let streamURL = '/';
let rawLink = {combo: [['default', config.iceServers]]};
if(data != null && data.title != null){ if(data != null && data.title != null){
//If the title is too long //If the title is too long
@ -365,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
@ -383,24 +382,33 @@ class queue{
//Kill schedule timers to prevent items from starting during the stream //Kill schedule timers to prevent items from starting during the stream
await this.stopScheduleTimers(); await this.stopScheduleTimers();
//Syntatic sugar because I'm lazy :P //If we're streaming via HLS
const streamURL = chanDB.settings.streamURL; if(chanDB.settings.streamType == "livehls"){
//Syntatic sugar because I'm lazy :P
streamURL = chanDB.settings.streamURL;
if(streamURL == ''){ if(streamURL == ''){
throw loggerUtils.exceptionSmith('This channel\'s HLS Livestream Source has not been set!', 'queue'); throw loggerUtils.exceptionSmith('This channel\'s HLS Livestream Source has not been set!', 'queue');
} }
//Pull filename from streamURL //Pull filename from streamURL
let filename = streamURL.match(/^.+\..+\/(.+)$/); filename = streamURL.match(/^.+\..+\/(.+)$/);
//If we're streaming from the root of the domain //If we're streaming from the root of the domain
if(filename == null){ if(filename == null){
//Set filename to root //Set filename to root
filename = '/'; filename = '/';
}else{ }else{
//Otherwise, hand over the filename //Otherwise, hand over the filename
filename = filename[1]; filename = filename[1];
}
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
@ -409,9 +417,9 @@ class queue{
filename, filename,
streamURL, streamURL,
streamURL, streamURL,
"livehls", chanDB.settings.streamType,
0, 0,
streamURL, rawLink,
new Date().getTime() new Date().getTime()
); );
@ -1261,7 +1269,7 @@ class queue{
} }
//If we're ending an HLS Livestream //If we're ending an HLS Livestream
if(wasPlaying.type == "livehls"){ if(wasPlaying.type == "livehls" || wasPlaying.type == "webrtc"){
//Redirect to the endLivestream function //Redirect to the endLivestream function
return this.endLivestream(wasPlaying, chanDB) return this.endLivestream(wasPlaying, chanDB)
} }
@ -1342,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);
@ -1569,7 +1582,7 @@ class queue{
const stoppedMedia = this.nowPlaying; const stoppedMedia = this.nowPlaying;
//Ignore early end for livestreams //Ignore early end for livestreams
if(this.nowPlaying.type != 'livehls'){ if(this.nowPlaying.type != 'livehls' && this.nowPlaying.type != 'webrtc'){
//Get difference between current time and start time and set as early end //Get difference between current time and start time and set as early end
stoppedMedia.earlyEnd = (new Date().getTime() - stoppedMedia.startTime) / 1000; stoppedMedia.earlyEnd = (new Date().getTime() - stoppedMedia.startTime) / 1000;
} }
@ -1806,7 +1819,7 @@ class queue{
const now = new Date().getTime(); const now = new Date().getTime();
//If something was playing //If something was playing
if(chanDB.media.nowPlaying != null && chanDB.media.nowPlaying.type != 'livehls'){ if(chanDB.media.nowPlaying != null && (chanDB.media.nowPlaying.type != 'livehls' && chanDB.media.nowPlaying.type != 'webrtc')){
//Rehydrate the currently playing item int oa queued media object //Rehydrate the currently playing item int oa queued media object
const wasPlaying = chanDB.media.nowPlaying.rehydrate(); const wasPlaying = chanDB.media.nowPlaying.rehydrate();

View file

@ -0,0 +1,103 @@
/*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/>.*/
/**
* Object to relay WebRTC Negotiation data for a single channel
*/
class webRTCHandler{
/**
* Instantiates a new object to relay WebRTC Negotiation data for a single channel
* @param {channelManager} server - Parent server object
* @param {activeChannel} channel - Parent Channel object
*/
constructor(server, channel){
/**
* Parent Server Object
*/
this.server = server
/**
* Parent Channel Object
*/
this.channel = channel;
/**
* Declare class property to hold socked it of the current device/browser-tab we're streaming from
*/
this.currentStreamer;
}
/**
* Defines server-side socket.io listeners for newly connected sockets
* @param {Socket} socket - Newly connected socket to define listeners against
*/
defineListeners(socket){
//Queueing Functions
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){
this.currentStreamer = socket.id;
}
endStream(){
this.currentStreamer = null;
}
relayRTCData(socket, data){
//Set the recipient to the current streamer by default, assuming that this isn't coming from the streaming socket
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(socket.id == this.currentStreamer){
//Change recipient to reflect streaming socket's request
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
const recipient = this.server.io.sockets.sockets.get(recipientID);
//If we have a recipient
if(recipient != null){
//Relay RTC Data
recipient.emit("RTCData", webRTCData);
}
}
}
module.exports = webRTCHandler;

View file

@ -38,7 +38,7 @@ const { exceptionHandler, errorHandler } = require('../../utils/loggerUtils');
/** /**
* "Enum" for emote type property * "Enum" for emote type property
*/ */
const streamMethodEnum = ["webrtc","hls"]; const streamMethodEnum = ["webrtc","livehls"];
/** /**
* DB Schema for Documents containing de-hydrated representations of Canopy Stream/Chat Channels * DB Schema for Documents containing de-hydrated representations of Canopy Stream/Chat Channels

View file

@ -85,7 +85,7 @@ module.exports.settingsMap = function(){
optional: true, optional: true,
errorMessage: "Invalid Stream Type", errorMessage: "Invalid Stream Type",
matches: { matches: {
options: [/\b(?:webrtc|hls)\b/] options: [/\b(?:webrtc|livehls)\b/]
} }
} }
}) })

View file

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

View file

@ -42,10 +42,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
<label for="channel-preference-list-stream-type-webrtc">Peer-to-Peer (WebRTC/STUN)</label> <label for="channel-preference-list-stream-type-webrtc">Peer-to-Peer (WebRTC/STUN)</label>
</span> </span>
<span> <span>
<input type="radio" name="streamType" id="channel-preference-list-stream-type-hls" value="hls" <% if(channel.settings["streamType"] == "hls"){ %> checked <% }%> > <input type="radio" name="streamType" id="channel-preference-list-stream-type-hls" value="livehls" <% if(channel.settings["streamType"] == "livehls"){ %> checked <% }%> >
<label for="channel-preference-list-stream-type-hls">Self-Hosted (HLS)</label> <label for="channel-preference-list-stream-type-hls">Self-Hosted (HLS)</label>
</span> </span>
<input <% if(channel.settings["streamType"] != "hls"){ %> disabled <%}%> id="channel-preference-list-stream-url" placeholder="Stream URL" value="<%= channel.settings["streamURL"] %>"> <input <% if(channel.settings["streamType"] != "livehls"){ %> disabled <%}%> id="channel-preference-list-stream-url" placeholder="Stream URL" value="<%= channel.settings["streamURL"] %>">
</span> </span>
</span> </span>

View file

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

View file

@ -927,3 +927,73 @@ class hlsLiveStreamHandler extends hlsBase{
} }
} }
} }
/**
* Wrapper handler for WebRTC Streams, calling back to webRTCHandler.js for heavy lifting
* @extends rawFileBase
*/
class WebRTCLivestreamHandler extends rawFileBase{
/**
/**
* Instantiates a new WebRTCMediaHandler
* @param {channel} client - Parent Client Management Object
* @param {player} player - Parent Canopy Player Object
* @param {Object} media - De-hydrated media object from server
*/
constructor(client, player, media){
//Call derived constructor
super(client, player, media, "webrtc");
this.defineListeners();
}
defineListeners(){
//Run derived method
super.defineListeners();
//Disable right clicking
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();
//Lock the player
this.setPlayerLock(true);
}
setVideoTitle(title){
//Add title as text content for security :P
this.player.title.innerText = `: ${utils.unescapeEntities(title)}`;
//Create glow span
const glowSpan = document.createElement('span');
//Fill glow span content
glowSpan.innerText = "🔴LIVE";
//Set glowspan class
glowSpan.classList.add('critical-danger-text');
//Inject glowspan into title in a way that allows it to be easily replaced
this.player.title.prepend(glowSpan);
}
}

View file

@ -188,7 +188,7 @@ class queuePanel extends panelObj{
*/ */
handleStart(data){ handleStart(data){
//If we're starting an HLS Livestream //If we're starting an HLS Livestream
if(data.media != null && data.media.type == 'livehls'){ if(data.media != null && (data.media.type == 'livehls' || data.media.type == 'webrtc' )){
//Hide the 'goLive' controls //Hide the 'goLive' controls
this.goLiveDiv.style.display = 'none'; this.goLiveDiv.style.display = 'none';
} }
@ -282,7 +282,7 @@ class queuePanel extends panelObj{
client.socket.emit('getStreamType'); client.socket.emit('getStreamType');
//If we're not livestreaming //If we're not livestreaming
if(client.player.mediaHandler.type != "livehls"){ if(client.player.mediaHandler.type != "livehls" && client.player.mediaHandler.type != "webrtc"){
//If the div is hidden //If the div is hidden
if(this.goLiveDiv.style.display == 'none'){ if(this.goLiveDiv.style.display == 'none'){
//Show the div //Show the div
@ -306,7 +306,14 @@ class queuePanel extends panelObj{
* Handles sending request to server to start a live stream * Handles sending request to server to start a live stream
* @param {Event} event - Event passed down from Event Listener * @param {Event} event - Event passed down from Event Listener
*/ */
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']});
} }
@ -1188,7 +1195,7 @@ class queuePanel extends panelObj{
const staleEntry = this.queueContainer.querySelector('.queue-entry.live'); const staleEntry = this.queueContainer.querySelector('.queue-entry.live');
//If we're not livestreaming //If we're not livestreaming
if(client.player.mediaHandler.type != "livehls"){ if(client.player.mediaHandler.type != "livehls" && client.player.mediaHandler.type != "webrtc"){
//If we have a stale entry //If we have a stale entry
if(staleEntry != null){ if(staleEntry != null){
//Remove stale entry since we're no longer streaming //Remove stale entry since we're no longer streaming

View file

@ -188,6 +188,9 @@ class player{
}else if(data.media.type == "livehls"){ }else if(data.media.type == "livehls"){
//Create a new HLS Livestream Handler for it //Create a new HLS Livestream Handler for it
this.mediaHandler = new hlsLiveStreamHandler(this.client, this, data.media); this.mediaHandler = new hlsLiveStreamHandler(this.client, this, data.media);
}else if(data.media.type == "webrtc"){
//Create a new HLS Livestream Handler for it
this.mediaHandler = new WebRTCLivestreamHandler(this.client, this, data.media);
}else if(data.media.type == 'dm'){ }else if(data.media.type == 'dm'){
this.mediaHandler = new hlsDailymotionHandler(this.client, this, data.media); this.mediaHandler = new hlsDailymotionHandler(this.client, this, data.media);
//Otherwise, if we have a raw-file compatible source //Otherwise, if we have a raw-file compatible source

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);
}
}

View file

@ -412,7 +412,7 @@ class livestreamPreferences{
handleTypeUpdate(data){ handleTypeUpdate(data){
//If the stream type is HLS //If the stream type is HLS
if(data.streamType == "hls"){ if(data.streamType == "livehls" || data.streamType == "webrtc"){
//Ensure the hls button is checked //Ensure the hls button is checked
this.hlsButton.checked = true; this.hlsButton.checked = true;
//Ensure the stream url input is enabled //Ensure the stream url input is enabled