@displayxr/inline3d 1.29.0 → 1.30.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/camera.d.ts ADDED
@@ -0,0 +1,257 @@
1
+ // Type definitions for @displayxr/inline3d/camera.
2
+ // PREVIEW tier — not covered by the 1.x semver promise. See docs/sdk-stability.md.
3
+ //
4
+ // RFC 0003 §4 (phase C2): the stereo camera as its own primitive. `openCamera` owns device
5
+ // selection and calibration, `addCameraView` is the correctly mirrored 3D self view (each half
6
+ // mirrored AND the halves swapped), `capturePhoto` / `record` produce plain side-by-side files
7
+ // with the layout in the name (`_2x1`) and the stereo record inside the file (XMP in a JPEG, a
8
+ // Tags element in a WebM — Decision 9). `/call` builds on it: `CallOptions.camera` takes a
9
+ // {@link StereoCamera}, and the call's self view is an `addCameraView`.
10
+ //
11
+ // The public surface of this file is pinned by test/api-snapshot: a change here fails `npm test`
12
+ // until the snapshot is updated in the same change (`node tools/api-surface.mjs --update`).
13
+
14
+ import type { Inline3D, Inline3DUnsupported, TileHandle } from './index.js';
15
+
16
+ /** What the camera delivers: a side-by-side pair (left eye left) or one flat picture. */
17
+ export type CameraFormat = 'sbs' | 'mono';
18
+
19
+ /**
20
+ * Which camera to open. `'auto'` (default): a stereo device when one is present — the DisplayXR
21
+ * Browser's "3D Camera" (recognised by its `displayxrStereo` track settings, never by its label),
22
+ * or a device delivering wider than 2.5:1 frames — else the default webcam. `'stereo'` prefers
23
+ * the pair and falls back to mono; `'mono'` never probes; a string is a `deviceId`; a
24
+ * `MediaStream` is used as given, with {@link CameraOptions.format} declaring what it is.
25
+ */
26
+ export type CameraPreference = 'auto' | 'stereo' | 'mono' | (string & {}) | MediaStream;
27
+
28
+ /** What is known about a stereo pair: from the device's own description, or from `calibration`. */
29
+ export interface StereoInfo {
30
+ /** True only for a calibrated, rectified pair (the runtime's 3D camera says so; a raw USB pair is false). */
31
+ readonly rectified: boolean;
32
+ /** Camera baseline, mm, or null when unknown. */
33
+ readonly baselineMm: number | null;
34
+ /** Horizontal field of view of ONE eye, degrees, or null when unknown. */
35
+ readonly horizontalFovDeg: number | null;
36
+ }
37
+
38
+ /** `baselineMm` / `horizontalFovDeg` / `rectified` the page knows about a stream the device does not describe. */
39
+ export interface CameraCalibration {
40
+ baselineMm?: number;
41
+ horizontalFovDeg?: number;
42
+ rectified?: boolean;
43
+ }
44
+
45
+ export interface CameraOptions {
46
+ /** See {@link CameraPreference}. Default `'auto'`. */
47
+ prefer?: CameraPreference;
48
+ /** The format of a page-supplied `MediaStream`. Default `'mono'`: 3D-ness is never guessed from a stream. */
49
+ format?: CameraFormat;
50
+ /** Fills {@link StereoCamera.stereo} where the device gives nothing; a page value wins over the device's. */
51
+ calibration?: CameraCalibration;
52
+ /**
53
+ * A calibrated rectification step for a raw stereo stream (needs the camera's intrinsics /
54
+ * extrinsics, from a plug-in or the runtime). Returns a rectified SBS stream; `stereo.rectified`
55
+ * is then true. A throwing hook keeps the raw pair (`cam.rectifyError`).
56
+ */
57
+ rectify?: (stream: MediaStream, info: { width: number; height: number; deviceId: string | null; label: string }) => MediaStream | Promise<MediaStream>;
58
+ /** Verbose console logging. */
59
+ debug?: boolean;
60
+ }
61
+
62
+ /** Why {@link openCamera} rejected (`error.code`). `skipped` lists every device tried and why it was not used. */
63
+ export type CameraErrorCode = 'camera-busy' | 'permission-denied' | 'no-camera';
64
+ export interface CameraError extends Error {
65
+ code: CameraErrorCode;
66
+ skipped: ReadonlyArray<{ label: string; error: string; busy?: boolean; denied?: boolean }>;
67
+ }
68
+
69
+ export interface CameraEvents {
70
+ /** The camera ended under you: revoked by the runtime, unplugged, or taken by another app. Not fired by `close()`. */
71
+ ended: { reason: string };
72
+ }
73
+
74
+ /** A photo from {@link StereoCamera.capturePhoto}: the RAW pair (never mirrored, never shifted). */
75
+ export interface CameraPhoto {
76
+ blob: Blob;
77
+ /** The blob's MIME type. */
78
+ type: string;
79
+ /** The whole frame's size (an SBS photo is twice its eye's width). */
80
+ width: number;
81
+ height: number;
82
+ layout: CameraFormat;
83
+ /**
84
+ * Horizontal disparity (left-eye x − right-eye x, source px) of the subject the camera converged
85
+ * on — the face — or null when none was measured. A viewer shifts the eyes toward each other by
86
+ * half of it each to put that subject at the display plane. Also written into a JPEG's XMP.
87
+ */
88
+ convergencePx: number | null;
89
+ stereo: StereoInfo | null;
90
+ /** `<base>_2x1.jpg` for a pair (`_2x1` = columns×rows, what `/player` reads), `<base>.jpg` for mono. */
91
+ suggestedName: string;
92
+ /** The XMP packet written into a JPEG (null for other types, which carry no metadata). */
93
+ xmp: string | null;
94
+ }
95
+
96
+ export interface CapturePhotoOptions {
97
+ /** `'image/jpeg'` (default; carries the XMP record) | `'image/png'` | `'image/webp'`. */
98
+ type?: string;
99
+ /** Encoder quality for lossy types. Default 0.92. */
100
+ quality?: number;
101
+ /** The file name's base (default `photo-<timestamp>`); the layout suffix and extension are added. */
102
+ name?: string;
103
+ /** Use this convergence instead of measuring the current frame. */
104
+ convergencePx?: number;
105
+ }
106
+
107
+ export interface RecordOptions {
108
+ /** Default: the first of `video/webm;codecs=vp9`, `…vp8`, `video/webm`, `video/mp4` this browser supports. */
109
+ mimeType?: string;
110
+ /** Also record a left-eye MONO copy for 2D platforms (Decision 13). Default false. */
111
+ mono?: boolean;
112
+ /** Audio to mix in: a `MediaStream` (its audio tracks) or one `MediaStreamTrack`. Default none. */
113
+ audio?: MediaStream | MediaStreamTrack | null;
114
+ videoBitsPerSecond?: number;
115
+ /** The file name's base (default `clip-<timestamp>`). */
116
+ name?: string;
117
+ }
118
+
119
+ /** A recording from {@link StereoCamera.record}; `stop()` resolves with the files. */
120
+ export interface CameraRecording {
121
+ readonly state: 'inactive' | 'recording' | 'paused';
122
+ readonly mimeType: string;
123
+ pause(): void;
124
+ resume(): void;
125
+ stop(): Promise<CameraClip>;
126
+ }
127
+
128
+ export interface CameraClip {
129
+ /** The side-by-side recording (a WebM carries the stereo record as `DXR_*` tags). */
130
+ blob: Blob;
131
+ type: string;
132
+ layout: CameraFormat;
133
+ durationMs: number;
134
+ /** The median of the convergence measured while recording (see {@link CameraPhoto.convergencePx}). */
135
+ convergencePx: number | null;
136
+ stereo: StereoInfo | null;
137
+ /** `<base>_2x1.webm` for a pair, `<base>.webm` for mono. */
138
+ suggestedName: string;
139
+ /** Whether the stereo record was written into the file (WebM only). */
140
+ tagged: boolean;
141
+ /** With `mono: true`: the left-eye copy and its name (`<base>.webm`). */
142
+ mono?: Blob;
143
+ monoSuggestedName?: string;
144
+ }
145
+
146
+ /** An open camera from {@link openCamera}. */
147
+ export interface StereoCamera {
148
+ readonly format: CameraFormat;
149
+ /** The pair's description, or null for a mono camera. */
150
+ readonly stereo: StereoInfo | null;
151
+ /** The MediaStream: hand it to WebRTC, a MediaRecorder, a canvas. Never mirrored. */
152
+ readonly stream: MediaStream;
153
+ /** The frame's size (an SBS frame is two eyes wide); {@link eyeWidth} is one eye's. */
154
+ readonly width: number;
155
+ readonly height: number;
156
+ readonly eyeWidth: number;
157
+ readonly deviceId: string | null;
158
+ readonly label: string;
159
+ /** True when this module opened the device (and `close()` stops it); false for a page-supplied stream. */
160
+ readonly owned: boolean;
161
+ /** Devices tried and not used (busy, denied, not a pair) — the clue a `'camera-busy'` report carries. */
162
+ readonly skipped: ReadonlyArray<{ label: string; error: string; busy?: boolean; denied?: boolean }>;
163
+ readonly state: 'live' | 'ended' | 'closed';
164
+ /** A detached, playing `<video>` on the stream (never in the DOM); null without a document. */
165
+ readonly video: HTMLVideoElement | null;
166
+ /** The error a throwing `rectify` hook produced (the raw pair is used). */
167
+ readonly rectifyError?: Error;
168
+ capturePhoto(opts?: CapturePhotoOptions): Promise<CameraPhoto>;
169
+ record(opts?: RecordOptions): CameraRecording;
170
+ /** One measurement of the face's disparity in the current frame (see {@link CameraPhoto.convergencePx}), or null. */
171
+ measureConvergence(): Promise<number | null>;
172
+ on<K extends keyof CameraEvents>(type: K, cb: (e: CameraEvents[K]) => void): () => void;
173
+ off<K extends keyof CameraEvents>(type: K, cb: (e: CameraEvents[K]) => void): void;
174
+ /** Stop the camera (only tracks this module opened), remove its views. Idempotent; fires no `ended`. */
175
+ close(): void;
176
+ }
177
+
178
+ /**
179
+ * Open the best camera. Never rejects for a busy optional device; rejects (a {@link CameraError})
180
+ * only when NO camera opens. `opts` may be just a {@link CameraPreference}.
181
+ */
182
+ export function openCamera(opts?: CameraOptions | CameraPreference): Promise<StereoCamera>;
183
+
184
+ /** Is `x` a {@link StereoCamera}? Duck-typed, so two copies of the SDK interoperate. */
185
+ export function isStereoCamera(x: unknown): x is StereoCamera;
186
+
187
+ export interface CameraViewOptions {
188
+ /** Selfie mirroring, done right for a pair (each half mirrored AND swapped). Default true. */
189
+ mirror?: boolean;
190
+ /** Measure the face's disparity ~5×/s and converge on it (the face at the display plane). Default false. */
191
+ autoConverge?: boolean;
192
+ /** [-1, 1], + = push back: an offset on top of the convergence. Default 0. */
193
+ depth?: number;
194
+ /** The tile's aspect (w/h) = the woven buffer's per-eye aspect. Default 16/9. */
195
+ aspect?: number;
196
+ }
197
+
198
+ /** A self view from {@link addCameraView}. */
199
+ export interface CameraView {
200
+ readonly canvas: HTMLCanvasElement;
201
+ /** `'woven-sbs'`: 3D on the panel; `'flat-left'`: a pair shown as its left eye; `'flat'`: mono. */
202
+ readonly route: 'woven-sbs' | 'flat-left' | 'flat' | null;
203
+ readonly woven: boolean;
204
+ /** The wall's tile handle while woven (its `firstWoven` settles when the tile is safe to reveal), else null. */
205
+ readonly handle: TileHandle | null;
206
+ readonly mirror: boolean;
207
+ readonly autoConverge: boolean;
208
+ readonly depth: number;
209
+ /** The per-eye convergence shift currently painted, source px. */
210
+ readonly convergencePx: number;
211
+ /** The last measured disparity of the face (source px), or null. */
212
+ readonly disparityPx: number | null;
213
+ setDepth(v: number | null): number;
214
+ /** No argument toggles. Returns the new state. */
215
+ setMirror(on?: boolean): boolean;
216
+ setAutoConverge(on?: boolean): boolean;
217
+ /** Paint one frame now (the view paints itself every animation frame; this is for tests and tools). */
218
+ paint(): void;
219
+ /** Stop painting and leave the wall. The camera stays open. */
220
+ remove(): void;
221
+ }
222
+
223
+ /**
224
+ * Put a camera on a canvas as a self view: on a woven wall a stereo camera is a 3D tile, mirrored
225
+ * correctly; anywhere else it is the left eye (or the mono frame), flat and mirrored. The
226
+ * canvas's CSS box is the shape the viewer sees; it must obey the woven-canvas rules.
227
+ */
228
+ export function addCameraView(wall: Inline3D | Inline3DUnsupported | null, canvas: HTMLCanvasElement, cam: StereoCamera, opts?: CameraViewOptions): CameraView;
229
+
230
+ /** `<base>_2x1.<ext>` for a pair, `<base>.<ext>` for mono — the name a capture suggests. Pure. */
231
+ export function suggestedName(base: string, layout: CameraFormat, type: string): string;
232
+
233
+ /** Stamped into every capture's metadata as `Software`. */
234
+ export const CAMERA_SDK: string;
235
+
236
+ /** The stereo record a capture carries (XMP `dxr:*` in a JPEG, `DXR_*` tags in a WebM). */
237
+ export interface StereoFileMeta {
238
+ layout: CameraFormat;
239
+ columns: number;
240
+ rows: number;
241
+ convergencePx: number | null;
242
+ baselineMm: number | null;
243
+ horizontalFovDeg: number | null;
244
+ rectified: boolean | null;
245
+ eyeWidth: number | null;
246
+ eyeHeight: number | null;
247
+ software: string | null;
248
+ }
249
+
250
+ /** Read the stereo record back from a JPEG's bytes (null when it carries none). */
251
+ export function readJpegStereoMeta(bytes: Uint8Array): StereoFileMeta | null;
252
+ /** Read the stereo record back from a WebM's bytes (null when it carries none). */
253
+ export function readWebmStereoMeta(bytes: Uint8Array): StereoFileMeta | null;
254
+ /** The raw XMP packet of a JPEG, or null. */
255
+ export function readJpegXmp(bytes: Uint8Array): string | null;
256
+ /** The stereo record in an XMP packet, or null. */
257
+ export function parseStereoXmp(xml: string): StereoFileMeta | null;