Home Reference Source

src/controller/fps-controller.ts

import { Events } from '../events';
import { logger } from '../utils/logger';
import type { ComponentAPI } from '../types/component-api';
import type Hls from '../hls';
import type { MediaAttachingData } from '../types/events';
import StreamController from './stream-controller';

class FPSController implements ComponentAPI {
  private hls: Hls;
  private isVideoPlaybackQualityAvailable: boolean = false;
  private timer?: number;
  private media: HTMLVideoElement | null = null;
  private lastTime: any;
  private lastDroppedFrames: number = 0;
  private lastDecodedFrames: number = 0;
  // stream controller must be provided as a dependency!
  private streamController!: StreamController;

  constructor(hls: Hls) {
    this.hls = hls;

    this.registerListeners();
  }

  public setStreamController(streamController: StreamController) {
    this.streamController = streamController;
  }

  protected registerListeners() {
    this.hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
  }

  protected unregisterListeners() {
    this.hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching);
  }

  destroy() {
    if (this.timer) {
      clearInterval(this.timer);
    }

    this.unregisterListeners();
    this.isVideoPlaybackQualityAvailable = false;
    this.media = null;
  }

  protected onMediaAttaching(
    event: Events.MEDIA_ATTACHING,
    data: MediaAttachingData
  ) {
    const config = this.hls.config;
    if (config.capLevelOnFPSDrop) {
      const media =
        data.media instanceof self.HTMLVideoElement ? data.media : null;
      this.media = media;
      if (media && typeof media.getVideoPlaybackQuality === 'function') {
        this.isVideoPlaybackQualityAvailable = true;
      }

      self.clearInterval(this.timer);
      this.timer = self.setInterval(
        this.checkFPSInterval.bind(this),
        config.fpsDroppedMonitoringPeriod
      );
    }
  }

  checkFPS(
    video: HTMLVideoElement,
    decodedFrames: number,
    droppedFrames: number
  ) {
    const currentTime = performance.now();
    if (decodedFrames) {
      if (this.lastTime) {
        const currentPeriod = currentTime - this.lastTime;
        const currentDropped = droppedFrames - this.lastDroppedFrames;
        const currentDecoded = decodedFrames - this.lastDecodedFrames;
        const droppedFPS = (1000 * currentDropped) / currentPeriod;
        const hls = this.hls;
        hls.trigger(Events.FPS_DROP, {
          currentDropped: currentDropped,
          currentDecoded: currentDecoded,
          totalDroppedFrames: droppedFrames,
        });
        if (droppedFPS > 0) {
          // logger.log('checkFPS : droppedFPS/decodedFPS:' + droppedFPS/(1000 * currentDecoded / currentPeriod));
          if (
            currentDropped >
            hls.config.fpsDroppedMonitoringThreshold * currentDecoded
          ) {
            let currentLevel = hls.currentLevel;
            logger.warn(
              'drop FPS ratio greater than max allowed value for currentLevel: ' +
                currentLevel
            );
            if (
              currentLevel > 0 &&
              (hls.autoLevelCapping === -1 ||
                hls.autoLevelCapping >= currentLevel)
            ) {
              currentLevel = currentLevel - 1;
              hls.trigger(Events.FPS_DROP_LEVEL_CAPPING, {
                level: currentLevel,
                droppedLevel: hls.currentLevel,
              });
              hls.autoLevelCapping = currentLevel;
              this.streamController.nextLevelSwitch();
            }
          }
        }
      }
      this.lastTime = currentTime;
      this.lastDroppedFrames = droppedFrames;
      this.lastDecodedFrames = decodedFrames;
    }
  }

  checkFPSInterval() {
    const video = this.media;
    if (video) {
      if (this.isVideoPlaybackQualityAvailable) {
        const videoPlaybackQuality = video.getVideoPlaybackQuality();
        this.checkFPS(
          video,
          videoPlaybackQuality.totalVideoFrames,
          videoPlaybackQuality.droppedVideoFrames
        );
      } else {
        // HTMLVideoElement doesn't include the webkit types
        this.checkFPS(
          video,
          (video as any).webkitDecodedFrameCount as number,
          (video as any).webkitDroppedFrameCount as number
        );
      }
    }
  }
}

export default FPSController;