rx-player
Version:
Canal+ HTML5 Video Player
244 lines (222 loc) • 7.47 kB
text/typescript
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;
}