James Kuszmaul | 4a42b18 | 2021-01-17 11:32:46 -0800 | [diff] [blame^] | 1 | <!DOCTYPE html> |
| 2 | <html> |
| 3 | <head lang="en"> |
| 4 | <meta charset="UTF-8"> |
| 5 | <title>WebRTC Data Channel Example</title> |
| 6 | <style> |
| 7 | @keyframes new-fade { |
| 8 | 0% { |
| 9 | background-color: #fffb85; |
| 10 | } |
| 11 | 100% { |
| 12 | background-color: transparent; |
| 13 | } |
| 14 | } |
| 15 | |
| 16 | html, body { |
| 17 | margin: 0; |
| 18 | padding: 0; |
| 19 | height: 100%; |
| 20 | } |
| 21 | body { |
| 22 | height: 100vh; |
| 23 | display: flex; |
| 24 | } |
| 25 | |
| 26 | main { |
| 27 | width: 100%; |
| 28 | display: flex; |
| 29 | flex-grow: 1; |
| 30 | flex-wrap: nowrap; |
| 31 | } |
| 32 | |
| 33 | section, aside { |
| 34 | padding: 5px; |
| 35 | flex-grow: 1; |
| 36 | flex-basis: 0; |
| 37 | } |
| 38 | aside { |
| 39 | overflow-y: auto; |
| 40 | } |
| 41 | |
| 42 | section textarea { |
| 43 | width: 100%; |
| 44 | } |
| 45 | |
| 46 | #log { |
| 47 | margin: 0; |
| 48 | padding: 0; |
| 49 | flex-grow: 1; |
| 50 | flex-basis: 0; |
| 51 | } |
| 52 | #log > * { |
| 53 | margin: 2px 0; |
| 54 | padding: 0 6px; |
| 55 | animation: new-fade 1.5s ease-out 1; |
| 56 | } |
| 57 | #log .debug, #log .log { |
| 58 | border-left: 2px solid lightgrey; |
| 59 | } |
| 60 | #log .error { |
| 61 | border-left: 2px solid red; |
| 62 | } |
| 63 | #log .info { |
| 64 | border-left: 2px solid cornflowerblue; |
| 65 | } |
| 66 | #log .warn { |
| 67 | border-left: 2px solid orange; |
| 68 | } |
| 69 | </style> |
| 70 | </head> |
| 71 | <body> |
| 72 | |
| 73 | <main> |
| 74 | <section> |
| 75 | <form autocomplete="off"> |
| 76 | <!-- Role --> |
| 77 | <label for="role">Role: Offering</label> |
| 78 | <input type="checkbox" id="role" checked> |
| 79 | <br> |
| 80 | |
| 81 | <!-- Message size to be used --> |
| 82 | <label for="message-size">Message size (bytes):</label> |
| 83 | <input id="message-size" type="number" name="message-size" value="16384" step="16384"> |
| 84 | <br> |
| 85 | |
| 86 | <!-- WebSocket URL & Start button --> |
| 87 | <input type="text" id="ws-url" placeholder="Optional Server URL"> |
| 88 | <button type="button" id="start" disabled>Start</button> |
| 89 | <br><hr> |
| 90 | |
| 91 | <!-- Copy local description from this textarea --> |
| 92 | Copy local description:<br> |
| 93 | <textarea rows="15" id="local-description" disabled readonly></textarea> |
| 94 | |
| 95 | <!-- Paste remote description into this textarea --> |
| 96 | Paste remote description:<br> |
| 97 | <textarea rows="15" id="remote-description" disabled></textarea> |
| 98 | <br> |
| 99 | </form> |
| 100 | </section> |
| 101 | <aside> |
| 102 | <pre id="log"></pre> |
| 103 | </aside> |
| 104 | </main> |
| 105 | |
| 106 | <!-- Import dependencies --> |
| 107 | <script src="https://webrtc.github.io/adapter/adapter-6.1.1.js"></script> |
| 108 | <script src="signaling.js"></script> |
| 109 | <script src="peerconnection.js"></script> |
| 110 | |
| 111 | <!-- UI code --> |
| 112 | <script> |
| 113 | 'use strict'; |
| 114 | |
| 115 | // Get elements |
| 116 | const roleCheckbox = document.getElementById('role'); |
| 117 | const messageSizeInput = document.getElementById('message-size'); |
| 118 | const localDescriptionTextarea = document.getElementById('local-description'); |
| 119 | const remoteDescriptionTextarea = document.getElementById('remote-description'); |
| 120 | const wsUrlInput = document.getElementById('ws-url'); |
| 121 | const startButton = document.getElementById('start'); |
| 122 | const logPre = document.getElementById('log'); |
| 123 | |
| 124 | // Load & store persistent values |
| 125 | if (typeof(Storage) !== 'undefined') { |
| 126 | const persistentElements = [ |
| 127 | ['role', 'checked', (value) => value === 'true'], |
| 128 | ['message-size', 'value'], |
| 129 | ['ws-url', 'value'] |
| 130 | ]; |
| 131 | for (const [id, property, transform] of persistentElements) { |
| 132 | let value = localStorage.getItem(id); |
| 133 | const element = document.getElementById(id); |
| 134 | if (transform !== undefined) { |
| 135 | value = transform(value); |
| 136 | } |
| 137 | if (value !== null) { |
| 138 | element[property] = value; |
| 139 | } |
| 140 | element.addEventListener('change', () => { |
| 141 | localStorage.setItem(id, element[property]); |
| 142 | }); |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | // Display console logs in the browser as well |
| 147 | for (const name of ['debug', 'error', 'info', 'log', 'warn']) { |
| 148 | const method = window.console[name]; |
| 149 | window.console[name] = function() { |
| 150 | method.apply(null, arguments); |
| 151 | const entry = document.createElement('div'); |
| 152 | entry.classList.add(name); |
| 153 | for (let i = 0; i < arguments.length; ++i) { |
| 154 | let item = arguments[i]; |
| 155 | if (typeof arguments[i] === 'object') { |
| 156 | entry.innerHTML += JSON.stringify(item, null, 2) + ' '; |
| 157 | } else { |
| 158 | entry.innerHTML += item + ' '; |
| 159 | } |
| 160 | } |
| 161 | logPre.prepend(entry); |
| 162 | }; |
| 163 | } |
| 164 | |
| 165 | // Auto-select all text when clicking local description |
| 166 | localDescriptionTextarea.addEventListener('click', function() { |
| 167 | this.select(); |
| 168 | }); |
| 169 | |
| 170 | // Bind start button & enable |
| 171 | startButton.addEventListener('click', () => { |
| 172 | roleCheckbox.disabled = true; |
| 173 | messageSizeInput.disabled = true; |
| 174 | wsUrlInput.disabled = true; |
| 175 | startButton.disabled = true; |
| 176 | localDescriptionTextarea.disabled = false; |
| 177 | start(roleCheckbox.checked); |
| 178 | }); |
| 179 | startButton.disabled = false; |
| 180 | |
| 181 | const start = (offering) => { |
| 182 | console.info('Starting with role:', offering ? 'Offering' : 'Answering'); |
| 183 | |
| 184 | // Create signaling instance |
| 185 | const wsUrl = wsUrlInput.value; |
| 186 | let signaling; |
| 187 | if (wsUrl === '') { |
| 188 | signaling = new CopyPasteSignaling(); |
| 189 | } else { |
| 190 | signaling = new WebSocketSignaling(wsUrl + (offering ? '/1' : '/0')); |
| 191 | } |
| 192 | signaling.onLocalDescriptionUpdate = (description) => { |
| 193 | localDescriptionTextarea.value = JSON.stringify(description); |
| 194 | |
| 195 | // Enable remote description once local description has been set |
| 196 | remoteDescriptionTextarea.disabled = false; |
| 197 | }; |
| 198 | signaling.onRemoteDescriptionUpdate = (description) => { |
| 199 | remoteDescriptionTextarea.value = JSON.stringify(description); |
| 200 | }; |
| 201 | |
| 202 | // Create peer connection instance |
| 203 | const pc = new WebRTCPeerConnection(signaling, offering); |
| 204 | window.pc = pc; |
| 205 | |
| 206 | // Apply remote description when pasting |
| 207 | const onRemoteDescriptionTextareaChange = () => { |
| 208 | // Remove event listener |
| 209 | remoteDescriptionTextarea.oninput = null; |
| 210 | remoteDescriptionTextarea.onchange = null; |
| 211 | |
| 212 | // Apply remote description once (needs to include candidates) |
| 213 | const description = JSON.parse(remoteDescriptionTextarea.value); |
| 214 | signaling.handleRemoteDescription(description, true) |
| 215 | .catch((error) => console.error(error)); |
| 216 | |
| 217 | // Make read-only |
| 218 | remoteDescriptionTextarea.readOnly = true; |
| 219 | }; |
| 220 | remoteDescriptionTextarea.oninput = onRemoteDescriptionTextareaChange; |
| 221 | remoteDescriptionTextarea.onchange = onRemoteDescriptionTextareaChange; |
| 222 | |
| 223 | // Enable remote description early (if not offering) |
| 224 | if (!offering) { |
| 225 | remoteDescriptionTextarea.disabled = false; |
| 226 | } |
| 227 | |
| 228 | // Get message size |
| 229 | const messageSize = parseInt(messageSizeInput.value); |
| 230 | if (isNaN(messageSize)) { |
| 231 | throw 'Invalid message size value'; |
| 232 | } |
| 233 | |
| 234 | // Create data channels |
| 235 | const createDataChannelWithName = ( |
| 236 | name, options = null, createOnOpenWithName = null, createOnOpenOptions = null |
| 237 | ) => { |
| 238 | const dc = pc.createDataChannel(name, options); |
| 239 | const defaultOnOpenHandler = dc.onopen; |
| 240 | dc.onopen = (event) => { |
| 241 | defaultOnOpenHandler(event); |
| 242 | if (createOnOpenWithName !== null) { |
| 243 | window.setTimeout(() => { |
| 244 | createDataChannelWithName(createOnOpenWithName, createOnOpenOptions); |
| 245 | }, 1000); |
| 246 | } |
| 247 | if (messageSize > pc.pc.sctp.maxMessageSize) { |
| 248 | console.warn(dc._name, 'message size (' + messageSize + ') > maximum message size' + |
| 249 | ' (' + pc.pc.sctp.maxMessageSize + ')'); |
| 250 | } |
| 251 | let data = new Uint8Array(messageSize); |
| 252 | console.log(dc._name, 'outgoing message (' + data.byteLength + ' bytes)'); |
| 253 | try { |
| 254 | dc.send(data); |
| 255 | } catch (error) { |
| 256 | if (error.name === 'TypeError') { |
| 257 | console.error(dc._name, 'message too large to send'); |
| 258 | } else { |
| 259 | console.error(dc._name, 'Unknown error:', error.name); |
| 260 | } |
| 261 | } |
| 262 | }; |
| 263 | }; |
| 264 | createDataChannelWithName('cat-noises', { |
| 265 | negotiated: true, |
| 266 | id: 0, |
| 267 | }, 'dinosaur-noises'); |
| 268 | }; |
| 269 | |
| 270 | // Introduction |
| 271 | console.info("Hello! Press 'Start' when you're ready."); |
| 272 | </script> |
| 273 | |
| 274 | </body> |
| 275 | </html> |