RTCRtpSender-replaceTrack.https.html (12851B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <meta name="timeout" content="long"> 4 <title>RTCRtpSender.prototype.replaceTrack</title> 5 <script src="/resources/testharness.js"></script> 6 <script src="/resources/testharnessreport.js"></script> 7 <script src="RTCPeerConnection-helper.js"></script> 8 <script> 9 'use strict'; 10 11 // Test is based on the following editor draft: 12 // https://w3c.github.io/webrtc-pc/archives/20170605/webrtc.html 13 14 /* 15 5.2. RTCRtpSender Interface 16 interface RTCRtpSender { 17 readonly attribute MediaStreamTrack? track; 18 Promise<void> replaceTrack(MediaStreamTrack? withTrack); 19 ... 20 }; 21 22 replaceTrack 23 Attempts to replace the track being sent with another track provided 24 (or with a null track), without renegotiation. 25 */ 26 27 /* 28 5.2. replaceTrack 29 4. If connection's [[isClosed]] slot is true, return a promise rejected 30 with a newly created InvalidStateError and abort these steps. 31 */ 32 promise_test(async t => { 33 const pc = new RTCPeerConnection(); 34 t.add_cleanup(() => pc.close()); 35 const stream = await getNoiseStream({audio: true}); 36 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 37 const [track] = stream.getTracks(); 38 39 const transceiver = pc.addTransceiver('audio'); 40 const { sender } = transceiver; 41 pc.close(); 42 43 return promise_rejects_dom(t, 'InvalidStateError', 44 sender.replaceTrack(track)); 45 }, 'Calling replaceTrack on closed connection should reject with InvalidStateError'); 46 47 /* 48 5.2. replaceTrack 49 7. If withTrack is non-null and withTrack.kind differs from the 50 transceiver kind of transceiver, return a promise rejected with a 51 newly created TypeError. 52 */ 53 promise_test(async t => { 54 const pc = new RTCPeerConnection(); 55 t.add_cleanup(() => pc.close()); 56 const stream = await getNoiseStream({video: true}); 57 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 58 const [track] = stream.getTracks(); 59 60 const transceiver = pc.addTransceiver('audio'); 61 const { sender } = transceiver; 62 63 return promise_rejects_js(t, TypeError, 64 sender.replaceTrack(track)); 65 }, 'Calling replaceTrack with track of different kind should reject with TypeError'); 66 67 /* 68 5.2. replaceTrack 69 5. If transceiver.stopped is true, return a promise rejected with a newly 70 created InvalidStateError. 71 */ 72 promise_test(async t => { 73 const pc = new RTCPeerConnection(); 74 t.add_cleanup(() => pc.close()); 75 const stream = await getNoiseStream({audio: true}); 76 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 77 const [track] = stream.getTracks(); 78 79 const transceiver = pc.addTransceiver('audio'); 80 const { sender } = transceiver; 81 transceiver.stop(); 82 return promise_rejects_dom(t, 'InvalidStateError', 83 sender.replaceTrack(track)); 84 }, 'Calling replaceTrack on stopped sender should reject with InvalidStateError'); 85 86 /* 87 5.2. replaceTrack 88 8. If transceiver is not yet associated with a media description [JSEP] 89 (section 3.4.1.), then set sender's track attribute to withTrack, and 90 return a promise resolved with undefined. 91 */ 92 promise_test(async t => { 93 const pc = new RTCPeerConnection(); 94 t.add_cleanup(() => pc.close()); 95 const stream = await getNoiseStream({audio: true}); 96 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 97 const [track] = stream.getTracks(); 98 99 const transceiver = pc.addTransceiver('audio'); 100 const { sender } = transceiver; 101 assert_equals(sender.track, null); 102 103 return sender.replaceTrack(track) 104 .then(() => { 105 assert_equals(sender.track, track); 106 }); 107 }, 'Calling replaceTrack on sender with null track and not set to session description should resolve with sender.track set to given track'); 108 109 promise_test(async t => { 110 const pc = new RTCPeerConnection(); 111 t.add_cleanup(() => pc.close()); 112 const stream1 = await getNoiseStream({audio: true}); 113 t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); 114 const [track1] = stream1.getTracks(); 115 const stream2 = await getNoiseStream({audio: true}); 116 t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); 117 const [track2] = stream2.getTracks(); 118 119 const transceiver = pc.addTransceiver(track1); 120 const { sender } = transceiver; 121 122 assert_equals(sender.track, track1); 123 124 return sender.replaceTrack(track2) 125 .then(() => { 126 assert_equals(sender.track, track2); 127 }); 128 }, 'Calling replaceTrack on sender not set to session description should resolve with sender.track set to given track'); 129 130 promise_test(async t => { 131 const pc = new RTCPeerConnection(); 132 t.add_cleanup(() => pc.close()); 133 const stream = await getNoiseStream({audio: true}); 134 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 135 const [track] = stream.getTracks(); 136 137 const transceiver = pc.addTransceiver(track); 138 const { sender } = transceiver; 139 140 assert_equals(sender.track, track); 141 142 return sender.replaceTrack(null) 143 .then(() => { 144 assert_equals(sender.track, null); 145 }); 146 }, 'Calling replaceTrack(null) on sender not set to session description should resolve with sender.track set to null'); 147 148 /* 149 5.2. replaceTrack 150 10. Run the following steps in parallel: 151 1. Determine if negotiation is needed to transmit withTrack in place 152 of the sender's existing track. 153 154 Negotiation is not needed if withTrack is null. 155 156 3. Queue a task that runs the following steps: 157 2. Set sender's track attribute to withTrack. 158 */ 159 promise_test(async t => { 160 const pc = new RTCPeerConnection(); 161 t.add_cleanup(() => pc.close()); 162 const stream = await getNoiseStream({audio: true}); 163 t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); 164 const [track] = stream.getTracks(); 165 166 const transceiver = pc.addTransceiver(track); 167 const { sender } = transceiver; 168 169 assert_equals(sender.track, track); 170 171 return pc.createOffer() 172 .then(offer => pc.setLocalDescription(offer)) 173 .then(() => sender.replaceTrack(null)) 174 .then(() => { 175 assert_equals(sender.track, null); 176 }); 177 }, 'Calling replaceTrack(null) on sender set to session description should resolve with sender.track set to null'); 178 179 /* 180 5.2. replaceTrack 181 10. Run the following steps in parallel: 182 1. Determine if negotiation is needed to transmit withTrack in place 183 of the sender's existing track. 184 185 Negotiation is not needed if the sender's existing track is 186 ended (which appears as though the track was muted). 187 188 3. Queue a task that runs the following steps: 189 2. Set sender's track attribute to withTrack. 190 */ 191 promise_test(async t => { 192 const pc = new RTCPeerConnection(); 193 t.add_cleanup(() => pc.close()); 194 const stream1 = await getNoiseStream({audio: true}); 195 t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); 196 const [track1] = stream1.getTracks(); 197 const stream2 = await getNoiseStream({audio: true}); 198 t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); 199 const [track2] = stream1.getTracks(); 200 201 const transceiver = pc.addTransceiver(track1); 202 const { sender } = transceiver; 203 assert_equals(sender.track, track1); 204 205 track1.stop(); 206 207 return pc.createOffer() 208 .then(offer => pc.setLocalDescription(offer)) 209 .then(() => sender.replaceTrack(track2)) 210 .then(() => { 211 assert_equals(sender.track, track2); 212 }); 213 }, 'Calling replaceTrack on sender with stopped track and and set to session description should resolve with sender.track set to given track'); 214 215 /* 216 5.2. replaceTrack 217 10. Run the following steps in parallel: 218 1. Determine if negotiation is needed to transmit withTrack in place 219 of the sender's existing track. 220 221 (tracks generated with default parameters *should* be similar 222 enough to not require re-negotiation) 223 224 3. Queue a task that runs the following steps: 225 2. Set sender's track attribute to withTrack. 226 */ 227 promise_test(async t => { 228 const pc = new RTCPeerConnection(); 229 t.add_cleanup(() => pc.close()); 230 const stream1 = await getNoiseStream({audio: true}); 231 t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); 232 const [track1] = stream1.getTracks(); 233 const stream2 = await getNoiseStream({audio: true}); 234 t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); 235 const [track2] = stream1.getTracks(); 236 237 const transceiver = pc.addTransceiver(track1); 238 const { sender } = transceiver; 239 assert_equals(sender.track, track1); 240 241 return pc.createOffer() 242 .then(offer => pc.setLocalDescription(offer)) 243 .then(() => sender.replaceTrack(track2)) 244 .then(() => { 245 assert_equals(sender.track, track2); 246 }); 247 }, 'Calling replaceTrack on sender with similar track and and set to session description should resolve with sender.track set to new track'); 248 249 /* 250 TODO 251 5.2. replaceTrack 252 To avoid track identifiers changing on the remote receiving end when 253 a track is replaced, the sender must retain the original track 254 identifier and stream associations and use these in subsequent 255 negotiations. 256 257 Non-Testable 258 5.2. replaceTrack 259 10. Run the following steps in parallel: 260 1. Determine if negotiation is needed to transmit withTrack in place 261 of the sender's existing track. 262 263 Ignore which MediaStream the track resides in and the id attribute 264 of the track in this determination. 265 266 If negotiation is needed, then reject p with a newly created 267 InvalidModificationError and abort these steps. 268 269 2. If withTrack is null, have the sender stop sending, without 270 negotiating. Otherwise, have the sender switch seamlessly to 271 transmitting withTrack instead of the sender's existing track, 272 without negotiating. 273 3. Queue a task that runs the following steps: 274 1. If connection's [[isClosed]] slot is true, abort these steps. 275 */ 276 277 promise_test(async t => { 278 const v = document.createElement('video'); 279 v.autoplay = true; 280 const pc1 = new RTCPeerConnection(); 281 t.add_cleanup(() => pc1.close()); 282 const pc2 = new RTCPeerConnection(); 283 t.add_cleanup(() => pc2.close()); 284 const stream1 = await getNoiseStream({video: {signal: 20}}); 285 t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); 286 const [track1] = stream1.getTracks(); 287 const stream2 = await getNoiseStream({video: {signal: 250}}); 288 t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); 289 const [track2] = stream2.getTracks(); 290 const sender = pc1.addTrack(track1); 291 pc2.ontrack = (e) => { 292 v.srcObject = new MediaStream([e.track]); 293 }; 294 const metadataToBeLoaded = new Promise((resolve) => { 295 v.addEventListener('loadedmetadata', () => { 296 resolve(); 297 }); 298 }); 299 exchangeIceCandidates(pc1, pc2); 300 exchangeOfferAnswer(pc1, pc2); 301 await metadataToBeLoaded; 302 await detectSignal(t, v, 20); 303 await sender.replaceTrack(track2); 304 await detectSignal(t, v, 250); 305 }, 'ReplaceTrack transmits the new track not the old track'); 306 307 promise_test(async t => { 308 const v = document.createElement('video'); 309 v.autoplay = true; 310 const pc1 = new RTCPeerConnection(); 311 t.add_cleanup(() => pc1.close()); 312 const pc2 = new RTCPeerConnection(); 313 t.add_cleanup(() => pc2.close()); 314 const stream1 = await getNoiseStream({video: {signal: 20}}); 315 t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); 316 const [track1] = stream1.getTracks(); 317 const stream2 = await getNoiseStream({video: {signal: 250}}); 318 t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); 319 const [track2] = stream2.getTracks(); 320 const sender = pc1.addTrack(track1); 321 pc2.ontrack = (e) => { 322 v.srcObject = new MediaStream([e.track]); 323 }; 324 const metadataToBeLoaded = new Promise((resolve) => { 325 v.addEventListener('loadedmetadata', () => { 326 resolve(); 327 }); 328 }); 329 exchangeIceCandidates(pc1, pc2); 330 exchangeOfferAnswer(pc1, pc2); 331 await metadataToBeLoaded; 332 await detectSignal(t, v, 20); 333 await sender.replaceTrack(null); 334 await sender.replaceTrack(track2); 335 await detectSignal(t, v, 250); 336 }, 'ReplaceTrack null -> new track transmits the new track'); 337 </script>