| <!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(); | |
| t.add_cleanup(() => pc.close()); | |
| 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 createDataChannelPairWithLabel(t, 'string', 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 createDataChannelPairWithLabel(t, 'unicode', 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 createDataChannelPairWithLabel(t, 'recv_string', 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 createDataChannelPairWithLabel(t, 'emptystring', 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 createDataChannelPairWithLabel(t, 'array_to_arraybuffer', 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 createDataChannelPairWithLabel(t, 'arraybuffer', 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 createDataChannelPairWithLabel(t, 'empty_arraybuffer', 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 createDataChannelPairWithLabel(t, 'blob_to_arraybuffer', 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 createDataChannelPairWithLabel(t, 'arraybuffer_to_blob', 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 createDataChannelPairWithLabel(t, 'arraybuffer_recv', 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 | |
| promise_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(); | |
| } | |
| }); | |
| return createDataChannelPairWithLabel(t, 'multitype', 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(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1.addTransceiver('audio'); | |
| pc1.addTransceiver('video'); | |
| const channel1 = pc1.createDataChannel('foo1', {id: 1, ordered: false, negotiated: true}); | |
| const channel2 = pc2.createDataChannel('foo2', {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; | |
| // TODO: crbug.com/490588131: this exceeds max-message-size which leads to unrelated failures. | |
| // Setting this to max-message-size causes a crash however. | |
| 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 t => { | |
| const offerer = new RTCPeerConnection(); | |
| const answerer = new RTCPeerConnection(); | |
| t.add_cleanup(() => offerer.close()); | |
| t.add_cleanup(() => answerer.close()); | |
| 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'); | |
| promise_test(async t => { | |
| const offerer = new RTCPeerConnection(); | |
| const answerer = new RTCPeerConnection(); | |
| t.add_cleanup(() => offerer.close()); | |
| t.add_cleanup(() => answerer.close()); | |
| const sender = offerer.createDataChannel('foo'); | |
| const data = 'something'; | |
| sender.onopen = () => { | |
| sender.send(data); | |
| }; | |
| const receiverPromise = new Promise(r => { | |
| answerer.ondatachannel = ({channel}) => { | |
| channel.onmessage = (e) => r(e.data); | |
| } | |
| }); | |
| await negotiate(offerer, answerer); | |
| assert_equals(data, await receiverPromise); | |
| }, 'Sending in onopen should work'); | |
| promise_test(async t => { | |
| const offerer = new RTCPeerConnection(); | |
| const answerer = new RTCPeerConnection(); | |
| t.add_cleanup(() => offerer.close()); | |
| t.add_cleanup(() => answerer.close()); | |
| const data = 'something'; | |
| answerer.ondatachannel = ({channel}) => { | |
| channel.send(data);onmessage = (e) => r(e.data); | |
| } | |
| const sender = offerer.createDataChannel('foo'); | |
| const senderPromise = new Promise(r => { | |
| sender.onmessage = e => r(data); | |
| }); | |
| await negotiate(offerer, answerer); | |
| assert_equals(data, await senderPromise); | |
| }, 'Sending in ondatachannel should work'); | |
| </script> |