Files
shaka-player/demo/visualizer.js
T
theodab e8e28073c1 chore(demo): Removed demo localization system (#5665)
A localization system was added to the demo, in preparation for future localizations of the demo page.
However, such further localizations were never added, and it seems likely that they will never be added.
Given that it will almost definitely never be used, the localization system has just become an annoyance that makes adding new features to the demo more difficult, so this removes the system entirely.
2023-09-25 02:58:39 -07:00

336 lines
11 KiB
JavaScript

/*! @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;