drawBufferInfoCanvasBar_(ctx, colors, y, h, scaleFactor, activeI) {
    // Define the muted colors. These are used to signify the end of buffered
    // periods.
    const mutedColors = colors.filter((color) => {
      return color.replaceAll('F', 'A').replaceAll('0', '4');
    });

    /**
     * Converts a time value from seconds to screen position.
     * @param {number} time
     * @return {number}
     */
    const timeToPosition = (time) => {
      return Math.round((time - this.video_.currentTime) * scaleFactor +
          (this.canvas_.width / 2));
    };

    // Choose text drawing settings.
    const fontSize = Math.floor(h / 4);
    ctx.textAlign = 'center';
    ctx.font = 'bold ' + fontSize + 'px serif';
    ctx.textBaseline = 'bottom';
    const longFormText = scaleFactor > fontSize * 4;

    // Draw updates.
    for (const update of this.updates_) {
      let s = timeToPosition(update.start);
      let e = timeToPosition(update.end);
      if (e >= 0 && s < this.canvas_.width) {
        s = Math.max(s, 0);
        e = Math.min(e, this.canvas_.width);
        ctx.fillStyle = '#FFFFFF';
        // Note that these are drawn at reduced opacity, so that multiple
        // updates in the same time range (e.g. video and audio) will visibly
        // overlap.
        ctx.globalAlpha = 0.3;
        ctx.fillRect(s, y, e - s, h);
        ctx.globalAlpha = 1;

        // Also draw text labels, to show what type of segment this was.
        let text = update.contentType.toUpperCase();
        if (update.isMuxed) {
          text = 'AUDIO&VIDEO';
        }
        if (!longFormText) {
          if (update.isMuxed) {
            text = 'AV';
          } else {
            text = text[0];
          }
        }
        const textX = s + (e - s) / 2;
        let textY = y + h;
        switch (update.contentType) {
          case 'video':
            textY -= fontSize * 2;
            break;
          case 'audio':
            textY -= fontSize;
            break;
          // Text is at the bottom.
        }
        ctx.fillText(text, textX, textY);
      }
    }

    // Draw buffered ranges.
    const gapDetectionThreshold =
        this.player_.getConfiguration().streaming.gapDetectionThreshold;
    for (let i = 0; i < this.video_.buffered.length; i++) {
      let s = timeToPosition(this.video_.buffered.start(i));
      let e = timeToPosition(this.video_.buffered.end(i));
      if (e >= 0 && s < this.canvas_.width) {
        s = Math.max(s, 0);
        e = Math.min(e, this.canvas_.width);
        const colorI = (i - activeI + this.colorIOffset_ +
            10000 * colors.length) % colors.length;
        const barHeight = (h - 3 * fontSize) / colors.length;
        const barY = y + (colorI * barHeight);

        // Draw the bar as a richer color.
        ctx.fillStyle = colors[colorI];
        ctx.fillRect(s, barY, e - s, barHeight);

        // Draw the gap detection threshold as a more muted color.
        const gdtS = Math.max(s, timeToPosition(
            this.video_.buffered.end(i) - gapDetectionThreshold));
        ctx.fillStyle = mutedColors[colorI];
        ctx.fillRect(gdtS, barY, e - gdtS, barHeight);
      }
    }
  }
@param {!CanvasRenderingContext2D} ctx
@param {!Array<string>} colors
@param {number} y
@param {number} h
@param {number} scaleFactor Measured in pixels per second.
@param {number} activeI
@private