Added an optional mode setting to force a custom worker mode.

Handled browser specific edge cases for older WebOS versions.
This commit is contained in:
David Schulte
2024-09-13 22:33:02 +02:00
parent f0586396ae
commit ac12e5aef5
5 changed files with 202 additions and 31 deletions
+71
View File
@@ -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;
}
}
}
+15 -31
View File
@@ -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);
}
}
+16
View File
@@ -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',
}
+8
View File
@@ -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;
}
+92
View File
@@ -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);
});
});