| <!doctype html> | |
| <meta charset=utf-8> | |
| <script src="/resources/testharness.js"></script> | |
| <script src="/resources/testharnessreport.js"></script> | |
| <script> | |
| 'use strict'; | |
| ['audio', 'video'].forEach((kind) => { | |
| // Make sure "ontrack" fires if a prevuously rolled back track is added back. | |
| promise_test(async t => { | |
| const constraints = {}; | |
| constraints[kind] = true; | |
| const stream = await navigator.mediaDevices.getUserMedia(constraints); | |
| const [track] = stream.getTracks(); | |
| t.add_cleanup(() => track.stop()); | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1.addTrack(track, stream); | |
| pc2.addTrack(track, stream); | |
| const [pc1Transceiver] = pc1.getTransceivers(); | |
| const [pc2Transceiver] = pc2.getTransceivers(); | |
| let remoteStreamViaOnTrackPromise = getRemoteStreamViaOnTrackPromise(pc2); | |
| // Apply remote offer, but don't complete the entire exchange. | |
| await pc1.setLocalDescription(); | |
| await pc2.setRemoteDescription(pc1.localDescription); | |
| // The addTrack-transceiver gets associated, no need for a second | |
| // transceiver. | |
| assert_equals(pc2.getTransceivers().length, 1); | |
| const remoteStream = await remoteStreamViaOnTrackPromise; | |
| assert_equals(remoteStream.id, stream.id); | |
| const onRemoveTrackPromise = new Promise(r => { | |
| remoteStream.onremovetrack = () => { r(); }; | |
| }); | |
| // Cause track removal due to rollback. | |
| await pc2.setRemoteDescription({type:'rollback'}); | |
| // The track was removed. | |
| await onRemoveTrackPromise; | |
| // Sanity check that ontrack still fires if we add it back again by applying | |
| // the same remote offer. | |
| remoteStreamViaOnTrackPromise = getRemoteStreamViaOnTrackPromise(pc2); | |
| await pc2.setRemoteDescription(pc1.localDescription); | |
| const revivedRemoteStream = await remoteStreamViaOnTrackPromise; | |
| // This test only expects IDs to be the same. The same stream object should | |
| // also be used, but this should be covered by separate tests. | |
| // TODO(https://crbug.com/1321738): Add MediaStream identity tests. | |
| assert_equals(remoteStream.id, revivedRemoteStream.id); | |
| // No cheating, the same transciever should be used as before. | |
| assert_equals(pc2.getTransceivers().length, 1); | |
| }, `[${kind}] Track with stream: removal due to disassociation in rollback and then add it back again`); | |
| // This is the same test as above, but this time without any remote streams. | |
| // This test could fail if [[FiredDirection]] was not reset in a rollback but | |
| // the above version of the test might still pass due to the track being | |
| // re-added to its stream. | |
| promise_test(async t => { | |
| const constraints = {}; | |
| constraints[kind] = true; | |
| const stream = await navigator.mediaDevices.getUserMedia(constraints); | |
| const [track] = stream.getTracks(); | |
| t.add_cleanup(() => track.stop()); | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1.addTrack(track); | |
| pc2.addTrack(track); | |
| const [pc1Transceiver] = pc1.getTransceivers(); | |
| const [pc2Transceiver] = pc2.getTransceivers(); | |
| let remoteTrackPromise = getTrackViaOnTrackPromise(pc2); | |
| // Apply remote offer, but don't complete the entire exchange. | |
| await pc1.setLocalDescription(); | |
| await pc2.setRemoteDescription(pc1.localDescription); | |
| // The addTrack-transceiver gets associated, no need for a second | |
| // transceiver. | |
| assert_equals(pc2.getTransceivers().length, 1); | |
| const remoteTrack = await remoteTrackPromise; | |
| assert_not_equals(remoteTrack, null); | |
| // Cause track removal due to rollback. | |
| await pc2.setRemoteDescription({type:'rollback'}); | |
| // There's nothing equivalent to stream.onremovetrack when you don't have a | |
| // stream, but the track should become muted (if it isn't already). | |
| if (!remoteTrack.muted) { | |
| await new Promise(r => remoteTrack.onmute = () => { r(); }); | |
| } | |
| assert_equals(remoteTrack.muted, true); | |
| // Sanity check that ontrack still fires if we add it back again by applying | |
| // the same remote offer. | |
| remoteTrackPromise = getTrackViaOnTrackPromise(pc2); | |
| await pc2.setRemoteDescription(pc1.localDescription); | |
| const revivedRemoteTrack = await remoteTrackPromise; | |
| // We can be sure the same track is used, because the same transceiver is | |
| // used (and transciever.receiver.track has same lifetime as transceiver). | |
| assert_equals(pc2.getTransceivers().length, 1); | |
| assert_equals(remoteTrack, revivedRemoteTrack); | |
| }, `[${kind}] Track without stream: removal due to disassociation in rollback and then add it back`); | |
| // Make sure "ontrack" can fire in a rollback (undo making it inactive). | |
| promise_test(async t => { | |
| const constraints = {}; | |
| constraints[kind] = true; | |
| const stream = await navigator.mediaDevices.getUserMedia(constraints); | |
| const [track] = stream.getTracks(); | |
| t.add_cleanup(() => track.stop()); | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1.addTrack(track, stream); | |
| const [pc1Transceiver] = pc1.getTransceivers(); | |
| let remoteStreamViaOnTrackPromise = getRemoteStreamViaOnTrackPromise(pc2); | |
| // Complete O/A exchange such that the transceiver gets associated. | |
| await pc1.setLocalDescription(); | |
| await pc2.setRemoteDescription(pc1.localDescription); | |
| await pc2.setLocalDescription(); | |
| await pc1.setRemoteDescription(pc2.localDescription); | |
| const [pc2Transceiver] = pc2.getTransceivers(); | |
| assert_equals(pc2Transceiver.direction, 'recvonly'); | |
| assert_equals(pc2Transceiver.currentDirection, 'recvonly'); | |
| const remoteStream = await remoteStreamViaOnTrackPromise; | |
| assert_equals(remoteStream.id, stream.id); | |
| const onRemoveTrackPromise = new Promise(r => { | |
| remoteStream.onremovetrack = () => { r(); }; | |
| }); | |
| // Cause track removal. | |
| pc1Transceiver.direction = 'inactive'; | |
| await pc1.setLocalDescription(); | |
| await pc2.setRemoteDescription(pc1.localDescription); | |
| // The track was removed. | |
| await onRemoveTrackPromise; | |
| // Rolling back the offer revives the track, causing ontrack to fire again. | |
| remoteStreamViaOnTrackPromise = getRemoteStreamViaOnTrackPromise(pc2); | |
| await pc2.setRemoteDescription({type:'rollback'}); | |
| const revivedRemoteStream = await remoteStreamViaOnTrackPromise; | |
| // This test only expects IDs to be the same. The same stream object should | |
| // also be used, but this should be covered by separate tests. | |
| // TODO(https://crbug.com/1321738): Add MediaStream identity tests. | |
| assert_equals(remoteStream.id, revivedRemoteStream.id); | |
| }, `[${kind}] Track with stream: removal due to direction changing and then add back using rollback`); | |
| // Same test as above but without remote streams. | |
| promise_test(async t => { | |
| const constraints = {}; | |
| constraints[kind] = true; | |
| const stream = await navigator.mediaDevices.getUserMedia(constraints); | |
| const [track] = stream.getTracks(); | |
| t.add_cleanup(() => track.stop()); | |
| const pc1 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc1.close()); | |
| const pc2 = new RTCPeerConnection(); | |
| t.add_cleanup(() => pc2.close()); | |
| pc1.addTrack(track); | |
| const [pc1Transceiver] = pc1.getTransceivers(); | |
| let remoteTrackPromise = getTrackViaOnTrackPromise(pc2); | |
| // Complete O/A exchange such that the transceiver gets associated. | |
| await pc1.setLocalDescription(); | |
| await pc2.setRemoteDescription(pc1.localDescription); | |
| await pc2.setLocalDescription(); | |
| await pc1.setRemoteDescription(pc2.localDescription); | |
| const [pc2Transceiver] = pc2.getTransceivers(); | |
| assert_equals(pc2Transceiver.direction, 'recvonly'); | |
| assert_equals(pc2Transceiver.currentDirection, 'recvonly'); | |
| const remoteTrack = await remoteTrackPromise; | |
| // Cause track removal. | |
| pc1Transceiver.direction = 'inactive'; | |
| await pc1.setLocalDescription(); | |
| await pc2.setRemoteDescription(pc1.localDescription); | |
| // There's nothing equivalent to stream.onremovetrack when you don't have a | |
| // stream, but the track should become muted (if it isn't already). | |
| if (!remoteTrack.muted) { | |
| await new Promise(r => remoteTrack.onmute = () => { r(); }); | |
| } | |
| assert_equals(remoteTrack.muted, true); | |
| // Rolling back the offer revives the track, causing ontrack to fire again. | |
| remoteTrackPromise = getTrackViaOnTrackPromise(pc2); | |
| await pc2.setRemoteDescription({type:'rollback'}); | |
| const revivedRemoteTrack = await remoteTrackPromise; | |
| // We can be sure the same track is used, because the same transceiver is | |
| // used (and transciever.receiver.track has same lifetime as transceiver). | |
| assert_equals(pc2.getTransceivers().length, 1); | |
| assert_equals(remoteTrack, revivedRemoteTrack); | |
| }, `[${kind}] Track without stream: removal due to direction changing and then add back using rollback`); | |
| }); | |
| function getTrackViaOnTrackPromise(pc) { | |
| return new Promise(r => { | |
| pc.ontrack = e => { | |
| pc.ontrack = null; | |
| r(e.track); | |
| }; | |
| }); | |
| } | |
| function getRemoteStreamViaOnTrackPromise(pc) { | |
| return new Promise(r => { | |
| pc.ontrack = e => { | |
| pc.ontrack = null; | |
| r(e.streams[0]); | |
| }; | |
| }); | |
| } | |
| </script> |