chromium/chrome/test/data/webrtc/peerconnection_replacetrack.js

/**
 * Copyright 2017 The Chromium Authors
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

/**
 * The resolver has a |promise| that can be resolved or rejected using |resolve|
 * or |reject|.
 */
// TODO(hbos): Remove when no longer needed. https://crbug.com/793808
class Resolver {
  constructor() {
    let promiseResolve;
    let promiseReject;
    this.promise = new Promise(function(resolve, reject) {
      promiseResolve = resolve;
      promiseReject = reject;
    });
    this.resolve = promiseResolve;
    this.reject = promiseReject;
  }
}

// TODO(hbos): Make this an external/wpt/webrtc/ test when video elements are
// updated by received webrtc streams in content_shell. https://crbug.com/793808
async function testRTCRtpSenderReplaceTrackSendsNewVideoTrack() {
  const redCanvas = document.getElementById('redCanvas');
  const redCanvasStream = redCanvas.captureStream(10);
  const blueCanvas = document.getElementById('blueCanvas');
  const blueCanvasStream = blueCanvas.captureStream(10);
  const remoteVideo = document.getElementById('remote-view');
  const remoteVideoCanvas = document.getElementById('whiteCanvas');

  const caller = new RTCPeerConnection();
  const callee = new RTCPeerConnection();

  // Connect and send "redCanvas" to callee.
  const sender = caller.addTrack(redCanvasStream.getTracks()[0],
                                 redCanvasStream);
  const connectPromise = connect(caller, callee);
  const trackEvent = await eventAsAsyncFunction(callee, 'ontrack');
  remoteVideo.srcObject = trackEvent.streams[0];
  await connectPromise;

  // Ensure a red frame is sent by redrawing the canvas while polling.
  function fillRedCanvas() { fillCanvas(redCanvas, 'red'); }
  let receivedColor = await pollNextVideoColor(
      fillRedCanvas, remoteVideo, remoteVideoCanvas);
  if (receivedColor != 'red')
    throw new Error('Expected red, but received: ' + receivedColor);

  // Send "blueCanvas" to callee using the existing sender.
  await sender.replaceTrack(blueCanvasStream.getTracks()[0]);

  // Ensure a blue frame is sent by redrawing the canvas while polling.
  function fillBlueCanvas() { fillCanvas(blueCanvas, 'blue'); }
  receivedColor = await pollNextVideoColor(
      fillBlueCanvas, remoteVideo, remoteVideoCanvas);
  if (receivedColor != 'blue')
    throw new Error('Expected blue, but received: ' + receivedColor);

  return logAndReturn('test-passed');
}

// Internals.

/** @private */
async function connect(caller, callee) {
  caller.onicecandidate = (e) => {
    if (e.candidate)
      callee.addIceCandidate(new RTCIceCandidate(e.candidate));
  }
  callee.onicecandidate = (e) => {
    if (e.candidate)
      caller.addIceCandidate(new RTCIceCandidate(e.candidate));
  }
  let offer = await caller.createOffer();
  await caller.setLocalDescription(offer);
  await callee.setRemoteDescription(offer);
  let answer = await callee.createAnswer();
  await callee.setLocalDescription(answer);
  return caller.setRemoteDescription(answer);
}

/**
 * Makes the next |object[eventname]| event resolve the returned promise with
 * the event argument and resets the event handler to null.
 * @private
 */
async function eventAsAsyncFunction(object, eventname) {
  const resolver = new Resolver();
  object[eventname] = e => {
    object[eventname] = null;
    resolver.resolve(e);
  }
  return resolver.promise;
}

/**
 * Updates the canvas, filling it with |color|, e.g. 'red', 'lime' or 'blue'.
 * @private
 */
function fillCanvas(canvas, color) {
  const canvasContext = canvas.getContext('2d');
  canvasContext.fillStyle = color;
  canvasContext.fillRect(0, 0, canvas.width, canvas.height);
}

/**
 * Gets the dominant color of the center of the canvas, meaning the color that
 * is closest to that pixel's color amongst: 'black', 'white', 'red', 'lime' and
 * 'blue'.
 * @private
 */
function getDominantCanvasColor(canvas) {
  const colorData = canvas.getContext('2d').getImageData(
      Math.floor(canvas.width / 2), Math.floor(canvas.height / 2), 1, 1).data;

  const dominantColors = [
    { name: 'black', colorData: [0, 0, 0] },
    { name: 'white', colorData: [255, 255, 255] },
    { name: 'red', colorData: [255, 0, 0] },
    { name: 'lime', colorData: [0, 255, 0] },
    { name: 'blue', colorData: [0, 0, 255] },
  ];
  function getColorDistanceSquared(colorData1, colorData2) {
    const colorDiff = [ colorData2[0] - colorData1[0],
                        colorData2[1] - colorData1[1],
                        colorData2[2] - colorData1[2] ];
    return colorDiff[0] * colorDiff[0] +
           colorDiff[1] * colorDiff[1] +
           colorDiff[2] * colorDiff[2];
  }
  let dominantColor = dominantColors[0];
  let dominantColorDistanceSquared =
      getColorDistanceSquared(dominantColor.colorData, colorData);
  for (let i = 1; i < dominantColors.length; ++i) {
    const colorDistanceSquared =
        getColorDistanceSquared(dominantColors[i].colorData, colorData);
    if (colorDistanceSquared < dominantColorDistanceSquared) {
      dominantColor = dominantColors[i];
      dominantColorDistanceSquared = colorDistanceSquared;
    }
  }
  return dominantColor.name;
}

/**
 * Polls the video's dominant color (see getDominantCanvasColor()) until a color
 * different than the initial color is retrieved, resolving the returned promise
 * with the new color name. The video color is read by drawing the video onto a
 * canvas and reading the color of the canvas. Before each time the color is
 * polled, doWhilePolling() is invoked.
 * @private
 */
async function pollNextVideoColor(doWhilePolling, video, canvas) {
  canvas.getContext('2d').drawImage(video, 0, 0);
  const initialColor = getDominantCanvasColor(canvas);
  const resolver = new Resolver();
  function checkColor() {
    doWhilePolling();
    canvas.getContext('2d').drawImage(video, 0, 0);
    const color = getDominantCanvasColor(canvas);
    if (color != initialColor) {
      resolver.resolve(color);
      return;
    }
    setTimeout(checkColor, 0);
  }
  setTimeout(checkColor, 0);
  return resolver.promise;
}