diff --git a/js/hang/package.json b/js/hang/package.json index 955c2e475..b972e8d61 100644 --- a/js/hang/package.json +++ b/js/hang/package.json @@ -38,12 +38,12 @@ "release": "tsx ../scripts/release.ts" }, "dependencies": { - "@huggingface/transformers": "^3.7.2", - "@kixelated/moq": "link:../moq", - "@kixelated/signals": "link:../signals", - "async-mutex": "^0.5.0", + "@kixelated/moq": "workspace:^", + "@kixelated/signals": "workspace:^", + "zod": "^4.1.3", "comlink": "^4.4.2", - "zod": "^4.1.3" + "@huggingface/transformers": "^3.7.2", + "async-mutex": "^0.5.0" }, "devDependencies": { "@types/audioworklet": "^0.0.77", diff --git a/js/hang/src/publish/audio/captions.ts b/js/hang/src/publish/audio/captions.ts index fe302fabb..dcb0dab6c 100644 --- a/js/hang/src/publish/audio/captions.ts +++ b/js/hang/src/publish/audio/captions.ts @@ -2,9 +2,9 @@ import * as Moq from "@kixelated/moq"; import { Effect, Signal } from "@kixelated/signals"; import type * as Catalog from "../../catalog"; import { u8 } from "../../catalog"; -import { loadAudioWorklet } from "../../util/hacks"; import type { Audio } from "."; import type { Request, Result } from "./captions-worker"; +import CaptureWorklet from "./capture-worklet?worker&url"; export type CaptionsProps = { enabled?: boolean; @@ -93,11 +93,7 @@ export class Captions { // The workload needs to be loaded asynchronously, unfortunately, but it should be instant. effect.spawn(async () => { - await ctx.audioWorklet.addModule( - await loadAudioWorklet(() => - navigator.serviceWorker.register(new URL("./capture-worklet", import.meta.url)), - ), - ); + await ctx.audioWorklet.addModule(CaptureWorklet); // Create the worklet. const worklet = new AudioWorkletNode(ctx, "capture", { diff --git a/js/hang/src/publish/audio/index.ts b/js/hang/src/publish/audio/index.ts index 01c022f71..9f3382df2 100644 --- a/js/hang/src/publish/audio/index.ts +++ b/js/hang/src/publish/audio/index.ts @@ -3,16 +3,18 @@ import { Effect, type Getter, Signal } from "@kixelated/signals"; import type * as Catalog from "../../catalog"; import { u8, u53 } from "../../catalog/integers"; import * as Frame from "../../frame"; -import { loadAudioWorklet } from "../../util/hacks"; import { Captions, type CaptionsProps } from "./captions"; import type * as Capture from "./capture"; -import { Speaking, type SpeakingProps } from "./speaking"; export * from "./captions"; const GAIN_MIN = 0.001; const FADE_TIME = 0.2; +// Unfortunately, we need to use a Vite-exclusive import for now. +import CaptureWorklet from "./capture-worklet?worker&url"; +import { Speaking, type SpeakingProps } from "./speaking"; + export type AudioConstraints = Omit< MediaTrackConstraints, "aspectRatio" | "backgroundBlur" | "displaySurface" | "facingMode" | "frameRate" | "height" | "width" @@ -130,12 +132,7 @@ export class Audio { // Async because we need to wait for the worklet to be registered. effect.spawn(async () => { - await context.audioWorklet.addModule( - await loadAudioWorklet(() => - navigator.serviceWorker.register(new URL("./capture-worklet", import.meta.url)), - ), - ); - + await context.audioWorklet.addModule(CaptureWorklet); const worklet = new AudioWorkletNode(context, "capture", { numberOfInputs: 1, numberOfOutputs: 0, diff --git a/js/hang/src/publish/audio/speaking.ts b/js/hang/src/publish/audio/speaking.ts index c59f268dc..c33d6ea10 100644 --- a/js/hang/src/publish/audio/speaking.ts +++ b/js/hang/src/publish/audio/speaking.ts @@ -2,8 +2,8 @@ import * as Moq from "@kixelated/moq"; import { Effect, Signal } from "@kixelated/signals"; import type * as Catalog from "../../catalog"; import { u8 } from "../../catalog"; -import { loadAudioWorklet } from "../../util/hacks"; import type { Audio } from "."; +import CaptureWorklet from "./capture-worklet?worker&url"; import type { Request, Result } from "./speaking-worker"; export type SpeakingProps = { @@ -84,11 +84,7 @@ export class Speaking { // The workload needs to be loaded asynchronously, unfortunately, but it should be instant. effect.spawn(async () => { - await ctx.audioWorklet.addModule( - await loadAudioWorklet(() => - navigator.serviceWorker.register(new URL("./capture-worklet", import.meta.url)), - ), - ); + await ctx.audioWorklet.addModule(CaptureWorklet); // Create the worklet. const worklet = new AudioWorkletNode(ctx, "capture", { diff --git a/js/hang/src/publish/video/detection.ts b/js/hang/src/publish/video/detection.ts index 0b3eafc03..4d66af46b 100644 --- a/js/hang/src/publish/video/detection.ts +++ b/js/hang/src/publish/video/detection.ts @@ -4,6 +4,8 @@ import * as Comlink from "comlink"; import * as Catalog from "../../catalog"; import type { Video } from "."; import type { DetectionWorker } from "./detection-worker"; +// Vite-specific import for worker +import WorkerUrl from "./detection-worker?worker&url"; export type DetectionProps = { enabled?: boolean; @@ -50,7 +52,8 @@ export class Detection { track: { name: this.#track.name, priority: Catalog.u8(this.#track.priority) }, }); - const worker = new Worker(new URL("./detection-worker", import.meta.url), { type: "module" }); + // Initialize worker + const worker = new Worker(WorkerUrl, { type: "module" }); effect.cleanup(() => worker.terminate()); const api = Comlink.wrap(worker); diff --git a/js/hang/src/util/hacks.ts b/js/hang/src/util/hacks.ts index cbfdb9eda..d6f2a8e57 100644 --- a/js/hang/src/util/hacks.ts +++ b/js/hang/src/util/hacks.ts @@ -1,29 +1,5 @@ -import { Mutex } from "async-mutex"; - // https://issues.chromium.org/issues/40504498 export const isChrome = navigator.userAgent.toLowerCase().includes("chrome"); // https://bugzilla.mozilla.org/show_bug.cgi?id=1967793 export const isFirefox = navigator.userAgent.toLowerCase().includes("firefox"); - -// Hacky workaround to support Webpack and Vite -// https://github.com/webpack/webpack/issues/11543#issuecomment-2045809214 - -// Note that Webpack needs to see `navigator.serviceWorker.register(new URL("literal"), ...)` for this to work - -const loadAudioWorkletMutex = new Mutex(); - -export async function loadAudioWorklet(registerFn: () => Promise) { - return await loadAudioWorkletMutex.runExclusive(async () => { - const { register } = navigator.serviceWorker; - - // @ts-ignore hack to make webpack believe that it is registering a worker - navigator.serviceWorker.register = (url: URL) => Promise.resolve(url); - - try { - return (await registerFn()) as unknown as URL; - } finally { - navigator.serviceWorker.register = register; - } - }); -} diff --git a/js/hang/src/watch/audio/index.ts b/js/hang/src/watch/audio/index.ts index a32ad9784..cd5fb7328 100644 --- a/js/hang/src/watch/audio/index.ts +++ b/js/hang/src/watch/audio/index.ts @@ -2,14 +2,14 @@ import type * as Moq from "@kixelated/moq"; import { Effect, type Getter, Signal } from "@kixelated/signals"; import type * as Catalog from "../../catalog"; import * as Frame from "../../frame"; -import { loadAudioWorklet } from "../../util/hacks"; import * as Hex from "../../util/hex"; -import { Captions, type CaptionsProps } from "./captions"; import type * as Render from "./render"; -import { Speaking, type SpeakingProps } from "./speaking"; export * from "./emitter"; +import { Captions, type CaptionsProps } from "./captions"; +import { Speaking, type SpeakingProps } from "./speaking"; + export type AudioProps = { // Enable to download the audio track. enabled?: boolean; @@ -24,6 +24,9 @@ export type AudioProps = { speaking?: SpeakingProps; }; +// Unfortunately, we need to use a Vite-exclusive import for now. +import RenderWorklet from "./render-worklet?worker&url"; + // Downloads audio from a track and emits it to an AudioContext. // The user is responsible for hooking up audio to speakers, an analyzer, etc. export class Audio { @@ -91,11 +94,7 @@ export class Audio { effect.spawn(async () => { // Register the AudioWorklet processor - await context.audioWorklet.addModule( - await loadAudioWorklet(() => - navigator.serviceWorker.register(new URL("./render-worklet", import.meta.url)), - ), - ); + await context.audioWorklet.addModule(RenderWorklet); // Create the worklet node const worklet = new AudioWorkletNode(context, "render"); diff --git a/js/package.json b/js/package.json index 8fa01c905..edf73b9dc 100644 --- a/js/package.json +++ b/js/package.json @@ -9,7 +9,6 @@ "concurrently": "^9.2.1", "cpy-cli": "^5.0.0", "knip": "^5.63.0", - "prettier": "^3.6.2", "rimraf": "^6.0.1", "tsx": "^4.20.5", "typescript": "^5.9.2" diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index 292e85eb2..59686b7c7 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -27,9 +27,6 @@ importers: knip: specifier: ^5.63.0 version: 5.63.0(@types/node@24.3.0)(typescript@5.9.2) - prettier: - specifier: ^3.6.2 - version: 3.6.2 rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -46,10 +43,10 @@ importers: specifier: ^3.7.2 version: 3.7.2 '@kixelated/moq': - specifier: link:../moq + specifier: workspace:^ version: link:../moq '@kixelated/signals': - specifier: link:../signals + specifier: workspace:^ version: link:../signals async-mutex: specifier: ^0.5.0 @@ -1575,11 +1572,6 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} - engines: {node: '>=14'} - hasBin: true - protobufjs@7.5.4: resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} engines: {node: '>=12.0.0'} @@ -3069,8 +3061,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prettier@3.6.2: {} - protobufjs@7.5.4: dependencies: '@protobufjs/aspromise': 1.1.2