Article WebRTC dynamic downscale

by jib1

HTML

<script src="https://webrtchacks.github.io/adapter/adapter-latest.js"></script>
Height <input id="heightNumber" type="number" min="2" max="1024" value="480" size="5"><input id="heightRange" type="range" min="2" max="1024" value="480">Max bitrate <input id="rateNumber" type="number" min="2" max="9000000" value="2000000" size="9">
<input id="rateRange" type="range" min="2" max="9000000" value="2000000"><br><br>
<table>
  <tr>
    <td>Receiver</td>
    <td>Sender</td>
  </tr>
  <tr>
    <td id="v2info">0x0</td>
    <td id="v1info">0x0</td>
  </tr>
  <tr>
    <td id="v2brinfo">0 bps</td>
    <td id="v1brinfo">0 bps</td>
  </tr>
  <tr>
    <td><video id="v2" autoplay></video></td>
    <td><video id="v1" autoplay muted></video></td>
  </tr>
</table>
<div id="div"></div>

CSS

body { white-space: nowrap; font-family: monospace; }

JavaScript

const console = { log: msg => div.innerHTML += msg + "<br>" };
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
const update = (div, msg) => div.innerHTML = msg;

const pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection();

heightRange.oninput = () => apply(heightNumber.value = heightRange.value, rateNumber.value);
heightNumber.oninput = () => apply(heightRange.value = heightNumber.value, rateNumber.value);
rateRange.oninput = () => apply(heightNumber.value, rateNumber.value = rateRange.value);
rateNumber.oninput = () => apply(heightNumber.value, rateRange.value = rateNumber.value);

let track, applying = false, height, range;

async function apply(h, r) {
  height = h;
  rate = r;
  if (applying) return;
  try {
    applying = true;
    do {
      h = height;
      r = rate;
      const [sender] = pc1.getSenders();
      const ratio = sender.track.getSettings().height / height;
      const params = sender.getParameters();
      params.encodings[0].scaleResolutionDownBy = Math.max(ratio, 1);
      params.encodings[0].maxBitrate = rate;
      await sender.setParameters(params);
    } while (h != height);
  } catch (e) {
    console.log(e);
  } finally {
    applying = false;
  }
}

(async () => {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({video: {height: 480}});
    await setVideo(v1, v1info, stream);
    const sender = pc1.addTrack(stream.getVideoTracks()[0], stream);
    await new Promise(r => pc1.oniceconnectionstatechange =
        () => pc1.iceConnectionState == "connected" && r());
    heightRange.max = heightNumber.max = sender.track.getSettings().height;
    heightRange.value = heightNumber.value = heightNumber.max * 3 / 4;
    apply(heightNumber.value, rateNumber.value);
  } catch (e) {
    console.log(e);
  }
})();

const dimensions = v => v.videoWidth +"x"+ v.videoHeight;
const bitrate = (n, o) => (n - (o.old || 0)) * 8 + (o.old = n) * 0;

async function setVideo(v, vinfo, stream) {
  v.srcObject =...