@viji-dev/core 0.4.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ class VijiCoreError extends Error {
6
6
  this.name = "VijiCoreError";
7
7
  }
8
8
  }
9
- const inlineScriptCode = '"use strict";\n(() => {\n // src/iframe/protocol.ts\n var PROTOCOL_VERSION = 1;\n function collectTransferables(msg) {\n switch (msg.type) {\n case "init": {\n const m = msg;\n const out = [];\n if (m.data?.canvas) out.push(m.data.canvas);\n const wasm = m.data?.cvWasmFiles;\n if (wasm) {\n out.push(\n wasm.simdLoaderJs,\n wasm.simdBinary,\n wasm.nosimdLoaderJs,\n wasm.nosimdBinary\n );\n }\n return out;\n }\n case "video-canvas-setup": {\n const m = msg;\n return m.data?.offscreenCanvas ? [m.data.offscreenCanvas] : [];\n }\n case "video-frame-update": {\n const m = msg;\n return m.data?.imageBitmap ? [m.data.imageBitmap] : [];\n }\n case "video-frame-direct": {\n const m = msg;\n return m.data?.imageBitmap ? [m.data.imageBitmap] : [];\n }\n case "parameter-update": {\n const m = msg;\n return m.data?.transferList ?? [];\n }\n case "parameter-batch-update": {\n const m = msg;\n return m.data?.transferList ?? [];\n }\n case "audio-analysis-update": {\n const m = msg;\n const out = [];\n if (m.data?.frequencyData?.buffer) out.push(m.data.frequencyData.buffer);\n if (m.data?.waveformData?.buffer) out.push(m.data.waveformData.buffer);\n return out;\n }\n case "capture-frame-result":\n case "auto-capture-result": {\n const data = msg.data;\n return data?.bitmap ? [data.bitmap] : [];\n }\n default:\n return [];\n }\n }\n\n // src/iframe/inline-script.ts\n var hostOrigin = null;\n var hostWindow = null;\n var worker = null;\n var workerBlobUrl = null;\n var canvas = null;\n var domHandlersAttached = false;\n var sensorHandlersAttached = false;\n function whenDomReady() {\n if (document.readyState === "loading") {\n return new Promise((resolve) => {\n document.addEventListener("DOMContentLoaded", () => resolve(), {\n once: true\n });\n });\n }\n return Promise.resolve();\n }\n async function announceReady() {\n await whenDomReady();\n const msg = {\n type: "iframe-ready",\n protocolVersion: PROTOCOL_VERSION,\n environment: {\n crossOriginIsolated: window.crossOriginIsolated,\n isSecureContext: window.isSecureContext,\n origin: self.origin\n }\n };\n window.parent.postMessage(msg, "*");\n }\n function installHostListener() {\n window.addEventListener("message", (event) => {\n if (event.source !== window.parent) return;\n if (hostOrigin === null) {\n hostOrigin = event.origin;\n hostWindow = window.parent;\n } else if (event.origin !== hostOrigin) {\n return;\n }\n handleHostMessage(event.data).catch((err) => {\n reportError(err);\n });\n });\n }\n function postToHost(msg, transfer = []) {\n if (hostOrigin === null || hostWindow === null) {\n return;\n }\n const targetOrigin = hostOrigin === "null" ? "*" : hostOrigin;\n hostWindow.postMessage(msg, targetOrigin, transfer);\n }\n function reportError(err) {\n const e = err;\n const msg = e?.stack ? {\n type: "iframe-error",\n message: e.message ?? String(err),\n stack: e.stack\n } : {\n type: "iframe-error",\n message: e?.message ?? String(err)\n };\n emitToHost(msg);\n }\n function reportWorkerOnError(e) {\n const msg = {\n type: "iframe-error",\n message: e.message ? `Worker error: ${e.message}` : "Worker error: (no detail \\u2014 likely cross-origin opacity)",\n ...e.error && e.error.stack ? { stack: e.error.stack } : {},\n ...e.filename ? { filename: e.filename } : {},\n ...e.lineno ? { lineno: e.lineno } : {},\n ...e.colno ? { colno: e.colno } : {}\n };\n emitToHost(msg);\n }\n function emitToHost(msg) {\n if (hostOrigin && hostWindow) {\n const targetOrigin = hostOrigin === "null" ? "*" : hostOrigin;\n hostWindow.postMessage(msg, targetOrigin);\n } else {\n window.parent.postMessage(msg, "*");\n }\n }\n async function handleHostMessage(msg) {\n switch (msg.type) {\n case "viji-bootstrap":\n await handleBootstrap(msg);\n return;\n case "viji-host-message":\n relayHostToWorker(msg.payload);\n return;\n case "viji-resize":\n handleResize(msg.width, msg.height);\n return;\n case "viji-enable-sensors":\n setSensorsEnabled(msg.enabled);\n return;\n case "viji-enable-interaction":\n setInteractionEnabled(msg.enabled);\n return;\n case "viji-terminate":\n handleTerminate();\n return;\n }\n }\n async function handleBootstrap(msg) {\n if (worker) {\n return;\n }\n canvas = document.createElement("canvas");\n canvas.id = "viji-canvas";\n canvas.width = msg.init.canvasWidth;\n canvas.height = msg.init.canvasHeight;\n canvas.style.width = "100%";\n canvas.style.height = "100%";\n canvas.style.display = "block";\n canvas.tabIndex = 0;\n canvas.style.outline = "none";\n document.body.appendChild(canvas);\n const offscreen = canvas.transferControlToOffscreen();\n workerBlobUrl = URL.createObjectURL(\n new Blob([msg.workerCode], { type: "application/javascript" })\n );\n worker = new Worker(workerBlobUrl, { type: "classic" });\n worker.onerror = (e) => {\n reportWorkerOnError(e);\n };\n worker.onmessage = (event) => {\n relayWorkerToHost(event.data);\n };\n const initId = `init_${Date.now()}`;\n const initMsg = {\n type: "init",\n id: initId,\n timestamp: Date.now(),\n data: {\n canvas: offscreen,\n isHeadless: msg.init.isHeadless,\n sc: msg.init.sc,\n rh: msg.init.rh,\n cvWasmFiles: msg.wasmFiles\n }\n };\n const initResponse = waitForWorkerMessage((m) => {\n return m && m.type === "init-response" && m.id === initId;\n });\n worker.postMessage(initMsg, [\n offscreen,\n msg.wasmFiles.simdLoaderJs,\n msg.wasmFiles.simdBinary,\n msg.wasmFiles.nosimdLoaderJs,\n msg.wasmFiles.nosimdBinary\n ]);\n await initResponse;\n if (msg.init.allowInteraction && !msg.init.isHeadless) {\n setInteractionEnabled(true);\n }\n if (msg.init.allowSensors && !msg.init.isHeadless) {\n setSensorsEnabled(true);\n }\n if (canvas) {\n canvas.addEventListener("contextmenu", (e) => e.preventDefault());\n }\n postToHost({ type: "viji-ready" });\n }\n function waitForWorkerMessage(predicate) {\n return new Promise((resolve, reject) => {\n if (!worker) {\n reject(new Error("worker not spawned"));\n return;\n }\n const w = worker;\n const onceListener = (event) => {\n if (predicate(event.data)) {\n w.removeEventListener("message", onceListener);\n resolve(event.data);\n }\n };\n w.addEventListener("message", onceListener);\n });\n }\n function relayHostToWorker(payload) {\n if (!worker || !payload) return;\n try {\n const transfer = collectTransferables(payload);\n worker.postMessage(payload, transfer);\n } catch (err) {\n reportError(err);\n }\n }\n function relayWorkerToHost(payload) {\n if (!payload) return;\n try {\n const transfer = collectTransferables(payload);\n postToHost(\n { type: "worker-message", payload },\n transfer\n );\n } catch (err) {\n reportError(err);\n }\n }\n function handleResize(width, height) {\n if (!worker) return;\n worker.postMessage({\n type: "resolution-update",\n id: `resize_${Date.now()}`,\n timestamp: Date.now(),\n data: {\n effectiveWidth: width,\n effectiveHeight: height,\n displayScale: 1\n }\n });\n }\n var isMouseInCanvas = false;\n function setInteractionEnabled(enabled) {\n if (enabled === domHandlersAttached) return;\n if (!canvas) return;\n if (enabled) {\n canvas.addEventListener("mousedown", handleMouseEvent, { passive: false });\n canvas.addEventListener("mousemove", handleMouseEvent, { passive: false });\n canvas.addEventListener("mouseup", handleMouseEvent, { passive: false });\n canvas.addEventListener("mouseenter", handleMouseEnter, { passive: false });\n canvas.addEventListener("mouseleave", handleMouseLeave, { passive: false });\n canvas.addEventListener("wheel", handleWheelEvent, { passive: false });\n canvas.addEventListener("touchstart", handleTouchEvent, { passive: false });\n canvas.addEventListener("touchmove", handleTouchEvent, { passive: false });\n canvas.addEventListener("touchend", handleTouchEvent, { passive: false });\n canvas.addEventListener("touchcancel", handleTouchEvent, { passive: false });\n document.addEventListener("keydown", handleKeyboardEvent, { passive: false });\n document.addEventListener("keyup", handleKeyboardEvent, { passive: false });\n canvas.addEventListener("mousedown", focusCanvas);\n canvas.addEventListener("touchstart", focusCanvas);\n domHandlersAttached = true;\n } else {\n canvas.removeEventListener("mousedown", handleMouseEvent);\n canvas.removeEventListener("mousemove", handleMouseEvent);\n canvas.removeEventListener("mouseup", handleMouseEvent);\n canvas.removeEventListener("mouseenter", handleMouseEnter);\n canvas.removeEventListener("mouseleave", handleMouseLeave);\n canvas.removeEventListener("wheel", handleWheelEvent);\n canvas.removeEventListener("touchstart", handleTouchEvent);\n canvas.removeEventListener("touchmove", handleTouchEvent);\n canvas.removeEventListener("touchend", handleTouchEvent);\n canvas.removeEventListener("touchcancel", handleTouchEvent);\n document.removeEventListener("keydown", handleKeyboardEvent);\n document.removeEventListener("keyup", handleKeyboardEvent);\n canvas.removeEventListener("mousedown", focusCanvas);\n canvas.removeEventListener("touchstart", focusCanvas);\n domHandlersAttached = false;\n }\n }\n function focusCanvas() {\n if (canvas) canvas.focus();\n }\n function dualRoute(workerType, data) {\n if (worker) {\n worker.postMessage({\n type: workerType,\n id: `${workerType}_${Date.now()}`,\n timestamp: Date.now(),\n data\n });\n }\n postToHost({\n type: "interaction-event",\n kind: workerType,\n data\n });\n }\n function handleMouseEvent(event) {\n if (!canvas) return;\n event.preventDefault();\n const rect = canvas.getBoundingClientRect();\n const x = (event.clientX - rect.left) * (canvas.width / rect.width);\n const y = (event.clientY - rect.top) * (canvas.height / rect.height);\n dualRoute("mouse-update", {\n x,\n y,\n buttons: event.buttons,\n deltaX: event.movementX || 0,\n deltaY: event.movementY || 0,\n wheelDeltaX: 0,\n wheelDeltaY: 0,\n isInCanvas: isMouseInCanvas,\n timestamp: performance.now()\n });\n }\n function handleMouseEnter(event) {\n isMouseInCanvas = true;\n handleMouseEvent(event);\n }\n function handleMouseLeave(event) {\n isMouseInCanvas = false;\n handleMouseEvent(event);\n }\n function handleWheelEvent(event) {\n if (!canvas) return;\n event.preventDefault();\n const rect = canvas.getBoundingClientRect();\n const x = (event.clientX - rect.left) * (canvas.width / rect.width);\n const y = (event.clientY - rect.top) * (canvas.height / rect.height);\n dualRoute("mouse-update", {\n x,\n y,\n buttons: event.buttons,\n deltaX: 0,\n deltaY: 0,\n wheelDeltaX: event.deltaX,\n wheelDeltaY: event.deltaY,\n isInCanvas: isMouseInCanvas,\n timestamp: performance.now()\n });\n }\n var KEY_PASSTHROUGH = /* @__PURE__ */ new Set([\n "Tab",\n "F1",\n "F2",\n "F3",\n "F4",\n "F5",\n "F11",\n "F12"\n ]);\n function handleKeyboardEvent(event) {\n if (!KEY_PASSTHROUGH.has(event.key)) {\n event.preventDefault();\n }\n dualRoute("keyboard-update", {\n type: event.type,\n key: event.key,\n code: event.code,\n keyCode: event.keyCode,\n shiftKey: event.shiftKey,\n ctrlKey: event.ctrlKey,\n altKey: event.altKey,\n metaKey: event.metaKey,\n timestamp: performance.now()\n });\n }\n function handleTouchEvent(event) {\n if (!canvas) return;\n event.preventDefault();\n const rect = canvas.getBoundingClientRect();\n const scaleX = canvas.width / rect.width;\n const scaleY = canvas.height / rect.height;\n const canvasW = canvas.width;\n const canvasH = canvas.height;\n const mapTouch = (t) => {\n const cx = (t.clientX - rect.left) * scaleX;\n const cy = (t.clientY - rect.top) * scaleY;\n return {\n identifier: t.identifier,\n clientX: cx,\n clientY: cy,\n radiusX: t.radiusX ?? 0,\n radiusY: t.radiusY ?? 0,\n rotationAngle: t.rotationAngle ?? 0,\n force: t.force ?? 0,\n isInCanvas: cx >= 0 && cx <= canvasW && cy >= 0 && cy <= canvasH\n };\n };\n const changedMap = /* @__PURE__ */ new Map();\n for (let i = 0; i < event.changedTouches.length; i++) {\n const t = event.changedTouches[i];\n changedMap.set(t.identifier, mapTouch(t));\n }\n const touches = Array.from(event.touches).map((t) => {\n return changedMap.get(t.identifier) ?? mapTouch(t);\n });\n if (event.type === "touchend" || event.type === "touchcancel") {\n const activeIds = new Set(touches.map((t) => t.identifier));\n for (const [id, mapped] of changedMap) {\n if (!activeIds.has(id)) {\n touches.push({ ...mapped, ended: true });\n }\n }\n }\n dualRoute("touch-update", {\n type: event.type,\n touches,\n timestamp: performance.now()\n });\n }\n function setSensorsEnabled(enabled) {\n if (enabled === sensorHandlersAttached) return;\n if (enabled) {\n window.addEventListener("devicemotion", handleDeviceMotion);\n window.addEventListener("deviceorientation", handleDeviceOrientation);\n sensorHandlersAttached = true;\n } else {\n window.removeEventListener("devicemotion", handleDeviceMotion);\n window.removeEventListener("deviceorientation", handleDeviceOrientation);\n sensorHandlersAttached = false;\n }\n }\n function handleDeviceMotion(event) {\n if (!event.acceleration && !event.accelerationIncludingGravity && !event.rotationRate) {\n return;\n }\n postToHost({\n type: "sensor-event",\n kind: "motion",\n data: {\n acceleration: event.acceleration ? {\n x: event.acceleration.x,\n y: event.acceleration.y,\n z: event.acceleration.z\n } : null,\n accelerationIncludingGravity: event.accelerationIncludingGravity ? {\n x: event.accelerationIncludingGravity.x,\n y: event.accelerationIncludingGravity.y,\n z: event.accelerationIncludingGravity.z\n } : null,\n rotationRate: event.rotationRate ? {\n alpha: event.rotationRate.alpha,\n beta: event.rotationRate.beta,\n gamma: event.rotationRate.gamma\n } : null,\n interval: event.interval || 16\n }\n });\n }\n function handleDeviceOrientation(event) {\n postToHost({\n type: "sensor-event",\n kind: "orientation",\n data: {\n alpha: event.alpha,\n beta: event.beta,\n gamma: event.gamma,\n absolute: event.absolute || false\n }\n });\n }\n function handleTerminate() {\n setInteractionEnabled(false);\n setSensorsEnabled(false);\n if (worker) {\n worker.terminate();\n worker = null;\n }\n if (workerBlobUrl) {\n URL.revokeObjectURL(workerBlobUrl);\n workerBlobUrl = null;\n }\n }\n installHostListener();\n announceReady().catch((err) => reportError(err));\n window.addEventListener("error", (e) => {\n reportError(e.error ?? new Error(e.message));\n });\n window.addEventListener("unhandledrejection", (e) => {\n reportError(e.reason ?? new Error("Unhandled promise rejection"));\n });\n})();\n';
9
+ const inlineScriptCode = '"use strict";\n(() => {\n // src/iframe/protocol.ts\n var PROTOCOL_VERSION = 1;\n function collectTransferables(msg) {\n switch (msg.type) {\n case "init": {\n const m = msg;\n const out = [];\n if (m.data?.canvas) out.push(m.data.canvas);\n const wasm = m.data?.cvWasmFiles;\n if (wasm) {\n out.push(\n wasm.simdLoaderJs,\n wasm.simdBinary,\n wasm.nosimdLoaderJs,\n wasm.nosimdBinary\n );\n }\n if (m.data?.controlPort) out.push(m.data.controlPort);\n return out;\n }\n case "video-canvas-setup": {\n const m = msg;\n return m.data?.offscreenCanvas ? [m.data.offscreenCanvas] : [];\n }\n case "video-frame-update": {\n const m = msg;\n return m.data?.imageBitmap ? [m.data.imageBitmap] : [];\n }\n case "video-frame-direct": {\n const m = msg;\n return m.data?.imageBitmap ? [m.data.imageBitmap] : [];\n }\n case "parameter-update": {\n const m = msg;\n return m.data?.transferList ?? [];\n }\n case "parameter-batch-update": {\n const m = msg;\n return m.data?.transferList ?? [];\n }\n case "audio-analysis-update": {\n const m = msg;\n const out = [];\n if (m.data?.frequencyData?.buffer) out.push(m.data.frequencyData.buffer);\n if (m.data?.waveformData?.buffer) out.push(m.data.waveformData.buffer);\n return out;\n }\n case "capture-frame-result":\n case "auto-capture-result": {\n const data = msg.data;\n return data?.bitmap ? [data.bitmap] : [];\n }\n default:\n return [];\n }\n }\n\n // src/iframe/inline-script.ts\n var hostOrigin = null;\n var hostWindow = null;\n var worker = null;\n var workerBlobUrl = null;\n var canvas = null;\n var domHandlersAttached = false;\n var sensorHandlersAttached = false;\n function whenDomReady() {\n if (document.readyState === "loading") {\n return new Promise((resolve) => {\n document.addEventListener("DOMContentLoaded", () => resolve(), {\n once: true\n });\n });\n }\n return Promise.resolve();\n }\n async function announceReady() {\n await whenDomReady();\n const msg = {\n type: "iframe-ready",\n protocolVersion: PROTOCOL_VERSION,\n environment: {\n crossOriginIsolated: window.crossOriginIsolated,\n isSecureContext: window.isSecureContext,\n origin: self.origin\n }\n };\n window.parent.postMessage(msg, "*");\n }\n function installHostListener() {\n window.addEventListener("message", (event) => {\n if (event.source !== window.parent) return;\n if (hostOrigin === null) {\n hostOrigin = event.origin;\n hostWindow = window.parent;\n } else if (event.origin !== hostOrigin) {\n return;\n }\n handleHostMessage(event.data).catch((err) => {\n reportError(err);\n });\n });\n }\n function postToHost(msg, transfer = []) {\n if (hostOrigin === null || hostWindow === null) {\n return;\n }\n const targetOrigin = hostOrigin === "null" ? "*" : hostOrigin;\n hostWindow.postMessage(msg, targetOrigin, transfer);\n }\n function reportError(err) {\n const e = err;\n const msg = e?.stack ? {\n type: "iframe-error",\n message: e.message ?? String(err),\n stack: e.stack\n } : {\n type: "iframe-error",\n message: e?.message ?? String(err)\n };\n emitToHost(msg);\n }\n function reportWorkerOnError(e) {\n const msg = {\n type: "iframe-error",\n message: e.message ? `Worker error: ${e.message}` : "Worker error: (no detail \\u2014 likely cross-origin opacity)",\n ...e.error && e.error.stack ? { stack: e.error.stack } : {},\n ...e.filename ? { filename: e.filename } : {},\n ...e.lineno ? { lineno: e.lineno } : {},\n ...e.colno ? { colno: e.colno } : {}\n };\n emitToHost(msg);\n }\n function emitToHost(msg) {\n if (hostOrigin && hostWindow) {\n const targetOrigin = hostOrigin === "null" ? "*" : hostOrigin;\n hostWindow.postMessage(msg, targetOrigin);\n } else {\n window.parent.postMessage(msg, "*");\n }\n }\n async function handleHostMessage(msg) {\n switch (msg.type) {\n case "viji-bootstrap":\n await handleBootstrap(msg);\n return;\n case "viji-host-message":\n relayHostToWorker(msg.payload);\n return;\n case "viji-resize":\n handleResize(msg.width, msg.height);\n return;\n case "viji-enable-sensors":\n setSensorsEnabled(msg.enabled);\n return;\n case "viji-enable-interaction":\n setInteractionEnabled(msg.enabled);\n return;\n case "viji-terminate":\n handleTerminate();\n return;\n }\n }\n async function handleBootstrap(msg) {\n if (worker) {\n return;\n }\n canvas = document.createElement("canvas");\n canvas.id = "viji-canvas";\n canvas.width = msg.init.canvasWidth;\n canvas.height = msg.init.canvasHeight;\n canvas.style.width = "100%";\n canvas.style.height = "100%";\n canvas.style.display = "block";\n canvas.tabIndex = 0;\n canvas.style.outline = "none";\n document.body.appendChild(canvas);\n const offscreen = canvas.transferControlToOffscreen();\n workerBlobUrl = URL.createObjectURL(\n new Blob([msg.workerCode], { type: "application/javascript" })\n );\n worker = new Worker(workerBlobUrl, { type: "classic" });\n worker.onerror = (e) => {\n reportWorkerOnError(e);\n };\n worker.onmessage = (event) => {\n relayWorkerToHost(event.data);\n };\n const initId = `init_${Date.now()}`;\n const initMsg = {\n type: "init",\n id: initId,\n timestamp: Date.now(),\n data: {\n canvas: offscreen,\n isHeadless: msg.init.isHeadless,\n sc: msg.init.sc,\n rh: msg.init.rh,\n cvWasmFiles: msg.wasmFiles,\n controlPort: msg.controlPort\n }\n };\n const initResponse = waitForWorkerMessage((m) => {\n return m && m.type === "init-response" && m.id === initId;\n });\n worker.postMessage(initMsg, [\n offscreen,\n msg.wasmFiles.simdLoaderJs,\n msg.wasmFiles.simdBinary,\n msg.wasmFiles.nosimdLoaderJs,\n msg.wasmFiles.nosimdBinary,\n msg.controlPort\n ]);\n await initResponse;\n if (msg.init.allowInteraction && !msg.init.isHeadless) {\n setInteractionEnabled(true);\n }\n if (msg.init.allowSensors && !msg.init.isHeadless) {\n setSensorsEnabled(true);\n }\n if (canvas) {\n canvas.addEventListener("contextmenu", (e) => e.preventDefault());\n }\n postToHost({ type: "viji-ready" });\n }\n function waitForWorkerMessage(predicate) {\n return new Promise((resolve, reject) => {\n if (!worker) {\n reject(new Error("worker not spawned"));\n return;\n }\n const w = worker;\n const onceListener = (event) => {\n if (predicate(event.data)) {\n w.removeEventListener("message", onceListener);\n resolve(event.data);\n }\n };\n w.addEventListener("message", onceListener);\n });\n }\n function relayHostToWorker(payload) {\n if (!worker || !payload) return;\n try {\n const transfer = collectTransferables(payload);\n worker.postMessage(payload, transfer);\n } catch (err) {\n reportError(err);\n }\n }\n function relayWorkerToHost(payload) {\n if (!payload) return;\n try {\n const transfer = collectTransferables(payload);\n postToHost(\n { type: "worker-message", payload },\n transfer\n );\n } catch (err) {\n reportError(err);\n }\n }\n function handleResize(width, height) {\n if (!worker) return;\n worker.postMessage({\n type: "resolution-update",\n id: `resize_${Date.now()}`,\n timestamp: Date.now(),\n data: {\n effectiveWidth: width,\n effectiveHeight: height,\n displayScale: 1\n }\n });\n }\n var isMouseInCanvas = false;\n function setInteractionEnabled(enabled) {\n if (enabled === domHandlersAttached) return;\n if (!canvas) return;\n if (enabled) {\n canvas.addEventListener("mousedown", handleMouseEvent, { passive: false });\n canvas.addEventListener("mousemove", handleMouseEvent, { passive: false });\n canvas.addEventListener("mouseup", handleMouseEvent, { passive: false });\n canvas.addEventListener("mouseenter", handleMouseEnter, { passive: false });\n canvas.addEventListener("mouseleave", handleMouseLeave, { passive: false });\n canvas.addEventListener("wheel", handleWheelEvent, { passive: false });\n canvas.addEventListener("touchstart", handleTouchEvent, { passive: false });\n canvas.addEventListener("touchmove", handleTouchEvent, { passive: false });\n canvas.addEventListener("touchend", handleTouchEvent, { passive: false });\n canvas.addEventListener("touchcancel", handleTouchEvent, { passive: false });\n document.addEventListener("keydown", handleKeyboardEvent, { passive: false });\n document.addEventListener("keyup", handleKeyboardEvent, { passive: false });\n canvas.addEventListener("mousedown", focusCanvas);\n canvas.addEventListener("touchstart", focusCanvas);\n domHandlersAttached = true;\n } else {\n canvas.removeEventListener("mousedown", handleMouseEvent);\n canvas.removeEventListener("mousemove", handleMouseEvent);\n canvas.removeEventListener("mouseup", handleMouseEvent);\n canvas.removeEventListener("mouseenter", handleMouseEnter);\n canvas.removeEventListener("mouseleave", handleMouseLeave);\n canvas.removeEventListener("wheel", handleWheelEvent);\n canvas.removeEventListener("touchstart", handleTouchEvent);\n canvas.removeEventListener("touchmove", handleTouchEvent);\n canvas.removeEventListener("touchend", handleTouchEvent);\n canvas.removeEventListener("touchcancel", handleTouchEvent);\n document.removeEventListener("keydown", handleKeyboardEvent);\n document.removeEventListener("keyup", handleKeyboardEvent);\n canvas.removeEventListener("mousedown", focusCanvas);\n canvas.removeEventListener("touchstart", focusCanvas);\n domHandlersAttached = false;\n }\n }\n function focusCanvas() {\n if (canvas) canvas.focus();\n }\n function dualRoute(workerType, data) {\n if (worker) {\n worker.postMessage({\n type: workerType,\n id: `${workerType}_${Date.now()}`,\n timestamp: Date.now(),\n data\n });\n }\n postToHost({\n type: "interaction-event",\n kind: workerType,\n data\n });\n }\n function handleMouseEvent(event) {\n if (!canvas) return;\n event.preventDefault();\n const rect = canvas.getBoundingClientRect();\n const x = (event.clientX - rect.left) * (canvas.width / rect.width);\n const y = (event.clientY - rect.top) * (canvas.height / rect.height);\n dualRoute("mouse-update", {\n x,\n y,\n buttons: event.buttons,\n deltaX: event.movementX || 0,\n deltaY: event.movementY || 0,\n wheelDeltaX: 0,\n wheelDeltaY: 0,\n isInCanvas: isMouseInCanvas,\n timestamp: performance.now()\n });\n }\n function handleMouseEnter(event) {\n isMouseInCanvas = true;\n handleMouseEvent(event);\n }\n function handleMouseLeave(event) {\n isMouseInCanvas = false;\n handleMouseEvent(event);\n }\n function handleWheelEvent(event) {\n if (!canvas) return;\n event.preventDefault();\n const rect = canvas.getBoundingClientRect();\n const x = (event.clientX - rect.left) * (canvas.width / rect.width);\n const y = (event.clientY - rect.top) * (canvas.height / rect.height);\n dualRoute("mouse-update", {\n x,\n y,\n buttons: event.buttons,\n deltaX: 0,\n deltaY: 0,\n wheelDeltaX: event.deltaX,\n wheelDeltaY: event.deltaY,\n isInCanvas: isMouseInCanvas,\n timestamp: performance.now()\n });\n }\n var KEY_PASSTHROUGH = /* @__PURE__ */ new Set([\n "Tab",\n "F1",\n "F2",\n "F3",\n "F4",\n "F5",\n "F11",\n "F12"\n ]);\n function handleKeyboardEvent(event) {\n if (!KEY_PASSTHROUGH.has(event.key)) {\n event.preventDefault();\n }\n dualRoute("keyboard-update", {\n type: event.type,\n key: event.key,\n code: event.code,\n keyCode: event.keyCode,\n shiftKey: event.shiftKey,\n ctrlKey: event.ctrlKey,\n altKey: event.altKey,\n metaKey: event.metaKey,\n timestamp: performance.now()\n });\n }\n function handleTouchEvent(event) {\n if (!canvas) return;\n event.preventDefault();\n const rect = canvas.getBoundingClientRect();\n const scaleX = canvas.width / rect.width;\n const scaleY = canvas.height / rect.height;\n const canvasW = canvas.width;\n const canvasH = canvas.height;\n const mapTouch = (t) => {\n const cx = (t.clientX - rect.left) * scaleX;\n const cy = (t.clientY - rect.top) * scaleY;\n return {\n identifier: t.identifier,\n clientX: cx,\n clientY: cy,\n radiusX: t.radiusX ?? 0,\n radiusY: t.radiusY ?? 0,\n rotationAngle: t.rotationAngle ?? 0,\n force: t.force ?? 0,\n isInCanvas: cx >= 0 && cx <= canvasW && cy >= 0 && cy <= canvasH\n };\n };\n const changedMap = /* @__PURE__ */ new Map();\n for (let i = 0; i < event.changedTouches.length; i++) {\n const t = event.changedTouches[i];\n changedMap.set(t.identifier, mapTouch(t));\n }\n const touches = Array.from(event.touches).map((t) => {\n return changedMap.get(t.identifier) ?? mapTouch(t);\n });\n if (event.type === "touchend" || event.type === "touchcancel") {\n const activeIds = new Set(touches.map((t) => t.identifier));\n for (const [id, mapped] of changedMap) {\n if (!activeIds.has(id)) {\n touches.push({ ...mapped, ended: true });\n }\n }\n }\n dualRoute("touch-update", {\n type: event.type,\n touches,\n timestamp: performance.now()\n });\n }\n function setSensorsEnabled(enabled) {\n if (enabled === sensorHandlersAttached) return;\n if (enabled) {\n window.addEventListener("devicemotion", handleDeviceMotion);\n window.addEventListener("deviceorientation", handleDeviceOrientation);\n sensorHandlersAttached = true;\n } else {\n window.removeEventListener("devicemotion", handleDeviceMotion);\n window.removeEventListener("deviceorientation", handleDeviceOrientation);\n sensorHandlersAttached = false;\n }\n }\n function handleDeviceMotion(event) {\n if (!event.acceleration && !event.accelerationIncludingGravity && !event.rotationRate) {\n return;\n }\n postToHost({\n type: "sensor-event",\n kind: "motion",\n data: {\n acceleration: event.acceleration ? {\n x: event.acceleration.x,\n y: event.acceleration.y,\n z: event.acceleration.z\n } : null,\n accelerationIncludingGravity: event.accelerationIncludingGravity ? {\n x: event.accelerationIncludingGravity.x,\n y: event.accelerationIncludingGravity.y,\n z: event.accelerationIncludingGravity.z\n } : null,\n rotationRate: event.rotationRate ? {\n alpha: event.rotationRate.alpha,\n beta: event.rotationRate.beta,\n gamma: event.rotationRate.gamma\n } : null,\n interval: event.interval || 16\n }\n });\n }\n function handleDeviceOrientation(event) {\n postToHost({\n type: "sensor-event",\n kind: "orientation",\n data: {\n alpha: event.alpha,\n beta: event.beta,\n gamma: event.gamma,\n absolute: event.absolute || false\n }\n });\n }\n function handleTerminate() {\n setInteractionEnabled(false);\n setSensorsEnabled(false);\n if (worker) {\n worker.terminate();\n worker = null;\n }\n if (workerBlobUrl) {\n URL.revokeObjectURL(workerBlobUrl);\n workerBlobUrl = null;\n }\n }\n installHostListener();\n announceReady().catch((err) => reportError(err));\n window.addEventListener("error", (e) => {\n reportError(e.error ?? new Error(e.message));\n });\n window.addEventListener("unhandledrejection", (e) => {\n reportError(e.reason ?? new Error("Unhandled promise rejection"));\n });\n})();\n';
10
10
  const IFRAME_ALLOW = "accelerometer; gyroscope; magnetometer; camera; microphone";
