minimal-webrtc/camera/templates/index.html

488 lines
14 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Minimal WebRTC</title>
<style>
body.justVideo {
margin: 0;
overflow: hidden;
}
#qrcodelink {
position: sticky;
top: 0;
right: 0;
float: right;
}
#qrcodelink svg path {
min-width: 30vw;
max-width: 100vw;
max-height: 50vh;
}
#selfView {
position: fixed;
right: 0;
bottom: 0;
border: 5px solid blue;
}
#unmute, #start {
position: fixed;
top: 0;
width: 100%;
font-size: 4em;
}
form {
background: lightgray;
border: solid gold 5px;
display: table;
}
form label {
display: table-row;
}
#status {
display: table;
}
body.justVideo #status {
display: none;
}
#status .log-entry {
display: block;
background: lightblue;
border: solid black 2px;
margin: 2px;
}
#status .log-entry:nth-child(odd) {
background: lightyellow;
}
</style>
<script type="text/javascript" src="static/js/qrcodegen.js"></script>
</head>
<body>
<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">
<option value="none">none</option>
<option value="true">any camera</option>
<option value="environment" selected>rear camera</option>
<option value="user">front camera</option>
<option value="screen">screen share</option>
</select>
</label>
<label>
Recieve remote audio:
<input name="client-audio" type="checkbox" value="true" />
</label>
<label>
Transmit video:
<select name="host-video">
<option value="none" selected>none</option>
<option value="true">any camera</option>
<option value="environment">rear camera</option>
<option value="user">front camera</option>
<option value="screen">screen share</option>
</select>
</label>
<label>
Transmit audio:
<input name="host-audio" type="checkbox" value="true" />
</label>
</form>
<div id="status"></div>
<div id="videos">
<button id="unmute" style="display:none;">Unmute</button>
<button id="start" style="display:none;">Start Streaming</button>
<video id="remoteView" width="100%" autoplay muted style="display:none;"></video>
<video id="selfView" width="200" height="150" autoplay muted style="display:none;"></video>
</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");
const unmute = document.getElementById("unmute");
const form = document.forms["settings"];
function _log(str, tag) {
const logEntry = document.createElement(tag);
logEntry.innerText = str;
logEntry.classList.add('log-entry');
out.appendChild(logEntry);
}
function log(str) {
_log(str, 'span');
}
function logPre(str) {
if (!str) {
_log('undefined', 'pre');
return;
}
_log(str.split('\\r\\n').join('\r\n'), 'pre');
}
log("Loading...");
unmute.addEventListener("click", _ => {
remoteView.muted = false;
unmute.style.display = 'none';
});
var settings = undefined;
function readSettingsForm(disableForm) {
const obj = {};
for (const el of form.elements) {
obj[el.name] = el.type == 'checkbox' ? el.checked : el.value;
if (disableForm) el.disabled = true;
}
return obj;
}
for (const el of form.elements) {
el.addEventListener("change", _ => {
window.location.hash = JSON.stringify(readSettingsForm(false));
});
}
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 && roomName.startsWith("#{")) {
roomName = undefined;
isHost = true;
try {
const hash = decodeURI(window.location.hash);
log("Reading settings from hash:");
logPre(hash);
settings = JSON.parse(hash.substring(1));
for (const name in settings) {
const el = form.elements[name];
const value = settings[name];
if (el.type == 'checkbox') {
el.checked = value;
} else {
el.value = value;
}
}
} catch (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) {
// From https://stackoverflow.com/a/1349426
function makeid(length) {
var result = '';
var characters = 'abcdefghijklmnopqrstuvwxyz';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
if (!isServerless()) {
roomName = makeid(8);
displayQRUrl(withHash(roomName));
}
} else {
if (!isServerless()) {
roomName = roomName.substring(1);
}
qrcodelink.style.display = 'none';
form.style.display = 'none';
}
log("Room: " + roomName);
var webSocket = undefined;
function sendJson(data) {
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.");
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]}) => {
log("In pc.ontrack...");
remoteView.srcObject = stream;
log("Set srcObject");
remoteView.play();
if (stream.getVideoTracks().length > 0) {
remoteView.style.display = '';
form.style.display = 'none';
videos.style.display = '';
body.classList.add('justVideo');
}
};
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
? settings['client-audio']
: settings['host-audio'];
if (otherAudioSettings) {
unmute.style.display = '';
}
const videoSettings = isHost
? settings['host-video']
: settings['client-video'];
log("videoSettings=" + videoSettings);
const audioSettings = isHost
? settings['host-audio']
: settings['client-audio'];
log("audioSettings=" + audioSettings);
if (videoSettings == 'screen' && !fromButton) {
start.style.display = '';
return;
}
start.style.display = 'none';
if (isHost && !isServerless()) {
sendJson({
settings: settings
});
}
if (pc !== undefined) return;
pc = createRTCPeerConnection();
const videoConstraints = videoSettings == 'none'
? false
: videoSettings == 'true'
? true
: { advanced: [{facingMode: videoSettings}] };
log("Created videoConstraints.");
if (videoConstraints || audioSettings) {
const stream = videoSettings == 'screen'
? await navigator.mediaDevices.getDisplayMedia({
audio: audioSettings,
video: true
})
: await navigator.mediaDevices.getUserMedia({
audio: audioSettings,
video: videoConstraints
});
log("Created stream.");
if (videoConstraints) {
selfView.srcObject = stream;
selfView.style.display = '';
}
for (const track of stream.getTracks()) {
log("Added track.");
pc.addTrack(track, stream);
}
}
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);
});
}
start.addEventListener("click", _ => {
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...");
logPre(e.data);
const data = JSON.parse(e.data);
if (data.requestSettings) {
settings = readSettingsForm(true);
startStartingWithErorrHandling(false);
} else if (data.settings) {
settings = data.settings;
if ('description' in data) {
startStartingWithErorrHandling(false, data.description);
} else {
startStartingWithErorrHandling(false);
}
} else if (data.description) {
if (startStreamingDone) {
handleOffer(data.description);
} else {
remoteDesc = data.description;
}
} else if (data.candidate) {
log("Adding ice candidate...");
await pc.addIceCandidate(data.candidate);
}
};
function createWebSocket() {
const webSocket = new WebSocket(
'ws' + (window.location.protocol == 'https:' ? 's' : '') + '://'
+ window.location.host
+ '/camera/ws/' + (isHost ? 'host' : 'client') + '/'
+ roomName
+ '/'
);
log("Created WebSocket.");
webSocket.onclose = function(e) {
log('WebSocket closed unexpectedly: ' + e);
};
webSocket.onerror = function(e) {
log('WebSocket error: ' + e);
};
webSocket.onmessage = receiveMessage;
return webSocket;
}
if (!isServerless()) {
webSocket = createWebSocket();
if (!isHost) {
webSocket.onopen = _ => sendJson({requestSettings: true});
}
} else if (isHost) {
startStartingWithErorrHandling(false);
}
log("Finished <script> block.");
</script>
</body>
</html>