<!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) => {
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const transceiver = pc1.addTransceiver(kind);
// 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);
assert_not_equals(transceiver.mid, null, 'mid before stop()');
assert_not_equals(transceiver.direction, 'stopped',
'direction before stop()');
assert_not_equals(transceiver.currentDirection, 'stopped',
'currentDirection before stop()');
// Stop makes it stopping, but not stopped.
transceiver.stop();
assert_not_equals(transceiver.mid, null, 'mid after stop()');
assert_equals(transceiver.direction, 'stopped', 'direction after stop()');
assert_not_equals(transceiver.currentDirection, 'stopped',
'currentDirection after stop()');
// Negotiating makes it stopped.
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
assert_equals(transceiver.mid, null, 'mid after negotiation');
assert_equals(transceiver.direction, 'stopped',
'direction after negotiation');
assert_equals(transceiver.currentDirection, 'stopped',
'currentDirection after negotiation');
}, `[${kind}] Locally stopped transceiver goes from stopping to stopped`);
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver(kind);
const trackEnded = new Promise(r => transceiver.receiver.track.onended = r);
assert_equals(transceiver.receiver.track.readyState, 'live');
transceiver.stop();
// Stopping triggers ending the track, but this happens asynchronously.
assert_equals(transceiver.receiver.track.readyState, 'live');
await trackEnded;
assert_equals(transceiver.receiver.track.readyState, 'ended');
}, `[${kind}] Locally stopping a transceiver ends the track`);
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const pc1Transceiver = pc1.addTransceiver(kind);
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
await pc2.setLocalDescription();
await pc1.setRemoteDescription(pc2.localDescription);
const [pc2Transceiver] = pc2.getTransceivers();
pc1Transceiver.stop();
await pc1.setLocalDescription();
assert_equals(pc2Transceiver.receiver.track.readyState, 'live');
// Applying the remote offer immediately ends the track, we don't need to
// create or apply an answer.
await pc2.setRemoteDescription(pc1.localDescription);
// sRD just resolved, so we're in the success task for sRD. The transition
// from live -> ended is queued right now.
assert_equals(pc2Transceiver.receiver.track.readyState, 'live');
await new Promise(r => pc2Transceiver.receiver.track.onended = r);
assert_equals(pc2Transceiver.receiver.track.readyState, 'ended');
}, `[${kind}] Remotely stopping a transceiver ends the track`);
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const pc1Transceiver = pc1.addTransceiver(kind);
// 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_not_equals(pc2Transceiver.mid, null, 'mid before stop()');
assert_not_equals(pc2Transceiver.direction, 'stopped',
'direction before stop()');
assert_not_equals(pc2Transceiver.currentDirection, 'stopped',
'currentDirection before stop()');
// Make the remote transceiver stopped.
pc1Transceiver.stop();
// Negotiating makes it stopped.
assert_equals(pc2.getTransceivers().length, 1);
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
// As soon as the remote offer is set, the transceiver is stopped but it is
// not disassociated or removed until setting the local answer.
assert_equals(pc2.getTransceivers().length, 1);
assert_not_equals(pc2Transceiver.mid, null, 'mid during negotiation');
assert_equals(pc2Transceiver.direction, 'stopped',
'direction during negotiation');
assert_equals(pc2Transceiver.currentDirection, 'stopped',
'currentDirection during negotiation');
await pc2.setLocalDescription();
assert_equals(pc2.getTransceivers().length, 0);
assert_equals(pc2Transceiver.mid, null, 'mid after negotiation');
assert_equals(pc2Transceiver.direction, 'stopped',
'direction after negotiation');
assert_equals(pc2Transceiver.currentDirection, 'stopped',
'currentDirection after negotiation');
}, `[${kind}] Remotely stopped transceiver goes directly to stopped`);
promise_test(async t => {
const pc = new RTCPeerConnection();
t.add_cleanup(() => pc.close());
const transceiver = pc.addTransceiver(kind);
// Rollback does not end the track, because the transceiver is not removed.
await pc.setLocalDescription();
await pc.setLocalDescription({type:'rollback'});
assert_equals(transceiver.receiver.track.readyState, 'live');
}, `[${kind}] Rollback when transceiver is not removed does not end track`);
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const pc1Transceiver = pc1.addTransceiver(kind);
// Start negotiation, causing a transceiver to be created.
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
const [pc2Transceiver] = pc2.getTransceivers();
// Rollback such that the transceiver is removed.
await pc2.setRemoteDescription({type:'rollback'});
assert_equals(pc2.getTransceivers().length, 0);
// sRD just resolved, so we're in the success task for sRD. The transition
// from live -> ended is queued right now.
assert_equals(pc2Transceiver.receiver.track.readyState, 'live');
await new Promise(r => pc2Transceiver.receiver.track.onended = r);
assert_equals(pc2Transceiver.receiver.track.readyState, 'ended');
}, `[${kind}] Rollback when removing transceiver does end the track`);
// Same test as above but looking at direction and currentDirection.
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const pc1Transceiver = pc1.addTransceiver(kind);
// Start negotiation, causing a transceiver to be created.
await pc1.setLocalDescription();
await pc2.setRemoteDescription(pc1.localDescription);
const [pc2Transceiver] = pc2.getTransceivers();
// Rollback such that the transceiver is removed.
await pc2.setRemoteDescription({type:'rollback'});
assert_equals(pc2.getTransceivers().length, 0);
// The removed transceiver is stopped.
assert_equals(pc2Transceiver.currentDirection, 'stopped',
'currentDirection indicate stopped');
// A stopped transceiver is necessarily also stopping.
assert_equals(pc2Transceiver.direction, 'stopped',
'direction indicate stopping');
// A stopped transceiver has no mid.
assert_equals(pc2Transceiver.mid, null, 'not associated');
}, `[${kind}] Rollback when removing transceiver makes it stopped`);
promise_test(async t => {
const pc1 = new RTCPeerConnection();
t.add_cleanup(() => pc1.close());
const pc2 = new RTCPeerConnection();
t.add_cleanup(() => pc2.close());
const constraints = {};
constraints[kind] = true;
const stream = await navigator.mediaDevices.getUserMedia(constraints);
const [track] = stream.getTracks();
pc1.addTrack(track);
pc2.addTrack(track);
const transceiver = pc2.getTransceivers()[0];
const ontrackEvent = new Promise(r => {
pc2.ontrack = e => r(e.track);
});
// Simulate glare: both peer connections set local offers.
await pc1.setLocalDescription();
await pc2.setLocalDescription();
// Set remote offer, which implicitly rolls back the local offer. Because
// `transceiver` is an addTrack-transceiver, it should get repurposed.
await pc2.setRemoteDescription(pc1.localDescription);
assert_equals(transceiver.receiver.track.readyState, 'live');
// Sanity check: the track should still be live when ontrack fires.
assert_equals((await ontrackEvent).readyState, 'live');
}, `[${kind}] Glare when transceiver is not removed does not end track`);
});
</script>