minimal-webrtc/camera/templates/index.html

404 lines
12 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;
}
#qrcode {
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;
}
#status .log-entry {
display: block;
background: lightblue;
border: solid black 2px;
margin: 2px;
}
#status .log-entry:nth-child(odd) {
background: lightyellow;
}
</style>
</head>
<body>
<a id="qrcodelink" style="display:none;"><img id="qrcode" /></a>
<form name="settings">
<label>
Recieve remote video:
<select name="client-video">
<option value="none">none</option>
<option value="environment" selected>rear camera</option>
<option value="user">front camera</option>
<option value="true" selected>any 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="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 body = document.querySelector("body");
const out = document.getElementById("status");
const qrcode = document.getElementById("qrcode");
const qrcodelink = document.getElementById("qrcodelink");
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) {
_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 getRoomName() {
JSON.parse(document.getElementById('room-name').textContent);
}
let roomName = window.location.hash;
let isHost = roomName === undefined || !roomName;
if (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);
}
}
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;
}
roomName = makeid(8);
qrcodelink.href = window.location.href.split('#')[0] + '#' + roomName;
qrcode.src = window.location.href.split('#')[0] + roomName + '/qr';
qrcodelink.style.display = '';
} else {
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);
const dcReady = dc && dc.readyState == "open";
const method = dcReady ? "dataConnection" : "webSocket";
log("Sending message via " + method + "...");
logPre(toSend);
(dcReady ? dc : webSocket).send(toSend);
}
var pc = undefined;
var dc = undefined;
function sendOffer() {
if (pc.iceGatheringState == "complete") {
sendJson({
description: pc.localDescription
});
}
}
function createRTCPeerConnection() {
const pc = new RTCPeerConnection();
log("Created RTCPeerConnection.");
pc.onsignalingstatechange = e => {
log("pc.onsignalingstatechange: " + pc.signalingState);
}
pc.oniceconnectionstatechange = e => {
log("pc.oniceconnectionstatechange: " + pc.iceConnectionState);
}
pc.onicegatheringstatechange = async function(e) {
log("pc.onicegatheringstatechange: " + pc.iceGatheringState);
sendOffer();
}
// let the "negotiationneeded" event trigger offer generation
// ... but only once icegathering is complete.
pc.onnegotiationneeded = async function () {
log("In pc.onnegotiationneeded...");
await pc.setLocalDescription(await pc.createOffer());
sendOffer();
}
pc.ontrack = ({streams: [stream]}) => {
log("In pc.ontrack...");
remoteView.srcObject = stream;
log("Set srcObject");
remoteView.play();
if (stream.getVideoTracks().length > 0) {
remoteView.style.display = '';
out.style.display = 'none';
form.style.display = 'none';
videos.style.display = '';
body.classList.add('justVideo');
}
};
pc.ondatachannel = e => {
dc = e.channel;
dc.onmessage = e => {
receiveMessage({source: "dataChannel", data: e.data});
}
log('Data channel initialized.');
}
if (isHost) {
dc = pc.createDataChannel('data');
dc.onmessage = e => {
receiveMessage({source: "dataChannel", data: e.data});
}
dc.onopen = e => {
log("Data channel open, sending settings...")
settings = readSettingsForm(true);
sendJson({settings});
startStreamingWithErorrHandling(false);
}
}
return pc;
}
// get a local stream, show it in a self-view and add it to be sent
async function startStreaming(fromButton) {
log('In startStreaming(fromButton=' + 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';
const videoConstraints = videoSettings == 'none'
? false
: videoSettings == 'true'
? true
: { advanced: [{facingMode: videoSettings}] };
log("Created videoConstraints.");
if (!videoConstraints && !audioSettings) return;
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);
}
}
function startStreamingWithErorrHandling(fromButton) {
startStreaming(fromButton)
.then(() => {
log("startStreaming() finished.");
})
.catch(e => {
log("startStreaming() errored: " + e.message);
});
}
start.addEventListener("click", _ => {
startStreamingWithErorrHandling(true)
});
async function receiveMessage(e) {
qrcode.style.display = 'none';
log("In receiveMessage from " + e.source + "...");
logPre(e.data);
const data = JSON.parse(e.data);
if (data.ready) {
// Ready message means client is open and ready for connection.
pc = createRTCPeerConnection();
} else if (data.settings) {
settings = data.settings;
startStreamingWithErorrHandling(false);
} else if (data.description) {
if (pc == undefined) pc = createRTCPeerConnection();
await pc.setRemoteDescription(data.description);
if (data.description.type == "offer") {
log("Got an offer...");
await pc.setLocalDescription(await pc.createAnswer());
sendOffer();
}
}
};
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 = e => {
receiveMessage({source: "webSocket", data: e.data});
}
return webSocket;
}
webSocket = createWebSocket();
if (!isHost) {
// To make serverless and server mode more similar,
// always make the first RTCPeerConnection offer from the host,
// so here just notify the host to start the process.
webSocket.onopen = _ => sendJson({ready: true});
}
log("Finished <script> block.");
</script>
</body>
</html>