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.
 
 
 
 

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