chromium/content/test/data/media/webrtc_test_utilities.js

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

// These must match with how the video and canvas tags are declared in html.
const VIDEO_TAG_WIDTH = 320;
const VIDEO_TAG_HEIGHT = 240;

// Fake video capture background green is of value 135.
const COLOR_BACKGROUND_GREEN = 135;

// Logs a success message to the console.
function logSuccess() {
  console.log('Test Success');
}

function detectVideoPlayingWithExpectedResolution(
    videoElementName, video_width, video_height, alignment) {
  return detectVideo(
      videoElementName, function(pixels, previous_pixels, videoElement) {
        return hasExpectedResolution(
                   videoElement, video_width, video_height, alignment) &&
            isVideoPlaying(pixels, previous_pixels);
      });
}

function detectVideoPlaying(videoElementName) {
  return detectVideo(videoElementName, isVideoPlaying);
}

function detectVideoWithDimensionPlaying(
    videoElementName, video_width, video_height) {
  return detectVideoWithDimension(
      videoElementName, isVideoPlaying, video_width, video_height);
}

function detectVideoStopped(videoElementName) {
  return detectVideo(videoElementName, function(pixels, previous_pixels) {
    return !isVideoPlaying(pixels, previous_pixels);
  });
}

function detectBlackVideo(videoElementName) {
  return detectVideo(videoElementName, function(pixels, previous_pixels) {
    return isVideoBlack(pixels);
  });
}

function detectUniformColorVideoWithDimensionPlaying(
    videoElementName, video_width, video_height) {
  return detectVideoWithDimension(
      videoElementName, function(pixels, previous_pixels) {
        return isVideoPlaying(pixels, previous_pixels) &&
            arePixelsUniformColor(pixels) &&
            arePixelsUniformColor(previous_pixels);
      }, video_width, video_height);
}

function detectVideo(videoElementName, predicate) {
  return detectVideoWithDimension(
      videoElementName, predicate, VIDEO_TAG_WIDTH, VIDEO_TAG_HEIGHT);
}

function detectVideoWithDimension(
    videoElementName, predicate, video_width, video_height) {
  console.log('Looking at video in element ' + videoElementName);

  return new Promise((resolve, reject) => {
    var width = video_width;
    var height = video_height;
    var videoElement = $(videoElementName);
    var oldPixels = [];
    var startTimeMs = new Date().getTime();
    var waitVideo = setInterval(function() {
      var canvas = $(videoElementName + '-canvas');
      if (canvas == null) {
        console.log(
            'Waiting for ' + videoElementName + '-canvas' +
            ' to appear');
        return;
      }
      var context = canvas.getContext('2d', {willReadFrequently: true});
      context.drawImage(videoElement, 0, 0);
      var pixels = context.getImageData(0, 0, width, height / 3).data;

      // Check that there is an old and a new picture with the same size to
      // compare and use the function |predicate| to detect the video state in
      // that case.
      // There's a failure(?) mode here where the video generated claims to
      // have size 2x2. Don't consider that a valid video.
      if (oldPixels.length == pixels.length &&
          predicate(pixels, oldPixels, videoElement)) {
        console.log('Done looking at video in element ' + videoElementName);
        console.log('DEBUG: video.width = ' + videoElement.videoWidth);
        console.log('DEBUG: video.height = ' + videoElement.videoHeight);
        clearInterval(waitVideo);
        resolve({
          'width': videoElement.videoWidth,
          'height': videoElement.videoHeight
        });
      }
      oldPixels = pixels;
      var elapsedTime = new Date().getTime() - startTimeMs;
      if (elapsedTime > 3000) {
        startTimeMs = new Date().getTime();
        console.log(
            'Still waiting for video to satisfy ' + predicate.toString());
        console.log('DEBUG: video.width = ' + videoElement.videoWidth);
        console.log('DEBUG: video.height = ' + videoElement.videoHeight);
      }
    }, 200);
  });
}

