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.
 
 
 
 

370 lines
11 KiB

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8"/>
  5. <title>Simple WebRTC</title>
  6. <style>
  7. body.justVideo {
  8. margin: 0;
  9. overflow: hidden;
  10. }
  11. #qrcodelink {
  12. position: sticky;
  13. top: 0;
  14. right: 0;
  15. float: right;
  16. }
  17. #qrcode {
  18. max-width: 100vw;
  19. max-height: 50vh;
  20. }
  21. #selfView {
  22. position: fixed;
  23. right: 0;
  24. bottom: 0;
  25. border: 5px solid blue;
  26. }
  27. #unmute, #start {
  28. position: fixed;
  29. top: 0;
  30. width: 100%;
  31. font-size: 4em;
  32. }
  33. form {
  34. background: lightgray;
  35. border: solid gold 5px;
  36. display: table;
  37. }
  38. form label {
  39. display: table-row;
  40. }
  41. #status {
  42. display: table;
  43. }
  44. #status .log-entry {
  45. display: block;
  46. background: lightblue;
  47. border: solid black 2px;
  48. margin: 2px;
  49. }
  50. #status .log-entry:nth-child(odd) {
  51. background: lightyellow;
  52. }
  53. </style>
  54. </head>
  55. <body>
  56. <a id="qrcodelink" style="display:none;"><img id="qrcode" /></a>
  57. <form name="settings">
  58. <label>
  59. Recieve remote video:
  60. <select name="client-video">
  61. <option value="none">none</option>
  62. <option value="environment" selected>rear camera</option>
  63. <option value="user">front camera</option>
  64. <option value="true" selected>any camera</option>
  65. <option value="screen">screen share</option>
  66. </select>
  67. </label>
  68. <label>
  69. Recieve remote audio:
  70. <input name="client-audio" type="checkbox" value="true" />
  71. </label>
  72. <label>
  73. Transmit video:
  74. <select name="host-video">
  75. <option value="none" selected>none</option>
  76. <option value="true">any camera</option>
  77. <option value="screen">screen share</option>
  78. </select>
  79. </label>
  80. <label>
  81. Transmit audio:
  82. <input name="host-audio" type="checkbox" value="true" />
  83. </label>
  84. </form>
  85. <div id="status"></div>
  86. <div id="videos">
  87. <button id="unmute" style="display:none;">Unmute</button>
  88. <button id="start" style="display:none;">Start Streaming</button>
  89. <video id="remoteView" width="100%" autoplay muted style="display:none;"></video>
  90. <video id="selfView" width="200" height="150" autoplay muted style="display:none;"></video>
  91. </div>
  92. <script >
  93. const create = (container, type) => container.appendChild(document.createElement(type));
  94. const body = document.querySelector("body");
  95. const out = document.getElementById("status");
  96. const qrcode = document.getElementById("qrcode");
  97. const qrcodelink = document.getElementById("qrcodelink");
  98. const remoteView = document.getElementById("remoteView");
  99. const selfView = document.getElementById("selfView");
  100. const start = document.getElementById("start");
  101. const unmute = document.getElementById("unmute");
  102. const form = document.forms["settings"];
  103. function _log(str, tag) {
  104. const logEntry = document.createElement(tag);
  105. logEntry.innerText = str;
  106. logEntry.classList.add('log-entry');
  107. out.appendChild(logEntry);
  108. }
  109. function log(str) {
  110. _log(str, 'span');
  111. }
  112. function logPre(str) {
  113. _log(str.split('\\r\\n').join('\r\n'), 'pre');
  114. }
  115. log("Loading...");
  116. unmute.addEventListener("click", _ => {
  117. remoteView.muted = false;
  118. unmute.style.display = 'none';
  119. });
  120. var settings = undefined;
  121. function readSettingsForm(disableForm) {
  122. const obj = {};
  123. for (const el of form.elements) {
  124. obj[el.name] = el.type == 'checkbox' ? el.checked : el.value;
  125. if (disableForm) el.disabled = true;
  126. }
  127. return obj;
  128. }
  129. for (const el of form.elements) {
  130. el.addEventListener("change", _ => {
  131. window.location.hash = JSON.stringify(readSettingsForm(false));
  132. });
  133. }
  134. function getRoomName() {
  135. JSON.parse(document.getElementById('room-name').textContent);
  136. }
  137. let roomName = window.location.hash;
  138. let isHost = roomName === undefined || !roomName;
  139. if (roomName.startsWith("#{")) {
  140. roomName = undefined;
  141. isHost = true;
  142. try {
  143. const hash = decodeURI(window.location.hash);
  144. log("Reading settings from hash:");
  145. logPre(hash);
  146. settings = JSON.parse(hash.substring(1));
  147. for (const name in settings) {
  148. const el = form.elements[name];
  149. const value = settings[name];
  150. if (el.type == 'checkbox') {
  151. el.checked = value;
  152. } else {
  153. el.value = value;
  154. }
  155. }
  156. } catch (error) {
  157. log("Failed to read settings from hash: " + error);
  158. }
  159. }
  160. if (isHost) {
  161. // From https://stackoverflow.com/a/1349426
  162. function makeid(length) {
  163. var result = '';
  164. var characters = 'abcdefghijklmnopqrstuvwxyz';
  165. var charactersLength = characters.length;
  166. for (var i = 0; i < length; i++) {
  167. result += characters.charAt(Math.floor(Math.random() * charactersLength));
  168. }
  169. return result;
  170. }
  171. roomName = makeid(8);
  172. qrcodelink.href = window.location.href.split('#')[0] + '#' + roomName;
  173. qrcode.src = window.location.href.split('#')[0] + roomName + '/qr';
  174. qrcodelink.style.display = '';
  175. } else {
  176. roomName = roomName.substring(1);
  177. qrcodelink.style.display = 'none';
  178. form.style.display = 'none';
  179. }
  180. log("Room: " + roomName);
  181. var webSocket = undefined;
  182. function sendJson(data) {
  183. const toSend = JSON.stringify(data);
  184. log("Sending message...");
  185. logPre(toSend);
  186. webSocket.send(toSend);
  187. }
  188. var pc = undefined;
  189. function createRTCPeerConnection() {
  190. const pc = new RTCPeerConnection();
  191. log("Created RTCPeerConnection.");
  192. pc.onicecandidate = ({candidate}) => sendJson({candidate});
  193. // let the "negotiationneeded" event trigger offer generation
  194. pc.onnegotiationneeded = async function () {
  195. log("In pc.onnegotiationneeded...");
  196. await pc.setLocalDescription(await pc.createOffer());
  197. sendJson({
  198. description: pc.localDescription
  199. });
  200. }
  201. pc.ontrack = ({streams: [stream]}) => {
  202. log("In pc.ontrack...");
  203. remoteView.srcObject = stream;
  204. log("Set srcObject");
  205. remoteView.play();
  206. if (stream.getVideoTracks().length > 0) {
  207. remoteView.style.display = '';
  208. out.style.display = 'none';
  209. form.style.display = 'none';
  210. videos.style.display = '';
  211. body.classList.add('justVideo');
  212. }
  213. };
  214. return pc;
  215. }
  216. // get a local stream, show it in a self-view and add it to be sent
  217. async function startStreaming(fromButton) {
  218. const otherAudioSettings = isHost
  219. ? settings['client-audio']
  220. : settings['host-audio'];
  221. if (otherAudioSettings) {
  222. unmute.style.display = '';
  223. }
  224. const videoSettings = isHost
  225. ? settings['host-video']
  226. : settings['client-video'];
  227. log("videoSettings=" + videoSettings);
  228. const audioSettings = isHost
  229. ? settings['host-audio']
  230. : settings['client-audio'];
  231. log("audioSettings=" + audioSettings);
  232. if (videoSettings == 'screen' && !fromButton) {
  233. start.style.display = '';
  234. return;
  235. }
  236. start.style.display = 'none';
  237. if (isHost) {
  238. sendJson({
  239. settings: settings
  240. });
  241. }
  242. if (pc !== undefined) return;
  243. pc = createRTCPeerConnection();
  244. const videoConstraints = videoSettings == 'none'
  245. ? false
  246. : videoSettings == 'true'
  247. ? true
  248. : { advanced: [{facingMode: videoSettings}] };
  249. log("Created videoConstraints.");
  250. if (!videoConstraints && !audioSettings) return;
  251. const stream = videoSettings == 'screen'
  252. ? await navigator.mediaDevices.getDisplayMedia({
  253. audio: audioSettings,
  254. video: true
  255. })
  256. : await navigator.mediaDevices.getUserMedia({
  257. audio: audioSettings,
  258. video: videoConstraints
  259. });
  260. log("Created stream.");
  261. if (videoConstraints) {
  262. selfView.srcObject = stream;
  263. selfView.style.display = '';
  264. }
  265. for (const track of stream.getTracks()) {
  266. log("Added track.");
  267. pc.addTrack(track, stream);
  268. }
  269. }
  270. function startStartingWithErorrHandling(fromButton) {
  271. startStreaming(fromButton)
  272. .then(() => {
  273. log("startStreaming() finished.");
  274. })
  275. .catch(e => {
  276. log("startStreaming() errored: " + e.message);
  277. });
  278. }
  279. start.addEventListener("click", _ => {
  280. startStartingWithErorrHandling(true)
  281. });
  282. async function receiveMessage(e) {
  283. qrcode.style.display = 'none';
  284. log("In webSocket.onmessage...");
  285. logPre(e.data);
  286. const data = JSON.parse(e.data);
  287. if (data.requestSettings) {
  288. settings = readSettingsForm(true);
  289. startStartingWithErorrHandling(false);
  290. } else if (data.settings) {
  291. settings = data.settings;
  292. startStartingWithErorrHandling(false);
  293. } else if (data.description) {
  294. await pc.setRemoteDescription(data.description);
  295. if (data.description.type == "offer") {
  296. log("Got an offer...");
  297. await pc.setLocalDescription(await pc.createAnswer());
  298. sendJson({
  299. description: pc.localDescription
  300. });
  301. }
  302. } else if (data.candidate) {
  303. log("Adding ice candidate...");
  304. await pc.addIceCandidate(data.candidate);
  305. }
  306. };
  307. function createWebSocket() {
  308. const webSocket = new WebSocket(
  309. 'ws' + (window.location.protocol == 'https:' ? 's' : '') + '://'
  310. + window.location.host
  311. + '/camera/ws/' + (isHost ? 'host' : 'client') + '/'
  312. + roomName
  313. + '/'
  314. );
  315. log("Created WebSocket.");
  316. webSocket.onclose = function(e) {
  317. log('WebSocket closed unexpectedly: ' + e);
  318. };
  319. webSocket.onerror = function(e) {
  320. log('WebSocket error: ' + e);
  321. };
  322. webSocket.onmessage = receiveMessage;
  323. return webSocket;
  324. }
  325. webSocket = createWebSocket();
  326. if (!isHost) {
  327. webSocket.onopen = _ => sendJson({requestSettings: true});
  328. }
  329. log("Finished <script> block.");
  330. </script>
  331. </body>
  332. </html>