| <!doctype html> |
| <meta charset=utf-8> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script> |
| 'use strict'; |
| |
| ['audio', 'video'].forEach((kind) => { |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| const transceiver = pc1.addTransceiver(kind); |
| |
| // Complete O/A exchange such that the transceiver gets associated. |
| await pc1.setLocalDescription(); |
| await pc2.setRemoteDescription(pc1.localDescription); |
| await pc2.setLocalDescription(); |
| await pc1.setRemoteDescription(pc2.localDescription); |
| assert_not_equals(transceiver.mid, null, 'mid before stop()'); |
| assert_not_equals(transceiver.direction, 'stopped', |
| 'direction before stop()'); |
| assert_not_equals(transceiver.currentDirection, 'stopped', |
| 'currentDirection before stop()'); |
| |
| // Stop makes it stopping, but not stopped. |
| transceiver.stop(); |
| assert_not_equals(transceiver.mid, null, 'mid after stop()'); |
| assert_equals(transceiver.direction, 'stopped', 'direction after stop()'); |
| assert_not_equals(transceiver.currentDirection, 'stopped', |
| 'currentDirection after stop()'); |
| |
| // Negotiating makes it stopped. |
| await pc1.setLocalDescription(); |
| await pc2.setRemoteDescription(pc1.localDescription); |
| await pc2.setLocalDescription(); |
| await pc1.setRemoteDescription(pc2.localDescription); |
| assert_equals(transceiver.mid, null, 'mid after negotiation'); |
| assert_equals(transceiver.direction, 'stopped', |
| 'direction after negotiation'); |
| assert_equals(transceiver.currentDirection, 'stopped', |
| 'currentDirection after negotiation'); |
| }, `[${kind}] Locally stopped transceiver goes from stopping to stopped`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| const transceiver = pc.addTransceiver(kind); |
| const trackEnded = new Promise(r => transceiver.receiver.track.onended = r); |
| assert_equals(transceiver.receiver.track.readyState, 'live'); |
| transceiver.stop(); |
| // Stopping triggers ending the track, but this happens asynchronously. |
| assert_equals(transceiver.receiver.track.readyState, 'live'); |
| await trackEnded; |
| assert_equals(transceiver.receiver.track.readyState, 'ended'); |
| }, `[${kind}] Locally stopping a transceiver ends the track`); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| const pc1Transceiver = pc1.addTransceiver(kind); |
| await pc1.setLocalDescription(); |
| await pc2.setRemoteDescription(pc1.localDescription); |
| await pc2.setLocalDescription(); |
| await pc1.setRemoteDescription(pc2.localDescription); |
| const [pc2Transceiver] = pc2.getTransceivers(); |
| |
| pc1Transceiver.stop(); |
| |
| await pc1.setLocalDescription(); |
| assert_equals(pc2Transceiver.receiver.track.readyState, 'live'); |
| // Applying the remote offer immediately ends the track, we don't need to |
| // create or apply an answer. |
| await pc2.setRemoteDescription(pc1.localDescription); |
| // sRD just resolved, so we're in the success task for sRD. The transition |
| // from live -> ended is queued right now. |
| assert_equals(pc2Transceiver.receiver.track.readyState, 'live'); |
| await new Promise(r => pc2Transceiver.receiver.track.onended = r); |
| assert_equals(pc2Transceiver.receiver.track.readyState, 'ended'); |
| }, `[${kind}] Remotely stopping a transceiver ends the track`); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| const pc1Transceiver = pc1.addTransceiver(kind); |
| |
| // Complete O/A exchange such that the transceiver gets associated. |
| await pc1.setLocalDescription(); |
| await pc2.setRemoteDescription(pc1.localDescription); |
| await pc2.setLocalDescription(); |
| await pc1.setRemoteDescription(pc2.localDescription); |
| const [pc2Transceiver] = pc2.getTransceivers(); |
| assert_not_equals(pc2Transceiver.mid, null, 'mid before stop()'); |
| assert_not_equals(pc2Transceiver.direction, 'stopped', |
| 'direction before stop()'); |
| assert_not_equals(pc2Transceiver.currentDirection, 'stopped', |
| 'currentDirection before stop()'); |
| |
| // Make the remote transceiver stopped. |
| pc1Transceiver.stop(); |
| |
| // Negotiating makes it stopped. |
| assert_equals(pc2.getTransceivers().length, 1); |
| await pc1.setLocalDescription(); |
| await pc2.setRemoteDescription(pc1.localDescription); |
| // As soon as the remote offer is set, the transceiver is stopped but it is |
| // not disassociated or removed until setting the local answer. |
| assert_equals(pc2.getTransceivers().length, 1); |
| assert_not_equals(pc2Transceiver.mid, null, 'mid during negotiation'); |
| assert_equals(pc2Transceiver.direction, 'stopped', |
| 'direction during negotiation'); |
| assert_equals(pc2Transceiver.currentDirection, 'stopped', |
| 'currentDirection during negotiation'); |
| await pc2.setLocalDescription(); |
| assert_equals(pc2.getTransceivers().length, 0); |
| assert_equals(pc2Transceiver.mid, null, 'mid after negotiation'); |
| assert_equals(pc2Transceiver.direction, 'stopped', |
| 'direction after negotiation'); |
| assert_equals(pc2Transceiver.currentDirection, 'stopped', |
| 'currentDirection after negotiation'); |
| }, `[${kind}] Remotely stopped transceiver goes directly to stopped`); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| const transceiver = pc.addTransceiver(kind); |
| |
| // Rollback does not end the track, because the transceiver is not removed. |
| await pc.setLocalDescription(); |
| await pc.setLocalDescription({type:'rollback'}); |
| assert_equals(transceiver.receiver.track.readyState, 'live'); |
| }, `[${kind}] Rollback when transceiver is not removed does not end track`); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| const pc1Transceiver = pc1.addTransceiver(kind); |
| |
| // Start negotiation, causing a transceiver to be created. |
| await pc1.setLocalDescription(); |
| await pc2.setRemoteDescription(pc1.localDescription); |
| const [pc2Transceiver] = pc2.getTransceivers(); |
| |
| // Rollback such that the transceiver is removed. |
| await pc2.setRemoteDescription({type:'rollback'}); |
| assert_equals(pc2.getTransceivers().length, 0); |
| // sRD just resolved, so we're in the success task for sRD. The transition |
| // from live -> ended is queued right now. |
| assert_equals(pc2Transceiver.receiver.track.readyState, 'live'); |
| await new Promise(r => pc2Transceiver.receiver.track.onended = r); |
| assert_equals(pc2Transceiver.receiver.track.readyState, 'ended'); |
| }, `[${kind}] Rollback when removing transceiver does end the track`); |
| |
| // Same test as above but looking at direction and currentDirection. |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| const pc1Transceiver = pc1.addTransceiver(kind); |
| |
| // Start negotiation, causing a transceiver to be created. |
| await pc1.setLocalDescription(); |
| await pc2.setRemoteDescription(pc1.localDescription); |
| const [pc2Transceiver] = pc2.getTransceivers(); |
| |
| // Rollback such that the transceiver is removed. |
| await pc2.setRemoteDescription({type:'rollback'}); |
| assert_equals(pc2.getTransceivers().length, 0); |
| // The removed transceiver is stopped. |
| assert_equals(pc2Transceiver.currentDirection, 'stopped', |
| 'currentDirection indicate stopped'); |
| // A stopped transceiver is necessarily also stopping. |
| assert_equals(pc2Transceiver.direction, 'stopped', |
| 'direction indicate stopping'); |
| // A stopped transceiver has no mid. |
| assert_equals(pc2Transceiver.mid, null, 'not associated'); |
| }, `[${kind}] Rollback when removing transceiver makes it stopped`); |
| |
| promise_test(async t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| |
| const constraints = {}; |
| constraints[kind] = true; |
| const stream = await navigator.mediaDevices.getUserMedia(constraints); |
| const [track] = stream.getTracks(); |
| |
| pc1.addTrack(track); |
| pc2.addTrack(track); |
| const transceiver = pc2.getTransceivers()[0]; |
| |
| const ontrackEvent = new Promise(r => { |
| pc2.ontrack = e => r(e.track); |
| }); |
| |
| // Simulate glare: both peer connections set local offers. |
| await pc1.setLocalDescription(); |
| await pc2.setLocalDescription(); |
| // Set remote offer, which implicitly rolls back the local offer. Because |
| // `transceiver` is an addTrack-transceiver, it should get repurposed. |
| await pc2.setRemoteDescription(pc1.localDescription); |
| assert_equals(transceiver.receiver.track.readyState, 'live'); |
| // Sanity check: the track should still be live when ontrack fires. |
| assert_equals((await ontrackEvent).readyState, 'live'); |
| }, `[${kind}] Glare when transceiver is not removed does not end track`); |
| }); |
| </script> |