UNPKG

shaka-player

Version:
336 lines (289 loc) 10.9 kB
/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shakaDemo.Visualizer'); goog.require('shakaDemo.BoolInput'); /** * Manages a visualizer that shows the buffering progress of the player. */ shakaDemo.Visualizer = class { /** * @param {!HTMLCanvasElement} canvas * @param {!HTMLElement} div * @param {!HTMLElement} screenshotDiv * @param {!HTMLElement} controlsDiv * @param {!HTMLVideoElement} video * @param {!shaka.Player} player */ constructor(canvas, div, screenshotDiv, controlsDiv, video, player) { /** @private {!HTMLCanvasElement} */ this.canvas_ = canvas; /** @private {!HTMLElement} */ this.div_ = div; this.active = false; /** @private {!HTMLElement} */ this.screenshotDiv_ = screenshotDiv; /** @private {!HTMLVideoElement} */ this.video_ = video; /** @private {!shaka.Player} */ this.player_ = player; /** @private {boolean} */ this.takeAutoScreenshots_ = false; /** @private {shaka.util.Timer} */ this.timer_ = new shaka.util.Timer(() => { this.ageUpdates_(); this.updateCanvas_(); this.takeAutomaticScreenshots_(); }); /** @private {number} */ this.lastCurrentTime_ = 0; /** @private {number} */ this.colorIOffset_ = 0; /** @private {boolean} */ this.autoScreenshotTaken_ = false; /** * @private {!Array.<{ * age: number, * start: number, * end: number, * contentType: string, * }>} */ this.updates_ = []; // Listen for when new buffers are appended. player.addEventListener('segmentappended', (event) => { const start = /** @type {number} */ (event['start']); const end = /** @type {number} */ (event['end']); const contentType = /** @type {string} */ (event['contentType']); this.updates_.push({age: 0, start, end, contentType}); }); // Add controls. const inputContainer = new shakaDemo.InputContainer( controlsDiv, null, shakaDemo.InputContainer.Style.VERTICAL, null); inputContainer.addRow(null, null); this.screenshotButton_ = document.createElement('button'); inputContainer.latestElementContainer.appendChild(this.screenshotButton_); this.screenshotButton_.textContent = 'Take Screenshot'; this.screenshotButton_.classList.add('mdl-button'); this.screenshotButton_.classList.add('mdl-button--colored'); this.screenshotButton_.classList.add('mdl-js-button'); this.screenshotButton_.classList.add('mdl-js-ripple-effect'); this.screenshotButton_.addEventListener('click', () => { this.takeScreenshot_(); }); inputContainer.addRow('Take Screenshot On Stall', null); /** @private {!shakaDemo.BoolInput} */ this.autoScreenshotToggle_ = new shakaDemo.BoolInput( inputContainer, 'Take Screenshot On Stall', (input) => { this.takeAutoScreenshots_ = input.checked; }); } /** Starts the visualizer updating, and un-hides it. */ start() { this.timer_.tickEvery(shakaDemo.Visualizer.updateFrequency_); this.div_.classList.remove('hidden'); // Start out as though an automatic screenshot had been taken, so that it // doesn't take a screenshot during the initial buffering. this.autoScreenshotTaken_ = true; } /** Stops the visualizer updating, and hides it. */ stop() { this.timer_.stop(); this.div_.classList.add('hidden'); this.updates_ = []; } /** * @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 */ 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. // They also fade away further over time, until they are gone entirely. ctx.globalAlpha = 0.1 + 0.2 * (1 - update.age / shakaDemo.Visualizer.maxUpdateAge_); 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 (!longFormText) { 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); } } } /** @private */ takeAutomaticScreenshots_() { if (this.video_.readyState <= 2) { // When the video stops, due to a lack of buffered material, take a // screenshot automatically, so that this information will not be lost // if this is a freeze. if (!this.autoScreenshotTaken_ && this.takeAutoScreenshots_) { this.takeScreenshot_(); this.autoScreenshotTaken_ = true; } } else { this.autoScreenshotTaken_ = false; } } /** @private */ ageUpdates_() { for (const update of this.updates_) { update.age += shakaDemo.Visualizer.updateFrequency_; } this.updates_ = this.updates_.filter((update) => { return update.age < shakaDemo.Visualizer.maxUpdateAge_; }); } /** @private */ takeScreenshot_() { shaka.util.Dom.removeAllChildren(this.screenshotDiv_); // Make the screenshot. const screenshotCanvas = /** @type {!HTMLCanvasElement} */ ( document.createElement('canvas')); screenshotCanvas.width = this.canvas_.width; screenshotCanvas.height = this.canvas_.height; const ctx = /** @type {CanvasRenderingContext2D} */ ( screenshotCanvas.getContext('2d')); ctx.drawImage(this.canvas_, 0, 0); this.screenshotDiv_.appendChild(screenshotCanvas); } /** @private */ updateCanvas_() { // Make sure the size of the canvas data is the size of the canvas element. this.canvas_.width = this.canvas_.offsetWidth; this.canvas_.height = this.canvas_.offsetHeight; // Get the context. const ctx = /** @type {CanvasRenderingContext2D} */ ( this.canvas_.getContext('2d')); ctx.imageSmoothingEnabled = false; // Make a black background. ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, this.canvas_.width, this.canvas_.height); // Define the colors. // Each buffered range is represented by a bar of a solid color, so that // gaps in the presentation are more visually obvious; the two bars // representing the two ranges will be at different y-positions and be // drawn with different colors. // These colors are, in order: red, green, and blue. const colors = ['#FF0000', '#00FF00', '#0000FF']; // Determine what buffered range is centered, so that colors can remain // consistent between frames. let activeI = -1; let lastActiveI = -1; const lastTime = this.lastCurrentTime_; const currentTime = this.video_.currentTime; const buffered = this.video_.buffered; for (let i = 0; i < buffered.length; i++) { if (lastTime >= buffered.start(i) && lastTime <= buffered.end(i)) { lastActiveI = i; } if (currentTime >= buffered.start(i) && currentTime <= buffered.end(i)) { activeI = i; } } this.lastCurrentTime_ = currentTime; // Determine if the video has moved between two buffered ranges. If so, // update the offset so that the colors remain consistent. if (activeI != -1 && lastActiveI != -1) { this.colorIOffset_ += activeI - lastActiveI; } // Draw bars at various zoom levels. const scaleFactors = [50, 5]; const overallHeight = this.canvas_.height / scaleFactors.length; for (let i = 0; i < scaleFactors.length; i++) { const h = overallHeight * 0.75; const y = (overallHeight * i) + ((overallHeight - h) / 2); this.drawBufferInfoCanvasBar_( ctx, colors, y, h, scaleFactors[i], activeI); } // Draw the indicator tick at the center. ctx.fillStyle = '#FFFFFF'; const tickWidth = 2; ctx.fillRect( (this.canvas_.width / 2) - (tickWidth / 2), 0, tickWidth, this.canvas_.height); } }; /** * How many seconds an update event should be displayed. * @const {number} */ shakaDemo.Visualizer.maxUpdateAge_ = 20; /** * How often the visualizer should update, in seconds. * @const {number} */ shakaDemo.Visualizer.updateFrequency_ = 0.05;