@lumencast/runtime 0.11.0 → 0.12.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 +39 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/{broadcast-DtHoU_fS.js → broadcast-Dg77Br5j.js} +3 -3
- package/dist/{broadcast-DtHoU_fS.js.map → broadcast-Dg77Br5j.js.map} +1 -1
- package/dist/{control-B9frEbNG.js → control-iPqpFp80.js} +4 -4
- package/dist/{control-B9frEbNG.js.map → control-iPqpFp80.js.map} +1 -1
- package/dist/{index-Dz27r92m.js → index-C4r-fG1q.js} +361 -326
- package/dist/index-C4r-fG1q.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lumencast.js +1 -1
- package/dist/mount.d.ts.map +1 -1
- package/dist/mount.js +52 -0
- package/dist/mount.js.map +1 -1
- package/dist/render/primitives/capture-stream-cache.d.ts +26 -0
- package/dist/render/primitives/capture-stream-cache.d.ts.map +1 -0
- package/dist/render/primitives/capture-stream-cache.js +141 -0
- package/dist/render/primitives/capture-stream-cache.js.map +1 -0
- package/dist/render/primitives/capture.d.ts +1 -1
- package/dist/render/primitives/capture.d.ts.map +1 -1
- package/dist/render/primitives/capture.js +20 -87
- package/dist/render/primitives/capture.js.map +1 -1
- package/dist/{status-pill-B2vBTwRC.js → status-pill-nls_2xkN.js} +2 -2
- package/dist/{status-pill-B2vBTwRC.js.map → status-pill-nls_2xkN.js.map} +1 -1
- package/dist/{test-DD2SBDku.js → test-d8HW2LcF.js} +4 -4
- package/dist/{test-DD2SBDku.js.map → test-d8HW2LcF.js.map} +1 -1
- package/dist/transport/ws.d.ts +4 -1
- package/dist/transport/ws.d.ts.map +1 -1
- package/dist/transport/ws.js +7 -0
- package/dist/transport/ws.js.map +1 -1
- package/dist/{tree-CgU_sUwI.js → tree-BME9ba3m.js} +589 -567
- package/dist/tree-BME9ba3m.js.map +1 -0
- package/dist/types.d.ts +18 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/webrtc/index.d.ts +1 -1
- package/dist/webrtc/index.d.ts.map +1 -1
- package/dist/webrtc/index.js.map +1 -1
- package/dist/webrtc/peer-stream-registry.d.ts +17 -0
- package/dist/webrtc/peer-stream-registry.d.ts.map +1 -1
- package/dist/webrtc/peer-stream-registry.js +22 -0
- package/dist/webrtc/peer-stream-registry.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +1 -0
- package/src/mount.ts +54 -0
- package/src/render/primitives/capture-stream-cache.ts +164 -0
- package/src/render/primitives/capture.tsx +19 -93
- package/src/transport/ws.ts +11 -0
- package/src/types.ts +18 -2
- package/src/webrtc/index.ts +1 -0
- package/src/webrtc/peer-stream-registry.ts +38 -0
- package/dist/index-Dz27r92m.js.map +0 -1
- package/dist/tree-CgU_sUwI.js.map +0 -1
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ErrorCode } from "@lumencast/protocol";
|
|
1
|
+
import type { ErrorCode, SceneRosterEntry } from "@lumencast/protocol";
|
|
2
2
|
import type { ResolveCaptureDevice } from "./render/primitives/capture";
|
|
3
3
|
import type { ResolvePeerStream, SubscribePeerStream } from "./render/primitives/media";
|
|
4
4
|
import type { ReservedCamLeaves } from "./state/reserved-leaves";
|
|
@@ -14,7 +14,12 @@ export interface LumencastError {
|
|
|
14
14
|
recoverable: boolean;
|
|
15
15
|
}
|
|
16
16
|
export interface LumencastMetric {
|
|
17
|
-
name: "delta_received" | "delta_applied" | "frame_dropped" | "reconnect" | "snapshot_received" | "scene_changed"
|
|
17
|
+
name: "delta_received" | "delta_applied" | "frame_dropped" | "reconnect" | "snapshot_received" | "scene_changed"
|
|
18
|
+
/** A render bundle was warmed ahead of time from a roster entry (either a
|
|
19
|
+
* `scene_roster` frame or the `preloadRoster` mount option). Emitted once
|
|
20
|
+
* the warm fetch resolves (or from cache). Carries `scene_id` +
|
|
21
|
+
* `scene_version` + `source` ("frame" | "option"). */
|
|
22
|
+
| "roster_preloaded";
|
|
18
23
|
[key: string]: unknown;
|
|
19
24
|
}
|
|
20
25
|
/** Anti-silent-drop render diagnostic (ADR 001 §3.4, issue #34).
|
|
@@ -88,6 +93,17 @@ export interface MountOptions {
|
|
|
88
93
|
* reads it. Omit it and the reserved leaves are simply not surfaced (the
|
|
89
94
|
* preview/headless paths are unaffected). */
|
|
90
95
|
onReservedLeaves?: (leaves: ReservedCamLeaves) => void;
|
|
96
|
+
/** Preload the render bundles of a known scene roster so the FIRST switch to
|
|
97
|
+
* each scene is instant (a warm cache hit instead of a blocking fetch).
|
|
98
|
+
* Each entry is `{ scene_id, scene_version }`. Warmed in the background right
|
|
99
|
+
* after mount — best-effort: a failed warm is swallowed (the scene still
|
|
100
|
+
* fetches on demand at switch time) and never blocks or errors the mount.
|
|
101
|
+
*
|
|
102
|
+
* This is the PUBLIC preload surface (lumencast-js #87b) for hosts that
|
|
103
|
+
* already know the roster at mount time. When the server also emits
|
|
104
|
+
* `scene_roster` frames the runtime warms from those too — both paths feed
|
|
105
|
+
* the same cache and are idempotent (a version is warmed at most once). */
|
|
106
|
+
preloadRoster?: readonly SceneRosterEntry[];
|
|
91
107
|
}
|
|
92
108
|
export interface LumencastHandle {
|
|
93
109
|
/** Tear down the WS, unmount the React tree, release timers. Idempotent. */
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACxF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAEjE,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,SAAS,GAAG,MAAM,CAAC;AAE7D,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,YAAY,GAAG,MAAM,CAAC;AAErE,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9B;AAED,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,sBAAsB,CAAC;AAE7D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EACA,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,WAAW,GACX,mBAAmB,GACnB,eAAe;IACjB;;;2DAGuD;OACrD,kBAAkB,CAAC;IACvB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;+BAE+B;AAC/B,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,WAAW,CAAC;IACpB,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,cAAc,CAAC;IACtB,IAAI,EAAE,aAAa,CAAC;IACpB,qCAAqC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;OAWG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IACrE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAC7C,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAC7C;;;yDAGqD;IACrD,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACzD;;;;;;;yEAOqE;IACrE,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C;;;;;gFAK4E;IAC5E,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC;;;qEAGiE;IACjE,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C;;;;;;;;;kDAS8C;IAC9C,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACvD;;;;;;;;;gFAS4E;IAC5E,aAAa,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC7C;AAED,MAAM,WAAW,eAAe;IAC9B,4EAA4E;IAC5E,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;CAC3C;AAED,YAAY,EAAE,SAAS,EAAE,CAAC"}
|
package/dist/webrtc/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MeetViewer, type MeetViewerOptions } from "./meet-viewer.js";
|
|
2
2
|
import { type PeerStreamListener, type PeerStreamRegistry } from "./peer-stream-registry.js";
|
|
3
3
|
export { MeetViewer, type MeetViewerOptions, type MeetViewerDeps, type PeerInfo, type RemoteTrackEvent, } from "./meet-viewer.js";
|
|
4
|
-
export { createPeerStreamRegistry, type PeerStreamRegistry, type PeerStreamListener, } from "./peer-stream-registry.js";
|
|
4
|
+
export { createPeerStreamRegistry, type PeerStreamRegistry, type PeerStreamListener, type RosterListener, } from "./peer-stream-registry.js";
|
|
5
5
|
export interface PeerViewer {
|
|
6
6
|
/** Join the room(s) (viewer role, no capture). */
|
|
7
7
|
join: () => Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/webrtc/index.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAyB,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACxB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,wBAAwB,EACxB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/webrtc/index.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAyB,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACxB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,wBAAwB,EACxB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,cAAc,GACpB,MAAM,2BAA2B,CAAC;AAEnC,MAAM,WAAW,UAAU;IACzB,kDAAkD;IAClD,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,8EAA8E;IAC9E,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,yEAAyE;IACzE,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,WAAW,GAAG,IAAI,CAAC;IAC7D,8EAA8E;IAC9E,mBAAmB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,KAAK,MAAM,IAAI,CAAC;IACrF,mDAAmD;IACnD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;gDAC4C;IAC5C,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED;;;;mFAImF;AACnF;;;;;;;8BAO8B;AAC9B,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAK1C;AA+BD;8EAC8E;AAC9E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,UAAU,CAevE;AAED,8DAA8D;AAC9D,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,GAAG;IAC1D,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB;oFACgF;IAChF,IAAI,CAAC,EAAE,iBAAiB,CAAC,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,mBAAoB,SAAQ,UAAU;IACrD;;oEAEgE;IAChE,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACnD;AAED;;;;;;;;;;2EAU2E;AAC3E,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,0BAA0B,GAClC,mBAAmB,CA8ErB;AAED;;oEAEoE;AACpE,MAAM,MAAM,mBAAmB,GAC3B,0BAA0B,GAC1B,CAAC,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE1D;;oCAEoC;AACpC,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,mBAAmB,GAAG,mBAAmB,CAYjG"}
|
package/dist/webrtc/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webrtc/index.ts"],"names":[],"mappings":"AAAA,2DAA2D;AAC3D,EAAE;AACF,mEAAmE;AACnE,mFAAmF;AACnF,8EAA8E;AAC9E,kEAAkE;AAClE,+EAA+E;AAC/E,EAAE;AACF,iFAAiF;AACjF,iFAAiF;AACjF,6EAA6E;AAE7E,OAAO,EAAE,UAAU,EAAiD,MAAM,kBAAkB,CAAC;AAC7F,OAAO,EACL,wBAAwB,GAGzB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,UAAU,GAKX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,wBAAwB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/webrtc/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
export type PeerStreamListener = (stream: MediaStream | null) => void;
|
|
2
|
+
/** Notified whenever the live roster changes ORDER (a peer connects for the first
|
|
3
|
+
* time, or a connected peer drops). A pure re-`set` of an already-present label's
|
|
4
|
+
* stream is NOT a roster change (same arrival order) — it reaches per-label
|
|
5
|
+
* `subscribe` listeners only. Carries no payload : the listener re-reads
|
|
6
|
+
* `orderedLabels()`. */
|
|
7
|
+
export type RosterListener = () => void;
|
|
2
8
|
export interface PeerStreamRegistry {
|
|
3
9
|
/** #4 contract — the current stream for a label, or `null` if the peer is not
|
|
4
10
|
* connected (yet / any more). Synchronous, side-effect free. */
|
|
5
11
|
resolve(peerLabel: string): MediaStream | null;
|
|
12
|
+
/** The live `peer_label`s in ARRIVAL ORDER (insertion order of the backing Map,
|
|
13
|
+
* restricted to labels that currently hold a stream). Drives positional slot
|
|
14
|
+
* resolution (`@<n>` → `orderedLabels()[n]`, ADR Blue 009 axe 1 positional
|
|
15
|
+
* variant). Returns a fresh array — safe for the caller to keep / index. */
|
|
16
|
+
orderedLabels(): string[];
|
|
6
17
|
/** Push channel for the LIVE `media` primitive : invoked immediately with the
|
|
7
18
|
* current value, then on every change for `peerLabel`. Returns an
|
|
8
19
|
* unsubscribe. */
|
|
9
20
|
subscribe(peerLabel: string, listener: PeerStreamListener): () => void;
|
|
21
|
+
/** Roster-change channel : invoked whenever a peer connects (new label) or
|
|
22
|
+
* leaves (label dropped) — i.e. whenever `orderedLabels()` could shift. Lets a
|
|
23
|
+
* positional consumer re-resolve `@<n>` when arrivals/departures shuffle the
|
|
24
|
+
* order. NOT invoked on a pure stream replacement of an existing label. Returns
|
|
25
|
+
* an unsubscribe. */
|
|
26
|
+
subscribeRoster(listener: RosterListener): () => void;
|
|
10
27
|
/** Viewer-side : publish / replace a peer's stream (peer connected). */
|
|
11
28
|
set(peerLabel: string, stream: MediaStream): void;
|
|
12
29
|
/** Viewer-side : drop a peer's stream (peer left / connection failed). The
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"peer-stream-registry.d.ts","sourceRoot":"","sources":["../../src/webrtc/peer-stream-registry.ts"],"names":[],"mappings":"AAuBA,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;AAEtE,MAAM,WAAW,kBAAkB;IACjC;qEACiE;IACjE,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;IAC/C;;uBAEmB;IACnB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAAC;IACvE,wEAAwE;IACxE,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAClD;;gDAE4C;IAC5C,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;iBACa;IACb,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,wBAAwB,IAAI,kBAAkB,
|
|
1
|
+
{"version":3,"file":"peer-stream-registry.d.ts","sourceRoot":"","sources":["../../src/webrtc/peer-stream-registry.ts"],"names":[],"mappings":"AAuBA,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;AAEtE;;;;yBAIyB;AACzB,MAAM,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC;AAExC,MAAM,WAAW,kBAAkB;IACjC;qEACiE;IACjE,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAAC;IAC/C;;;iFAG6E;IAC7E,aAAa,IAAI,MAAM,EAAE,CAAC;IAC1B;;uBAEmB;IACnB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAAC;IACvE;;;;0BAIsB;IACtB,eAAe,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,IAAI,CAAC;IACtD,wEAAwE;IACxE,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAClD;;gDAE4C;IAC5C,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC;iBACa;IACb,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,wBAAwB,IAAI,kBAAkB,CAoE7D"}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
export function createPeerStreamRegistry() {
|
|
24
24
|
const streams = new Map();
|
|
25
25
|
const listeners = new Map();
|
|
26
|
+
const rosterListeners = new Set();
|
|
26
27
|
function notify(peerLabel) {
|
|
27
28
|
const set = listeners.get(peerLabel);
|
|
28
29
|
if (set === undefined)
|
|
@@ -31,10 +32,25 @@ export function createPeerStreamRegistry() {
|
|
|
31
32
|
for (const listener of set)
|
|
32
33
|
listener(value);
|
|
33
34
|
}
|
|
35
|
+
function notifyRoster() {
|
|
36
|
+
for (const listener of [...rosterListeners])
|
|
37
|
+
listener();
|
|
38
|
+
}
|
|
34
39
|
return {
|
|
35
40
|
resolve(peerLabel) {
|
|
36
41
|
return streams.get(peerLabel) ?? null;
|
|
37
42
|
},
|
|
43
|
+
orderedLabels() {
|
|
44
|
+
// Map preserves insertion order = arrival order ; every key holds a stream
|
|
45
|
+
// (a dropped peer is `delete`d in remove()).
|
|
46
|
+
return [...streams.keys()];
|
|
47
|
+
},
|
|
48
|
+
subscribeRoster(listener) {
|
|
49
|
+
rosterListeners.add(listener);
|
|
50
|
+
return () => {
|
|
51
|
+
rosterListeners.delete(listener);
|
|
52
|
+
};
|
|
53
|
+
},
|
|
38
54
|
subscribe(peerLabel, listener) {
|
|
39
55
|
let set = listeners.get(peerLabel);
|
|
40
56
|
if (set === undefined) {
|
|
@@ -57,20 +73,26 @@ export function createPeerStreamRegistry() {
|
|
|
57
73
|
set(peerLabel, stream) {
|
|
58
74
|
if (streams.get(peerLabel) === stream)
|
|
59
75
|
return; // idempotent re-emit guard
|
|
76
|
+
const isNew = !streams.has(peerLabel); // a brand-new arrival shifts the roster
|
|
60
77
|
streams.set(peerLabel, stream);
|
|
61
78
|
notify(peerLabel);
|
|
79
|
+
if (isNew)
|
|
80
|
+
notifyRoster();
|
|
62
81
|
},
|
|
63
82
|
remove(peerLabel) {
|
|
64
83
|
if (!streams.has(peerLabel))
|
|
65
84
|
return;
|
|
66
85
|
streams.delete(peerLabel);
|
|
67
86
|
notify(peerLabel);
|
|
87
|
+
notifyRoster(); // a departure shifts every later position
|
|
68
88
|
},
|
|
69
89
|
clear() {
|
|
70
90
|
const labels = [...streams.keys()];
|
|
71
91
|
streams.clear();
|
|
72
92
|
for (const label of labels)
|
|
73
93
|
notify(label);
|
|
94
|
+
if (labels.length > 0)
|
|
95
|
+
notifyRoster();
|
|
74
96
|
},
|
|
75
97
|
};
|
|
76
98
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"peer-stream-registry.js","sourceRoot":"","sources":["../../src/webrtc/peer-stream-registry.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,sCAAsC;AACtC,EAAE;AACF,4EAA4E;AAC5E,6EAA6E;AAC7E,8EAA8E;AAC9E,uEAAuE;AACvE,EAAE;AACF,gFAAgF;AAChF,4EAA4E;AAC5E,wEAAwE;AACxE,oEAAoE;AACpE,2EAA2E;AAC3E,2EAA2E;AAC3E,+EAA+E;AAC/E,EAAE;AACF,2EAA2E;AAC3E,2EAA2E;AAC3E,8EAA8E;AAC9E,+EAA+E;AAC/E,+EAA+E;AAC/E,4DAA4D;
|
|
1
|
+
{"version":3,"file":"peer-stream-registry.js","sourceRoot":"","sources":["../../src/webrtc/peer-stream-registry.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumencast/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Browser runtime for Lumencast — mount(), LSDP/1 transport, leaf-grain store, LSML render, animations, overlays.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"framer-motion": "^12.0.0",
|
|
37
37
|
"react": "^19.0.0",
|
|
38
38
|
"react-dom": "^19.0.0",
|
|
39
|
-
"@lumencast/protocol": "0.
|
|
39
|
+
"@lumencast/protocol": "0.12.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@playwright/test": "^1.49.1",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"vite-plugin-dts": "^4.5.0",
|
|
51
51
|
"vitest": "^4.1.5",
|
|
52
52
|
"ws": "^8.18.0",
|
|
53
|
-
"@lumencast/server": "0.
|
|
54
|
-
"@lumencast/dev-server": "0.
|
|
53
|
+
"@lumencast/server": "0.12.1",
|
|
54
|
+
"@lumencast/dev-server": "0.12.1"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"dev": "vite",
|
package/src/index.ts
CHANGED
package/src/mount.ts
CHANGED
|
@@ -46,6 +46,11 @@ export function mount(options: MountOptions): LumencastHandle {
|
|
|
46
46
|
|
|
47
47
|
let active = true;
|
|
48
48
|
|
|
49
|
+
// Render-bundle versions already warmed (or warming) by the preload path.
|
|
50
|
+
// Keeps both roster sources (the `scene_roster` frame and the `preloadRoster`
|
|
51
|
+
// mount option) idempotent — a version is fetched by the warmer at most once.
|
|
52
|
+
const warmedVersions = new Set<string>();
|
|
53
|
+
|
|
49
54
|
// ADR Blue 009 §3.2–3.3 — surface the reserved `__cam.*` LSDP leaves (the
|
|
50
55
|
// slot→peer assignments + the receive-only viewer creds) to the host so its
|
|
51
56
|
// WebRTC viewer (Solar) can drive room joins + `x-zab.meet-peer` slot re-keying.
|
|
@@ -107,6 +112,13 @@ export function mount(options: MountOptions): LumencastHandle {
|
|
|
107
112
|
to: frame.scene_version,
|
|
108
113
|
});
|
|
109
114
|
},
|
|
115
|
+
onSceneRoster: (frame) => {
|
|
116
|
+
if (!active) return;
|
|
117
|
+
// The server advertised the show roster (LSDP/1.1 `scene_roster`).
|
|
118
|
+
// Warm every scene's render bundle in the background so the first switch
|
|
119
|
+
// to each is a cache hit, not a blocking fetch.
|
|
120
|
+
warmRoster(frame.entries, "frame");
|
|
121
|
+
},
|
|
110
122
|
onServerError: (frame) => {
|
|
111
123
|
reportError({
|
|
112
124
|
code: frame.code,
|
|
@@ -121,6 +133,12 @@ export function mount(options: MountOptions): LumencastHandle {
|
|
|
121
133
|
|
|
122
134
|
ws.start();
|
|
123
135
|
|
|
136
|
+
// Public preload surface (#87b) — warm a host-supplied roster right after
|
|
137
|
+
// mount, before any switch. Same warmer + cache as the `scene_roster` frame.
|
|
138
|
+
if (options.preloadRoster !== undefined && options.preloadRoster.length > 0) {
|
|
139
|
+
warmRoster(options.preloadRoster, "option");
|
|
140
|
+
}
|
|
141
|
+
|
|
124
142
|
const root: Root = createRoot(options.target);
|
|
125
143
|
root.render(
|
|
126
144
|
createElement(LumencastApp, {
|
|
@@ -165,6 +183,42 @@ export function mount(options: MountOptions): LumencastHandle {
|
|
|
165
183
|
|
|
166
184
|
// --- helpers ----------------------------------------------------------
|
|
167
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Warm the render bundles for a set of roster entries in the background.
|
|
188
|
+
* Best-effort and non-blocking: each `get()` populates the fetcher's cache
|
|
189
|
+
* keyed by `scene_version`, so the eventual `onSnapshot` fetch for that scene
|
|
190
|
+
* is an instant cache hit. Idempotent via `warmedVersions`; the already-active
|
|
191
|
+
* scene is skipped (its bundle is already loaded or in flight). A failed warm
|
|
192
|
+
* is swallowed and its version released so a later roster can retry — the
|
|
193
|
+
* scene still fetches on demand at switch time.
|
|
194
|
+
*/
|
|
195
|
+
function warmRoster(
|
|
196
|
+
entries: readonly { scene_id: string; scene_version: string }[],
|
|
197
|
+
source: "frame" | "option",
|
|
198
|
+
): void {
|
|
199
|
+
const activeVersion = bundleSignal.value?.scene_version;
|
|
200
|
+
for (const { scene_id, scene_version } of entries) {
|
|
201
|
+
if (scene_version === activeVersion) continue;
|
|
202
|
+
if (warmedVersions.has(scene_version)) continue;
|
|
203
|
+
warmedVersions.add(scene_version);
|
|
204
|
+
void bundleFetcher
|
|
205
|
+
.get(scene_id, scene_version)
|
|
206
|
+
.then(() => {
|
|
207
|
+
if (!active) return;
|
|
208
|
+
options.onMetric?.({
|
|
209
|
+
name: "roster_preloaded",
|
|
210
|
+
scene_id,
|
|
211
|
+
scene_version,
|
|
212
|
+
source,
|
|
213
|
+
});
|
|
214
|
+
})
|
|
215
|
+
.catch(() => {
|
|
216
|
+
// Release so a subsequent roster advertisement can retry the warm.
|
|
217
|
+
warmedVersions.delete(scene_version);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
168
222
|
async function onSnapshot(
|
|
169
223
|
fetcher: BundleFetcher,
|
|
170
224
|
bSignal: typeof bundleSignal,
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Per-device stream cache for `x-zab.capture` ACQUIRE mode.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists: a scene switch remounts the WHOLE render tree
|
|
4
|
+
// (`AnimatePresence` keyed on the scene id in app.tsx / crossfade.tsx), so every
|
|
5
|
+
// `Capture` node unmounts and a fresh one mounts. Without sharing, each switch
|
|
6
|
+
// stops the old node's tracks (RC11) and re-acquires the same physical device in
|
|
7
|
+
// the new node. A real USB webcam reopens near-instantly, but a synthetic
|
|
8
|
+
// DirectShow filter (OBS Virtual Camera) renegotiates slowly → a visible blink
|
|
9
|
+
// on every switch, because — by design (ADR 007 Prism) — every scene references
|
|
10
|
+
// the SAME shared vcam device.
|
|
11
|
+
//
|
|
12
|
+
// The fix mirrors the editor's `use-live-source.ts`: a ref-counted cache keyed by
|
|
13
|
+
// the resolved PHYSICAL device. Two `Capture` nodes on the same device share one
|
|
14
|
+
// `getUserMedia`; tracks stop (RC11) only when the LAST consumer releases. During
|
|
15
|
+
// a crossfade both the exiting and entering scenes are mounted at once
|
|
16
|
+
// (`AnimatePresence mode="sync"`), so the ref-count never reaches 0 across a
|
|
17
|
+
// switch and the device is never renegotiated — no blink.
|
|
18
|
+
//
|
|
19
|
+
// Cache key stability: the key is the resolved `deviceId` / `captureSourceId`, not
|
|
20
|
+
// the raw logical `deviceRef`. Physical ids are salted per origin/partition (see
|
|
21
|
+
// capture.tsx §A1.3), but the salt is constant WITHIN one runtime origin, so the
|
|
22
|
+
// resolved id is stable across scene switches inside a single mount/session — the
|
|
23
|
+
// exact scope over which sharing must hold. Keying on the physical id also
|
|
24
|
+
// correctly de-dupes two distinct `deviceRef`s that resolve to the same device.
|
|
25
|
+
|
|
26
|
+
import type { ResolveCaptureDevice } from "./capture";
|
|
27
|
+
|
|
28
|
+
interface CacheEntry {
|
|
29
|
+
promise: Promise<MediaStream>;
|
|
30
|
+
stream: MediaStream | null;
|
|
31
|
+
refs: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cache = new Map<string, CacheEntry>();
|
|
35
|
+
|
|
36
|
+
/** Outcome of a claim: either a shared stream promise (with the key to release
|
|
37
|
+
* later) or PLACEHOLDER — the unknown-kind / declared-but-unresolved-ref path
|
|
38
|
+
* that acquires nothing, exactly as the old inline `acquireStream` returning
|
|
39
|
+
* `null` did. A PLACEHOLDER claim holds NO ref (nothing to release). */
|
|
40
|
+
export type CaptureClaim =
|
|
41
|
+
| { kind: "stream"; key: string; promise: Promise<MediaStream> }
|
|
42
|
+
| { kind: "placeholder" };
|
|
43
|
+
|
|
44
|
+
/** Resolve the device, then claim a shared stream for it (incrementing the
|
|
45
|
+
* ref-count), or return PLACEHOLDER. Awaits the resolver first — physical ids
|
|
46
|
+
* are salted per origin/partition, so the host may re-resolve a portable key
|
|
47
|
+
* (label) against THIS context (capture.tsx §A1.3). A throw from the underlying
|
|
48
|
+
* `getUserMedia` surfaces via the returned promise (caller → PLACEHOLDER). */
|
|
49
|
+
export async function claimCaptureStream(
|
|
50
|
+
sourceKind: string,
|
|
51
|
+
deviceRef: string,
|
|
52
|
+
resolveCaptureDevice: ResolveCaptureDevice | undefined,
|
|
53
|
+
): Promise<CaptureClaim> {
|
|
54
|
+
const md = navigator.mediaDevices;
|
|
55
|
+
const resolved = (await resolveCaptureDevice?.(deviceRef, sourceKind)) ?? null;
|
|
56
|
+
const deviceId = resolved?.deviceId;
|
|
57
|
+
const captureSourceId = resolved?.captureSourceId;
|
|
58
|
+
const declaredRef = deviceRef.length > 0;
|
|
59
|
+
|
|
60
|
+
// Compute a stable physical key + a lazy acquisition thunk, or bail to
|
|
61
|
+
// PLACEHOLDER for the same reasons the inline switch used to return `null`.
|
|
62
|
+
let physicalId: string;
|
|
63
|
+
let acquire: () => Promise<MediaStream>;
|
|
64
|
+
switch (sourceKind) {
|
|
65
|
+
case "media.webcam":
|
|
66
|
+
case "media.mic":
|
|
67
|
+
case "media.app_audio": {
|
|
68
|
+
// §A1.3 (amended) — NO default-device fallback for a DECLARED deviceRef
|
|
69
|
+
// that did not resolve. Acquiring the host default here is the silent
|
|
70
|
+
// "automatic allocation" of the WRONG camera. → PLACEHOLDER. The bare
|
|
71
|
+
// default constraint stays ONLY when no deviceRef is declared at all.
|
|
72
|
+
if (declaredRef && (typeof deviceId !== "string" || deviceId.length === 0)) {
|
|
73
|
+
return { kind: "placeholder" };
|
|
74
|
+
}
|
|
75
|
+
const channel = sourceKind === "media.webcam" ? "video" : "audio";
|
|
76
|
+
physicalId = typeof deviceId === "string" && deviceId.length > 0 ? deviceId : "default";
|
|
77
|
+
acquire = () => md.getUserMedia({ [channel]: deviceConstraint(deviceId) });
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case "media.screen":
|
|
81
|
+
case "media.window": {
|
|
82
|
+
// DIRECT capture of the picked desktopCapturer surface (no system picker)
|
|
83
|
+
// via Electron's legacy `chromeMediaSource:desktop` + resolved id.
|
|
84
|
+
if (typeof captureSourceId === "string" && captureSourceId.length > 0) {
|
|
85
|
+
physicalId = captureSourceId;
|
|
86
|
+
acquire = () =>
|
|
87
|
+
md.getUserMedia({
|
|
88
|
+
video: {
|
|
89
|
+
mandatory: {
|
|
90
|
+
chromeMediaSource: "desktop",
|
|
91
|
+
chromeMediaSourceId: captureSourceId,
|
|
92
|
+
},
|
|
93
|
+
} as unknown as MediaTrackConstraints,
|
|
94
|
+
});
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
// A declared surface ref that didn't resolve → PLACEHOLDER, never a
|
|
98
|
+
// default picker. The picker stays only when no ref is declared.
|
|
99
|
+
if (declaredRef) return { kind: "placeholder" };
|
|
100
|
+
physicalId = "display";
|
|
101
|
+
acquire = () => md.getDisplayMedia({ video: true });
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
default:
|
|
105
|
+
return { kind: "placeholder" };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const key = `${sourceKind}:${physicalId}`;
|
|
109
|
+
const existing = cache.get(key);
|
|
110
|
+
if (existing) {
|
|
111
|
+
existing.refs += 1;
|
|
112
|
+
return { kind: "stream", key, promise: existing.promise };
|
|
113
|
+
}
|
|
114
|
+
const promise = acquire();
|
|
115
|
+
const entry: CacheEntry = { promise, stream: null, refs: 1 };
|
|
116
|
+
cache.set(key, entry);
|
|
117
|
+
promise
|
|
118
|
+
.then((s) => {
|
|
119
|
+
entry.stream = s;
|
|
120
|
+
})
|
|
121
|
+
.catch(() => {
|
|
122
|
+
// Acquisition failed — evict so a later mount retries instead of sharing a
|
|
123
|
+
// rejected promise. Consumers of this promise all fall back to PLACEHOLDER.
|
|
124
|
+
cache.delete(key);
|
|
125
|
+
});
|
|
126
|
+
return { kind: "stream", key, promise };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Drop one consumer's claim. Stops every track (RC11 — kill the device light)
|
|
130
|
+
* ONLY when the last consumer releases. A no-op for an already-evicted key
|
|
131
|
+
* (e.g. an acquisition that rejected and self-evicted). */
|
|
132
|
+
export function releaseCaptureStream(key: string): void {
|
|
133
|
+
const entry = cache.get(key);
|
|
134
|
+
if (entry === undefined) return;
|
|
135
|
+
entry.refs -= 1;
|
|
136
|
+
if (entry.refs > 0) return;
|
|
137
|
+
cache.delete(key);
|
|
138
|
+
if (entry.stream !== null) stopStream(entry.stream);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A `getUserMedia` track constraint. A resolved deviceId is pinned with
|
|
142
|
+
* `exact`, NOT a bare (ideal) deviceId: an *ideal* constraint SILENTLY falls
|
|
143
|
+
* back to the host default camera when the requested device can't start (e.g.
|
|
144
|
+
* an INACTIVE virtual cam enumerated but producing no stream). `exact` yields
|
|
145
|
+
* the requested device (its placeholder frame if idle), or an
|
|
146
|
+
* OverconstrainedError the caller catches into PLACEHOLDER — never the wrong
|
|
147
|
+
* cam. No deviceId → `true` (host default) applies ONLY when no deviceRef was
|
|
148
|
+
* declared. */
|
|
149
|
+
function deviceConstraint(deviceId: string | undefined): MediaTrackConstraints | boolean {
|
|
150
|
+
return typeof deviceId === "string" && deviceId.length > 0
|
|
151
|
+
? { deviceId: { exact: deviceId } }
|
|
152
|
+
: true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Stop every track of a stream (RC11 — release the camera/mic, kill the light). */
|
|
156
|
+
function stopStream(stream: MediaStream): void {
|
|
157
|
+
for (const track of stream.getTracks()) track.stop();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Test-only: drop all cached entries WITHOUT stopping tracks. Lets a test file
|
|
161
|
+
* start from a clean ref-count without leaking state between cases. */
|
|
162
|
+
export function __resetCaptureStreamCache(): void {
|
|
163
|
+
cache.clear();
|
|
164
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import type { PrimitiveProps } from "./index";
|
|
3
3
|
import { useOptionalLumencastRuntime } from "../../overlay/runtime-context";
|
|
4
|
+
import { claimCaptureStream, releaseCaptureStream } from "./capture-stream-cache";
|
|
4
5
|
|
|
5
6
|
/** `x-zab.capture` — context-aware capture primitive (Zab vendor primitive,
|
|
6
7
|
* RFC-0001 / ADR 004 §Amendment 1).
|
|
@@ -62,32 +63,38 @@ export function Capture({ resolved }: PrimitiveProps) {
|
|
|
62
63
|
if (!isCaptureCapable()) return;
|
|
63
64
|
|
|
64
65
|
let cancelled = false;
|
|
65
|
-
|
|
66
|
+
// The cache key of the claim we hold, so cleanup releases exactly one ref.
|
|
67
|
+
let claimedKey: string | null = null;
|
|
66
68
|
|
|
67
69
|
void (async () => {
|
|
68
70
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
// Claim a per-device shared stream (ref-counted): a scene switch that
|
|
72
|
+
// remounts a Capture on the SAME device reuses the live stream instead
|
|
73
|
+
// of stopping and re-acquiring it — the vcam-blink fix.
|
|
74
|
+
const claim = await claimCaptureStream(sourceKind, deviceRef, resolveCaptureDevice);
|
|
75
|
+
if (claim.kind === "placeholder") return; // unknown/unresolved → PLACEHOLDER
|
|
76
|
+
const media = await claim.promise;
|
|
71
77
|
if (cancelled) {
|
|
72
|
-
// Unmounted (or scene-changed) during acquisition —
|
|
73
|
-
//
|
|
74
|
-
|
|
78
|
+
// Unmounted (or scene-changed) during acquisition — drop our claim.
|
|
79
|
+
// The shared stream stays alive if another consumer still holds it,
|
|
80
|
+
// and stops (RC11) only when the last one releases.
|
|
81
|
+
releaseCaptureStream(claim.key);
|
|
75
82
|
return;
|
|
76
83
|
}
|
|
77
|
-
|
|
84
|
+
claimedKey = claim.key;
|
|
78
85
|
setStream(media);
|
|
79
86
|
} catch {
|
|
80
87
|
// §A1.2(2)(a) — any acquisition failure (permission denied, no device,
|
|
81
88
|
// getUserMedia rejected) falls back to PLACEHOLDER, no throw, no
|
|
82
|
-
// diagnostic.
|
|
89
|
+
// diagnostic. The rejected cache entry self-evicts; our ref went with it.
|
|
83
90
|
}
|
|
84
91
|
})();
|
|
85
92
|
|
|
86
93
|
return () => {
|
|
87
94
|
cancelled = true;
|
|
88
|
-
// RC11 —
|
|
89
|
-
//
|
|
90
|
-
if (
|
|
95
|
+
// RC11 — release our claim at unmount / scene change. Tracks stop only
|
|
96
|
+
// when this was the last consumer of the shared stream.
|
|
97
|
+
if (claimedKey !== null) releaseCaptureStream(claimedKey);
|
|
91
98
|
};
|
|
92
99
|
// Re-acquire when the logical source identity changes (a scene switch can
|
|
93
100
|
// reuse the node with a new sourceKind/deviceRef).
|
|
@@ -142,7 +149,7 @@ export type ResolvedCaptureDevice = {
|
|
|
142
149
|
* hash. MAY be async: physical ids (e.g. getUserMedia `deviceId`) are salted
|
|
143
150
|
* per origin/partition, so the host often must re-resolve a portable key
|
|
144
151
|
* (label) against THIS context's devices — an inherently asynchronous step
|
|
145
|
-
* (`enumerateDevices`). `
|
|
152
|
+
* (`enumerateDevices`). `claimCaptureStream` awaits it, so the device is bound
|
|
146
153
|
* before acquisition rather than racing a late global mutation. */
|
|
147
154
|
export type ResolveCaptureDevice = (
|
|
148
155
|
deviceRef: string,
|
|
@@ -165,87 +172,6 @@ function isVisualKind(sourceKind: string): boolean {
|
|
|
165
172
|
);
|
|
166
173
|
}
|
|
167
174
|
|
|
168
|
-
/** Acquire a live stream for `sourceKind`, applying a host-resolved `deviceId`
|
|
169
|
-
* when available. Returns `null` for an unsupported/unknown kind (→
|
|
170
|
-
* PLACEHOLDER) ; throws are caught by the caller (→ PLACEHOLDER fallback). */
|
|
171
|
-
async function acquireStream(
|
|
172
|
-
sourceKind: string,
|
|
173
|
-
deviceRef: string,
|
|
174
|
-
resolveCaptureDevice: ResolveCaptureDevice | undefined,
|
|
175
|
-
): Promise<MediaStream | null> {
|
|
176
|
-
const md = navigator.mediaDevices;
|
|
177
|
-
|
|
178
|
-
// §A1.3 (amended 2026-06-27) — AWAIT the resolver: physical ids are salted
|
|
179
|
-
// per origin/partition, so the host may need an async re-resolution by a
|
|
180
|
-
// portable key (label) in THIS context. Awaiting binds the device before
|
|
181
|
-
// acquisition instead of racing a late global update (the previous sync call
|
|
182
|
-
// let the node acquire with a stale/absent id first).
|
|
183
|
-
const resolved = (await resolveCaptureDevice?.(deviceRef, sourceKind)) ?? null;
|
|
184
|
-
const deviceId = resolved?.deviceId;
|
|
185
|
-
const declaredRef = deviceRef.length > 0;
|
|
186
|
-
|
|
187
|
-
switch (sourceKind) {
|
|
188
|
-
case "media.webcam":
|
|
189
|
-
case "media.mic":
|
|
190
|
-
case "media.app_audio": {
|
|
191
|
-
// §A1.3 (amended) — NO default-device fallback for a DECLARED deviceRef
|
|
192
|
-
// that did not resolve to a real device. Acquiring the host default cam
|
|
193
|
-
// here is the silent "automatic allocation" of the WRONG camera the
|
|
194
|
-
// consuming app must never get. → PLACEHOLDER (return null). The bare
|
|
195
|
-
// default constraint stays ONLY when no deviceRef is declared at all.
|
|
196
|
-
if (declaredRef && (typeof deviceId !== "string" || deviceId.length === 0)) {
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
const channel = sourceKind === "media.webcam" ? "video" : "audio";
|
|
200
|
-
return md.getUserMedia({ [channel]: deviceConstraint(deviceId) });
|
|
201
|
-
}
|
|
202
|
-
case "media.screen":
|
|
203
|
-
case "media.window": {
|
|
204
|
-
// DIRECT capture of the picked desktopCapturer surface (no system picker)
|
|
205
|
-
// via Electron's legacy `chromeMediaSource:desktop` + the resolved
|
|
206
|
-
// `captureSourceId`.
|
|
207
|
-
const captureSourceId = resolved?.captureSourceId;
|
|
208
|
-
if (typeof captureSourceId === "string" && captureSourceId.length > 0) {
|
|
209
|
-
return md.getUserMedia({
|
|
210
|
-
video: {
|
|
211
|
-
mandatory: {
|
|
212
|
-
chromeMediaSource: "desktop",
|
|
213
|
-
chromeMediaSourceId: captureSourceId,
|
|
214
|
-
},
|
|
215
|
-
} as unknown as MediaTrackConstraints,
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
// A declared surface ref that didn't resolve → PLACEHOLDER, never a
|
|
219
|
-
// default `getDisplayMedia` picker. The picker stays only when no ref is
|
|
220
|
-
// declared (a bare capture node on a non-Electron host).
|
|
221
|
-
if (declaredRef) return null;
|
|
222
|
-
return md.getDisplayMedia({ video: true });
|
|
223
|
-
}
|
|
224
|
-
default:
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/** A `getUserMedia` track constraint. A resolved deviceId is pinned with
|
|
230
|
-
* `exact`, NOT a bare (ideal) deviceId: an *ideal* constraint SILENTLY falls
|
|
231
|
-
* back to the host default camera when the requested device can't start (e.g.
|
|
232
|
-
* an INACTIVE virtual cam that's enumerated but produces no stream) — the
|
|
233
|
-
* "automatic allocation" of the WRONG camera. `exact` yields the requested
|
|
234
|
-
* device (its placeholder frame if idle), or an OverconstrainedError the
|
|
235
|
-
* caller catches into PLACEHOLDER — never the wrong cam. No deviceId → `true`
|
|
236
|
-
* (host default) applies ONLY when no deviceRef was declared. */
|
|
237
|
-
function deviceConstraint(deviceId: string | undefined): MediaTrackConstraints | boolean {
|
|
238
|
-
return typeof deviceId === "string" && deviceId.length > 0
|
|
239
|
-
? { deviceId: { exact: deviceId } }
|
|
240
|
-
: true;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** Stop every track of a stream (RC11 — release the camera/mic, kill the
|
|
244
|
-
* device light). */
|
|
245
|
-
function stopStream(stream: MediaStream): void {
|
|
246
|
-
for (const track of stream.getTracks()) track.stop();
|
|
247
|
-
}
|
|
248
|
-
|
|
249
175
|
/** A render dimension: a finite number → px, a non-empty string → verbatim,
|
|
250
176
|
* anything else → the fallback (matches the `image` primitive's helper). */
|
|
251
177
|
function dimOr(v: unknown, fallback: string): string {
|