function WebDriverScreenshotMiddlewareFactory(launcher) {
  return screenshotMiddleware;

  /**
   * Extract URL params from the request.
   *
   * @param {express.Request} request
   * @return {!Object<string, string>}
   */
  function getParams(request) {
    // This can be null for manually-connected browsers.
    if (!request._parsedUrl.search) {
      return {};
    }
    return util.parseQueryParams(request._parsedUrl.search);
  }

  /**
   * Find the browser associated with the "id" parameter of the request.
   * This ID was assigned by Karma when the browser was launched, and passed to
   * the web server from the Jasmine tests.
   *
   * If the browser is not found, this function will return null.
   *
   * @param {?string} id
   * @return {karma.Launcher.Browser|null}
   */
  function getBrowser(id) {
    if (!id) {
      // No ID parameter?  No such browser.
      return null;
    }
    const browser = launcher._browsers.find((b) => b.id == id);
    if (!browser) {
      return null;
    }
    return browser;
  }

  /**
   * @param {?karma.Launcher.Browser} browser
   * @return {wd.remote|null} A WebDriver client, an object from the "wd"
   *   package, created by "wd.remote()".
   */
  function getWebDriverClient(browser) {
    if (!browser) {
      // If we didn't launch the browser, then there's definitely no WebDriver
      // client for it.
      return null;
    }

    // If this browser was launched by the WebDriver launcher, the launcher's
    // browser object has a WebDriver client in the "browser" field.  Yes, this
    // looks weird.
    const webDriverClient = browser.browser;

    // To make sure we have an actual WebDriver client and to screen out other
    // launchers who may also have a "browser" field in their browser object,
    // we check to make sure it has a screenshot method.
    if (webDriverClient && webDriverClient.takeScreenshot) {
      return webDriverClient;
    }
    return null;
  }

  /**
   * @param {karma.Launcher.Browser.spec} spec
   * @param {wd.remote} webDriverClient A WebDriver client, an object from the
   *   "wd" package, created by "wd.remote()".
   * @return {!Promise<!Buffer>} A Buffer containing a PNG screenshot
   */
  function getScreenshot(spec, webDriverClient) {
    return new Promise((resolve, reject) => {
      webDriverClient.takeScreenshot((error, pngBase64) => {
        if (error) {
          reject(error);
        } else if (pngBase64.error) {
          // In some failure cases, pngBase64 is an object with "error",
          // "message", and "stacktrace" fields.  This happens, for example,
          // with a timeout from the screenshot command.  This is not an
          // expected situation, so log it.  The extra newlines keep this from
          // being overwritten on the terminal when running tests against many
          // browsers at once.
          console.log('\n\nUnexpected screenshot failure:\n' +
              `  Error: ${JSON.stringify(pngBase64)}\n` +
              `  WebDriver spec: ${JSON.stringify(spec)}\n\n\n`);
          reject(pngBase64);
        } else {
          // Convert the screenshot to a binary buffer.
          resolve(Buffer.from(pngBase64, 'base64'));
        }
      });
    });
  }

  /**
   * Take a screenshot, write it to disk, and diff it against the old one.
   * Write the diff to disk, as well.
   *
   * @param {karma.Launcher.Browser} browser
   * @param {!Object<string, string>} params
   * @return {!Promise<number>} A similarity score between 0 and 1.
   */
  async function diffScreenshot(browser, params) {
    const webDriverClient = getWebDriverClient(browser);
    if (!webDriverClient) {
      throw new Error('No screenshot support!');
    }

    /** @type {!Buffer} */
    const fullPageScreenshotData =
        await getScreenshot(browser.spec, webDriverClient);

    // Crop the screenshot to the dimensions specified in the test.
    // Jimp is picky about types, so convert these strings to numbers.
    const x = parseFloat(params.x);
    const y = parseFloat(params.y);
    const width = parseFloat(params.width);
    const height = parseFloat(params.height);
    const bodyWidth = parseFloat(params.bodyWidth);
    const bodyHeight = parseFloat(params.bodyHeight);

    /** @type {!Jimp.image} */
    const fullScreenshot = (await Jimp.read(fullPageScreenshotData));

    // Because WebDriver may screenshot at a different resolution than we
    // saw in JS, convert the crop region coordinates to the screenshot scale,
    // then crop, then resize.  This order produces the most accurate cropped
    // screenshot.

    // Scaling by height breaks everything on Android, which has screenshots
    // that are taller than expected based on the body size.  So use width only.
    const scale = fullScreenshot.bitmap.width / bodyWidth;

    /** @type {!Jimp.image} */
    const newScreenshot = fullScreenshot.clone()
        .crop(
            // Sub-pixel rendering in browsers makes this much trickier than you
            // might expect.  Offsets are not necessarily integers even before
            // we scale them, but the image has been quantized into pixels at
            // that scale.  Experimentation with different rounding methods has
            // led to the conclusion that rounding up is the only way to get
            // consistent results.
            Math.ceil(x * scale),
            Math.ceil(y * scale),
            Math.ceil(width * scale),
            Math.ceil(height * scale))
        .resize(width, height, Jimp.RESIZE_BICUBIC);

    // Get the WebDriver spec (including browser name, platform, etc)
    const spec = browser.spec;
    // Compute the folder for the screenshots for this platform.
    const baseFolder = `${__dirname}/test/test/assets/screenshots`;
    let folder = `${baseFolder}/${spec.browserName}`;
    if (spec.platform) {
      folder += `-${spec.platform}`;
    }

    const oldScreenshotPath = `${folder}/${params.name}.png`;
    const fullScreenshotPath = `${folder}/${params.name}.png-full`;
    const newScreenshotPath = `${folder}/${params.name}.png-new`;
    const diffScreenshotPath = `${folder}/${params.name}.png-diff`;

    // Write the full screenshot to disk.  This should be done early in case a
    // later stage fails and we need to analyze what happened.
    fs.mkdirSync(folder, {recursive: true});
    fs.writeFileSync(
        fullScreenshotPath, await fullScreenshot.getBufferAsync('image/png'));

    // Write the cropped screenshot to disk next.  This is used in review
    // changes and to update the "official" screenshot when needed.
    fs.writeFileSync(
        newScreenshotPath, await newScreenshot.getBufferAsync('image/png'));

    /** @type {!Jimp.image} */
    let oldScreenshot;
    if (!fs.existsSync(oldScreenshotPath)) {
      // If the "official" screenshot doesn't exist yet, create a blank image
      // in memory.
      oldScreenshot = new Jimp(width, height);
    } else {
      oldScreenshot = await Jimp.read(oldScreenshotPath);
    }

    // Compare the new screenshot to the old one and produce a diff image.
    // Initially, the image data will be raw pixels, 4 bytes per pixel.
    // The threshold parameter affects the sensitivity of individual pixel
    // comparisons.  This diff is only used for visual review, not for
    // automated similarity checks, so the threshold setting is not so critical
    // as it used to be.
    const threshold = 0.10;
    const diff = Jimp.diff(oldScreenshot, newScreenshot, threshold);

    // Write the diff to disk.  This is used to review when there are changes.
    const fullSizeDiff =
        diff.image.clone().resize(width, height, Jimp.RESIZE_BICUBIC);
    fs.writeFileSync(
        diffScreenshotPath, await fullSizeDiff.getBufferAsync('image/png'));

    // Compare with a structural similarity algorithm.  This produces a
    // similarity score that we will use to pass or fail the test.
    const ssimResult = ssim(oldScreenshot.bitmap, newScreenshot.bitmap);
    return ssimResult.mssim;  // A score between 0 and 1.
  }

  /**
   * This function is the middleware.  It gets request and response objects and
   * a next() callback which passes control off to the next middleware in the
   * system.  This is similar to how expressjs works.
   *
   * @param {karma.MiddleWare.Request} request
   * @param {karma.MiddleWare.Response} response
   * @param {function()} next
   */
  async function screenshotMiddleware(request, response, next) {
    const pathname = request._parsedUrl.pathname;

    if (pathname == '/screenshot/isSupported') {
      const params = getParams(request);
      const browser = getBrowser(params.id);
      const webDriverClient = getWebDriverClient(browser);

      let isSupported = false;
      if (webDriverClient) {
        // Some platforms in our Selenium grid can't take screenshots.  We don't
        // have a good way to check for this in the platform capabilities
        // reported by Selenium, so we have to take a screenshot to find out.
        // The result is cached for the sake of performance.
        if (webDriverClient.canTakeScreenshot === undefined) {
          try {
            await getScreenshot(browser.spec, webDriverClient);
            webDriverClient.canTakeScreenshot = true;
          } catch (error) {
            webDriverClient.canTakeScreenshot = false;
          }
        }

        isSupported = webDriverClient.canTakeScreenshot;
      }

      response.setHeader('Content-Type', 'application/json');
      response.end(JSON.stringify(isSupported));
    } else if (pathname == '/screenshot/diff') {
      const params = getParams(request);
      const browser = getBrowser(params.id);
      if (!browser) {
        response.writeHead(404);
        response.end('No such browser!');
        return;
      }

      // Check the URL parameters.
      const requiredParams = [
        'x', 'y', 'width', 'height', 'bodyWidth', 'bodyHeight', 'name',
      ];
      for (const k of requiredParams) {
        if (!params[k]) {
          response.writeHead(400);
          response.end(`Screenshot param ${k} is missing!`);
          return;
        }
      }

      // To avoid creating an open HTTP endpoint where anyone can write to any
      // path on the filesystem, only accept alphanumeric names (plus
      // underscore and dash).  No colons, periods, forward slashes, or
      // backslashes should ever be added to this regex, as any of those could
      // be used on some platform to write outside of the screenshots folder.
      if (!params.name.match(/[a-zA-Z0-9_-]+/)) {
        response.writeHead(400);
        response.end(`Screenshot name not valid: "${params.name}"`);
        return;
      }

      try {
        const pixelsChanged = await diffScreenshot(browser, params);
        response.setHeader('Content-Type', 'application/json');
        response.end(JSON.stringify(pixelsChanged));
      } catch (error) {
        console.error(error);
        response.writeHead(500);
        response.end('Screenshot error: ' + JSON.stringify(error));
      }
    } else {
      // Requests for paths that we don't handle are passed on to the next
      // middleware in the system.
      next();
    }
  }
}
This is a factory for a "middleware" component that handles requests in
Karma's webserver.  This one will let us take screenshots of browsers
connected through WebDriver.  The factory uses Karma's dependency injection
system to get a reference to the launcher module, which we will use to get
access to the remote browsers.

@param {karma.Launcher} launcher
@return {karma.Middleware}