UNPKG

rx-player

Version:
244 lines (222 loc) 7.47 kB
import type { IBufferedChunkSnapshot } from "../../../core/segment_sinks/segment_sinks_store"; import type { IRepresentationMetadata } from "../../../manifest"; const BUFFER_WIDTH_IN_SECONDS = 30 * 60; const COLORS = [ "#2ab7ca", "#fed766", "#4dd248", "#a22c28", "#556b2f", // darkolivegreen "#add8e6", // lightblue "#90ee90", // lightgreen "#444444", "#40bfc1", "#57557e", "#fbe555", ]; export interface ISegmentSinkGrapUpdateData { currentTime: number; inventory: IBufferedChunkSnapshot[]; width: number; height: number; minimumPosition: number | undefined; maximumPosition: number | undefined; } export default class SegmentSinkGraph { /** Link buffered Representation's uniqueId to their corresponding color. */ private readonly _colorMap: Map<string, string>; /** Current amount of colors chosen to represent the various Representation. */ private _currNbColors: number; /** Canvas that will contain the buffer graph itself. */ private readonly _canvasElt: HTMLCanvasElement; private readonly _canvasCtxt: CanvasRenderingContext2D | null; constructor(canvasElt: HTMLCanvasElement) { this._colorMap = new Map(); this._currNbColors = 0; this._canvasElt = canvasElt; this._canvasCtxt = this._canvasElt.getContext("2d"); this.clear(); } public clear() { if (this._canvasCtxt !== null) { this._canvasCtxt.clearRect(0, 0, this._canvasElt.width, this._canvasElt.height); } } public update(data: ISegmentSinkGrapUpdateData): void { // Following logic clear the colorMap entries if they are not used anymore // to prevent memory usage. const representationStillInUse: Set<string> = new Set(); data.inventory.forEach((chunk) => { representationStillInUse.add(chunk.infos.representation.uniqueId); }); this._colorMap.forEach((representationId) => { if (!representationStillInUse.has(representationId)) { this._colorMap.delete(representationId); } }); if (this._canvasCtxt === null) { return; } const { inventory, currentTime, width, height } = data; this._canvasElt.style.width = `${width}px`; this._canvasElt.style.height = `${height}px`; this._canvasElt.width = width; this._canvasElt.height = height; this.clear(); let minimumPoint: number; if (data.minimumPosition !== undefined) { if (inventory.length > 0) { minimumPoint = Math.min(data.minimumPosition, inventory[0].start); } else { minimumPoint = data.minimumPosition; } } else { minimumPoint = inventory[0]?.start ?? 0; } let maximumPoint: number; if (data.maximumPosition !== undefined) { if (inventory.length > 0) { maximumPoint = Math.max( data.maximumPosition, inventory[inventory.length - 1].end, ); } else { maximumPoint = data.maximumPosition; } } else { maximumPoint = inventory[inventory.length - 1]?.end ?? 1000; } minimumPoint = Math.min(currentTime, minimumPoint); maximumPoint = Math.max(currentTime, maximumPoint); let minimumPosition; let maximumPosition; if (maximumPoint - minimumPoint > BUFFER_WIDTH_IN_SECONDS) { if (maximumPoint - currentTime < BUFFER_WIDTH_IN_SECONDS / 2) { maximumPosition = maximumPoint; minimumPosition = maximumPoint - BUFFER_WIDTH_IN_SECONDS; } else if (currentTime - minimumPoint < BUFFER_WIDTH_IN_SECONDS / 2) { minimumPosition = minimumPoint; maximumPosition = minimumPoint + BUFFER_WIDTH_IN_SECONDS; } else { minimumPosition = currentTime - BUFFER_WIDTH_IN_SECONDS / 2; maximumPosition = currentTime + BUFFER_WIDTH_IN_SECONDS / 2; } } else { minimumPosition = minimumPoint; maximumPosition = maximumPoint; } if (minimumPosition >= maximumPosition) { this.clear(); return; } const currentRangesScaled = scaleSegments( inventory, minimumPosition, maximumPosition, ); for (const currentRange of currentRangesScaled) { this._paintRange(currentRange, width, height); } if (currentTime !== undefined) { paintCurrentPosition( currentTime, minimumPosition, maximumPosition, this._canvasCtxt, width, height, ); } } /** * Paint a given range in the canvas * @param {Object} rangeScaled - Buffered segment information with added * "scaling" information to know where it fits in the canvas. */ private _paintRange(rangeScaled: IScaledChunk, width: number, height: number): void { if (this._canvasCtxt === null) { return; } const startX = rangeScaled.scaledStart * width; const endX = rangeScaled.scaledEnd * width; this._canvasCtxt.fillStyle = this._getColorForRepresentation( rangeScaled.info.infos.representation, ); this._canvasCtxt.fillRect(Math.ceil(startX), 0, Math.ceil(endX - startX), height); } private _getColorForRepresentation(representation: IRepresentationMetadata): string { const color = this._colorMap.get(representation.uniqueId); if (color !== undefined) { return color; } const newColor = COLORS[this._currNbColors % COLORS.length]; this._currNbColors++; this._colorMap.set(representation.uniqueId, newColor); return newColor; } } /** * Represent the current position in the canvas. * @param {number|undefined} position - The current position * @param {number} minimumPosition - minimum possible position represented in * the canvas. * @param {number} maximumPosition - maximum possible position represented in * the canvas. * @param {Object} canvasCtx - The canvas' 2D context */ function paintCurrentPosition( position: number, minimumPosition: number, maximumPosition: number, canvasCtx: CanvasRenderingContext2D, width: number, height: number, ) { if ( typeof position === "number" && position >= minimumPosition && position < maximumPosition ) { const lengthCanvas = maximumPosition - minimumPosition; canvasCtx.fillStyle = "#FF0000"; canvasCtx.fillRect( Math.ceil(((position - minimumPosition) / lengthCanvas) * width) - 1, 5, 5, height, ); } } /** * Scale given bufferedData in terms of percentage between the minimum and * maximum position. Filter out ranges which are not part of it. * @param {Array.<Object>} bufferedData * @param {number} minimumPosition * @param {number} maximumPosition * @returns {Array.<Object>} */ function scaleSegments( bufferedData: IBufferedChunkSnapshot[], minimumPosition: number, maximumPosition: number, ): IScaledChunk[] { const scaledSegments = []; const wholeDuration = maximumPosition - minimumPosition; for (const info of bufferedData) { const start = info.bufferedStart ?? info.start; const end = info.bufferedEnd ?? info.end; if (end > minimumPosition && start < maximumPosition) { const startPoint = Math.max(start - minimumPosition, 0); const endPoint = Math.min(end - minimumPosition, maximumPosition); const scaledStart = startPoint / wholeDuration; const scaledEnd = endPoint / wholeDuration; scaledSegments.push({ scaledStart, scaledEnd, info }); } } return scaledSegments; } interface IScaledChunk { scaledStart: number; scaledEnd: number; info: IBufferedChunkSnapshot; }