| <!doctype html> | |
| <meta charset=utf-8> | |
| <meta name="timeout" content="long"> | |
| <title>RTCPeerConnection.prototype.createDataChannel</title> | |
| <script src=/resources/testharness.js></script> | |
| <script src=/resources/testharnessreport.js></script> | |
| <script src="RTCPeerConnection-helper.js"></script> | |
| <script> | |
| 'use strict'; | |
| const stopTracks = (...streams) => { | |
| streams.forEach(stream => stream.getTracks().forEach(track => track.stop())); | |
| }; | |
| // Test is based on the following revision: | |
| // https://rawgit.com/w3c/webrtc-pc/1cc5bfc3ff18741033d804c4a71f7891242fb5b3/webrtc.html | |
| /* | |
| 6.1. RTCPeerConnection Interface Extensions | |
| partial interface RTCPeerConnection { | |
| [...] | |
| RTCDataChannel createDataChannel(USVString label, | |
| optional RTCDataChannelInit dataChannelDict); | |
| [...] | |
| }; | |
| 6.2. RTCDataChannel | |
| interface RTCDataChannel : EventTarget { | |
| readonly attribute USVString label; | |
| readonly attribute boolean ordered; | |
| readonly attribute unsigned short? maxPacketLifeTime; | |
| readonly attribute unsigned short? maxRetransmits; | |
| readonly attribute USVString protocol; | |
| readonly attribute boolean negotiated; | |
| readonly attribute unsigned short? id; | |
| readonly attribute RTCDataChannelState readyState; | |
| readonly attribute unsigned long bufferedAmount; | |
| attribute unsigned long bufferedAmountLowThreshold; | |
| [...] | |
| attribute DOMString binaryType; | |
| [...] | |
| }; | |
| dictionary RTCDataChannelInit { | |
| boolean ordered = true; | |
| unsigned short maxPacketLifeTime; | |
| unsigned short maxRetransmits; | |
| USVString protocol = ""; | |
| boolean negotiated = false; | |
| [EnforceRange] | |
| unsigned short id; | |
| }; | |
| */ | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| assert_equals(pc.createDataChannel.length, 1); | |
| assert_throws_js(TypeError, () => pc.createDataChannel()); | |
| }, 'createDataChannel with no argument should throw TypeError'); | |
| /* | |
| 6.2. createDataChannel | |
| 2. If connection's [[isClosed]] slot is true, throw an InvalidStateError. | |
| */ | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| pc.close(); | |
| assert_equals(pc.signalingState, 'closed', 'signaling state'); | |
| assert_throws_dom('InvalidStateError', () => pc.createDataChannel('')); | |
| }, 'createDataChannel with closed connection should throw InvalidStateError'); | |
| /* | |
| 6.1. createDataChannel | |
| 4. Let channel have a [[DataChannelLabel]] internal slot initialized to the value of the | |
| first argument. | |
| 6. Let options be the second argument. | |
| 7. Let channel have an [[MaxPacketLifeTime]] internal slot initialized to | |
| option's maxPacketLifeTime member, if present, otherwise null. | |
| 8. Let channel have a [[ReadyState]] internal slot initialized to "connecting". | |
| 9. Let channel have a [[BufferedAmount]] internal slot initialized to 0. | |
| 10. Let channel have an [[MaxRetransmits]] internal slot initialized to | |
| option's maxRetransmits member, if present, otherwise null. | |
| 11. Let channel have an [[Ordered]] internal slot initialized to option's | |
| ordered member. | |
| 12. Let channel have a [[DataChannelProtocol]] internal slot initialized to option's | |
| protocol member. | |
| 14. Let channel have a [[Negotiated]] internal slot initialized to option's negotiated | |
| member. | |
| 15. Let channel have an [[DataChannelId]] internal slot initialized to option's id | |
| member, if it is present and [[Negotiated]] is true, otherwise null. | |
| 21. If the [[DataChannelId]] slot is null (due to no ID being passed into | |
| createDataChannel, or [[Negotiated]] being false), and the DTLS role of the SCTP | |
| transport has already been negotiated, then initialize [[DataChannelId]] to a value | |
| generated by the user agent, according to [RTCWEB-DATA-PROTOCOL], and skip | |
| to the next step. If no available ID could be generated, or if the value of the | |
| [[DataChannelId]] slot is being used by an existing RTCDataChannel, throw an | |
| OperationError exception. | |
| Note | |
| If the [[DataChannelId]] slot is null after this step, it will be populated once | |
| the DTLS role is determined during the process of setting an RTCSessionDescription. | |
| 22. If channel is the first RTCDataChannel created on connection, update the | |
| negotiation-needed flag for connection. | |
| 6.2. RTCDataChannel | |
| A RTCDataChannel, created with createDataChannel or dispatched via a | |
| RTCDataChannelEvent, MUST initially be in the connecting state | |
| bufferedAmountLowThreshold | |
| [...] The bufferedAmountLowThreshold is initially zero on each new RTCDataChannel, | |
| but the application may change its value at any time. | |
| binaryType | |
| [...] When a RTCDataChannel object is created, the binaryType attribute MUST | |
| be initialized to the string "blob". | |
| */ | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const dc = pc.createDataChannel(''); | |
| assert_true(dc instanceof RTCDataChannel, 'is RTCDataChannel'); | |
| assert_equals(dc.label, ''); | |
| assert_equals(dc.ordered, true); | |
| assert_equals(dc.maxPacketLifeTime, null); | |
| assert_equals(dc.maxRetransmits, null); | |
| assert_equals(dc.protocol, ''); | |
| assert_equals(dc.negotiated, false); | |
| // Since no offer/answer exchange has occurred yet, the DTLS role is unknown | |
| // and so the ID should be null. | |
| assert_equals(dc.id, null); | |
| assert_equals(dc.readyState, 'connecting'); | |
| assert_equals(dc.bufferedAmount, 0); | |
| assert_equals(dc.bufferedAmountLowThreshold, 0); | |
| assert_equals(dc.binaryType, 'arraybuffer'); | |
| }, 'createDataChannel attribute default values'); | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const dc = pc.createDataChannel('test', { | |
| ordered: false, | |
| maxRetransmits: 1, | |
| // Note: maxPacketLifeTime is not set in this test. | |
| protocol: 'custom', | |
| negotiated: true, | |
| id: 3 | |
| }); | |
| assert_true(dc instanceof RTCDataChannel, 'is RTCDataChannel'); | |
| assert_equals(dc.label, 'test'); | |
| assert_equals(dc.ordered, false); | |
| assert_equals(dc.maxPacketLifeTime, null); | |
| assert_equals(dc.maxRetransmits, 1); | |
| assert_equals(dc.protocol, 'custom'); | |
| assert_equals(dc.negotiated, true); | |
| assert_equals(dc.id, 3); | |
| assert_equals(dc.readyState, 'connecting'); | |
| assert_equals(dc.bufferedAmount, 0); | |
| assert_equals(dc.bufferedAmountLowThreshold, 0); | |
| assert_equals(dc.binaryType, 'arraybuffer'); | |
| const dc2 = pc.createDataChannel('test2', { | |
| ordered: false, | |
| maxPacketLifeTime: 42 | |
| }); | |
| assert_equals(dc2.label, 'test2'); | |
| assert_equals(dc2.maxPacketLifeTime, 42); | |
| assert_equals(dc2.maxRetransmits, null); | |
| }, 'createDataChannel with provided parameters should initialize attributes to provided values'); | |
| /* | |
| 6.2. createDataChannel | |
| 4. Let channel have a [[DataChannelLabel]] internal slot initialized to the value of the | |
| first argument. | |
| [ECMA262] 7.1.12. ToString(argument) | |
| undefined -> "undefined" | |
| null -> "null" | |
| [WebIDL] 3.10.15. Convert a DOMString to a sequence of Unicode scalar values | |
| */ | |
| const labels = [ | |
| ['"foo"', 'foo', 'foo'], | |
| ['null', null, 'null'], | |
| ['undefined', undefined, 'undefined'], | |
| ['lone surrogate', '\uD800', '\uFFFD'], | |
| ]; | |
| for (const [description, label, expected] of labels) { | |
| test(t => { | |
| const pc = new RTCPeerConnection; | |
| t.add_cleanup(() => pc.close()); | |
| const dc = pc.createDataChannel(label); | |
| assert_equals(dc.label, expected); | |
| }, `createDataChannel with label ${description} should succeed`); | |
| } | |
| /* | |
| 6.2. RTCDataChannel | |
| createDataChannel | |
| 11. Let channel have an [[Ordered]] internal slot initialized to option's | |
| ordered member. | |
| */ | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const dc = pc.createDataChannel('', { ordered: false }); | |
| assert_equals(dc.ordered, false); | |
| }, 'createDataChannel with ordered false should succeed'); | |
| // true as the default value of a boolean is confusing because null is converted | |
| // to false while undefined is converted to true. | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const dc1 = pc.createDataChannel('', { ordered: null }); | |
| assert_equals(dc1.ordered, false); | |
| const dc2 = pc.createDataChannel('', { ordered: undefined }); | |
| assert_equals(dc2.ordered, true); | |
| }, 'createDataChannel with ordered null/undefined should succeed'); | |
| /* | |
| 6.2. RTCDataChannel | |
| createDataChannel | |
| 7. Let channel have an [[MaxPacketLifeTime]] internal slot initialized to | |
| option's maxPacketLifeTime member, if present, otherwise null. | |
| */ | |
| test(t => { | |
| const pc = new RTCPeerConnection; | |
| t.add_cleanup(() => pc.close()); | |
| const dc = pc.createDataChannel('', { maxPacketLifeTime: 0 }); | |
| assert_equals(dc.maxPacketLifeTime, 0); | |
| }, 'createDataChannel with maxPacketLifeTime 0 should succeed'); | |
| /* | |
| 6.2. RTCDataChannel | |
| createDataChannel | |
| 10. Let channel have an [[MaxRetransmits]] internal slot initialized to | |
| option's maxRetransmits member, if present, otherwise null. | |
| */ | |
| test(t => { | |
| const pc = new RTCPeerConnection; | |
| t.add_cleanup(() => pc.close()); | |
| const dc = pc.createDataChannel('', { maxRetransmits: 0 }); | |
| assert_equals(dc.maxRetransmits, 0); | |
| }, 'createDataChannel with maxRetransmits 0 should succeed'); | |
| /* | |
| 6.2. createDataChannel | |
| 18. If both [[MaxPacketLifeTime]] and [[MaxRetransmits]] attributes are set (not null), | |
| throw a TypeError. | |
| */ | |
| test(t => { | |
| const pc = new RTCPeerConnection; | |
| t.add_cleanup(() => pc.close()); | |
| pc.createDataChannel('', { | |
| maxPacketLifeTime: undefined, | |
| maxRetransmits: undefined | |
| }); | |
| }, 'createDataChannel with both maxPacketLifeTime and maxRetransmits undefined should succeed'); | |
| test(t => { | |
| const pc = new RTCPeerConnection; | |
| t.add_cleanup(() => pc.close()); | |
| assert_throws_js(TypeError, () => pc.createDataChannel('', { | |
| maxPacketLifeTime: 0, | |
| maxRetransmits: 0 | |
| })); | |
| assert_throws_js(TypeError, () => pc.createDataChannel('', { | |
| maxPacketLifeTime: 42, | |
| maxRetransmits: 42 | |
| })); | |
| }, 'createDataChannel with both maxPacketLifeTime and maxRetransmits should throw TypeError'); | |
| /* | |
| 6.2. RTCDataChannel | |
| createDataChannel | |
| 12. Let channel have a [[DataChannelProtocol]] internal slot initialized to option's | |
| protocol member. | |
| */ | |
| const protocols = [ | |
| ['"foo"', 'foo', 'foo'], | |
| ['null', null, 'null'], | |
| ['undefined', undefined, ''], | |
| ['lone surrogate', '\uD800', '\uFFFD'], | |
| ]; | |
| for (const [description, protocol, expected] of protocols) { | |
| test(t => { | |
| const pc = new RTCPeerConnection; | |
| t.add_cleanup(() => pc.close()); | |
| const dc = pc.createDataChannel('', { protocol }); | |
| assert_equals(dc.protocol, expected); | |
| }, `createDataChannel with protocol ${description} should succeed`); | |
| } | |
| /* | |
| 6.2. RTCDataChannel | |
| createDataChannel | |
| 20. If [[DataChannelId]] is equal to 65535, which is greater than the maximum allowed | |
| ID of 65534 but still qualifies as an unsigned short, throw a TypeError. | |
| */ | |
| for (const id of [0, 1, 65534, 65535]) { | |
| test((t) => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const dc = pc.createDataChannel('', { id }); | |
| assert_equals(dc.id, null); | |
| }, `createDataChannel with id ${id} and negotiated not set should succeed, but not set the channel's id`); | |
| } | |
| for (const id of [0, 1, 65534]) { | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const dc = pc.createDataChannel('', { 'negotiated': true, 'id': id }); | |
| assert_equals(dc.id, id); | |
| }, `createDataChannel with id ${id} and negotiated true should succeed, and set the channel's id`); | |
| } | |
| for (const id of [-1, 65536]) { | |
| test((t) => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| assert_throws_js(TypeError, () => pc.createDataChannel('', { id })); | |
| }, `createDataChannel with id ${id} and negotiated not set should throw TypeError`); | |
| } | |
| for (const id of [-1, 65535, 65536]) { | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| assert_throws_js(TypeError, () => pc.createDataChannel('', | |
| { 'negotiated': true, 'id': id })); | |
| }, `createDataChannel with id ${id} should throw TypeError`); | |
| } | |
| /* | |
| 6.2. createDataChannel | |
| 5. If [[DataChannelLabel]] is longer than 65535 bytes, throw a TypeError. | |
| */ | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| assert_throws_js(TypeError, () => | |
| pc.createDataChannel('l'.repeat(65536))); | |
| assert_throws_js(TypeError, () => | |
| pc.createDataChannel('l'.repeat(65536), { | |
| negotiated: true, | |
| id: 42 | |
| })); | |
| }, 'createDataChannel with too long label should throw TypeError'); | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| assert_throws_js(TypeError, () => | |
| pc.createDataChannel('\u00b5'.repeat(32768))); | |
| assert_throws_js(TypeError, () => | |
| pc.createDataChannel('\u00b5'.repeat(32768), { | |
| negotiated: true, | |
| id: 42 | |
| })); | |
| }, 'createDataChannel with too long label (2 byte unicode) should throw TypeError'); | |
| /* | |
| 6.2. label | |
| [...] Scripts are allowed to create multiple RTCDataChannel objects with the same label. | |
| [...] | |
| */ | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const label = 'test'; | |
| pc.createDataChannel(label); | |
| pc.createDataChannel(label); | |
| }, 'createDataChannel with same label used twice should not throw'); | |
| /* | |
| 6.2. createDataChannel | |
| 13. If [[DataChannelProtocol]] is longer than 65535 bytes long, throw a TypeError. | |
| */ | |
| test(t => { | |
| const pc = new RTCPeerConnection; | |
| t.add_cleanup(() => pc.close()); | |
| const channel = pc.createDataChannel('', { negotiated: true, id: 42 }); | |
| assert_equals(channel.negotiated, true); | |
| }, 'createDataChannel with negotiated true and id should succeed'); | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| assert_throws_js(TypeError, () => | |
| pc.createDataChannel('', { | |
| protocol: 'p'.repeat(65536) | |
| })); | |
| assert_throws_js(TypeError, () => | |
| pc.createDataChannel('', { | |
| protocol: 'p'.repeat(65536), | |
| negotiated: true, | |
| id: 42 | |
| })); | |
| }, 'createDataChannel with too long protocol should throw TypeError'); | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| assert_throws_js(TypeError, () => | |
| pc.createDataChannel('', { | |
| protocol: '\u00b6'.repeat(32768) | |
| })); | |
| assert_throws_js(TypeError, () => | |
| pc.createDataChannel('', { | |
| protocol: '\u00b6'.repeat(32768), | |
| negotiated: true, | |
| id: 42 | |
| })); | |
| }, 'createDataChannel with too long protocol (2 byte unicode) should throw TypeError'); | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const label = 'l'.repeat(65535); | |
| const protocol = 'p'.repeat(65535); | |
| const dc = pc.createDataChannel(label, { | |
| protocol: protocol | |
| }); | |
| assert_equals(dc.label, label); | |
| assert_equals(dc.protocol, protocol); | |
| }, 'createDataChannel with maximum length label and protocol should succeed'); | |
| /* | |
| 6.2 createDataChannel | |
| 15. Let channel have an [[DataChannelId]] internal slot initialized to option's id member, | |
| if it is present and [[Negotiated]] is true, otherwise null. | |
| NOTE | |
| This means the id member will be ignored if the data channel is negotiated in-band; this | |
| is intentional. Data channels negotiated in-band should have IDs selected based on the | |
| DTLS role, as specified in [RTCWEB-DATA-PROTOCOL]. | |
| */ | |
| test(t => { | |
| const pc = new RTCPeerConnection; | |
| t.add_cleanup(() => pc.close()); | |
| const dc = pc.createDataChannel('', { | |
| negotiated: false, | |
| }); | |
| assert_equals(dc.negotiated, false, 'Expect dc.negotiated to be false'); | |
| }, 'createDataChannel with negotiated false should succeed'); | |
| test(t => { | |
| const pc = new RTCPeerConnection; | |
| t.add_cleanup(() => pc.close()); | |
| const dc = pc.createDataChannel('', { | |
| negotiated: false, | |
| id: 42 | |
| }); | |
| assert_equals(dc.negotiated, false, 'Expect dc.negotiated to be false'); | |
| assert_equals(dc.id, null, 'Expect dc.id to be ignored (null)'); | |
| }, 'createDataChannel with negotiated false and id 42 should ignore the id'); | |
| /* | |
| 6.2. createDataChannel | |
| 16. If [[Negotiated]] is true and [[DataChannelId]] is null, throw a TypeError. | |
| */ | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| assert_throws_js(TypeError, () => | |
| pc.createDataChannel('test', { | |
| negotiated: true | |
| })); | |
| }, 'createDataChannel with negotiated true and id not defined should throw TypeError'); | |
| /* | |
| 4.4.1.6. Set the RTCSessionSessionDescription | |
| 2.2.6. If description is of type "answer" or "pranswer", then run the | |
| following steps: | |
| 3. If description negotiates the DTLS role of the SCTP transport, and there is an | |
| RTCDataChannel with a null id, then generate an ID according to | |
| [RTCWEB-DATA-PROTOCOL]. [...] | |
| 6.1. createDataChannel | |
| 21. If the [[DataChannelId]] slot is null (due to no ID being passed into | |
| createDataChannel, or [[Negotiated]] being false), and the DTLS role of the SCTP | |
| transport has already been negotiated, then initialize [[DataChannelId]] to a value | |
| generated by the user agent, according to [RTCWEB-DATA-PROTOCOL], and skip | |
| to the next step. If no available ID could be generated, or if the value of the | |
| [[DataChannelId]] slot is being used by an existing RTCDataChannel, throw an | |
| OperationError exception. | |
| Note | |
| If the [[DataChannelId]] slot is null after this step, it will be populated once | |
| the DTLS role is determined during the process of setting an RTCSessionDescription. | |
| */ | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const negotiatedDc = pc1.createDataChannel('negotiated-channel', { | |
| negotiated: true, | |
| id: 42, | |
| }); | |
| assert_equals(negotiatedDc.id, 42, 'Expect negotiatedDc.id to be 42'); | |
| const dc1 = pc1.createDataChannel('channel'); | |
| assert_equals(dc1.id, null, 'Expect initial id to be null'); | |
| const offer = await pc1.createOffer(); | |
| await Promise.all([pc1.setLocalDescription(offer), pc2.setRemoteDescription(offer)]); | |
| const answer = await pc2.createAnswer(); | |
| await pc1.setRemoteDescription(answer); | |
| assert_not_equals(dc1.id, null, | |
| 'Expect dc1.id to be assigned after remote description has been set'); | |
| assert_greater_than_equal(dc1.id, 0, | |
| 'Expect dc1.id to be set to valid unsigned short'); | |
| assert_less_than(dc1.id, 65535, | |
| 'Expect dc1.id to be set to valid unsigned short'); | |
| const dc2 = pc1.createDataChannel('channel'); | |
| assert_not_equals(dc2.id, null, | |
| 'Expect dc2.id to be assigned after remote description has been set'); | |
| assert_greater_than_equal(dc2.id, 0, | |
| 'Expect dc2.id to be set to valid unsigned short'); | |
| assert_less_than(dc2.id, 65535, | |
| 'Expect dc2.id to be set to valid unsigned short'); | |
| assert_not_equals(dc2, dc1, | |
| 'Expect channels created from same label to be different'); | |
| assert_equals(dc2.label, dc1.label, | |
| 'Expect different channels can have the same label but different id'); | |
| assert_not_equals(dc2.id, dc1.id, | |
| 'Expect different channels can have the same label but different id'); | |
| assert_equals(negotiatedDc.id, 42, | |
| 'Expect negotiatedDc.id to be 42 after remote description has been set'); | |
| }, 'Channels created (after setRemoteDescription) should have id assigned'); | |
| test(t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const dc1 = pc.createDataChannel('channel-1', { | |
| negotiated: true, | |
| id: 42, | |
| }); | |
| assert_equals(dc1.id, 42, | |
| 'Expect dc1.id to be 42'); | |
| const dc2 = pc.createDataChannel('channel-2', { | |
| negotiated: true, | |
| id: 43, | |
| }); | |
| assert_equals(dc2.id, 43, | |
| 'Expect dc2.id to be 43'); | |
| assert_throws_dom('OperationError', () => | |
| pc.createDataChannel('channel-3', { | |
| negotiated: true, | |
| id: 42, | |
| })); | |
| }, 'Reusing a data channel id that is in use should throw OperationError'); | |
| // We've seen implementations behaving differently before and after the connection has been | |
| // established. | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const dc1 = pc1.createDataChannel('channel-1', { | |
| negotiated: true, | |
| id: 42, | |
| }); | |
| assert_equals(dc1.id, 42, 'Expect dc1.id to be 42'); | |
| const dc2 = pc1.createDataChannel('channel-2', { | |
| negotiated: true, | |
| id: 43, | |
| }); | |
| assert_equals(dc2.id, 43, 'Expect dc2.id to be 43'); | |
| const offer = await pc1.createOffer(); | |
| await Promise.all([pc1.setLocalDescription(offer), pc2.setRemoteDescription(offer)]); | |
| const answer = await pc2.createAnswer(); | |
| await pc1.setRemoteDescription(answer); | |
| assert_equals(dc1.id, 42, 'Expect dc1.id to be 42'); | |
| assert_equals(dc2.id, 43, 'Expect dc2.id to be 43'); | |
| assert_throws_dom('OperationError', () => | |
| pc1.createDataChannel('channel-3', { | |
| negotiated: true, | |
| id: 42, | |
| })); | |
| }, 'Reusing a data channel id that is in use (after setRemoteDescription) should throw ' + | |
| 'OperationError'); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const dc1 = pc1.createDataChannel('channel-1'); | |
| const offer = await pc1.createOffer(); | |
| await Promise.all([pc1.setLocalDescription(offer), pc2.setRemoteDescription(offer)]); | |
| const answer = await pc2.createAnswer(); | |
| await pc1.setRemoteDescription(answer); | |
| assert_not_equals(dc1.id, null, | |
| 'Expect dc1.id to be assigned after remote description has been set'); | |
| assert_throws_dom('OperationError', () => | |
| pc1.createDataChannel('channel-2', { | |
| negotiated: true, | |
| id: dc1.id, | |
| })); | |
| }, 'Reusing a data channel id that is in use (after setRemoteDescription, negotiated via DCEP) ' + | |
| 'should throw OperationError'); | |
| for (const options of [{}, {negotiated: true, id: 0}]) { | |
| const mode = `${options.negotiated? "negotiated " : ""}datachannel`; | |
| // Based on https://bugzilla.mozilla.org/show_bug.cgi?id=1441723 | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| await createDataChannelPair(t, options, pc1); | |
| const dc = pc1.createDataChannel(''); | |
| assert_equals(dc.readyState, 'connecting', 'Channel should be in the connecting state'); | |
| }, `New ${mode} should be in the connecting state after creation ` + | |
| `(after connection establishment)`); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const stream = await getNoiseStream({audio: true, video: true}); | |
| t.add_cleanup(() => stopTracks(stream)); | |
| const audio = stream.getAudioTracks()[0]; | |
| const video = stream.getVideoTracks()[0]; | |
| pc1.addTrack(audio, stream); | |
| pc1.addTrack(video, stream); | |
| await createDataChannelPair(t, options, pc1); | |
| }, `addTrack, then creating ${mode}, should negotiate properly`); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection({bundlePolicy: "max-bundle"}); | |
| t.add_cleanup(() => pc1.close()); | |
| const stream = await getNoiseStream({audio: true, video: true}); | |
| t.add_cleanup(() => stopTracks(stream)); | |
| const audio = stream.getAudioTracks()[0]; | |
| const video = stream.getVideoTracks()[0]; | |
| pc1.addTrack(audio, stream); | |
| pc1.addTrack(video, stream); | |
| await createDataChannelPair(t, options, pc1); | |
| }, `addTrack, then creating ${mode}, should negotiate properly when max-bundle is used`); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| const pc2 = new RTCPeerConnection(); | |
| const stream = await getNoiseStream({audio: true, video: true}); | |
| stream.getTracks().forEach((track) => pc1.addTrack(track, stream)); | |
| await createDataChannelPair(t, options, pc1, pc2); | |
| await pc1.setLocalDescription(); | |
| await pc2.setRemoteDescription(pc1.localDescription); | |
| await pc2.setLocalDescription(); | |
| await pc1.setRemoteDescription(pc2.localDescription); | |
| }, `Renegotiation with audio/video and ${mode} should work properly`); | |
| /* | |
| This test is disabled until https://github.com/w3c/webrtc-pc/issues/2562 | |
| has been resolved; it presupposes that stopping the first transceiver | |
| breaks the transport. | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection({bundlePolicy: "max-bundle"}); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({audio: true, video: true}); | |
| t.add_cleanup(() => stopTracks(stream)); | |
| const audio = stream.getAudioTracks()[0]; | |
| const video = stream.getVideoTracks()[0]; | |
| pc1.addTrack(audio, stream); | |
| pc1.addTrack(video, stream); | |
| const [dc1, dc2] = await createDataChannelPair(t, options, pc1, pc2); | |
| pc2.getTransceivers()[0].stop(); | |
| const dc1Closed = new Promise(r => dc1.onclose = r); | |
| await exchangeOfferAnswer(pc1, pc2); | |
| await dc1Closed; | |
| }, `Stopping the bundle-tag when there is a ${mode} in the bundle ` + | |
| `should kill the DataChannel`); | |
| */ | |
| } | |
| /* | |
| Untestable | |
| 6.1. createDataChannel | |
| 19. If a setting, either [[MaxPacketLifeTime]] or [[MaxRetransmits]], has been set to | |
| indicate unreliable mode, and that value exceeds the maximum value supported | |
| by the user agent, the value MUST be set to the user agents maximum value. | |
| 23. Return channel and continue the following steps in parallel. | |
| 24. Create channel's associated underlying data transport and configure | |
| it according to the relevant properties of channel. | |
| Tested in RTCPeerConnection-onnegotiationneeded.html | |
| 22. If channel is the first RTCDataChannel created on connection, update the | |
| negotiation-needed flag for connection. | |
| Tested in RTCDataChannel-id.html | |
| - Odd/even rules for '.id' | |
| Tested in RTCDataChannel-dcep.html | |
| - Transmission of '.label' and further options | |
| */ | |
| </script> |