RTCDataChannel-send.html (18128B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <meta name="timeout" content="long"> 4 <title>RTCDataChannel.prototype.send</title> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 <script src="RTCPeerConnection-helper.js"></script> 8 <script src="RTCDataChannel-helper.js"></script> 9 <script src="third_party/sdp/sdp.js"></script> 10 <script> 11 'use strict'; 12 13 // Test is based on the following editor draft: 14 // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html 15 16 // The following helper functions are called from RTCPeerConnection-helper.js: 17 // createDataChannelPair 18 // awaitMessage 19 // blobToArrayBuffer 20 // assert_equals_typed_array 21 22 /* 23 6.2. RTCDataChannel 24 interface RTCDataChannel : EventTarget { 25 ... 26 readonly attribute RTCDataChannelState readyState; 27 readonly attribute unsigned long bufferedAmount; 28 attribute EventHandler onmessage; 29 attribute DOMString binaryType; 30 31 void send(USVString data); 32 void send(Blob data); 33 void send(ArrayBuffer data); 34 void send(ArrayBufferView data); 35 }; 36 */ 37 38 // Simple ASCII encoded string 39 const helloString = 'hello'; 40 const emptyString = ''; 41 // ASCII encoded buffer representation of the string 42 const helloBuffer = Uint8Array.of(0x68, 0x65, 0x6c, 0x6c, 0x6f); 43 const emptyBuffer = new Uint8Array(); 44 const helloBlob = new Blob([helloBuffer]); 45 46 // Unicode string with multiple code units 47 const unicodeString = '世界你好'; 48 // UTF-8 encoded buffer representation of the string 49 const unicodeBuffer = Uint8Array.of( 50 0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c, 51 0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd); 52 53 /* 54 6.2. send() 55 2. If channel's readyState attribute is connecting, throw an InvalidStateError. 56 */ 57 test(t => { 58 const pc = new RTCPeerConnection(); 59 t.add_cleanup(() => pc.close()); 60 const channel = pc.createDataChannel('test'); 61 assert_equals(channel.readyState, 'connecting'); 62 assert_throws_dom('InvalidStateError', () => channel.send(helloString)); 63 }, 'Calling send() when data channel is in connecting state should throw InvalidStateError'); 64 65 for (const options of [{}, {negotiated: true, id: 0}]) { 66 const mode = `${options.negotiated? "Negotiated d" : "D"}atachannel`; 67 68 /* 69 6.2. send() 70 3. Execute the sub step that corresponds to the type of the methods argument: 71 72 string object 73 Let data be the object and increase the bufferedAmount attribute 74 by the number of bytes needed to express data as UTF-8. 75 76 [WebSocket] 77 5. Feedback from the protocol 78 When a WebSocket message has been received 79 4. If type indicates that the data is Text, then initialize event's data 80 attribute to data. 81 */ 82 promise_test(t => { 83 return createDataChannelPairWithLabel(t, 'string', options) 84 .then(([channel1, channel2]) => { 85 channel1.send(helloString); 86 return awaitMessage(channel2) 87 }).then(message => { 88 assert_equals(typeof message, 'string', 89 'Expect message to be a string'); 90 91 assert_equals(message, helloString); 92 }); 93 }, `${mode} should be able to send simple string and receive as string`); 94 95 promise_test(t => { 96 return createDataChannelPairWithLabel(t, 'unicode', options) 97 .then(([channel1, channel2]) => { 98 channel1.send(unicodeString); 99 return awaitMessage(channel2) 100 }).then(message => { 101 assert_equals(typeof message, 'string', 102 'Expect message to be a string'); 103 104 assert_equals(message, unicodeString); 105 }); 106 }, `${mode} should be able to send unicode string and receive as unicode string`); 107 promise_test(t => { 108 return createDataChannelPairWithLabel(t, 'recv_string', options) 109 .then(([channel1, channel2]) => { 110 channel2.binaryType = 'arraybuffer'; 111 channel1.send(helloString); 112 return awaitMessage(channel2); 113 }).then(message => { 114 assert_equals(typeof message, 'string', 115 'Expect message to be a string'); 116 117 assert_equals(message, helloString); 118 }); 119 }, `${mode} should ignore binaryType and always receive string message as string`); 120 promise_test(t => { 121 return createDataChannelPairWithLabel(t, 'emptystring', options) 122 .then(([channel1, channel2]) => { 123 channel1.send(emptyString); 124 // Send a non-empty string in case the implementation ignores empty messages 125 channel1.send(helloString); 126 return awaitMessage(channel2) 127 }).then(message => { 128 assert_equals(typeof message, 'string', 129 'Expect message to be a string'); 130 131 assert_equals(message, emptyString); 132 }); 133 }, `${mode} should be able to send an empty string and receive an empty string`); 134 135 /* 136 6.2. send() 137 3. Execute the sub step that corresponds to the type of the methods argument: 138 ArrayBufferView object 139 Let data be the data stored in the section of the buffer described 140 by the ArrayBuffer object that the ArrayBufferView object references 141 and increase the bufferedAmount attribute by the length of the 142 ArrayBufferView in bytes. 143 144 [WebSocket] 145 5. Feedback from the protocol 146 When a WebSocket message has been received 147 4. If binaryType is set to "arraybuffer", then initialize event's data 148 attribute to a new read-only ArrayBuffer object whose contents are data. 149 150 [WebIDL] 151 4.1. ArrayBufferView 152 typedef (Int8Array or Int16Array or Int32Array or 153 Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or 154 Float32Array or Float64Array or DataView) ArrayBufferView; 155 */ 156 promise_test(t => { 157 return createDataChannelPairWithLabel(t, 'array_to_arraybuffer', options) 158 .then(([channel1, channel2]) => { 159 channel2.binaryType = 'arraybuffer'; 160 channel1.send(helloBuffer); 161 return awaitMessage(channel2) 162 }).then(messageBuffer => { 163 assert_true(messageBuffer instanceof ArrayBuffer, 164 'Expect messageBuffer to be an ArrayBuffer'); 165 166 assert_equals_typed_array(messageBuffer, helloBuffer.buffer); 167 }); 168 }, `${mode} should be able to send Uint8Array message and receive as ArrayBuffer`); 169 170 /* 171 6.2. send() 172 3. Execute the sub step that corresponds to the type of the methods argument: 173 ArrayBuffer object 174 Let data be the data stored in the buffer described by the ArrayBuffer 175 object and increase the bufferedAmount attribute by the length of the 176 ArrayBuffer in bytes. 177 */ 178 promise_test(t => { 179 return createDataChannelPairWithLabel(t, 'arraybuffer', options) 180 .then(([channel1, channel2]) => { 181 channel2.binaryType = 'arraybuffer'; 182 channel1.send(helloBuffer.buffer); 183 return awaitMessage(channel2) 184 }).then(messageBuffer => { 185 assert_true(messageBuffer instanceof ArrayBuffer, 186 'Expect messageBuffer to be an ArrayBuffer'); 187 188 assert_equals_typed_array(messageBuffer, helloBuffer.buffer); 189 }); 190 }, `${mode} should be able to send ArrayBuffer message and receive as ArrayBuffer`); 191 192 promise_test(t => { 193 return createDataChannelPairWithLabel(t, 'empty_arraybuffer', options) 194 .then(([channel1, channel2]) => { 195 channel2.binaryType = 'arraybuffer'; 196 channel1.send(emptyBuffer.buffer); 197 // Send a non-empty buffer in case the implementation ignores empty messages 198 channel1.send(helloBuffer.buffer); 199 return awaitMessage(channel2) 200 }).then(messageBuffer => { 201 assert_true(messageBuffer instanceof ArrayBuffer, 202 'Expect messageBuffer to be an ArrayBuffer'); 203 204 assert_equals_typed_array(messageBuffer, emptyBuffer.buffer); 205 }); 206 }, `${mode} should be able to send an empty ArrayBuffer message and receive as ArrayBuffer`); 207 208 /* 209 6.2. send() 210 3. Execute the sub step that corresponds to the type of the methods argument: 211 Blob object 212 Let data be the raw data represented by the Blob object and increase 213 the bufferedAmount attribute by the size of data, in bytes. 214 */ 215 promise_test(t => { 216 return createDataChannelPairWithLabel(t, 'blob_to_arraybuffer', options) 217 .then(([channel1, channel2]) => { 218 channel2.binaryType = 'arraybuffer'; 219 channel1.send(helloBlob); 220 return awaitMessage(channel2); 221 }).then(messageBuffer => { 222 assert_true(messageBuffer instanceof ArrayBuffer, 223 'Expect messageBuffer to be an ArrayBuffer'); 224 225 assert_equals_typed_array(messageBuffer, helloBuffer.buffer); 226 }); 227 }, `${mode} should be able to send Blob message and receive as ArrayBuffer`); 228 229 /* 230 [WebSocket] 231 5. Feedback from the protocol 232 When a WebSocket message has been received 233 4. If binaryType is set to "blob", then initialize event's data attribute 234 to a new Blob object that represents data as its raw data. 235 */ 236 promise_test(t => { 237 return createDataChannelPairWithLabel(t, 'arraybuffer_to_blob', options) 238 .then(([channel1, channel2]) => { 239 channel2.binaryType = 'blob'; 240 channel1.send(helloBuffer); 241 return awaitMessage(channel2); 242 }) 243 .then(messageBlob => { 244 assert_true(messageBlob instanceof Blob, 245 'Expect received messageBlob to be a Blob'); 246 247 return blobToArrayBuffer(messageBlob); 248 }).then(messageBuffer => { 249 assert_true(messageBuffer instanceof ArrayBuffer, 250 'Expect messageBuffer to be an ArrayBuffer'); 251 252 assert_equals_typed_array(messageBuffer, helloBuffer.buffer); 253 }); 254 }, `${mode} should be able to send ArrayBuffer message and receive as Blob`); 255 256 /* 257 6.2. RTCDataChannel 258 binaryType 259 The binaryType attribute must, on getting, return the value to which it was 260 last set. On setting, the user agent must set the IDL attribute to the new 261 value. When a RTCDataChannel object is created, the binaryType attribute must 262 be initialized to the string "blob". 263 */ 264 promise_test(t => { 265 return createDataChannelPairWithLabel(t, 'arraybuffer_recv', options) 266 .then(([channel1, channel2]) => { 267 assert_equals(channel2.binaryType, 'arraybuffer', 268 'Expect initial binaryType value to be arraybuffer'); 269 270 channel1.send(helloBuffer); 271 return awaitMessage(channel2); 272 }).then(messageBuffer => { 273 assert_true(messageBuffer instanceof ArrayBuffer, 274 'Expect messageBuffer to be an ArrayBuffer'); 275 276 assert_equals_typed_array(messageBuffer, helloBuffer.buffer); 277 }); 278 }, `${mode} binaryType should receive message as ArrayBuffer by default`); 279 280 // Test sending 3 messages: helloBuffer, unicodeString, helloBlob 281 promise_test(t => { 282 const receivedMessages = []; 283 284 const onMessage = t.step_func(event => { 285 const { data } = event; 286 receivedMessages.push(data); 287 288 if(receivedMessages.length === 3) { 289 assert_equals_typed_array(receivedMessages[0], helloBuffer.buffer); 290 assert_equals(receivedMessages[1], unicodeString); 291 assert_equals_typed_array(receivedMessages[2], helloBuffer.buffer); 292 293 t.done(); 294 } 295 }); 296 297 return createDataChannelPairWithLabel(t, 'multitype', options) 298 .then(([channel1, channel2]) => { 299 channel2.binaryType = 'arraybuffer'; 300 channel2.addEventListener('message', onMessage); 301 302 channel1.send(helloBuffer); 303 channel1.send(unicodeString); 304 channel1.send(helloBlob); 305 306 }).catch(t.step_func(err => 307 assert_unreached(`Unexpected promise rejection: ${err}`))); 308 }, `${mode} sending multiple messages with different types should succeed and be received`); 309 310 /* 311 [Deferred] 312 6.2. RTCDataChannel 313 The send() method is being amended in w3c/webrtc-pc#1209 to throw error instead 314 of closing data channel when buffer is full 315 316 send() 317 4. If channel's underlying data transport is not established yet, or if the 318 closing procedure has started, then abort these steps. 319 5. Attempt to send data on channel's underlying data transport; if the data 320 cannot be sent, e.g. because it would need to be buffered but the buffer 321 is full, the user agent must abruptly close channel's underlying data 322 transport with an error. 323 324 test(t => { 325 const pc = new RTCPeerConnection(); 326 const channel = pc.createDataChannel('test'); 327 channel.close(); 328 assert_equals(channel.readyState, 'closing'); 329 channel.send(helloString); 330 }, 'Calling send() when data channel is in closing state should succeed'); 331 */ 332 } 333 334 promise_test(async t => { 335 // This test uses some bundle shenanigans to cause packet loss 336 // The intent is to create a situation where a small message is received 337 // when there is a partially received large message, and check whether 338 // that small message is misinterpreted as part of the large message 339 // instead of its own thing. 340 const pc1 = new RTCPeerConnection({bundlePolicy: 'balanced'}); 341 const pc2 = new RTCPeerConnection(); 342 t.add_cleanup(() => pc1.close()); 343 t.add_cleanup(() => pc2.close()); 344 pc1.addTransceiver('audio'); 345 pc1.addTransceiver('video'); 346 const channel1 = pc1.createDataChannel('foo1', {id: 1, ordered: false, negotiated: true}); 347 const channel2 = pc2.createDataChannel('foo2', {id: 1, ordered: false, negotiated: true}); 348 const channel1Open = new Promise(r => channel1.onopen = r); 349 const channel2Open = new Promise(r => channel2.onopen = r); 350 exchangeIceCandidates(pc1, pc2); 351 352 const size = 1024*1024*5; 353 const bigMessage = 'a'.repeat(size); 354 const smallMessage = 'boom'; 355 const numLittle = 1000; 356 357 const receivedMessages = new Promise((resolve, reject) => { 358 let littleReceived = 0; 359 let bigReceived = false; 360 channel2.onmessage = ({data}) => { 361 try { 362 if (data.length == size) { 363 assert_false(bigReceived, 'Only received big message once'); 364 bigReceived = true; 365 assert_equals(data, bigMessage, 'Big message has correct contents'); 366 } else if (data.length == smallMessage.length) { 367 littleReceived++; 368 assert_equals(data, smallMessage, 'Small message has correct contents'); 369 assert_less_than_equal(littleReceived, numLittle, 'Got no more than expected small messages'); 370 } else { 371 assert_true(false, `Message was an expected size (got ${data.length}, last bytes are ${data.slice(-20)})`); 372 } 373 374 if (littleReceived == 1000 && bigReceived) { 375 resolve(); 376 } 377 } catch (e) { 378 reject(e); 379 } 380 }; 381 }); 382 383 function rebundleMids(offer, mids) { 384 // Disable any existing groups 385 let sdp = offer.sdp.replaceAll('a=group:BUNDLE','a=moop:BUNGLE'); 386 sdp = sdp.replaceAll('a=msid-semantic:', 'a=msid-pedantic:'); 387 // Create group attrs for the mids we want 388 const midsString = mids.join(' '); 389 sdp = sdp.replace('\r\nm=', `\r\na=group:BUNDLE ${midsString}\r\na=msid-semantic:WMS *\r\nm=`); 390 return {type: 'offer', sdp}; 391 } 392 393 await pc1.setLocalDescription(); 394 let offer = pc1.localDescription; 395 396 // offer should have three separate transports, one for each mid 397 const sections = SDPUtils.splitSections(offer.sdp); 398 assert_equals(sections.length, 4); 399 const mids = sections.slice(1).map(section => SDPUtils.getMid(section)); 400 assert_equals(mids.length, 3); 401 402 // Cause the DataChannel msection to be bundled with the audio msection 403 await pc2.setRemoteDescription(rebundleMids(offer, [mids[0], mids[2]])); 404 await pc2.setLocalDescription(); 405 await pc1.setRemoteDescription(pc2.localDescription); 406 await channel1Open; 407 await channel2Open; 408 409 channel1.send(bigMessage); 410 411 // Send until we're almost done with the first (big) message 412 channel1.bufferedAmountLowThreshold = 20000; 413 await new Promise(r => channel1.onbufferedamountlow = r); 414 415 // Ok, now for the shenanigans 416 let reoffer = await pc1.createOffer(); 417 // Make pc2 _think_ the datachannel is now bundled with the video msection, 418 // so it will discard the packets 419 await pc2.setRemoteDescription(rebundleMids(reoffer, [mids[1], mids[2]])); 420 await pc2.setLocalDescription(); 421 422 for (let i = 0; i < numLittle; i++) { 423 channel1.send('boom'); 424 } 425 426 // Now, put it back the way it is supposed to be 427 await pc1.setLocalDescription(); 428 reoffer = pc1.localDescription; 429 await pc2.setRemoteDescription(rebundleMids(reoffer, [mids[0], mids[2]])); 430 await pc2.setLocalDescription(); 431 await pc1.setRemoteDescription(pc2.localDescription); 432 await receivedMessages; 433 }, `Sending multiple messages simultaneously in unordered mode works reliably`); 434 435 promise_test(async t => { 436 const offerer = new RTCPeerConnection(); 437 const answerer = new RTCPeerConnection(); 438 t.add_cleanup(() => offerer.close()); 439 t.add_cleanup(() => answerer.close()); 440 const channel1 = offerer.createDataChannel('foo'); 441 442 const toSend = []; 443 for (let i = 0; i < 100; i++) { 444 toSend.push(`So anyway I started blastin' ${i}`); 445 } 446 447 function blastMessages(channel) { 448 assert_equals(channel.readyState, 'open', 'channel should already be open'); 449 for (const message of toSend) { 450 channel.send(message); 451 } 452 } 453 454 // Set up both channels to send messages as soon as they are open 455 channel1.onopen = () => blastMessages(channel1); 456 457 const channel2Created = new Promise(r => { 458 answerer.ondatachannel = ({channel}) => { 459 blastMessages(channel); 460 r(channel); 461 } 462 }); 463 464 async function receiveMessages(channel) { 465 const received = []; 466 while (received.length < toSend.length) { 467 const {data} = await new Promise(r => channel.onmessage = r); 468 received.push(data); 469 } 470 return received; 471 } 472 473 const received1 = receiveMessages(channel1); 474 475 await negotiate(offerer, answerer); 476 477 const channel2 = await channel2Created; 478 assert_array_equals(await receiveMessages(channel2), toSend); 479 assert_array_equals(await received1, toSend); 480 }, 'Sending before the other side is open should work'); 481 482 483 </script>