@myzonerocks/gosslens 0.9.0 → 0.10.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.
- package/README.md +412 -45
- package/dist/src/audio-output.d.ts +12 -0
- package/dist/src/audio-output.d.ts.map +1 -0
- package/dist/src/audio-output.js +99 -0
- package/dist/src/audio-output.js.map +1 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mic-input.d.ts +12 -0
- package/dist/src/mic-input.d.ts.map +1 -0
- package/dist/src/mic-input.js +67 -0
- package/dist/src/mic-input.js.map +1 -0
- package/dist/src/video-texture.d.ts +14 -0
- package/dist/src/video-texture.d.ts.map +1 -0
- package/dist/src/video-texture.js +58 -0
- package/dist/src/video-texture.js.map +1 -0
- package/package.json +1 -1
- package/src/audio-output.ts +102 -0
- package/src/index.ts +3 -0
- package/src/mic-input.ts +69 -0
- package/src/video-texture.ts +60 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { GossSession } from "./index";
|
|
2
|
+
export declare class GossMicInput {
|
|
3
|
+
private session;
|
|
4
|
+
private context;
|
|
5
|
+
private stream;
|
|
6
|
+
private node;
|
|
7
|
+
private source;
|
|
8
|
+
constructor(session: GossSession);
|
|
9
|
+
start(): Promise<void>;
|
|
10
|
+
stop(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=mic-input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mic-input.d.ts","sourceRoot":"","sources":["../../src/mic-input.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAiB3C,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,IAAI,CAAiC;IAC7C,OAAO,CAAC,MAAM,CAA2C;IAEzD,YAAY,OAAO,EAAE,WAAW,EAE/B;IAKK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAuB3B;IAGK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAU1B;CACF"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/// Captures the microphone and feeds it to the engine's audio analysis,
|
|
2
|
+
/// so the level and beat triggers a lens reads fire on web the same way
|
|
3
|
+
/// submitted audio drives them on the native SDKs. A worklet forwards
|
|
4
|
+
/// each block to the main thread, which hands it to submitAudio.
|
|
5
|
+
const captureProcessor = `
|
|
6
|
+
class GossMicCapture extends AudioWorkletProcessor {
|
|
7
|
+
process(inputs) {
|
|
8
|
+
const chan = inputs[0] && inputs[0][0];
|
|
9
|
+
if (chan && chan.length) this.port.postMessage(chan.slice());
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
registerProcessor("goss-mic-capture", GossMicCapture);
|
|
14
|
+
`;
|
|
15
|
+
export class GossMicInput {
|
|
16
|
+
session;
|
|
17
|
+
context = null;
|
|
18
|
+
stream = null;
|
|
19
|
+
node = null;
|
|
20
|
+
source = null;
|
|
21
|
+
constructor(session) {
|
|
22
|
+
this.session = session;
|
|
23
|
+
}
|
|
24
|
+
/// Requests the microphone and starts forwarding its PCM to the
|
|
25
|
+
/// engine. Call from a user gesture; browsers gate both getUserMedia
|
|
26
|
+
/// and the AudioContext behind one.
|
|
27
|
+
async start() {
|
|
28
|
+
if (this.context)
|
|
29
|
+
return;
|
|
30
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
31
|
+
const context = new AudioContext();
|
|
32
|
+
const moduleUrl = URL.createObjectURL(new Blob([captureProcessor], { type: "text/javascript" }));
|
|
33
|
+
try {
|
|
34
|
+
await context.audioWorklet.addModule(moduleUrl);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
URL.revokeObjectURL(moduleUrl);
|
|
38
|
+
}
|
|
39
|
+
const source = context.createMediaStreamSource(stream);
|
|
40
|
+
const node = new AudioWorkletNode(context, "goss-mic-capture", { numberOfOutputs: 0 });
|
|
41
|
+
const rate = context.sampleRate;
|
|
42
|
+
node.port.onmessage = (event) => {
|
|
43
|
+
const block = event.data;
|
|
44
|
+
this.session.submitAudio(block, block.length, rate, 1);
|
|
45
|
+
};
|
|
46
|
+
source.connect(node);
|
|
47
|
+
await context.resume();
|
|
48
|
+
this.context = context;
|
|
49
|
+
this.stream = stream;
|
|
50
|
+
this.source = source;
|
|
51
|
+
this.node = node;
|
|
52
|
+
}
|
|
53
|
+
/// Stops the capture and releases the microphone.
|
|
54
|
+
async stop() {
|
|
55
|
+
this.source?.disconnect();
|
|
56
|
+
this.node?.disconnect();
|
|
57
|
+
this.source = null;
|
|
58
|
+
this.node = null;
|
|
59
|
+
this.stream?.getTracks().forEach((track) => track.stop());
|
|
60
|
+
this.stream = null;
|
|
61
|
+
const context = this.context;
|
|
62
|
+
this.context = null;
|
|
63
|
+
if (context)
|
|
64
|
+
await context.close();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=mic-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mic-input.js","sourceRoot":"","sources":["../../src/mic-input.ts"],"names":[],"mappings":"AAEA,wEAAwE;AACxE,wEAAwE;AACxE,sEAAsE;AACtE,iEAAiE;AACjE,MAAM,gBAAgB,GAAG;;;;;;;;;CASxB,CAAC;AAEF,MAAM,OAAO,YAAY;IACf,OAAO,CAAc;IACrB,OAAO,GAAwB,IAAI,CAAC;IACpC,MAAM,GAAuB,IAAI,CAAC;IAClC,IAAI,GAA4B,IAAI,CAAC;IACrC,MAAM,GAAsC,IAAI,CAAC;IAEzD,YAAY,OAAoB;QAC9B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,gEAAgE;IAChE,qEAAqE;IACrE,oCAAoC;IACpC,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QACzB,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,YAAY,CAAC,YAAY,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1E,MAAM,OAAO,GAAG,IAAI,YAAY,EAAE,CAAC;QACnC,MAAM,SAAS,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,CAAC,gBAAgB,CAAC,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC;QACjG,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,YAAY,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QAClD,CAAC;gBAAS,CAAC;YACT,GAAG,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC;QACjC,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC;QACvD,MAAM,IAAI,GAAG,IAAI,gBAAgB,CAAC,OAAO,EAAE,kBAAkB,EAAE,EAAE,eAAe,EAAE,CAAC,EAAE,CAAC,CAAC;QACvF,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC;QAChC,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,CAAC,KAAK,EAAE,EAAE;YAC9B,MAAM,KAAK,GAAG,KAAK,CAAC,IAAoB,CAAC;YACzC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACzD,CAAC,CAAC;QACF,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACrB,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,kDAAkD;IAClD,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC;QAC1B,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC;QACxB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;QAC1D,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,OAAO;YAAE,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACrC,CAAC;CACF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { GossSession } from "./index";
|
|
2
|
+
export declare class GossVideoTexture {
|
|
3
|
+
private session;
|
|
4
|
+
private name;
|
|
5
|
+
private video;
|
|
6
|
+
private canvas;
|
|
7
|
+
private ctx;
|
|
8
|
+
private running;
|
|
9
|
+
constructor(session: GossSession, name: string, url: string);
|
|
10
|
+
start(): Promise<void>;
|
|
11
|
+
pump(): void;
|
|
12
|
+
stop(): void;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=video-texture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-texture.d.ts","sourceRoot":"","sources":["../../src/video-texture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAM3C,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,KAAK,CAAmB;IAChC,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,GAAG,CAAkC;IAC7C,OAAO,CAAC,OAAO,CAAS;IAExB,YAAY,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAU1D;IAIK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAK3B;IAKD,IAAI,IAAI,IAAI,CAYX;IAGD,IAAI,IAAI,IAAI,CAKX;CACF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/// Plays an MP4 through a hidden video element, the browser's own
|
|
2
|
+
/// decoder, and feeds each frame to a named engine source a lens
|
|
3
|
+
/// composites, so a web lens shows a video texture with no wasm
|
|
4
|
+
/// decoder. The `video.texture` node stays native; this serves web.
|
|
5
|
+
export class GossVideoTexture {
|
|
6
|
+
session;
|
|
7
|
+
name;
|
|
8
|
+
video;
|
|
9
|
+
canvas;
|
|
10
|
+
ctx;
|
|
11
|
+
running = false;
|
|
12
|
+
constructor(session, name, url) {
|
|
13
|
+
this.session = session;
|
|
14
|
+
this.name = name;
|
|
15
|
+
this.video = document.createElement("video");
|
|
16
|
+
this.video.src = url;
|
|
17
|
+
this.video.loop = true;
|
|
18
|
+
this.video.muted = true;
|
|
19
|
+
this.video.playsInline = true;
|
|
20
|
+
this.canvas = document.createElement("canvas");
|
|
21
|
+
this.ctx = this.canvas.getContext("2d", { willReadFrequently: true });
|
|
22
|
+
}
|
|
23
|
+
/// Registers the source and starts playback; pumps run from the
|
|
24
|
+
/// frame loop. Call from a user gesture so autoplay is allowed.
|
|
25
|
+
async start() {
|
|
26
|
+
if (this.running)
|
|
27
|
+
return;
|
|
28
|
+
this.session.defineSource(this.name);
|
|
29
|
+
await this.video.play();
|
|
30
|
+
this.running = true;
|
|
31
|
+
}
|
|
32
|
+
/// Grabs the current video frame and submits it to the source. Call
|
|
33
|
+
/// once per frame from the same loop that renders the session; a no-op
|
|
34
|
+
/// until the first frame has enough data to draw.
|
|
35
|
+
pump() {
|
|
36
|
+
if (!this.running || !this.ctx)
|
|
37
|
+
return;
|
|
38
|
+
if (this.video.readyState < 2 || this.video.videoWidth === 0)
|
|
39
|
+
return;
|
|
40
|
+
const w = this.video.videoWidth;
|
|
41
|
+
const h = this.video.videoHeight;
|
|
42
|
+
if (this.canvas.width !== w || this.canvas.height !== h) {
|
|
43
|
+
this.canvas.width = w;
|
|
44
|
+
this.canvas.height = h;
|
|
45
|
+
}
|
|
46
|
+
this.ctx.drawImage(this.video, 0, 0, w, h);
|
|
47
|
+
const rgba = this.ctx.getImageData(0, 0, w, h).data;
|
|
48
|
+
this.session.submitSourceFrameRgba(this.name, new Uint8Array(rgba.buffer), w, h, w * 4);
|
|
49
|
+
}
|
|
50
|
+
/// Stops playback and releases the element.
|
|
51
|
+
stop() {
|
|
52
|
+
this.running = false;
|
|
53
|
+
this.video.pause();
|
|
54
|
+
this.video.removeAttribute("src");
|
|
55
|
+
this.video.load();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=video-texture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-texture.js","sourceRoot":"","sources":["../../src/video-texture.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { GossSession } from "./index";
|
|
2
|
+
|
|
3
|
+
/// The worklet drains chunks the page posts it, so the audio thread
|
|
4
|
+
/// never touches the wasm heap and the engine's graph-thread pull
|
|
5
|
+
/// contract holds; an underrun renders silence.
|
|
6
|
+
const processorSource = `
|
|
7
|
+
class GossLensAudioProcessor extends AudioWorkletProcessor {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
this.queue = [];
|
|
11
|
+
this.offset = 0;
|
|
12
|
+
this.queued = 0;
|
|
13
|
+
this.port.onmessage = (event) => {
|
|
14
|
+
this.queue.push(event.data);
|
|
15
|
+
this.queued += event.data.length;
|
|
16
|
+
while (this.queue.length > 1 && this.queued - this.queue[0].length + this.offset > 16384) {
|
|
17
|
+
this.queued -= this.queue[0].length;
|
|
18
|
+
this.queue.shift();
|
|
19
|
+
this.offset = 0;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
process(_inputs, outputs) {
|
|
24
|
+
const out = outputs[0][0];
|
|
25
|
+
if (!out) return true;
|
|
26
|
+
for (let i = 0; i < out.length; i += 1) {
|
|
27
|
+
const head = this.queue[0];
|
|
28
|
+
if (!head) {
|
|
29
|
+
out[i] = 0;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
out[i] = head[this.offset] / 32768;
|
|
33
|
+
this.offset += 1;
|
|
34
|
+
if (this.offset >= head.length) {
|
|
35
|
+
this.queued -= head.length;
|
|
36
|
+
this.queue.shift();
|
|
37
|
+
this.offset = 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
registerProcessor("goss-lens-audio", GossLensAudioProcessor);
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
/// Routes the lens mixer to the page's speakers. The engine pull is
|
|
47
|
+
/// graph-thread only, so pump() runs in the frame loop on the main
|
|
48
|
+
/// thread and posts each block to an AudioWorklet that plays it.
|
|
49
|
+
export class GossAudioOutput {
|
|
50
|
+
/// The lens mixer's fixed output format.
|
|
51
|
+
static readonly sampleRate = 48_000;
|
|
52
|
+
|
|
53
|
+
private session: GossSession;
|
|
54
|
+
private context: AudioContext | null = null;
|
|
55
|
+
private node: AudioWorkletNode | null = null;
|
|
56
|
+
|
|
57
|
+
constructor(session: GossSession) {
|
|
58
|
+
this.session = session;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Stands the worklet up; browsers require a user gesture before an
|
|
62
|
+
/// AudioContext runs, so call this from one and await it.
|
|
63
|
+
async start(): Promise<void> {
|
|
64
|
+
if (this.context) return;
|
|
65
|
+
const context = new AudioContext({ sampleRate: GossAudioOutput.sampleRate });
|
|
66
|
+
const moduleUrl = URL.createObjectURL(new Blob([processorSource], { type: "text/javascript" }));
|
|
67
|
+
try {
|
|
68
|
+
await context.audioWorklet.addModule(moduleUrl);
|
|
69
|
+
} finally {
|
|
70
|
+
URL.revokeObjectURL(moduleUrl);
|
|
71
|
+
}
|
|
72
|
+
const node = new AudioWorkletNode(context, "goss-lens-audio", {
|
|
73
|
+
numberOfInputs: 0,
|
|
74
|
+
numberOfOutputs: 1,
|
|
75
|
+
outputChannelCount: [1],
|
|
76
|
+
});
|
|
77
|
+
node.connect(context.destination);
|
|
78
|
+
await context.resume();
|
|
79
|
+
this.context = context;
|
|
80
|
+
this.node = node;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Pulls the next mixer block and hands it to the worklet. Call once
|
|
84
|
+
/// per frame from the same loop that ticks the lens. The post
|
|
85
|
+
/// transfers the block's buffer, which detaches it, so each pump is
|
|
86
|
+
/// one fresh small block by design rather than a reusable buffer.
|
|
87
|
+
pump(frames = 800): void {
|
|
88
|
+
const node = this.node;
|
|
89
|
+
if (!node) return;
|
|
90
|
+
const block = this.session.pullAudio(frames);
|
|
91
|
+
node.port.postMessage(block, [block.buffer]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Tears the worklet and context down.
|
|
95
|
+
async stop(): Promise<void> {
|
|
96
|
+
this.node?.disconnect();
|
|
97
|
+
this.node = null;
|
|
98
|
+
const context = this.context;
|
|
99
|
+
this.context = null;
|
|
100
|
+
if (context) await context.close();
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2854,3 +2854,6 @@ export class GossPreviewSession {
|
|
|
2854
2854
|
|
|
2855
2855
|
export { GossWebXRWorldSource } from "./world";
|
|
2856
2856
|
export type { GossXRFrameLike } from "./world";
|
|
2857
|
+
export { GossAudioOutput } from "./audio-output";
|
|
2858
|
+
export { GossMicInput } from "./mic-input";
|
|
2859
|
+
export { GossVideoTexture } from "./video-texture";
|
package/src/mic-input.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { GossSession } from "./index";
|
|
2
|
+
|
|
3
|
+
/// Captures the microphone and feeds it to the engine's audio analysis,
|
|
4
|
+
/// so the level and beat triggers a lens reads fire on web the same way
|
|
5
|
+
/// submitted audio drives them on the native SDKs. A worklet forwards
|
|
6
|
+
/// each block to the main thread, which hands it to submitAudio.
|
|
7
|
+
const captureProcessor = `
|
|
8
|
+
class GossMicCapture extends AudioWorkletProcessor {
|
|
9
|
+
process(inputs) {
|
|
10
|
+
const chan = inputs[0] && inputs[0][0];
|
|
11
|
+
if (chan && chan.length) this.port.postMessage(chan.slice());
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
registerProcessor("goss-mic-capture", GossMicCapture);
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
export class GossMicInput {
|
|
19
|
+
private session: GossSession;
|
|
20
|
+
private context: AudioContext | null = null;
|
|
21
|
+
private stream: MediaStream | null = null;
|
|
22
|
+
private node: AudioWorkletNode | null = null;
|
|
23
|
+
private source: MediaStreamAudioSourceNode | null = null;
|
|
24
|
+
|
|
25
|
+
constructor(session: GossSession) {
|
|
26
|
+
this.session = session;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Requests the microphone and starts forwarding its PCM to the
|
|
30
|
+
/// engine. Call from a user gesture; browsers gate both getUserMedia
|
|
31
|
+
/// and the AudioContext behind one.
|
|
32
|
+
async start(): Promise<void> {
|
|
33
|
+
if (this.context) return;
|
|
34
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
35
|
+
const context = new AudioContext();
|
|
36
|
+
const moduleUrl = URL.createObjectURL(new Blob([captureProcessor], { type: "text/javascript" }));
|
|
37
|
+
try {
|
|
38
|
+
await context.audioWorklet.addModule(moduleUrl);
|
|
39
|
+
} finally {
|
|
40
|
+
URL.revokeObjectURL(moduleUrl);
|
|
41
|
+
}
|
|
42
|
+
const source = context.createMediaStreamSource(stream);
|
|
43
|
+
const node = new AudioWorkletNode(context, "goss-mic-capture", { numberOfOutputs: 0 });
|
|
44
|
+
const rate = context.sampleRate;
|
|
45
|
+
node.port.onmessage = (event) => {
|
|
46
|
+
const block = event.data as Float32Array;
|
|
47
|
+
this.session.submitAudio(block, block.length, rate, 1);
|
|
48
|
+
};
|
|
49
|
+
source.connect(node);
|
|
50
|
+
await context.resume();
|
|
51
|
+
this.context = context;
|
|
52
|
+
this.stream = stream;
|
|
53
|
+
this.source = source;
|
|
54
|
+
this.node = node;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Stops the capture and releases the microphone.
|
|
58
|
+
async stop(): Promise<void> {
|
|
59
|
+
this.source?.disconnect();
|
|
60
|
+
this.node?.disconnect();
|
|
61
|
+
this.source = null;
|
|
62
|
+
this.node = null;
|
|
63
|
+
this.stream?.getTracks().forEach((track) => track.stop());
|
|
64
|
+
this.stream = null;
|
|
65
|
+
const context = this.context;
|
|
66
|
+
this.context = null;
|
|
67
|
+
if (context) await context.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { GossSession } from "./index";
|
|
2
|
+
|
|
3
|
+
/// Plays an MP4 through a hidden video element, the browser's own
|
|
4
|
+
/// decoder, and feeds each frame to a named engine source a lens
|
|
5
|
+
/// composites, so a web lens shows a video texture with no wasm
|
|
6
|
+
/// decoder. The `video.texture` node stays native; this serves web.
|
|
7
|
+
export class GossVideoTexture {
|
|
8
|
+
private session: GossSession;
|
|
9
|
+
private name: string;
|
|
10
|
+
private video: HTMLVideoElement;
|
|
11
|
+
private canvas: HTMLCanvasElement;
|
|
12
|
+
private ctx: CanvasRenderingContext2D | null;
|
|
13
|
+
private running = false;
|
|
14
|
+
|
|
15
|
+
constructor(session: GossSession, name: string, url: string) {
|
|
16
|
+
this.session = session;
|
|
17
|
+
this.name = name;
|
|
18
|
+
this.video = document.createElement("video");
|
|
19
|
+
this.video.src = url;
|
|
20
|
+
this.video.loop = true;
|
|
21
|
+
this.video.muted = true;
|
|
22
|
+
this.video.playsInline = true;
|
|
23
|
+
this.canvas = document.createElement("canvas");
|
|
24
|
+
this.ctx = this.canvas.getContext("2d", { willReadFrequently: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Registers the source and starts playback; pumps run from the
|
|
28
|
+
/// frame loop. Call from a user gesture so autoplay is allowed.
|
|
29
|
+
async start(): Promise<void> {
|
|
30
|
+
if (this.running) return;
|
|
31
|
+
this.session.defineSource(this.name);
|
|
32
|
+
await this.video.play();
|
|
33
|
+
this.running = true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Grabs the current video frame and submits it to the source. Call
|
|
37
|
+
/// once per frame from the same loop that renders the session; a no-op
|
|
38
|
+
/// until the first frame has enough data to draw.
|
|
39
|
+
pump(): void {
|
|
40
|
+
if (!this.running || !this.ctx) return;
|
|
41
|
+
if (this.video.readyState < 2 || this.video.videoWidth === 0) return;
|
|
42
|
+
const w = this.video.videoWidth;
|
|
43
|
+
const h = this.video.videoHeight;
|
|
44
|
+
if (this.canvas.width !== w || this.canvas.height !== h) {
|
|
45
|
+
this.canvas.width = w;
|
|
46
|
+
this.canvas.height = h;
|
|
47
|
+
}
|
|
48
|
+
this.ctx.drawImage(this.video, 0, 0, w, h);
|
|
49
|
+
const rgba = this.ctx.getImageData(0, 0, w, h).data;
|
|
50
|
+
this.session.submitSourceFrameRgba(this.name, new Uint8Array(rgba.buffer), w, h, w * 4);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Stops playback and releases the element.
|
|
54
|
+
stop(): void {
|
|
55
|
+
this.running = false;
|
|
56
|
+
this.video.pause();
|
|
57
|
+
this.video.removeAttribute("src");
|
|
58
|
+
this.video.load();
|
|
59
|
+
}
|
|
60
|
+
}
|