| <!doctype html> |
| <meta charset=utf-8> |
| <title>RTCPeerConnection.prototype.addTransceiver</title> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script> |
| 'use strict'; |
| |
| // Test is based on the following editor draft: |
| // https://rawgit.com/w3c/webrtc-pc/cc8d80f455b86c8041d63bceb8b457f45c72aa89/webrtc.html |
| |
| /* |
| 5.1. RTCPeerConnection Interface Extensions |
| |
| partial interface RTCPeerConnection { |
| sequence<RTCRtpSender> getSenders(); |
| sequence<RTCRtpReceiver> getReceivers(); |
| sequence<RTCRtpTransceiver> getTransceivers(); |
| RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind, |
| optional RTCRtpTransceiverInit init); |
| ... |
| }; |
| |
| dictionary RTCRtpTransceiverInit { |
| RTCRtpTransceiverDirection direction = "sendrecv"; |
| sequence<MediaStream> streams; |
| sequence<RTCRtpEncodingParameters> sendEncodings; |
| }; |
| |
| enum RTCRtpTransceiverDirection { |
| "sendrecv", |
| "sendonly", |
| "recvonly", |
| "inactive" |
| }; |
| |
| 5.2. RTCRtpSender Interface |
| |
| interface RTCRtpSender { |
| readonly attribute MediaStreamTrack? track; |
| ... |
| }; |
| |
| 5.3. RTCRtpReceiver Interface |
| |
| interface RTCRtpReceiver { |
| readonly attribute MediaStreamTrack track; |
| ... |
| }; |
| |
| 5.4. RTCRtpTransceiver Interface |
| |
| interface RTCRtpTransceiver { |
| readonly attribute DOMString? mid; |
| [SameObject] |
| readonly attribute RTCRtpSender sender; |
| [SameObject] |
| readonly attribute RTCRtpReceiver receiver; |
| readonly attribute boolean stopped; |
| readonly attribute RTCRtpTransceiverDirection direction; |
| readonly attribute RTCRtpTransceiverDirection? currentDirection; |
| ... |
| }; |
| |
| Note |
| While addTrack checks if the MediaStreamTrack given as an argument is |
| already being sent to avoid sending the same MediaStreamTrack twice, |
| the other ways do not, allowing the same MediaStreamTrack to be sent |
| several times simultaneously. |
| */ |
| |
| /* |
| 5.1. addTransceiver |
| 3. If the first argument is a string, let it be kind and run the following steps: |
| 1. If kind is not a legal MediaStreamTrack kind, throw a TypeError. |
| */ |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| assert_idl_attribute(pc, 'addTransceiver'); |
| assert_throws(new TypeError(), () => pc.addTransceiver('invalid')); |
| }, 'addTransceiver() with string argument as invalid kind should throw TypeError'); |
| |
| /* |
| 5.1. addTransceiver |
| The initial value of mid is null. |
| |
| 3. If the dictionary argument is present, let direction be the value of the |
| direction member. Otherwise let direction be sendrecv. |
| 4. If the first argument is a string, let it be kind and run the following steps: |
| 2. Let track be null. |
| 8. Create an RTCRtpSender with track, streams and sendEncodings and let |
| sender be the result. |
| 9. Create an RTCRtpReceiver with kind and let receiver be the result. |
| 10. Create an RTCRtpTransceiver with sender, receiver and direction, and let |
| transceiver be the result. |
| 11. Add transceiver to connection's set of transceivers. |
| |
| 5.2. RTCRtpSender Interface |
| Create an RTCRtpSender |
| 2. Set sender.track to track. |
| |
| 5.3. RTCRtpReceiver Interface |
| Create an RTCRtpReceiver |
| 2. Let track be a new MediaStreamTrack object [GETUSERMEDIA]. The source of |
| track is a remote source provided by receiver. |
| 3. Initialize track.kind to kind. |
| 5. Initialize track.label to the result of concatenating the string "remote " |
| with kind. |
| 6. Initialize track.readyState to live. |
| 7. Initialize track.muted to true. |
| 8. Set receiver.track to track. |
| |
| 5.4. RTCRtpTransceiver Interface |
| Create an RTCRtpTransceiver |
| 2. Set transceiver.sender to sender. |
| 3. Set transceiver.receiver to receiver. |
| 4. Let transceiver have a [[Direction]] internal slot, initialized to direction. |
| 5. Let transceiver have a [[CurrentDirection]] internal slot, initialized |
| to null. |
| 6. Set transceiver.stopped to false. |
| */ |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| assert_idl_attribute(pc, 'addTransceiver'); |
| |
| const transceiver = pc.addTransceiver('audio'); |
| assert_true(transceiver instanceof RTCRtpTransceiver, |
| 'Expect transceiver to be instance of RTCRtpTransceiver'); |
| |
| assert_equals(transceiver.mid, null); |
| assert_equals(transceiver.stopped, false); |
| assert_equals(transceiver.direction, 'sendrecv'); |
| assert_equals(transceiver.currentDirection, null); |
| |
| assert_array_equals([transceiver], pc.getTransceivers(), |
| `Expect added transceiver to be the only element in connection's list of transceivers`); |
| |
| const sender = transceiver.sender; |
| |
| assert_true(sender instanceof RTCRtpSender, |
| 'Expect sender to be instance of RTCRtpSender'); |
| |
| assert_equals(sender.track, null); |
| |
| assert_array_equals([sender], pc.getSenders(), |
| `Expect added sender to be the only element in connection's list of senders`); |
| |
| const receiver = transceiver.receiver; |
| assert_true(receiver instanceof RTCRtpReceiver, |
| 'Expect receiver to be instance of RTCRtpReceiver'); |
| |
| const track = receiver.track; |
| assert_true(track instanceof MediaStreamTrack, |
| 'Expect receiver.track to be instance of MediaStreamTrack'); |
| |
| assert_equals(track.kind, 'audio'); |
| assert_equals(track.readyState, 'live'); |
| assert_equals(track.muted, true); |
| |
| assert_array_equals([receiver], pc.getReceivers(), |
| `Expect added receiver to be the only element in connection's list of receivers`); |
| |
| }, `addTransceiver('audio') should return an audio transceiver`); |
| |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| assert_idl_attribute(pc, 'addTransceiver'); |
| |
| const transceiver = pc.addTransceiver('video'); |
| assert_true(transceiver instanceof RTCRtpTransceiver, |
| 'Expect transceiver to be instance of RTCRtpTransceiver'); |
| |
| assert_equals(transceiver.mid, null); |
| assert_equals(transceiver.stopped, false); |
| assert_equals(transceiver.direction, 'sendrecv'); |
| |
| assert_array_equals([transceiver], pc.getTransceivers(), |
| `Expect added transceiver to be the only element in connection's list of transceivers`); |
| |
| const sender = transceiver.sender; |
| |
| assert_true(sender instanceof RTCRtpSender, |
| 'Expect sender to be instance of RTCRtpSender'); |
| |
| assert_equals(sender.track, null); |
| |
| assert_array_equals([sender], pc.getSenders(), |
| `Expect added sender to be the only element in connection's list of senders`); |
| |
| const receiver = transceiver.receiver; |
| assert_true(receiver instanceof RTCRtpReceiver, |
| 'Expect receiver to be instance of RTCRtpReceiver'); |
| |
| const track = receiver.track; |
| assert_true(track instanceof MediaStreamTrack, |
| 'Expect receiver.track to be instance of MediaStreamTrack'); |
| |
| assert_equals(track.kind, 'video'); |
| assert_equals(track.readyState, 'live'); |
| assert_equals(track.muted, true); |
| |
| assert_array_equals([receiver], pc.getReceivers(), |
| `Expect added receiver to be the only element in connection's list of receivers`); |
| |
| }, `addTransceiver('video') should return a video transceiver`); |
| |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| const transceiver = pc.addTransceiver('audio', { direction: 'sendonly' }); |
| assert_equals(transceiver.direction, 'sendonly'); |
| }, `addTransceiver() with direction sendonly should have result transceiver.direction be the same`); |
| |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| const transceiver = pc.addTransceiver('audio', { direction: 'inactive' }); |
| assert_equals(transceiver.direction, 'inactive'); |
| }, `addTransceiver() with direction inactive should have result transceiver.direction be the same`); |
| |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| assert_idl_attribute(pc, 'addTransceiver'); |
| assert_throws(new TypeError(), () => |
| pc.addTransceiver('audio', { direction: 'invalid' })); |
| }, `addTransceiver() with invalid direction should throw TypeError`); |
| |
| /* |
| 5.1. addTransceiver |
| 5. If the first argument is a MediaStreamTrack , let it be track and let |
| kind be track.kind. |
| */ |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| const stream = await navigator.mediaDevices.getUserMedia({audio: true}); |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); |
| const [track] = stream.getTracks(); |
| const transceiver = pc.addTransceiver(track); |
| const { sender, receiver } = transceiver; |
| |
| assert_true(sender instanceof RTCRtpSender, |
| 'Expect sender to be instance of RTCRtpSender'); |
| |
| assert_true(receiver instanceof RTCRtpReceiver, |
| 'Expect receiver to be instance of RTCRtpReceiver'); |
| |
| assert_equals(sender.track, track, |
| 'Expect sender.track should be the track that is added'); |
| |
| const receiverTrack = receiver.track; |
| assert_true(receiverTrack instanceof MediaStreamTrack, |
| 'Expect receiver.track to be instance of MediaStreamTrack'); |
| |
| assert_equals(receiverTrack.kind, 'audio', |
| `receiver.track should have the same kind as added track's kind`); |
| |
| assert_equals(receiverTrack.readyState, 'live'); |
| assert_equals(receiverTrack.muted, true); |
| |
| assert_array_equals([transceiver], pc.getTransceivers(), |
| `Expect added transceiver to be the only element in connection's list of transceivers`); |
| |
| assert_array_equals([sender], pc.getSenders(), |
| `Expect added sender to be the only element in connection's list of senders`); |
| |
| assert_array_equals([receiver], pc.getReceivers(), |
| `Expect added receiver to be the only element in connection's list of receivers`); |
| |
| }, 'addTransceiver(track) should have result with sender.track be given track'); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| const stream = await navigator.mediaDevices.getUserMedia({audio: true}); |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); |
| const [track] = stream.getTracks(); |
| const transceiver1 = pc.addTransceiver(track); |
| const transceiver2 = pc.addTransceiver(track); |
| |
| assert_not_equals(transceiver1, transceiver2); |
| |
| const sender1 = transceiver1.sender; |
| const sender2 = transceiver2.sender; |
| |
| assert_not_equals(sender1, sender2); |
| assert_equals(transceiver1.sender.track, track); |
| assert_equals(transceiver2.sender.track, track); |
| |
| const transceivers = pc.getTransceivers(); |
| assert_equals(transceivers.length, 2); |
| assert_true(transceivers.includes(transceiver1)); |
| assert_true(transceivers.includes(transceiver2)); |
| |
| const senders = pc.getSenders(); |
| assert_equals(senders.length, 2); |
| assert_true(senders.includes(sender1)); |
| assert_true(senders.includes(sender2)); |
| |
| }, 'addTransceiver(track) multiple times should create multiple transceivers'); |
| |
| /* |
| 5.1. addTransceiver |
| 6. Verify that each rid value in sendEncodings is composed only of |
| case-sensitive alphanumeric characters (a-z, A-Z, 0-9) up to a maximum |
| of 16 characters. If one of the RIDs does not meet these requirements, |
| throw a TypeError. |
| */ |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| assert_idl_attribute(pc, 'addTransceiver'); |
| |
| assert_throws(new TypeError(), () => |
| pc.addTransceiver('audio', { |
| sendEncodings: [{ |
| rid: '@Invalid!' |
| }] |
| })); |
| }, 'addTransceiver() with rid containing invalid non-alphanumeric characters should throw TypeError'); |
| |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| assert_idl_attribute(pc, 'addTransceiver'); |
| |
| assert_throws(new TypeError(), () => |
| pc.addTransceiver('audio', { |
| sendEncodings: [{ |
| rid: 'a'.repeat(17) |
| }] |
| })); |
| }, 'addTransceiver() with rid longer than 16 characters should throw TypeError'); |
| |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| pc.addTransceiver('audio', { |
| sendEncodings: [{ |
| rid: 'foo' |
| }] |
| }); |
| }, `addTransceiver() with valid rid value should succeed`); |
| |
| test(t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| |
| pc.addTransceiver('audio', { |
| sendEncodings: [{ |
| dtx: 'enabled', |
| active: false, |
| priority: 'low', |
| ptime: 5, |
| maxBitrate: 8, |
| maxFramerate: 25, |
| rid: 'foo' |
| }] |
| }); |
| }, `addTransceiver() with valid sendEncodings should succeed`); |
| |
| /* |
| TODO |
| 5.1. addTransceiver |
| - Adding a transceiver will cause future calls to createOffer to add a media |
| description for the corresponding transceiver, as defined in [JSEP] |
| (section 5.2.2.). |
| |
| - Setting a new RTCSessionDescription may change mid to a non-null value, |
| as defined in [JSEP] (section 5.5. and section 5.6.). |
| |
| 1. If the dictionary argument is present, and it has a streams member, let |
| streams be that list of MediaStream objects. |
| |
| 5.2. RTCRtpSender Interface |
| Create an RTCRtpSender |
| 3. Let sender have an [[associated MediaStreams]] internal slot, representing |
| a list of MediaStream objects that the MediaStreamTrack object of this |
| sender is associated with. |
| |
| 4. Set sender's [[associated MediaStreams]] slot to streams. |
| |
| 5. Let sender have a [[send encodings]] internal slot, representing a list |
| of RTCRtpEncodingParameters dictionaries. |
| |
| 6. If sendEncodings is given as input to this algorithm, and is non-empty, |
| set the [[send encodings]] slot to sendEncodings. Otherwise, set it to a |
| list containing a single RTCRtpEncodingParameters with active set to true. |
| |
| 5.3. RTCRtpReceiver Interface |
| Create an RTCRtpReceiver |
| 4. If an id string, id, was given as input to this algorithm, initialize |
| track.id to id. (Otherwise the value generated when track was created |
| will be used.) |
| |
| Tested in RTCPeerConnection-onnegotiationneeded.html |
| 5.1. addTransceiver |
| 12. Update the negotiation-needed flag for connection. |
| |
| Out of Scope |
| 5.1. addTransceiver |
| 8. If sendEncodings is set, then subsequent calls to createOffer will be |
| configured to send multiple RTP encodings as defined in [JSEP] |
| (section 5.2.2. and section 5.2.1.). |
| |
| When setRemoteDescription is called with a corresponding remote |
| description that is able to receive multiple RTP encodings as defined |
| in [JSEP] (section 3.7.), the RTCRtpSender may send multiple RTP |
| encodings and the parameters retrieved via the transceiver's |
| sender.getParameters() will reflect the encodings negotiated. |
| |
| 9. This specification does not define how to configure createOffer to |
| receive multiple RTP encodings. However when setRemoteDescription is |
| called with a corresponding remote description that is able to send |
| multiple RTP encodings as defined in [JSEP], the RTCRtpReceiver may |
| receive multiple RTP encodings and the parameters retrieved via the |
| transceiver's receiver.getParameters() will reflect the encodings |
| negotiated. |
| |
| Coverage Report |
| Tested Not-Tested Non-Testable Total |
| addTransceiver 14 1 3 18 |
| Create Sender 3 4 0 7 |
| Create Receiver 8 1 0 9 |
| Create Transceiver 7 0 0 7 |
| |
| Total 32 6 3 41 |
| */ |
| </script> |