<!doctype html>
<meta charset=utf-8>
<title>RTCPeerConnection Simulcast Tests - setParameters/encodings</title>
<meta name="timeout" content="long">
<script src="../third_party/sdp/sdp.js"></script>
<script src="simulcast.js"></script>
<script src="../RTCPeerConnection-helper.js"></script>
<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>

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc2.close());

  const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});

  await doOfferToSendSimulcast(pc1, pc2);

  await pc2.setLocalDescription();
  const simulcastAnswer = midToRid(pc2.localDescription, pc1.localDescription, ["foo"]);

  const parameters = sender.getParameters();
  parameters.encodings[1].scaleResolutionDownBy = 3.3;
  const answerDone = pc1.setRemoteDescription({type: "answer", sdp: simulcastAnswer});
  await sender.setParameters(parameters);
  await answerDone;

  assert_equals(pc1.getTransceivers().length, 1);
  const {encodings} = sender.getParameters();
  const rids = encodings.map(({rid}) => rid);
  assert_array_equals(rids, ["foo"]);
}, 'sRD(simulcast answer) can narrow the simulcast envelope when interrupted by a setParameters');

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc2.close());

  const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});

  await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);

  assert_equals(pc1.getTransceivers().length, 1);
  let encodings = sender.getParameters().encodings;
  let rids = encodings.map(({rid}) => rid);
  assert_array_equals(rids, ["foo", "bar"]);

  const reoffer = await pc2.createOffer();
  const simulcastSdp = midToRid(reoffer, pc1.localDescription, ["foo"]);

  const parameters = sender.getParameters();
  parameters.encodings[1].scaleResolutionDownBy = 3.3;
  const reofferDone = pc1.setRemoteDescription({type: "offer", sdp: simulcastSdp});
  await sender.setParameters(parameters);
  await reofferDone;
  await pc1.setLocalDescription();

  encodings = sender.getParameters().encodings;
  rids = encodings.map(({rid}) => rid);
  assert_array_equals(rids, ["foo"]);
}, 'sRD(simulcast offer) can narrow the simulcast envelope when interrupted by a setParameters');

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc2.close());

  const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});

  const parameters = sender.getParameters();
  parameters.encodings[0].scaleResolutionDownBy = 2.3;
  parameters.encodings[1].scaleResolutionDownBy = 3.3;
  await sender.setParameters(parameters);

  await doOfferToSendSimulcast(pc1, pc2);
  await doAnswerToRecvSimulcast(pc1, pc2, []);

  assert_equals(pc1.getTransceivers().length, 1);
  const encodings = sender.getParameters().encodings;
  const rids = encodings.map(({rid}) => rid);
  assert_array_equals(rids, ["foo"]);
  assert_equals(encodings[0].scaleResolutionDownBy, 2.3);
}, 'a simulcast setParameters followed by a sRD(unicast answer) results in keeping the first encoding');

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc2.close());

  const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
  await doOfferToSendSimulcast(pc1, pc2);

  await pc2.setLocalDescription();
  const unicastAnswer = midToRid(pc2.localDescription, pc1.localDescription, []);

  const parameters = sender.getParameters();
  parameters.encodings[0].scaleResolutionDownBy = 2.3;
  parameters.encodings[1].scaleResolutionDownBy = 3.3;
  const answerDone = pc1.setRemoteDescription({type: "answer", sdp: unicastAnswer});
  await sender.setParameters(parameters);
  await answerDone;

  assert_equals(pc1.getTransceivers().length, 1);
  const encodings = sender.getParameters().encodings;
  const rids = encodings.map(({rid}) => rid);
  assert_array_equals(rids, ["foo"]);
  assert_equals(encodings[0].scaleResolutionDownBy, 2.3);
}, 'sRD(unicast answer) interrupted by setParameters(simulcast) results in keeping the first encoding');

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc2.close());

  const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});

  await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
  assert_equals(pc1.getTransceivers().length, 1);
  let encodings = sender.getParameters().encodings;
  let rids = encodings.map(({rid}) => rid);
  assert_array_equals(rids, ["foo", "bar"]);

  const reoffer = await pc2.createOffer();
  const unicastSdp = midToRid(reoffer, pc1.localDescription, []);
  const parameters = sender.getParameters();
  parameters.encodings[0].scaleResolutionDownBy = 2.3;
  parameters.encodings[1].scaleResolutionDownBy = 3.3;
  const reofferDone = pc1.setRemoteDescription({type: "offer", sdp: unicastSdp});
  await sender.setParameters(parameters);
  await reofferDone;
  await pc1.setLocalDescription();

  encodings = sender.getParameters().encodings;
  rids = encodings.map(({rid}) => rid);
  assert_array_equals(rids, ["foo"]);
  assert_equals(encodings[0].scaleResolutionDownBy, 2.3);
}, 'sRD(unicast reoffer) interrupted by setParameters(simulcast) results in keeping the first encoding');

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc2.close());

  const {sender} = pc1.addTransceiver("video", {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});

  await doOfferToSendSimulcast(pc1, pc2);
  await pc2.setLocalDescription();
  const simulcastAnswer = midToRid(pc2.localDescription, pc1.localDescription, ["foo"]);
  const parameters = sender.getParameters();
  parameters.encodings[0].scaleResolutionDownBy = 3.3;
  const answerDone = pc1.setRemoteDescription({type: "answer", sdp: simulcastAnswer});
  await sender.setParameters(parameters);
  await answerDone;

  const {encodings} = sender.getParameters();
  assert_equals(encodings.length, 1);
  assert_equals(encodings[0].scaleResolutionDownBy, 3.3);
}, 'sRD(simulcast answer) interrupted by a setParameters does not result in losing modifications from the setParameters to the encodings that remain');

