@myzonerocks/gosslens 0.9.0 → 0.10.1
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
package/README.md
CHANGED
|
@@ -1,44 +1,79 @@
|
|
|
1
1
|
# Gosslens - TypeScript SDK
|
|
2
2
|
|
|
3
|
-
TypeScript SDK for [Gosslens](../../include/gosslens.h), a camera engine
|
|
4
|
-
|
|
5
|
-
`
|
|
6
|
-
[
|
|
3
|
+
TypeScript SDK for [Gosslens](../../include/gosslens.h), a camera engine behind
|
|
4
|
+
one C ABI, compiled to `wasm32`. It wraps the engine as `GossEngine`,
|
|
5
|
+
`GossSession`, and `Gosslens`, the same names the [Swift](../swift/README.md) and
|
|
6
|
+
[Kotlin](../kotlin/README.md) SDKs use.
|
|
7
7
|
|
|
8
|
-
This SDK owns camera capture through `getUserMedia`, the render loop,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
[
|
|
13
|
-
guide: build and host the wasm and model assets, the render loop, and the
|
|
14
|
-
tracking worker.
|
|
8
|
+
This SDK owns camera capture through `getUserMedia`, the render loop, and
|
|
9
|
+
decoding the PNGs the core has no decoder for. The frame graph, lens runtime, and
|
|
10
|
+
effect pipeline live in the core. You write TypeScript; the engine is a prebuilt
|
|
11
|
+
WebAssembly build you host and hand the SDK. The [demo](demo/) is a full working
|
|
12
|
+
page, and the cross-platform SDK overview is in the [root README](../../README.md).
|
|
15
13
|
|
|
16
14
|
## Install
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
The npm package is the JavaScript wrapper you add with bun and write TypeScript
|
|
17
|
+
against. It does not carry the engine. The engine it drives is the emscripten
|
|
18
|
+
`gosslens_web.js`/`.wasm` (a WebGL2 build and a WebGPU build) plus
|
|
19
|
+
`gosslens_tracking.wasm`, which you host next to your app.
|
|
20
|
+
|
|
21
|
+
```sh
|
|
19
22
|
bun add @myzonerocks/gosslens
|
|
20
23
|
```
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
Because the browser cannot fetch a `.wasm` out of `node_modules` the way a native
|
|
26
|
+
app links an archive, you serve the prebuilt engine files from your own static
|
|
27
|
+
host and hand the SDK their URLs. The SDK never guesses a path. Every release
|
|
28
|
+
attaches `gosslens-web-engine.zip` - the two `gosslens_web` builds (WebGPU and
|
|
29
|
+
WebGL2 are separate artifacts, not a runtime toggle) and the tracking wasm - so
|
|
30
|
+
unzip it into your static assets. Grab it from the
|
|
31
|
+
[releases page](https://github.com/myzonerocks/gosslens/releases). `pickEngineUrl`
|
|
32
|
+
picks the WebGL2 or WebGPU build at load time from the URLs you point it at, after
|
|
33
|
+
confirming a real WebGPU adapter.
|
|
34
|
+
|
|
35
|
+
### Building from source
|
|
36
|
+
|
|
37
|
+
Prefer compiling the engine and the SDK yourself, from a clone or your own
|
|
38
|
+
fork? Build the engine artifacts:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
zig build wasm-emscripten
|
|
42
|
+
zig build wasm-emscripten-webgpu
|
|
43
|
+
zig build tracking-wasm
|
|
44
|
+
zig build fetch-models
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Inside this monorepo, consume the SDK as a workspace dependency instead of the
|
|
48
|
+
published package:
|
|
23
49
|
|
|
24
50
|
```json
|
|
25
51
|
{ "dependencies": { "@myzonerocks/gosslens": "workspace:*" } }
|
|
26
52
|
```
|
|
27
53
|
|
|
28
|
-
`bun run build` compiles `src/` to `dist/src/` (the package points `main`
|
|
29
|
-
|
|
54
|
+
`bun run build` compiles `src/` to `dist/src/` (the package points `main` and
|
|
55
|
+
`types` there); run it once in `sdk/ts` so `dist/` exists before a workspace
|
|
30
56
|
consumer resolves it.
|
|
31
57
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
They are separate artifacts, not bundled.
|
|
58
|
+
## The render loop
|
|
59
|
+
|
|
60
|
+
`GossPreviewSession` does the engine, renderer, session, and capture loop in one
|
|
61
|
+
call - most apps want this:
|
|
37
62
|
|
|
38
|
-
|
|
63
|
+
```typescript
|
|
64
|
+
import { GossPreviewSession, pickEngineUrl } from "@myzonerocks/gosslens";
|
|
65
|
+
|
|
66
|
+
const wasmJsUrl = await pickEngineUrl(webgpuUrl, webgl2Url);
|
|
67
|
+
const preview = await GossPreviewSession.create(canvas, wasmJsUrl);
|
|
68
|
+
preview.activateLens(manifestJson);
|
|
69
|
+
```
|
|
39
70
|
|
|
40
|
-
|
|
41
|
-
|
|
71
|
+
`pickEngineUrl` confirms a real WebGPU adapter before choosing, and falls back to
|
|
72
|
+
the WebGL2 URL. `create` takes an optional third `events` argument for the
|
|
73
|
+
capture-loop callbacks. If you drive the loop yourself, the pieces are public too:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { Gosslens, GossEngine, GossSession } from "@myzonerocks/gosslens";
|
|
42
77
|
|
|
43
78
|
const gosslens = await Gosslens.load(canvas, wasmJsUrl);
|
|
44
79
|
const engine = GossEngine.create(gosslens);
|
|
@@ -47,37 +82,374 @@ const session = GossSession.create(engine);
|
|
|
47
82
|
|
|
48
83
|
session.submitFrameRgbaCopy(rgba, width * 4, width, height);
|
|
49
84
|
engine.renderFrame(session);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Camera controls
|
|
50
88
|
|
|
51
|
-
|
|
89
|
+
The engine never touches the camera. It holds declarative intent you set,
|
|
90
|
+
normalizes it, and hands it back for you to apply as getUserMedia track
|
|
91
|
+
constraints. The controls live on `GossSession`, so reach them through
|
|
92
|
+
`preview.session` if you took the `GossPreviewSession` path:
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
session.setCameraControls({ ...session.cameraControls(), flashMode: 2, zoomFactor: 2 });
|
|
96
|
+
|
|
97
|
+
const applied = session.cameraControls();
|
|
98
|
+
await track.applyConstraints({ advanced: [{ zoom: applied.zoomFactor,
|
|
99
|
+
torch: applied.torch === 1 }] });
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`GossCameraControls` also carries focus and exposure mode and points, the
|
|
103
|
+
exposure bias, and the front-camera mirror-save policy. `setRecordingPolicy`/
|
|
104
|
+
`recordingPolicy` (clip cap, segment and loop mode, speed preset, mic mute,
|
|
105
|
+
save-original, stabilization) round-trips for your `MediaRecorder`, and
|
|
106
|
+
`setCaptureUi`/`captureUi` (grid, level, shutter mode, self-timer, night mode,
|
|
107
|
+
front-screen flash) for the capture chrome you draw. The engine validates and
|
|
108
|
+
clamps every field; you read it back and apply it.
|
|
109
|
+
|
|
110
|
+
## Lenses
|
|
111
|
+
|
|
112
|
+
A lens is a manifest plus its assets. Activate one from its manifest JSON, and
|
|
113
|
+
drop it again with `deactivateLens`:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
52
116
|
session.activateLens(manifestJson);
|
|
117
|
+
session.deactivateLens();
|
|
53
118
|
```
|
|
54
119
|
|
|
55
|
-
|
|
56
|
-
|
|
120
|
+
The web build activates from the manifest JSON directly. The directory-bundle
|
|
121
|
+
path the native SDKs also take is not wired here: a wasm target has no file IO,
|
|
122
|
+
and the `shader.pass`, `lut.pass`, and `blend.pass` nodes it would carry need
|
|
123
|
+
compiled resources this SDK has no way to hand over yet. A lens built entirely
|
|
124
|
+
from `beauty.*` nodes (the beauty-baseline lens, say) activates and runs for real
|
|
125
|
+
regardless, through the same beauty chain the effects below drive.
|
|
57
126
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
127
|
+
A lens that reads face, hand, pose, or segmentation data renders nothing until
|
|
128
|
+
you feed the matching tracker result each frame (see Tracking); it stays silent,
|
|
129
|
+
with no error. A lens's triggers also react to signals you already feed:
|
|
130
|
+
`camera.zoom`, `camera.focus` and `camera.exposure` follow the camera controls
|
|
131
|
+
above, `geo.in_region` follows the geofence below, and `gaze.*` and
|
|
132
|
+
`head.nod`/`head.shake`/`head.tilt` follow the face tracker. The full grammar is
|
|
133
|
+
in [the lens spec](../../lenses/SPEC.md).
|
|
61
134
|
|
|
62
|
-
|
|
135
|
+
Advance the lens's own clock, triggers, and script nodes once per display frame
|
|
136
|
+
with `tickLens`, passing the live signals this tick evaluates against:
|
|
63
137
|
|
|
64
|
-
```
|
|
65
|
-
|
|
138
|
+
```typescript
|
|
139
|
+
session.tickLens(dtUs, {
|
|
140
|
+
hasFace: result.presence >= 0.5,
|
|
141
|
+
blendshapes: result.blendshapes,
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Omitted signal fields read as false or zero, so a bare `tickLens(dtUs)` only
|
|
146
|
+
advances triggers with no `when` gate. `fireEvent(name)` delivers a named event
|
|
147
|
+
to the lens's `event('name')` triggers for the next tick, and
|
|
148
|
+
`parameterValue(name)` reads a live lens parameter back, including whatever a
|
|
149
|
+
script node last wrote.
|
|
150
|
+
|
|
151
|
+
## Beauty and makeup
|
|
152
|
+
|
|
153
|
+
The beauty effects are direct session calls, each an amount in 0..1:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
session.setSmooth(0.6);
|
|
157
|
+
session.setWhiten(0.5);
|
|
158
|
+
session.setThinFace(0.3);
|
|
159
|
+
session.setBigEye(0.2);
|
|
160
|
+
session.setLipstick(0.7);
|
|
161
|
+
session.setBlush(0.4);
|
|
162
|
+
```
|
|
66
163
|
|
|
67
|
-
|
|
164
|
+
Whiten reads four lookup textures, and lipstick and blush their own source
|
|
165
|
+
images, none of which the core decodes. Load them once after setup; the SDK
|
|
166
|
+
decodes the PNGs, and until they resolve the matching setter stays a no-op:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
await session.loadWhitenLuts(new URL("./res/", baseUrl)); // the four lookup_*.png
|
|
170
|
+
await session.loadMakeupTextures(new URL("./res/", baseUrl)); // mouth.png, blusher.png
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`setMakeupReference` samples a reference photo's makeup color per face part: the
|
|
174
|
+
lips, eyes, brows, and a cheek-and-forehead skin patch, so a lens's `tint.pass`
|
|
175
|
+
with a reference source paints the live face in that color and a foundation over
|
|
176
|
+
`face_skin` matches the reference's skin tone. Pass the reference RGBA and its
|
|
177
|
+
478-point face landmarks; an empty landmarks array clears it:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
session.setMakeupReference(refRgba, refWidth, refHeight, refLandmarks);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Tracking
|
|
184
|
+
|
|
185
|
+
Face, hand, pose, and segmentation run in the `gosslens_tracking.wasm` module,
|
|
186
|
+
off the main thread in a Worker. Each pipeline is a class that takes the module
|
|
187
|
+
bytes and a model bundle and runs inference synchronously inside the worker:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// tracking-worker.ts
|
|
191
|
+
import { GossFaceTracker, GossHandTracker, GossPoseTracker } from "@myzonerocks/gosslens";
|
|
192
|
+
|
|
193
|
+
const moduleBytes = await (await fetch(trackingWasmUrl)).arrayBuffer();
|
|
194
|
+
const face = await GossFaceTracker.create(moduleBytes, faceTaskBytes);
|
|
195
|
+
const hands = await GossHandTracker.create(moduleBytes, gestureTaskBytes);
|
|
196
|
+
const pose = await GossPoseTracker.create(moduleBytes, poseTaskBytes);
|
|
197
|
+
// per frame, on RGBA pixels from the camera canvas:
|
|
198
|
+
const faceResult = face.process(rgba, width, height, timestampUs);
|
|
199
|
+
const handResult = hands.process(rgba, width, height, timestampUs);
|
|
200
|
+
const poseResult = pose.process(rgba, width, height, timestampUs);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Standing a tracker up is the web equivalent of the native SDKs' enable calls:
|
|
204
|
+
this build runs no internal engine tracker, so you run the pipeline you need and
|
|
205
|
+
feed its result back. `GossHandTracker` returns up to two hands, each with 21
|
|
206
|
+
landmarks, a handedness score, and a canned gesture when the gesture bundle is
|
|
207
|
+
loaded; `GossPoseTracker` returns the 33-point skeleton.
|
|
208
|
+
[`demo/tracking-worker.ts`](demo/tracking-worker.ts) is the reference worker, and
|
|
209
|
+
[`demo/track-worker.ts`](demo/track-worker.ts) stands all four pipelines up over
|
|
210
|
+
still images.
|
|
211
|
+
|
|
212
|
+
The `.task`/`.tflite` bundles (`face_landmarker.task`, `gesture_recognizer.task`,
|
|
213
|
+
`pose_landmarker_full.task`, `selfie_multiclass.tflite`) are the ones
|
|
214
|
+
`fetch-models` writes; host and fetch the ones your lenses use.
|
|
215
|
+
|
|
216
|
+
Feed the tracked results back to the session for a lens to anchor to. The
|
|
217
|
+
single-face path is `setFaceLandmarks`. To fan a face-anchored lens out across
|
|
218
|
+
every face in frame, hand the engine the faces you tracked this frame:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
session.submitFaces(faces); // GossFaceInput[], up to GOSS_FACE_MAX; [] clears
|
|
222
|
+
for (let i = 0; i < session.faceCount(); i++) {
|
|
223
|
+
const face = session.faceResultAt(i);
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`submitBodies` is the multi-person equivalent, reaching every tracked figure;
|
|
228
|
+
`setPoseUpperBody(true)` drops the skeleton's legs (knees down) for selfie
|
|
229
|
+
framing where they are out of shot:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
session.submitBodies(bodies); // GossPoseInput[], up to GOSS_BODY_MAX; [] clears
|
|
233
|
+
for (let i = 0; i < session.bodyCount(); i++) {
|
|
234
|
+
const body = session.bodyResultAt(i);
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`faceRegion` returns the tracked point of a named attach point for pinning
|
|
239
|
+
content - forehead, glabella, nose tip, chin, an eye, a cheek, an ear, or a mouth
|
|
240
|
+
corner - reading the face landmarks you last submitted:
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
const p = session.faceRegion(GossFaceRegion.Forehead);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
For a body or hand joint, read the point off the tracker result directly: the
|
|
247
|
+
pose result carries all 33 landmarks, each hand its 21. The `bodyJoint` and
|
|
248
|
+
`handJoint` session pins read the engine's own internal trackers, which this
|
|
249
|
+
build does not stand up, so they stay empty here; on native they return the named
|
|
250
|
+
joint's point.
|
|
251
|
+
|
|
252
|
+
The face tracker also drives the `gaze.*` and `head.nod`/`head.shake`/`head.tilt`
|
|
253
|
+
lens triggers; `camera.*` follow the camera controls and `geo.in_region` the
|
|
254
|
+
geofence below. The full grammar is in [the lens spec](../../lenses/SPEC.md).
|
|
255
|
+
|
|
256
|
+
## Segmentation
|
|
257
|
+
|
|
258
|
+
`GossSegmenter` runs a single `.tflite` model through the same tracking module
|
|
259
|
+
and returns a subject mask, `GOSS_SEGMENTATION_MASK_SIDE` squared floats, plus the
|
|
260
|
+
model's own class channels. Feed the subject mask back as the texture the lens's
|
|
261
|
+
blend and mask channels sample:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
const segmenter = await GossSegmenter.create(moduleBytes, selfieMulticlassBytes);
|
|
265
|
+
const subject = segmenter.process(rgba, width, height);
|
|
266
|
+
session.setSegmentationMask(subject); // null clears it
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`selfie_multiclass.tflite` publishes the seven channels in
|
|
270
|
+
`GOSS_SEGMENTATION_CHANNELS` (person, background, hair, body_skin, face_skin,
|
|
271
|
+
clothes, others); `hair_segmenter.tflite` and `deeplab_v3.tflite` are the other
|
|
272
|
+
shipped models, a single hair mask and another multiclass label set. A lens that
|
|
273
|
+
names class channels reports them as a bitmask from `segmentationChannels`; upload
|
|
274
|
+
exactly those each frame with `setSegmentationClassMask`, after the subject mask,
|
|
275
|
+
since setting the subject clears the classes:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
const wanted = session.segmentationChannels();
|
|
279
|
+
for (let channel = 1; channel < GOSS_SEGMENTATION_CHANNELS.length; channel++) {
|
|
280
|
+
if (wanted & (1 << channel)) {
|
|
281
|
+
session.setSegmentationClassMask(channel, segmenter.classMask(channel - 1));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## World and AR
|
|
287
|
+
|
|
288
|
+
`GossWebXRWorldSource` feeds WebXR frames into the session's world tracker, so a
|
|
289
|
+
world-anchored lens or AR brush stroke stays fixed in the scene. Drive it from the
|
|
290
|
+
XR animation loop:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { GossWebXRWorldSource } from "@myzonerocks/gosslens";
|
|
68
294
|
|
|
69
295
|
const world = new GossWebXRWorldSource(session);
|
|
70
296
|
// in the XR animation loop:
|
|
71
297
|
world.onFrame(xrFrame, referenceSpace, timestampUs);
|
|
72
298
|
```
|
|
73
299
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
300
|
+
## Depth
|
|
301
|
+
|
|
302
|
+
If the XR session was granted `depth-sensing`, feed each frame's depth so a
|
|
303
|
+
depth-aware lens can occlude content behind real geometry. The map is metres per
|
|
304
|
+
pixel, row major, with the near and far range it spans:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
const info = frame.getDepthInformation(view); // WebXR depth-sensing
|
|
308
|
+
// read info into a Float32Array of metres, then:
|
|
309
|
+
session.submitDepth(depth, info.width, info.height, 0.1, 5.0);
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
An empty array clears it. The engine keeps the latest map for the occlusion pass.
|
|
313
|
+
|
|
314
|
+
## Camera intrinsics
|
|
315
|
+
|
|
316
|
+
If the camera reports its calibration, feed it once so an `undistort.pass` can
|
|
317
|
+
straighten wide-angle lens distortion. The focal lengths and principal point are
|
|
318
|
+
in pixels of the submitted frame, followed by the radial coefficients (k1, k2):
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
// fx, fy, cx, cy from the platform's camera calibration, then:
|
|
322
|
+
session.submitCameraIntrinsics(fx, fy, cx, cy, new Float32Array([k1, k2]));
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
An empty array clears them, leaving an `undistort.pass` inert.
|
|
326
|
+
|
|
327
|
+
## Geofilters
|
|
328
|
+
|
|
329
|
+
A lens can gate on place. Feed a location fix from the Geolocation API and
|
|
330
|
+
describe the region the lens belongs to; the engine decides membership in wasm
|
|
331
|
+
and the fix never leaves the page:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
navigator.geolocation.watchPosition((pos) => {
|
|
335
|
+
session.submitLocation(pos.coords.latitude, pos.coords.longitude,
|
|
336
|
+
pos.coords.accuracy, pos.timestamp * 1000);
|
|
337
|
+
});
|
|
338
|
+
session.setGeofence(lat, lon, 150);
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
`setGeofenceBBox` and `setGeofencePolygon` describe a box or a ring instead;
|
|
342
|
+
`setGeoAccuracy` sets the worst fix that still counts as inside, and
|
|
343
|
+
`clearGeofence` drops the gate. Membership drives the lens grammar's
|
|
344
|
+
`geo.in_region` trigger.
|
|
345
|
+
|
|
346
|
+
## Brush
|
|
347
|
+
|
|
348
|
+
Freehand strokes composite over the frame. Open a stroke, push normalized points,
|
|
349
|
+
and close it; the engine keeps the undo/redo stack and hands back the ribbon
|
|
350
|
+
(x, y, r, g, b, a per vertex):
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
session.setBrushStyle(1, 0.4, 0.6, 1, 0.01);
|
|
354
|
+
session.setBrushMode(3); // 0 pen, 1 highlighter, 2 marker, 3 neon
|
|
355
|
+
session.beginStroke();
|
|
356
|
+
session.addStrokePoint(nx, ny);
|
|
357
|
+
session.endStroke();
|
|
358
|
+
const ribbon = session.brushVertices();
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
`setARBrushStyle`/`beginARStroke`/`addARStrokePoint(x, y, z)`/`endARStroke` are the
|
|
362
|
+
world-anchored twin: points are pushed in the world frame world tracking reports,
|
|
363
|
+
so a stroke stays fixed in the scene.
|
|
364
|
+
|
|
365
|
+
## Capture and recording
|
|
366
|
+
|
|
367
|
+
The browser owns encoding on the web, so photo and video capture run through it.
|
|
368
|
+
`captureFrame` reads the composited canvas back and returns a PNG data URL:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
const png = await engine.captureFrame(session); // or preview.captureFrame()
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`captureStill` is the high-resolution path, decoupled from the preview size: it
|
|
375
|
+
renders the composite at its own or a requested resolution, supersamples, and
|
|
376
|
+
returns the encoded bytes with the format, gamut, and bit depth you tag. It needs
|
|
377
|
+
the wasm renderer, so it is a no-op on the pure WebGL2 fallback:
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
const jpeg = await engine.captureStill(session, { width: 4032, height: 3024, format: 1, quality: 90 });
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
For video, drive a `MediaRecorder` off `canvas.captureStream()` yourself.
|
|
384
|
+
`setRecordingPolicy`/`recordingPolicy` round-trips the clip cap, segment and loop
|
|
385
|
+
mode, speed preset, mic mute, save-original, and stabilization the engine
|
|
386
|
+
normalizes, so the recorder and chrome you build read one validated policy. The
|
|
387
|
+
still encoder is the core's own (PNG and JPEG, wide-gamut and 16-bit PNG tagged);
|
|
388
|
+
the live video encoder stays native-only, which is why recording runs through the
|
|
389
|
+
browser. See [PARITY.md](../../docs/PARITY.md).
|
|
390
|
+
|
|
391
|
+
## Compositing
|
|
392
|
+
|
|
393
|
+
Beyond the camera, a session composites named RGBA sources into one frame, for a
|
|
394
|
+
duet, a stitch, or a live grid. The camera is the implicit source 0; define others
|
|
395
|
+
by name and push a frame into each:
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
session.defineSource("guest");
|
|
399
|
+
session.submitSourceFrameRgba("guest", rgba, width, height, width * 4);
|
|
400
|
+
session.setLayout(3); // 0 custom, 1 side-by-side, 2 top-bottom, 3 pip, 4 grid, 5 overlay
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
`setSourceComposite` sets a source's opacity and key mode (none, matte, or a
|
|
404
|
+
chroma key with its color and similarity), so a keyed guest drops onto the base:
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
session.setSourceComposite("guest", 1, 2, [0, 1, 0], 0.4); // chroma-key green
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
`defineScreenShare` registers a source whose frame letterboxes to fit its cell,
|
|
411
|
+
`removeSource` drops one, and `clearLayout` returns to the camera alone.
|
|
412
|
+
|
|
413
|
+
## Lives and calls
|
|
414
|
+
|
|
415
|
+
The web is the easy case: the rendered canvas is already a live video source.
|
|
416
|
+
`canvas.captureStream()` hands you a `MediaStreamTrack` of the composited,
|
|
417
|
+
lens-baked output with no readback and no copy - publish it straight to LiveKit or
|
|
418
|
+
any WebRTC peer:
|
|
419
|
+
|
|
420
|
+
```typescript
|
|
421
|
+
const track = canvas.captureStream(30).getVideoTracks()[0];
|
|
422
|
+
// publish `track` through your LiveKit room or RTCPeerConnection
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Keep the render loop running (`renderFrame` per frame) and the track carries every
|
|
426
|
+
composited frame. Reach for `captureLiveFrame` only when you need the raw pixels
|
|
427
|
+
(BGRA by default) rather than a track. For audio, `mixOutputAudio` folds the
|
|
428
|
+
lens's own sound into the mic block you are about to publish and returns the mixed
|
|
429
|
+
interleaved s16 for your outgoing WebRTC audio track: it resamples the lens sound
|
|
430
|
+
to your track's rate and sums it in, so there is nothing to hand-mix (pass `null`
|
|
431
|
+
for the mic to send the lens sound over silence). `pullAudio` still pulls the lens
|
|
432
|
+
sound alone for local WebAudio playback with no call in progress; `GossAudioOutput`
|
|
433
|
+
wraps that playback (an `AudioWorklet` it owns, `start()` from a gesture, `pump()`
|
|
434
|
+
each frame beside `tickLens`). `GossMicInput` captures the microphone into
|
|
435
|
+
`submitAudio` so level and beat triggers fire in the browser, and
|
|
436
|
+
`GossVideoTexture` plays an MP4 through the browser's decoder into a named
|
|
437
|
+
source a lens composites.
|
|
438
|
+
|
|
439
|
+
When the lens carries an `audio.infer` node with a caption binding, the engine
|
|
440
|
+
runs on-device ASR over the mic and `captionText` reads the decoded text by the
|
|
441
|
+
node's id, for the page to draw as a live subtitle:
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
const line = session.captionText("caption");
|
|
445
|
+
if (line) subtitleEl.textContent = line;
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## Method names
|
|
449
|
+
|
|
450
|
+
The operation names match the other SDKs: `GossEngine.create(gosslens)`,
|
|
451
|
+
`GossSession.create(engine)`, `submitFrameRgbaCopy`, `renderFrame`. The full table
|
|
452
|
+
is in [API.md](../../docs/API.md).
|
|
81
453
|
|
|
82
454
|
## Demo app
|
|
83
455
|
|
|
@@ -89,8 +461,3 @@ cross-platform SDK overview is in the [root README](../../README.md).
|
|
|
89
461
|
parsers and the WebGPU/WebGL2 pick. The browser end-to-end proofs live in
|
|
90
462
|
[`demo/prove.ts`](demo/prove.ts) and [`demo/track-prove.ts`](demo/track-prove.ts),
|
|
91
463
|
with the host conformance in [`harness/`](../../harness/).
|
|
92
|
-
|
|
93
|
-
## TODO
|
|
94
|
-
|
|
95
|
-
- Publish `@myzonerocks/gosslens` to npm; until then `bun add` resolves
|
|
96
|
-
only inside the workspace.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { GossSession } from "./index";
|
|
2
|
+
export declare class GossAudioOutput {
|
|
3
|
+
static readonly sampleRate = 48000;
|
|
4
|
+
private session;
|
|
5
|
+
private context;
|
|
6
|
+
private node;
|
|
7
|
+
constructor(session: GossSession);
|
|
8
|
+
start(): Promise<void>;
|
|
9
|
+
pump(frames?: number): void;
|
|
10
|
+
stop(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=audio-output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-output.d.ts","sourceRoot":"","sources":["../../src/audio-output.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAgD3C,qBAAa,eAAe;IAE1B,MAAM,CAAC,QAAQ,CAAC,UAAU,SAAU;IAEpC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,IAAI,CAAiC;IAE7C,YAAY,OAAO,EAAE,WAAW,EAE/B;IAIK,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAkB3B;IAMD,IAAI,CAAC,MAAM,SAAM,GAAG,IAAI,CAKvB;IAGK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAM1B;CACF"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/// The worklet drains chunks the page posts it, so the audio thread
|
|
2
|
+
/// never touches the wasm heap and the engine's graph-thread pull
|
|
3
|
+
/// contract holds; an underrun renders silence.
|
|
4
|
+
const processorSource = `
|
|
5
|
+
class GossLensAudioProcessor extends AudioWorkletProcessor {
|
|
6
|
+
constructor() {
|
|
7
|
+
super();
|
|
8
|
+
this.queue = [];
|
|
9
|
+
this.offset = 0;
|
|
10
|
+
this.queued = 0;
|
|
11
|
+
this.port.onmessage = (event) => {
|
|
12
|
+
this.queue.push(event.data);
|
|
13
|
+
this.queued += event.data.length;
|
|
14
|
+
while (this.queue.length > 1 && this.queued - this.queue[0].length + this.offset > 16384) {
|
|
15
|
+
this.queued -= this.queue[0].length;
|
|
16
|
+
this.queue.shift();
|
|
17
|
+
this.offset = 0;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
process(_inputs, outputs) {
|
|
22
|
+
const out = outputs[0][0];
|
|
23
|
+
if (!out) return true;
|
|
24
|
+
for (let i = 0; i < out.length; i += 1) {
|
|
25
|
+
const head = this.queue[0];
|
|
26
|
+
if (!head) {
|
|
27
|
+
out[i] = 0;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
out[i] = head[this.offset] / 32768;
|
|
31
|
+
this.offset += 1;
|
|
32
|
+
if (this.offset >= head.length) {
|
|
33
|
+
this.queued -= head.length;
|
|
34
|
+
this.queue.shift();
|
|
35
|
+
this.offset = 0;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
registerProcessor("goss-lens-audio", GossLensAudioProcessor);
|
|
42
|
+
`;
|
|
43
|
+
/// Routes the lens mixer to the page's speakers. The engine pull is
|
|
44
|
+
/// graph-thread only, so pump() runs in the frame loop on the main
|
|
45
|
+
/// thread and posts each block to an AudioWorklet that plays it.
|
|
46
|
+
export class GossAudioOutput {
|
|
47
|
+
/// The lens mixer's fixed output format.
|
|
48
|
+
static sampleRate = 48_000;
|
|
49
|
+
session;
|
|
50
|
+
context = null;
|
|
51
|
+
node = null;
|
|
52
|
+
constructor(session) {
|
|
53
|
+
this.session = session;
|
|
54
|
+
}
|
|
55
|
+
/// Stands the worklet up; browsers require a user gesture before an
|
|
56
|
+
/// AudioContext runs, so call this from one and await it.
|
|
57
|
+
async start() {
|
|
58
|
+
if (this.context)
|
|
59
|
+
return;
|
|
60
|
+
const context = new AudioContext({ sampleRate: GossAudioOutput.sampleRate });
|
|
61
|
+
const moduleUrl = URL.createObjectURL(new Blob([processorSource], { type: "text/javascript" }));
|
|
62
|
+
try {
|
|
63
|
+
await context.audioWorklet.addModule(moduleUrl);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
URL.revokeObjectURL(moduleUrl);
|
|
67
|
+
}
|
|
68
|
+
const node = new AudioWorkletNode(context, "goss-lens-audio", {
|
|
69
|
+
numberOfInputs: 0,
|
|
70
|
+
numberOfOutputs: 1,
|
|
71
|
+
outputChannelCount: [1],
|
|
72
|
+
});
|
|
73
|
+
node.connect(context.destination);
|
|
74
|
+
await context.resume();
|
|
75
|
+
this.context = context;
|
|
76
|
+
this.node = node;
|
|
77
|
+
}
|
|
78
|
+
/// Pulls the next mixer block and hands it to the worklet. Call once
|
|
79
|
+
/// per frame from the same loop that ticks the lens. The post
|
|
80
|
+
/// transfers the block's buffer, which detaches it, so each pump is
|
|
81
|
+
/// one fresh small block by design rather than a reusable buffer.
|
|
82
|
+
pump(frames = 800) {
|
|
83
|
+
const node = this.node;
|
|
84
|
+
if (!node)
|
|
85
|
+
return;
|
|
86
|
+
const block = this.session.pullAudio(frames);
|
|
87
|
+
node.port.postMessage(block, [block.buffer]);
|
|
88
|
+
}
|
|
89
|
+
/// Tears the worklet and context down.
|
|
90
|
+
async stop() {
|
|
91
|
+
this.node?.disconnect();
|
|
92
|
+
this.node = null;
|
|
93
|
+
const context = this.context;
|
|
94
|
+
this.context = null;
|
|
95
|
+
if (context)
|
|
96
|
+
await context.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=audio-output.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audio-output.js","sourceRoot":"","sources":["../../src/audio-output.ts"],"names":[],"mappings":"AAEA,oEAAoE;AACpE,kEAAkE;AAClE,gDAAgD;AAChD,MAAM,eAAe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCvB,CAAC;AAEF,oEAAoE;AACpE,mEAAmE;AACnE,iEAAiE;AACjE,MAAM,OAAO,eAAe;IAC1B,yCAAyC;IACzC,MAAM,CAAU,UAAU,GAAG,MAAM,CAAC;IAE5B,OAAO,CAAc;IACrB,OAAO,GAAwB,IAAI,CAAC;IACpC,IAAI,GAA4B,IAAI,CAAC;IAE7C,YAAY,OAAoB;QAC9B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,oEAAoE;IACpE,0DAA0D;IAC1D,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QACzB,MAAM,OAAO,GAAG,IAAI,YAAY,CAAC,EAAE,UAAU,EAAE,eAAe,CAAC,UAAU,EAAE,CAAC,CAAC;QAC7E,MAAM,SAAS,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,IAAI,CAAC,CAAC,eAAe,CAAC,EAAE,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC,CAAC;QAChG,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,IAAI,GAAG,IAAI,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,EAAE;YAC5D,cAAc,EAAE,CAAC;YACjB,eAAe,EAAE,CAAC;YAClB,kBAAkB,EAAE,CAAC,CAAC,CAAC;SACxB,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QAClC,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,qEAAqE;IACrE,8DAA8D;IAC9D,oEAAoE;IACpE,kEAAkE;IAClE,IAAI,CAAC,MAAM,GAAG,GAAG;QACf,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC7C,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;IAC/C,CAAC;IAED,uCAAuC;IACvC,KAAK,CAAC,IAAI;QACR,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,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"}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -526,4 +526,7 @@ export declare class GossPreviewSession {
|
|
|
526
526
|
}
|
|
527
527
|
export { GossWebXRWorldSource } from "./world";
|
|
528
528
|
export type { GossXRFrameLike } from "./world";
|
|
529
|
+
export { GossAudioOutput } from "./audio-output";
|
|
530
|
+
export { GossMicInput } from "./mic-input";
|
|
531
|
+
export { GossVideoTexture } from "./video-texture";
|
|
529
532
|
//# sourceMappingURL=index.d.ts.map
|