| <!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'); | |
| 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(null); | |
| await sender.replaceTrack(track2); | |
| await detectSignal(t, v, 250); | |
| }, 'ReplaceTrack null -> new track transmits the new track'); | |
| </script> |