diff --git a/src/pgsRenderer.ts b/src/pgsRenderer.ts index 6075a4b..566e802 100644 --- a/src/pgsRenderer.ts +++ b/src/pgsRenderer.ts @@ -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; diff --git a/src/pgsRendererHelper.ts b/src/pgsRendererHelper.ts new file mode 100644 index 0000000..b0f9d56 --- /dev/null +++ b/src/pgsRendererHelper.ts @@ -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; + } +} \ No newline at end of file diff --git a/src/pgsRendererInternal.ts b/src/pgsRendererInternal.ts index d696729..c39862e 100644 --- a/src/pgsRendererInternal.ts +++ b/src/pgsRendererInternal.ts @@ -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 { diff --git a/src/renderData.ts b/src/renderData.ts new file mode 100644 index 0000000..20bf2f7 --- /dev/null +++ b/src/renderData.ts @@ -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); + } +} \ No newline at end of file