diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /testing/web-platform/tests/webrtc/RTCPeerConnection-onnegotiationneeded.html | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/webrtc/RTCPeerConnection-onnegotiationneeded.html')
-rw-r--r-- | testing/web-platform/tests/webrtc/RTCPeerConnection-onnegotiationneeded.html | 627 |
1 files changed, 627 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webrtc/RTCPeerConnection-onnegotiationneeded.html b/testing/web-platform/tests/webrtc/RTCPeerConnection-onnegotiationneeded.html new file mode 100644 index 0000000000..6ede5ccebf --- /dev/null +++ b/testing/web-platform/tests/webrtc/RTCPeerConnection-onnegotiationneeded.html @@ -0,0 +1,627 @@ +<!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> |