11
11
  const OPAQUE_ORIGIN = "null";
12
12
  function collectTransferables(msg) {
@@ -24,6 +24,7 @@ function collectTransferables(msg) {
24
24
  wasm.nosimdBinary
25
25
  );
26
26
  }
27
+ if (m.data?.controlPort) out.push(m.data.controlPort);
27
28
  return out;
28
29
  }
29
30
  case "video-canvas-setup": {
@@ -68,7 +69,8 @@ function collectBootstrapTransferables(msg) {
68
69
  msg.wasmFiles.simdLoaderJs,
69
70
  msg.wasmFiles.simdBinary,
70
71
  msg.wasmFiles.nosimdLoaderJs,
71
- msg.wasmFiles.nosimdBinary
72
+ msg.wasmFiles.nosimdBinary,
73
+ msg.controlPort
72
74
  ];
73
75
  }
74
76
  class IFrameManager {
@@ -103,6 +105,19 @@ class IFrameManager {
103
105
  hostMessageListener = null;
104
106
  workerMessageHandlers = [];
105
107
  vijiReadyHandlers = [];
108
+ /**
109
+ * Direct host↔worker control channel. `port1` stays on the host side and is
110
+ * handed to `WorkerManager` via `getControlPort1()` after `viji-ready`;
111
+ * `port2` is shipped to the worker through the bootstrap envelope so all
112
+ * steady-state host↔worker traffic can bypass the per-frame iframe relay
113
+ * tax. The trust boundary is unchanged — the worker still inherits the
114
+ * iframe's opaque origin; the port is just a faster transport.
115
+ *
116
+ * Single-owner discipline: IFrameManager owns the channel lifecycle and
117
+ * closes `port1` on destroy. WorkerManager only borrows the port reference
118
+ * for its own listener (which it detaches without closing).
119
+ */
120
+ controlChannel = null;
106
121
  initiallyVisible;
107
122
  /**
108
123
  * Enable or disable debug logging
@@ -260,6 +275,35 @@ class IFrameManager {
260
275
  onVijiReady(handler) {
261
276
  this.vijiReadyHandlers.push(handler);
262
277
  }
278
+ /**
279
+ * Create the host↔worker control channel and return the iframe-side end
280
+ * (`port2`) for inclusion in the bootstrap envelope. Idempotent: a second
281
+ * call returns the same `port2` (the underlying channel is created once
282
+ * per IFrameManager lifetime).
283
+ *
284
+ * Called by `WorkerManager.createWorker` while assembling the
285
+ * `HostBootstrapMessage` so the port hops host → iframe → worker as a
286
+ * transferable. The host-side `port1` is consumed via `getControlPort1()`
287
+ * after `viji-ready`.
288
+ */
289
+ createControlChannel() {
290
+ if (!this.controlChannel) {
291
+ this.controlChannel = new MessageChannel();
292
+ }
293
+ return this.controlChannel.port2;
294
+ }
295
+ /**
296
+ * Returns the host-side end (`port1`) of the control channel created by
297
+ * `createControlChannel()`. WorkerManager grabs this after `viji-ready` to
298
+ * route per-frame traffic directly to the worker.
299
+ *
300
+ * Returns `null` if the channel was never created (e.g. legacy bootstrap
301
+ * shape) or has been torn down by `destroy()`. Callers should treat null
302
+ * as "stay on the iframe relay path" rather than as an error.
303
+ */
304
+ getControlPort1() {
305
+ return this.controlChannel?.port1 ?? null;
306
+ }
263
307
  // ========================================
264
308
  // Host-side message dispatch
265
309
  // ========================================
@@ -425,6 +469,13 @@ class IFrameManager {
425
469
  } catch {
426
470
  }
427
471
  }
472
+ if (this.controlChannel) {
473
+ try {
474
+ this.controlChannel.port1.close();
475
+ } catch {
476
+ }
477
+ this.controlChannel = null;
478
+ }
428
479
  if (this.hostMessageListener) {
429
480
  window.removeEventListener("message", this.hostMessageListener);
430
481
  this.hostMessageListener = null;
@@ -589,7 +640,7 @@ class IFrameManager {
589
640
  }
590
641
  }
591
642
  }
