Skip to content

Source: src/util/PerformanceMonitor.js

import DomSmith from '../../lib/dom/DomSmith.js';
import Looper from '../../lib/util/Looper.js';

/**
 * Displays a small overlay with real-time FPS & video rendering stats inside the VisionPlayer UI.
 * @exports module:src/util/PerformanceMonitor
 * @requires lib/dom/DomSmith
 * @requires lib/util/Looper
 * @author Frank Kudermann - alphanull
 * @version 1.0.0
 * @license MIT
 */
export default class PerformanceMonitor {

    /**
     * Reference to the VisionPlayer instance.
     * @type {module:src/core/Player}
     */
    #player;

    /**
     * Secret key only known to the player instance and initialized components.
     * Used to be able to restrict access to API methods in conjunction with secure mode.
     * @type {symbol}
     */
    #apiKey;

    /**
     * Text line with latest dropped frame info.
     * @type {string}
     */
    #qualityInfo = '';

    /**
     * Previously recorded totalVideoFrames value.
     * @type {number}
     */
    #lastTotal = 0;

    /**
     * Previously recorded droppedVideoFrames value.
     * @type {number}
     */
    #lastDropped = 0;

    /**
     * DomSmith instance holding the floating stats overlay.
     * @type {module:lib/dom/DomSmith}
     */
    #dom;

    /**
     * Render Loop Instance, used for updating the scrubber.
     * @type {module:lib/util/Looper}
     */
    #renderLoop;

    /**
     * ID for the stats loop.
     * @type {number}
     */
    #statsInterval;

    /**
     * Creates an instance of the PerformanceMonitor component.
     * @param {module:src/core/Player} player            Reference to the VisionPlayer instance.
     * @param {module:src/core/Player} parent            Reference to the parent instance.
     * @param {Object}                 [options]         Additional options.
     * @param {symbol}                 [options.apiKey]  Token for extended access to the player API.
     */
    constructor(player, parent, { apiKey }) {

        if (!player.initConfig('performanceMonitor', true)) return [false];

        this.#player = player;
        this.#apiKey = apiKey;

        this.#dom = new DomSmith({
            class: 'vip-performance-monitor',
            _ref: 'container',
            style: `
                position: absolute;
                top: 0;
                left: 0;
                font-family: monospace;
                font-size: 12px;
                background: rgba(0,0,0,0.6);
                color: #0f0;
                padding: 4px 6px;
                pointer-events: none;
                z-index: 9999;
                line-height: 1.4;
                white-space: pre;
            `
        }, player.dom.getElement(apiKey));

        this.#renderLoop = new Looper(() => {});
        this.#renderLoop.start();
        this.#statsInterval = setInterval(this.#update, 1000);

    }

    /**
     * Update loop, runs every 1 second.
     * Updates FPS and video quality stats (if supported).
     */
    #update = () => {

        // update FPS
        const fps = this.#renderLoop.getFPS();
        this.#dom.container.textContent = `FPS : ${fps}${this.#qualityInfo}`;

        // update dropped frames
        const video = this.#player.media.getElement(this.#apiKey);
        if (!video || typeof video.getVideoPlaybackQuality !== 'function') return;

        const q = video.getVideoPlaybackQuality(),
              total = q.totalVideoFrames,
              dropped = q.droppedVideoFrames,
              deltaTotal = total - this.#lastTotal,
              deltaDropped = dropped - this.#lastDropped,
              percent = deltaTotal > 0
                  ? (deltaDropped / deltaTotal * 100).toFixed(1)
                  : '0.0';

        this.#qualityInfo = `\nDrop: ${dropped} (${percent}%)`;
        this.#lastTotal = total;
        this.#lastDropped = dropped;

    };

    /**
     * Teardown and cleanup.
     */
    destroy() {

        clearInterval(this.#statsInterval);
        this.#renderLoop.destroy();
        this.#dom.destroy();
        this.#player = this.#dom = this.#apiKey = null;

    }
}