| <!doctype html> |
| <meta charset=utf-8> |
| <meta name="timeout" content="long"> |
| <title>RTCDataChannel.prototype.send</title> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script src="RTCPeerConnection-helper.js"></script> |
| <script src="RTCDataChannel-helper.js"></script> |
| <script src="third_party/sdp/sdp.js"></script> |
| <script> |
| 'use strict'; |
| |
| // Test is based on the following editor draft: |
| // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html |
| |
| // The following helper functions are called from RTCPeerConnection-helper.js: |
| // createDataChannelPair |
| // awaitMessage |
| // blobToArrayBuffer |
| // assert_equals_typed_array |
| |
| /* |
| 6.2. RTCDataChannel |
| interface RTCDataChannel : EventTarget { |
| ... |
| readonly attribute RTCDataChannelState readyState; |
| readonly attribute unsigned long bufferedAmount; |
| attribute EventHandler onmessage; |
| attribute DOMString binaryType; |
| |
| void send(USVString data); |
| void send(Blob data); |
| void send(ArrayBuffer data); |
| void send(ArrayBufferView data); |
| }; |
| */ |
| |
| // Simple ASCII encoded string |
| const helloString = 'hello'; |
| const emptyString = ''; |
| // ASCII encoded buffer representation of the string |
| const helloBuffer = Uint8Array.of(0x68, 0x65, 0x6c, 0x6c, 0x6f); |
| const emptyBuffer = new Uint8Array(); |
| const helloBlob = new Blob([helloBuffer]); |
| |
| // Unicode string with multiple code units |
| const unicodeString = '世界你好'; |
| // UTF-8 encoded buffer representation of the string |
| const unicodeBuffer = Uint8Array.of( |
| 0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c, |
| 0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd); |
| |
| /* |
| 6.2. send() |
| 2. If channel's readyState attribute is connecting, throw an InvalidStateError. |
| */ |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| const channel = pc.createDataChannel('test'); |
| assert_equals(channel.readyState, 'connecting'); |
| assert_throws_dom('InvalidStateError', () => channel.send(helloString)); |
| }, 'Calling send() when data channel is in connecting state should throw InvalidStateError'); |
| |
| for (const options of [{}, {negotiated: true, id: 0}]) { |
| const mode = `${options.negotiated? "Negotiated d" : "D"}atachannel`; |
| |
| /* |
| 6.2. send() |
| 3. Execute the sub step that corresponds to the type of the methods argument: |
| |
| string object |
| Let data be the object and increase the bufferedAmount attribute |
| by the number of bytes needed to express data as UTF-8. |
| |
| [WebSocket] |
| 5. Feedback from the protocol |
| When a WebSocket message has been received |
| 4. If type indicates that the data is Text, then initialize event's data |
| attribute to data. |
| */ |
| promise_test(t => { |
| return createDataChannelPair(t, options) |
| .then(([channel1, channel2]) => { |
| channel1.send(helloString); |
| return awaitMessage(channel2) |
| }).then(message => { |
| assert_equals(typeof message, 'string', |
| 'Expect message to be a string'); |
| |
| assert_equals(message, helloString); |
| }); |
| }, `${mode} should be able to send simple string and receive as string`); |
| |
| promise_test(t => { |
| return createDataChannelPair(t, options) |
| .then(([channel1, channel2]) => { |
| channel1.send(unicodeString); |
| return awaitMessage(channel2) |
| }).then(message => { |
| assert_equals(typeof message, 'string', |
| 'Expect message to be a string'); |
| |
| assert_equals(message, unicodeString); |
| }); |
| }, `${mode} should be able to send unicode string and receive as unicode string`); |
| promise_test(t => { |
| return createDataChannelPair(t, options) |
| .then(([channel1, channel2]) => { |
| channel2.binaryType = 'arraybuffer'; |
| channel1.send(helloString); |
| return awaitMessage(channel2); |
| }).then(message => { |
| assert_equals(typeof message, 'string', |
| 'Expect message to be a string'); |
| |
| assert_equals(message, helloString); |
| }); |
| }, `${mode} should ignore binaryType and always receive string message as string`); |
| promise_test(t => { |
| return createDataChannelPair(t, options) |
| .then(([channel1, channel2]) => { |
| channel1.send(emptyString); |
| // Send a non-empty string in case the implementation ignores empty messages |
| channel1.send(helloString); |
| return awaitMessage(channel2) |
| }).then(message => { |
| assert_equals(typeof message, 'string', |
| 'Expect message to be a string'); |
| |
| assert_equals(message, emptyString); |
| }); |
| }, `${mode} should be able to send an empty string and receive an empty string`); |
| |
| /* |
| 6.2. send() |
| 3. Execute the sub step that corresponds to the type of the methods argument: |
| ArrayBufferView object |
| Let data be the data stored in the section of the buffer described |
| by the ArrayBuffer object that the ArrayBufferView object references |
| and increase the bufferedAmount attribute by the length of the |
| ArrayBufferView in bytes. |
| |
| [WebSocket] |
| 5. Feedback from the protocol |
| When a WebSocket message has been received |
| 4. If binaryType is set to "arraybuffer", then initialize event's data |
| attribute to a new read-only ArrayBuffer object whose contents are data. |
| |
| [WebIDL] |
| 4.1. ArrayBufferView |
| typedef (Int8Array or Int16Array or Int32Array or |
| Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or |
| Float32Array or Float64Array or DataView) ArrayBufferView; |
| */ |
| promise_test(t => { |
| return createDataChannelPair(t, options) |
| .then(([channel1, channel2]) => { |
| channel2.binaryType = 'arraybuffer'; |
| channel1.send(helloBuffer); |
| return awaitMessage(channel2) |
| }).then(messageBuffer => { |
| assert_true(messageBuffer instanceof ArrayBuffer, |
| 'Expect messageBuffer to be an ArrayBuffer'); |
| |
| assert_equals_typed_array(messageBuffer, helloBuffer.buffer); |
| }); |
| }, `${mode} should be able to send Uint8Array message and receive as ArrayBuffer`); |
| |
| /* |
| 6.2. send() |
| 3. Execute the sub step that corresponds to the type of the methods argument: |
| ArrayBuffer object |
| Let data be the data stored in the buffer described by the ArrayBuffer |
| object and increase the bufferedAmount attribute by the length of the |
| ArrayBuffer in bytes. |
| */ |
| promise_test(t => { |
| return createDataChannelPair(t, options) |
| .then(([channel1, channel2]) => { |
| channel2.binaryType = 'arraybuffer'; |
| channel1.send(helloBuffer.buffer); |
| return awaitMessage(channel2) |
| }).then(messageBuffer => { |
| assert_true(messageBuffer instanceof ArrayBuffer, |
| 'Expect messageBuffer to be an ArrayBuffer'); |
| |
| assert_equals_typed_array(messageBuffer, helloBuffer.buffer); |
| }); |
| }, `${mode} should be able to send ArrayBuffer message and receive as ArrayBuffer`); |
| |
| promise_test(t => { |
| return createDataChannelPair(t, options) |
| .then(([channel1, channel2]) => { |
| channel2.binaryType = 'arraybuffer'; |
| channel1.send(emptyBuffer.buffer); |
| // Send a non-empty buffer in case the implementation ignores empty messages |
| channel1.send(helloBuffer.buffer); |
| return awaitMessage(channel2) |
| }).then(messageBuffer => { |
| assert_true(messageBuffer instanceof ArrayBuffer, |
| 'Expect messageBuffer to be an ArrayBuffer'); |
| |
| assert_equals_typed_array(messageBuffer, emptyBuffer.buffer); |
| }); |
| }, `${mode} should be able to send an empty ArrayBuffer message and receive as ArrayBuffer`); |
| |
| /* |
| 6.2. send() |
| 3. Execute the sub step that corresponds to the type of the methods argument: |
| Blob object |
| Let data be the raw data represented by the Blob object and increase |
| the bufferedAmount attribute by the size of data, in bytes. |
| */ |
| promise_test(t => { |
| return createDataChannelPair(t, options) |
| .then(([channel1, channel2]) => { |
| channel2.binaryType = 'arraybuffer'; |
| channel1.send(helloBlob); |
| return awaitMessage(channel2); |
| }).then(messageBuffer => { |
| assert_true(messageBuffer instanceof ArrayBuffer, |
| 'Expect messageBuffer to be an ArrayBuffer'); |
| |
| assert_equals_typed_array(messageBuffer, helloBuffer.buffer); |
| }); |
| }, `${mode} should be able to send Blob message and receive as ArrayBuffer`); |
| |
| /* |
| [WebSocket] |
| 5. Feedback from the protocol |
| When a WebSocket message has been received |
| 4. If binaryType is set to "blob", then initialize event's data attribute |
| to a new Blob object that represents data as its raw data. |
| */ |
| promise_test(t => { |
| return createDataChannelPair(t, options) |
| .then(([channel1, channel2]) => { |
| channel2.binaryType = 'blob'; |
| channel1.send(helloBuffer); |
| return awaitMessage(channel2); |
| }) |
| .then(messageBlob => { |
| assert_true(messageBlob instanceof Blob, |
| 'Expect received messageBlob to be a Blob'); |
| |
| return blobToArrayBuffer(messageBlob); |
| }).then(messageBuffer => { |
| assert_true(messageBuffer instanceof ArrayBuffer, |
| 'Expect messageBuffer to be an ArrayBuffer'); |
| |
| assert_equals_typed_array(messageBuffer, helloBuffer.buffer); |
| }); |
| }, `${mode} should be able to send ArrayBuffer message and receive as Blob`); |
| |
| /* |
| 6.2. RTCDataChannel |
| binaryType |
| The binaryType attribute must, on getting, return the value to which it was |
| last set. On setting, the user agent must set the IDL attribute to the new |
| value. When a RTCDataChannel object is created, the binaryType attribute must |
| be initialized to the string "blob". |
| */ |
| promise_test(t => { |
| return createDataChannelPair(t, options) |
| .then(([channel1, channel2]) => { |
| assert_equals(channel2.binaryType, 'arraybuffer', |
| 'Expect initial binaryType value to be arraybuffer'); |
| |
| channel1.send(helloBuffer); |
| return awaitMessage(channel2); |
| }).then(messageBuffer => { |
| assert_true(messageBuffer instanceof ArrayBuffer, |
| 'Expect messageBuffer to be an ArrayBuffer'); |
| |
| assert_equals_typed_array(messageBuffer, helloBuffer.buffer); |
| }); |
| }, `${mode} binaryType should receive message as ArrayBuffer by default`); |
| |
| // Test sending 3 messages: helloBuffer, unicodeString, helloBlob |
| async_test(t => { |
| const receivedMessages = []; |
| |
| const onMessage = t.step_func(event => { |
| const { data } = event; |
| receivedMessages.push(data); |
| |
| if(receivedMessages.length === 3) { |
| assert_equals_typed_array(receivedMessages[0], helloBuffer.buffer); |
| assert_equals(receivedMessages[1], unicodeString); |
| assert_equals_typed_array(receivedMessages[2], helloBuffer.buffer); |
| |
| t.done(); |
| } |
| }); |
| |
| createDataChannelPair(t, options) |
| .then(([channel1, channel2]) => { |
| channel2.binaryType = 'arraybuffer'; |
| channel2.addEventListener('message', onMessage); |
| |
| channel1.send(helloBuffer); |
| channel1.send(unicodeString); |
| channel1.send(helloBlob); |
| |
| }).catch(t.step_func(err => |
| assert_unreached(`Unexpected promise rejection: ${err}`))); |
| }, `${mode} sending multiple messages with different types should succeed and be received`); |
| |
| /* |
| [Deferred] |
| 6.2. RTCDataChannel |
| The send() method is being amended in w3c/webrtc-pc#1209 to throw error instead |
| of closing data channel when buffer is full |
| |
| send() |
| 4. If channel's underlying data transport is not established yet, or if the |
| closing procedure has started, then abort these steps. |
| 5. Attempt to send data on channel's underlying data transport; if the data |
| cannot be sent, e.g. because it would need to be buffered but the buffer |
| is full, the user agent must abruptly close channel's underlying data |
| transport with an error. |
| |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| const channel = pc.createDataChannel('test'); |
| channel.close(); |
| assert_equals(channel.readyState, 'closing'); |
| channel.send(helloString); |
| }, 'Calling send() when data channel is in closing state should succeed'); |
| */ |
| } |
| |
| promise_test(async t => { |
| // This test uses some bundle shenanigans to cause packet loss |
| // The intent is to create a situation where a small message is received |
| // when there is a partially received large message, and check whether |
| // that small message is misinterpreted as part of the large message |
| // instead of its own thing. |
| const pc1 = new RTCPeerConnection({bundlePolicy: 'balanced'}); |
| const pc2 = new RTCPeerConnection(); |
| pc1.addTransceiver('audio'); |
| pc1.addTransceiver('video'); |
| const channel1 = pc1.createDataChannel('foo', {id: 1, ordered: false, negotiated: true}); |
| const channel2 = pc2.createDataChannel('foo', {id: 1, ordered: false, negotiated: true}); |
| const channel1Open = new Promise(r => channel1.onopen = r); |
| const channel2Open = new Promise(r => channel2.onopen = r); |
| exchangeIceCandidates(pc1, pc2); |
| |
| const size = 1024*1024*5; |
| const bigMessage = 'a'.repeat(size); |
| const smallMessage = 'boom'; |
| const numLittle = 1000; |
| |
| const receivedMessages = new Promise((resolve, reject) => { |
| let littleReceived = 0; |
| let bigReceived = false; |
| channel2.onmessage = ({data}) => { |
| try { |
| if (data.length == size) { |
| assert_false(bigReceived, 'Only received big message once'); |
| bigReceived = true; |
| assert_equals(data, bigMessage, 'Big message has correct contents'); |
| } else if (data.length == smallMessage.length) { |
| littleReceived++; |
| assert_equals(data, smallMessage, 'Small message has correct contents'); |
| assert_less_than_equal(littleReceived, numLittle, 'Got no more than expected small messages'); |
| } else { |
| assert_true(false, `Message was an expected size (got ${data.length}, last bytes are ${data.slice(-20)})`); |
| } |
| |
| if (littleReceived == 1000 && bigReceived) { |
| resolve(); |
| } |
| } catch (e) { |
| reject(e); |
| } |
| }; |
| }); |
| |
| function rebundleMids(offer, mids) { |
| // Disable any existing groups |
| let sdp = offer.sdp.replaceAll('a=group:BUNDLE','a=moop:BUNGLE'); |
| sdp = sdp.replaceAll('a=msid-semantic:', 'a=msid-pedantic:'); |
| // Create group attrs for the mids we want |
| const midsString = mids.join(' '); |
| sdp = sdp.replace('\r\nm=', `\r\na=group:BUNDLE ${midsString}\r\na=msid-semantic:WMS *\r\nm=`); |
| return {type: 'offer', sdp}; |
| } |
| |
| await pc1.setLocalDescription(); |
| let offer = pc1.localDescription; |
| |
| // offer should have three separate transports, one for each mid |
| const sections = SDPUtils.splitSections(offer.sdp); |
| assert_equals(sections.length, 4); |
| const mids = sections.slice(1).map(section => SDPUtils.getMid(section)); |
| assert_equals(mids.length, 3); |
| |
| // Cause the DataChannel msection to be bundled with the audio msection |
| await pc2.setRemoteDescription(rebundleMids(offer, [mids[0], mids[2]])); |
| await pc2.setLocalDescription(); |
| await pc1.setRemoteDescription(pc2.localDescription); |
| await channel1Open; |
| await channel2Open; |
| |
| channel1.send(bigMessage); |
| |
| // Send until we're almost done with the first (big) message |
| channel1.bufferedAmountLowThreshold = 20000; |
| await new Promise(r => channel1.onbufferedamountlow = r); |
| |
| // Ok, now for the shenanigans |
| let reoffer = await pc1.createOffer(); |
| // Make pc2 _think_ the datachannel is now bundled with the video msection, |
| // so it will discard the packets |
| await pc2.setRemoteDescription(rebundleMids(reoffer, [mids[1], mids[2]])); |
| await pc2.setLocalDescription(); |
| |
| for (let i = 0; i < numLittle; i++) { |
| channel1.send('boom'); |
| } |
| |
| // Now, put it back the way it is supposed to be |
| await pc1.setLocalDescription(); |
| reoffer = pc1.localDescription; |
| await pc2.setRemoteDescription(rebundleMids(reoffer, [mids[0], mids[2]])); |
| await pc2.setLocalDescription(); |
| await pc1.setRemoteDescription(pc2.localDescription); |
| await receivedMessages; |
| }, `Sending multiple messages simultaneously in unordered mode works reliably`); |
| |
| promise_test(async () => { |
| const offerer = new RTCPeerConnection(); |
| const answerer = new RTCPeerConnection(); |
| const channel1 = offerer.createDataChannel('foo'); |
| |
| const toSend = []; |
| for (let i = 0; i < 100; i++) { |
| toSend.push(`So anyway I started blastin' ${i}`); |
| } |
| |
| function blastMessages(channel) { |
| assert_equals(channel.readyState, 'open', 'channel should already be open'); |
| for (const message of toSend) { |
| channel.send(message); |
| } |
| } |
| |
| // Set up both channels to send messages as soon as they are open |
| channel1.onopen = () => blastMessages(channel1); |
| |
| const channel2Created = new Promise(r => { |
| answerer.ondatachannel = ({channel}) => { |
| blastMessages(channel); |
| r(channel); |
| } |
| }); |
| |
| async function receiveMessages(channel) { |
| const received = []; |
| while (received.length < toSend.length) { |
| const {data} = await new Promise(r => channel.onmessage = r); |
| received.push(data); |
| } |
| return received; |
| } |
| |
| const received1 = receiveMessages(channel1); |
| |
| await negotiate(offerer, answerer); |
| |
| const channel2 = await channel2Created; |
| assert_array_equals(await receiveMessages(channel2), toSend); |
| assert_array_equals(await received1, toSend); |
| }, 'Sending before the other side is open should work'); |
| |
| |
| </script> |