function waitForConnectionToStabilize(peerConnection) {
  return new Promise((resolve, reject) => {
    peerConnection.onsignalingstatechange =
        function(event) {
          if (peerConnection.signalingState == 'stable') {
            peerConnection.onsignalingstatechange = null;
            resolve();
          }
        }
  });
}

function waitForConnectionToStabilizeIfNeeded(peerConnection) {
  return new Promise((resolve, reject) => {
    if (peerConnection.signalingState == 'stable') {
      resolve();
      return;
    }
    return waitForConnectionToStabilize(peerConnection).then(resolve);
  });
}

function hasExpectedResolution(
    videoElement, expected_width, expected_height, alignment) {
  if (videoElement.videoWidth == expected_width &&
      videoElement.videoHeight == expected_height) {
    return true;
  }

  if (!alignment)
    return false;

  expected_width -= expected_width % alignment;
  expected_height -= expected_height % alignment;
  return videoElement.videoWidth == expected_width &&
      videoElement.videoHeight == expected_height;
}

// This very basic video verification algorithm will be satisfied if any
// pixels are changed.
function isVideoPlaying(pixels, previousPixels) {
  for (var i = 0; i < pixels.length; i++) {
    if (pixels[i] != previousPixels[i]) {
      return true;
    }
  }
  return false;
}

// Checks if the frame is black. |pixels| is in RGBA (i.e. pixels[0] is the R
// value for the first pixel).
function isVideoBlack(pixels) {
  var threshold = 20;
  var accumulatedLuma = 0;
  for (var i = 0; i < pixels.length; i += 4) {
    // Ignore the alpha channel.
    accumulatedLuma += rec709Luma_(pixels[i], pixels[i + 1], pixels[i + 2]);
    if (accumulatedLuma > threshold * (i / 4 + 1))
      return false;
  }
  return true;
}

// |pixels| is in RGBA (i.e. pixels[0] is the R value for the first pixel).
function arePixelsUniformColor(pixels) {
  if (pixels.length < 4) {
    throw new Error('expected at least one pixel');
  }
  var reference_r = pixels[0];
  var reference_g = pixels[1];
  var reference_b = pixels[2];
  var reference_a = pixels[3];
  for (var i = 4; i < pixels.length; i += 4) {
    if (pixels[i + 0] != reference_r) {
      console.log('red value at pixel ' + i + ' does not match reference');
      return false;
    }
    if (pixels[i + 1] != reference_g) {
      console.log('green value at pixel ' + i + ' does not match reference');
      return false;
    }
    if (pixels[i + 2] != reference_b) {
      console.log('blue value at pixel ' + i + ' does not match reference');
      return false;
    }
    if (pixels[i + 3] != reference_a) {
      console.log('alpha value at pixel ' + i + ' does not match reference');
      return false;
    }
  }
  return true;
}

// Checks if the given color is within 1 value away from COLOR_BACKGROUND_GREEN.
function isAlmostBackgroundGreen(color) {
  if (Math.abs(color - COLOR_BACKGROUND_GREEN) > 1)
    return false;
  return true;
}

// Use Luma as in Rec. 709: Y′709 = 0.2126R + 0.7152G + 0.0722B;
// See http://en.wikipedia.org/wiki/Rec._709.
function rec709Luma_(r, g, b) {
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

// This function matches |left| and |right| and fails the test if the
// values don't match using normal javascript equality (i.e. the hard
// types of the operands aren't checked).
function assertEquals(expected, actual) {
  if (actual != expected) {
    throw new Error('expected \'' + expected + '\', got \'' + actual + '\'.');
  }
}

function assertNotEquals(expected, actual) {
  if (actual === expected) {
    throw new Error('expected \'' + expected + '\', got \'' + actual + '\'.');
  }
}

function assertTrue(booleanExpression, description) {
  if (!booleanExpression) {
    throw new Error(description);
  }
}

function assertFalse(booleanExpression, description) {
  if (!!booleanExpression) {
    throw new Error(description);
  }
}