@lightningtv/renderer
Version:
Lightning 3 Renderer
280 lines (255 loc) • 8.72 kB
text/typescript
/*
* If not stated otherwise in this file or this component's LICENSE file the
* following copyright and licenses apply:
*
* Copyright 2023 Comcast Cable Communications Management, LLC.
*
* Licensed under the Apache License, Version 2.0 (the License);
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { CreateImageBitmapSupport } from '../lib/validateImageBitmap.js';
import { type TextureData } from '../textures/Texture.js';
type MessageCallback = [(value: any) => void, (reason: any) => void];
interface getImageReturn {
data: ImageBitmap;
premultiplyAlpha: boolean | null;
}
interface ImageWorkerMessage {
id: number;
src: string;
data: getImageReturn;
error: string;
sx: number | null;
sy: number | null;
sw: number | null;
sh: number | null;
}
/**
* Note that, within the createImageWorker function, we must only use ES5 code to keep it ES5-valid after babelifying, as
* the converted code of this section is converted to a blob and used as the js of the web worker thread.
*
* The createImageWorker function is a web worker that fetches an image from a URL and returns an ImageBitmap object.
* The eslint @typescript rule is disabled for the entire function because the function is converted to a blob and used as the
* js of the web worker thread, so the typescript syntax is not valid in this context.
*/
/* eslint-disable */
function createImageWorker() {
function hasAlphaChannel(mimeType: string) {
return mimeType.indexOf('image/png') !== -1;
}
function getImage(
src: string,
premultiplyAlpha: boolean | null,
x: number | null,
y: number | null,
width: number | null,
height: number | null,
options: {
supportsOptionsCreateImageBitmap: boolean;
supportsFullCreateImageBitmap: boolean;
},
): Promise<getImageReturn> {
return new Promise(function (resolve, reject) {
var supportsOptionsCreateImageBitmap =
options.supportsOptionsCreateImageBitmap;
var supportsFullCreateImageBitmap = options.supportsFullCreateImageBitmap;
var xhr = new XMLHttpRequest();
xhr.open('GET', src, true);
xhr.responseType = 'blob';
xhr.onload = function () {
// On most devices like WebOS and Tizen, the file protocol returns 0 while http(s) protocol returns 200
if (xhr.status !== 200 && xhr.status !== 0) {
return reject(new Error('Failed to load image: ' + xhr.statusText));
}
var blob = xhr.response;
var withAlphaChannel =
premultiplyAlpha !== undefined
? premultiplyAlpha
: hasAlphaChannel(blob.type);
// createImageBitmap with crop and options
if (
supportsFullCreateImageBitmap === true &&
width !== null &&
height !== null
) {
createImageBitmap(blob, x || 0, y || 0, width, height, {
premultiplyAlpha: withAlphaChannel ? 'premultiply' : 'none',
colorSpaceConversion: 'none',
imageOrientation: 'none',
})
.then(function (data) {
resolve({ data, premultiplyAlpha: premultiplyAlpha });
})
.catch(function (error) {
reject(error);
});
return;
} else if (
supportsOptionsCreateImageBitmap === false &&
supportsOptionsCreateImageBitmap === false
) {
// Fallback for browsers that do not support createImageBitmap with options
// this is supported for Chrome v50 to v52/54 that doesn't support options
createImageBitmap(blob)
.then(function (data) {
resolve({ data, premultiplyAlpha: premultiplyAlpha });
})
.catch(function (error) {
reject(error);
});
} else {
createImageBitmap(blob, {
premultiplyAlpha: withAlphaChannel ? 'premultiply' : 'none',
colorSpaceConversion: 'none',
imageOrientation: 'none',
})
.then(function (data) {
resolve({ data, premultiplyAlpha: premultiplyAlpha });
})
.catch(function (error) {
reject(error);
});
}
};
xhr.onerror = function () {
reject(
new Error('Network error occurred while trying to fetch the image.'),
);
};
xhr.send();
});
}
self.onmessage = (event) => {
var src = event.data.src;
var id = event.data.id;
var premultiplyAlpha = event.data.premultiplyAlpha;
var x = event.data.sx;
var y = event.data.sy;
var width = event.data.sw;
var height = event.data.sh;
// these will be set to true if the browser supports the createImageBitmap options or full
var supportsOptionsCreateImageBitmap = false;
var supportsFullCreateImageBitmap = false;
getImage(src, premultiplyAlpha, x, y, width, height, {
supportsOptionsCreateImageBitmap,
supportsFullCreateImageBitmap,
})
.then(function (data) {
self.postMessage({ id: id, src: src, data: data });
})
.catch(function (error) {
self.postMessage({ id: id, src: src, error: error.message });
});
};
}
/* eslint-enable */
export class ImageWorkerManager {
imageWorkersEnabled = true;
messageManager: Record<number, MessageCallback> = {};
workers: Worker[] = [];
workerIndex = 0;
nextId = 0;
constructor(
numImageWorkers: number,
createImageBitmapSupport: CreateImageBitmapSupport,
) {
this.workers = this.createWorkers(
numImageWorkers,
createImageBitmapSupport,
);
this.workers.forEach((worker) => {
worker.onmessage = this.handleMessage.bind(this);
});
}
private handleMessage(event: MessageEvent) {
const { id, data, error } = event.data as ImageWorkerMessage;
const msg = this.messageManager[id];
if (msg) {
const [resolve, reject] = msg;
delete this.messageManager[id];
if (error) {
reject(new Error(error));
} else {
resolve(data);
}
}
}
private createWorkers(
numWorkers = 1,
createImageBitmapSupport: CreateImageBitmapSupport,
): Worker[] {
let workerCode = `(${createImageWorker.toString()})()`;
// Replace placeholders with actual initialization values
if (createImageBitmapSupport.options === true) {
workerCode = workerCode.replace(
'var supportsOptionsCreateImageBitmap = false;',
'var supportsOptionsCreateImageBitmap = true;',
);
}
if (createImageBitmapSupport.full === true) {
workerCode = workerCode.replace(
'var supportsOptionsCreateImageBitmap = false;',
'var supportsOptionsCreateImageBitmap = true;',
);
workerCode = workerCode.replace(
'var supportsFullCreateImageBitmap = false;',
'var supportsFullCreateImageBitmap = true;',
);
}
workerCode = workerCode.replace('"use strict";', '');
const blob: Blob = new Blob([workerCode], {
type: 'application/javascript',
});
const blobURL: string = (self.URL ? URL : webkitURL).createObjectURL(blob);
const workers: Worker[] = [];
for (let i = 0; i < numWorkers; i++) {
workers.push(new Worker(blobURL));
}
return workers;
}
private getNextWorker(): Worker | undefined {
const worker = this.workers[this.workerIndex];
this.workerIndex = (this.workerIndex + 1) % this.workers.length;
return worker;
}
getImage(
src: string,
premultiplyAlpha: boolean | null,
sx: number | null,
sy: number | null,
sw: number | null,
sh: number | null,
): Promise<TextureData> {
return new Promise((resolve, reject) => {
try {
if (this.workers) {
const id = this.nextId++;
this.messageManager[id] = [resolve, reject];
const nextWorker = this.getNextWorker();
if (nextWorker) {
nextWorker.postMessage({
id,
src: src,
premultiplyAlpha,
sx,
sy,
sw,
sh,
});
}
}
} catch (error) {
reject(error);
}
});
}
}