<!DOCTYPE HTML>
<html>
<head>
  <script type="application/javascript" src="pc.js"></script>
</head>
<body>
<pre id="test">
<script type="application/javascript">
createHTML({
  bug: "1230184",
  title: "Set parameters on sender",
  visible: true
});

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
`;

function buildMaximumSendEncodings() {
  const sendEncodings = [];
  while (true) {
    // isDeeply does not see identical string primitives and String objects
    // as the same, so we make this a string primitive.
    sendEncodings.push({rid: `${sendEncodings.length}`});
    const pc = new RTCPeerConnection();
    const {sender} = pc.addTransceiver('video', {sendEncodings});
    const {encodings} = sender.getParameters();
    if (encodings.length < sendEncodings.length) {
      sendEncodings.pop();
      return sendEncodings;
    }
  }
}

async function queueAWebrtcTask() {
  const pc = new RTCPeerConnection();
  pc.addTransceiver('audio');
  await new Promise(r => pc.onnegotiationneeded = r);
  pc.close();
}

// setParameters is mostly tested in wpt, but we test a few
// implementation-specific things here. Other mochitests check whether the
// set parameters actually have the desired effect on the media streams.
const tests = [

  // wpt currently does not assume support for 3 encodings, which limits the
  // effectiveness of its powers-of-2 test (since it can test only for 1 and 2)
  async function checkScaleResolutionDownByAutoFillPowersOf2() {
    const pc = new RTCPeerConnection();
    const {sender} = pc.addTransceiver('video', {
      sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
    });
    const {encodings} = sender.getParameters();
    const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
    isDeeply(scaleValues, [4, 2, 1]);
  },

  // wpt currently does not assume support for 3 encodings, which limits the
  // effectiveness of its fill-with-1 test
  async function checkScaleResolutionDownByAutoFillWith1() {
    const pc = new RTCPeerConnection();
    const {sender} = pc.addTransceiver('video', {
      sendEncodings: [
        {rid: "0"},{rid: "1", scaleResolutionDownBy: 3},{rid: "2"}
      ]
    });
    const {encodings} = sender.getParameters();
    const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
    isDeeply(scaleValues, [1, 3, 1]);
  },

  async function checkVideoEncodingLimit() {
    const pc = new RTCPeerConnection();
    const maxSendEncodings = buildMaximumSendEncodings();
    const sendEncodings = maxSendEncodings.concat({rid: "a"});
    const {sender} = pc.addTransceiver('video', {sendEncodings});
    const {encodings} = sender.getParameters();

    const rids = encodings.map(({rid}) => rid);
    const expectedRids = maxSendEncodings.map(({rid}) => rid);
    isDeeply(rids, expectedRids);

    const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
    const expectedScaleValues = [];
    let scale = 1;
    while (expectedScaleValues.length < maxSendEncodings.length) {
      expectedScaleValues.push(scale);
      scale *= 2;
    }
    isDeeply(scaleValues, expectedScaleValues.reverse());
  },

  async function checkScaleDownByInTrimmedEncoding() {
    const pc = new RTCPeerConnection();
    const maxSendEncodings = buildMaximumSendEncodings();
    const sendEncodings = maxSendEncodings.concat({rid: "a", scaleResolutionDownBy: 3});
    const {sender} = pc.addTransceiver('video', {sendEncodings});
    const {encodings} = sender.getParameters();
    const rids = encodings.map(({rid}) => rid);
    const expectedRids = maxSendEncodings.map(({rid}) => rid);
    isDeeply(rids, expectedRids);
    const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
    const expectedScaleValues = maxSendEncodings.map(() => 1);
    isDeeply(scaleValues, expectedScaleValues);
  },

  async function checkLibwebrtcRidLengthLimit() {
    const pc = new RTCPeerConnection();
    try {
      pc.addTransceiver('video', {
        sendEncodings: [{rid: "wibblywobblyjeremybearimy"}]}
      );
      ok(false, "Rid should be too long for libwebrtc!");
    } catch (e) {
      is(e.name, "TypeError",
        "Rid that is too long for libwebrtc should result in a TypeError");
    }
  },

  async function checkErrorsInTrimmedEncodings() {
    const pc = new RTCPeerConnection();
    const maxSendEncodings = buildMaximumSendEncodings();
    try {
      const sendEncodings = maxSendEncodings.concat({rid: "foo-bar"});
      pc.addTransceiver('video', { sendEncodings });
      ok(false, "Should throw due to invalid rid characters");
    } catch (e) {
      is(e.name, "TypeError")
    }
    try {
      const sendEncodings = maxSendEncodings.concat({rid: "wibblywobblyjeremybearimy"});
      pc.addTransceiver('video', { sendEncodings });
      ok(false, "Should throw because rid too long");
    } catch (e) {
      is(e.name, "TypeError")
    }
    try {
      const sendEncodings = maxSendEncodings.concat({scaleResolutionDownBy: 2});
      pc.addTransceiver('video', { sendEncodings });
      ok(false, "Should throw due to missing rid");
    } catch (e) {
      is(e.name, "TypeError")
    }
    try {
      const sendEncodings = maxSendEncodings.concat(maxSendEncodings[0]);
      pc.addTransceiver('video', { sendEncodings });
      ok(false, "Should throw due to duplicate rid");
    } catch (e) {
      is(e.name, "TypeError")
    }
    try {
      const sendEncodings = maxSendEncodings.concat({rid: maxSendEncodings.length, scaleResolutionDownBy: 0});
      pc.addTransceiver('video', { sendEncodings });
      ok(false, "Should throw due to invalid scaleResolutionDownBy");
    } catch (e) {
      is(e.name, "RangeError")
    }
    try {
      const sendEncodings = maxSendEncodings.concat({rid: maxSendEncodings.length, maxFramerate: -1});
      pc.addTransceiver('video', { sendEncodings });
      ok(false, "Should throw due to invalid maxFramerate");
    } catch (e) {
      is(e.name, "RangeError")
    }
  },

  async function checkCompatModeUnicastSetParametersAllowsSimulcastOffer() {
    await pushPrefs(
      ["media.peerconnection.allow_old_setParameters", true]);
    const pc1 = new RTCPeerConnection();
    const stream = await navigator.mediaDevices.getUserMedia({video: true});
    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);
    isDeeply(rids, ["foo", "bar"]);
    is(encodings[0].scaleResolutionDownBy, 2.0);
    is(encodings[1].scaleResolutionDownBy, 1.0);
  },

  async function checkCompatModeUnicastSetParametersInterruptAllowsSimulcastOffer() {
    await pushPrefs(
      ["media.peerconnection.allow_old_setParameters", true]);
    const pc1 = new RTCPeerConnection();
    const stream = await navigator.mediaDevices.getUserMedia({video: true});
    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);
    isDeeply(rids, ["foo", "bar"]);
    is(encodings[0].scaleResolutionDownBy, 2.0);
    is(encodings[1].scaleResolutionDownBy, 1.0);
  },

  async function checkCompatModeSimulcastSetParametersSetsSimulcastEnvelope() {
    await pushPrefs(
      ["media.peerconnection.allow_old_setParameters", true]);
    const pc1 = new RTCPeerConnection();
    const stream = await navigator.mediaDevices.getUserMedia({video: true});
    const sender = pc1.addTrack(stream.getTracks()[0]);
    const parameters = sender.getParameters();
    parameters.encodings[0].rid = "1";
    parameters.encodings.push({rid: "2"});
    await sender.setParameters(parameters);

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

    const {encodings} = sender.getParameters();
    const rids = encodings.map(({rid}) => rid);
    // No overlap in rids -> unicast
    isDeeply(rids, [undefined]);
  },

  async function checkCompatModeSimulcastSetParametersRacesLocalUnicastOffer() {
    await pushPrefs(
      ["media.peerconnection.allow_old_setParameters", true]);
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    const stream = await navigator.mediaDevices.getUserMedia({video: true});
    const sender = pc1.addTrack(stream.getTracks()[0]);
    // unicast offer
    const offer = await pc1.createOffer();
    const aTask = queueAWebrtcTask();
    const sldPromise =  pc1.setLocalDescription(offer);

    // Right now, we have aTask queued. The success task for sLD is not queued
    // yet, because Firefox performs the initial steps on the microtask queue,
    // which we have not allowed to run yet. Awaiting aTask will first clear
    // the microtask queue, then run the task queue until aTask is finished.
    // That _should_ result in the success task for sLD(offer) being queued.
    await aTask;

    const parameters = sender.getParameters();
    parameters.encodings[0].rid = "foo";
    parameters.encodings.push({rid: "bar"});
    // simulcast setParameters; the task to update [[SendEncodings]] should be
    // queued after the success task for sLD(offer)
    await sender.setParameters(parameters);
    await sldPromise;

    const {encodings} = sender.getParameters();
    const rids = encodings.map(({rid}) => rid);
    // Compat mode lets this slide, but won't try to negotiate it since we've
    // already applied a unicast local offer.
    isDeeply(rids, ["foo", "bar"]);

    // Let negotiation finish, so we can generate a new offer
    await pc2.setRemoteDescription(pc1.localDescription);
    await pc2.setLocalDescription();
    await pc1.setRemoteDescription(pc2.localDescription);

    const reoffer = await pc1.createOffer();
    ok(!reoffer.sdp.includes("a=simulcast"), "reoffer should be unicast");
  },

  async function checkCompatModeSimulcastSetParametersRacesRemoteOffer() {
    await pushPrefs(
      ["media.peerconnection.allow_old_setParameters", true]);
    const pc1 = new RTCPeerConnection();
    const stream = await navigator.mediaDevices.getUserMedia({video: true});
    const sender = pc1.addTrack(stream.getTracks()[0]);
    const parameters = sender.getParameters();
    parameters.encodings[0].rid = "foo";
    parameters.encodings.push({rid: "bar"});
    const p = sender.setParameters(parameters);
    await pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer});
    await p;
    const answer = await pc1.createAnswer();

    const {encodings} = sender.getParameters();
    const rids = encodings.map(({rid}) => rid);
    isDeeply(rids, ["foo", "bar"]);
    ok(answer.sdp.includes("a=simulcast:send foo;bar"), "answer should be simulcast");
  },

  async function checkCompatModeSimulcastSetParametersRacesLocalAnswer() {
    await pushPrefs(
      ["media.peerconnection.allow_old_setParameters", true]);
    // We do an initial negotiation, and while the local answer is pending,
    // perform a setParameters on a not-yet-negotiated video sender. The intent
    // here is to have the success task for sLD(answer) run while the
    // setParameters is pending.
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();

    const audioStream = await navigator.mediaDevices.getUserMedia({audio: true});
    // We use this later on, but set it up now so we don't inadvertently
    // crank the event loop more than we intend below.
    const videoStream = await navigator.mediaDevices.getUserMedia({video: true});
    pc2.addTrack(audioStream.getTracks()[0]);
    await pc2.setLocalDescription();
    await pc1.setRemoteDescription(pc2.localDescription);
    const answer = await pc1.createAnswer();
    const aTask = queueAWebrtcTask();
    const sldPromise = pc1.setLocalDescription(answer);

    // Right now, we have aTask queued. The success task for sLD is not queued
    // yet, because Firefox performs the initial steps on the microtask queue,
    // which we have not allowed to run yet. Awaiting aTask will first clear
    // the microtask queue, then run the task queue until aTask is finished.
    // That _should_ result in the success task for sLD(answer) being queued.
    await aTask;

    // The success task for sLD(answer) should be queued now. Don't relinquish
    // the event loop!

    // New sender that has nothing to do with the negotiation in progress.
    const sender = pc1.addTrack(videoStream.getTracks()[0]);
    const parameters = sender.getParameters();
    parameters.encodings[0].rid = "foo";
    parameters.encodings.push({rid: "bar"});

    // We have not relinquished the event loop, so the sLD(answer) should still
    // be queued. The task that updates [[SendEncodings]] (from setParameters)
    // should be queued behind it. Let them both run.
    await sender.setParameters(parameters);
    await sldPromise;

    const offer = await pc1.createOffer();

    const {encodings} = sender.getParameters();
    const rids = encodings.map(({rid}) => rid);
    isDeeply(rids, ["foo", "bar"]);
    ok(offer.sdp.includes("a=simulcast:send foo;bar"), "offer should be simulcast");
  },

  async function checkCompatModeSimulcastSetParametersRacesRemoteAnswer() {
    await pushPrefs(
      ["media.peerconnection.allow_old_setParameters", true]);
    // We do an initial negotiation, and while the remote answer is pending,
    // perform a setParameters on a not-yet-negotiated video sender. The intent
    // here is to have the success task for sRD(answer) run while the
    // setParameters is pending.
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();

    const audioStream = await navigator.mediaDevices.getUserMedia({audio: true});
    // We use this later on, but set it up now so we don't inadvertently
    // crank the event loop more than we intend below.
    const videoStream = await navigator.mediaDevices.getUserMedia({video: true});
    pc1.addTrack(audioStream.getTracks()[0]);
    await pc1.setLocalDescription();
    await pc2.setRemoteDescription(pc1.localDescription);
    await pc2.setLocalDescription();
    const aTask = queueAWebrtcTask();
    const srdPromise = pc1.setRemoteDescription(pc2.localDescription);

    // Right now, we have aTask queued. The success task for sRD is not queued
    // yet, because Firefox performs the initial steps on the microtask queue,
    // which we have not allowed to run yet. Awaiting aTask will first clear
    // the microtask queue, then run the task queue until aTask is finished.
    // That _should_ result in the success task for sRD(answer) being queued.
    await aTask;

    // The success task for sRD(answer) should be queued now. Don't relinquish
    // the event loop!

    const sender = pc1.addTrack(videoStream.getTracks()[0]);
    const parameters = sender.getParameters();
    parameters.encodings[0].rid = "foo";
    parameters.encodings.push({rid: "bar"});

    // We have not relinquished the event loop, so the sRD(answer) should still
    // be queued. The task that updates [[SendEncodings]] (from setParameters)
    // should be queued behind it. Let them both run.
    await sender.setParameters(parameters);
    await srdPromise;

    const offer = await pc1.createOffer();

    const {encodings} = sender.getParameters();
    const rids = encodings.map(({rid}) => rid);
    isDeeply(rids, ["foo", "bar"]);
    ok(offer.sdp.includes("a=simulcast:send foo;bar"), "offer should be simulcast");
  },

  async function checkCompatModeSimulcastRidlessSetParametersRacesLocalOffer() {
    await pushPrefs(
      ["media.peerconnection.allow_old_setParameters", true]);
    const pc1 = new RTCPeerConnection();
    const pc2 = new RTCPeerConnection();
    const stream = await navigator.mediaDevices.getUserMedia({video: true});
    const sender = pc1.addTrack(stream.getTracks()[0]);
    // unicast offer
    const aTask = queueAWebrtcTask();
    const sldPromise =  pc1.setLocalDescription();

    // Right now, we have aTask queued. The success task for sLD is not queued
    // yet, because Firefox performs the initial steps on the microtask queue,
    // which we have not allowed to run yet. Awaiting aTask will first clear
    // the microtask queue, then run the task queue until aTask is finished.
    // That _should_ result in the success task for sLD(offer) being queued.
    await aTask;

    // simulcast setParameters; the task to update [[SendEncodings]] should be
    // queued after the success task for sLD(offer)
    try {
      await sender.setParameters({"encodings": [{}, {}]});
      ok(false, "setParameters with two ridless encodings should fail");
    } catch (e) {
      ok(true, "setParameters with two ridless encodings should fail");
    }
    await sldPromise;

    const {encodings} = sender.getParameters();
    const rids = encodings.map(({rid}) => rid);
    // Compat mode lets this slide, but won't try to negotiate it since we've
    // already applied a unicast local offer.
    isDeeply(rids, [undefined]);

    // Let negotiation finish, so we can generate a new offer
    await pc2.setRemoteDescription(pc1.localDescription);
    await pc2.setLocalDescription();
    await pc1.setRemoteDescription(pc2.localDescription);

    const reoffer = await pc1.createOffer();
    ok(!reoffer.sdp.includes("a=simulcast"), "reoffer should be unicast");
  },

];

runNetworkTest(async () => {
  await pushPrefs(
    ["media.peerconnection.allow_old_setParameters", false]);
  for (const test of tests) {
    info(`Running test: ${test.name}`);
    await test();
    info(`Done running test: ${test.name}`);
  }
});

</script>
</pre>
</body>
</html>
