blob: f8b27b930ddc97227548553600bdfffd15012f28 [file] [log] [blame]
James Kuszmaul4a42b182021-01-17 11:32:46 -08001<!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
116const roleCheckbox = document.getElementById('role');
117const messageSizeInput = document.getElementById('message-size');
118const localDescriptionTextarea = document.getElementById('local-description');
119const remoteDescriptionTextarea = document.getElementById('remote-description');
120const wsUrlInput = document.getElementById('ws-url');
121const startButton = document.getElementById('start');
122const logPre = document.getElementById('log');
123
124// Load & store persistent values
125if (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
147for (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
166localDescriptionTextarea.addEventListener('click', function() {
167 this.select();
168});
169
170// Bind start button & enable
171startButton.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});
179startButton.disabled = false;
180
181const 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
271console.info("Hello! Press 'Start' when you're ready.");
272</script>
273
274</body>
275</html>