1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
|
<!doctype html>
<meta charset=utf-8>
<title>RTCPeerConnection.prototype.setRemoteDescription - offer</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="RTCPeerConnection-helper.js"></script>
<script src="/webrtc/third_party/sdp/sdp.js"></script>
<script>
'use strict';
// The following helper functions are called from RTCPeerConnection-helper.js:
// assert_session_desc_similar()
// generateAudioReceiveOnlyOffer
/*
4.3.2. Interface Definition
[Constructor(optional RTCConfiguration configuration)]
interface RTCPeerConnection : EventTarget {
Promise<void> setRemoteDescription(
RTCSessionDescriptionInit description);
readonly attribute RTCSessionDescription? remoteDescription;
readonly attribute RTCSessionDescription? currentRemoteDescription;
readonly attribute RTCSessionDescription? pendingRemoteDescription;
...
};
4.6.2. RTCSessionDescription Class
dictionary RTCSessionDescriptionInit {
required RTCSdpType type;
DOMString sdp = "";
};
4.6.1. RTCSdpType
enum RTCSdpType {
"offer",
"pranswer",
"answer",
"rollback"
};
*/
/*
4.3.1.6. Set the RTCSessionSessionDescription
2.2.3. Otherwise, if description is set as a remote description, then run one of
the following steps:
- If description is of type "offer", set connection.pendingRemoteDescription
attribute to description and signaling state to have-remote-offer.
*/
promise_test(t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
pc1.createDataChannel('datachannel');
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const states = [];
pc2.addEventListener('signalingstatechange', () => states.push(pc2.signalingState));
return pc1.createOffer()
.then(offer => {
return pc2.setRemoteDescription(offer)
.then(() => {
assert_equals(pc2.signalingState, 'have-remote-offer');
assert_session_desc_similar(pc2.remoteDescription, offer);
assert_session_desc_similar(pc2.pendingRemoteDescription, offer);
assert_equals(pc2.currentRemoteDescription, null);
assert_array_equals(states, ['have-remote-offer']);
});
});
}, 'setRemoteDescription with valid offer should succeed');
promise_test(t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
pc1.createDataChannel('datachannel');
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const states = [];
pc2.addEventListener('signalingstatechange', () => states.push(pc2.signalingState));
return pc1.createOffer()
.then(offer => {
return pc2.setRemoteDescription(offer)
.then(() => pc2.setRemoteDescription(offer))
.then(() => {
assert_equals(pc2.signalingState, 'have-remote-offer');
assert_session_desc_similar(pc2.remoteDescription, offer);
assert_session_desc_similar(pc2.pendingRemoteDescription, offer);
assert_equals(pc2.currentRemoteDescription, null);
assert_array_equals(states, ['have-remote-offer']);
});
});
}, 'setRemoteDescription multiple times should succeed');
promise_test(t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
pc1.createDataChannel('datachannel');
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const states = [];
pc2.addEventListener('signalingstatechange', () => states.push(pc2.signalingState));
return pc1.createOffer()
.then(offer1 => {
return pc1.setLocalDescription(offer1)
.then(()=> {
return generateAudioReceiveOnlyOffer(pc1)
.then(offer2 => {
assert_session_desc_not_similar(offer1, offer2);
return pc2.setRemoteDescription(offer1)
.then(() => pc2.setRemoteDescription(offer2))
.then(() => {
assert_equals(pc2.signalingState, 'have-remote-offer');
assert_session_desc_similar(pc2.remoteDescription, offer2);
assert_session_desc_similar(pc2.pendingRemoteDescription, offer2);
assert_equals(pc2.currentRemoteDescription, null);
assert_array_equals(states, ['have-remote-offer']);
});
});
});
});
}, 'setRemoteDescription multiple times with different offer should succeed');
/*
4.3.1.6. Set the RTCSessionSessionDescription
2.1.4. If the content of description is not valid SDP syntax, then reject p with
an RTCError (with errorDetail set to "sdp-syntax-error" and the
sdpLineNumber attribute set to the line number in the SDP where the syntax
error was detected) and abort these steps.
*/
promise_test(t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
return pc.setRemoteDescription({
type: 'offer',
sdp: 'Invalid SDP'
})
.then(() => {
assert_unreached('Expect promise to be rejected');
}, err => {
assert_equals(err.errorDetail, 'sdp-syntax-error',
'Expect error detail field to set to sdp-syntax-error');
assert_true(err instanceof RTCError,
'Expect err to be instance of RTCError');
});
}, 'setRemoteDescription(offer) with invalid SDP should reject with RTCError');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
t.add_cleanup(() => pc2.close());
await pc1.setLocalDescription(await pc1.createOffer());
await pc1.setRemoteDescription(await pc2.createOffer());
assert_equals(pc1.signalingState, 'have-remote-offer');
}, 'setRemoteDescription(offer) from have-local-offer should roll back and succeed');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
t.add_cleanup(() => pc2.close());
await pc1.setLocalDescription(await pc1.createOffer());
const p = pc1.setRemoteDescription(await pc2.createOffer());
await new Promise(r => pc1.onsignalingstatechange = r);
assert_equals(pc1.signalingState, 'stable');
assert_equals(pc1.pendingLocalDescription, null);
assert_equals(pc1.pendingRemoteDescription, null);
await new Promise(r => pc1.onsignalingstatechange = r);
assert_equals(pc1.signalingState, 'have-remote-offer');
assert_equals(pc1.pendingLocalDescription, null);
assert_equals(pc1.pendingRemoteDescription.type, 'offer');
await p;
}, 'setRemoteDescription(offer) from have-local-offer fires signalingstatechange twice');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.addTransceiver('audio', { direction: 'recvonly' });
const srdPromise = pc2.setRemoteDescription(await pc1.createOffer());
assert_equals(pc2.signalingState, "stable", "signalingState should not be set synchronously after a call to sRD");
assert_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should not be set synchronously after a call to sRD");
assert_equals(pc2.currentRemoteDescription, null, "currentRemoteDescription should not be set synchronously after a call to sRD");
const statePromise = new Promise(resolve => {
pc2.onsignalingstatechange = () => {
resolve(pc2.signalingState);
}
});
const raceValue = await Promise.race([statePromise, srdPromise]);
assert_equals(raceValue, "have-remote-offer", "signalingstatechange event should fire before sRD resolves");
assert_not_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event");
assert_equals(pc2.pendingRemoteDescription.type, "offer");
assert_equals(pc2.pendingRemoteDescription, pc2.remoteDescription);
assert_equals(pc2.currentRemoteDescription, null, "currentRemoteDescription should not be set after a call to sRD(offer)");
await srdPromise;
}, "setRemoteDescription(offer) in stable should update internal state with a queued task, in the right order");
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc2.addTransceiver('audio', { direction: 'recvonly' });
await pc2.setLocalDescription(await pc2.createOffer());
// Implicit rollback!
pc1.addTransceiver('audio', { direction: 'recvonly' });
const srdPromise = pc2.setRemoteDescription(await pc1.createOffer());
assert_equals(pc2.signalingState, "have-local-offer", "signalingState should not be set synchronously after a call to sRD");
assert_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should not be set synchronously after a call to sRD");
assert_not_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should not be set synchronously after a call to sRD");
assert_equals(pc2.pendingLocalDescription.type, "offer");
assert_equals(pc2.pendingLocalDescription, pc2.localDescription);
// First, we should go through stable (the implicit rollback part)
const stablePromise = new Promise(resolve => {
pc2.onsignalingstatechange = () => {
resolve(pc2.signalingState);
}
});
let raceValue = await Promise.race([stablePromise, srdPromise]);
assert_equals(raceValue, "stable", "signalingstatechange event should fire before sRD resolves");
assert_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should be updated before the signalingstatechange event");
assert_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event");
const haveRemoteOfferPromise = new Promise(resolve => {
pc2.onsignalingstatechange = () => {
resolve(pc2.signalingState);
}
});
raceValue = await Promise.race([haveRemoteOfferPromise, srdPromise]);
assert_equals(raceValue, "have-remote-offer", "signalingstatechange event should fire before sRD resolves");
assert_not_equals(pc2.pendingRemoteDescription, null, "pendingRemoteDescription should be updated before the signalingstatechange event");
assert_equals(pc2.pendingRemoteDescription.type, "offer");
assert_equals(pc2.pendingRemoteDescription, pc2.remoteDescription);
assert_equals(pc2.pendingRemoteDescription, pc2.pendingRemoteDescription);
assert_equals(pc2.pendingLocalDescription, null, "pendingLocalDescription should be updated before the signalingstatechange event");
await srdPromise;
}, "setRemoteDescription(offer) in have-local-offer should update internal state with a queued task, in the right order");
promise_test(async t => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
t.add_cleanup(() => pc2.close());
await pc1.setLocalDescription(await pc1.createOffer());
const offer = await pc2.createOffer();
const p1 = pc1.setLocalDescription({type: 'rollback'});
await new Promise(r => pc1.onsignalingstatechange = r);
assert_equals(pc1.signalingState, 'stable');
const p2 = pc1.addIceCandidate();
const p3 = pc1.setRemoteDescription(offer);
await promise_rejects_dom(t, 'InvalidStateError', p2);
await p1;
await p3;
assert_equals(pc1.signalingState, 'have-remote-offer');
}, 'Naive rollback approach is not glare-proof (control)');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
t.add_cleanup(() => pc2.close());
await pc1.setLocalDescription(await pc1.createOffer());
const p = pc1.setRemoteDescription(await pc2.createOffer());
await new Promise(r => pc1.onsignalingstatechange = r);
assert_equals(pc1.signalingState, 'stable');
await pc1.addIceCandidate();
await p;
assert_equals(pc1.signalingState, 'have-remote-offer');
}, 'setRemoteDescription(offer) from have-local-offer is glare-proof');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
t.add_cleanup(() => pc2.close());
await pc1.setLocalDescription(await pc1.createOffer());
const statePromise = new Promise(r => pc1.onsignalingstatechange = r);
// SRD with invalid SDP causes rollback.
const p = pc1.setRemoteDescription({type: 'offer', sdp: 'Invalid SDP'});
// Ensure that p is eventually awaited for
t.add_cleanup(async () => Promise.allSettled([p]));
// Ensure that the test will fail rather than timing out if state
// does not change.
const timeoutPromise = new Promise(
(resolve, reject) => t.step_timeout(reject, 1000));
try {
await Promise.any(statePromise, timeoutPromise);
} catch (error) {
assert_unreached('State should have changed');
}
assert_equals(pc1.signalingState, 'stable');
assert_equals(pc1.pendingLocalDescription, null);
assert_equals(pc1.pendingRemoteDescription, null);
await promise_rejects_dom(t, 'RTCError', p);
}, 'setRemoteDescription(invalidOffer) from have-local-offer does not undo rollback');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.addTransceiver('video');
const offer = await pc1.createOffer();
await pc2.setRemoteDescription(offer);
assert_equals(pc2.getTransceivers().length, 1);
await pc2.setRemoteDescription(offer);
assert_equals(pc2.getTransceivers().length, 1);
await pc1.setLocalDescription(offer);
const answer = await pc2.createAnswer();
await pc2.setLocalDescription(answer);
await pc1.setRemoteDescription(answer);
}, 'repeated sRD(offer) works');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.addTransceiver('video');
await exchangeOfferAnswer(pc1, pc2);
await waitForIceGatheringState(pc1, ['complete']);
await exchangeOfferAnswer(pc1, pc2);
await waitForIceStateChange(pc2, ['connected', 'completed']);
}, 'sRD(reoffer) with candidates and without trickle works');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
pc1.addTransceiver('video');
const offer = await pc1.createOffer();
const srdPromise = pc2.setRemoteDescription(offer);
assert_equals(pc2.getTransceivers().length, 0);
await srdPromise;
assert_equals(pc2.getTransceivers().length, 1);
}, 'Transceivers added by sRD(offer) should not show up until sRD resolves');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
t.add_cleanup(() => pc2.close());
pc1.addTransceiver('video');
const description = await pc1.createOffer();
const sections = SDPUtils.splitSections(description.sdp);
// Compose SDP with a duplicated media section (equal MSID lines)
// This is not permitted according to RFC 8830 section 2.
const sdp = sections[0] + sections[1] + sections[1].replace('a=mid:', 'a=mid:unique');
const p = pc2.setRemoteDescription({type: 'offer', sdp: sdp});
await promise_rejects_dom(t, 'OperationError', p);
}, 'setRemoteDescription(section with duplicate msid) rejects');
</script>
|