Compare commits

...

1 Commits

View File

@ -14,7 +14,7 @@ body.justVideo {
right: 0;
float: right;
}
#qrcodelink img {
#qrcodelink svg path {
min-width: 30vw;
max-width: 100vw;
max-height: 50vh;
@ -42,6 +42,9 @@ form label {
#status {
display: table;
}
body.justVideo #status {
display: none;
}
#status .log-entry {
display: block;
background: lightblue;
@ -52,11 +55,24 @@ form label {
background: lightyellow;
}
</style>
<script type="text/javascript" src="static/js/qrcode.js"></script>
<script type="text/javascript" src="static/js/qrcodegen.js"></script>
</head>
<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">
<label>
Serverless mode:
<input name="serverless" type="checkbox" value="true" />
</label>
<label>
Recieve remote video:
<select name="client-video">
@ -95,11 +111,14 @@ form label {
</div>
<script >
const create = (container, type) => container.appendChild(document.createElement(type));
const QRGen = qrcodegen.QrCode;
const body = document.querySelector("body");
const out = document.getElementById("status");
const qrcode = document.getElementById("qrcode");
const qrcodelink = document.getElementById("qrcodelink");
const qrcodemsg = document.getElementById("qrcodemsg");
const qrcodemessage = document.getElementById("qrcodemessage");
const remoteView = document.getElementById("remoteView");
const selfView = document.getElementById("selfView");
const start = document.getElementById("start");
@ -117,6 +136,10 @@ form label {
}
function logPre(str) {
if (!str) {
_log('undefined', 'pre');
return;
}
_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 isHost = roomName === undefined || !roomName;
if (roomName.startsWith("#{")) {
if (roomName && roomName.startsWith("#{")) {
roomName = undefined;
isHost = true;
@ -167,6 +211,25 @@ form label {
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) {
// From https://stackoverflow.com/a/1349426
function makeid(length) {
@ -179,12 +242,14 @@ form label {
return result;
}
if (!isServerless()) {
roomName = makeid(8);
qrcodelink.href = window.location.href.split('#')[0] + '#' + roomName;
new QRCode(qrcode, qrcodelink.href);
qrcodelink.style.display = '';
displayQRUrl(withHash(roomName));
}
} else {
if (!isServerless()) {
roomName = roomName.substring(1);
}
qrcodelink.style.display = 'none';
form.style.display = 'none';
}
@ -197,10 +262,15 @@ form label {
const toSend = JSON.stringify(data);
log("Sending message...");
logPre(toSend);
if (isServerless()) {
} else {
webSocket.send(toSend);
}
}
var pc = undefined;
var localDescriptionSet = false;
function createRTCPeerConnection() {
const pc = new RTCPeerConnection();
log("Created RTCPeerConnection.");
@ -208,17 +278,30 @@ form label {
pc.addEventListener('icegatheringstatechange', e => {
log("In pc.icegatheringstatechange... 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.
if (e.target.iceGatheringState === 'complete') {
if (isHost && isServerless()) {
displayQRUrlForObj({
settings: settings,
description: pc.localDescription
});
} else {
sendJson({
description: pc.localDescription
});
}
}
});
pc.onnegotiationneeded = async function () {
localDescriptionSet = false;
log("In pc.onnegotiationneeded...");
await pc.setLocalDescription(await pc.createOffer());
localDescriptionSet = true;
}
pc.ontrack = ({streams: [stream]}) => {
@ -230,7 +313,6 @@ form label {
if (stream.getVideoTracks().length > 0) {
remoteView.style.display = '';
out.style.display = 'none';
form.style.display = 'none';
videos.style.display = '';
body.classList.add('justVideo');
@ -240,6 +322,8 @@ form label {
return pc;
}
var startStreamingDone = false;
var remoteDesc = null;
// get a local stream, show it in a self-view and add it to be sent
async function startStreaming(fromButton) {
const otherAudioSettings = isHost
@ -264,7 +348,7 @@ form label {
}
start.style.display = 'none';
if (isHost) {
if (isHost && !isServerless()) {
sendJson({
settings: settings
});
@ -279,8 +363,8 @@ form label {
? true
: { advanced: [{facingMode: videoSettings}] };
log("Created videoConstraints.");
if (!videoConstraints && !audioSettings) return;
if (videoConstraints || audioSettings) {
const stream = videoSettings == 'screen'
? await navigator.mediaDevices.getDisplayMedia({
audio: audioSettings,
@ -300,10 +384,24 @@ form label {
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)
.then(() => {
log("startStreaming() finished.");
if (!description && remoteDesc) {
description = remoteDesc;
remoteDesc = null;
}
if (description) {
handleOffer(description);
}
})
.catch(e => {
log("startStreaming() errored: " + e.message);
@ -314,6 +412,16 @@ form label {
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) {
qrcode.style.display = 'none';
log("In webSocket.onmessage...");
@ -324,12 +432,16 @@ form label {
startStartingWithErorrHandling(false);
} else if (data.settings) {
settings = data.settings;
if ('description' in data) {
startStartingWithErorrHandling(false, data.description);
} else {
startStartingWithErorrHandling(false);
}
} else if (data.description) {
await pc.setRemoteDescription(data.description);
if (data.description.type == "offer") {
log("Got an offer...");
await pc.setLocalDescription(await pc.createAnswer());
if (startStreamingDone) {
handleOffer(data.description);
} else {
remoteDesc = data.description;
}
} else if (data.candidate) {
log("Adding ice candidate...");
@ -359,11 +471,15 @@ form label {
return webSocket;
}
if (!isServerless()) {
webSocket = createWebSocket();
if (!isHost) {
webSocket.onopen = _ => sendJson({requestSettings: true});
}
} else if (isHost) {
startStartingWithErorrHandling(false);
}
log("Finished <script> block.");
</script>