RTCPeerConnection-ontrack.https.html (11512B)
1 <!doctype html> 2 <meta charset=utf-8> 3 <title>RTCPeerConnection.prototype.ontrack</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 // getTrackFromUserMedia 15 16 /* 17 4.3.1.6. Set the RTCSessionSessionDescription 18 2.2.8. If description is set as a remote description, then run the following 19 steps for each media description in description: 20 3. Set transceiver's mid value to the mid of the corresponding media 21 description. If the media description has no MID, and transceiver's 22 mid is unset, generate a random value as described in [JSEP] (section 5.9.). 23 4. If the direction of the media description is sendrecv or sendonly, and 24 transceiver.receiver.track has not yet been fired in a track event, 25 process the remote track for the media description, given transceiver. 26 27 5.1.1. Processing Remote MediaStreamTracks 28 To process the remote track for an incoming media description [JSEP] 29 (section 5.9.) given RTCRtpTransceiver transceiver, the user agent MUST 30 run the following steps: 31 32 1. Let connection be the RTCPeerConnection object associated with transceiver. 33 2. Let streams be a list of MediaStream objects that the media description 34 indicates the MediaStreamTrack belongs to. 35 3. Add track to all MediaStream objects in streams. 36 4. Queue a task to fire an event named track with transceiver, track, and 37 streams at the connection object. 38 39 5.7. RTCTrackEvent 40 [Constructor(DOMString type, RTCTrackEventInit eventInitDict)] 41 interface RTCTrackEvent : Event { 42 readonly attribute RTCRtpReceiver receiver; 43 readonly attribute MediaStreamTrack track; 44 [SameObject] 45 readonly attribute FrozenArray<MediaStream> streams; 46 readonly attribute RTCRtpTransceiver transceiver; 47 }; 48 49 [mediacapture-main] 50 4.2. MediaStream 51 interface MediaStream : EventTarget { 52 readonly attribute DOMString id; 53 sequence<MediaStreamTrack> getTracks(); 54 ... 55 }; 56 57 [mediacapture-main] 58 4.3. MediaStreamTrack 59 interface MediaStreamTrack : EventTarget { 60 readonly attribute DOMString kind; 61 readonly attribute DOMString id; 62 ... 63 }; 64 */ 65 66 function validateTrackEvent(trackEvent) { 67 const { receiver, track, streams, transceiver } = trackEvent; 68 69 assert_true(track instanceof MediaStreamTrack, 70 'Expect track to be instance of MediaStreamTrack'); 71 72 assert_true(Array.isArray(streams), 73 'Expect streams to be an array'); 74 75 for(const mediaStream of streams) { 76 assert_true(mediaStream instanceof MediaStream, 77 'Expect elements in streams to be instance of MediaStream'); 78 79 assert_true(mediaStream.getTracks().includes(track), 80 'Expect each mediaStream to have track as one of their tracks'); 81 } 82 83 assert_true(receiver instanceof RTCRtpReceiver, 84 'Expect trackEvent.receiver to be defined and is instance of RTCRtpReceiver'); 85 86 assert_equals(receiver.track, track, 87 'Expect trackEvent.receiver.track to be the same as trackEvent.track'); 88 89 assert_true(transceiver instanceof RTCRtpTransceiver, 90 'Expect trackEvent.transceiver to be defined and is instance of RTCRtpTransceiver'); 91 92 assert_equals(transceiver.receiver, receiver, 93 'Expect trackEvent.transceiver.receiver to be the same as trackEvent.receiver'); 94 } 95 96 // tests that ontrack is called and parses the msid information from the SDP and creates 97 // the streams with matching identifiers. 98 promise_test(async t => { 99 const pc = new RTCPeerConnection(); 100 101 t.add_cleanup(() => pc.close()); 102 103 // Fail the test if the ontrack event handler is not implemented 104 assert_idl_attribute(pc, 'ontrack', 'Expect pc to have ontrack event handler attribute'); 105 106 const sdp = `v=0 107 o=- 166855176514521964 2 IN IP4 127.0.0.1 108 s=- 109 t=0 0 110 a=msid-semantic:WMS * 111 m=audio 9 UDP/TLS/RTP/SAVPF 111 112 c=IN IP4 0.0.0.0 113 a=rtcp:9 IN IP4 0.0.0.0 114 a=ice-ufrag:someufrag 115 a=ice-pwd:somelongpwdwithenoughrandomness 116 a=fingerprint:sha-256 8C:71:B3:8D:A5:38:FD:8F:A4:2E:A2:65:6C:86:52:BC:E0:6E:94:F2:9F:7C:4D:B5:DF:AF:AA:6F:44:90:8D:F4 117 a=setup:actpass 118 a=rtcp-mux 119 a=mid:mid1 120 a=sendonly 121 a=rtpmap:111 opus/48000/2 122 a=msid:stream1 track1 123 a=ssrc:1001 cname:some 124 `; 125 126 const trackEventPromise = addEventListenerPromise(t, pc, 'track'); 127 await pc.setRemoteDescription({ type: 'offer', sdp }); 128 const trackEvent = await trackEventPromise; 129 const { streams, track, transceiver } = trackEvent; 130 131 assert_equals(streams.length, 1, 132 'the track belongs to one MediaStream'); 133 134 const [stream] = streams; 135 assert_equals(stream.id, 'stream1', 136 'Expect stream.id to be the same as specified in the a=msid line'); 137 138 assert_equals(track.kind, 'audio', 139 'Expect track.kind to be audio'); 140 141 validateTrackEvent(trackEvent); 142 143 assert_equals(transceiver.direction, 'recvonly', 144 'Expect transceiver.direction to be reverse of sendonly (recvonly)'); 145 }, 'setRemoteDescription should trigger ontrack event when the MSID of the stream is is parsed.'); 146 147 promise_test(async t => { 148 const pc = new RTCPeerConnection(); 149 150 t.add_cleanup(() => pc.close()); 151 152 assert_idl_attribute(pc, 'ontrack', 'Expect pc to have ontrack event handler attribute'); 153 154 const sdp = `v=0 155 o=- 166855176514521964 2 IN IP4 127.0.0.1 156 s=- 157 t=0 0 158 a=msid-semantic:WMS * 159 m=audio 9 UDP/TLS/RTP/SAVPF 111 160 c=IN IP4 0.0.0.0 161 a=rtcp:9 IN IP4 0.0.0.0 162 a=ice-ufrag:someufrag 163 a=ice-pwd:somelongpwdwithenoughrandomness 164 a=fingerprint:sha-256 8C:71:B3:8D:A5:38:FD:8F:A4:2E:A2:65:6C:86:52:BC:E0:6E:94:F2:9F:7C:4D:B5:DF:AF:AA:6F:44:90:8D:F4 165 a=setup:actpass 166 a=rtcp-mux 167 a=mid:mid1 168 a=recvonly 169 a=rtpmap:111 opus/48000/2 170 a=msid:stream1 track1 171 a=ssrc:1001 cname:some 172 `; 173 174 pc.ontrack = t.unreached_func('ontrack event should not fire for track with recvonly direction'); 175 176 await pc.setRemoteDescription({ type: 'offer', sdp }); 177 await new Promise(resolve => t.step_timeout(resolve, 100)); 178 }, 'setRemoteDescription() with m= line of recvonly direction should not trigger track event'); 179 180 promise_test(async t => { 181 const pc1 = new RTCPeerConnection(); 182 t.add_cleanup(() => pc1.close()); 183 const pc2 = new RTCPeerConnection(); 184 185 t.add_cleanup(() => pc2.close()); 186 187 const [track, mediaStream] = await getTrackFromUserMedia('audio'); 188 pc1.addTrack(track, mediaStream); 189 const trackEventPromise = addEventListenerPromise(t, pc2, 'track'); 190 await pc2.setRemoteDescription(await pc1.createOffer()); 191 const trackEvent = await trackEventPromise; 192 193 assert_equals(trackEvent.track.kind, 'audio', 194 'Expect track.kind to be audio'); 195 196 validateTrackEvent(trackEvent); 197 }, 'addTrack() should cause remote connection to fire ontrack when setRemoteDescription()'); 198 199 promise_test(async t => { 200 const pc1 = new RTCPeerConnection(); 201 t.add_cleanup(() => pc1.close()); 202 const pc2 = new RTCPeerConnection(); 203 204 t.add_cleanup(() => pc2.close()); 205 206 pc1.addTransceiver('video'); 207 208 const trackEventPromise = addEventListenerPromise(t, pc2, 'track'); 209 await pc2.setRemoteDescription(await pc1.createOffer()); 210 const trackEvent = await trackEventPromise; 211 const { track } = trackEvent; 212 213 assert_equals(track.kind, 'video', 214 'Expect track.kind to be video'); 215 216 validateTrackEvent(trackEvent); 217 }, `addTransceiver('video') should cause remote connection to fire ontrack when setRemoteDescription()`); 218 219 promise_test(async t => { 220 const pc1 = new RTCPeerConnection(); 221 t.add_cleanup(() => pc1.close()); 222 const pc2 = new RTCPeerConnection(); 223 224 t.add_cleanup(() => pc2.close()); 225 226 pc1.addTransceiver('audio', { direction: 'inactive' }); 227 pc2.ontrack = t.unreached_func('ontrack event should not fire for track with inactive direction'); 228 229 await pc2.setRemoteDescription(await pc1.createOffer()); 230 await new Promise(resolve => t.step_timeout(resolve, 100)); 231 }, `addTransceiver() with inactive direction should not cause remote connection to fire ontrack when setRemoteDescription()`); 232 233 ["audio", "video"].forEach(type => promise_test(async t => { 234 const pc1 = new RTCPeerConnection(); 235 t.add_cleanup(() => pc1.close()); 236 const pc2 = new RTCPeerConnection(); 237 t.add_cleanup(() => pc2.close()); 238 239 const checkNoUnexpectedTrack = ({track}) => { 240 assert_equals(track.kind, type, `ontrack event should not fire for ${track.kind}`); 241 }; 242 243 pc2.ontrack = t.step_func(checkNoUnexpectedTrack); 244 pc1.ontrack = t.step_func(checkNoUnexpectedTrack); 245 246 await pc1.setLocalDescription(await pc1.createOffer( 247 { offerToReceiveVideo: true, offerToReceiveAudio: true })); 248 249 pc2.addTrack(...await getTrackFromUserMedia(type)); 250 251 await pc2.setRemoteDescription(pc1.localDescription); 252 await pc2.setLocalDescription(await pc2.createAnswer()); 253 await pc1.setRemoteDescription(pc2.localDescription); 254 255 await new Promise(resolve => t.step_timeout(resolve, 100)); 256 }, `Using offerToReceiveAudio and offerToReceiveVideo should only cause a ${type} track event to fire, if ${type} was the only type negotiated`)); 257 258 promise_test(async t => { 259 const pc1 = new RTCPeerConnection(); 260 t.add_cleanup(() => pc1.close()); 261 const pc2 = new RTCPeerConnection(); 262 t.add_cleanup(() => pc2.close()); 263 264 const stream = new MediaStream(); 265 const stream2 = new MediaStream(); 266 // audio-then-video. 267 pc1.addTransceiver('audio', {streams: [stream]}); 268 pc1.addTransceiver('video', {streams: [stream]}); 269 // video-then-audio 270 pc1.addTransceiver('video', {streams: [stream2]}); 271 pc1.addTransceiver('audio', {streams: [stream2]}); 272 const tracks = []; 273 pc2.ontrack = (e) => tracks.push(e.track) 274 275 await pc2.setRemoteDescription(await pc1.createOffer()); 276 assert_equals(tracks.length, 4, 'ontrack should have fired twice'); 277 assert_equals(tracks[0].kind, 'audio', 'first ontrack fires with an audio track'); 278 assert_equals(tracks[1].kind, 'video', 'second ontrack fires with a video track'); 279 assert_equals(tracks[2].kind, 'video', 'third ontrack fires with a video track'); 280 assert_equals(tracks[3].kind, 'audio', 'fourth ontrack fires with an audio track'); 281 }, `addTransceiver order of kinds is retained in ontrack at the receiver`); 282 283 promise_test(async t => { 284 const pc = new RTCPeerConnection(); 285 t.add_cleanup(() => pc.close()); 286 287 const sdp = `v=0 288 o=- 166855176514521964 2 IN IP4 127.0.0.1 289 s=- 290 t=0 0 291 a=msid-semantic:WMS * 292 m=audio 0 UDP/TLS/RTP/SAVPF 111 293 c=IN IP4 0.0.0.0 294 a=rtcp:9 IN IP4 0.0.0.0 295 a=ice-ufrag:someufrag 296 a=ice-pwd:somelongpwdwithenoughrandomness 297 a=fingerprint:sha-256 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 298 a=setup:actpass 299 a=mid:0 300 a=sendonly 301 a=rtcp-mux 302 a=rtpmap:111 opus/48000/2 303 a=ssrc:1001 cname:some 304 `; 305 pc.ontrack = t.unreached_func('ontrack event should not fire for track in a rejected media section'); 306 307 await pc.setRemoteDescription({type: 'offer', sdp}); 308 }, `ontrack should not fire for rejected media sections`); 309 </script>