592
- const workerUrl = "" + new URL("assets/viji.worker-BbzNOVhB.js", import.meta.url).href;
643
+ const workerUrl = "" + new URL("assets/viji.worker-CWKkFyOs.js", import.meta.url).href;
593
644
  const simdLoaderJs = new URL("assets/wasm/vision_wasm_internal.js", import.meta.url).href;
594
645
  const simdBinary = new URL("assets/wasm/vision_wasm_internal.wasm", import.meta.url).href;
595
646
  const nosimdLoaderJs = new URL("assets/wasm/vision_wasm_nosimd_internal.js", import.meta.url).href;
@@ -606,6 +657,19 @@ class WorkerManager {
606
657
  isInitialized = false;
607
658
  /** Bound worker-message handler so we can detach on destroy. */
608
659
  onWorkerMessageBound = null;
660
+ /**
661
+ * Host-side end of the direct host↔worker MessageChannel (created by
662
+ * `IFrameManager.createControlChannel()` at bootstrap time, handed to us
663
+ * after `viji-ready`). Once non-null, `relayHostToWorker` posts on this
664
+ * port instead of through the iframe relay — one cross-process hop
665
+ * instead of two for every per-frame message.
666
+ *
667
+ * Single-owner discipline (docs/14, rule 5): IFrameManager owns the
668
+ * channel lifecycle and closes `port1` on destroy. WorkerManager only
669
+ * detaches its `onmessage` listener.
670
+ */
671
+ controlPort = null;
672
+ onControlPortMessageBound = null;
609
673
  /**
610
674
  * Bootstraps the worker inside the iframe.
611
675
  *
@@ -636,6 +700,7 @@ class WorkerManager {
636
700
  ]);
637
701
  this.setupMessageHandling();
638
702
  const res = this.iframeManager.getEffectiveResolution();
703
+ const controlPort2 = this.iframeManager.createControlChannel();
639
704
  const bootstrap = {
640
705
  type: "viji-bootstrap",
641
706
  workerCode,
@@ -645,6 +710,7 @@ class WorkerManager {
645
710
  nosimdLoaderJs: nosimdLoaderJs$1,
646
711
  nosimdBinary: nosimdBinary$1
647
712
  },
713
+ controlPort: controlPort2,
648
714
  init: {
649
715
  canvasWidth: res.width,
650
716
  canvasHeight: res.height,
@@ -675,6 +741,14 @@ class WorkerManager {
675
741
  collectBootstrapTransferables(bootstrap)
676
742
  );
677
743
  await vijiReady;
744
+ this.controlPort = this.iframeManager.getControlPort1();
745
+ if (this.controlPort && this.onWorkerMessageBound) {
746
+ const dispatcher = this.onWorkerMessageBound;
747
+ this.onControlPortMessageBound = (evt) => {
748
+ dispatcher(evt.data);
749
+ };
750
+ this.controlPort.onmessage = this.onControlPortMessageBound;
751
+ }
678
752
  this.postMessage("set-scene-code", { sceneCode: this.sceneCode });
679
753
  this.isInitialized = true;
680
754
  } catch (error) {
@@ -726,8 +800,17 @@ class WorkerManager {
726
800
  this.relayHostToWorker(message);
727
801
  }
728
802
  /**
729
- * Sends a host-to-worker message wrapped in a `viji-host-message`
730
- * envelope, with transferables harvested via the centralized helper.
803
+ * Sends a host-to-worker message. Once the direct control channel is
804
+ * wired (after `viji-ready`), per-message traffic flows on `port1`
805
+ * directly to the worker — bypassing the iframe relay's per-frame cost.
806
+ * During the bootstrap window (before `viji-ready` fires) and any
807
+ * post-teardown call we fall back to the iframe relay path so init-time
808
+ * messages still reach the worker.
809
+ *
810
+ * Transferables are harvested via the centralized `collectTransferables`
811
+ * helper either way, and the `transferList` shim added by parameter
812
+ * messages is stripped before send (the worker has no use for it once
813
+ * the actual handles have been transferred).
731
814
  */
732
815
  relayHostToWorker(message) {
733
816
  if (!this.iframeManager.ready && !this.isInitialized) {
@@ -739,6 +822,10 @@ class WorkerManager {
739
822
  try {
740
823
  const transfer = collectTransferables(message);
741
824
  const sanitized = stripTransferList(message);
825
+ if (this.controlPort) {
826
+ this.controlPort.postMessage(sanitized, transfer);
827
+ return;
828
+ }
742
829
  this.iframeManager.postEnvelope(
743
830
  { type: "viji-host-message", payload: sanitized },
744
831
  transfer
@@ -779,6 +866,11 @@ class WorkerManager {
779
866
  this.iframeManager.offWorkerMessage(this.onWorkerMessageBound);
780
867
  this.onWorkerMessageBound = null;
781
868
  }
869
+ if (this.controlPort && this.onControlPortMessageBound) {
870
+ this.controlPort.onmessage = null;
871
+ this.onControlPortMessageBound = null;
872
+ }
873
+ this.controlPort = null;
782
874
  this.isInitialized = false;
783
875
  } catch (error) {
784
876
  console.warn("Error during worker cleanup:", error);
@@ -1805,7 +1897,7 @@ class EssentiaOnsetDetection {
1805
1897
  this.initPromise = (async () => {
1806
1898
  try {
1807
1899
  const essentiaModule = await import("./essentia.js-core.es-DnrJE0uR.js");
1808
- const wasmModule = await import("./essentia-wasm.web-qCUJ8zjr.js").then((n) => n.e);
1900
+ const wasmModule = await import("./essentia-wasm.web-CmPC-AKu.js").then((n) => n.e);
1809
1901
  const EssentiaClass = essentiaModule.Essentia || essentiaModule.default?.Essentia || essentiaModule.default;
1810
1902
  let WASMModule = wasmModule.default || wasmModule.EssentiaWASM || wasmModule.default?.EssentiaWASM;
1811
1903
  if (!WASMModule) {
@@ -10601,7 +10693,7 @@ class VijiCore {
10601
10693
  }
10602
10694
  }
10603
10695
  }
10604
- const VERSION = "0.3.29";
10696
+ const VERSION = "0.5.0";
10605
10697
  export {
10606
10698
  AudioSystem as A,
10607
10699
  VERSION as V,
@@ -10609,4 +10701,4 @@ export {
10609
10701
  VijiCoreError as b,
10610
10702
  getDefaultExportFromCjs as g
10611
10703
  };
10612
- //# sourceMappingURL=index-Oj_9v8r4.js.map
10704
+ //# sourceMappingURL=index-Cp9G0z4E.js.map