| <!doctype html> |
| <meta charset=utf-8> |
| <meta name="timeout" content="long"> |
| <title>RTCRtpSender.prototype.replaceTrack</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 |
| |
| /* |
| 5.2. RTCRtpSender Interface |
| interface RTCRtpSender { |
| readonly attribute MediaStreamTrack? track; |
| Promise<void> replaceTrack(MediaStreamTrack? withTrack); |
| ... |
| }; |
| |
| replaceTrack |
| Attempts to replace the track being sent with another track provided |
| (or with a null track), without renegotiation. |
| */ |
| |
| /* |
| 5.2. replaceTrack |
| 4. If connection's [[isClosed]] slot is true, return a promise rejected |
| with a newly created InvalidStateError and abort these steps. |
| */ |
| 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 transceiver = pc.addTransceiver('audio'); |
| const { sender } = transceiver; |
| pc.close(); |
| |
| return promise_rejects_dom(t, 'InvalidStateError', |
| sender.replaceTrack(track)); |
| }, 'Calling replaceTrack on closed connection should reject with InvalidStateError'); |
| |
| /* |
| 5.2. replaceTrack |
| 7. If withTrack is non-null and withTrack.kind differs from the |
| transceiver kind of transceiver, return a promise rejected with a |
| newly created TypeError. |
| */ |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const stream = await getNoiseStream({video: true}); |
| t.add_cleanup(() => stream.getTracks().forEach(track => track.stop())); |
| const [track] = stream.getTracks(); |
| |
| const transceiver = pc.addTransceiver('audio'); |
| const { sender } = transceiver; |
| |
| return promise_rejects_js(t, TypeError, |
| sender.replaceTrack(track)); |
| }, 'Calling replaceTrack with track of different kind should reject with TypeError'); |
| |
| /* |
| 5.2. replaceTrack |
| 5. If transceiver.stopped is true, return a promise rejected with a newly |
| created 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(); |
| |
| const transceiver = pc.addTransceiver('audio'); |
| const { sender } = transceiver; |
| transceiver.stop(); |
| |
| return promise_rejects_dom(t, 'InvalidStateError', |
| sender.replaceTrack(track)); |
| }, 'Calling replaceTrack on stopped sender should reject with InvalidStateError'); |
| |
| /* |
| 5.2. replaceTrack |
| 8. If transceiver is not yet associated with a media description [JSEP] |
| (section 3.4.1.), then set sender's track attribute to withTrack, and |
| return a promise resolved with undefined. |
| */ |
| 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 transceiver = pc.addTransceiver('audio'); |
| const { sender } = transceiver; |
| assert_equals(sender.track, null); |
| |
| return sender.replaceTrack(track) |
| .then(() => { |
| assert_equals(sender.track, track); |
| }); |
| }, 'Calling replaceTrack on sender with null track and not set to session description should resolve with sender.track set to given track'); |
| |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const stream1 = await getNoiseStream({audio: true}); |
| t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); |
| const [track1] = stream1.getTracks(); |
| const stream2 = await getNoiseStream({audio: true}); |
| t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); |
| const [track2] = stream2.getTracks(); |
| |
| const transceiver = pc.addTransceiver(track1); |
| const { sender } = transceiver; |
| |
| assert_equals(sender.track, track1); |
| |
| return sender.replaceTrack(track2) |
| .then(() => { |
| assert_equals(sender.track, track2); |
| }); |
| }, 'Calling replaceTrack on sender not set to session description should resolve with sender.track set to given track'); |
| |
| 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 transceiver = pc.addTransceiver(track); |
| const { sender } = transceiver; |
| |
| assert_equals(sender.track, track); |
| |
| return sender.replaceTrack(null) |
| .then(() => { |
| assert_equals(sender.track, null); |
| }); |
| }, 'Calling replaceTrack(null) on sender not set to session description should resolve with sender.track set to null'); |
| |
| /* |
| 5.2. replaceTrack |
| 10. Run the following steps in parallel: |
| 1. Determine if negotiation is needed to transmit withTrack in place |
| of the sender's existing track. |
| |
| Negotiation is not needed if withTrack is null. |
| |
| 3. Queue a task that runs the following steps: |
| 2. Set sender's track attribute to withTrack. |
| */ |
| 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 transceiver = pc.addTransceiver(track); |
| const { sender } = transceiver; |
| |
| assert_equals(sender.track, track); |
| |
| return pc.createOffer() |
| .then(offer => pc.setLocalDescription(offer)) |
| .then(() => sender.replaceTrack(null)) |
| .then(() => { |
| assert_equals(sender.track, null); |
| }); |
| }, 'Calling replaceTrack(null) on sender set to session description should resolve with sender.track set to null'); |
| |
| /* |
| 5.2. replaceTrack |
| 10. Run the following steps in parallel: |
| 1. Determine if negotiation is needed to transmit withTrack in place |
| of the sender's existing track. |
| |
| Negotiation is not needed if the sender's existing track is |
| ended (which appears as though the track was muted). |
| |
| 3. Queue a task that runs the following steps: |
| 2. Set sender's track attribute to withTrack. |
| */ |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const stream1 = await getNoiseStream({audio: true}); |
| t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); |
| const [track1] = stream1.getTracks(); |
| const stream2 = await getNoiseStream({audio: true}); |
| t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); |
| const [track2] = stream1.getTracks(); |
| |
| const transceiver = pc.addTransceiver(track1); |
| const { sender } = transceiver; |
| assert_equals(sender.track, track1); |
| |
| track1.stop(); |
| |
| return pc.createOffer() |
| .then(offer => pc.setLocalDescription(offer)) |
| .then(() => sender.replaceTrack(track2)) |
| .then(() => { |
| assert_equals(sender.track, track2); |
| }); |
| }, 'Calling replaceTrack on sender with stopped track and and set to session description should resolve with sender.track set to given track'); |
| |
| /* |
| 5.2. replaceTrack |
| 10. Run the following steps in parallel: |
| 1. Determine if negotiation is needed to transmit withTrack in place |
| of the sender's existing track. |
| |
| (tracks generated with default parameters *should* be similar |
| enough to not require re-negotiation) |
| |
| 3. Queue a task that runs the following steps: |
| 2. Set sender's track attribute to withTrack. |
| */ |
| promise_test(async t => { |
| const pc = new RTCPeerConnection(); |
| t.add_cleanup(() => pc.close()); |
| const stream1 = await getNoiseStream({audio: true}); |
| t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); |
| const [track1] = stream1.getTracks(); |
| const stream2 = await getNoiseStream({audio: true}); |
| t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); |
| const [track2] = stream1.getTracks(); |
| |
| const transceiver = pc.addTransceiver(track1); |
| const { sender } = transceiver; |
| assert_equals(sender.track, track1); |
| |
| return pc.createOffer() |
| .then(offer => pc.setLocalDescription(offer)) |
| .then(() => sender.replaceTrack(track2)) |
| .then(() => { |
| assert_equals(sender.track, track2); |
| }); |
| }, 'Calling replaceTrack on sender with similar track and and set to session description should resolve with sender.track set to new track'); |
| |
| /* |
| TODO |
| 5.2. replaceTrack |
| To avoid track identifiers changing on the remote receiving end when |
| a track is replaced, the sender must retain the original track |
| identifier and stream associations and use these in subsequent |
| negotiations. |
| |
| Non-Testable |
| 5.2. replaceTrack |
| 10. Run the following steps in parallel: |
| 1. Determine if negotiation is needed to transmit withTrack in place |
| of the sender's existing track. |
| |
| Ignore which MediaStream the track resides in and the id attribute |
| of the track in this determination. |
| |
| If negotiation is needed, then reject p with a newly created |
| InvalidModificationError and abort these steps. |
| |
| 2. If withTrack is null, have the sender stop sending, without |
| negotiating. Otherwise, have the sender switch seamlessly to |
| transmitting withTrack instead of the sender's existing track, |
| without negotiating. |
| 3. Queue a task that runs the following steps: |
| 1. If connection's [[isClosed]] slot is true, abort these steps. |
| */ |
| |
| promise_test(async t => { |
| const v = document.createElement('video'); |
| v.autoplay = true; |
| const pc1 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc1.close()); |
| const pc2 = new RTCPeerConnection(); |
| t.add_cleanup(() => pc2.close()); |
| const stream1 = await getNoiseStream({video: {signal: 20}}); |
| t.add_cleanup(() => stream1.getTracks().forEach(track => track.stop())); |
| const [track1] = stream1.getTracks(); |
| const stream2 = await getNoiseStream({video: {signal: 250}}); |
| t.add_cleanup(() => stream2.getTracks().forEach(track => track.stop())); |
| const [track2] = stream2.getTracks(); |
| const sender = pc1.addTrack(track1); |
| pc2.ontrack = (e) => { |
| v.srcObject = new MediaStream([e.track]); |
| }; |
| const metadataToBeLoaded = new Promise((resolve) => { |
| v.addEventListener('loadedmetadata', () => { |
| resolve(); |
| }); |
| }); |
| exchangeIceCandidates(pc1, pc2); |
| exchangeOfferAnswer(pc1, pc2); |
| await metadataToBeLoaded; |
| await detectSignal(t, v, 20); |
| await sender.replaceTrack(track2); |
| await detectSignal(t, v, 250); |
| }, 'ReplaceTrack transmits the new track not the old track'); |
| </script> |