| <!doctype html> | |
| <meta charset=utf-8> | |
| <title>RTCPeerConnection.prototype.addTrack</title> | |
| <script src="/resources/testharness.js"></script> | |
| <script src="/resources/testharnessreport.js"></script> | |
| <script src=/resources/testdriver.js></script> | |
| <script src=/resources/testdriver-vendor.js></script> | |
| <script src='../mediacapture-streams/permission-helper.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: | |
| // getNoiseStream() | |
| /* | |
| 5.1. RTCPeerConnection Interface Extensions | |
| partial interface RTCPeerConnection { | |
| ... | |
| sequence<RTCRtpSender> getSenders(); | |
| sequence<RTCRtpReceiver> getReceivers(); | |
| sequence<RTCRtpTransceiver> getTransceivers(); | |
| RTCRtpSender addTrack(MediaStreamTrack track, | |
| MediaStream... streams); | |
| RTCRtpTransceiver addTransceiver((MediaStreamTrack or DOMString) trackOrKind, | |
| optional RTCRtpTransceiverInit init); | |
| }; | |
| 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. addTrack | |
| 4. If connection's [[isClosed]] slot is true, throw an InvalidStateError. | |
| */ | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const stream = await getNoiseStream({ audio: true }); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| pc.close(); | |
| assert_throws_dom('InvalidStateError', () => pc.addTrack(track, stream)) | |
| }, 'addTrack when pc is closed should throw InvalidStateError'); | |
| /* | |
| 5.1. addTrack | |
| 8. If sender is null, run the following steps: | |
| 1. Create an RTCRtpSender with track and streams and let sender be | |
| the result. | |
| 2. Create an RTCRtpReceiver with track.kind as kind and let receiver | |
| be the result. | |
| 3. Create an RTCRtpTransceiver with sender and receiver and let | |
| transceiver be the result. | |
| 4. Add transceiver to connection's set of transceivers. | |
| */ | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const stream = await getNoiseStream({ audio: true }); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| const sender = pc.addTrack(track); | |
| assert_true(sender instanceof RTCRtpSender, | |
| 'Expect sender to be instance of RTCRtpSender'); | |
| assert_equals(sender.track, track, | |
| `Expect sender's track to be the added track`); | |
| const transceivers = pc.getTransceivers(); | |
| assert_equals(transceivers.length, 1, | |
| 'Expect only one transceiver with sender added'); | |
| const [transceiver] = transceivers; | |
| assert_equals(transceiver.sender, sender); | |
| assert_array_equals([sender], pc.getSenders(), | |
| 'Expect only one sender with given track added'); | |
| const { receiver } = transceiver; | |
| assert_equals(receiver.track.kind, 'audio'); | |
| assert_array_equals([transceiver.receiver], pc.getReceivers(), | |
| 'Expect only one receiver associated with transceiver added'); | |
| }, 'addTrack with single track argument and no stream should succeed'); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const stream = await getNoiseStream({ audio: true }); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| const sender = pc.addTrack(track, stream); | |
| assert_true(sender instanceof RTCRtpSender, | |
| 'Expect sender to be instance of RTCRtpSender'); | |
| assert_equals(sender.track, track, | |
| `Expect sender's track to be the added track`); | |
| }, 'addTrack with single track argument and single stream should succeed'); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const stream = await getNoiseStream({ audio: true }); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| const stream2 = new MediaStream([track]); | |
| const sender = pc.addTrack(track, stream, stream2); | |
| assert_true(sender instanceof RTCRtpSender, | |
| 'Expect sender to be instance of RTCRtpSender'); | |
| assert_equals(sender.track, track, | |
| `Expect sender's track to be the added track`); | |
| }, 'addTrack with single track argument and multiple streams should succeed'); | |
| /* | |
| 5.1. addTrack | |
| 5. Let senders be the result of executing the CollectSenders algorithm. | |
| If an RTCRtpSender for track already exists in senders, throw an | |
| InvalidAccessError. | |
| */ | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const stream = await getNoiseStream({ audio: true }); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| pc.addTrack(track, stream); | |
| assert_throws_dom('InvalidAccessError', () => pc.addTrack(track, stream)); | |
| }, 'Adding the same track multiple times should throw InvalidAccessError'); | |
| /* | |
| 5.1. addTrack | |
| 6. The steps below describe how to determine if an existing sender can | |
| be reused. | |
| If any RTCRtpSender object in senders matches all the following | |
| criteria, let sender be that object, or null otherwise: | |
| - The sender's track is null. | |
| - The transceiver kind of the RTCRtpTransceiver, associated with | |
| the sender, matches track's kind. | |
| - The sender has never been used to send. More precisely, the | |
| RTCRtpTransceiver associated with the sender has never had a | |
| currentDirection of sendrecv or sendonly. | |
| 7. If sender is not null, run the following steps to use that sender: | |
| 1. Set sender.track to track. | |
| 3. Enable sending direction on the RTCRtpTransceiver associated | |
| with sender. | |
| */ | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const transceiver = pc.addTransceiver('audio', { direction: 'recvonly' }); | |
| assert_equals(transceiver.sender.track, null); | |
| assert_equals(transceiver.direction, 'recvonly'); | |
| await setMediaPermission("granted", ["microphone"]); | |
| const stream = await navigator.mediaDevices.getUserMedia({audio: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| const sender = pc.addTrack(track); | |
| assert_equals(sender, transceiver.sender); | |
| assert_equals(sender.track, track); | |
| assert_equals(transceiver.direction, 'sendrecv'); | |
| assert_array_equals([sender], pc.getSenders()); | |
| }, 'addTrack with existing sender with null track, same kind, and recvonly direction should reuse sender'); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const transceiver = pc.addTransceiver('audio'); | |
| assert_equals(transceiver.sender.track, null); | |
| assert_equals(transceiver.direction, 'sendrecv'); | |
| const stream = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| const sender = pc.addTrack(track); | |
| assert_equals(sender.track, track); | |
| assert_equals(sender, transceiver.sender); | |
| }, 'addTrack with existing sender that has not been used to send should reuse the sender'); | |
| promise_test(async t => { | |
| const caller = new RTCPeerConnection(); | |
| t.add_cleanup(() => caller.close()); | |
| const callee = new RTCPeerConnection(); | |
| t.add_cleanup(() => callee.close()); | |
| const stream = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| const transceiver = caller.addTransceiver(track); | |
| { | |
| const offer = await caller.createOffer(); | |
| await caller.setLocalDescription(offer); | |
| await callee.setRemoteDescription(offer); | |
| const answer = await callee.createAnswer(); | |
| await callee.setLocalDescription(answer); | |
| await caller.setRemoteDescription(answer); | |
| } | |
| assert_equals(transceiver.currentDirection, 'sendonly'); | |
| caller.removeTrack(transceiver.sender); | |
| { | |
| const offer = await caller.createOffer(); | |
| await caller.setLocalDescription(offer); | |
| await callee.setRemoteDescription(offer); | |
| const answer = await callee.createAnswer(); | |
| await callee.setLocalDescription(answer); | |
| await caller.setRemoteDescription(answer); | |
| } | |
| assert_equals(transceiver.direction, 'recvonly'); | |
| assert_equals(transceiver.currentDirection, 'inactive'); | |
| // |transceiver.sender| is currently not used for sending, but it should not | |
| // be reused because it has been used for sending before. | |
| const sender = caller.addTrack(track); | |
| assert_true(sender != null); | |
| assert_not_equals(sender, transceiver.sender); | |
| }, 'addTrack with existing sender that has been used to send should create new sender'); | |
| promise_test(async t => { | |
| const pc = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc.close()); | |
| const transceiver = pc.addTransceiver('video', { direction: 'recvonly' }); | |
| assert_equals(transceiver.sender.track, null); | |
| assert_equals(transceiver.direction, 'recvonly'); | |
| const stream = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| const sender = pc.addTrack(track); | |
| assert_equals(sender.track, track); | |
| assert_not_equals(sender, transceiver.sender); | |
| const senders = pc.getSenders(); | |
| assert_equals(senders.length, 2, | |
| 'Expect 2 senders added to connection'); | |
| assert_true(senders.includes(sender), | |
| 'Expect senders list to include sender'); | |
| assert_true(senders.includes(transceiver.sender), | |
| `Expect senders list to include first transceiver's sender`); | |
| }, 'addTrack with existing sender with null track, different kind, and recvonly direction should create new sender'); | |
| /* | |
| TODO | |
| 5.1. addTrack | |
| 3. Let streams be a list of MediaStream objects constructed from the | |
| method's remaining arguments, or an empty list if the method was | |
| called with a single argument. | |
| 6. The steps below describe how to determine if an existing sender can | |
| be reused. Doing so will cause future calls to createOffer and | |
| createAnswer to mark the corresponding media description as sendrecv | |
| or sendonly and add the MSID of the track added, as defined in [JSEP] | |
| (section 5.2.2. and section 5.3.2.). | |
| Non-Testable | |
| 5.1. addTrack | |
| 7. If sender is not null, run the following steps to use that sender: | |
| 2. Set sender's [[associated MediaStreams]] to streams. | |
| Tested in RTCPeerConnection-onnegotiationneeded.html: | |
| 5.1. addTrack | |
| 10. Update the negotiation-needed flag for connection. | |
| */ | |
| promise_test(async t => { | |
| const caller = new RTCPeerConnection(); | |
| t.add_cleanup(() => caller.close()); | |
| const callee = new RTCPeerConnection(); | |
| t.add_cleanup(() => callee.close()); | |
| const stream = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| const transceiver = caller.addTransceiver(track); | |
| // Note that this test doesn't process canididates. | |
| { | |
| const offer = await caller.createOffer(); | |
| await caller.setLocalDescription(offer); | |
| await callee.setRemoteDescription(offer); | |
| const answer = await callee.createAnswer(); | |
| await callee.setLocalDescription(answer); | |
| await caller.setRemoteDescription(answer); | |
| } | |
| assert_equals(transceiver.currentDirection, 'sendonly'); | |
| await waitForIceGatheringState(caller, ['complete']); | |
| await waitForIceGatheringState(callee, ['complete']); | |
| const second_stream = await getNoiseStream({audio: true}); | |
| t.add_cleanup(() => second_stream.getTracks().forEach(track => track.stop())); | |
| // There may be callee candidates in flight. It seems that waiting | |
| // for a createOffer() is enough time to let them complete processing. | |
| // TODO(https://crbug.com/webrtc/13095): Fix bug and remove. | |
| await caller.createOffer(); | |
| const [second_track] = second_stream.getTracks(); | |
| caller.onicecandidate = t.unreached_func( | |
| 'No caller candidates should be generated.'); | |
| callee.onicecandidate = t.unreached_func( | |
| 'No callee candidates should be generated.'); | |
| caller.addTrack(second_track); | |
| { | |
| const offer = await caller.createOffer(); | |
| await caller.setLocalDescription(offer); | |
| await callee.setRemoteDescription(offer); | |
| const answer = await callee.createAnswer(); | |
| await callee.setLocalDescription(answer); | |
| await caller.setRemoteDescription(answer); | |
| } | |
| // Check that we're bundled. | |
| const [first_transceiver, second_transceiver] = caller.getTransceivers(); | |
| assert_equals(first_transceiver.transport, second_transceiver.transport); | |
| }, 'Adding more tracks does not generate more candidates if bundled'); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| const stream = await getNoiseStream({ audio: true }); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| pc1.addTrack(track); | |
| const offer = await pc1.createOffer(); | |
| // We do not await here; we want to ensure that the transceiver this creates | |
| // is untouched by addTrack, and that addTrack creates _another_ transceiver | |
| const srdPromise = pc2.setRemoteDescription(offer); | |
| const sender = pc2.addTrack(track); | |
| await srdPromise; | |
| assert_equals(pc2.getTransceivers().length, 1, "Should have 1 transceiver"); | |
| assert_equals(pc2.getTransceivers()[0].sender, sender, "The transceiver should be the one added by addTrack"); | |
| }, 'Calling addTrack while sRD(offer) is pending should allow the new remote transceiver to be the same one that addTrack creates'); | |
| promise_test(async t => { | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1.addTransceiver('video'); | |
| const stream = await getNoiseStream({ audio: true }); | |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); | |
| const [track] = stream.getTracks(); | |
| const offer = await pc1.createOffer(); | |
| const srdPromise = pc2.setRemoteDescription(offer); | |
| assert_equals(pc2.getTransceivers().length, 0); | |
| pc2.addTrack(track); | |
| assert_equals(pc2.getTransceivers().length, 1); | |
| const transceiver0 = pc2.getTransceivers()[0]; | |
| assert_equals(transceiver0.mid, null); | |
| await srdPromise; | |
| assert_equals(pc2.getTransceivers().length, 2); | |
| const transceiver1 = pc2.getTransceivers()[1]; | |
| assert_equals(transceiver0.mid, null); | |
| assert_not_equals(transceiver1.mid, null); | |
| }, 'When addTrack is called while sRD is in progress, and both addTrack and sRD add a transceiver of different media types, the addTrack transceiver should come first, and then the sRD transceiver.'); | |
| </script> |