Refactored renderer to return pre-compiled render data without rendering it. Will be used for caching.

This commit is contained in:
David Schulte
2024-07-15 18:13:54 +02:00
parent a9bf6dd604
commit 7ddf8c77de
4 changed files with 131 additions and 44 deletions
+2 -15
View File
@@ -1,4 +1,5 @@
import {PgsRendererOptions} from "./pgsRendererOptions";
import {PgsRendererHelper} from "./pgsRendererHelper";
/**
* Renders PGS subtitle on-top of a video element using a canvas element. This also handles timestamp updates if a
@@ -132,21 +133,7 @@ export class PgsRenderer {
* @param time The timestamp in seconds.
*/
public renderAtTimestamp(time: number): void {
time = time * 1000 * 90; // Convert to PGS time
// All position before and after the available timestamps are invalid (-1).
let index = -1;
if (this.updateTimestamps.length > 0 && time < this.updateTimestamps[this.updateTimestamps.length - 1]) {
// Find the last subtitle index for the given time stamp
for (const updateTimestamp of this.updateTimestamps) {
if (updateTimestamp > time) {
break;
}
index++;
}
}
const index = PgsRendererHelper.getIndexFromTimestamps(time, this.updateTimestamps);
// Only tell the worker, if the subtitle index was changed!
if (this.previousTimestampIndex === index) return;
+27
View File
@@ -0,0 +1,27 @@
export class PgsRendererHelper {
/**
* Returns the array index position for the previous timestamp position from the given array.
* Returns -1 if the given time is outside the timestamp range.
* @param time The timestamp to check in seconds.
* @param pgsTimestamps The list of available PGS timestamps.
*/
public static getIndexFromTimestamps(time: number, pgsTimestamps: number[]): number {
const pgsTime = time * 1000 * 90; // Convert to PGS time
// All position before and after the available timestamps are invalid (-1).
let index = -1;
if (pgsTimestamps.length > 0 && pgsTime < pgsTimestamps[pgsTimestamps.length - 1]) {
// Find the last subtitle index for the given time stamp
for (const pgsTimestamp of pgsTimestamps) {
if (pgsTimestamp > pgsTime) {
break;
}
index++;
}
}
return index;
}
}
+30 -29
View File
@@ -10,6 +10,8 @@ import {Rect} from "./utils/rect";
import {StreamBinaryReader} from "./utils/streamBinaryReader";
import {BinaryReader} from "./utils/binaryReader";
import {ArrayBinaryReader} from "./utils/arrayBinaryReader";
import {CompositionRenderData, RenderData} from "./renderData";
import {PgsRendererHelper} from "./pgsRendererHelper";
export interface PgsLoadOptions {
/**
@@ -121,22 +123,7 @@ export class PgsRendererInternal {
* @param time The timestamp in seconds.
*/
public renderAtTimestamp(time: number): void {
time = time * 1000 * 90; // Convert to PGS time
// All position before and after the available timestamps are invalid (-1).
let index = -1;
if (this.updateTimestamps.length > 0 && time < this.updateTimestamps[this.updateTimestamps.length - 1]) {
// Find the last subtitle index for the given time stamp
for (const updateTimestamp of this.updateTimestamps) {
if (updateTimestamp > time) {
break;
}
index++;
}
}
const index = PgsRendererHelper.getIndexFromTimestamps(time, this.updateTimestamps);
this.renderAtIndex(index);
}
@@ -147,12 +134,31 @@ export class PgsRendererInternal {
public renderAtIndex(index: number): void {
if (!this.canvas || !this.context) return;
// Clear the canvas on invalid indices. It is possible to seek to a position before the first subtitle while
// a later subtile is on screen. This subtitle must be clear, even there is no valid new subtitle data.
// a later subtitle is on screen. This subtitle must be clear, even there is no valid new subtitle data.
// Ignoring the render would keep the previous subtitle on screen.
if (!this.dirtyArea.empty) {
this.context.clearRect(this.dirtyArea.x, this.dirtyArea.y, this.dirtyArea.width, this.dirtyArea.height);
this.dirtyArea.reset();
}
const renderData = this.buildRenderDataAtIndex(index);
if (!renderData) return;
// Resize the canvas if needed.
if (this.canvas.width != renderData.width ||
this.canvas.height != renderData.height) {
this.canvas.width = renderData.width;
this.canvas.height = renderData.height;
}
renderData.draw(this.context, this.dirtyArea);
}
/**
* Pre-compiles the required render data (windows and pixel data) for the frame at the given index.
* @param index The index of the display set to render.
*/
public buildRenderDataAtIndex(index: number): RenderData | undefined {
if (index < 0 || index >= this.displaySets.length) {
return;
}
@@ -160,13 +166,6 @@ export class PgsRendererInternal {
const displaySet = this.displaySets[index];
if (!displaySet.presentationComposition) return;
// Resize the canvas if needed.
if (this.canvas.width != displaySet.presentationComposition.width ||
this.canvas.height != displaySet.presentationComposition.height) {
this.canvas.width = displaySet.presentationComposition.width;
this.canvas.height = displaySet.presentationComposition.height;
}
// We need to collect all valid objects and palettes up to this point. PGS can update and reuse elements from
// previous display sets. The `compositionState` defines if the previous elements should be cleared.
// If it is `0` previous elements should be kept. Because the user can seek through the file, we can not store
@@ -196,6 +195,7 @@ export class PgsRendererInternal {
.find(w => w.id === displaySet.presentationComposition?.paletteId);
if (!palette) return;
const compositionData: CompositionRenderData[] = [];
for (const compositionObject of displaySet.presentationComposition.compositionObjects) {
// Find the window to draw on.
let window = ctxWindows.find(w => w.id === compositionObject.windowId);
@@ -204,13 +204,14 @@ export class PgsRendererInternal {
// Builds the subtitle.
const pixelData = this.getPixelDataFromComposition(compositionObject, palette, ctxObjects);
if (pixelData) {
this.context.drawImage(pixelData, window.horizontalPosition, window.verticalPosition);
// Mark this area as dirty.
this.dirtyArea.union(window.horizontalPosition, window.verticalPosition,
pixelData.width, pixelData.height);
compositionData.push(new CompositionRenderData(window, pixelData));
}
}
if (compositionData.length === 0) return;
return new RenderData(displaySet.presentationComposition.width, displaySet.presentationComposition.height,
compositionData);
}
private createCanvas(width: number, height: number): OffscreenCanvas | HTMLCanvasElement {
+72
View File
@@ -0,0 +1,72 @@
import {WindowDefinition} from "./pgs/windowDefinitionSegment";
import {Rect} from "./utils/rect";
/**
* This class contains the compiled subtitle data for a whole frame.
*/
export class RenderData {
/**
* The total width of the presentation composition (screen).
*/
public readonly width: number;
/**
* The total height of the presentation composition (screen).
*/
public readonly height: number;
/**
* The pre-compiled composition elements.
*/
public readonly compositionData: CompositionRenderData[];
public constructor(width: number, height: number, compositionData: CompositionRenderData[]) {
this.width = width;
this.height = height;
this.compositionData = compositionData;
}
/**
* Draws the whole subtitle frame to the given context.
* @param context The context to draw on.
* @param dirtyArea If given, it will extend the dirty rect to include the affected subtitle area.
*/
public draw(context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, dirtyArea?: Rect): void {
for (const composition of this.compositionData) {
composition.draw(context, dirtyArea);
}
}
}
/**
* This class contains the compiled subtitle data for a single composition.
*/
export class CompositionRenderData {
/**
* The pgs window to draw on (the on-screen position).
*/
public readonly window: WindowDefinition;
/**
* The compiled pixel data of the subtitle.
*/
public readonly pixelData: HTMLCanvasElement | OffscreenCanvas;
public constructor(window: WindowDefinition, pixelData: HTMLCanvasElement | OffscreenCanvas) {
this.window = window;
this.pixelData = pixelData;
}
/**
* Draws this subtitle composition to the given context.
* @param context The context to draw on.
* @param dirtyArea If given, it will extend the dirty rect to include the affected subtitle area.
*/
public draw(context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D, dirtyArea?: Rect): void {
context.drawImage(this.pixelData, this.window.horizontalPosition, this.window.verticalPosition);
// Mark this area as dirty.
dirtyArea?.union(this.window.horizontalPosition, this.window.verticalPosition,
this.pixelData.width, this.pixelData.height);
}
}