const simulcastOffer = `v=0
o=- 3840232462471583827 0 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
a=msid-semantic: WMS
m=video 9 UDP/TLS/RTP/SAVPF 96
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:Li6+
a=ice-pwd:3C05CTZBRQVmGCAq7hVasHlT
a=ice-options:trickle
a=fingerprint:sha-256 5B:D3:8E:66:0E:7D:D3:F3:8E:E6:80:28:19:FC:55:AD:58:5D:B9:3D:A8:DE:45:4A:E7:87:02:F8:3C:0B:3B:B3
a=setup:actpass
a=mid:0
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=recvonly
a=rtcp-mux
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
a=rid:foo recv
a=rid:bar recv
a=simulcast:recv foo;bar
`;

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());

  const stream = await getNoiseStream({video: true});
  t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
  const sender = pc1.addTrack(stream.getTracks()[0]);
  const parameters = sender.getParameters();
  parameters.encodings[0].scaleResolutionDownBy = 3.0;
  await sender.setParameters(parameters);

  await pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer});

  const {encodings} = sender.getParameters();
  const rids = encodings.map(({rid}) => rid);
  assert_array_equals(rids, ["foo", "bar"]);
  assert_equals(encodings[0].scaleResolutionDownBy, 2.0);
  assert_equals(encodings[1].scaleResolutionDownBy, 1.0);
}, 'addTrack, then a unicast setParameters, then sRD(simulcast offer) results in simulcast without the settings from setParameters');

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());

  const stream = await getNoiseStream({video: true});
  t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
  const sender = pc1.addTrack(stream.getTracks()[0]);
  const parameters = sender.getParameters();
  parameters.encodings[0].scaleResolutionDownBy = 3.0;

  const offerDone = pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer});
  await sender.setParameters(parameters);
  await offerDone;

  const {encodings} = sender.getParameters();
  const rids = encodings.map(({rid}) => rid);
  assert_array_equals(rids, ["foo", "bar"]);
  assert_equals(encodings[0].scaleResolutionDownBy, 2.0);
  assert_equals(encodings[1].scaleResolutionDownBy, 1.0);
}, 'addTrack, then sRD(simulcast offer) interrupted by a unicast setParameters results in simulcast without the settings from setParameters');

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  t.add_cleanup(() => pc2.close());

  const stream = await getNoiseStream({video: true});
  t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
  const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
  await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
  pc2.getTransceivers()[0].direction = "sendrecv";
  pc2.getTransceivers()[1].direction = "sendrecv";

  await doOfferToRecvSimulcast(pc2, pc1, []);
  // Race simulcast setParameters against sLD(unicast reanswer)
  const answer = await pc1.createAnswer();
  const aTask = queueAWebrtcTask();
  // This also queues a task to clear [[LastReturnedParameters]]
  const parameters = sender.getParameters();
  // This might or might not queue a task right away (it might do some
  // microtask stuff first), but it doesn't really matter.
  const sLDDone = pc1.setLocalDescription(answer);
  await aTask;
  // Task queue should now have the task that clears
  // [[LastReturnedParameters]], _then_ the success task for sLD.
  // setParameters should succeed because [[LastReturnedParameters]] has not
  // yet been cleared, and the steps in the success task for sLD have not run
  // either.
  await sender.setParameters(parameters);
  await sLDDone;

  assert_equals(pc1.getTransceivers().length, 1);
  const {encodings} = sender.getParameters();
  const rids = encodings.map(({rid}) => rid);
  assert_array_equals(rids, ["foo"]);
}, 'getParameters, then sLD(unicast answer) interrupted by a simulcast setParameters results in unicast');

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  t.add_cleanup(() => pc2.close());

  const stream = await getNoiseStream({video: true});
  t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
  const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
  await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
  pc2.getTransceivers()[0].direction = "sendrecv";
  pc2.getTransceivers()[1].direction = "sendrecv";

  await doOfferToRecvSimulcast(pc2, pc1, []);
  const answer = await pc1.createAnswer();

  // The timing on this is very difficult. We want to ensure that our
  // getParameters call happens after the initial steps in sLD, but
  // before the queued task that sLD runs when it completes.
  const aTask = queueAWebrtcTask();
  const sLDDone = pc1.setLocalDescription(answer);
  // We now have a queued task (aTask). We might also have the success task for
  // sLD, but maybe not. Allowing aTask to finish gives us our best chance that
  // the success task for sLD is queued, but not run yet.
  await aTask;
  const parameters = sender.getParameters();
  // Hopefully we now have the success task for sLD, followed by the
  // success task for getParameters.
  await sLDDone;
  // Success task for getParameters should not have run yet.
  await promise_rejects_dom(t, 'InvalidStateError', sender.setParameters(parameters));
},'Success task for setLocalDescription(answer) clears [[LastReturnedParameters]]');

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  t.add_cleanup(() => pc2.close());

  const stream = await getNoiseStream({video: true});
  t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
  const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
  await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
  pc2.getTransceivers()[0].direction = "sendrecv";
  pc2.getTransceivers()[1].direction = "sendrecv";

  await pc2.setLocalDescription();
  const simulcastOffer = midToRid(
    pc2.localDescription,
    pc1.localDescription,
    []
  );

  // The timing on this is very difficult. We need to ensure that our
  // getParameters call happens after the initial steps in sRD, but
  // before the queued task that sRD runs when it completes.
  const aTask = queueAWebrtcTask();
  const sRDDone = pc1.setRemoteDescription({ type: "offer", sdp: simulcastOffer });

  await aTask;
  const parameters = sender.getParameters();
  await sRDDone;
  await promise_rejects_dom(t, 'InvalidStateError', sender.setParameters(parameters));
},'Success task for setRemoteDescription(offer) clears [[LastReturnedParameters]]');

