mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 01:18:34 +00:00
Feature/shared web worker (#7)
* Added SharedWorker to the renderer. * test RenderEngine using shared workers. * Creating primitives for shared workers, the issue is this queued work thing is mainly what these are going to be used for. I should write this for any open cv operation or any media operations, this should be keeping things snappy.
This commit is contained in:
1 parent
f586e2e25c
commit
95ca2c333b
12 files changed
+10008
-66
No files matched your search
Generated
+9211
File diff suppressed because it is too large.
Load diff
@@ -12,6 +12,8 @@
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.1.3",
|
||||
"@preact/signals-react": "^2.2.0",
|
||||
"@techstark/opencv-js": "^4.10.0-release.1",
|
||||
"@zip.js/zip.js": "^2.7.52",
|
||||
"konva": "^9.3.14",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
+64
-8
@@ -2,10 +2,15 @@ import Konva from "konva";
|
||||
import { VideoNode } from "./Nodes/VideoNode";
|
||||
import { uiAccess } from "~/signals";
|
||||
import { uiEvents } from "~/signals";
|
||||
import { RenderEngine } from "./RenderEngine";
|
||||
import { RenderEngine } from "./RenderingPrimitives/RenderEngine";
|
||||
|
||||
import { toolbarMain } from "~/signals/uiAccess/toolbarMain";
|
||||
import { ToolbarMainButtonNames } from "~/components/features/ToolbarMain/enum";
|
||||
import {
|
||||
WorkFunction,
|
||||
ProgressData,
|
||||
WorkQueue,
|
||||
} from "./WorkerPrimitives/GenericWorker";
|
||||
import { SharedWorkerClient } from "./WorkerPrimitives/SharedWorkerClient";
|
||||
|
||||
export class Engine {
|
||||
private canvasReference: HTMLDivElement;
|
||||
@@ -53,11 +58,6 @@ export class Engine {
|
||||
this.addVideo(video.file);
|
||||
});
|
||||
|
||||
uiEvents.toolbarMain.AI_STYLIZE.onClick(async (event) => {
|
||||
// disable the buttons to debounce
|
||||
await this.renderEngine.startProcessing();
|
||||
});
|
||||
|
||||
// TODO: You may listen to all the image toolbar events here
|
||||
uiEvents.imageToolbar.MOVE.onClick(() => {
|
||||
console.log("move");
|
||||
@@ -77,12 +77,13 @@ export class Engine {
|
||||
"SLEEP",
|
||||
`${sleepytstart.getMinutes()}:${sleepytstart.getSeconds()}`,
|
||||
);
|
||||
await this.sleep(5000);
|
||||
await this.renderEngine.startProcessing();
|
||||
const sleeptend = new Date();
|
||||
console.log(
|
||||
"DONE",
|
||||
`${sleeptend.getMinutes()}:${sleeptend.getSeconds()}`,
|
||||
);
|
||||
|
||||
uiAccess.toolbarMain.changeButtonState(
|
||||
ToolbarMainButtonNames.AI_STYLIZE,
|
||||
{ disabled: false },
|
||||
@@ -100,6 +101,61 @@ export class Engine {
|
||||
this.stage.draw(); // Redraw the canvas
|
||||
}
|
||||
|
||||
// Sandbox is quickly a way to test your idea.
|
||||
public sandbox() {
|
||||
// How to use:
|
||||
// const exampleWorkFunction: WorkFunction<number, number> = async (
|
||||
// data: number,
|
||||
// reportProgress: (progress: number) => void,
|
||||
// ) => {
|
||||
// // Simulate some asynchronous work with progress reporting
|
||||
// for (let i = 0; i <= 100; i += 20) {
|
||||
// await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
// reportProgress(i);
|
||||
// }
|
||||
// return data * 2;
|
||||
// };
|
||||
|
||||
// const progressCallback = (progressData: ProgressData) => {
|
||||
// console.log(
|
||||
// `Job ${progressData.jobId} progress: ${progressData.progress}%`,
|
||||
// );
|
||||
// };
|
||||
|
||||
// const workQueue = new WorkQueue<number, number>(
|
||||
// exampleWorkFunction,
|
||||
// progressCallback,
|
||||
// );
|
||||
// workQueue.addWork(1, 10);
|
||||
// workQueue.addWork(2, 20);
|
||||
// workQueue.addWork(3, 30);
|
||||
|
||||
// const sharedWorker = new SharedWorker(
|
||||
// "src\\KonvaApp\\WorkerPrimitives\\NumberSharedWorker.ts",
|
||||
// {
|
||||
// type: "module",
|
||||
// },
|
||||
// );
|
||||
|
||||
// // Get the port for communication
|
||||
// let port = sharedWorker.port;
|
||||
// port.start();
|
||||
// // Set up the message event listener
|
||||
// port.onmessage = this.onMessage.bind(this);
|
||||
// port.postMessage({ jobID: 1, data: 2, isDoneStreaming: false });
|
||||
|
||||
const value = new SharedWorkerClient<number, number, number>(
|
||||
"src\\KonvaApp\\WorkerPrimitives\\NumberSharedWorker.ts",
|
||||
undefined,
|
||||
);
|
||||
value.send({ jobID: 1, data: 1, isDoneStreaming: false });
|
||||
}
|
||||
|
||||
public onMessage(event: MessageEvent) {
|
||||
console.log("Message From Shared Worker");
|
||||
console.log(event);
|
||||
}
|
||||
|
||||
public initializeStage(sceneToken: string) {
|
||||
// load canvas that was originaly saved TODO Save manager for resharing.
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { VideoNode } from "./Nodes/VideoNode";
|
||||
import { VideoNode } from "../Nodes/VideoNode";
|
||||
import Konva from "konva";
|
||||
|
||||
import { RenderTask, WorkerEvent } from "./RenderTask";
|
||||
|
||||
// https://www.aiseesoft.com/resource/phone-aspect-ratio-screen-resolution.html#:~:text=16%3A9%20Aspect%20Ratio
|
||||
export class RenderEngine {
|
||||
private videoNodes: VideoNode[];
|
||||
private offScreenCanvas: OffscreenCanvas;
|
||||
private context: OffscreenCanvasRenderingContext2D | null;
|
||||
|
||||
private offscreenCanvasCache: OffscreenCanvas[] = [];
|
||||
private isProcessing: boolean;
|
||||
|
||||
private frames: ImageBitmap[];
|
||||
@@ -18,6 +22,10 @@ export class RenderEngine {
|
||||
private positionX: number;
|
||||
private positionY: number;
|
||||
|
||||
private canUseSharedWorker: boolean;
|
||||
private sharedWorker: SharedWorker | undefined;
|
||||
private port: MessagePort | undefined;
|
||||
private maxFrames: number;
|
||||
constructor(videoLayer: Konva.Layer, offScreenCanvas: OffscreenCanvas) {
|
||||
this.videoNodes = [];
|
||||
|
||||
@@ -39,6 +47,7 @@ export class RenderEngine {
|
||||
|
||||
this.videoLayer = videoLayer;
|
||||
|
||||
this.port = undefined;
|
||||
const captureCanvas = new Konva.Rect({
|
||||
x: this.positionX,
|
||||
y: this.positionY,
|
||||
@@ -50,6 +59,8 @@ export class RenderEngine {
|
||||
draggable: false,
|
||||
});
|
||||
|
||||
this.maxFrames = 7 * 24;
|
||||
|
||||
// DEBUG ONLY
|
||||
// const rectangle = new Konva.Rect({
|
||||
// x: this.positionX,
|
||||
@@ -63,8 +74,69 @@ export class RenderEngine {
|
||||
// });
|
||||
//this.videoLayer.add(rectangle);
|
||||
this.videoLayer.add(captureCanvas);
|
||||
// set furest back
|
||||
// set back
|
||||
captureCanvas.setZIndex(0);
|
||||
|
||||
this.sharedWorker = undefined;
|
||||
this.canUseSharedWorker = false;
|
||||
this.setupSharedWorker();
|
||||
}
|
||||
|
||||
// Manage the worker
|
||||
onMessage(event: MessageEvent) {
|
||||
const message = event.data;
|
||||
console.log("Main script received:", message);
|
||||
// Send a response back to the worker
|
||||
if (!this.port) {
|
||||
return console.log("Undefined Worker");
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage() {
|
||||
if (!this.port) {
|
||||
return console.log("Undefined Worker");
|
||||
}
|
||||
this.port.postMessage("Hello from Main Script");
|
||||
}
|
||||
|
||||
async sendCanvasPayload(renderTask: RenderTask) {
|
||||
if (!this.port) {
|
||||
return console.log("Undefined Worker");
|
||||
}
|
||||
this.port.postMessage(renderTask);
|
||||
}
|
||||
|
||||
setupSharedWorker() {
|
||||
if (typeof SharedWorker !== "undefined") {
|
||||
this.offscreenCanvasCache = [];
|
||||
// canvas size should be defined here
|
||||
for (let i = 0; i < this.maxFrames; i++) {
|
||||
let offScreenCanvas = new OffscreenCanvas(this.width, this.height);
|
||||
this.offscreenCanvasCache.push(offScreenCanvas);
|
||||
}
|
||||
|
||||
console.log("Shared Workers are supported in this browser.");
|
||||
// Debug chrome://inspect/#workers
|
||||
// TODO ensure that the cors is solved for this.
|
||||
this.sharedWorker = new SharedWorker(
|
||||
"src\\KonvaApp\\RenderingPrimitives\\RenderSharedWorker.ts",
|
||||
{
|
||||
type: "module",
|
||||
},
|
||||
);
|
||||
|
||||
// Get the port for communication
|
||||
this.port = this.sharedWorker.port;
|
||||
this.port.start();
|
||||
// Set up the message event listener
|
||||
this.port.onmessage = this.onMessage.bind(this);
|
||||
|
||||
this.canUseSharedWorker = true;
|
||||
} else {
|
||||
console.log("Shared Workers are not supported in this browser.");
|
||||
// Handle the lack of Shared Worker support (e.g., fallback to another solution)
|
||||
this.canUseSharedWorker = false;
|
||||
}
|
||||
}
|
||||
|
||||
// This function uses a portion of the video layer to capture just the capture canvas.
|
||||
@@ -197,16 +269,41 @@ export class RenderEngine {
|
||||
} // End frame time
|
||||
this.videoLayer.draw();
|
||||
|
||||
// SCOPES the capture for the context
|
||||
// Correct size for the mobile canvas.
|
||||
this.offScreenCanvas.width = this.width;
|
||||
this.offScreenCanvas.height = this.height;
|
||||
// use main thread
|
||||
if (this.canUseSharedWorker === false) {
|
||||
// SCOPES the capture for the context
|
||||
// Correct size for the mobile canvas.
|
||||
this.offScreenCanvas.width = this.width;
|
||||
this.offScreenCanvas.height = this.height;
|
||||
if (this.context) {
|
||||
// This crops it starting at position X / Y where the mobile canvas is
|
||||
// Then picks the height and width range
|
||||
// then we draw it at 0,0,width and height of the canvas
|
||||
this.context.drawImage(
|
||||
this.videoLayer.canvas._canvas,
|
||||
this.positionX,
|
||||
this.positionY,
|
||||
this.width,
|
||||
this.height,
|
||||
0,
|
||||
0,
|
||||
this.width,
|
||||
this.height,
|
||||
);
|
||||
|
||||
if (this.context) {
|
||||
// This crops it starting at position X / Y where the mobile canvas is
|
||||
// Then picks the height and width range
|
||||
// then we draw it at 0,0,width and height of the canvas
|
||||
this.context.drawImage(
|
||||
const blob = await this.offScreenCanvas.convertToBlob({
|
||||
quality: 1.0,
|
||||
type: "image/jpeg",
|
||||
});
|
||||
|
||||
await this.blobToFile(blob, `${j}`);
|
||||
} // end of for each frame
|
||||
} else {
|
||||
console.log("Using Shared Worker");
|
||||
const canvas = this.offscreenCanvasCache[j];
|
||||
const context = canvas.getContext("2d");
|
||||
// decode on the shared webworker.
|
||||
context?.drawImage(
|
||||
this.videoLayer.canvas._canvas,
|
||||
this.positionX,
|
||||
this.positionY,
|
||||
@@ -217,56 +314,29 @@ export class RenderEngine {
|
||||
this.width,
|
||||
this.height,
|
||||
);
|
||||
const blob = await this.offScreenCanvas.convertToBlob({
|
||||
quality: 1.0,
|
||||
type: "image/jpeg",
|
||||
|
||||
this.sendCanvasPayload({
|
||||
jobID: 1,
|
||||
isDone: false,
|
||||
event: WorkerEvent.DATA,
|
||||
data: {
|
||||
offscreenCanvas: canvas,
|
||||
id: j,
|
||||
},
|
||||
});
|
||||
await this.blobToFile(blob, `${j}`);
|
||||
} // end of for each frame
|
||||
|
||||
if (j == this.maxFrames - 1) {
|
||||
this.sendCanvasPayload({
|
||||
jobID: 1,
|
||||
isDone: true,
|
||||
event: WorkerEvent.DATA,
|
||||
data: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//DO NOT USE: This code is to test whether or not we can seek frames on each video nodes.
|
||||
//To check the correctness of the seeking.
|
||||
private async processFrame() {
|
||||
if (!this.isProcessing) return;
|
||||
|
||||
for (let j = 0; j < this.videoNodes.length; j++) {
|
||||
const videoNode = this.videoNodes[j];
|
||||
|
||||
videoNode.stop();
|
||||
const numberOfFrames = videoNode.getNumberFrames();
|
||||
|
||||
for (let i = 0; i <= numberOfFrames; i++) {
|
||||
const frameTime = this.calculateFrameTime(i, videoNode.fps);
|
||||
|
||||
console.log(i);
|
||||
console.log(frameTime);
|
||||
console.log(videoNode.duration);
|
||||
|
||||
if (frameTime < videoNode.duration) {
|
||||
await videoNode.seek(frameTime);
|
||||
this.offScreenCanvas.width = videoNode.node.getSize().width;
|
||||
this.offScreenCanvas.height = videoNode.node.getSize().height;
|
||||
|
||||
if (this.context) {
|
||||
this.context.drawImage(
|
||||
videoNode.videoComponent,
|
||||
0,
|
||||
0,
|
||||
this.offScreenCanvas.width,
|
||||
this.offScreenCanvas.height,
|
||||
);
|
||||
console.log("Pushing");
|
||||
this.frames.push(this.offScreenCanvas.transferToImageBitmap());
|
||||
}
|
||||
} // end of if
|
||||
} // end of for.
|
||||
}
|
||||
|
||||
console.log(this.frames);
|
||||
}
|
||||
|
||||
private blobToFile(blob: Blob, index: string) {
|
||||
try {
|
||||
const link = document.createElement("a");
|
||||
@@ -283,3 +353,43 @@ export class RenderEngine {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// //DO NOT USE: This code is to test whether or not we can seek frames on each video nodes.
|
||||
// //To check the correctness of the seeking.
|
||||
// private async processFrame() {
|
||||
// if (!this.isProcessing) return;
|
||||
|
||||
// for (let j = 0; j < this.videoNodes.length; j++) {
|
||||
// const videoNode = this.videoNodes[j];
|
||||
|
||||
// videoNode.stop();
|
||||
// const numberOfFrames = videoNode.getNumberFrames();
|
||||
|
||||
// for (let i = 0; i <= numberOfFrames; i++) {
|
||||
// const frameTime = this.calculateFrameTime(i, videoNode.fps);
|
||||
|
||||
// console.log(i);
|
||||
// console.log(frameTime);
|
||||
// console.log(videoNode.duration);
|
||||
|
||||
// if (frameTime < videoNode.duration) {
|
||||
// await videoNode.seek(frameTime);
|
||||
// this.offScreenCanvas.width = videoNode.node.getSize().width;
|
||||
// this.offScreenCanvas.height = videoNode.node.getSize().height;
|
||||
|
||||
// if (this.context) {
|
||||
// this.context.drawImage(
|
||||
// videoNode.videoComponent,
|
||||
// 0,
|
||||
// 0,
|
||||
// this.offScreenCanvas.width,
|
||||
// this.offScreenCanvas.height,
|
||||
// );
|
||||
// console.log("Pushing");
|
||||
// this.frames.push(this.offScreenCanvas.transferToImageBitmap());
|
||||
// }
|
||||
// } // end of if
|
||||
// } // end of for.
|
||||
// }
|
||||
// console.log(this.frames);
|
||||
// }
|
||||
@@ -0,0 +1,132 @@
|
||||
// RenderSharedWorker.ts
|
||||
|
||||
import { BlobReader, BlobWriter, ZipWriter } from "@zip.js/zip.js";
|
||||
import { RenderTask } from "./RenderTask";
|
||||
|
||||
class FrameProcessorWorker {
|
||||
public imageType: string;
|
||||
|
||||
private zipFileWriter: BlobWriter;
|
||||
private zipWriter: ZipWriter<Blob>;
|
||||
|
||||
private decodeQueue: RenderTask[];
|
||||
|
||||
public isProcessing: boolean;
|
||||
|
||||
public final_length: number;
|
||||
public item_count: number;
|
||||
|
||||
public port: MessagePort;
|
||||
|
||||
constructor(port: MessagePort) {
|
||||
this.final_length = 0;
|
||||
|
||||
this.imageType = "image/jpeg";
|
||||
// Zipping
|
||||
this.decodeQueue = [];
|
||||
this.zipFileWriter = new BlobWriter(this.imageType);
|
||||
this.zipWriter = new ZipWriter(this.zipFileWriter);
|
||||
|
||||
this.isProcessing = false;
|
||||
this.item_count = 0;
|
||||
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
async startZipProcessQueue(): Promise<void> {
|
||||
if (this.decodeQueue.length > 0 && this.isProcessing == false) {
|
||||
this.isProcessing = true;
|
||||
|
||||
const task = this.decodeQueue.shift();
|
||||
if (task) {
|
||||
const { offscreenCanvas, id } = task;
|
||||
// Proceed with processing the task
|
||||
try {
|
||||
// Convert the canvas content to a Blob
|
||||
const blob = await offscreenCanvas.convertToBlob({
|
||||
quality: 1.0,
|
||||
type: this.imageType,
|
||||
});
|
||||
|
||||
// Send the Blob back to the main thread
|
||||
console.log(`Zipping Frame ${id} to Zip`);
|
||||
const name = String(id).padStart(5, "0"); // '0009'
|
||||
await this.zipWriter.add(`${name}.jpg`, new BlobReader(blob));
|
||||
|
||||
this.item_count++;
|
||||
} catch (error) {
|
||||
console.error("Error processing image:", error);
|
||||
self.postMessage({ error: (error as Error).message }); //
|
||||
} finally {
|
||||
this.isProcessing = false;
|
||||
}
|
||||
} else {
|
||||
console.log("Queue is empty, no task to process.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// have this function loop
|
||||
setTimeout(this.startZipProcessQueue.bind(this), 0);
|
||||
}
|
||||
|
||||
async zipFile() {
|
||||
// ensure that this queue is done before zipping
|
||||
console.log(`Waiting to Finish Decoding: ${this.decodeQueue.length}`);
|
||||
console.log(`Decoded Count: ${this.item_count}`);
|
||||
console.log(`Final Length: ${this.final_length}`);
|
||||
if (this.item_count === this.final_length) {
|
||||
console.log("Zipping");
|
||||
const result = await this.zipWriter.close();
|
||||
self.postMessage({ result: result });
|
||||
// reset the item count and final length
|
||||
this.item_count = 0;
|
||||
this.final_length = 0;
|
||||
|
||||
// Zipping
|
||||
this.decodeQueue = [];
|
||||
this.zipFileWriter = new BlobWriter(this.imageType);
|
||||
this.zipWriter = new ZipWriter(this.zipFileWriter);
|
||||
this.isProcessing = false;
|
||||
return;
|
||||
} else {
|
||||
setTimeout(this.zipFile.bind(this), 0);
|
||||
}
|
||||
}
|
||||
|
||||
public enqueueTask(offscreenCanvas: OffscreenCanvas, id: number): void {
|
||||
this.decodeQueue.push({ offscreenCanvas, id });
|
||||
}
|
||||
}
|
||||
|
||||
self.onconnect = (e: any) => {
|
||||
const port = e.ports[0];
|
||||
console.log("RenderSharedWorker Started");
|
||||
const runner = new FrameProcessorWorker(port);
|
||||
|
||||
let start = false;
|
||||
|
||||
port.addEventListener("message", async (event: any) => {
|
||||
//const { type, offscreenCanvas, frame, totalFrames, zip } = event.data;
|
||||
console.log(event.data);
|
||||
|
||||
//console.log(`${type}: ${frame} ${zip}`);
|
||||
|
||||
if (start == false) {
|
||||
start = true;
|
||||
//runner.imageType = type;
|
||||
await runner.startZipProcessQueue();
|
||||
}
|
||||
|
||||
// if (zip) {
|
||||
// await runner.zipFile();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// runner.enqueueTask(offscreenCanvas, frame);
|
||||
|
||||
const workerResult = "Got Message for Startup";
|
||||
port.postMessage(workerResult);
|
||||
});
|
||||
|
||||
port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter.
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { bool } from "@techstark/opencv-js";
|
||||
|
||||
export enum WorkerEvent {
|
||||
START = "start",
|
||||
PROGRESS = "progress",
|
||||
COMPLETE = "complete",
|
||||
DATA = "data",
|
||||
ERROR = "error",
|
||||
}
|
||||
|
||||
export interface RenderTaskPayload {
|
||||
offscreenCanvas: OffscreenCanvas;
|
||||
id: number;
|
||||
}
|
||||
|
||||
// RenderSharedWorker.ts
|
||||
export interface RenderTask {
|
||||
jobID: number;
|
||||
isDone: bool;
|
||||
event: WorkerEvent;
|
||||
data: RenderTaskPayload | undefined;
|
||||
}
|
||||
|
||||
export interface RenderTaskProgress {
|
||||
isProcessing: boolean;
|
||||
progress: number;
|
||||
jobID: number;
|
||||
event: WorkerEvent;
|
||||
}
|
||||
|
||||
export interface RenderTaskResult {
|
||||
event: Worker;
|
||||
jobID: number;
|
||||
data: {
|
||||
mediaUrl: string;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// Item in and Result of the Promise, the response is a boolean
|
||||
// that will dictate whether we should return the result to the main thread.
|
||||
|
||||
import { ResponseType, SharedWorkerResponse } from "./SharedWorkerBase";
|
||||
|
||||
// Does work with the data and returns progress and any progress data.
|
||||
export type WorkFunction<I, R, P> = (
|
||||
workItem: I,
|
||||
reportProgress: (progress: number, data: P) => void,
|
||||
) => Promise<[R, boolean]>;
|
||||
|
||||
// Work Item with Type T
|
||||
export interface WorkItem<T> {
|
||||
jobID: number;
|
||||
data: T;
|
||||
finishedStreamingInput: boolean;
|
||||
}
|
||||
|
||||
// Work Result R
|
||||
export interface WorkResult<R> {
|
||||
jobID: number;
|
||||
data: R;
|
||||
}
|
||||
|
||||
// Streamed Progress with or without data
|
||||
export interface ProgressData<P> {
|
||||
jobID: number;
|
||||
progress: number;
|
||||
data: P | undefined;
|
||||
}
|
||||
|
||||
export enum WorkerEvent {
|
||||
START = "start",
|
||||
PROGRESS = "progress",
|
||||
COMPLETE = "complete",
|
||||
DATA = "data",
|
||||
ERROR = "error",
|
||||
}
|
||||
|
||||
// Handles streaming work in a queue.
|
||||
export class WorkQueue<I, R, P> {
|
||||
private queue: WorkItem<I>[] = [];
|
||||
private isProcessing: boolean = false;
|
||||
|
||||
// Streaming workers wait for a stop before they return a result used for frames.
|
||||
// if not then it produces one result for one work item off the main thread.
|
||||
public isStreamingWorker: boolean = true;
|
||||
|
||||
protected workFunction: WorkFunction<I, R, P> | undefined;
|
||||
protected resultFunction: (result: WorkResult<R>) => void;
|
||||
protected errorFunction: (error: SharedWorkerResponse<I, R>) => void;
|
||||
protected progressCallback: (
|
||||
progressData: ProgressData<P>,
|
||||
) => void | undefined;
|
||||
|
||||
constructor(
|
||||
workFunction: WorkFunction<I, R, P>,
|
||||
progressCallback: (progressData: ProgressData<P>) => void,
|
||||
resultFunction: (result: WorkResult<R>) => void,
|
||||
errorFunction: (error: SharedWorkerResponse<I, R>) => void,
|
||||
) {
|
||||
this.workFunction = workFunction;
|
||||
this.progressCallback = progressCallback;
|
||||
this.resultFunction = resultFunction;
|
||||
this.errorFunction = errorFunction;
|
||||
}
|
||||
|
||||
async addWork(workItem: WorkItem<I>): Promise<void> {
|
||||
this.queue.push(workItem);
|
||||
}
|
||||
|
||||
private async processQueue(): Promise<void> {
|
||||
while (this.queue.length > 0) {
|
||||
const workItem = this.queue.shift();
|
||||
if (workItem) {
|
||||
try {
|
||||
// The Data E here is used to stream back any extra images or anything.
|
||||
const reportProgress = (progress: number, data: P) => {
|
||||
this.progressCallback({
|
||||
jobID: workItem.jobID,
|
||||
progress: progress,
|
||||
data: data, // could be url's some kind of data
|
||||
});
|
||||
};
|
||||
if (!this.workFunction) {
|
||||
console.log("Work Function is Null Generic Worker");
|
||||
return;
|
||||
}
|
||||
const result = await this.workFunction(workItem.data, reportProgress);
|
||||
// should report
|
||||
if (result[1] === true) {
|
||||
this.reportResult({ jobID: workItem.jobID, data: result[0] });
|
||||
} else {
|
||||
console.log("Work Function is not done producing result");
|
||||
}
|
||||
} catch (error) {
|
||||
// catches errors from the worker and sends it back as an error
|
||||
console.error(`Error processing job ${workItem.jobID}:`, error);
|
||||
if (!this.errorFunction) {
|
||||
console.log("Didn't Setup Error Function in Generic Worker");
|
||||
return;
|
||||
}
|
||||
|
||||
this.errorFunction({
|
||||
jobID: workItem.jobID,
|
||||
responseType: ResponseType.error,
|
||||
data: error as string,
|
||||
});
|
||||
// proprogate to the error callback
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("Done Processing Waiting");
|
||||
setTimeout(this.processQueue.bind(this), 500);
|
||||
}
|
||||
|
||||
public start() {
|
||||
if (this.isProcessing) {
|
||||
console.log("Started Already");
|
||||
return;
|
||||
}
|
||||
this.isProcessing = true;
|
||||
this.processQueue();
|
||||
}
|
||||
|
||||
public reportResult(result: WorkResult<R>): void {
|
||||
console.log(`Job ${result.jobID} completed with result:`, result.data);
|
||||
// You can replace this with any reporting mechanism you need
|
||||
if (!this.resultFunction) {
|
||||
console.log("Missing Result Function in Generic Worker");
|
||||
return;
|
||||
}
|
||||
// passes off to sharedworkerbase to be overriden
|
||||
this.resultFunction(result);
|
||||
}
|
||||
}
|
||||
|
||||
// How to use:
|
||||
// const exampleWorkFunction: WorkFunction<number, number> = async (
|
||||
// data: number,
|
||||
// reportProgress: (progress: number) => void,
|
||||
// ) => {
|
||||
// // Simulate some asynchronous work with progress reporting
|
||||
// for (let i = 0; i <= 100; i += 20) {
|
||||
// await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
// reportProgress(i);
|
||||
// }
|
||||
// return data * 2;
|
||||
// };
|
||||
|
||||
// const progressCallback = (progressData: ProgressData) => {
|
||||
// console.log(`Job ${progressData.jobId} progress: ${progressData.progress}%`);
|
||||
// };
|
||||
|
||||
// const workQueue = new WorkQueue<number, number>(
|
||||
// exampleWorkFunction,
|
||||
// progressCallback,
|
||||
// );
|
||||
|
||||
// workQueue.addWork(1, 10);
|
||||
// workQueue.addWork(2, 20);
|
||||
// workQueue.addWork(3, 30);
|
||||
@@ -0,0 +1,77 @@
|
||||
import { ProgressData, WorkResult } from "./GenericWorker";
|
||||
import {
|
||||
SharedWorkerBase,
|
||||
SharedWorkerRequest,
|
||||
SharedWorkerResponse,
|
||||
ResponseType,
|
||||
} from "./SharedWorkerBase";
|
||||
|
||||
class SharedNumberWorker extends SharedWorkerBase<number, number, number> {
|
||||
constructor(port: MessagePort) {
|
||||
super(port);
|
||||
this.setup(this.workFunction.bind(this), this.progressFunction.bind(this));
|
||||
}
|
||||
// Data here will be shipped off for progressive loading
|
||||
async workFunction(
|
||||
item: number,
|
||||
reportProgress: (progress: number, data: number) => void,
|
||||
): Promise<[number, boolean]> {
|
||||
console.log(`Working Item ${item}`);
|
||||
|
||||
for (let i = 0; i < 1000; i++) {}
|
||||
reportProgress(item, 100);
|
||||
|
||||
return [5, true];
|
||||
}
|
||||
|
||||
progressFunction(progress: ProgressData<number>) {
|
||||
console.log(
|
||||
`Progress Function JobID:${progress.jobID} Data:${progress.data} Progress:${progress.progress}`,
|
||||
);
|
||||
|
||||
// send out to node as a worker response
|
||||
this.send({
|
||||
jobID: progress.jobID,
|
||||
responseType: ResponseType.progress,
|
||||
data: progress.data,
|
||||
});
|
||||
}
|
||||
|
||||
reportResult(result: WorkResult<number>) {
|
||||
console.log(`Result: jobID:${result.jobID} result:${result.data}`);
|
||||
this.send({
|
||||
jobID: result.jobID,
|
||||
responseType: ResponseType.result,
|
||||
data: result.data,
|
||||
});
|
||||
}
|
||||
|
||||
async receive(request: SharedWorkerRequest<number>) {
|
||||
console.log("Received Request");
|
||||
console.log(request);
|
||||
this.submitWork({
|
||||
jobID: request.jobID,
|
||||
data: request.data,
|
||||
isDoneStreaming: request.isDoneStreaming,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// This is a copy paste to create a worker now.
|
||||
self.onconnect = (e: any) => {
|
||||
const port = e.ports[0];
|
||||
console.log("NumberSharedWorker Started");
|
||||
let worker: SharedNumberWorker | undefined = undefined;
|
||||
let started = false;
|
||||
|
||||
if (started === false) {
|
||||
started = true;
|
||||
worker = new SharedNumberWorker(port);
|
||||
worker.start();
|
||||
}
|
||||
|
||||
// Response For Start.
|
||||
const workerResult = "Number Shared Worker Started";
|
||||
port.postMessage(workerResult);
|
||||
port.start(); // Required when using addEventListener. Otherwise called implicitly by onmessage setter.
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
WorkFunction,
|
||||
ProgressData,
|
||||
WorkQueue,
|
||||
WorkResult,
|
||||
} from "./GenericWorker";
|
||||
|
||||
// Protocol
|
||||
export enum ResponseType {
|
||||
progress,
|
||||
result,
|
||||
error,
|
||||
}
|
||||
|
||||
export enum RequestType {
|
||||
cancel,
|
||||
start,
|
||||
startStream,
|
||||
endStream,
|
||||
}
|
||||
|
||||
// These are the containers to send data over the ports
|
||||
// where R is response data
|
||||
// where P is progress data
|
||||
// where I is item data
|
||||
export interface SharedWorkerResponse<R, P> {
|
||||
jobID: number;
|
||||
responseType: ResponseType;
|
||||
data: R | P | string | undefined;
|
||||
}
|
||||
|
||||
export interface SharedWorkerRequest<I> {
|
||||
jobID: number;
|
||||
data: I;
|
||||
isDoneStreaming: boolean;
|
||||
}
|
||||
|
||||
// Where I is the work item data
|
||||
// Where R is the response data
|
||||
// Where P is the progress data
|
||||
export class SharedWorkerBase<I, R, P> {
|
||||
private port: MessagePort;
|
||||
protected workQueue: WorkQueue<I, R, P> | undefined;
|
||||
|
||||
constructor(port: MessagePort) {
|
||||
this.port = port;
|
||||
this.workQueue = undefined;
|
||||
this.port.onmessage = this.receiveFromPort.bind(this);
|
||||
}
|
||||
|
||||
setup(
|
||||
workFunction: WorkFunction<I, R, P> | undefined,
|
||||
progressCallback: (progressData: ProgressData<P>) => void,
|
||||
) {
|
||||
if (!workFunction) {
|
||||
console.log("Work Function is Undefined.");
|
||||
return;
|
||||
}
|
||||
this.workQueue = new WorkQueue<I, R, P>(
|
||||
workFunction,
|
||||
progressCallback,
|
||||
this.reportResult.bind(this),
|
||||
this.errorFunction.bind(this),
|
||||
);
|
||||
}
|
||||
|
||||
// Override
|
||||
public errorFunction(error: SharedWorkerResponse<I, R>) {
|
||||
console.log("Please Override Function");
|
||||
}
|
||||
|
||||
// Call to start the worker
|
||||
public async start() {
|
||||
if (!this.workQueue) {
|
||||
console.log("Work Queue Not Initialized");
|
||||
return;
|
||||
}
|
||||
this.workQueue.start();
|
||||
}
|
||||
|
||||
protected async send(data: SharedWorkerResponse<R, P>) {
|
||||
this.port.postMessage(data);
|
||||
}
|
||||
|
||||
// Must override in SharedWorker subclass
|
||||
public async receive(request: SharedWorkerRequest<I>) {
|
||||
// override this
|
||||
console.log("receive Override in subclass");
|
||||
}
|
||||
|
||||
public async receiveFromPort(event: any) {
|
||||
console.log(`UnWrapping Event ${event}`);
|
||||
this.receive(event.data);
|
||||
}
|
||||
|
||||
public async submitWork(request: SharedWorkerRequest<I>) {
|
||||
if (!this.workQueue) {
|
||||
console.log("Missing work queue cannot submit work");
|
||||
return;
|
||||
}
|
||||
this.workQueue.addWork({
|
||||
jobID: request.jobID,
|
||||
data: request.data,
|
||||
finishedStreamingInput: request.isDoneStreaming,
|
||||
});
|
||||
}
|
||||
|
||||
// override this for subclass;
|
||||
public reportResult(result: WorkResult<R>): void {
|
||||
console.log("Must Override reportResult");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { SharedWorkerRequest, SharedWorkerResponse } from "./SharedWorkerBase";
|
||||
|
||||
export class SharedWorkerClient<I, R, P> {
|
||||
private port: MessagePort;
|
||||
private sharedWorker: SharedWorker;
|
||||
|
||||
private messageReceived: (response: SharedWorkerResponse<R, P>) => void;
|
||||
constructor(
|
||||
workerPath: string,
|
||||
messageReceived: (response: SharedWorkerResponse<R, P>) => void,
|
||||
) {
|
||||
// "src\\KonvaApp\\WorkerPrimitives\\NumberSharedWorker.ts"
|
||||
this.sharedWorker = new SharedWorker(workerPath, {
|
||||
type: "module",
|
||||
});
|
||||
this.port = this.sharedWorker.port;
|
||||
// make sure to bind this for this from the invoker
|
||||
this.port.onmessage = this.onMessage.bind(this);
|
||||
this.port.start();
|
||||
}
|
||||
|
||||
async onMessage(event: MessageEvent) {
|
||||
// returns progress and returns result need to check error tomorrow then were good to go.
|
||||
console.log(`GOTBACK ${JSON.stringify(event.data)}`);
|
||||
}
|
||||
|
||||
async send(sharedWorkerRequest: SharedWorkerRequest<I>) {
|
||||
this.port.postMessage(sharedWorkerRequest);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@ import { Engine } from "./Engine";
|
||||
export { Engine as EngineType };
|
||||
export const KonvaApp = (element: HTMLDivElement) => {
|
||||
const engine = new Engine(element);
|
||||
engine.initializeStage("");
|
||||
|
||||
engine.sandbox();
|
||||
//engine.initializeStage("");
|
||||
|
||||
return engine;
|
||||
};
|
||||
@@ -936,6 +936,11 @@
|
||||
resolved "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.10.5.tgz"
|
||||
integrity sha512-WlJp8CipNzBz8Q4g159uMfswYHZ8p1+xJwLBdnbBTcSSu0zVoY6i27Suw5J0Y88YBnJ9jHR8jJMH/qSuZWWfNw==
|
||||
|
||||
"@techstark/opencv-js@^4.10.0-release.1":
|
||||
version "4.10.0-release.1"
|
||||
resolved "https://registry.npmjs.org/@techstark/opencv-js/-/opencv-js-4.10.0-release.1.tgz"
|
||||
integrity sha512-S4XELidRiQeA0q1s9VQLo540wCxUo24r1O4C+LqZ6llX+sPCXvZCPv3Ice8dEIr0uavyZ8YZeKXSBdDgMXSXjw==
|
||||
|
||||
"@tufjs/canonical-json@2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz"
|
||||
@@ -1109,6 +1114,11 @@
|
||||
"@types/babel__core" "^7.20.5"
|
||||
react-refresh "^0.14.2"
|
||||
|
||||
"@zip.js/zip.js@^2.7.52":
|
||||
version "2.7.52"
|
||||
resolved "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.52.tgz"
|
||||
integrity sha512-+5g7FQswvrCHwYKNMd/KFxZSObctLSsQOgqBSi0LzwHo3li9Eh1w5cF5ndjQw9Zbr3ajVnd2+XyiX85gAetx1Q==
|
||||
|
||||
abbrev@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz"
|
||||
|
||||
Reference in new issue
Block a user