From 03c7aaccf1f35fdc8273b18d652598c844c5c0ec Mon Sep 17 00:00:00 2001 From: a-wing <1@233.email> Date: Sun, 24 Dec 2023 14:49:34 +0800 Subject: [PATCH] refactor(webapp): add npm @binbat/whip-whep remove lib/{whip.js,whep.js} --- package-lock.json | 6 + package.json | 1 + webapp/components/player/whep-player.tsx | 2 +- webapp/components/player/whip-player.tsx | 2 +- webapp/lib/whep.js | 469 ----------------------- webapp/lib/whip.js | 374 ------------------ 6 files changed, 9 insertions(+), 845 deletions(-) delete mode 100644 webapp/lib/whep.js delete mode 100644 webapp/lib/whip.js diff --git a/package-lock.json b/package-lock.json index 110b544..502484a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "vite-react", "version": "0.0.0", "dependencies": { + "@binbat/whip-whep": "^1.0.8", "@types/lodash.uniq": "^4.5.7", "copy-to-clipboard": "^3.3.3", "jotai": "^2.4.3", @@ -399,6 +400,11 @@ "node": ">=6.9.0" } }, + "node_modules/@binbat/whip-whep": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@binbat/whip-whep/-/whip-whep-1.0.8.tgz", + "integrity": "sha512-WRwInDdhC/v1uqsvuEwiqxkXitZrZPp8QzZLnwtatPdfpWLxocXgFIIzQpLyzW20Sf4UBhovTSWwCNRgSQNH5g==" + }, "node_modules/@emotion/babel-plugin": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", diff --git a/package.json b/package.json index 905e59f..3882a1c 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "preview": "vite preview" }, "dependencies": { + "@binbat/whip-whep": "^1.0.8", "@types/lodash.uniq": "^4.5.7", "copy-to-clipboard": "^3.3.3", "jotai": "^2.4.3", diff --git a/webapp/components/player/whep-player.tsx b/webapp/components/player/whep-player.tsx index 5dde3a5..f5e3695 100644 --- a/webapp/components/player/whep-player.tsx +++ b/webapp/components/player/whep-player.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react' import Player from './player' import { UserStream, UserStatus } from '../../store/atom' -import WHEPClient from '../../lib/whep' +import { WHEPClient } from '@binbat/whip-whep/whep' export default function WhepPlayer(props: { streamId: string, status: UserStatus, width: string }) { const refEnabled = useRef(false) diff --git a/webapp/components/player/whip-player.tsx b/webapp/components/player/whip-player.tsx index b7de43f..23f70b1 100644 --- a/webapp/components/player/whip-player.tsx +++ b/webapp/components/player/whip-player.tsx @@ -7,7 +7,7 @@ import { currentDeviceVideoAtom, } from '../../store/atom' import Player from './player' -import WHIPClient from '../../lib/whip' +import { WHIPClient } from '@binbat/whip-whep/whip' export default function WhipPlayer(props: { streamId: string, width: string }) { const refEnabled = useRef(false) diff --git a/webapp/lib/whep.js b/webapp/lib/whep.js deleted file mode 100644 index 9f5b85f..0000000 --- a/webapp/lib/whep.js +++ /dev/null @@ -1,469 +0,0 @@ -const Extensions = { - Core: { - ServerSentEvents: "urn:ietf:params:whep:ext:core:server-sent-events", - Layer: "urn:ietf:params:whep:ext:core:layer", - } -} - - -export default class WHEPClient extends EventTarget { - constructor() { - super(); - //Ice properties - this.iceUsername = null; - this.icePassword = null; - //Pending candidadtes - this.candidates = []; - this.endOfcandidates = false; - this.id = ""; - } - - async view(pc, url, token) { - //If already publishing - if (this.pc) - throw new Error("Already viewing") - - //Store pc object and token - this.token = token; - this.pc = pc; - - //Listen for state change events - //pc.onconnectionstatechange = (event) => { - // switch (pc.connectionState) { - // case "connected": - // // The connection has become fully connected - // break; - // case "disconnected": - // case "failed": - // // One or more transports has terminated unexpectedly or in an error - // break; - // case "closed": - // // The connection has been closed - // break; - // } - //} - - //Listen for candidates - pc.onicecandidate = (event) => { - - if (event.candidate) { - //Ignore candidates not from the first m line - if (event.candidate.sdpMLineIndex > 0) - //Skip - return; - //Store candidate - this.candidates.push(event.candidate); - } else { - //No more candidates - this.endOfcandidates = true; - } - //Schedule trickle on next tick - if (!this.iceTrickeTimeout) - this.iceTrickeTimeout = setTimeout(() => this.trickle(), 0); - } - //Create SDP offer - const offer = await pc.createOffer(); - - //Request headers - const headers = { - "Content-Type": "application/sdp" - }; - - //If token is set - if (token) - headers["Authorization"] = "Bearer " + token; - - //Do the post request to the WHEP endpoint with the SDP offer - const fetched = await fetch(url, { - method: "POST", - body: offer.sdp, - headers - }); - - if (!fetched.ok) - throw new Error("Request rejected with status " + fetched.status) - if (!fetched.headers.get("location")) - throw new Error("Response missing location header") - - //Get the resource url - this.resourceURL = new URL(fetched.headers.get("location"), url); - - //Get the links - const links = {}; - - //If the response contained any - if (fetched.headers.has("link")) { - //Get all links headers - const linkHeaders = fetched.headers.get("link").split(/,\s+(?=<)/) - - //For each one - for (const header of linkHeaders) { - try { - let rel, params = {}; - //Split in parts - const items = header.split(";"); - //Create url server - const url = items[0].trim().replace(/<(.*)>/, "$1").trim(); - //For each other item - for (let i = 1; i < items.length; ++i) { - //Split into key/val - const subitems = items[i].split(/=(.*)/); - //Get key - const key = subitems[0].trim(); - //Unquote value - const value = subitems[1] - ? subitems[1] - .trim() - .replaceAll('"', '') - .replaceAll("'", "") - : subitems[1]; - //Check if it is the rel attribute - if (key == "rel") - //Get rel value - rel = value; - else - //Unquote value and set them - params[key] = value - } - //Ensure it is an ice server - if (!rel) - continue; - if (!links[rel]) - links[rel] = []; - //Add to config - links[rel].push({ url, params }); - } catch (e) { - console.error(e) - } - } - } - - //Get extensions url - if (links.hasOwnProperty(Extensions.Core.ServerSentEvents)) - //Get url - this.eventsUrl = new URL(links[Extensions.Core.ServerSentEvents][0].url, url); - if (links.hasOwnProperty(Extensions.Core.Layer)) - this.layerUrl = new URL(links[Extensions.Core.Layer][0].url, url); - - //If we have an event url - if (this.eventsUrl) { - //Get supported events - const events = links[Extensions.Core.ServerSentEvents]["events"] - ? links[Extensions.Core.ServerSentEvents]["events"].split(" ") - : ["active", "inactive", "layers", "viewercount"]; - //Request headers - const headers = { - "Content-Type": "application/json" - }; - - //If token is set - if (this.token) - headers["Authorization"] = "Bearer " + this.token; - - //Do the post request to the whep resource - fetch(this.eventsUrl, { - method: "POST", - body: JSON.stringify(events), - headers - }).then((fetched) => { - //If the event channel could be created - if (!fetched.ok) - return; - //Get the resource url - const sseUrl = new URL(fetched.headers.get("location"), this.eventsUrl); - //Open it - this.eventSource = new EventSource(sseUrl); - this.eventSource.onopen = (event) => console.log(event); - this.eventSource.onerror = (event) => console.log(event); - //Listen for events - this.eventSource.onmessage = (event) => { - console.dir(event); - this.dispatchEvent(event); - }; - }); - } - - //Get current config - const config = pc.getConfiguration(); - - //If it has ice server info and it is not overriden by the client - if ((!config.iceServer || !config.iceServer.length) && links.hasOwnProperty("ice-server")) { - //ICe server config - config.iceServers = []; - - //For each one - for (const server of links["ice-server"]) { - try { - //Create ice server - const iceServer = { - urls: server.url - } - //For each other param - for (const [key, value] of Object.entries(server.params)) { - //Get key in cammel case - const cammelCase = key.replace(/([-_][a-z])/ig, $1 => $1.toUpperCase().replace('-', '').replace('_', '')) - //Unquote value and set them - iceServer[cammelCase] = value; - } - //Add to config - //config.iceServers.push(iceServer); - } catch (e) { - } - } - - //If any configured - if (config.iceServers.length) - //Set it - pc.setConfiguration(config); - } - - //Get the SDP answer - this.id = fetched.headers.get("E-tag") - const answer = await fetched.text(); - - //Schedule trickle on next tick - if (!this.iceTrickeTimeout) - this.iceTrickeTimeout = setTimeout(() => this.trickle(), 0); - - //Set local description - await pc.setLocalDescription(offer); - - // TODO: chrome is returning a wrong value, so don't use it for now - //try { - // //Get local ice properties - // const local = this.pc.getTransceivers()[0].sender.transport.iceTransport.getLocalParameters(); - // //Get them for transport - // this.iceUsername = local.usernameFragment; - // this.icePassword = local.password; - //} catch (e) { - //Fallback for browsers not supporting ice transport - this.iceUsername = offer.sdp.match(/a=ice-ufrag:(.*)\r\n/)[1]; - this.icePassword = offer.sdp.match(/a=ice-pwd:(.*)\r\n/)[1]; - //} - - //And set remote description - await pc.setRemoteDescription({ type: "answer", sdp: answer }); - } - - restart() { - //Set restart flag - this.restartIce = true; - - //Schedule trickle on next tick - if (!this.iceTrickeTimeout) - this.iceTrickeTimeout = setTimeout(() => this.trickle(), 0); - } - - async trickle() { - if (!this.id) { - return - } - //Clear timeout - this.iceTrickeTimeout = null; - - //Check if there is any pending data - if (!(this.candidates.length || this.endOfcandidates || this.restartIce) || !this.resourceURL) - //Do nothing - return; - - //Get data - const candidates = this.candidates; - let endOfcandidates = this.endOfcandidates; - const restartIce = this.restartIce; - - //Clean pending data before async operation - this.candidates = []; - this.endOfcandidates = false; - this.restartIce = false; - - //If we need to restart - if (restartIce) { - //Restart ice - this.pc.restartIce(); - //Create a new offer - const offer = await this.pc.createOffer({ iceRestart: true }); - //Update ice - this.iceUsername = offer.sdp.match(/a=ice-ufrag:(.*)\r\n/)[1]; - this.icePassword = offer.sdp.match(/a=ice-pwd:(.*)\r\n/)[1]; - //Set it - await this.pc.setLocalDescription(offer); - //Clean end of candidates flag as new ones will be retrieved - endOfcandidates = false; - } - //Prepare fragment - let fragment = - "a=ice-ufrag:" + this.iceUsername + "\r\n" + - "a=ice-pwd:" + this.icePassword + "\r\n"; - //Get peerconnection transceivers - const transceivers = this.pc.getTransceivers(); - //Get medias - const medias = {}; - //If doing something else than a restart - if (candidates.length || endOfcandidates) - //Create media object for first media always - medias[transceivers[0].mid] = { - mid: transceivers[0].mid, - kind: transceivers[0].receiver.track.kind, - candidates: [], - }; - //For each candidate - for (const candidate of candidates) { - //Get mid for candidate - const mid = candidate.sdpMid - //Get associated transceiver - const transceiver = transceivers.find(t => t.mid == mid); - //Get media - let media = medias[mid]; - //If not found yet - if (!media) - //Create media object - media = medias[mid] = { - mid, - kind: transceiver.receiver.track.kind, - candidates: [], - }; - //Add candidate - media.candidates.push(candidate); - } - //For each media - for (const media of Object.values(medias)) { - //Add media to fragment - fragment += - "m=" + media.kind + " 9 RTP/AVP 0\r\n" + - "a=mid:" + media.mid + "\r\n"; - //Add candidate - for (const candidate of media.candidates) - fragment += "a=" + candidate.candidate + "\r\n"; - if (endOfcandidates) - fragment += "a=end-of-candidates\r\n"; - } - - //Request headers - const headers = { - "Content-Type": "application/trickle-ice-sdpfrag" - }; - headers["If-Match"] = this.id - //If token is set - if (this.token) - headers["Authorization"] = "Bearer " + this.token; - - //Do the post request to the WHEP resource - const fetched = await fetch(this.resourceURL, { - method: "PATCH", - body: fragment, - headers - }); - if (!fetched.ok) - throw new Error("Request rejected with status " + fetched.status) - - //If we have got an answer - if (fetched.status == 200) { - //Get the SDP answer - const answer = await fetched.text(); - //Get remote icename and password - const iceUsername = answer.match(/a=ice-ufrag:(.*)\r\n/)[1]; - const icePassword = answer.match(/a=ice-pwd:(.*)\r\n/)[1]; - - //Get current remote rescription - const remoteDescription = this.pc.remoteDescription; - - //Patch - remoteDescription.sdp = remoteDescription.sdp.replaceAll(/(a=ice-ufrag:)(.*)\r\n/gm, "$1" + iceUsername + "\r\n"); - remoteDescription.sdp = remoteDescription.sdp.replaceAll(/(a=ice-pwd:)(.*)\r\n/gm, "$1" + icePassword + "\r\n"); - - //Set it - await this.pc.setRemoteDescription(remoteDescription); - } - } - - async mute(muted) { - //Request headers - const headers = { - "Content-Type": "application/json" - }; - - //If token is set - if (this.token) - headers["Authorization"] = "Bearer " + this.token; - - //Do the post request to the whep resource - const fetched = await fetch(this.resourceURL, { - method: "POST", - body: JSON.stringify(muted), - headers - }); - } - - async selectLayer(layer) { - if (!this.layerUrl) - throw new Error("whep resource does not support layer selection"); - - //Request headers - const headers = { - "Content-Type": "application/json" - }; - - //If token is set - if (this.token) - headers["Authorization"] = "Bearer " + this.token; - - //Do the post request to the whep resource - const fetched = await fetch(this.layerUrl, { - method: "POST", - body: JSON.stringify(layer), - headers - }); - } - - async unselectLayer() { - if (!this.layerUrl) - throw new Error("whep resource does not support layer selection"); - - - //Request headers - const headers = {}; - - //If token is set - if (this.token) - headers["Authorization"] = "Bearer " + this.token; - - //Do the post request to the whep resource - const fetched = await fetch(this.layerUrl, { - method: "DELETE", - headers - }); - } - - async stop() { - if (!this.pc) { - // Already stopped - return - } - - //Cancel any pending timeout - this.iceTrickeTimeout = clearTimeout(this.iceTrickeTimeout); - - //Close peerconnection - this.pc.close(); - - //Null - this.pc = null; - - //If we don't have the resource url - if (!this.resourceURL) - throw new Error("WHEP resource url not available yet"); - - //Request headers - const headers = {}; - - //If token is set - if (this.token) - headers["Authorization"] = "Bearer " + this.token; - - //Send a delete - await fetch(this.resourceURL, { - method: "DELETE", - headers - }); - } -}; diff --git a/webapp/lib/whip.js b/webapp/lib/whip.js deleted file mode 100644 index bbfc164..0000000 --- a/webapp/lib/whip.js +++ /dev/null @@ -1,374 +0,0 @@ - -export default class WHIPClient { - constructor() { - //Ice properties - this.iceUsername = null; - this.icePassword = null; - //Pending candidadtes - this.candidates = []; - this.endOfcandidates = false; - this.id = "" - } - - async publish(pc, url, token) { - //If already publishing - if (this.pc) - throw new Error("Already publishing") - - //Store pc object and token - this.token = token; - this.pc = pc; - - //Listen for state change events - //pc.onconnectionstatechange = (event) => { - // switch (pc.connectionState) { - // case "connected": - // // The connection has become fully connected - // break; - // case "disconnected": - // case "failed": - // // One or more transports has terminated unexpectedly or in an error - // break; - // case "closed": - // // The connection has been closed - // break; - // } - //} - - //Listen for candidates - pc.onicecandidate = (event) => { - - if (event.candidate) { - //Ignore candidates not from the first m line - if (event.candidate.sdpMLineIndex > 0) - //Skip - return; - //Store candidate - this.candidates.push(event.candidate); - } else { - //No more candidates - this.endOfcandidates = true; - } - //Schedule trickle on next tick - if (!this.iceTrickeTimeout) - this.iceTrickeTimeout = setTimeout(() => this.trickle(), 0); - } - //Create SDP offer - const offer = await pc.createOffer(); - - //Request headers - const headers = { - "Content-Type": "application/sdp" - }; - - //If token is set - if (token) - headers["Authorization"] = "Bearer " + token; - - //Do the post request to the WHIP endpoint with the SDP offer - const fetched = await fetch(url, { - method: "POST", - body: offer.sdp, - headers - }); - - if (!fetched.ok) - throw new Error("Request rejected with status " + fetched.status) - if (!fetched.headers.get("location")) - throw new Error("Response missing location header") - - //Get the resource url - this.resourceURL = new URL(fetched.headers.get("location"), url); - - //Get the links - const links = {}; - - //If the response contained any - if (fetched.headers.has("link")) { - //Get all links headers - const linkHeaders = fetched.headers.get("link").split(/,\s+(?=<)/) - - //For each one - for (const header of linkHeaders) { - try { - let rel, params = {}; - //Split in parts - const items = header.split(";"); - //Create url server - const url = items[0].trim().replace(/<(.*)>/, "$1").trim(); - //For each other item - for (let i = 1; i < items.length; ++i) { - //Split into key/val - const subitems = items[i].split(/=(.*)/); - //Get key - const key = subitems[0].trim(); - //Unquote value - const value = subitems[1] - ? subitems[1] - .trim() - .replaceAll('"', '') - .replaceAll("'", "") - : subitems[1]; - //Check if it is the rel attribute - if (key == "rel") - //Get rel value - rel = value; - else - //Unquote value and set them - params[key] = value - } - //Ensure it is an ice server - if (!rel) - continue; - if (!links[rel]) - links[rel] = []; - //Add to config - links[rel].push({ url, params }); - } catch (e) { - console.error(e) - } - } - } - - //Get current config - const config = pc.getConfiguration(); - - //If it has ice server info and it is not overriden by the client - if ((!config.iceServer || !config.iceServer.length) && links.hasOwnProperty("ice-server")) { - //ICe server config - config.iceServers = []; - - //For each one - for (const server of links["ice-server"]) { - try { - //Create ice server - const iceServer = { - urls: server.url - } - //For each other param - for (const [key, value] of Object.entries(server.params)) { - //Get key in cammel case - const cammelCase = key.replace(/([-_][a-z])/ig, $1 => $1.toUpperCase().replace('-', '').replace('_', '')) - //Unquote value and set them - iceServer[cammelCase] = value; - } - //Add to config - config.iceServers.push(iceServer); - } catch (e) { - } - } - - //If any configured - if (config.iceServers.length) - //Set it - pc.setConfiguration(config); - } - - //Get the SDP answer - const answer = await fetched.text(); - this.id = fetched.headers.get("E-tag") - //Schedule trickle on next tick - if (!this.iceTrickeTimeout) - this.iceTrickeTimeout = setTimeout(() => this.trickle(), 0); - - //Set local description - await pc.setLocalDescription(offer); - - // TODO: chrome is returning a wrong value, so don't use it for now - //try { - // //Get local ice properties - // const local = this.pc.getTransceivers()[0].sender.transport.iceTransport.getLocalParameters(); - // //Get them for transport - // this.iceUsername = local.usernameFragment; - // this.icePassword = local.password; - //} catch (e) { - //Fallback for browsers not supporting ice transport - this.iceUsername = offer.sdp.match(/a=ice-ufrag:(.*)\r\n/)[1]; - this.icePassword = offer.sdp.match(/a=ice-pwd:(.*)\r\n/)[1]; - //} - - //And set remote description - await pc.setRemoteDescription({ type: "answer", sdp: answer }); - } - - restart() { - //Set restart flag - this.restartIce = true; - - //Schedule trickle on next tick - if (!this.iceTrickeTimeout) - this.iceTrickeTimeout = setTimeout(() => this.trickle(), 0); - } - - async trickle() { - if (!this.id) { - return; - } - //Clear timeout - this.iceTrickeTimeout = null; - - //Check if there is any pending data - if (!(this.candidates.length || this.endOfcandidates || this.restartIce) || !this.resourceURL) - //Do nothing - return; - - //Get data - const candidates = this.candidates; - let endOfcandidates = this.endOfcandidates; - const restartIce = this.restartIce; - - //Clean pending data before async operation - this.candidates = []; - this.endOfcandidates = false; - this.restartIce = false; - - //If we need to restart - if (restartIce) { - //Restart ice - this.pc.restartIce(); - //Create a new offer - const offer = await this.pc.createOffer({ iceRestart: true }); - //Update ice - this.iceUsername = offer.sdp.match(/a=ice-ufrag:(.*)\r\n/)[1]; - this.icePassword = offer.sdp.match(/a=ice-pwd:(.*)\r\n/)[1]; - //Set it - await this.pc.setLocalDescription(offer); - //Clean end of candidates flag as new ones will be retrieved - endOfcandidates = false; - } - //Prepare fragment - let fragment = - "a=ice-ufrag:" + this.iceUsername + "\r\n" + - "a=ice-pwd:" + this.icePassword + "\r\n"; - //Get peerconnection transceivers - const transceivers = this.pc.getTransceivers(); - //Get medias - const medias = {}; - //If doing something else than a restart - if (candidates.length || endOfcandidates) - //Create media object for first media always - medias[transceivers[0].mid] = { - mid: transceivers[0].mid, - kind: transceivers[0].receiver.track.kind, - candidates: [], - }; - //For each candidate - for (const candidate of candidates) { - //Get mid for candidate - const mid = candidate.sdpMid - //Get associated transceiver - const transceiver = transceivers.find(t => t.mid == mid); - //Get media - let media = medias[mid]; - //If not found yet - if (!media) - //Create media object - media = medias[mid] = { - mid, - kind: transceiver.receiver.track.kind, - candidates: [], - }; - //Add candidate - media.candidates.push(candidate); - } - //For each media - for (const media of Object.values(medias)) { - //Add media to fragment - fragment += - "m=" + media.kind + " 9 RTP/AVP 0\r\n" + - "a=mid:" + media.mid + "\r\n"; - //Add candidate - for (const candidate of media.candidates) - fragment += "a=" + candidate.candidate + "\r\n"; - if (endOfcandidates) - fragment += "a=end-of-candidates\r\n"; - } - - //Request headers - const headers = { - "Content-Type": "application/trickle-ice-sdpfrag" - }; - //If token is set - if (this.token) - headers["Authorization"] = "Bearer " + this.token; - //Do the post request to the WHIP resource - headers["If-Match"] = this.id - const fetched = await fetch(this.resourceURL, { - method: "PATCH", - body: fragment, - headers - }); - if (!fetched.ok) - throw new Error("Request rejected with status " + fetched.status) - - //If we have got an answer - if (fetched.status == 200) { - //Get the SDP answer - const answer = await fetched.text(); - //Get remote icename and password - const iceUsername = answer.match(/a=ice-ufrag:(.*)\r\n/)[1]; - const icePassword = answer.match(/a=ice-pwd:(.*)\r\n/)[1]; - - //Get current remote rescription - const remoteDescription = this.pc.remoteDescription; - - //Patch - remoteDescription.sdp = remoteDescription.sdp.replaceAll(/(a=ice-ufrag:)(.*)\r\n/gm, "$1" + iceUsername + "\r\n"); - remoteDescription.sdp = remoteDescription.sdp.replaceAll(/(a=ice-pwd:)(.*)\r\n/gm, "$1" + icePassword + "\r\n"); - - //Set it - await this.pc.setRemoteDescription(remoteDescription); - } - } - - async mute(muted) { - //Request headers - const headers = { - "Content-Type": "application/json" - }; - - //If token is set - if (this.token) - headers["Authorization"] = "Bearer " + this.token; - - //Do the post request to the WHIP resource - const fetched = await fetch(this.resourceURL, { - method: "POST", - body: JSON.stringify(muted), - headers - }); - } - - async stop() { - if (!this.pc) { - // Already stopped - return - } - - //Cancel any pending timeout - this.iceTrickeTimeout = clearTimeout(this.iceTrickeTimeout); - - //Close peerconnection - this.pc.close(); - - //Null - this.pc = null; - - //If we don't have the resource url - if (!this.resourceURL) - throw new Error("WHIP resource url not available yet"); - - //Request headers - const headers = {}; - - //If token is set - if (this.token) - headers["Authorization"] = "Bearer " + this.token; - - //Send a delete - await fetch(this.resourceURL, { - method: "DELETE", - headers - }); - } -};