RTCPeerConnection-addTrack.https.html (16097B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <title>RTCPeerConnection.prototype.addTrack</title> 4 <script src="/resources/testharness.js"></script> 5 <script src="/resources/testharnessreport.js"></script> 6 <script src=/resources/testdriver.js></script> 7 <script src=/resources/testdriver-vendor.js></script> 8 <script src='../mediacapture-streams/permission-helper.js'></script> 9 <script src="RTCPeerConnection-helper.js"></script> 10 <script> 11 'use strict'; 12 13 // Test is based on the following editor draft: 14 // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html 15 16 // The following helper functions are called from RTCPeerConnection-helper.js: 17 // getNoiseStream() 18 19 /* 20 5.1. RTCPeerConnection Interface Extensions 21 partial interface RTCPeerConnection { 22 ... 23 sequence<RTCRtpSender> getSenders(); 24 sequence<RTCRtpReceiver> getReceivers(); 25 sequence<RTCRtpTransceiver> getTransceivers(); 26 RTCRtpSender addTrack(MediaStreamTrack track, 27 MediaStream... streams); 28 RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind, 29 optional RTCRtpTransceiverInit init); 30 }; 31 32 Note 33 While addTrack checks if the MediaStreamTrack given as an argument is 34 already being sent to avoid sending the same MediaStreamTrack twice, 35 the other ways do not, allowing the same MediaStreamTrack to be sent 36 several times simultaneously. 37 */ 38 39 /* 40 5.1. addTrack 41 4. If connection's [[isClosed]] slot is true, throw an InvalidStateError. 42 */ 43 promise_test(async t => { 44 const pc = new RTCPeerConnection(); 45 t.add_cleanup(() => pc.close()); 46 47 const stream = await getNoiseStream({ audio: true }); 48 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 49 const [track] = stream.getTracks(); 50 51 pc.close(); 52 assert_throws_dom('InvalidStateError', () => pc.addTrack(track, stream)) 53 }, 'addTrack when pc is closed should throw InvalidStateError'); 54 55 /* 56 5.1. addTrack 57 8. If sender is null, run the following steps: 58 1. Create an RTCRtpSender with track and streams and let sender be 59 the result. 60 2. Create an RTCRtpReceiver with track.kind as kind and let receiver 61 be the result. 62 3. Create an RTCRtpTransceiver with sender and receiver and let 63 transceiver be the result. 64 4. Add transceiver to connection's set of transceivers. 65 */ 66 promise_test(async t => { 67 const pc = new RTCPeerConnection(); 68 t.add_cleanup(() => pc.close()); 69 70 const stream = await getNoiseStream({ audio: true }); 71 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 72 const [track] = stream.getTracks(); 73 74 const sender = pc.addTrack(track); 75 76 assert_true(sender instanceof RTCRtpSender, 77 'Expect sender to be instance of RTCRtpSender'); 78 79 assert_equals(sender.track, track, 80 `Expect sender's track to be the added track`); 81 82 const transceivers = pc.getTransceivers(); 83 assert_equals(transceivers.length, 1, 84 'Expect only one transceiver with sender added'); 85 86 const [transceiver] = transceivers; 87 assert_equals(transceiver.sender, sender); 88 89 assert_array_equals([sender], pc.getSenders(), 90 'Expect only one sender with given track added'); 91 92 const { receiver } = transceiver; 93 assert_equals(receiver.track.kind, 'audio'); 94 assert_array_equals([transceiver.receiver], pc.getReceivers(), 95 'Expect only one receiver associated with transceiver added'); 96 }, 'addTrack with single track argument and no stream should succeed'); 97 98 promise_test(async t => { 99 const pc = new RTCPeerConnection(); 100 t.add_cleanup(() => pc.close()); 101 102 const stream = await getNoiseStream({ audio: true }); 103 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 104 const [track] = stream.getTracks(); 105 106 const sender = pc.addTrack(track, stream); 107 108 assert_true(sender instanceof RTCRtpSender, 109 'Expect sender to be instance of RTCRtpSender'); 110 111 assert_equals(sender.track, track, 112 `Expect sender's track to be the added track`); 113 }, 'addTrack with single track argument and single stream should succeed'); 114 115 promise_test(async t => { 116 const pc = new RTCPeerConnection(); 117 t.add_cleanup(() => pc.close()); 118 119 const stream = await getNoiseStream({ audio: true }); 120 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 121 const [track] = stream.getTracks(); 122 123 const stream2 = new MediaStream([track]); 124 const sender = pc.addTrack(track, stream, stream2); 125 126 assert_true(sender instanceof RTCRtpSender, 127 'Expect sender to be instance of RTCRtpSender'); 128 129 assert_equals(sender.track, track, 130 `Expect sender's track to be the added track`); 131 }, 'addTrack with single track argument and multiple streams should succeed'); 132 133 /* 134 5.1. addTrack 135 5. Let senders be the result of executing the CollectSenders algorithm. 136 If an RTCRtpSender for track already exists in senders, throw an 137 InvalidAccessError. 138 */ 139 promise_test(async t => { 140 const pc = new RTCPeerConnection(); 141 t.add_cleanup(() => pc.close()); 142 143 const stream = await getNoiseStream({ audio: true }); 144 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 145 const [track] = stream.getTracks(); 146 147 pc.addTrack(track, stream); 148 assert_throws_dom('InvalidAccessError', () => pc.addTrack(track, stream)); 149 }, 'Adding the same track multiple times should throw InvalidAccessError'); 150 151 /* 152 5.1. addTrack 153 6. The steps below describe how to determine if an existing sender can 154 be reused. 155 156 If any RTCRtpSender object in senders matches all the following 157 criteria, let sender be that object, or null otherwise: 158 - The sender's track is null. 159 - The transceiver kind of the RTCRtpTransceiver, associated with 160 the sender, matches track's kind. 161 - The sender has never been used to send. More precisely, the 162 RTCRtpTransceiver associated with the sender has never had a 163 currentDirection of sendrecv or sendonly. 164 7. If sender is not null, run the following steps to use that sender: 165 1. Set sender.track to track. 166 3. Enable sending direction on the RTCRtpTransceiver associated 167 with sender. 168 */ 169 promise_test(async t => { 170 const pc = new RTCPeerConnection(); 171 t.add_cleanup(() => pc.close()); 172 173 const transceiver = pc.addTransceiver('audio', { direction: 'recvonly' }); 174 assert_equals(transceiver.sender.track, null); 175 assert_equals(transceiver.direction, 'recvonly'); 176 177 await setMediaPermission("granted", ["microphone"]); 178 const stream = await navigator.mediaDevices.getUserMedia({audio: true}); 179 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 180 const [track] = stream.getTracks(); 181 const sender = pc.addTrack(track); 182 183 assert_equals(sender, transceiver.sender); 184 assert_equals(sender.track, track); 185 assert_equals(transceiver.direction, 'sendrecv'); 186 assert_array_equals([sender], pc.getSenders()); 187 }, 'addTrack with existing sender with null track, same kind, and recvonly direction should reuse sender'); 188 189 promise_test(async t => { 190 const pc = new RTCPeerConnection(); 191 t.add_cleanup(() => pc.close()); 192 193 const transceiver = pc.addTransceiver('audio'); 194 assert_equals(transceiver.sender.track, null); 195 assert_equals(transceiver.direction, 'sendrecv'); 196 197 const stream = await getNoiseStream({audio: true}); 198 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 199 const [track] = stream.getTracks(); 200 const sender = pc.addTrack(track); 201 202 assert_equals(sender.track, track); 203 assert_equals(sender, transceiver.sender); 204 }, 'addTrack with existing sender that has not been used to send should reuse the sender'); 205 206 promise_test(async t => { 207 const caller = new RTCPeerConnection(); 208 t.add_cleanup(() => caller.close()); 209 const callee = new RTCPeerConnection(); 210 t.add_cleanup(() => callee.close()); 211 212 const stream = await getNoiseStream({audio: true}); 213 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 214 const [track] = stream.getTracks(); 215 const transceiver = caller.addTransceiver(track); 216 { 217 const offer = await caller.createOffer(); 218 await caller.setLocalDescription(offer); 219 await callee.setRemoteDescription(offer); 220 const answer = await callee.createAnswer(); 221 await callee.setLocalDescription(answer); 222 await caller.setRemoteDescription(answer); 223 } 224 assert_equals(transceiver.currentDirection, 'sendonly'); 225 226 caller.removeTrack(transceiver.sender); 227 { 228 const offer = await caller.createOffer(); 229 await caller.setLocalDescription(offer); 230 await callee.setRemoteDescription(offer); 231 const answer = await callee.createAnswer(); 232 await callee.setLocalDescription(answer); 233 await caller.setRemoteDescription(answer); 234 } 235 assert_equals(transceiver.direction, 'recvonly'); 236 assert_equals(transceiver.currentDirection, 'inactive'); 237 238 // |transceiver.sender| is currently not used for sending, but it should not 239 // be reused because it has been used for sending before. 240 const sender = caller.addTrack(track); 241 assert_true(sender != null); 242 assert_not_equals(sender, transceiver.sender); 243 }, 'addTrack with existing sender that has been used to send should create new sender'); 244 245 promise_test(async t => { 246 const pc = new RTCPeerConnection(); 247 t.add_cleanup(() => pc.close()); 248 249 const transceiver = pc.addTransceiver('video', { direction: 'recvonly' }); 250 assert_equals(transceiver.sender.track, null); 251 assert_equals(transceiver.direction, 'recvonly'); 252 253 const stream = await getNoiseStream({audio: true}); 254 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 255 const [track] = stream.getTracks(); 256 const sender = pc.addTrack(track); 257 258 assert_equals(sender.track, track); 259 assert_not_equals(sender, transceiver.sender); 260 261 const senders = pc.getSenders(); 262 assert_equals(senders.length, 2, 263 'Expect 2 senders added to connection'); 264 265 assert_true(senders.includes(sender), 266 'Expect senders list to include sender'); 267 268 assert_true(senders.includes(transceiver.sender), 269 `Expect senders list to include first transceiver's sender`); 270 }, 'addTrack with existing sender with null track, different kind, and recvonly direction should create new sender'); 271 272 /* 273 TODO 274 5.1. addTrack 275 3. Let streams be a list of MediaStream objects constructed from the 276 method's remaining arguments, or an empty list if the method was 277 called with a single argument. 278 6. The steps below describe how to determine if an existing sender can 279 be reused. Doing so will cause future calls to createOffer and 280 createAnswer to mark the corresponding media description as sendrecv 281 or sendonly and add the MSID of the track added, as defined in [JSEP] 282 (section 5.2.2. and section 5.3.2.). 283 284 Non-Testable 285 5.1. addTrack 286 7. If sender is not null, run the following steps to use that sender: 287 2. Set sender's [[associated MediaStreams]] to streams. 288 289 Tested in RTCPeerConnection-onnegotiationneeded.html: 290 5.1. addTrack 291 10. Update the negotiation-needed flag for connection. 292 293 */ 294 295 promise_test(async t => { 296 const caller = new RTCPeerConnection(); 297 t.add_cleanup(() => caller.close()); 298 const callee = new RTCPeerConnection(); 299 t.add_cleanup(() => callee.close()); 300 301 const stream = await getNoiseStream({audio: true}); 302 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 303 const [track] = stream.getTracks(); 304 const transceiver = caller.addTransceiver(track); 305 // Note that this test doesn't process canididates. 306 { 307 const offer = await caller.createOffer(); 308 await caller.setLocalDescription(offer); 309 await callee.setRemoteDescription(offer); 310 const answer = await callee.createAnswer(); 311 await callee.setLocalDescription(answer); 312 await caller.setRemoteDescription(answer); 313 } 314 assert_equals(transceiver.currentDirection, 'sendonly'); 315 await waitForIceGatheringState(caller, ['complete']); 316 await waitForIceGatheringState(callee, ['complete']); 317 318 const second_stream = await getNoiseStream({audio: true}); 319 t.add_cleanup(() => second_stream.getTracks().forEach(track => track.stop())); 320 // There may be callee candidates in flight. It seems that waiting 321 // for a createOffer() is enough time to let them complete processing. 322 // TODO(https://crbug.com/webrtc/13095): Fix bug and remove. 323 await caller.createOffer(); 324 325 const [second_track] = second_stream.getTracks(); 326 caller.onicecandidate = t.unreached_func( 327 'No caller candidates should be generated.'); 328 callee.onicecandidate = t.unreached_func( 329 'No callee candidates should be generated.'); 330 caller.addTrack(second_track); 331 { 332 const offer = await caller.createOffer(); 333 await caller.setLocalDescription(offer); 334 await callee.setRemoteDescription(offer); 335 const answer = await callee.createAnswer(); 336 await callee.setLocalDescription(answer); 337 await caller.setRemoteDescription(answer); 338 } 339 // Check that we're bundled. 340 const [first_transceiver, second_transceiver] = caller.getTransceivers(); 341 assert_equals(first_transceiver.transport, second_transceiver.transport); 342 343 }, 'Adding more tracks does not generate more candidates if bundled'); 344 345 promise_test(async t => { 346 const pc1 = new RTCPeerConnection(); 347 t.add_cleanup(() => pc1.close()); 348 const pc2 = new RTCPeerConnection(); 349 t.add_cleanup(() => pc2.close()); 350 351 const stream = await getNoiseStream({ audio: true }); 352 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 353 const [track] = stream.getTracks(); 354 355 pc1.addTrack(track); 356 const offer = await pc1.createOffer(); 357 // We do not await here; we want to ensure that the transceiver this creates 358 // is untouched by addTrack, and that addTrack creates _another_ transceiver 359 const srdPromise = pc2.setRemoteDescription(offer); 360 361 const sender = pc2.addTrack(track); 362 363 await srdPromise; 364 365 assert_equals(pc2.getTransceivers().length, 1, "Should have 1 transceiver"); 366 assert_equals(pc2.getTransceivers()[0].sender, sender, "The transceiver should be the one added by addTrack"); 367 }, 'Calling addTrack while sRD(offer) is pending should allow the new remote transceiver to be the same one that addTrack creates'); 368 369 promise_test(async t => { 370 const pc1 = new RTCPeerConnection(); 371 t.add_cleanup(() => pc1.close()); 372 const pc2 = new RTCPeerConnection(); 373 t.add_cleanup(() => pc2.close()); 374 pc1.addTransceiver('video'); 375 376 const stream = await getNoiseStream({ audio: true }); 377 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 378 const [track] = stream.getTracks(); 379 380 const offer = await pc1.createOffer(); 381 const srdPromise = pc2.setRemoteDescription(offer); 382 assert_equals(pc2.getTransceivers().length, 0); 383 pc2.addTrack(track); 384 assert_equals(pc2.getTransceivers().length, 1); 385 const transceiver0 = pc2.getTransceivers()[0]; 386 assert_equals(transceiver0.mid, null); 387 await srdPromise; 388 assert_equals(pc2.getTransceivers().length, 2); 389 const transceiver1 = pc2.getTransceivers()[1]; 390 assert_equals(transceiver0.mid, null); 391 assert_not_equals(transceiver1.mid, null); 392 }, 'When addTrack is called while sRD is in progress, and both addTrack and sRD add a transceiver of different media types, the addTrack transceiver should come first, and then the sRD transceiver.'); 393 394 </script>