Peer-to-peer WebRTC connection with minimal setup. https://apps.aweirdimagination.net/camera/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

430 lines
13 KiB

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8"/>
  5. <title>Minimal WebRTC</title>
  6. <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
  7. <style>
  8. body.justVideo {
  9. margin: 0;
  10. overflow: hidden;
  11. }
  12. body.justVideo #status, body.justVideo form {
  13. display: none;
  14. }
  15. #qrcodelink {
  16. position: sticky;
  17. top: 0;
  18. right: 0;
  19. float: right;
  20. }
  21. #qrcodelink svg path {
  22. min-width: 30vw;
  23. max-width: 100vw;
  24. max-height: 50vh;
  25. }
  26. #selfView {
  27. position: fixed;
  28. right: 0;
  29. bottom: 0;
  30. border: 5px solid blue;
  31. }
  32. #unmute, #start {
  33. position: fixed;
  34. top: 0;
  35. width: 100%;
  36. font-size: 4em;
  37. }
  38. form {
  39. background: lightgray;
  40. border: solid gold 5px;
  41. display: table;
  42. }
  43. form label {
  44. display: table-row;
  45. }
  46. #status {
  47. display: table;
  48. }
  49. #status .log-entry {
  50. display: block;
  51. background: lightblue;
  52. border: solid black 2px;
  53. margin: 2px;
  54. }
  55. #status .log-entry:nth-child(odd) {
  56. background: lightyellow;
  57. }
  58. </style>
  59. <script type="text/javascript" src="static/js/qrcodegen.js"></script>
  60. </head>
  61. <body>
  62. <a id="qrcodelink" style="display:none;">
  63. <svg xmlns="http://www.w3.org/2000/svg" id="qrcode" style="width:20em; height:20em;" stroke="none">
  64. <rect width="100%" height="100%" fill="#FFFFFF"/>
  65. <path d="" fill="#000000"/>
  66. </svg>
  67. </a>
  68. <form name="settings">
  69. <label>
  70. Recieve remote video:
  71. <select name="client-video">
  72. <option value="none">none</option>
  73. <option value="true">any camera</option>
  74. <option value="environment" selected>rear camera</option>
  75. <option value="user">front camera</option>
  76. <option value="screen">screen share</option>
  77. </select>
  78. </label>
  79. <label>
  80. Recieve remote audio:
  81. <input name="client-audio" type="checkbox" value="true" />
  82. </label>
  83. <label>
  84. Transmit video:
  85. <select name="host-video">
  86. <option value="none" selected>none</option>
  87. <option value="true">any camera</option>
  88. <option value="environment">rear camera</option>
  89. <option value="user">front camera</option>
  90. <option value="screen">screen share</option>
  91. </select>
  92. </label>
  93. <label>
  94. Transmit audio:
  95. <input name="host-audio" type="checkbox" value="true" />
  96. </label>
  97. <label>
  98. Debug mode (show log even after starting video):
  99. <input name="debug" type="checkbox" value="true" />
  100. </label>
  101. </form>
  102. <div id="status"></div>
  103. <div id="videos">
  104. <button id="unmute" style="display:none;">Unmute</button>
  105. <button id="start" style="display:none;">Start Streaming</button>
  106. <video id="remoteView" width="100%" autoplay muted style="display:none;"></video>
  107. <video id="selfView" width="200" height="150" autoplay muted style="display:none;"></video>
  108. </div>
  109. <script >
  110. const create = (container, type) => container.appendChild(document.createElement(type));
  111. const QRGen = qrcodegen.QrCode;
  112. const body = document.querySelector("body");
  113. const out = document.getElementById("status");
  114. const qrcode = document.getElementById("qrcode");
  115. const qrcodelink = document.getElementById("qrcodelink");
  116. const remoteView = document.getElementById("remoteView");
  117. const selfView = document.getElementById("selfView");
  118. const start = document.getElementById("start");
  119. const unmute = document.getElementById("unmute");
  120. const form = document.forms["settings"];
  121. function _log(str, tag) {
  122. const logEntry = document.createElement(tag);
  123. logEntry.innerText = str;
  124. logEntry.classList.add('log-entry');
  125. out.appendChild(logEntry);
  126. }
  127. function log(str) {
  128. _log(str, 'span');
  129. }
  130. function logPre(str) {
  131. _log(str.split('\\r\\n').join('\r\n'), 'pre');
  132. }
  133. log("Loading...");
  134. unmute.addEventListener("click", _ => {
  135. remoteView.muted = false;
  136. unmute.style.display = 'none';
  137. });
  138. var settings = undefined;
  139. function readSettingsForm(disableForm) {
  140. const obj = {};
  141. for (const el of form.elements) {
  142. obj[el.name] = el.type == 'checkbox' ? el.checked : el.value;
  143. if (disableForm) el.disabled = true;
  144. }
  145. return obj;
  146. }
  147. for (const el of form.elements) {
  148. el.addEventListener("change", _ => {
  149. window.location.hash = JSON.stringify(readSettingsForm(false));
  150. });
  151. }
  152. function withHash(hash) {
  153. return window.location.href.split('#')[0] + '#' + hash;
  154. }
  155. function displayQRUrl(url) {
  156. qrcodelink.href = url;
  157. const qr = QRGen.encodeText(url, QRGen.Ecc.MEDIUM);
  158. const code = qr.toSvgString(1);
  159. const viewBox = (/ viewBox="([^"]*)"/.exec(code))[1];
  160. const pathD = (/ d="([^"]*)"/.exec(code))[1];
  161. qrcode.setAttribute("viewBox", viewBox);
  162. qrcode.querySelector("path").setAttribute("d", pathD);
  163. qrcodelink.style.display = '';
  164. }
  165. let roomName = window.location.hash;
  166. let isHost = roomName === undefined || !roomName;
  167. if (roomName && roomName.startsWith("#{")) {
  168. roomName = undefined;
  169. isHost = true;
  170. try {
  171. const hash = decodeURI(window.location.hash);
  172. log("Reading settings from hash:");
  173. logPre(hash);
  174. settings = JSON.parse(hash.substring(1));
  175. for (const name in settings) {
  176. const el = form.elements[name];
  177. const value = settings[name];
  178. if (el.type == 'checkbox') {
  179. el.checked = value;
  180. } else {
  181. el.value = value;
  182. }
  183. }
  184. } catch (error) {
  185. log("Failed to read settings from hash: " + error);
  186. }
  187. }
  188. if (isHost) {
  189. // From https://stackoverflow.com/a/1349426
  190. function makeid(length) {
  191. var result = '';
  192. var characters = 'abcdefghijklmnopqrstuvwxyz';
  193. var charactersLength = characters.length;
  194. for (var i = 0; i < length; i++) {
  195. result += characters.charAt(Math.floor(Math.random() * charactersLength));
  196. }
  197. return result;
  198. }
  199. roomName = makeid(8);
  200. displayQRUrl(withHash(roomName));
  201. } else {
  202. roomName = roomName.substring(1);
  203. qrcodelink.style.display = 'none';
  204. form.style.display = 'none';
  205. }
  206. log("Room: " + roomName);
  207. var webSocket = undefined;
  208. function sendJson(data) {
  209. const toSend = JSON.stringify(data);
  210. const dcReady = dc && dc.readyState == "open";
  211. const method = dcReady ? "dataConnection" : "webSocket";
  212. log("Sending message via " + method + "...");
  213. logPre(toSend);
  214. (dcReady ? dc : webSocket).send(toSend);
  215. }
  216. var pc = undefined;
  217. var dc = undefined;
  218. function sendOffer() {
  219. if (pc.iceGatheringState == "complete") {
  220. sendJson({
  221. description: pc.localDescription
  222. });
  223. }
  224. }
  225. function createRTCPeerConnection() {
  226. const pc = new RTCPeerConnection();
  227. log("Created RTCPeerConnection.");
  228. pc.onsignalingstatechange = e => {
  229. log("pc.onsignalingstatechange: " + pc.signalingState);
  230. }
  231. pc.oniceconnectionstatechange = e => {
  232. log("pc.oniceconnectionstatechange: " + pc.iceConnectionState);
  233. }
  234. pc.onicegatheringstatechange = async function(e) {
  235. log("pc.onicegatheringstatechange: " + pc.iceGatheringState);
  236. sendOffer();
  237. }
  238. // let the "negotiationneeded" event trigger offer generation
  239. // ... but only once icegathering is complete.
  240. pc.onnegotiationneeded = async function () {
  241. log("In pc.onnegotiationneeded...");
  242. await pc.setLocalDescription(await pc.createOffer());
  243. sendOffer();
  244. }
  245. pc.ontrack = ({streams: [stream]}) => {
  246. log("In pc.ontrack...");
  247. remoteView.srcObject = stream;
  248. log("Set srcObject");
  249. remoteView.play();
  250. if (stream.getVideoTracks().length > 0) {
  251. remoteView.style.display = '';
  252. videos.style.display = '';
  253. if (!settings.debug) {
  254. body.classList.add('justVideo');
  255. }
  256. }
  257. };
  258. pc.ondatachannel = e => {
  259. dc = e.channel;
  260. dc.onmessage = e => {
  261. receiveMessage({source: "dataChannel", data: e.data});
  262. }
  263. log('Data channel initialized.');
  264. }
  265. if (isHost) {
  266. dc = pc.createDataChannel('data');
  267. dc.onmessage = e => {
  268. receiveMessage({source: "dataChannel", data: e.data});
  269. }
  270. dc.onopen = e => {
  271. log("Data channel open, sending settings...")
  272. settings = readSettingsForm(true);
  273. sendJson({settings});
  274. startStreamingWithErorrHandling(false);
  275. }
  276. }
  277. return pc;
  278. }
  279. // get a local stream, show it in a self-view and add it to be sent
  280. async function startStreaming(fromButton) {
  281. log('In startStreaming(fromButton=' + fromButton + ')...');
  282. const otherAudioSettings = isHost
  283. ? settings['client-audio']
  284. : settings['host-audio'];
  285. if (otherAudioSettings) {
  286. unmute.style.display = '';
  287. }
  288. const videoSettings = isHost
  289. ? settings['host-video']
  290. : settings['client-video'];
  291. log("videoSettings=" + videoSettings);
  292. const audioSettings = isHost
  293. ? settings['host-audio']
  294. : settings['client-audio'];
  295. log("audioSettings=" + audioSettings);
  296. if (videoSettings == 'screen' && !fromButton) {
  297. start.style.display = '';
  298. return;
  299. }
  300. start.style.display = 'none';
  301. const videoConstraints = videoSettings == 'none'
  302. ? false
  303. : videoSettings == 'true'
  304. ? true
  305. : { advanced: [{facingMode: videoSettings}] };
  306. log("Created videoConstraints.");
  307. if (!videoConstraints && !audioSettings) return;
  308. const stream = videoSettings == 'screen'
  309. ? await navigator.mediaDevices.getDisplayMedia({
  310. audio: audioSettings,
  311. video: true
  312. })
  313. : await navigator.mediaDevices.getUserMedia({
  314. audio: audioSettings,
  315. video: videoConstraints
  316. });
  317. log("Created stream.");
  318. if (videoConstraints) {
  319. selfView.srcObject = stream;
  320. selfView.style.display = '';
  321. }
  322. for (const track of stream.getTracks()) {
  323. log("Added track.");
  324. pc.addTrack(track, stream);
  325. }
  326. }
  327. function startStreamingWithErorrHandling(fromButton) {
  328. startStreaming(fromButton)
  329. .then(() => {
  330. log("startStreaming() finished.");
  331. })
  332. .catch(e => {
  333. log("startStreaming() errored: " + e.message);
  334. });
  335. }
  336. start.addEventListener("click", _ => {
  337. startStreamingWithErorrHandling(true)
  338. });
  339. async function receiveMessage(e) {
  340. qrcode.style.display = 'none';
  341. log("In receiveMessage from " + e.source + "...");
  342. logPre(e.data);
  343. const data = JSON.parse(e.data);
  344. if (data.ready) {
  345. // Ready message means client is open and ready for connection.
  346. pc = createRTCPeerConnection();
  347. } else if (data.settings) {
  348. settings = data.settings;
  349. startStreamingWithErorrHandling(false);
  350. } else if (data.description) {
  351. if (pc == undefined) pc = createRTCPeerConnection();
  352. await pc.setRemoteDescription(data.description);
  353. if (data.description.type == "offer") {
  354. log("Got an offer...");
  355. await pc.setLocalDescription(await pc.createAnswer());
  356. sendOffer();
  357. }
  358. }
  359. };
  360. function createWebSocket() {
  361. const webSocket = new WebSocket(
  362. 'ws' + (window.location.protocol == 'https:' ? 's' : '') + '://'
  363. + window.location.host
  364. + '/camera/ws/' + (isHost ? 'host' : 'client') + '/'
  365. + roomName
  366. + '/'
  367. );
  368. log("Created WebSocket.");
  369. webSocket.onclose = function(e) {
  370. log('WebSocket closed unexpectedly: ' + e);
  371. };
  372. webSocket.onerror = function(e) {
  373. log('WebSocket error: ' + e);
  374. };
  375. webSocket.onmessage = e => {
  376. receiveMessage({source: "webSocket", data: e.data});
  377. }
  378. return webSocket;
  379. }
  380. webSocket = createWebSocket();
  381. if (!isHost) {
  382. // To make serverless and server mode more similar,
  383. // always make the first RTCPeerConnection offer from the host,
  384. // so here just notify the host to start the process.
  385. webSocket.onopen = _ => sendJson({ready: true});
  386. }
  387. log("Finished <script> block.");
  388. </script>
  389. </body>
  390. </html>