RTCPeerConnection-removeTrack.https.html (13618B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <title>RTCPeerConnection.prototype.removeTrack</title> 4 <script src="/resources/testharness.js"></script> 5 <script src="/resources/testharnessreport.js"></script> 6 <script src="RTCPeerConnection-helper.js"></script> 7 <script> 8 'use strict'; 9 10 // Test is based on the following editor draft: 11 // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html 12 13 // The following helper functions are called from RTCPeerConnection-helper.js: 14 // generateAnswer 15 16 /* 17 5.1. RTCPeerConnection Interface Extensions 18 partial interface RTCPeerConnection { 19 ... 20 void removeTrack(RTCRtpSender sender); 21 RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind, 22 optional RTCRtpTransceiverInit init); 23 }; 24 */ 25 26 // Before calling removeTrack can be tested, one needs to add MediaStreamTracks to 27 // a peer connection. There are two ways for adding MediaStreamTrack: addTrack and 28 // addTransceiver. addTransceiver is a newer API while addTrack has been implemented 29 // in current browsers for some time. As a result some of the removeTrack tests have 30 // two versions so that removeTrack can be partially tested without addTransceiver 31 // and the transceiver APIs being implemented. 32 33 /* 34 5.1. removeTrack 35 3. If connection's [[isClosed]] slot is true, throw an InvalidStateError. 36 */ 37 promise_test(async t => { 38 const pc = new RTCPeerConnection(); 39 const stream = await getNoiseStream({audio: true}); 40 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 41 const [track] = stream.getTracks(); 42 const transceiver = pc.addTransceiver(track); 43 const { sender } = transceiver; 44 45 pc.close(); 46 assert_throws_dom('InvalidStateError', () => pc.removeTrack(sender)); 47 }, 'addTransceiver - Calling removeTrack when connection is closed should throw InvalidStateError'); 48 49 promise_test(async t => { 50 const pc = new RTCPeerConnection(); 51 t.add_cleanup(() => pc.close()); 52 53 const stream = await getNoiseStream({ audio: true }); 54 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 55 const [track] = stream.getTracks(); 56 const sender = pc.addTrack(track, stream); 57 58 pc.close(); 59 assert_throws_dom('InvalidStateError', () => pc.removeTrack(sender)); 60 }, 'addTrack - Calling removeTrack when connection is closed should throw InvalidStateError'); 61 62 promise_test(async t => { 63 const pc = new RTCPeerConnection(); 64 t.add_cleanup(() => pc.close()); 65 const stream = await getNoiseStream({audio: true}); 66 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 67 const [track] = stream.getTracks(); 68 const transceiver = pc.addTransceiver(track); 69 const { sender } = transceiver; 70 71 const pc2 = new RTCPeerConnection(); 72 pc2.close(); 73 assert_throws_dom('InvalidStateError', () => pc2.removeTrack(sender)); 74 }, 'addTransceiver - Calling removeTrack on different connection that is closed should throw InvalidStateError'); 75 76 promise_test(async t => { 77 const pc = new RTCPeerConnection(); 78 t.add_cleanup(() => pc.close()); 79 80 const stream = await getNoiseStream({ audio: true }); 81 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 82 const [track] = stream.getTracks(); 83 const sender = pc.addTrack(track, stream); 84 85 const pc2 = new RTCPeerConnection(); 86 pc2.close(); 87 assert_throws_dom('InvalidStateError', () => pc2.removeTrack(sender)); 88 }, 'addTrack - Calling removeTrack on different connection that is closed should throw InvalidStateError'); 89 90 /* 91 5.1. removeTrack 92 4. If sender was not created by connection, throw an InvalidAccessError. 93 */ 94 promise_test(async t => { 95 const pc = new RTCPeerConnection(); 96 t.add_cleanup(() => pc.close()); 97 const stream = await getNoiseStream({audio: true}); 98 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 99 const [track] = stream.getTracks(); 100 const transceiver = pc.addTransceiver(track); 101 const { sender } = transceiver; 102 103 const pc2 = new RTCPeerConnection(); 104 t.add_cleanup(() => pc2.close()); 105 assert_throws_dom('InvalidAccessError', () => pc2.removeTrack(sender)); 106 }, 'addTransceiver - Calling removeTrack on different connection should throw InvalidAccessError'); 107 108 promise_test(async t => { 109 const pc = new RTCPeerConnection(); 110 t.add_cleanup(() => pc.close()); 111 112 const stream = await getNoiseStream({ audio: true }); 113 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 114 const [track] = stream.getTracks(); 115 const sender = pc.addTrack(track, stream); 116 117 const pc2 = new RTCPeerConnection(); 118 t.add_cleanup(() => pc2.close()); 119 assert_throws_dom('InvalidAccessError', () => pc2.removeTrack(sender)); 120 }, 'addTrack - Calling removeTrack on different connection should throw InvalidAccessError') 121 122 /* 123 5.1. removeTrack 124 7. Set sender.track to null. 125 */ 126 promise_test(async t => { 127 const pc = new RTCPeerConnection(); 128 t.add_cleanup(() => pc.close()); 129 const stream = await getNoiseStream({audio: true}); 130 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 131 const [track] = stream.getTracks(); 132 const transceiver = pc.addTransceiver(track); 133 const { sender } = transceiver; 134 135 assert_equals(sender.track, track); 136 assert_equals(transceiver.direction, 'sendrecv'); 137 assert_equals(transceiver.currentDirection, null); 138 139 pc.removeTrack(sender); 140 assert_equals(sender.track, null); 141 assert_equals(transceiver.direction, 'recvonly'); 142 }, 'addTransceiver - Calling removeTrack with valid sender should set sender.track to null'); 143 144 promise_test(async t => { 145 const pc = new RTCPeerConnection(); 146 t.add_cleanup(() => pc.close()); 147 148 const stream = await getNoiseStream({ audio: true }); 149 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 150 const [track] = stream.getTracks(); 151 const sender = pc.addTrack(track, stream); 152 153 assert_equals(sender.track, track); 154 155 pc.removeTrack(sender); 156 assert_equals(sender.track, null); 157 }, 'addTrack - Calling removeTrack with valid sender should set sender.track to null'); 158 159 /* 160 5.1. removeTrack 161 7. Set sender.track to null. 162 10. If transceiver.currentDirection is sendrecv set transceiver.direction 163 to recvonly. 164 */ 165 promise_test(async t => { 166 const caller = new RTCPeerConnection(); 167 t.add_cleanup(() => caller.close()); 168 const callee = new RTCPeerConnection(); 169 t.add_cleanup(() => callee.close()); 170 const stream = await getNoiseStream({audio: true}); 171 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 172 const [track] = stream.getTracks(); 173 const transceiver = caller.addTransceiver(track); 174 const { sender } = transceiver; 175 176 assert_equals(sender.track, track); 177 assert_equals(transceiver.direction, 'sendrecv'); 178 assert_equals(transceiver.currentDirection, null); 179 180 const offer = await caller.createOffer(); 181 await caller.setLocalDescription(offer); 182 await callee.setRemoteDescription(offer); 183 callee.addTrack(track, stream); 184 const answer = await callee.createAnswer(); 185 await callee.setLocalDescription(answer); 186 await caller.setRemoteDescription(answer); 187 assert_equals(transceiver.currentDirection, 'sendrecv'); 188 189 caller.removeTrack(sender); 190 assert_equals(sender.track, null); 191 assert_equals(transceiver.direction, 'recvonly'); 192 assert_equals(transceiver.currentDirection, 'sendrecv', 193 'Expect currentDirection to not change'); 194 }, 'Calling removeTrack with currentDirection sendrecv should set direction to recvonly'); 195 196 /* 197 5.1. removeTrack 198 7. Set sender.track to null. 199 11. If transceiver.currentDirection is sendonly set transceiver.direction 200 to inactive. 201 */ 202 promise_test(async t => { 203 const pc = new RTCPeerConnection(); 204 t.add_cleanup(() => pc.close()); 205 const stream = await getNoiseStream({audio: true}); 206 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 207 const [track] = stream.getTracks(); 208 const transceiver = pc.addTransceiver(track, { direction: 'sendonly' }); 209 const { sender } = transceiver; 210 211 assert_equals(sender.track, track); 212 assert_equals(transceiver.direction, 'sendonly'); 213 assert_equals(transceiver.currentDirection, null); 214 215 const offer = await pc.createOffer(); 216 await pc.setLocalDescription(offer); 217 const answer = await generateAnswer(offer); 218 await pc.setRemoteDescription(answer); 219 assert_equals(transceiver.currentDirection, 'sendonly'); 220 221 pc.removeTrack(sender); 222 assert_equals(sender.track, null); 223 assert_equals(transceiver.direction, 'inactive'); 224 assert_equals(transceiver.currentDirection, 'sendonly', 225 'Expect currentDirection to not change'); 226 }, 'Calling removeTrack with currentDirection sendonly should set direction to inactive'); 227 228 /* 229 5.1. removeTrack 230 7. Set sender.track to null. 231 9. If transceiver.currentDirection is recvonly or inactive, 232 then abort these steps. 233 */ 234 promise_test(async t => { 235 const caller = new RTCPeerConnection(); 236 t.add_cleanup(() => caller.close()); 237 const callee = new RTCPeerConnection(); 238 t.add_cleanup(() => callee.close()); 239 const stream = await getNoiseStream({audio: true}); 240 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 241 const [track] = stream.getTracks(); 242 const transceiver = caller.addTransceiver(track, { direction: 'recvonly' }); 243 const { sender } = transceiver; 244 245 assert_equals(sender.track, track); 246 assert_equals(transceiver.direction, 'recvonly'); 247 assert_equals(transceiver.currentDirection, null); 248 249 const offer = await caller.createOffer(); 250 await caller.setLocalDescription(offer); 251 await callee.setRemoteDescription(offer); 252 callee.addTrack(track, stream); 253 const answer = await callee.createAnswer(); 254 await callee.setLocalDescription(answer); 255 await caller.setRemoteDescription(answer); 256 assert_equals(transceiver.currentDirection, 'recvonly'); 257 258 caller.removeTrack(sender); 259 assert_equals(sender.track, null); 260 assert_equals(transceiver.direction, 'recvonly'); 261 assert_equals(transceiver.currentDirection, 'recvonly'); 262 }, 'Calling removeTrack with currentDirection recvonly should not change direction'); 263 264 /* 265 5.1. removeTrack 266 7. Set sender.track to null. 267 9. If transceiver.currentDirection is recvonly or inactive, 268 then abort these steps. 269 */ 270 promise_test(async t => { 271 const pc = new RTCPeerConnection(); 272 t.add_cleanup(() => pc.close()); 273 const stream = await getNoiseStream({audio: true}); 274 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 275 const [track] = stream.getTracks(); 276 const transceiver = pc.addTransceiver(track, { direction: 'inactive' }); 277 const { sender } = transceiver; 278 279 assert_equals(sender.track, track); 280 assert_equals(transceiver.direction, 'inactive'); 281 assert_equals(transceiver.currentDirection, null); 282 283 const offer = await pc.createOffer(); 284 await pc.setLocalDescription(offer); 285 const answer = await generateAnswer(offer); 286 await pc.setRemoteDescription(answer); 287 assert_equals(transceiver.currentDirection, 'inactive'); 288 289 pc.removeTrack(sender); 290 assert_equals(sender.track, null); 291 assert_equals(transceiver.direction, 'inactive'); 292 assert_equals(transceiver.currentDirection, 'inactive'); 293 }, 'Calling removeTrack with currentDirection inactive should not change direction'); 294 295 promise_test(async t => { 296 const pc = new RTCPeerConnection(); 297 t.add_cleanup(() => pc.close()); 298 const stream = await getNoiseStream({audio: true}); 299 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 300 const [track] = stream.getTracks(); 301 const sender = pc.addTrack(track, stream); 302 303 pc.getTransceivers()[0].stop(); 304 // TODO: Spec says this only sets [[Stopping]], not [[Stopped]]. Spec 305 // might change: https://github.com/w3c/webrtc-pc/issues/2874 306 pc.removeTrack(sender); 307 assert_equals(sender.track, track); 308 }, "Calling removeTrack on a stopped transceiver should be a no-op"); 309 310 promise_test(async t => { 311 const pc = new RTCPeerConnection(); 312 t.add_cleanup(() => pc.close()); 313 const stream = await getNoiseStream({audio: true}); 314 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 315 const [track] = stream.getTracks(); 316 const sender = pc.addTrack(track, stream); 317 318 await sender.replaceTrack(null); 319 pc.removeTrack(sender); 320 assert_equals(sender.track, null); 321 }, "Calling removeTrack on a null track should have no effect"); 322 323 324 /* 325 TODO 326 5.1. removeTrack 327 Stops sending media from sender. The RTCRtpSender will still appear 328 in getSenders. Doing so will cause future calls to createOffer to 329 mark the media description for the corresponding transceiver as 330 recvonly or inactive, as defined in [JSEP] (section 5.2.2.). 331 332 When the other peer stops sending a track in this manner, an ended 333 event is fired at the MediaStreamTrack object. 334 335 6. If sender is not in senders (which indicates that it was removed 336 due to setting an RTCSessionDescription of type "rollback"), 337 then abort these steps. 338 12. Update the negotiation-needed flag for connection. 339 */ 340 </script>