Compare commits

...

1 Commits

View File

@ -14,7 +14,7 @@ body.justVideo {
right: 0; right: 0;
float: right; float: right;
} }
#qrcodelink img { #qrcodelink svg path {
min-width: 30vw; min-width: 30vw;
max-width: 100vw; max-width: 100vw;
max-height: 50vh; max-height: 50vh;
@ -42,6 +42,9 @@ form label {
#status { #status {
display: table; display: table;
} }
body.justVideo #status {
display: none;
}
#status .log-entry { #status .log-entry {
display: block; display: block;
background: lightblue; background: lightblue;
@ -52,11 +55,24 @@ form label {
background: lightyellow; background: lightyellow;
} }
</style> </style>
<script type="text/javascript" src="static/js/qrcode.js"></script> <script type="text/javascript" src="static/js/qrcodegen.js"></script>
</head> </head>
<body> <body>
<a id="qrcodelink" style="display:none;"><div id="qrcode"></div></a> <a id="qrcodelink" style="display:none;">
<svg xmlns="http://www.w3.org/2000/svg" id="qrcode" style="width:30em; height:30em;" stroke="none">
<rect width="100%" height="100%" fill="#FFFFFF"/>
<path d="" fill="#000000"/>
</svg>
</a>
<div id="qrcodemessage" style="display:none;">
<!-- TODO Add copy button. -->
<div id="qrcodemsg"></div>
</div>
<form name="settings"> <form name="settings">
<label>
Serverless mode:
<input name="serverless" type="checkbox" value="true" />
</label>
<label> <label>
Recieve remote video: Recieve remote video:
<select name="client-video"> <select name="client-video">
@ -95,11 +111,14 @@ form label {
</div> </div>
<script > <script >
const create = (container, type) => container.appendChild(document.createElement(type)); const create = (container, type) => container.appendChild(document.createElement(type));
const QRGen = qrcodegen.QrCode;
const body = document.querySelector("body"); const body = document.querySelector("body");
const out = document.getElementById("status"); const out = document.getElementById("status");
const qrcode = document.getElementById("qrcode"); const qrcode = document.getElementById("qrcode");
const qrcodelink = document.getElementById("qrcodelink"); const qrcodelink = document.getElementById("qrcodelink");
const qrcodemsg = document.getElementById("qrcodemsg");
const qrcodemessage = document.getElementById("qrcodemessage");
const remoteView = document.getElementById("remoteView"); const remoteView = document.getElementById("remoteView");
const selfView = document.getElementById("selfView"); const selfView = document.getElementById("selfView");
const start = document.getElementById("start"); const start = document.getElementById("start");
@ -117,6 +136,10 @@ form label {
} }
function logPre(str) { function logPre(str) {
if (!str) {
_log('undefined', 'pre');
return;
}
_log(str.split('\\r\\n').join('\r\n'), 'pre'); _log(str.split('\\r\\n').join('\r\n'), 'pre');
} }
@ -143,9 +166,30 @@ form label {
}); });
} }
function isServerless() {
return settings && 'serverless' in settings && settings.serverless;
}
function withHash(hash) {
return window.location.href.split('#')[0] + '#' + hash;
}
function displayQRUrl(url) {
qrcodelink.href = url;
const qr = QRGen.encodeText(url, QRGen.Ecc.MEDIUM);
const code = qr.toSvgString(4);
const viewBox = (/ viewBox="([^"]*)"/.exec(code))[1];
const pathD = (/ d="([^"]*)"/.exec(code))[1];
qrcode.setAttribute("viewBox", viewBox);
qrcode.querySelector("path").setAttribute("d", pathD);
qrcodelink.style.display = '';
}
function displayQRUrlForObj(obj) {
displayQRUrl(withHash('^' + btoa(JSON.stringify(obj))))
}
let roomName = window.location.hash; let roomName = window.location.hash;
let isHost = roomName === undefined || !roomName; let isHost = roomName === undefined || !roomName;
if (roomName.startsWith("#{")) { if (roomName && roomName.startsWith("#{")) {
roomName = undefined; roomName = undefined;
isHost = true; isHost = true;
@ -167,6 +211,25 @@ form label {
log("Failed to read settings from hash: " + error); log("Failed to read settings from hash: " + error);
} }
} }
var firstMessage = undefined;
if (roomName && roomName.startsWith("#^")) {
roomName = undefined;
isHost = false;
try {
const hash = decodeURI(window.location.hash);
log("Reading first message from hash:");
logPre(hash);
log("Decoded base64:");
const decoded = atob(hash.substring(2));
logPre(decoded);
firstMessage = JSON.parse(decoded);
settings = firstMessage.settings;
} catch (error) {
log("Failed to read message from hash: " + error);
}
}
if (isHost) { if (isHost) {
// From https://stackoverflow.com/a/1349426 // From https://stackoverflow.com/a/1349426
function makeid(length) { function makeid(length) {
@ -179,12 +242,14 @@ form label {
return result; return result;
} }
if (!isServerless()) {
roomName = makeid(8); roomName = makeid(8);
qrcodelink.href = window.location.href.split('#')[0] + '#' + roomName; displayQRUrl(withHash(roomName));
new QRCode(qrcode, qrcodelink.href); }
qrcodelink.style.display = '';
} else { } else {
if (!isServerless()) {
roomName = roomName.substring(1); roomName = roomName.substring(1);
}
qrcodelink.style.display = 'none'; qrcodelink.style.display = 'none';
form.style.display = 'none'; form.style.display = 'none';
} }
@ -197,10 +262,15 @@ form label {
const toSend = JSON.stringify(data); const toSend = JSON.stringify(data);
log("Sending message..."); log("Sending message...");
logPre(toSend); logPre(toSend);
if (isServerless()) {
} else {
webSocket.send(toSend); webSocket.send(toSend);
} }
}
var pc = undefined; var pc = undefined;
var localDescriptionSet = false;
function createRTCPeerConnection() { function createRTCPeerConnection() {
const pc = new RTCPeerConnection(); const pc = new RTCPeerConnection();
log("Created RTCPeerConnection."); log("Created RTCPeerConnection.");
@ -208,17 +278,30 @@ form label {
pc.addEventListener('icegatheringstatechange', e => { pc.addEventListener('icegatheringstatechange', e => {
log("In pc.icegatheringstatechange... e.target.iceGatheringState=" log("In pc.icegatheringstatechange... e.target.iceGatheringState="
+ e.target.iceGatheringState); + e.target.iceGatheringState);
if (!startStreamingDone || !localDescriptionSet) return;
// In serverless mode, host transmits first.
// In server mode, client transmits first.
if ((isServerless() != isHost) && !handledOffer) return;
// Don't send offer until all ICE candidates are generated. // Don't send offer until all ICE candidates are generated.
if (e.target.iceGatheringState === 'complete') { if (e.target.iceGatheringState === 'complete') {
if (isHost && isServerless()) {
displayQRUrlForObj({
settings: settings,
description: pc.localDescription
});
} else {
sendJson({ sendJson({
description: pc.localDescription description: pc.localDescription
}); });
} }
}
}); });
pc.onnegotiationneeded = async function () { pc.onnegotiationneeded = async function () {
localDescriptionSet = false;
log("In pc.onnegotiationneeded..."); log("In pc.onnegotiationneeded...");
await pc.setLocalDescription(await pc.createOffer()); await pc.setLocalDescription(await pc.createOffer());
localDescriptionSet = true;
} }
pc.ontrack = ({streams: [stream]}) => { pc.ontrack = ({streams: [stream]}) => {
@ -230,7 +313,6 @@ form label {
if (stream.getVideoTracks().length > 0) { if (stream.getVideoTracks().length > 0) {
remoteView.style.display = ''; remoteView.style.display = '';
out.style.display = 'none';
form.style.display = 'none'; form.style.display = 'none';
videos.style.display = ''; videos.style.display = '';
body.classList.add('justVideo'); body.classList.add('justVideo');
@ -240,6 +322,8 @@ form label {
return pc; return pc;
} }
var startStreamingDone = false;
var remoteDesc = null;
// get a local stream, show it in a self-view and add it to be sent // get a local stream, show it in a self-view and add it to be sent
async function startStreaming(fromButton) { async function startStreaming(fromButton) {
const otherAudioSettings = isHost const otherAudioSettings = isHost
@ -264,7 +348,7 @@ form label {
} }
start.style.display = 'none'; start.style.display = 'none';
if (isHost) { if (isHost && !isServerless()) {
sendJson({ sendJson({
settings: settings settings: settings
}); });
@ -279,8 +363,8 @@ form label {
? true ? true
: { advanced: [{facingMode: videoSettings}] }; : { advanced: [{facingMode: videoSettings}] };
log("Created videoConstraints."); log("Created videoConstraints.");
if (!videoConstraints && !audioSettings) return;
if (videoConstraints || audioSettings) {
const stream = videoSettings == 'screen' const stream = videoSettings == 'screen'
? await navigator.mediaDevices.getDisplayMedia({ ? await navigator.mediaDevices.getDisplayMedia({
audio: audioSettings, audio: audioSettings,
@ -300,10 +384,24 @@ form label {
pc.addTrack(track, stream); pc.addTrack(track, stream);
} }
} }
function startStartingWithErorrHandling(fromButton) { else if (isServerless() && isHost) {
log("No audio/video host.");
//sendJson({description: pc.localDescription});
await pc.setLocalDescription(await pc.createAnswer());
}
startStreamingDone = true;
}
function startStartingWithErorrHandling(fromButton, description) {
startStreaming(fromButton) startStreaming(fromButton)
.then(() => { .then(() => {
log("startStreaming() finished."); log("startStreaming() finished.");
if (!description && remoteDesc) {
description = remoteDesc;
remoteDesc = null;
}
if (description) {
handleOffer(description);
}
}) })
.catch(e => { .catch(e => {
log("startStreaming() errored: " + e.message); log("startStreaming() errored: " + e.message);
@ -314,6 +412,16 @@ form label {
startStartingWithErorrHandling(true) startStartingWithErorrHandling(true)
}); });
var handledOffer = false;
async function handleOffer(description) {
await pc.setRemoteDescription(description);
if (description.type == "offer") {
log("Got an offer...");
await pc.setLocalDescription(await pc.createAnswer());
handledOffer = true;
}
}
async function receiveMessage(e) { async function receiveMessage(e) {
qrcode.style.display = 'none'; qrcode.style.display = 'none';
log("In webSocket.onmessage..."); log("In webSocket.onmessage...");
@ -324,12 +432,16 @@ form label {
startStartingWithErorrHandling(false); startStartingWithErorrHandling(false);
} else if (data.settings) { } else if (data.settings) {
settings = data.settings; settings = data.settings;
if ('description' in data) {
startStartingWithErorrHandling(false, data.description);
} else {
startStartingWithErorrHandling(false); startStartingWithErorrHandling(false);
}
} else if (data.description) { } else if (data.description) {
await pc.setRemoteDescription(data.description); if (startStreamingDone) {
if (data.description.type == "offer") { handleOffer(data.description);
log("Got an offer..."); } else {
await pc.setLocalDescription(await pc.createAnswer()); remoteDesc = data.description;
} }
} else if (data.candidate) { } else if (data.candidate) {
log("Adding ice candidate..."); log("Adding ice candidate...");
@ -359,11 +471,15 @@ form label {
return webSocket; return webSocket;
} }
if (!isServerless()) {
webSocket = createWebSocket(); webSocket = createWebSocket();
if (!isHost) { if (!isHost) {
webSocket.onopen = _ => sendJson({requestSettings: true}); webSocket.onopen = _ => sendJson({requestSettings: true});
} }
} else if (isHost) {
startStartingWithErorrHandling(false);
}
log("Finished <script> block."); log("Finished <script> block.");
</script> </script>