"use strict"; // todo: deprecate const iceConfig = { iceServers: [{ urls: [ 'stun:stun1.l.google.com:19302', 'stun:stun2.l.google.com:19302', ], }], iceCandidatePoolSize: 10, }; const displayMediaOptions = { noConstraint: { video: { height: 1080, frameRate: 120, }, audio: true, }, v720p30: { video: { height: 1080, frameRate: 120, }, audio: true, }, v480p60: { video: { height: 480, frameRate: 60, }, audio: true, }, }; // const rtpPeerConnectionOptions = { // stunGoogle: { // iceServers: [{ // urls: [ // 'stun:stun1.l.google.com:19302', // 'stun:stun2.l.google.com:19302', // ], // }], // iceCandidatePoolSize: 10, // }, // noStun: { // iceServers: [], // } // }; const rtpPeerConnectionOptions = { stunGoogle: { iceServers: [ { urls: "stun:relay.metered.ca:80", }, { urls: "turn:relay.metered.ca:80", username: "f0f0d71ba61e44b74c0e4e9e", credential: "gKLJ6NUO0ZqsVN1D", }, { urls: "turn:relay.metered.ca:443", username: "f0f0d71ba61e44b74c0e4e9e", credential: "gKLJ6NUO0ZqsVN1D", }, { urls: "turn:relay.metered.ca:443?transport=tcp", username: "f0f0d71ba61e44b74c0e4e9e", credential: "gKLJ6NUO0ZqsVN1D", }, ], }, noStun: { iceServers: [], } } const preset = { balanced: { displayMediaOption: displayMediaOptions.v720p30, rtpPeerConnectionOption: rtpPeerConnectionOptions.stunGoogle, }, performance: { displayMediaOption: displayMediaOptions.v480p60, rtpPeerConnectionOption: rtpPeerConnectionOptions.stunGoogle, }, highQuality: { displayMediaOption: displayMediaOptions.noConstraint, rtpPeerConnectionOption: rtpPeerConnectionOptions.stunGoogle, }, balancedLanOnly: { displayMediaOption: displayMediaOptions.v720p30, rtpPeerConnectionOption: rtpPeerConnectionOptions.noStun, }, performanceLanOnly: { displayMediaOption: displayMediaOptions.v480p60, rtpPeerConnectionOption: rtpPeerConnectionOptions.noStun, }, highQualityLanOnly: { displayMediaOption: displayMediaOptions.noConstraint, rtpPeerConnectionOption: rtpPeerConnectionOptions.noStun, }, }; const LaplaceVar = { ui: {}, }; function avg(arr) { return (arr.reduce((a, b) => a + b, 0) / arr.length) | 0 } function print(s) { LaplaceVar.ui.output.innerHTML += s + '\n'; } function getBaseUrl() { return `${window.location.protocol}//${window.location.host}` } function getStreamUrl() { return `${getBaseUrl()}/?stream=1`; } function getJoinUrl(roomID,BarrierIP="") { if(BarrierIP == ""){ return `${getBaseUrl()}/?id=${roomID}`; } return `${getBaseUrl()}/?id=${roomID}&barrierip=${BarrierIP}`; } function updateRoomUI() { LaplaceVar.ui.panel.style.display = 'none'; LaplaceVar.ui.videoContainer.style.display = 'block'; LaplaceVar.ui.streamPageUI.style.display = 'block'; if (LaplaceVar.roomID) { const joinUrl = getJoinUrl(LaplaceVar.roomID); LaplaceVar.ui.qrcodeObj = new QRCode(LaplaceVar.ui.qrcode, { text: joinUrl, width: 128, height: 128, }); LaplaceVar.ui.roomText.innerHTML = 'RoomID: #' + LaplaceVar.roomID; LaplaceVar.ui.joinLinkText.innerHTML = joinUrl; LaplaceVar.ui.joinLinkText.href = joinUrl; } } function initUI() { LaplaceVar.ui.btnStream = document.getElementById('btnStream'); LaplaceVar.ui.btnStartStream = document.getElementById('btnStartStream'); LaplaceVar.ui.inputRoomID = document.getElementById('inputRoomID'); LaplaceVar.ui.inputDisplayMediaOption = document.getElementById('inputDisplayMediaOption'); LaplaceVar.ui.inputRTPPeerConnectionOption = document.getElementById('inputRTPPeerConnectionOption'); LaplaceVar.ui.joinLinkText = document.getElementById("join-link"); LaplaceVar.ui.joinForm = document.getElementById('joinForm'); LaplaceVar.ui.output = document.getElementById('output'); LaplaceVar.ui.qrcode = document.getElementById("qrcode"); LaplaceVar.ui.panel = document.getElementById('panel'); LaplaceVar.ui.roomText = document.getElementById('room-text'); LaplaceVar.ui.statusNumConn = document.getElementById('statusNumConn'); LaplaceVar.ui.statusPeers = document.getElementById('statusPeers'); LaplaceVar.ui.statusPing = document.getElementById('statusPing'); LaplaceVar.ui.selectOptionPreset = document.getElementById('inputOptionPreset'); LaplaceVar.ui.streamPageUI = document.getElementById('stream-page-ui'); LaplaceVar.ui.streamServePageUI = document.getElementById('stream-serve-page-ui'); LaplaceVar.ui.video = document.getElementById('mainVideo'); LaplaceVar.ui.videoContainer = document.getElementById('video-container'); LaplaceVar.ui.barrierIP = document.getElementById('barrierIP'); LaplaceVar.ui.barrierhostname = document.getElementById('hostname'); LaplaceVar.ui.serverUsername = document.getElementById('Username'); LaplaceVar.ui.serverIP = document.getElementById('IP'); LaplaceVar.ui.SSHPassword = document.getElementById('SSHPassword') LaplaceVar.headless = { result :"0"}; //getting server hostname getServerIPandHostname() LaplaceVar.ui.joinForm.onsubmit = async e => { e.preventDefault(); LaplaceVar.roomID = LaplaceVar.ui.inputRoomID.value; LaplaceVar.barrierIP = LaplaceVar.ui.barrierIP.value; // Check if the ip address for barrier is given if(LaplaceVar.barrierIP == "") { window.history.pushState('', '', getJoinUrl(LaplaceVar.roomID)); } else { window.history.pushState('', '', getJoinUrl(LaplaceVar.roomID,LaplaceVar.barrierIP)); } await doJoin(LaplaceVar.roomID,LaplaceVar.barrierIP); }; LaplaceVar.ui.btnStream.onclick = async () => { window.history.pushState('', '', getStreamUrl()); await doStream(); }; LaplaceVar.ui.btnStartStream.onclick = () => { LaplaceVar.ui.streamServePageUI.style.display = 'none'; const mediaOption = JSON.parse(LaplaceVar.ui.inputDisplayMediaOption.value); const pcOption = JSON.parse(LaplaceVar.ui.inputRTPPeerConnectionOption.value); return startStream(mediaOption, pcOption); }; for (const presetName of Object.keys(preset)) { const optionElement = document.createElement('option'); optionElement.appendChild(document.createTextNode(presetName)); optionElement.value = presetName; LaplaceVar.ui.selectOptionPreset.appendChild(optionElement); } LaplaceVar.ui.selectOptionPreset.onchange = () => { const v = LaplaceVar.ui.selectOptionPreset.value; if (preset[v] != null) { LaplaceVar.ui.inputDisplayMediaOption.value = JSON.stringify(preset[v].displayMediaOption, null, 1); LaplaceVar.ui.inputRTPPeerConnectionOption.value = JSON.stringify(preset[v].rtpPeerConnectionOption, null, 1); } }; const defaultPresetValue = Object.keys(preset)[0]; LaplaceVar.ui.inputDisplayMediaOption.value = JSON.stringify(preset[defaultPresetValue].displayMediaOption, null, 1); LaplaceVar.ui.inputRTPPeerConnectionOption.value = JSON.stringify(preset[defaultPresetValue].rtpPeerConnectionOption, null, 1); LaplaceVar.headless.onactivate = () => { LaplaceVar.ui.streamServePageUI.style.display = 'none'; const defaultPresetValue = Object.keys(preset)[0]; const mediaOption = JSON.parse(LaplaceVar.ui.inputDisplayMediaOption.value); const pcOption = JSON.parse(LaplaceVar.ui.inputRTPPeerConnectionOption.value); return startStream(mediaOption, pcOption); } //getting SSHPassword getSSHPassword() // Getting query string from the URL const queryString = window.location.search; // Getting url parameters from the URL const urlParams = new URLSearchParams(queryString); // Running headless mode if(urlParams.get('mode') == "headless") { // setTimeout(function() { LaplaceVar.ui.btnStartStream.click() // },2000) } LaplaceVar.barrierIP = urlParams.get('barrierip') print("Logs:"); print("[+] Page loaded"); } // Gets host name of the server // This is so that user can // add the host name to connect // to laplace function getServerIPandHostname() { // Source https://stackoverflow.com/questions/247483/http-get-request-in-javascript var xmlHttp = new XMLHttpRequest() xmlHttp.open( "GET", getHttpUrl() + "/hostname", false ); // false for synchronous request xmlHttp.send(null); //LaplaceVar.ui.barrierhostname.innerHTML = xmlHttp.responseText LaplaceVar.ui.serverUsername.innerHTML = xmlHttp.responseText // Server IP address LaplaceVar.ui.serverIP.innerHTML = window.location.hostname //console.log(window.location.href.split('?')[0]) } function getSSHPassword() { // Source https://stackoverflow.com/questions/247483/http-get-request-in-javascript var xmlHttp = new XMLHttpRequest() xmlHttp.open( "GET", getHttpUrl() + "/SSHPassword", false ); // false for synchronous request xmlHttp.send(null); LaplaceVar.ui.SSHPassword.innerHTML = xmlHttp.responseText } function updateStatusUIStream() { LaplaceVar.status.peers = Object.keys(LaplaceVar.pcs).map(s => s.split('$')[1]); LaplaceVar.status.numConn = LaplaceVar.status.peers.length; LaplaceVar.ui.statusPeers.innerHTML = LaplaceVar.status.peers .map(s => `${s} (${LaplaceVar.pings[LaplaceVar.roomID + '$' + s]} ms)`).join(', '); LaplaceVar.ui.statusNumConn.innerHTML = LaplaceVar.status.numConn; } function updateStatusUIJoin() { LaplaceVar.ui.statusNumConn.innerHTML = LaplaceVar.status.numConn; LaplaceVar.ui.statusPeers.innerHTML = LaplaceVar.status.peers.map(s => LaplaceVar.sessionID.endsWith(s) ? s + ' (you)' : s).join(', '); } function getWebsocketUrl() { if (window.location.protocol === "https:") { return `wss://${window.location.host}` } else { return `ws://${window.location.host}` } } function getHttpUrl() { //if (window.location.protocol === "https:") { return window.location.href.split('?')[0] // } else { // return `${window.location.href}` // } } async function newRoom(rID) { print("[+] Get room ID: " + rID); LaplaceVar.roomID = rID; updateRoomUI(); } async function newSessionStream(sessionID, pcOption) { print('[+] New session: ' + sessionID); LaplaceVar.pcs[sessionID] = new RTCPeerConnection(pcOption); LaplaceVar.pcs[sessionID].onicecandidate = e => { print('[+] Debug onicecandidate: ' + JSON.stringify(e.candidate)); if (!e.candidate) { print('[+] Debug onicecandidate: got final candidate!'); return; } print('[+] Send addCallerIceCandidate to websocket: ' + JSON.stringify(e.candidate)); LaplaceVar.socket.send(JSON.stringify({ Type: "addCallerIceCandidate", SessionID: sessionID, Value: JSON.stringify(e.candidate), })) }; LaplaceVar.pcs[sessionID].oniceconnectionstatechange = () => { print('[+] Debug oniceconnectionstatechange ' + LaplaceVar.pcs[sessionID].iceConnectionState); if (LaplaceVar.pcs[sessionID].iceConnectionState === 'disconnected') { print("[-] Disconnected with a Peer " + sessionID); LaplaceVar.pcs[sessionID].close(); delete LaplaceVar.pcs[sessionID]; delete LaplaceVar.dataChannels[sessionID]; delete LaplaceVar.pings[sessionID]; delete LaplaceVar.pingHistories[sessionID]; updateStatusUIStream(); } }; updateStatusUIStream(); LaplaceVar.dataChannels[sessionID] = LaplaceVar.pcs[sessionID].createDataChannel('ping'); LaplaceVar.dataChannels[sessionID].addEventListener('open', () => { print('[+] Start ping interval: ', sessionID); LaplaceVar.pingHistories[sessionID] = []; LaplaceVar.pingIntervals[sessionID] = window.setInterval(() => { const now = new Date().getTime(); LaplaceVar.dataChannels[sessionID].send('ping ' + now.toString()); LaplaceVar.dataChannels[sessionID].send('status ' + JSON.stringify(LaplaceVar.status)); }, 5000); }); LaplaceVar.dataChannels[sessionID].addEventListener('close', () => { print('[+] Clear ping interval: ', sessionID); window.clearInterval(LaplaceVar.pingIntervals[sessionID]); }); LaplaceVar.dataChannels[sessionID].addEventListener('message', e => { if (e.data.startsWith('ping')) { LaplaceVar.dataChannels[sessionID].send('pong' + e.data.slice(4)); } else if (e.data.startsWith('pong')) { const now = new Date().getTime(); const then = parseInt(e.data.slice(4)); if (!isNaN(then)) { LaplaceVar.pingHistories[sessionID].push(now - then); if (LaplaceVar.pingHistories[sessionID].length > 3) { LaplaceVar.pingHistories[sessionID].shift() } LaplaceVar.pings[sessionID] = avg(LaplaceVar.pingHistories[sessionID]); updateStatusUIStream(); } } }); LaplaceVar.mediaStream.getTracks().forEach(track => { LaplaceVar.pcs[sessionID].addTrack(track, LaplaceVar.mediaStream); }); print('[+] Creating offer'); const offer = await LaplaceVar.pcs[sessionID].createOffer({ offerToReceiveAudio: true, offerToReceiveVideo: true, }); await LaplaceVar.pcs[sessionID].setLocalDescription(offer); print('[+] Send offer to websocket: ' + JSON.stringify(offer)); LaplaceVar.socket.send(JSON.stringify({ Type: "gotOffer", SessionID: sessionID, Value: JSON.stringify(offer), })); } async function addCalleeIceCandidate(sessionID, v) { print('[+] Debug addCalleeIceCandidate ' + sessionID + ' ' + JSON.stringify(v)); return LaplaceVar.pcs[sessionID].addIceCandidate(v); } async function gotAnswer(sessionID, v) { print('[+] Debug gotAnswer ' + sessionID + ' ' + JSON.stringify(v)); return LaplaceVar.pcs[sessionID].setRemoteDescription(new RTCSessionDescription(v)); } async function doStream() { LaplaceVar.ui.panel.style.display = 'none'; LaplaceVar.ui.streamServePageUI.style.display = 'block'; } async function startStream(displayMediaOption, pcOption) { LaplaceVar.pcs = {}; // contains RTCPeerConnections LaplaceVar.dataChannels = {}; LaplaceVar.pings = {}; LaplaceVar.pingHistories = {}; LaplaceVar.pingIntervals = {}; LaplaceVar.status = { numConn: 0, peers: [], }; updateRoomUI(); print('[+] Initiate media: capture display media'); try { // noinspection JSUnresolvedFunction LaplaceVar.mediaStream = await navigator.mediaDevices.getDisplayMedia(displayMediaOption); } catch(err) { alert(err); leaveRoom() } LaplaceVar.ui.video.srcObject = LaplaceVar.mediaStream; LaplaceVar.ui.video.muted = true; // prevent duplicate sound played print('[+] Initiate websocket'); LaplaceVar.socket = new WebSocket(getWebsocketUrl() + '/ws_serve'); LaplaceVar.socket.onerror = () => { alert('WebSocket error'); leaveRoom(); }; LaplaceVar.socket.onopen = async function () { print("[+] Connected to websocket"); }; LaplaceVar.socket.onmessage = async function (e) { try { const jsonData = JSON.parse(e.data); if (jsonData.Type !== 'beat') { print("[+] Received websocket message: " + JSON.stringify(e.data)); } if (jsonData.Type === "newRoom") { await newRoom(jsonData.Value); } else if (jsonData.Type === "newSession") { await newSessionStream(jsonData.SessionID, pcOption); } else if (jsonData.Type === "addCalleeIceCandidate") { await addCalleeIceCandidate(jsonData.SessionID, JSON.parse(jsonData.Value)); } else if (jsonData.Type === "gotAnswer") { await gotAnswer(jsonData.SessionID, JSON.parse(jsonData.Value)); } } catch (e) { print("[!] ERROR: " + e); console.error(e); } }; } async function newSessionJoin(sID) { print('[+] New session: ' + sID); LaplaceVar.sessionID = sID; LaplaceVar.pc = new RTCPeerConnection(iceConfig); LaplaceVar.pc.onicecandidate = e => { print('[+] Debug onicecandidate: ' + JSON.stringify(e.candidate)); if (!e.candidate) { print('[+] Debug onicecandidate: got final candidate!'); return; } print('[+] Send addCalleeIceCandidate to websocket: ' + JSON.stringify(e.candidate)); LaplaceVar.socket.send(JSON.stringify({ Type: "addCalleeIceCandidate", SessionID: LaplaceVar.sessionID, Value: JSON.stringify(e.candidate), })) }; LaplaceVar.pc.oniceconnectionstatechange = () => { print('[+] pc.oniceconnectionstatechange ' + LaplaceVar.pc.iceConnectionState); if (LaplaceVar.pc.iceConnectionState === 'disconnected') { print("[-] Disconnected with Peer"); LaplaceVar.pc.close(); LaplaceVar.pc = null; } }; LaplaceVar.pc.ontrack = event => { // print('[+] Debug pc.ontrack: ' + JSON.stringify(event.track)); // /* does not work on safari */ // event.streams[0].getTracks().forEach(track => { // print('[+] Debug addTrack ' + JSON.stringify(track)); // LaplaceVar.mediaStream.addTrack(track) // }); LaplaceVar.mediaStream.addTrack(event.track); LaplaceVar.ui.video.srcObject = LaplaceVar.mediaStream; try { LaplaceVar.ui.video.play(); } catch {} }; LaplaceVar.pc.addEventListener('datachannel', e => { LaplaceVar.dataChannel = e.channel; LaplaceVar.dataChannel.addEventListener('open', () => { print('[+] Start ping interval'); LaplaceVar.pingHistory = []; LaplaceVar.pingInterval = window.setInterval(() => { const now = new Date().getTime(); LaplaceVar.dataChannel.send('ping ' + now.toString()); }, 1000); }); LaplaceVar.dataChannel.addEventListener('close', () => { print('[+] Clear ping interval'); clearInterval(LaplaceVar.pingInterval); }); LaplaceVar.dataChannel.addEventListener('message', e => { if (e.data.startsWith('ping')) { LaplaceVar.dataChannel.send('pong' + e.data.slice(4)); } else if (e.data.startsWith('pong')) { const now = new Date().getTime(); const then = parseInt(e.data.slice(4)); if (!isNaN(then)) { LaplaceVar.pingHistory.push(now - then); if (LaplaceVar.pingHistory.length > 3) { LaplaceVar.pingHistory.shift() } LaplaceVar.ping = avg(LaplaceVar.pingHistory); LaplaceVar.ui.statusPing.innerHTML = LaplaceVar.ping + ' ms'; } } else if (e.data.startsWith('status')) { LaplaceVar.status = JSON.parse(e.data.slice(7)); updateStatusUIJoin(); } }); }); } async function addCallerIceCandidate(sID, v) { print('[+] Debug addCallerIceCandidate ' + sID + ' ' + JSON.stringify(v)); if (LaplaceVar.sessionID !== sID) return; return LaplaceVar.pc.addIceCandidate(v); } async function gotOffer(sID, v) { print('[+] Debug gotOffer ' + sID + ' ' + JSON.stringify(v)); if (LaplaceVar.sessionID !== sID) return; await LaplaceVar.pc.setRemoteDescription(new RTCSessionDescription(v)); print('[+] Create answer'); const answer = await LaplaceVar.pc.createAnswer(); await LaplaceVar.pc.setLocalDescription(answer); print('[+] Send answer to websocket: ' + JSON.stringify(answer)); LaplaceVar.socket.send(JSON.stringify({ Type: "gotAnswer", SessionID: LaplaceVar.sessionID, Value: JSON.stringify(answer), })) } async function doJoin(roomID,BarrierIP = "") { if (roomID) { LaplaceVar.roomID = roomID; } else { return alert('roomID is not provided'); } // normalize roomID starting with # LaplaceVar.roomID = LaplaceVar.roomID.startsWith('#') ? LaplaceVar.roomID.slice(1) : LaplaceVar.roomID; LaplaceVar.status = { numConn: 0, peers: [], }; updateRoomUI(); print('[+] Initiate media: set remote source'); LaplaceVar.mediaStream = new MediaStream(); LaplaceVar.ui.video.srcObject = LaplaceVar.mediaStream; print('[+] Initiate websocket'); if (BarrierIP == "") { LaplaceVar.socket = new WebSocket(getWebsocketUrl() + "/ws_connect?id=" + LaplaceVar.roomID); } else { LaplaceVar.socket = new WebSocket(getWebsocketUrl() + "/ws_connect?id=" + LaplaceVar.roomID + "&barrierip="+BarrierIP); } LaplaceVar.socket.onerror = () => { alert('WebSocket error'); leaveRoom(); }; LaplaceVar.socket.onopen = async function () { print("[+] Connected to websocket"); }; LaplaceVar.socket.onmessage = async function (e) { try { const jsonData = JSON.parse(e.data); if (jsonData.Type !== 'beat') { print("[+] Received websocket message: " + JSON.stringify(e.data)); } if (jsonData.Type === "newSession") { await newSessionJoin(jsonData.SessionID); } else if (jsonData.Type === "addCallerIceCandidate") { await addCallerIceCandidate(jsonData.SessionID, JSON.parse(jsonData.Value)); } else if (jsonData.Type === "gotOffer") { await gotOffer(jsonData.SessionID, JSON.parse(jsonData.Value)); } else if (jsonData.Type === 'roomNotFound') { alert('Room not found'); leaveRoom(); } else if (jsonData.Type === 'roomClosed') { alert('Room closed'); } } catch (e) { print("[!] ERROR: " + e); console.error(e) } }; } function routeByUrl() { const u = new URL(window.location); const paramId = u.searchParams.get('id'); if (paramId && paramId.length > 0) { return doJoin(paramId); } const paramStream = u.searchParams.get('stream'); if (paramStream && paramStream.length > 0) { return doStream(); } // If the room ID is provided const roomId = u.searchParams.get('roomID'); if (roomId && roomId.length > 0) { AddRoomID(roomId); } } // Adds the room information to the ID inputRoomID function AddRoomID(roomID) { document.getElementById('inputRoomID').value = roomID } function leaveRoom() { window.location.href = getBaseUrl(); } initUI(); routeByUrl();