<!DOCTYPE HTML>
<html>
<head>
  <script type="application/javascript" src="pc.js"></script>
  <script type="application/javascript" src="iceTestUtils.js"></script>
</head>
<body>
<pre id="test">
<script type="application/javascript">
  createHTML({
    bug: "1799932",
    title: "RTCPeerConnection check renegotiation of extmap"
  });

  function setExtmap(sdp, uri, id) {
    const regex = new RegExp(`a=extmap:[0-9]+(\/[a-z]+)? ${uri}`, 'g');
    if (id) {
      return sdp.replaceAll(regex, `a=extmap:${id}$1 ${uri}`);
    } else {
      return sdp.replaceAll(regex, `a=unknownattr`);
    }
  }

  function getExtmap(sdp, uri) {
    const regex = new RegExp(`a=extmap:([0-9]+)(\/[a-z]+)? ${uri}`);
    return sdp.match(regex)[1];
  }

  function replaceExtUri(sdp, oldUri, newUri) {
    const regex = new RegExp(`(a=extmap:[0-9]+\/[a-z]+)? ${oldUri}`, 'g');
    return sdp.replaceAll(regex, `$1 ${newUri}`);
  }

  const tests = [
    async function checkAudioMidChange() {
      const pc1 = new RTCPeerConnection();
      const pc2 = new RTCPeerConnection();

      const stream = await navigator.mediaDevices.getUserMedia({audio: true});
      pc1.addTrack(stream.getTracks()[0]);
      pc2.addTrack(stream.getTracks()[0]);

      await connect(pc1, pc2, 32000, "Initial connection");

      // Sadly, there's no way to tell the offerer to change the extmap. Other
      // types of endpoint could conceivably do this, so we at least don't want
      // to crash.
      // TODO: Would be nice to be able to test this with an endpoint that
      // actually changes the ids it uses.
      const reoffer = await pc1.createOffer();
      reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", 14);
      info(`New reoffer: ${reoffer.sdp}`);
      await pc2.setRemoteDescription(reoffer);
      await pc2.setLocalDescription();
      await wait(2000);
    },

    async function checkVideoMidChange() {
      const pc1 = new RTCPeerConnection();
      const pc2 = new RTCPeerConnection();

      const stream = await navigator.mediaDevices.getUserMedia({video: true});
      pc1.addTrack(stream.getTracks()[0]);
      pc2.addTrack(stream.getTracks()[0]);

      await connect(pc1, pc2, 32000, "Initial connection");

      // Sadly, there's no way to tell the offerer to change the extmap. Other
      // types of endpoint could conceivably do this, so we at least don't want
      // to crash.
      // TODO: Would be nice to be able to test this with an endpoint that
      // actually changes the ids it uses.
      const reoffer = await pc1.createOffer();
      reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", 14);
      info(`New reoffer: ${reoffer.sdp}`);
      await pc2.setRemoteDescription(reoffer);
      await pc2.setLocalDescription();
      await wait(2000);
    },

    async function checkAudioMidSwap() {
      const pc1 = new RTCPeerConnection();
      const pc2 = new RTCPeerConnection();

      const stream = await navigator.mediaDevices.getUserMedia({audio: true});
      pc1.addTrack(stream.getTracks()[0]);
      pc2.addTrack(stream.getTracks()[0]);

      await connect(pc1, pc2, 32000, "Initial connection");

      // Sadly, there's no way to tell the offerer to change the extmap. Other
      // types of endpoint could conceivably do this, so we at least don't want
      // to crash.
      // TODO: Would be nice to be able to test this with an endpoint that
      // actually changes the ids it uses.
      const reoffer = await pc1.createOffer();
      const midId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid");
      const ssrcLevelId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level");
      reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", ssrcLevelId);
      reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", midId);
      info(`New reoffer: ${reoffer.sdp}`);
      try {
        await pc2.setRemoteDescription(reoffer);
        ok(false, "sRD should fail when it attempts extension id remapping");
      } catch (e) {
        ok(true, "sRD should fail when it attempts extension id remapping");
      }
    },

    async function checkVideoMidSwap() {
      const pc1 = new RTCPeerConnection();
      const pc2 = new RTCPeerConnection();

      const stream = await navigator.mediaDevices.getUserMedia({video: true});
      pc1.addTrack(stream.getTracks()[0]);
      pc2.addTrack(stream.getTracks()[0]);

      await connect(pc1, pc2, 32000, "Initial connection");

      // Sadly, there's no way to tell the offerer to change the extmap. Other
      // types of endpoint could conceivably do this, so we at least don't want
      // to crash.
      // TODO: Would be nice to be able to test this with an endpoint that
      // actually changes the ids it uses.
      const reoffer = await pc1.createOffer();
      const midId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid");
      const toffsetId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:toffset");
      reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", toffsetId);
      reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:toffset", midId);
      info(`New reoffer: ${reoffer.sdp}`);
      try {
        await pc2.setRemoteDescription(reoffer);
        ok(false, "sRD should fail when it attempts extension id remapping");
      } catch (e) {
        ok(true, "sRD should fail when it attempts extension id remapping");
      }
    },

    async function checkAudioIdReuse() {
      const pc1 = new RTCPeerConnection();
      const pc2 = new RTCPeerConnection();

      const stream = await navigator.mediaDevices.getUserMedia({audio: true});
      pc1.addTrack(stream.getTracks()[0]);
      pc2.addTrack(stream.getTracks()[0]);

      await connect(pc1, pc2, 32000, "Initial connection");

      // Sadly, there's no way to tell the offerer to change the extmap. Other
      // types of endpoint could conceivably do this, so we at least don't want
      // to crash.
      // TODO: Would be nice to be able to test this with an endpoint that
      // actually changes the ids it uses.
      const reoffer = await pc1.createOffer();
      // Change uri, but not the id, so the id now refers to foo.
      reoffer.sdp = replaceExtUri(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", "foo");
      info(`New reoffer: ${reoffer.sdp}`);
      try {
        await pc2.setRemoteDescription(reoffer);
        ok(false, "sRD should fail when it attempts extension id remapping");
      } catch (e) {
        ok(true, "sRD should fail when it attempts extension id remapping");
      }
    },

    async function checkVideoIdReuse() {
      const pc1 = new RTCPeerConnection();
      const pc2 = new RTCPeerConnection();

      const stream = await navigator.mediaDevices.getUserMedia({video: true});
      pc1.addTrack(stream.getTracks()[0]);
      pc2.addTrack(stream.getTracks()[0]);

      await connect(pc1, pc2, 32000, "Initial connection");

      // Sadly, there's no way to tell the offerer to change the extmap. Other
      // types of endpoint could conceivably do this, so we at least don't want
      // to crash.
      // TODO: Would be nice to be able to test this with an endpoint that
      // actually changes the ids it uses.
      const reoffer = await pc1.createOffer();
      // Change uri, but not the id, so the id now refers to foo.
      reoffer.sdp = replaceExtUri(reoffer.sdp, "urn:ietf:params:rtp-hdrext:toffset", "foo");
      info(`New reoffer: ${reoffer.sdp}`);
      try {
        await pc2.setRemoteDescription(reoffer);
        ok(false, "sRD should fail when it attempts extension id remapping");
      } catch (e) {
        ok(true, "sRD should fail when it attempts extension id remapping");
      }
    },

    // What happens when remote answer uses an extmap id, and then a remote
    // reoffer tries to use the same id for something else?
    async function checkAudioIdReuseOffererThenAnswerer() {
      const pc1 = new RTCPeerConnection();
      const pc2 = new RTCPeerConnection();

      const stream = await navigator.mediaDevices.getUserMedia({audio: true});
      pc1.addTrack(stream.getTracks()[0]);
      pc2.addTrack(stream.getTracks()[0]);

      await connect(pc1, pc2, 32000, "Initial connection");

      const reoffer = await pc2.createOffer();
      // Change uri, but not the id, so the id now refers to foo.
      reoffer.sdp = replaceExtUri(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", "foo");
      info(`New reoffer: ${reoffer.sdp}`);
      try {
        await pc1.setRemoteDescription(reoffer);
        ok(false, "sRD should fail when it attempts extension id remapping");
      } catch (e) {
        ok(true, "sRD should fail when it attempts extension id remapping");
      }
    },

    // What happens when a remote offer uses a different extmap id than the
    // default? Does the answerer remember the new id in reoffers?
    async function checkAudioIdReuseOffererThenAnswerer() {
      const pc1 = new RTCPeerConnection();
      const pc2 = new RTCPeerConnection();

      const stream = await navigator.mediaDevices.getUserMedia({audio: true});
      pc1.addTrack(stream.getTracks()[0]);
      pc2.addTrack(stream.getTracks()[0]);

      // Negotiate, but change id for ssrc-audio-level to something pc2 would
      // not typically use.
      await pc1.setLocalDescription();
      const mungedOffer = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", 12);
      await pc2.setRemoteDescription({sdp: mungedOffer, type: "offer"});
      await pc2.setLocalDescription();

      const reoffer = await pc2.createOffer();
      is(getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level"), "12");
    },

    async function checkAudioUnnegotiatedIdReuse1() {
      const pc1 = new RTCPeerConnection();
      const pc2 = new RTCPeerConnection();

      const stream = await navigator.mediaDevices.getUserMedia({audio: true});
      pc1.addTrack(stream.getTracks()[0]);
      pc2.addTrack(stream.getTracks()[0]);

      // Negotiate, but remove ssrc-audio-level from answer
      await pc1.setLocalDescription();
      const levelId = getExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level");
      await pc2.setRemoteDescription(pc1.localDescription);
      await pc2.setLocalDescription();
      const answerNoExt = setExtmap(pc2.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined);
      await pc1.setRemoteDescription({sdp: answerNoExt, type: "answer"});

      // Renegotiate, and use the id that offerer used for ssrc-audio-level for
      // something different (while making sure we don't use it twice)
      await pc2.setLocalDescription();
      const mungedReoffer = setExtmap(pc2.localDescription.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", levelId);
      const twiceMungedReoffer = setExtmap(mungedReoffer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined);
      await pc1.setRemoteDescription({sdp: twiceMungedReoffer, type: "offer"});
    },

    async function checkAudioUnnegotiatedIdReuse2() {
      const pc1 = new RTCPeerConnection();
      const pc2 = new RTCPeerConnection();

      const stream = await navigator.mediaDevices.getUserMedia({audio: true});
      pc1.addTrack(stream.getTracks()[0]);
      pc2.addTrack(stream.getTracks()[0]);

      // Negotiate, but remove ssrc-audio-level from offer. pc2 has never seen
      // |levelId| in extmap yet, but internally probably wants to use that for
      // ssrc-audio-level
      await pc1.setLocalDescription();
      const levelId = getExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level");
      const offerNoExt = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined);
      await pc2.setRemoteDescription({sdp: offerNoExt, type: "offer"});
      await pc2.setLocalDescription();
      await pc1.setRemoteDescription(pc2.localDescription);

      // Renegotiate, but use |levelId| for something other than
      // ssrc-audio-level. pc2 should not throw.
      await pc1.setLocalDescription();
      const mungedReoffer = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", levelId);
      const twiceMungedReoffer = setExtmap(mungedReoffer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined);
      await pc2.setRemoteDescription({sdp: twiceMungedReoffer, type: "offer"});
    },

    async function checkAudioUnnegotiatedIdReuse3() {
      const pc1 = new RTCPeerConnection();
      const pc2 = new RTCPeerConnection();

      const stream = await navigator.mediaDevices.getUserMedia({audio: true});
      pc1.addTrack(stream.getTracks()[0]);
      pc2.addTrack(stream.getTracks()[0]);

      // Negotiate, but replace ssrc-audio-level with something pc2 won't
      // support in offer.
      await pc1.setLocalDescription();
      const levelId = getExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level");
      const mungedOffer = replaceExtUri(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", "fooba");
      await pc2.setRemoteDescription({sdp: mungedOffer, type: "offer"});
      await pc2.setLocalDescription();
      await pc1.setRemoteDescription(pc2.localDescription);

      // Renegotiate, and use levelId for something pc2 _will_ support.
      await pc1.setLocalDescription();
      const mungedReoffer = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", levelId);
      const twiceMungedReoffer = setExtmap(mungedReoffer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined);
      await pc2.setRemoteDescription({sdp: twiceMungedReoffer, type: "offer"});
    },

  ];

  runNetworkTest(async () => {
    for (const test of tests) {
      info(`Running test: ${test.name}`);
      await test();
      info(`Done running test: ${test.name}`);
    }
  });

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