diff --git a/src/browserSupport.ts b/src/browserSupport.ts new file mode 100644 index 0000000..6b73937 --- /dev/null +++ b/src/browserSupport.ts @@ -0,0 +1,71 @@ +import {PgsRendererMode} from "./pgsRendererMode"; + +export class BrowserSupport { + /** + * Checks if the web worker is supported in the current environment. + */ + public static isWorkerSupported(): boolean { + return !!Worker; + } + + /** + * Checks if the offscreen-canvas and `transferControlToOffscreen` are supported in the current environment. + */ + public static isOffscreenCanvasSupported(): boolean { + return !!HTMLCanvasElement.prototype.transferControlToOffscreen; + } + + /** + * Returns the optimal PGS renderer mode for the current platform. + */ + public static getRendererModeByPlatform(): PgsRendererMode { + // Handle browser specific edge cases. This list is not complete! + // If you find any more compatible issues with legacy browsers, please report them on the issue tracker. + const userAgent = navigator.userAgent; + + // Detect Chrome + const chrome = /Chrome\/(\d+)/.exec(userAgent); + const chromeVersion = chrome ? parseInt(chrome[1]) : undefined; + + // Detect Firefox + const firefox = /Firefox\/(\d+)/.exec(userAgent); + const firefoxVersion = firefox ? parseInt(firefox[1]) : undefined; + + // Detect WebOS + const webOS = navigator.userAgent.indexOf('Web0S') >= 0; + + + // ImageData in workers is supported since Chrome 36 + if (chromeVersion && chromeVersion < 36) { + return PgsRendererMode.mainThread; + } + + // ImageData in workers is supported since Firefox 25 + if (firefoxVersion && firefoxVersion < 25) { + return PgsRendererMode.mainThread; + } + + // Older version of WebOS are based on WebKit and don't support ImageData in Workers. + if (webOS && chromeVersion === undefined) { + return PgsRendererMode.mainThread; + } + // WebOS 5 / Chrome 68 should support workers, but they do not load nor respond to messages. + if (webOS && chromeVersion && chromeVersion <= 68) { + return PgsRendererMode.mainThread; + } + + + // Detect by features + const isWorkerSupported = this.isWorkerSupported(); + const isOffscreenCanvasSupported = this.isOffscreenCanvasSupported(); + if (isWorkerSupported) { + if (isOffscreenCanvasSupported) { + return PgsRendererMode.worker; + } else { + return PgsRendererMode.workerWithoutOffscreenCanvas; + } + } else { + return PgsRendererMode.mainThread; + } + } +} diff --git a/src/pgsRenderer.ts b/src/pgsRenderer.ts index 7f8e7f3..2ed3596 100644 --- a/src/pgsRenderer.ts +++ b/src/pgsRenderer.ts @@ -2,6 +2,9 @@ import {PgsRendererOptions} from "./pgsRendererOptions"; import {PgsRendererImpl} from "./pgsRendererImpl"; import {PgsRendererInWorkerWithOffscreenCanvas} from "./pgsRendererInWorkerWithOffscreenCanvas"; import {PgsRendererInMainThread} from "./pgsRendererInMainThread"; +import {PgsRendererInWorkerWithoutOffscreenCanvas} from "./pgsRendererInWorkerWithoutOffscreenCanvas"; +import {PgsRendererMode} from "./pgsRendererMode"; +import {BrowserSupport} from "./browserSupport"; /** * Renders PGS subtitle on-top of a video element using a canvas element. This also handles timestamp updates if a @@ -32,7 +35,7 @@ export class PgsRenderer { } - this.implementation = this.createPgsRendererFormPlatform(options); + this.implementation = this.createPgsRenderer(options); this.implementation.onTimestampsUpdated = () => { // Re-render the current subtitle if the timestamps were updates (e.g. through partial load). this.renderAtVideoTimestamp(); @@ -48,38 +51,19 @@ export class PgsRenderer { } /** - * Checks if the web worker is supported in the current environment. - */ - private isWorkerSupported(): boolean { - return !!window.Worker; - } - - /** - * Checks if the offscreen-canvas and `transferControlToOffscreen` are supported in the current environment. - */ - private isOffscreenCanvasSupported(): boolean { - return !!HTMLCanvasElement.prototype.transferControlToOffscreen; - } - - /** - * Performs a platform check and returns the optimal implementation for subtitle rendering. + * Creates the PGS renderer implementation for the given option. * @param options The PGS renderer options. */ - private createPgsRendererFormPlatform(options: PgsRendererOptions): PgsRendererImpl { - // Jellyfin still supports webOS 1.2 and Tizen 2.3 with real old WebKit and Chromium version. - // We can run a different fallback implementation if the offscreen canvas is not supported. - - const isWorkerSupported = this.isWorkerSupported(); - const isOffscreenCanvasSupported = this.isOffscreenCanvasSupported(); - console.log(`isWebWorkerSupported: ${isWorkerSupported}, isOffscreenCanvasSupported: ${isOffscreenCanvasSupported}`); - - // FIXME: I would like to use `PgsRendererInWorkerWithoutOffscreenCanvas` if the offscreen canvas is not - // available, but even the web worker thread won't start on webOS and I can't figure out why. - if (isWorkerSupported && isOffscreenCanvasSupported) { - return new PgsRendererInWorkerWithOffscreenCanvas(options, this.canvas); - } else { - - return new PgsRendererInMainThread(options, this.canvas); + private createPgsRenderer(options: PgsRendererOptions): PgsRendererImpl { + // Use the mode provided in the settings or detect by browser. + const mode = options.mode ?? BrowserSupport.getRendererModeByPlatform(); + switch (mode) { + case PgsRendererMode.worker: + return new PgsRendererInWorkerWithOffscreenCanvas(options, this.canvas); + case PgsRendererMode.workerWithoutOffscreenCanvas: + return new PgsRendererInWorkerWithoutOffscreenCanvas(options, this.canvas); + case PgsRendererMode.mainThread: + return new PgsRendererInMainThread(options, this.canvas); } } diff --git a/src/pgsRendererMode.ts b/src/pgsRendererMode.ts new file mode 100644 index 0000000..b9ce04a --- /dev/null +++ b/src/pgsRendererMode.ts @@ -0,0 +1,16 @@ +export enum PgsRendererMode { + /** + * A worker thread is used to load, build and render the subtitles. This requires OffscreenCanvas support. + */ + worker = 'worker', + + /** + * A worker thread is used to load and build the subtitles. They are rendered in the main thread. + */ + workerWithoutOffscreenCanvas = 'workerWithoutOffscreenCanvas', + + /** + * The subtitles are loaded, built and rendered in the main thread. + */ + mainThread = 'mainThread', +} diff --git a/src/pgsRendererOptions.ts b/src/pgsRendererOptions.ts index 4eaa9ef..95a8a5b 100644 --- a/src/pgsRendererOptions.ts +++ b/src/pgsRendererOptions.ts @@ -1,3 +1,5 @@ +import {PgsRendererMode} from "./pgsRendererMode"; + export interface PgsRendererOptions { /** * The video element to sync the subtitle to. @@ -25,4 +27,10 @@ export interface PgsRendererOptions { * The url to the worker javascript file. */ workerUrl?: string; + + /** + * The forced renderer mode. + * If not provided, the renderer checks the current browser to detect the best mode for your platform. + */ + mode?: PgsRendererMode; } diff --git a/tests/browserSupport.test.ts b/tests/browserSupport.test.ts new file mode 100644 index 0000000..6f36920 --- /dev/null +++ b/tests/browserSupport.test.ts @@ -0,0 +1,92 @@ +import {BrowserSupport} from "../src/browserSupport"; +import {PgsRendererMode} from "../src/pgsRendererMode"; + +const devices = [ + { + name: 'WebOS 1.2', + userAgent: 'Mozilla/5.0 (Web0S; Linux i686) AppleWebKit/537.41 (KHTML, like Gecko) Large Screen WebAppManager Safari/537.41', + worker: true, + offscreenCanvas: false, + expectedMode: PgsRendererMode.mainThread + }, + { + name: 'WebOS 5.0', + userAgent: 'Mozilla/5.0 (Web0S; Linux/SmartTV) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36 WebAppManager', + worker: true, + offscreenCanvas: false, + expectedMode: PgsRendererMode.mainThread + }, + { + name: 'Chrome 35', + userAgent: 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.3319.102 Safari/537.36', + worker: true, + offscreenCanvas: false, + expectedMode: PgsRendererMode.mainThread + }, + { + name: 'Chrome 68', + userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.0.0 Safari/537.36', + worker: true, + offscreenCanvas: false, + expectedMode: PgsRendererMode.workerWithoutOffscreenCanvas + }, + { + name: 'Chrome 128', + userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + worker: true, + offscreenCanvas: true, + expectedMode: PgsRendererMode.worker + }, + { + name: 'Firefox 24', + userAgent: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0', + worker: true, + offscreenCanvas: false, + expectedMode: PgsRendererMode.mainThread + }, + { + name: 'Firefox 130', + userAgent: 'Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0', + worker: true, + offscreenCanvas: true, + expectedMode: PgsRendererMode.worker + } +]; + +describe.each(devices)('on $name', (device) => { + beforeEach(() => { + Object.defineProperty(global, 'navigator', { + configurable: true, + value: { + userAgent: device.userAgent + } + }); + + Object.defineProperty(global, 'Worker', { + configurable: true, + value: device.worker ? {} : undefined, + }); + + Object.defineProperty(global, 'HTMLCanvasElement', { + configurable: true, + value: { + prototype: { + transferControlToOffscreen: device.offscreenCanvas ? () => {} : undefined + } + } + }); + }); + afterEach(() => jest.resetAllMocks()); + + it(`worker support is ${device.worker}`, () => { + expect(BrowserSupport.isWorkerSupported()).toBe(device.worker); + }); + + it(`offscreen canvas support is ${device.offscreenCanvas}`, () => { + expect(BrowserSupport.isOffscreenCanvasSupported()).toBe(device.offscreenCanvas); + }); + + it(`render mode is ${device.expectedMode}`, () => { + expect(BrowserSupport.getRendererModeByPlatform()).toBe(device.expectedMode); + }); +});