| <!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(); | |
| // 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(); | |
| 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(); | |
| const pc2 = new RTCPeerConnection(); | |
| 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(); | |
| const pc2 = new RTCPeerConnection(); | |
| 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(); | |
| const pc2 = new RTCPeerConnection(); | |
| 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> |