Files
Remote-Gameplay/files/static/main.js
2023-03-10 17:54:22 +00:00

661 lines
23 KiB
JavaScript

"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 = '<b>RoomID:</b> #' + 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');
const barrierip = u.searchParams.get('barrierip');
if (barrierip && barrierip.length > 0 && paramId && paramId.length > 0) {
return doJoin(paramId,barrierip);
}
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();