| <!doctype html> |
| <meta charset=utf-8> |
| <title>RTCPeerConnection.prototype.ontrack</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: |
| // getTrackFromUserMedia |
| |
| /* |
| 4.3.1.6. Set the RTCSessionSessionDescription |
| 2.2.8. If description is set as a remote description, then run the following |
| steps for each media description in description: |
| 3. Set transceiver's mid value to the mid of the corresponding media |
| description. If the media description has no MID, and transceiver's |
| mid is unset, generate a random value as described in [JSEP] (section 5.9.). |
| 4. If the direction of the media description is sendrecv or sendonly, and |
| transceiver.receiver.track has not yet been fired in a track event, |
| process the remote track for the media description, given transceiver. |
| |
| 5.1.1. Processing Remote MediaStreamTracks |
| To process the remote track for an incoming media description [JSEP] |
| (section 5.9.) given RTCRtpTransceiver transceiver, the user agent MUST |
| run the following steps: |
| |
| 1. Let connection be the RTCPeerConnection object associated with transceiver. |
| 2. Let streams be a list of MediaStream objects that the media description |
| indicates the MediaStreamTrack belongs to. |
| 3. Add track to all MediaStream objects in streams. |
| 4. Queue a task to fire an event named track with transceiver, track, and |
| streams at the connection object. |
| |
| 5.7. RTCTrackEvent |
| [Constructor(DOMString type, RTCTrackEventInit eventInitDict)] |
| interface RTCTrackEvent : Event { |
| readonly attribute RTCRtpReceiver receiver; |
| readonly attribute MediaStreamTrack track; |
| [SameObject] |
| readonly attribute FrozenArray<MediaStream> streams; |
| readonly attribute RTCRtpTransceiver transceiver; |
| }; |
| |
| [mediacapture-main] |
| 4.2. MediaStream |
| interface MediaStream : EventTarget { |
| readonly attribute DOMString id; |
| sequence<MediaStreamTrack> getTracks(); |
| ... |
| }; |
| |
| [mediacapture-main] |
| 4.3. MediaStreamTrack |
| interface MediaStreamTrack : EventTarget { |
| readonly attribute DOMString kind; |
| readonly attribute DOMString id; |
| ... |
| }; |
| */ |
| |
| function validateTrackEvent(trackEvent) { |
| const { receiver, track, streams, transceiver } = trackEvent; |
| |
| assert_true(track instanceof MediaStreamTrack, |
| 'Expect track to be instance of MediaStreamTrack'); |
| |
| assert_true(Array.isArray(streams), |
| 'Expect streams to be an array'); |
| |
| for(const mediaStream of streams) { |
| assert_true(mediaStream instanceof MediaStream, |
| 'Expect elements in streams to be instance of MediaStream'); |
| |
| assert_true(mediaStream.getTracks().includes(track), |
| 'Expect each mediaStream to have track as one of their tracks'); |
| } |
| |
| assert_true(receiver instanceof RTCRtpReceiver, |
| 'Expect trackEvent.receiver to be defined and is instance of RTCRtpReceiver'); |
| |
| assert_equals(receiver.track, track, |
| 'Expect trackEvent.receiver.track to be the same as trackEvent.track'); |
| |
| assert_true(transceiver instanceof RTCRtpTransceiver, |
| 'Expect trackEvent.transceiver to be defined and is instance of RTCRtpTransceiver'); |
| |
| assert_equals(transceiver.receiver, receiver, |
| 'Expect trackEvent.transceiver.receiver to be the same as trackEvent.receiver'); |
| } |
| |
| // tests that ontrack is called and parses the msid information from the SDP and creates |
| // the streams with matching identifiers. |
| async_test(t => { |
| const pc = new RTCPeerConnection(); |
| |
| t.add_cleanup(() => pc.close()); |
| |
| // Fail the test if the ontrack event handler is not implemented |
| assert_idl_attribute(pc, 'ontrack', 'Expect pc to have ontrack event handler attribute'); |
| |
| const sdp = `v=0 |
| o=- 166855176514521964 2 IN IP4 127.0.0.1 |
| s=- |
| t=0 0 |
| a=msid-semantic:WMS * |
| m=audio 9 UDP/TLS/RTP/SAVPF 111 |
| c=IN IP4 0.0.0.0 |
| a=rtcp:9 IN IP4 0.0.0.0 |
| a=ice-ufrag:someufrag |
| a=ice-pwd:somelongpwdwithenoughrandomness |
| 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 |
| a=setup:actpass |
| a=rtcp-mux |
| a=mid:mid1 |
| a=sendonly |
| a=rtpmap:111 opus/48000/2 |
| a=msid:stream1 track1 |
| a=ssrc:1001 cname:some |
| `; |
| |
| pc.ontrack = t.step_func(trackEvent => { |
| const { streams, track, transceiver } = trackEvent; |
| |
| assert_equals(streams.length, 1, |
| 'the track belongs to one MediaStream'); |
| |
| const [stream] = streams; |
| assert_equals(stream.id, 'stream1', |
| 'Expect stream.id to be the same as specified in the a=msid line'); |
| |
| assert_equals(track.kind, 'audio', |
| 'Expect track.kind to be audio'); |
| |
| validateTrackEvent(trackEvent); |
| |
| assert_equals(transceiver.direction, 'recvonly', |
| 'Expect transceiver.direction to be reverse of sendonly (recvonly)'); |
| |
| t.done(); |
| }); |
| |
| pc.setRemoteDescription({ type: 'offer', sdp }) |
| .catch(t.step_func(err => { |
| assert_unreached('Error ' + err.name + ': ' + err.message); |
| })); |
| }, 'setRemoteDescription should trigger ontrack event when the MSID of the stream is is parsed.'); |
| |
| async_test(t => { |
| const pc = new RTCPeerConnection(); |
| |
| t.add_cleanup(() => pc.close()); |
| |
| assert_idl_attribute(pc, 'ontrack', 'Expect pc to have ontrack event handler attribute'); |
| |
| const sdp = `v=0 |
| o=- 166855176514521964 2 IN IP4 127.0.0.1 |
| s=- |
| t=0 0 |
| a=msid-semantic:WMS * |
| m=audio 9 UDP/TLS/RTP/SAVPF 111 |
| c=IN IP4 0.0.0.0 |
| a=rtcp:9 IN IP4 0.0.0.0 |
| a=ice-ufrag:someufrag |
| a=ice-pwd:somelongpwdwithenoughrandomness |
| 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 |
| a=setup:actpass |
| a=rtcp-mux |
| a=mid:mid1 |
| a=recvonly |
| a=rtpmap:111 opus/48000/2 |
| a=msid:stream1 track1 |
| a=ssrc:1001 cname:some |
| `; |
| |
| pc.ontrack = t.unreached_func('ontrack event should not fire for track with recvonly direction'); |
| |
| pc.setRemoteDescription({ type: 'offer', sdp }) |
| .catch(t.step_func(err => { |
| assert_unreached('Error ' + err.name + ': ' + err.message); |
| })) |
| .then(t.step_func(() => { |
| t.step_timeout(t.step_func_done(), 100); |
| })); |
| }, 'setRemoteDescription() with m= line of recvonly direction should not trigger track event'); |
| |
| async_test(t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| |
| t.add_cleanup(() => pc2.close()); |
| |
| pc2.ontrack = t.step_func(trackEvent => { |
| const { track } = trackEvent; |
| |
| assert_equals(track.kind, 'audio', |
| 'Expect track.kind to be audio'); |
| |
| validateTrackEvent(trackEvent); |
| |
| t.done(); |
| }); |
| |
| return getTrackFromUserMedia('audio') |
| .then(([track, mediaStream]) => { |
| pc1.addTrack(track, mediaStream); |
| |
| return pc1.createOffer() |
| .then(offer => pc2.setRemoteDescription(offer)); |
| }) |
| .catch(t.step_func(err => { |
| assert_unreached('Error ' + err.name + ': ' + err.message); |
| })); |
| }, 'addTrack() should cause remote connection to fire ontrack when setRemoteDescription()'); |
| |
| async_test(t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| |
| t.add_cleanup(() => pc2.close()); |
| |
| pc2.ontrack = t.step_func(trackEvent => { |
| const { track } = trackEvent; |
| |
| assert_equals(track.kind, 'video', |
| 'Expect track.kind to be video'); |
| |
| validateTrackEvent(trackEvent); |
| |
| t.done(); |
| }); |
| |
| pc1.addTransceiver('video'); |
| |
| return pc1.createOffer() |
| .then(offer => pc2.setRemoteDescription(offer)) |
| .catch(t.step_func(err => { |
| assert_unreached('Error ' + err.name + ': ' + err.message); |
| })); |
| }, `addTransceiver('video') should cause remote connection to fire ontrack when setRemoteDescription()`); |
| |
| async_test(t => { |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| |
| t.add_cleanup(() => pc2.close()); |
| |
| pc2.ontrack = t.step_func(trackEvent => { |
| const { track } = trackEvent; |
| |
| assert_equals(track.kind, 'video', |
| 'Expect track.kind to be video'); |
| |
| validateTrackEvent(trackEvent); |
| |
| t.done(); |
| }); |
| |
| pc1.addTransceiver('audio', { direction: 'inactive' }); |
| pc2.ontrack = t.unreached_func('ontrack event should not fire for track with inactive direction'); |
| |
| return pc1.createOffer() |
| .then(offer => pc2.setRemoteDescription(offer)) |
| .catch(t.step_func(err => { |
| assert_unreached('Error ' + err.name + ': ' + err.message); |
| })) |
| .then(t.step_func(() => { |
| t.step_timeout(t.step_func_done(), 100); |
| })); |
| }, `addTransceiver() with inactive direction should not cause remote connection to fire ontrack when setRemoteDescription()`); |
| |
| </script> |