promise_test(async t => {
  const pc1 = new RTCPeerConnection();
  const pc2 = new RTCPeerConnection();
  t.add_cleanup(() => pc1.close());
  t.add_cleanup(() => pc2.close());

  const stream = await getNoiseStream({video: true});
  t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
  const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});
  await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
  pc2.getTransceivers()[0].direction = "sendrecv";
  pc2.getTransceivers()[1].direction = "sendrecv";

  await doOfferToSendSimulcast(pc1, pc2);
  await pc2.setLocalDescription();
  const simulcastAnswer = midToRid(
    pc2.localDescription,
    pc1.localDescription,
    []
  );

  // The timing on this is very difficult. We need to ensure that our
  // getParameters call happens after the initial steps in sRD, but
  // before the queued task that sRD runs when it completes.
  const aTask = queueAWebrtcTask();
  const sRDDone = pc1.setRemoteDescription({ type: "answer", sdp: simulcastAnswer });
  await aTask;

  const parameters = sender.getParameters();
  await sRDDone;
  await promise_rejects_dom(t, 'InvalidStateError', sender.setParameters(parameters));
},'Success task for setRemoteDescription(answer) clears [[LastReturnedParameters]]');

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({video: true});
  t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
  const sender = pc1.addTrack(stream.getTracks()[0]);
  pc2.addTrack(stream.getTracks()[0]);

  await doOfferToRecvSimulcast(pc2, pc1, ["foo", "bar"]);
  let parameters = sender.getParameters();
  let rids = parameters.encodings.map(({rid}) => rid);
  assert_array_equals(rids, ["foo", "bar"]);
  parameters.encodings[0].scaleResolutionDownBy = 3;
  parameters.encodings[1].scaleResolutionDownBy = 5;
  await sender.setParameters(parameters);

  await pc1.setRemoteDescription({sdp: "", type: "rollback"});

  parameters = sender.getParameters();
  rids = parameters.encodings.map(({rid}) => rid);
  assert_array_equals(rids, [undefined]);
  assert_equals(parameters.encodings[0].scaleResolutionDownBy, 1);
}, 'addTrack, then rollback of sRD(simulcast offer), brings us back to having a single encoding without any previously set parameters');

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({video: true});
  t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
  const {sender} = pc1.addTransceiver(stream.getTracks()[0], {sendEncodings: [{rid: "foo"}, {rid: "bar"}]});

  await doOfferToSendSimulcastAndAnswer(pc1, pc2, ["foo", "bar"]);
  let parameters = sender.getParameters();
  let rids = parameters.encodings.map(({rid}) => rid);
  assert_array_equals(rids, ["foo", "bar"]);
  parameters.encodings[0].scaleResolutionDownBy = 3;
  parameters.encodings[1].scaleResolutionDownBy = 5;
  await sender.setParameters(parameters);

  await doOfferToRecvSimulcast(pc2, pc1, []);
  parameters = sender.getParameters();
  rids = parameters.encodings.map(({rid}) => rid);
  assert_array_equals(rids, ["foo", "bar"]);

  await pc1.setRemoteDescription({sdp: "", type: "rollback"});
  parameters = sender.getParameters();
  rids = parameters.encodings.map(({rid}) => rid);
  assert_array_equals(rids, ["foo", "bar"]);
  assert_equals(parameters.encodings[0].scaleResolutionDownBy, 3);
  assert_equals(parameters.encodings[1].scaleResolutionDownBy, 5);
}, 'rollback of a remote offer that disabled a previously negotiated simulcast should restore simulcast along with any previously set parameters');

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({video: true});
  t.add_cleanup(() => stream.getTracks().forEach(track => track.stop()));
  const sender = pc1.addTrack(stream.getTracks()[0]);
  pc2.addTrack(stream.getTracks()[0]);

  await doOfferToRecvSimulcast(pc2, pc1, ["foo", "bar"]);
  const aTask = queueAWebrtcTask();
  let parameters = sender.getParameters();
  let rids = parameters.encodings.map(({rid}) => rid);
  assert_array_equals(rids, ["foo", "bar"]);
  parameters.encodings[0].scaleResolutionDownBy = 3;
  parameters.encodings[1].scaleResolutionDownBy = 5;

  const rollbackDone = pc1.setRemoteDescription({sdp: "", type: "rollback"});
  await aTask;
  await sender.setParameters(parameters);
  await rollbackDone;

  parameters = sender.getParameters();
  rids = parameters.encodings.map(({rid}) => rid);
  assert_array_equals(rids, [undefined]);
  assert_equals(parameters.encodings[0].scaleResolutionDownBy, 1);
}, 'rollback of sRD(simulcast offer) interrupted by setParameters(simulcast) brings us back to having a single encoding without any previously set parameters');

</script>
