summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/webrtc/RTCPeerConnection-onnegotiationneeded.html
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/webrtc/RTCPeerConnection-onnegotiationneeded.html')
-rw-r--r--testing/web-platform/tests/webrtc/RTCPeerConnection-onnegotiationneeded.html627
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>