| <!doctype html> |
| <meta charset=utf-8> |
| <title>RTCPeerConnection.prototype.removeTrack</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: |
| // generateAnswer |
| |
| /* |
| 5.1. RTCPeerConnection Interface Extensions |
| partial interface RTCPeerConnection { |
| ... |
| void removeTrack(RTCRtpSender sender); |
| RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind, |
| optional RTCRtpTransceiverInit init); |
| }; |
| */ |
| |
| // Before calling removeTrack can be tested, one needs to add MediaStreamTracks to |
| // a peer connection. There are two ways for adding MediaStreamTrack: addTrack and |
| // addTransceiver. addTransceiver is a newer API while addTrack has been implemented |
| // in current browsers for some time. As a result some of the removeTrack tests have |
| // two versions so that removeTrack can be partially tested without addTransceiver |
| // and the transceiver APIs being implemented. |
| |
| /* |
| 5.1. removeTrack |
| 3. If connection's [[isClosed]] slot is true, throw an InvalidStateError. |
| */ |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| const stream = await navigator.mediaDevices.getUserMedia({audio: true}); |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); |
| const [track] = stream.getTracks(); |
| const transceiver = pc.addTransceiver(track); |
| const { sender } = transceiver; |
| |
| pc.close(); |
| assert_throws('InvalidStateError', () => pc.removeTrack(sender)); |
| }, 'addTransceiver - Calling removeTrack when connection is closed should throw InvalidStateError'); |
| |
| 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); |
| |
| pc.close(); |
| assert_throws('InvalidStateError', () => pc.removeTrack(sender)); |
| }, 'addTrack - Calling removeTrack when connection is closed should throw InvalidStateError'); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const stream = await navigator.mediaDevices.getUserMedia({audio: true}); |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); |
| const [track] = stream.getTracks(); |
| const transceiver = pc.addTransceiver(track); |
| const { sender } = transceiver; |
| |
| const pc2 = new RTCPeerConnection(); |
| pc2.close(); |
| assert_throws('InvalidStateError', () => pc2.removeTrack(sender)); |
| }, 'addTransceiver - Calling removeTrack on different connection that is closed should throw InvalidStateError'); |
| |
| 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); |
| |
| const pc2 = new RTCPeerConnection(); |
| pc2.close(); |
| assert_throws('InvalidStateError', () => pc2.removeTrack(sender)); |
| }, 'addTrack - Calling removeTrack on different connection that is closed should throw InvalidStateError'); |
| |
| /* |
| 5.1. removeTrack |
| 4. If sender was not created by connection, throw an InvalidAccessError. |
| */ |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const stream = await navigator.mediaDevices.getUserMedia({audio: true}); |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); |
| const [track] = stream.getTracks(); |
| const transceiver = pc.addTransceiver(track); |
| const { sender } = transceiver; |
| |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| assert_throws('InvalidAccessError', () => pc2.removeTrack(sender)); |
| }, 'addTransceiver - Calling removeTrack on different connection should throw InvalidAccessError'); |
| |
| 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); |
| |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| assert_throws('InvalidAccessError', () => pc2.removeTrack(sender)); |
| }, 'addTrack - Calling removeTrack on different connection should throw InvalidAccessError') |
| |
| /* |
| 5.1. removeTrack |
| 7. Set sender.track to null. |
| */ |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const stream = await navigator.mediaDevices.getUserMedia({audio: true}); |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); |
| const [track] = stream.getTracks(); |
| const transceiver = pc.addTransceiver(track); |
| const { sender } = transceiver; |
| |
| assert_equals(sender.track, track); |
| assert_equals(transceiver.direction, 'sendrecv'); |
| assert_equals(transceiver.currentDirection, null); |
| |
| pc.removeTrack(sender); |
| assert_equals(sender.track, null); |
| assert_equals(transceiver.direction, 'recvonly'); |
| }, 'addTransceiver - Calling removeTrack with valid sender should set sender.track to null'); |
| |
| 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); |
| |
| assert_equals(sender.track, track); |
| |
| pc.removeTrack(sender); |
| assert_equals(sender.track, null); |
| }, 'addTrack - Calling removeTrack with valid sender should set sender.track to null'); |
| |
| /* |
| 5.1. removeTrack |
| 7. Set sender.track to null. |
| 10. If transceiver.currentDirection is sendrecv set transceiver.direction |
| to recvonly. |
| */ |
| promise_test(async t => { |
| const caller = new RTCPeerConnection(); |
| t.add_cleanup(() => caller.close()); |
| const callee = new RTCPeerConnection(); |
| t.add_cleanup(() => callee.close()); |
| const stream = await navigator.mediaDevices.getUserMedia({audio: true}); |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); |
| const [track] = stream.getTracks(); |
| const transceiver = caller.addTransceiver(track); |
| const { sender } = transceiver; |
| |
| assert_equals(sender.track, track); |
| assert_equals(transceiver.direction, 'sendrecv'); |
| assert_equals(transceiver.currentDirection, null); |
| |
| const offer = await caller.createOffer(); |
| await caller.setLocalDescription(offer); |
| await callee.setRemoteDescription(offer); |
| callee.addTrack(track, stream); |
| const answer = await callee.createAnswer(); |
| await callee.setLocalDescription(answer); |
| await caller.setRemoteDescription(answer); |
| assert_equals(transceiver.currentDirection, 'sendrecv'); |
| |
| caller.removeTrack(sender); |
| assert_equals(sender.track, null); |
| assert_equals(transceiver.direction, 'recvonly'); |
| assert_equals(transceiver.currentDirection, 'sendrecv', |
| 'Expect currentDirection to not change'); |
| }, 'Calling removeTrack with currentDirection sendrecv should set direction to recvonly'); |
| |
| /* |
| 5.1. removeTrack |
| 7. Set sender.track to null. |
| 11. If transceiver.currentDirection is sendonly set transceiver.direction |
| to inactive. |
| */ |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const stream = await navigator.mediaDevices.getUserMedia({audio: true}); |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); |
| const [track] = stream.getTracks(); |
| const transceiver = pc.addTransceiver(track, { direction: 'sendonly' }); |
| const { sender } = transceiver; |
| |
| assert_equals(sender.track, track); |
| assert_equals(transceiver.direction, 'sendonly'); |
| assert_equals(transceiver.currentDirection, null); |
| |
| const offer = await pc.createOffer(); |
| await pc.setLocalDescription(offer); |
| const answer = await generateAnswer(offer); |
| await pc.setRemoteDescription(answer); |
| assert_equals(transceiver.currentDirection, 'sendonly'); |
| |
| pc.removeTrack(sender); |
| assert_equals(sender.track, null); |
| assert_equals(transceiver.direction, 'inactive'); |
| assert_equals(transceiver.currentDirection, 'sendonly', |
| 'Expect currentDirection to not change'); |
| }, 'Calling removeTrack with currentDirection sendonly should set direction to inactive'); |
| |
| /* |
| 5.1. removeTrack |
| 7. Set sender.track to null. |
| 9. If transceiver.currentDirection is recvonly or inactive, |
| then abort these steps. |
| */ |
| promise_test(async t => { |
| const caller = new RTCPeerConnection(); |
| t.add_cleanup(() => caller.close()); |
| const callee = new RTCPeerConnection(); |
| t.add_cleanup(() => callee.close()); |
| const stream = await navigator.mediaDevices.getUserMedia({audio: true}); |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); |
| const [track] = stream.getTracks(); |
| const transceiver = caller.addTransceiver(track, { direction: 'recvonly' }); |
| const { sender } = transceiver; |
| |
| assert_equals(sender.track, track); |
| assert_equals(transceiver.direction, 'recvonly'); |
| assert_equals(transceiver.currentDirection, null); |
| |
| const offer = await caller.createOffer(); |
| await caller.setLocalDescription(offer); |
| await callee.setRemoteDescription(offer); |
| callee.addTrack(track, stream); |
| const answer = await callee.createAnswer(); |
| await callee.setLocalDescription(answer); |
| await caller.setRemoteDescription(answer); |
| assert_equals(transceiver.currentDirection, 'recvonly'); |
| |
| caller.removeTrack(sender); |
| assert_equals(sender.track, null); |
| assert_equals(transceiver.direction, 'recvonly'); |
| assert_equals(transceiver.currentDirection, 'recvonly'); |
| }, 'Calling removeTrack with currentDirection recvonly should not change direction'); |
| |
| /* |
| 5.1. removeTrack |
| 7. Set sender.track to null. |
| 9. If transceiver.currentDirection is recvonly or inactive, |
| then abort these steps. |
| */ |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const stream = await navigator.mediaDevices.getUserMedia({audio: true}); |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); |
| const [track] = stream.getTracks(); |
| const transceiver = pc.addTransceiver(track, { direction: 'inactive' }); |
| const { sender } = transceiver; |
| |
| assert_equals(sender.track, track); |
| assert_equals(transceiver.direction, 'inactive'); |
| assert_equals(transceiver.currentDirection, null); |
| |
| const offer = await pc.createOffer(); |
| await pc.setLocalDescription(offer); |
| const answer = await generateAnswer(offer); |
| await pc.setRemoteDescription(answer); |
| assert_equals(transceiver.currentDirection, 'inactive'); |
| |
| pc.removeTrack(sender); |
| assert_equals(sender.track, null); |
| assert_equals(transceiver.direction, 'inactive'); |
| assert_equals(transceiver.currentDirection, 'inactive'); |
| }, 'Calling removeTrack with currentDirection inactive should not change direction'); |
| |
| /* |
| TODO |
| 5.1. removeTrack |
| Stops sending media from sender. The RTCRtpSender will still appear |
| in getSenders. Doing so will cause future calls to createOffer to |
| mark the media description for the corresponding transceiver as |
| recvonly or inactive, as defined in [JSEP] (section 5.2.2.). |
| |
| When the other peer stops sending a track in this manner, an ended |
| event is fired at the MediaStreamTrack object. |
| |
| 6. If sender is not in senders (which indicates that it was removed |
| due to setting an RTCSessionDescription of type "rollback"), |
| then abort these steps. |
| 12. Update the negotiation-needed flag for connection. |
| */ |
| </script> |