<!doctype html>
<meta charset="utf-8">
<title>Test RTCPeerConnection.prototype.onnegotiationneeded</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="RTCPeerConnection-helper.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:
// generateOffer
// generateAnswer
// generateAudioReceiveOnlyOffer
// test_never_resolve
// Listen to the negotiationneeded event on a peer connection
// Returns a promise that resolves when the first event is fired.
// The resolve result is a dictionary with event and nextPromise,
// which resolves when the next negotiationneeded event is fired.
// This allow us to promisify the event listening and assert whether
// an event is fired or not by testing whether a promise is resolved.
function awaitNegotiation(pc) {
if(pc.onnegotiationneeded) {
throw new Error('connection is already attached with onnegotiationneeded event handler');
}
function waitNextNegotiation() {
return new Promise(resolve => {
pc.onnegotiationneeded = event => {
const nextPromise = waitNextNegotiation();
resolve({ nextPromise, event });
}
});
}
return waitNextNegotiation();
}
// Return a promise that rejects if the first promise is resolved before second promise.
// Also rejects when either promise rejects.
function assert_first_promise_fulfill_after_second(promise1, promise2, message) {
if(!message) {
message = 'first promise is resolved before second promise';
}
return new Promise((resolve, reject) => {
let secondResolved = false;
promise1.then(() => {
if(secondResolved) {
resolve();
} else {
assert_unreached(message);
}
})
.catch(reject);
promise2.then(() => {
secondResolved = true;
}, reject);
});
}
/*
4.7.3. Updating the Negotiation-Needed flag
To update the negotiation-needed flag
5. Set connection's [[needNegotiation]] slot to true.
6. Queue a task that runs the following steps:
3. Fire a simple event named negotiationneeded at connection.
To check if negotiation is needed
2. If connection has created any RTCDataChannels, and no m= section has
been negotiated yet for data, return "true".
6.1. RTCPeerConnection Interface Extensions
createDataChannel
14. If channel was the first RTCDataChannel created on connection,
update the negotiation-needed flag for connection.
*/
promise_test(t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const negotiated = awaitNegotiation(pc);
pc.createDataChannel('test');
return negotiated;
}, 'Creating first data channel should fire negotiationneeded event');
test_never_resolve(t => {
const pc = new RTCPeerConnection();
const negotiated = awaitNegotiation(pc);
pc.createDataChannel('foo');
return negotiated
.then(({nextPromise}) => {
pc.createDataChannel('bar');
return nextPromise;
});
}, 'calling createDataChannel twice should fire negotiationneeded event once');
/*
4.7.3. Updating the Negotiation-Needed flag
To check if negotiation is needed
3. For each transceiver t in connection's set of transceivers, perform
the following checks:
1. If t isn't stopped and isn't yet associated with an m= section
according to [JSEP] (section 3.4.1.), return "true".
5.1. RTCPeerConnection Interface Extensions
addTransceiver
9. Update the negotiation-needed flag for connection.
*/
promise_test(t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const negotiated = awaitNegotiation(pc);
pc.addTransceiver('audio');
return negotiated;
}, 'addTransceiver() should fire negotiationneeded event');
/*
4.7.3. Updating the Negotiation-Needed flag
To update the negotiation-needed flag
4. If connection's [[needNegotiation]] slot is already true, abort these steps.
*/
test_never_resolve(t => {
const pc = new RTCPeerConnection();
const negotiated = awaitNegotiation(pc);
pc.addTransceiver('audio');
return negotiated
.then(({nextPromise}) => {
pc.addTransceiver('video');
return nextPromise;
});
}, 'Calling addTransceiver() twice should fire negotiationneeded event once');
/*
4.7.3. Updating the Negotiation-Needed flag
To update the negotiation-needed flag
4. If connection's [[needNegotiation]] slot is already true, abort these steps.
*/
test_never_resolve(t => {
const pc = new RTCPeerConnection();
const negotiated = awaitNegotiation(pc);
pc.createDataChannel('test');
return negotiated
.then(({nextPromise}) => {
pc.addTransceiver('video');
return nextPromise;
});
}, 'Calling both addTransceiver() and createDataChannel() should fire negotiationneeded event once');
/*
4.7.3. Updating the Negotiation-Needed flag
To update the negotiation-needed flag
2. If connection's signaling state is not "stable", abort these steps.
*/
test_never_resolve(t => {
const pc = new RTCPeerConnection();
let negotiated;
return generateAudioReceiveOnlyOffer(pc)
.then(offer => {
pc.setLocalDescription(offer);
negotiated = awaitNegotiation(pc);
})
.then(() => negotiated)
.then(({nextPromise}) => {
assert_equals(pc.signalingState, 'have-local-offer');
pc.createDataChannel('test');
return nextPromise;
});
}, 'negotiationneeded event should not fire if signaling state is not stable');
/*
4.4.1.6. Set the RTCSessionSessionDescription
2.2.10. If connection's signaling state is now stable, update the negotiation-needed
flag. If connection's [[NegotiationNeeded]] slot was true both before and after
this update, queue a task that runs the following steps:
2. If connection's [[NegotiationNeeded]] slot is false, abort these steps.
3. Fire a simple event named negotiationneeded at connection.
*/
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
pc.addTransceiver('audio');
await new Promise(resolve => pc.onnegotiationneeded = resolve);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
let fired = false;
pc.onnegotiationneeded = e => fired = true;
pc.createDataChannel('test');
await pc.setRemoteDescription(await generateAnswer(offer));
await undefined;
assert_false(fired, "negotiationneeded should not fire until the next iteration of the event loop after SRD success");
await new Promise(resolve => pc.onnegotiationneeded = resolve);
}, 'negotiationneeded event should fire only after signaling state goes back to stable after setRemoteDescription');
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
pc.addTransceiver('audio');
await new Promise(resolve => pc.onnegotiationneeded = resolve);
let fired = false;
pc.onnegotiationneeded = e => fired = true;
await pc.setRemoteDescription(await generateOffer());
pc.createDataChannel('test');
await pc.setLocalDescription(await pc.createAnswer());
await undefined;
assert_false(fired, "negotiationneeded should not fire until the next iteration of the event loop after SLD success");
await new Promise(resolve => pc.onnegotiationneeded = resolve);
}, 'negotiationneeded event should fire only after signaling state goes back to stable after setLocalDescription');
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
pc.addTransceiver('audio');
await new Promise(resolve => pc.onnegotiationneeded = resolve);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
let fired = false;
pc.onnegotiationneeded = e => fired = true;
pc.createDataChannel('test');
const p = pc.setRemoteDescription(await generateAnswer(offer));
await new Promise(resolve => pc.onsignalingstatechange = resolve);
assert_false(fired, "negotiationneeded should not fire before signalingstatechange fires");
await new Promise(resolve => pc.onnegotiationneeded = resolve);
await p;
}, 'negotiationneeded event should fire only after signalingstatechange event fires from setRemoteDescription');
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
pc.addTransceiver('audio');
await new Promise(resolve => pc.onnegotiationneeded = resolve);
let fired = false;
pc.onnegotiationneeded = e => fired = true;
await pc.setRemoteDescription(await generateOffer());
pc.createDataChannel('test');
const p = pc.setLocalDescription(await pc.createAnswer());
await new Promise(resolve => pc.onsignalingstatechange = resolve);
assert_false(fired, "negotiationneeded should not fire until the next iteration of the event loop after returning to stable");
await new Promise(resolve => pc.onnegotiationneeded = resolve);
await p;
}, 'negotiationneeded event should fire only after signalingstatechange event fires from setLocalDescription');
/*
5.1. RTCPeerConnection Interface Extensions
addTrack
10. Update the negotiation-needed flag for connection.
*/
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const stream = await getNoiseStream({ audio: true });
t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
const [track] = stream.getTracks();
pc.addTrack(track, stream);
await new Promise(resolve => pc.onnegotiationneeded = resolve);
}, 'addTrack should cause negotiationneeded to fire');
/*
5.1. RTCPeerConnection Interface Extensions
removeTrack
12. Update the negotiation-needed flag for connection.
*/
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const stream = await getNoiseStream({ audio: true });
t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
const [track] = stream.getTracks();
const sender = pc.addTrack(track, stream);
await new Promise(resolve => pc.onnegotiationneeded = resolve);
pc.onnegotiationneeded = t.step_func(() => {
assert_unreached('onnegotiationneeded misfired');
});
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const answer = await generateAnswer(offer);
await pc.setRemoteDescription(answer);
pc.removeTrack(sender);
await new Promise(resolve => pc.onnegotiationneeded = resolve)
}, 'removeTrack should cause negotiationneeded to fire on the caller');
/*
5.1. RTCPeerConnection Interface Extensions
removeTrack
12. Update the negotiation-needed flag for connection.
*/
promise_test(async t => {
const caller = new RTCPeerConnection();
t.add_cleanup(() => caller.close());
caller.addTransceiver('audio', {direction:'recvonly'});
const offer = await caller.createOffer();
const callee = new RTCPeerConnection();
t.add_cleanup(() => callee.close());
const stream = await getNoiseStream({ audio: true });
t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
const [track] = stream.getTracks();
const sender = callee.addTrack(track, stream);
await new Promise(resolve => callee.onnegotiationneeded = resolve);
callee.onnegotiationneeded = t.step_func(() => {
assert_unreached('onnegotiationneeded misfired');
});
await callee.setRemoteDescription(offer);
const answer = await callee.createAnswer();
callee.setLocalDescription(answer);
callee.removeTrack(sender);
await new Promise(resolve => callee.onnegotiationneeded = resolve)
}, 'removeTrack should cause negotiationneeded to fire on the callee');
/*
5.4. RTCRtpTransceiver Interface
setDirection
7. Update the negotiation-needed flag for connection.
*/
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const answer = await generateAnswer(offer);
await pc.setRemoteDescription(answer);
transceiver.direction = 'recvonly';
await new Promise(resolve => pc.onnegotiationneeded = resolve);
}, 'Updating the direction of the transceiver should cause negotiationneeded to fire');
/*
5.2. RTCRtpSender Interface
setStreams
7. Update the negotiation-needed flag for connection.
*/
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const answer = await generateAnswer(offer);
await pc.setRemoteDescription(answer);
const stream = new MediaStream();
transceiver.sender.setStreams(stream);
await new Promise(resolve => pc.onnegotiationneeded = resolve);
}, 'Calling setStreams should cause negotiationneeded to fire');
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
const stream = new MediaStream();
transceiver.sender.setStreams(stream);
await new Promise(resolve => pc.onnegotiationneeded = resolve);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const answer = await generateAnswer(offer);
await pc.setRemoteDescription(answer);
const stream2 = new MediaStream();
transceiver.sender.setStreams(stream2);
await new Promise(resolve => pc.onnegotiationneeded = resolve);
}, 'Calling setStreams with a different stream as before should cause negotiationneeded to fire');
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
const stream = new MediaStream();
transceiver.sender.setStreams(stream);
await new Promise(resolve => pc.onnegotiationneeded = resolve);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const answer = await generateAnswer(offer);
await pc.setRemoteDescription(answer);
const stream2 = new MediaStream();
transceiver.sender.setStreams(stream, stream2);
await new Promise(resolve => pc.onnegotiationneeded = resolve);
}, 'Calling setStreams with an additional stream should cause negotiationneeded to fire');
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
const stream1 = new MediaStream();
const stream2 = new MediaStream();
transceiver.sender.setStreams(stream1, stream2);
await new Promise(resolve => pc.onnegotiationneeded = resolve);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const answer = await generateAnswer(offer);
await pc.setRemoteDescription(answer);
transceiver.sender.setStreams(stream2);
await new Promise(resolve => pc.onnegotiationneeded = resolve);
}, 'Calling setStreams with a stream removed should cause negotiationneeded to fire');
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
const stream1 = new MediaStream();
const stream2 = new MediaStream();
transceiver.sender.setStreams(stream1, stream2);
await new Promise(resolve => pc.onnegotiationneeded = resolve);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const answer = await generateAnswer(offer);
await pc.setRemoteDescription(answer);
transceiver.sender.setStreams();
await new Promise(resolve => pc.onnegotiationneeded = resolve);
}, 'Calling setStreams with all streams removed should cause negotiationneeded to fire');
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
const stream = new MediaStream();
transceiver.sender.setStreams(stream);
await new Promise(resolve => pc.onnegotiationneeded = resolve);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const answer = await generateAnswer(offer);
await pc.setRemoteDescription(answer);
transceiver.sender.setStreams(stream);
const event = await Promise.race([
new Promise(r => pc.onnegotiationneeded = r),
new Promise(r => t.step_timeout(r, 10))
]);
assert_equals(event, undefined, "No negotiationneeded event");
}, 'Calling setStreams with the same stream as before should not cause negotiationneeded to fire');
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
const stream = new MediaStream();
transceiver.sender.setStreams(stream);
await new Promise(resolve => pc.onnegotiationneeded = resolve);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const answer = await generateAnswer(offer);
await pc.setRemoteDescription(answer);
transceiver.sender.setStreams(stream, stream);
const event = await Promise.race([
new Promise(r => pc.onnegotiationneeded = r),
new Promise(r => t.step_timeout(r, 10))
]);
assert_equals(event, undefined, "No negotiationneeded event");
}, 'Calling setStreams with duplicates of the same stream as before should not cause negotiationneeded to fire');
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
const stream1 = new MediaStream();
const stream2 = new MediaStream();
transceiver.sender.setStreams(stream1, stream2);
await new Promise(resolve => pc.onnegotiationneeded = resolve);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const answer = await generateAnswer(offer);
await pc.setRemoteDescription(answer);
transceiver.sender.setStreams(stream2, stream1);
const event = await Promise.race([
new Promise(r => pc.onnegotiationneeded = r),
new Promise(r => t.step_timeout(r, 10))
]);
assert_equals(event, undefined, "No negotiationneeded event");
}, 'Calling setStreams with the same streams as before in a different order should not cause negotiationneeded to fire');
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver('audio', {direction:'sendrecv'});
const stream1 = new MediaStream();
const stream2 = new MediaStream();
transceiver.sender.setStreams(stream1, stream2);
await new Promise(resolve => pc.onnegotiationneeded = resolve);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
const answer = await generateAnswer(offer);
await pc.setRemoteDescription(answer);
transceiver.sender.setStreams(stream1, stream2, stream1);
const event = await Promise.race([
new Promise(r => pc.onnegotiationneeded = r),
new Promise(r => t.step_timeout(r, 10))
]);
assert_equals(event, undefined, "No negotiationneeded event");
}, 'Calling setStreams with duplicates of the same streams as before should not cause negotiationneeded to fire');
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
let negotiationCount = 0;
pc1.onnegotiationneeded = async () => {
negotiationCount++;
await pc1.setLocalDescription(await pc1.createOffer());
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription(await pc2.createAnswer());
await pc1.setRemoteDescription(pc2.localDescription);
}
pc1.addTransceiver("video");
await new Promise(r => pc1.onsignalingstatechange = () => pc1.signalingState == "stable" && r());
pc1.addTransceiver("audio");
await new Promise(r => pc1.onsignalingstatechange = () => pc1.signalingState == "stable" && r());
assert_equals(negotiationCount, 2);
}, 'Adding two transceivers, one at a time, results in the expected number of negotiationneeded events');
/*
TODO
4.7.3. Updating the Negotiation-Needed flag
To update the negotiation-needed flag
3. If the result of checking if negotiation is needed is "false",
clear the negotiation-needed flag by setting connection's
[[needNegotiation]] slot to false, and abort these steps.
6. Queue a task that runs the following steps:
2. If connection's [[needNegotiation]] slot is false, abort these steps.
To check if negotiation is needed
3. For each transceiver t in connection's set of transceivers, perform
the following checks:
2. If t isn't stopped and is associated with an m= section according
to [JSEP] (section 3.4.1.), then perform the following checks:
1. If t's direction is "sendrecv" or "sendonly", and the
associated m= section in connection's currentLocalDescription
doesn't contain an "a=msid" line, return "true".
2. If connection's currentLocalDescription if of type "offer",
and the direction of the associated m= section in neither the
offer nor answer matches t's direction, return "true".
3. If connection's currentLocalDescription if of type "answer",
and the direction of the associated m= section in the answer
does not match t's direction intersected with the offered
direction (as described in [JSEP] (section 5.3.1.)),
return "true".
3. If t is stopped and is associated with an m= section according
to [JSEP] (section 3.4.1.), but the associated m= section is
not yet rejected in connection's currentLocalDescription or
currentRemoteDescription , return "true".
4. If all the preceding checks were performed and "true" was not returned,
nothing remains to be negotiated; return "false".
4.3.1. RTCPeerConnection Operation
When the RTCPeerConnection() constructor is invoked
7. Let connection have a [[needNegotiation]] internal slot, initialized to false.
5.4. RTCRtpTransceiver Interface
stop
11. Update the negotiation-needed flag for connection.
Untestable
4.7.3. Updating the Negotiation-Needed flag
1. If connection's [[isClosed]] slot is true, abort these steps.
6. Queue a task that runs the following steps:
1. If connection's [[isClosed]] slot is true, abort these steps.
*/
</script>