mirror of
https://github.com/zoriya/libpgs-js.git
synced 2026-08-27 00:00:51 +00:00
Added better error handling when loading PGS streams.
This commit is contained in:
+34
-21
@@ -4,7 +4,7 @@ import {PaletteDefinitionSegment} from "./paletteDefinitionSegment";
|
||||
import {ObjectDefinitionSegment} from "./objectDefinitionSegment";
|
||||
import {WindowDefinitionSegment} from "./windowDefinitionSegment";
|
||||
import {SegmentType} from "./segmentType";
|
||||
import {AsyncBinaryReader} from "../utils/asyncBinaryReader";
|
||||
import {PgsRendererResult} from "../pgsRendererResult";
|
||||
|
||||
/**
|
||||
* The PGS display set holds all data for the current subtitle update at a given timestamp.
|
||||
@@ -23,7 +23,7 @@ export class DisplaySet {
|
||||
* @param includeHeader If true, the magic-number and timestamps are read. If false, reading starts at the first
|
||||
* segment.
|
||||
*/
|
||||
public async read(reader: BigEndianBinaryReader, includeHeader: boolean) {
|
||||
public async read(reader: BigEndianBinaryReader, includeHeader: boolean): Promise<PgsRendererResult> {
|
||||
|
||||
// Clear
|
||||
this.presentationTimestamp = 0;
|
||||
@@ -33,12 +33,6 @@ export class DisplaySet {
|
||||
this.objectDefinitions = [];
|
||||
this.windowDefinitions = [];
|
||||
|
||||
// Handles async readers
|
||||
let asyncReader: AsyncBinaryReader | undefined = undefined;
|
||||
if ('requestData' in reader.baseReader) {
|
||||
asyncReader = reader.baseReader as AsyncBinaryReader;
|
||||
}
|
||||
|
||||
while (true)
|
||||
{
|
||||
let presentationTimestamp: number = 0;
|
||||
@@ -47,35 +41,46 @@ export class DisplaySet {
|
||||
// The header is included before every segment. Even for the end segment.
|
||||
if (includeHeader)
|
||||
{
|
||||
await asyncReader?.requestData(10);
|
||||
if (!await reader.requestData(10)) {
|
||||
console.error(`[libpgs] Unexpected end of stream at 0x${reader.position.toString(16)}`);
|
||||
return PgsRendererResult.ErrUnexpectedEndOfStream;
|
||||
}
|
||||
const magicNumber = reader.readUInt16();
|
||||
if (magicNumber != 0x5047) {
|
||||
throw new Error("Invalid magic number!");
|
||||
console.error(`[libpgs] Invalid magic number: Expected: 0x5047 - Given: 0x${magicNumber.toString(16)}`);
|
||||
return PgsRendererResult.ErrInvalidMagicNumber;
|
||||
}
|
||||
|
||||
presentationTimestamp = reader.readUInt32();
|
||||
decodingTimestamp = reader.readUInt32();
|
||||
}
|
||||
|
||||
await asyncReader?.requestData(3);
|
||||
const type = reader.readUInt8();
|
||||
const size = reader.readUInt16()
|
||||
if (!await reader.requestData(3)) {
|
||||
console.error(`[libpgs] Unexpected end of stream at 0x${reader.position.toString(16)}`);
|
||||
return PgsRendererResult.ErrUnexpectedEndOfStream;
|
||||
}
|
||||
const segmentType = reader.readUInt8();
|
||||
const segmentSize = reader.readUInt16();
|
||||
const segmentStart = reader.position;
|
||||
|
||||
await asyncReader?.requestData(size);
|
||||
switch (type) {
|
||||
if (!await reader.requestData(segmentSize)) {
|
||||
console.error(`[libpgs] Unexpected end of stream at 0x${reader.position.toString(16)}`);
|
||||
return PgsRendererResult.ErrUnexpectedEndOfStream;
|
||||
}
|
||||
switch (segmentType) {
|
||||
case SegmentType.paletteDefinition:
|
||||
const pds = new PaletteDefinitionSegment();
|
||||
pds.read(reader, size);
|
||||
pds.read(reader, segmentSize);
|
||||
this.paletteDefinitions.push(pds);
|
||||
break;
|
||||
case SegmentType.objectDefinition:
|
||||
const ods = new ObjectDefinitionSegment();
|
||||
ods.read(reader, size);
|
||||
ods.read(reader, segmentSize);
|
||||
this.objectDefinitions.push(ods);
|
||||
break;
|
||||
case SegmentType.presentationComposition:
|
||||
const pcs = new PresentationCompositionSegment();
|
||||
pcs.read(reader, size);
|
||||
pcs.read(reader, segmentSize);
|
||||
this.presentationComposition = pcs;
|
||||
|
||||
// SubtitleEdit only writes the relevant timestamp to the PCS.
|
||||
@@ -84,13 +89,21 @@ export class DisplaySet {
|
||||
break;
|
||||
case SegmentType.windowDefinition:
|
||||
const wds = new WindowDefinitionSegment();
|
||||
wds.read(reader, size);
|
||||
wds.read(reader, segmentSize);
|
||||
this.windowDefinitions.push(wds);
|
||||
break;
|
||||
case SegmentType.end:
|
||||
return;
|
||||
return PgsRendererResult.Success;
|
||||
default:
|
||||
throw new Error(`Unsupported segment type ${type}`);
|
||||
console.error(`[libpgs] Unknown segment type: 0x${segmentType.toString(16)}`);
|
||||
return PgsRendererResult.ErrUnknownSegment;
|
||||
}
|
||||
|
||||
// Validates the current stream position
|
||||
const expectedPosition = segmentStart + segmentSize;
|
||||
if (reader.position !== expectedPosition) {
|
||||
console.error(`[libpgs] Invalid stream position after segment. Expected: 0x${expectedPosition.toString(16)} - Given: 0x${reader.position.toString(16)}`);
|
||||
return PgsRendererResult.ErrStreamPositionMismatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -1,5 +1,6 @@
|
||||
import {PgsRendererOptions} from "./pgsRendererOptions";
|
||||
import {PgsRendererHelper} from "./pgsRendererHelper";
|
||||
import {PgsRendererResult} from "./pgsRendererResult";
|
||||
|
||||
/**
|
||||
* Renders PGS subtitle on-top of a video element using a canvas element. This also handles timestamp updates if a
|
||||
@@ -154,8 +155,13 @@ export class PgsRenderer {
|
||||
|
||||
private onWorkerMessage = (e: MessageEvent) => {
|
||||
switch (e.data.op) {
|
||||
// Is called once a subtitle file was loaded.
|
||||
case 'updateTimestamps':
|
||||
// Is called for every progress update of for loading the file.
|
||||
case 'progress':
|
||||
const result = e.data.result as PgsRendererResult;
|
||||
if (result !== PgsRendererResult.Success && result !== PgsRendererResult.Pending) {
|
||||
console.error(`[libpgs] Couldn't load subtitle stream. Worker returned: ${result}`);
|
||||
}
|
||||
|
||||
// Stores the update timestamps, so we don't need to push the timestamp to the worker on every tick.
|
||||
// Instead, we push the timestamp index if it was changed.
|
||||
this.updateTimestamps = e.data.updateTimestamps;
|
||||
|
||||
@@ -12,6 +12,7 @@ import {BinaryReader} from "./utils/binaryReader";
|
||||
import {ArrayBinaryReader} from "./utils/arrayBinaryReader";
|
||||
import {CompositionRenderData, RenderData} from "./renderData";
|
||||
import {PgsRendererHelper} from "./pgsRendererHelper";
|
||||
import {PgsRendererResult} from "./pgsRendererResult";
|
||||
|
||||
export interface PgsLoadOptions {
|
||||
/**
|
||||
@@ -44,15 +45,16 @@ export class PgsRendererInternal {
|
||||
* @param url The url to the PGS file.
|
||||
* @param options Optional loading options. Use `onProgress` as callback for partial update while loading.
|
||||
*/
|
||||
public async loadFromUrl(url: string, options?: PgsLoadOptions): Promise<void> {
|
||||
public async loadFromUrl(url: string, options?: PgsLoadOptions): Promise<PgsRendererResult> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error: ${response.status}`);
|
||||
console.error(`[libpgs] Failed to load subtitle stream from url. HTTP Status: ${response.status}`);
|
||||
return PgsRendererResult.ErrHttp;
|
||||
}
|
||||
const stream = response.body?.getReader()!;
|
||||
const reader = new StreamBinaryReader(stream)
|
||||
|
||||
await this.loadFromReader(reader, options);
|
||||
return await this.loadFromReader(reader, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,8 +62,8 @@ export class PgsRendererInternal {
|
||||
* @param buffer The PGS data.
|
||||
* @param options Optional loading options. Use `onProgress` as callback for partial update while loading.
|
||||
*/
|
||||
public async loadFromBuffer(buffer: ArrayBuffer, options?: PgsLoadOptions): Promise<void> {
|
||||
await this.loadFromReader(new ArrayBinaryReader(new Uint8Array(buffer)), options);
|
||||
public async loadFromBuffer(buffer: ArrayBuffer, options?: PgsLoadOptions): Promise<PgsRendererResult> {
|
||||
return await this.loadFromReader(new ArrayBinaryReader(new Uint8Array(buffer)), options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,7 +71,7 @@ export class PgsRendererInternal {
|
||||
* @param reader The PGS data reader.
|
||||
* @param options Optional loading options. Use `onProgress` as callback for partial update while loading.
|
||||
*/
|
||||
public async loadFromReader(reader: BinaryReader, options?: PgsLoadOptions): Promise<void> {
|
||||
public async loadFromReader(reader: BinaryReader, options?: PgsLoadOptions): Promise<PgsRendererResult> {
|
||||
this.displaySets = [];
|
||||
this.updateTimestamps = [];
|
||||
this.cachedRenderData = undefined;
|
||||
@@ -79,7 +81,10 @@ export class PgsRendererInternal {
|
||||
const bigEndianReader = new BigEndianBinaryReader(reader);
|
||||
while (!reader.eof) {
|
||||
const displaySet = new DisplaySet();
|
||||
await displaySet.read(bigEndianReader, true);
|
||||
const result = await displaySet.read(bigEndianReader, true);
|
||||
if (result !== PgsRendererResult.Success) {
|
||||
return result;
|
||||
}
|
||||
this.displaySets.push(displaySet);
|
||||
this.updateTimestamps.push(displaySet.presentationTimestamp);
|
||||
|
||||
@@ -98,6 +103,8 @@ export class PgsRendererInternal {
|
||||
if (options?.onProgress) {
|
||||
options.onProgress();
|
||||
}
|
||||
|
||||
return PgsRendererResult.Success;
|
||||
}
|
||||
|
||||
// endregion
|
||||
@@ -107,7 +114,7 @@ export class PgsRendererInternal {
|
||||
private canvas?: OffscreenCanvas | HTMLCanvasElement;
|
||||
private context?: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D;
|
||||
|
||||
// Information about the next compiled render data. This is calculated after the current subtitle is renderd.
|
||||
// Information about the next compiled render data. This is calculated after the current subtitle is rendered.
|
||||
// So by the time the next subtitle change is requested, this should already be completed.
|
||||
private cachedRenderData?: { index: number, data: RenderData | undefined };
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
export enum PgsRendererResult {
|
||||
/**
|
||||
* The renderer has loaded and processed the subtitle stream.
|
||||
*/
|
||||
Success,
|
||||
|
||||
/**
|
||||
* The renderer is still processing the subtitle stream.
|
||||
*/
|
||||
Pending,
|
||||
|
||||
/**
|
||||
* Processing the subtitle stream failed due to an invalid magic number. Probably not a PGS file.
|
||||
*/
|
||||
ErrInvalidMagicNumber,
|
||||
|
||||
/**
|
||||
* Processing the subtitle stream failed due to an unexpected end of the stream.
|
||||
*/
|
||||
ErrUnexpectedEndOfStream,
|
||||
|
||||
/**
|
||||
* Processing the subtitle stream failed due to an unknown segment type.
|
||||
*/
|
||||
ErrUnknownSegment,
|
||||
|
||||
/**
|
||||
* Processing the subtitle stream failed due to a stream position mismatch after reading a segment.
|
||||
*/
|
||||
ErrStreamPositionMismatch,
|
||||
|
||||
/**
|
||||
* Loading the subtitle stream failed due to an HTTP error.
|
||||
*/
|
||||
ErrHttp
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import {BinaryReader} from "./binaryReader";
|
||||
import {ArrayBinaryReader} from "./arrayBinaryReader";
|
||||
import {AsyncBinaryReader} from "./asyncBinaryReader";
|
||||
|
||||
export class BigEndianBinaryReader {
|
||||
/**
|
||||
* The base binary reader.
|
||||
*/
|
||||
public readonly baseReader: BinaryReader;
|
||||
private readonly baseReader: BinaryReader;
|
||||
|
||||
/**
|
||||
* Set if `baseReader` is an async reader.
|
||||
*/
|
||||
private readonly asyncReader?: AsyncBinaryReader;
|
||||
|
||||
public constructor(buffer: BinaryReader | Uint8Array) {
|
||||
if (buffer instanceof Uint8Array) {
|
||||
@@ -14,6 +20,11 @@ export class BigEndianBinaryReader {
|
||||
else {
|
||||
this.baseReader = buffer;
|
||||
}
|
||||
|
||||
// Handles async readers
|
||||
if ('requestData' in this.baseReader) {
|
||||
this.asyncReader = this.baseReader as AsyncBinaryReader;
|
||||
}
|
||||
}
|
||||
|
||||
public get position(): number {
|
||||
@@ -56,4 +67,18 @@ export class BigEndianBinaryReader {
|
||||
public readBytes(count: number): Uint8Array {
|
||||
return this.baseReader.readBytes(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the given number of bytes is available to read synchronously.
|
||||
* This will wait until the data is ready to read if an underlying async stream is used.
|
||||
* @param count The number of bytes requested.
|
||||
* @return Returns if the requested number of bytes could be loaded.
|
||||
*/
|
||||
public async requestData(count: number): Promise<boolean> {
|
||||
if (this.asyncReader) {
|
||||
return await this.asyncReader.requestData(count);
|
||||
}
|
||||
|
||||
return this.baseReader.position + count <= this.baseReader.length;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-8
@@ -1,11 +1,13 @@
|
||||
import {PgsRendererInternal} from "./pgsRendererInternal";
|
||||
import {PgsRendererResult} from "./pgsRendererResult";
|
||||
|
||||
const renderer = new PgsRendererInternal();
|
||||
|
||||
// Inform the main process that the subtitle data was loaded and return all update timestamps
|
||||
const submitTimestamps = () => {
|
||||
// Inform the main process about the renderer status update.
|
||||
const sendProgressUpdate = (result: PgsRendererResult) => {
|
||||
postMessage({
|
||||
op: 'updateTimestamps',
|
||||
op: 'progress',
|
||||
result: result,
|
||||
updateTimestamps: renderer.updateTimestamps
|
||||
})
|
||||
}
|
||||
@@ -22,17 +24,17 @@ onmessage = (e: MessageEvent) => {
|
||||
const url: string = e.data.url;
|
||||
renderer.loadFromUrl(url, {
|
||||
onProgress: () => {
|
||||
submitTimestamps();
|
||||
sendProgressUpdate(PgsRendererResult.Pending);
|
||||
}
|
||||
}).then(() => {
|
||||
submitTimestamps();
|
||||
}).then((result) => {
|
||||
sendProgressUpdate(result);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'loadFromBuffer':
|
||||
const buffer: ArrayBuffer = e.data.buffer;
|
||||
renderer.loadFromBuffer(buffer).then(() => {
|
||||
submitTimestamps();
|
||||
renderer.loadFromBuffer(buffer).then((result) => {
|
||||
sendProgressUpdate(result);
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import {PgsRendererInternal} from "../src/pgsRendererInternal";
|
||||
import * as fs from "node:fs";
|
||||
import {PgsRendererResult} from "../src/pgsRendererResult";
|
||||
|
||||
test('load and render pgs', async () => {
|
||||
const dataSup = fs.readFileSync(`${__dirname}/files/test.sup`);
|
||||
@@ -12,7 +13,8 @@ test('load and render pgs', async () => {
|
||||
const context = canvas.getContext("2d")!;
|
||||
const renderer = new PgsRendererInternal();
|
||||
renderer.setCanvas(canvas);
|
||||
await renderer.loadFromBuffer(dataSup);
|
||||
const result = await renderer.loadFromBuffer(dataSup);
|
||||
expect(result).toBe(PgsRendererResult.Success);
|
||||
|
||||
// Helper function to render and compare the image in the test directory.
|
||||
// Since we only set pixel data and don't use font rendering this should be deterministic on every machine.
|
||||
@@ -33,3 +35,13 @@ test('load and render pgs', async () => {
|
||||
expectImageAtTimestamp('test-1.rgba', 2.5);
|
||||
expectImageAtTimestamp('test-2.rgba', 3.5);
|
||||
});
|
||||
|
||||
test('load non-pgs file and catch error', async () => {
|
||||
const consoleErrorMock = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
const dataSup = fs.readFileSync(`${__dirname}/files/test.srt`);
|
||||
const renderer = new PgsRendererInternal();
|
||||
const result = await renderer.loadFromBuffer(dataSup);
|
||||
expect(result).toBe(PgsRendererResult.ErrInvalidMagicNumber);
|
||||
expect(consoleErrorMock).toHaveBeenCalled();
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user