Source: lib/polyfill/videoframecallback.js

/*! @license
 * Shaka Player
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

goog.provide('shaka.polyfill.VideoFrameCallback');

goog.require('shaka.polyfill');


/**
 * @summary A polyfill for requestVideoFrameCallback.
 * Uses requestAnimationFrame + getVideoPlaybackQuality.
 * @export
 */
shaka.polyfill.VideoFrameCallback = class {
  /**
   * Install the polyfill if needed.
   * @export
   */
  static install() {
    if (!window.HTMLVideoElement) {
      return;
    }

    // eslint-disable-next-line no-restricted-syntax
    const proto = HTMLVideoElement.prototype;

    if (proto.requestVideoFrameCallback) {
      return;
    }

    if (!window.requestAnimationFrame) {
      return;
    }

    if (!proto.getVideoPlaybackQuality) {
      return;
    }

    proto.requestVideoFrameCallback =
        shaka.polyfill.VideoFrameCallback.requestVideoFrameCallback_;

    proto.cancelVideoFrameCallback =
        shaka.polyfill.VideoFrameCallback.cancelVideoFrameCallback_;
  }

  /**
   * @param {!HTMLVideoElement} video
   * @return {!shaka.polyfill.VideoFrameCallback.CallbackData_}
   * @private
   */
  static getData_(video) {
    let data = shaka.polyfill.VideoFrameCallback.callbackMap_.get(video);

    if (!data) {
      data = {
        nextHandle: 1,
        callbackHandlesToRafIds: new Map(),
        presentedFrames: 0,
      };

      shaka.polyfill.VideoFrameCallback.callbackMap_.set(video, data);
    }

    return data;
  }

  /**
   * @this {HTMLVideoElement}
   * @param {function(number, ?VideoFrameMetadata)} callback
   * @return {number}
   * @private
   */
  static requestVideoFrameCallback_(callback) {
    /** @type {!HTMLVideoElement} */
    const video = this;

    const data =
        shaka.polyfill.VideoFrameCallback.getData_(video);

    const handle = data.nextHandle++;

    const checkFrame = (now) => {
      if (!data.callbackHandlesToRafIds.has(handle)) {
        return;
      }

      const quality = video.getVideoPlaybackQuality();

      const presentedFrames = quality.totalVideoFrames || 0;

      if (presentedFrames !== data.presentedFrames) {
        data.presentedFrames = presentedFrames;

        /** @type {!VideoFrameMetadata} */
        const metadata = /** @type {!VideoFrameMetadata} */ ({
          presentationTime: now,
          expectedDisplayTime: now,
          mediaTime: video.currentTime,
          presentedFrames,
          width: video.videoWidth,
          height: video.videoHeight,
          processingDuration: 0,
        });

        data.callbackHandlesToRafIds.delete(handle);

        callback(now, metadata);

        return;
      }

      const rafId = window.requestAnimationFrame(checkFrame);

      data.callbackHandlesToRafIds.set(handle, rafId);
    };

    const rafId = window.requestAnimationFrame(checkFrame);

    data.callbackHandlesToRafIds.set(handle, rafId);

    return handle;
  }

  /**
   * @this {HTMLVideoElement}
   * @param {number} handle
   * @private
   */
  static cancelVideoFrameCallback_(handle) {
    /** @type {!HTMLVideoElement} */
    const video = this;

    const data =
        shaka.polyfill.VideoFrameCallback.callbackMap_.get(video);

    if (!data) {
      return;
    }

    const rafId = data.callbackHandlesToRafIds.get(handle);

    if (rafId != null) {
      window.cancelAnimationFrame(rafId);
      data.callbackHandlesToRafIds.delete(handle);
    }
  }
};


/**
 * @typedef {{
 *   nextHandle: number,
 *   callbackHandlesToRafIds: !Map<number, number>,
 *   presentedFrames: number
 * }}
 * @property {number} nextHandle
 * @property {!Map<number, number>} callbackHandlesToRafIds
 *   Maps requestVideoFrameCallback handles to the corresponding
 *   requestAnimationFrame IDs so they can be cancelled later.
 * @property {number} presentedFrames
 * @private
 */
shaka.polyfill.VideoFrameCallback.CallbackData_;


/**
 * @private {!WeakMap<
 *   !HTMLVideoElement,
 *   !shaka.polyfill.VideoFrameCallback.CallbackData_>}
 */
shaka.polyfill.VideoFrameCallback.callbackMap_ =
    new WeakMap();


// Install at a low priority so that other Video polyfills go first.
shaka.polyfill.register(shaka.polyfill.VideoFrameCallback.install, -2);