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.setTimeout(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;