@mediaway/device 0.1.5 → 0.1.8

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 CHANGED
@@ -55,24 +55,53 @@ await mic.close();
55
55
 
56
56
  ## Screen capture
57
57
 
58
+ GPU-only, Zero-Copy — there is no CPU fallback in the wrapped Rust backend
59
+ (`adr/0003-gpu-handle-c-abi.md` §4), so `openScreenCapture()` needs a live GPU
60
+ device. It creates one internally by default (via the GPU device factory,
61
+ `mediaway-device` ADR-0007):
62
+
58
63
  ```ts
59
64
  import { openScreenCapture } from "@mediaway/device";
60
65
 
61
- const screen = await openScreenCapture({
62
- timeBase: { num: 1, den: 30 },
63
- captureCursor: true,
64
- });
65
- const frame = screen.pollFrame(); // VideoFrame | null
66
+ const screen = await openScreenCapture({ timeBase: { num: 1, den: 30 }, monitorIndex: 0 });
67
+ console.log(`screen: ${screen.width}x${screen.height}`);
68
+ const frame = screen.pollFrame(); // VideoFrame | null — data is ALWAYS empty (see below)
69
+ await screen.close(); // also closes the internally-created GPU device
70
+ ```
71
+
72
+ `pollFrame()`'s `VideoFrame.data` is always empty — it proves frames are
73
+ genuinely arriving (real `pts`/geometry) but does not copy pixels out (no CPU
74
+ readback path for Screen). For real pixel data, feed the session straight
75
+ into an encoder with `EncodeSession.writeFrameFromDesktopCapture()`
76
+ (`@mediaway/encoder`), Zero-Copy end to end — see that package's README.
77
+
78
+ ## GPU device factory
79
+
80
+ ```ts
81
+ import { listGpuAdapters, GpuDevice, openScreenCapture } from "@mediaway/device";
82
+
83
+ const adapters = listGpuAdapters(); // name, vendorId, deviceId, dedicatedVideoMemoryBytes, isHardware
84
+ const device = await GpuDevice.create({ adapterIndex: adapters[0].index });
85
+
86
+ // Share one device across capture and encode (see @mediaway/encoder):
87
+ const screen = await openScreenCapture({ timeBase: { num: 1, den: 30 }, gpuDevice: device });
88
+ // ... screen.close() leaves `device` open since you supplied it — close it yourself:
66
89
  await screen.close();
90
+ device.close();
67
91
  ```
68
92
 
93
+ Omit `adapterIndex` (or the whole `options` object) for the first hardware
94
+ adapter DXGI reports.
95
+
69
96
  ## API
70
97
 
71
98
  | Function | Session | Frames |
72
99
  | --- | --- | --- |
73
100
  | `openCamera({ index, timeBase })` | `CameraSession` (`width`, `height`, `pixelFormat` = `"nv12"`) | `pollFrame(): VideoFrame \| null` |
74
101
  | `openMicrophone({ sampleRate, channels })` | `MicSession` (`sampleRate`, `channels`) | `pollFrame(): AudioFrame \| null` |
75
- | `openScreenCapture({ timeBase, captureCursor? })` | `ScreenSession` | `pollFrame(): VideoFrame \| null` |
102
+ | `openScreenCapture({ timeBase, monitorIndex?, gpuDevice? })` | `ScreenSession` | `pollFrame(): VideoFrame \| null` (`data` always empty) |
103
+ | `listGpuAdapters()` | — | `GpuAdapterInfo[]` |
104
+ | `GpuDevice.create({ adapterIndex?, videoSupport?, debugLayer? })` | `GpuDevice` | — |
76
105
 
77
106
  Every session's `close()` is async (joins the backend worker thread).
78
107
  `CaptureUnavailableError` signals a missing device.
package/dist/index.d.ts CHANGED
@@ -1,16 +1,28 @@
1
1
  /**
2
- * @mediaway/device — device capability: camera / screen / microphone capture.
2
+ * @mediaway/device — device capability: camera / screen / microphone capture,
3
+ * plus the GPU device factory (mediaway-device ADR-0007).
3
4
  *
4
5
  * Implements the DX contract in bindings/nodejs/README.md over the
5
6
  * mediaway-ffi C ABI (via @mediaway/ffi). The ABI is domain-split
6
- * (`adr/0004-domain-feature-split.md`): this package folds it back into the
7
- * three DX functions. CPU-storage only: Camera delivers CPU frames; Screen
8
- * capture raises CaptureUnavailableError from the C ABI today (it needs a
9
- * live GPU device handle with no CPU fallback). Audio capture delivers raw
7
+ * (`adr/0004-domain-feature-split.md`): this package folds it back into DX
8
+ * functions. Camera delivers CPU frames; Screen capture is GPU-only, Zero-Copy
9
+ * — it needs a live GPU device handle with no CPU fallback
10
+ * (`adr/0003-gpu-handle-c-abi.md`), which `openScreenCapture()` now gets from
11
+ * `GpuDevice` (auto-created, or caller-supplied for explicit adapter control
12
+ * / sharing one device across capture + encode). Audio capture delivers raw
10
13
  * interleaved f32le PCM — there is no audio encoder in the ABI.
11
14
  */
15
+ import { type RawGpuDeviceHandle } from "@mediaway/ffi";
12
16
  import { MediawayError, type Rational } from "@mediaway/container";
13
17
  export type { Rational } from "@mediaway/container";
18
+ /**
19
+ * @internal Key for the opaque native handle behind a capture/GPU-device
20
+ * session, consumed only by @mediaway/encoder's capture-to-encode bridge
21
+ * (`EncodeSession.writeFrameFromCameraCapture`/`writeFrameFromDesktopCapture`)
22
+ * and by `AutoVideoEncodeConfig.gpuDevice`. Not part of the public DX
23
+ * contract — never read this directly.
24
+ */
25
+ export declare const NATIVE_HANDLE: unique symbol;
14
26
  export interface VideoFrame {
15
27
  pts: number;
16
28
  duration: number;
@@ -46,6 +58,8 @@ export declare class CameraSession {
46
58
  pollFrame(): VideoFrame | null;
47
59
  /** Joins the backend worker thread — can block up to one frame interval. */
48
60
  close(): Promise<void>;
61
+ /** @internal */
62
+ [NATIVE_HANDLE](): unknown;
49
63
  }
50
64
  /** Open camera `index` at `timeBase`; throws CaptureUnavailableError when no
51
65
  * camera exists on this machine. */
@@ -71,25 +85,90 @@ export declare function openMicrophone(options: {
71
85
  sampleRate: number;
72
86
  channels?: number;
73
87
  }): Promise<MicSession>;
88
+ export interface GpuAdapterInfo {
89
+ /** Pass to `GpuDevice.create({ adapterIndex })` to select this exact adapter. */
90
+ index: number;
91
+ name: string;
92
+ vendorId: number;
93
+ deviceId: number;
94
+ dedicatedVideoMemoryBytes: number;
95
+ /** `false` for a software rasterizer adapter (e.g. WARP). */
96
+ isHardware: boolean;
97
+ }
98
+ /** List every GPU adapter this machine's DXGI factory reports. */
99
+ export declare function listGpuAdapters(): GpuAdapterInfo[];
100
+ export interface GpuDeviceOptions {
101
+ /** Explicit adapter (from `listGpuAdapters()`'s `index`); omit for the first
102
+ * hardware adapter DXGI reports. */
103
+ adapterIndex?: number;
104
+ /** D3D11_CREATE_DEVICE_VIDEO_SUPPORT. Default true. */
105
+ videoSupport?: boolean;
106
+ /** D3D11_CREATE_DEVICE_DEBUG. Default false. */
107
+ debugLayer?: boolean;
108
+ }
109
+ /**
110
+ * A real GPU device, created via the factory (no "bring your own device" path
111
+ * exists from JS). Pass into `openScreenCapture({ gpuDevice })` and/or
112
+ * `AutoVideoEncodeConfig.gpuDevice` (`@mediaway/encoder`) to share one device
113
+ * across capture and encode — Zero-Copy end to end. Close it after every
114
+ * session built from it is closed.
115
+ */
116
+ export declare class GpuDevice {
117
+ private handle;
118
+ /** Wrap a native GPU device handle. Prefer the create() factory. */
119
+ private constructor();
120
+ /** Create a real GPU device. Throws CaptureUnavailableError when no usable
121
+ * adapter exists (e.g. headless CI without even WARP). */
122
+ static create(options?: GpuDeviceOptions): Promise<GpuDevice>;
123
+ /** Release the underlying GPU device. Every session built from it (Screen
124
+ * capture, encode config) becomes invalid the moment this returns. */
125
+ close(): void;
126
+ /** @internal — the `mediaway_gpu_device_handle_t` value bits, read once at
127
+ * create() time. Consumed by openScreenCapture() and
128
+ * AutoVideoEncodeConfig.gpuDevice (@mediaway/encoder). Not part of the
129
+ * public DX contract. */
130
+ [NATIVE_HANDLE](): RawGpuDeviceHandle;
131
+ }
74
132
  /**
75
- * A Screen capture session — NOT available from the C ABI today. Screen needs
76
- * a live GPU device handle (ID3D11Device*) with no CPU fallback, and its C
77
- * representation is deferred (crates/mediaway-ffi/adr/0001 § Deferred),
78
- * so openScreenCapture always throws CaptureUnavailableError. The ideal DX
79
- * (BGRA8 CPU frames at the display's native geometry) is what the aspirational
80
- * screen-record example targets.
133
+ * A Screen capture session — GPU-only, Zero-Copy (`adr/0003-gpu-handle-c-abi.md`
134
+ * §4: no CPU fallback exists in the wrapped Rust backend, so unlike Camera
135
+ * there is no CPU pixel readback path here either). `pollFrame()` proves
136
+ * frames are genuinely arriving (real `pts`/geometry) but its `VideoFrame.data`
137
+ * is always empty — it does not copy pixels out. For real pixel data, feed
138
+ * the session straight into the encoder with
139
+ * `EncodeSession.writeFrameFromDesktopCapture()` (`@mediaway/encoder`), which
140
+ * moves the GPU texture Zero-Copy with no CPU round trip at all.
81
141
  */
82
142
  export declare class ScreenSession {
83
- readonly width = 0;
84
- readonly height = 0;
143
+ readonly width: number;
144
+ readonly height: number;
85
145
  readonly pixelFormat: "bgra8";
86
146
  readonly timeBase: Rational;
87
- /** Screen session (always throws — no C-ABI screen path yet). */
88
- constructor(timeBase: Rational);
147
+ private handle;
148
+ private readonly ownsGpuDevice;
149
+ /** Wrap a native desktop capture handle. Prefer the openScreenCapture() factory. */
150
+ constructor(handle: unknown, width: number, height: number, timeBase: Rational, ownsGpuDevice: GpuDevice | undefined);
151
+ /** Poll the next frame; null when nothing is ready yet. Sync, never blocks.
152
+ * `data` is always empty — see the class doc. */
89
153
  pollFrame(): VideoFrame | null;
154
+ /** Joins the backend worker thread — can block up to one frame interval.
155
+ * Also closes the GPU device this session created internally (auto mode);
156
+ * a caller-supplied device (`options.gpuDevice`) is left open — the caller
157
+ * owns it and must close it themselves. */
90
158
  close(): Promise<void>;
159
+ /** @internal */
160
+ [NATIVE_HANDLE](): unknown;
91
161
  }
162
+ /**
163
+ * Open Screen capture for output `monitorIndex` (default the primary
164
+ * display). Creates a `GpuDevice` internally unless `options.gpuDevice` is
165
+ * supplied — pass one in to pick an explicit adapter
166
+ * (`GpuDevice.create({ adapterIndex })`) or to share one device across
167
+ * capture and encode. Throws `CaptureUnavailableError` when no usable
168
+ * GPU/Desktop-Duplication path exists.
169
+ */
92
170
  export declare function openScreenCapture(options: {
93
171
  timeBase: Rational;
94
172
  monitorIndex?: number;
173
+ gpuDevice?: GpuDevice;
95
174
  }): Promise<ScreenSession>;
package/dist/index.js CHANGED
@@ -1,16 +1,27 @@
1
1
  /**
2
- * @mediaway/device — device capability: camera / screen / microphone capture.
2
+ * @mediaway/device — device capability: camera / screen / microphone capture,
3
+ * plus the GPU device factory (mediaway-device ADR-0007).
3
4
  *
4
5
  * Implements the DX contract in bindings/nodejs/README.md over the
5
6
  * mediaway-ffi C ABI (via @mediaway/ffi). The ABI is domain-split
6
- * (`adr/0004-domain-feature-split.md`): this package folds it back into the
7
- * three DX functions. CPU-storage only: Camera delivers CPU frames; Screen
8
- * capture raises CaptureUnavailableError from the C ABI today (it needs a
9
- * live GPU device handle with no CPU fallback). Audio capture delivers raw
7
+ * (`adr/0004-domain-feature-split.md`): this package folds it back into DX
8
+ * functions. Camera delivers CPU frames; Screen capture is GPU-only, Zero-Copy
9
+ * — it needs a live GPU device handle with no CPU fallback
10
+ * (`adr/0003-gpu-handle-c-abi.md`), which `openScreenCapture()` now gets from
11
+ * `GpuDevice` (auto-created, or caller-supplied for explicit adapter control
12
+ * / sharing one device across capture + encode). Audio capture delivers raw
10
13
  * interleaved f32le PCM — there is no audio encoder in the ABI.
11
14
  */
12
- import { MwAudioFrame, MwAudioConfig, MwCameraConfig, MwCameraFrame, device, copyBytes, } from "@mediaway/ffi";
15
+ import { MwAudioFrame, MwAudioConfig, MwCameraConfig, MwCameraFrame, MwGpuAdapterInfo, device, copyBytes, decodeArray, } from "@mediaway/ffi";
13
16
  import { MediawayError } from "@mediaway/container";
17
+ /**
18
+ * @internal Key for the opaque native handle behind a capture/GPU-device
19
+ * session, consumed only by @mediaway/encoder's capture-to-encode bridge
20
+ * (`EncodeSession.writeFrameFromCameraCapture`/`writeFrameFromDesktopCapture`)
21
+ * and by `AutoVideoEncodeConfig.gpuDevice`. Not part of the public DX
22
+ * contract — never read this directly.
23
+ */
24
+ export const NATIVE_HANDLE = Symbol("mediaway.device.nativeHandle");
14
25
  /** No capture device/backend is available, or the ABI rejects this
15
26
  * configuration as unsupported (today: Screen capture) — expected outcomes,
16
27
  * not hard failures. */
@@ -82,6 +93,10 @@ export class CameraSession {
82
93
  this.handle = null;
83
94
  }
84
95
  }
96
+ /** @internal */
97
+ [NATIVE_HANDLE]() {
98
+ return this.handle;
99
+ }
85
100
  }
86
101
  /** Open camera `index` at `timeBase`; throws CaptureUnavailableError when no
87
102
  * camera exists on this machine. */
@@ -140,30 +155,163 @@ export async function openMicrophone(options) {
140
155
  checkDevice(device.audioFormat(out[0], rate, channels));
141
156
  return new MicSession(out[0], rate[0], channels[0]);
142
157
  }
143
- // ── Screen (ideal only) ────────────────────────────────────────────────────────
158
+ /** List every GPU adapter this machine's DXGI factory reports. */
159
+ export function listGpuAdapters() {
160
+ const outAdapters = [null];
161
+ const outCount = [0];
162
+ checkDevice(device.gpuAdapterList(outAdapters, outCount));
163
+ const count = outCount[0];
164
+ const decoded = decodeArray(outAdapters[0], MwGpuAdapterInfo, count);
165
+ const result = decoded.map((raw) => ({
166
+ index: raw.index,
167
+ name: raw.name,
168
+ vendorId: raw.vendor_id,
169
+ deviceId: raw.device_id,
170
+ dedicatedVideoMemoryBytes: Number(raw.dedicated_video_memory),
171
+ isHardware: raw.is_hardware,
172
+ }));
173
+ if (count > 0)
174
+ device.gpuAdapterListFree(outAdapters[0], count);
175
+ return result;
176
+ }
177
+ /**
178
+ * A real GPU device, created via the factory (no "bring your own device" path
179
+ * exists from JS). Pass into `openScreenCapture({ gpuDevice })` and/or
180
+ * `AutoVideoEncodeConfig.gpuDevice` (`@mediaway/encoder`) to share one device
181
+ * across capture and encode — Zero-Copy end to end. Close it after every
182
+ * session built from it is closed.
183
+ */
184
+ export class GpuDevice {
185
+ handle;
186
+ /** Wrap a native GPU device handle. Prefer the create() factory. */
187
+ constructor(handle) {
188
+ this.handle = handle;
189
+ }
190
+ /** Create a real GPU device. Throws CaptureUnavailableError when no usable
191
+ * adapter exists (e.g. headless CI without even WARP). */
192
+ static async create(options = {}) {
193
+ const raw = {
194
+ adapter: {
195
+ kind: options.adapterIndex === undefined ? 0 : 1,
196
+ index: options.adapterIndex ?? 0,
197
+ },
198
+ video_support: options.videoSupport ?? true,
199
+ debug_layer: options.debugLayer ?? false,
200
+ };
201
+ const out = [null];
202
+ checkDevice(device.gpuDeviceCreate(raw, out));
203
+ if (!out[0])
204
+ throw new MediawayError(9, "GPU device create returned no handle");
205
+ return new GpuDevice(out[0]);
206
+ }
207
+ /** Release the underlying GPU device. Every session built from it (Screen
208
+ * capture, encode config) becomes invalid the moment this returns. */
209
+ close() {
210
+ if (this.handle) {
211
+ device.gpuDeviceClose(this.handle);
212
+ this.handle = null;
213
+ }
214
+ }
215
+ /** @internal — the `mediaway_gpu_device_handle_t` value bits, read once at
216
+ * create() time. Consumed by openScreenCapture() and
217
+ * AutoVideoEncodeConfig.gpuDevice (@mediaway/encoder). Not part of the
218
+ * public DX contract. */
219
+ [NATIVE_HANDLE]() {
220
+ const out = {};
221
+ checkDevice(device.gpuDeviceHandle(this.handle, out));
222
+ return out;
223
+ }
224
+ }
225
+ // ── Screen ───────────────────────────────────────────────────────────────────
144
226
  /**
145
- * A Screen capture session — NOT available from the C ABI today. Screen needs
146
- * a live GPU device handle (ID3D11Device*) with no CPU fallback, and its C
147
- * representation is deferred (crates/mediaway-ffi/adr/0001 § Deferred),
148
- * so openScreenCapture always throws CaptureUnavailableError. The ideal DX
149
- * (BGRA8 CPU frames at the display's native geometry) is what the aspirational
150
- * screen-record example targets.
227
+ * A Screen capture session — GPU-only, Zero-Copy (`adr/0003-gpu-handle-c-abi.md`
228
+ * §4: no CPU fallback exists in the wrapped Rust backend, so unlike Camera
229
+ * there is no CPU pixel readback path here either). `pollFrame()` proves
230
+ * frames are genuinely arriving (real `pts`/geometry) but its `VideoFrame.data`
231
+ * is always empty — it does not copy pixels out. For real pixel data, feed
232
+ * the session straight into the encoder with
233
+ * `EncodeSession.writeFrameFromDesktopCapture()` (`@mediaway/encoder`), which
234
+ * moves the GPU texture Zero-Copy with no CPU round trip at all.
151
235
  */
152
236
  export class ScreenSession {
153
- width = 0;
154
- height = 0;
237
+ width;
238
+ height;
155
239
  pixelFormat = "bgra8";
156
240
  timeBase;
157
- /** Screen session (always throws — no C-ABI screen path yet). */
158
- constructor(timeBase) {
241
+ handle;
242
+ ownsGpuDevice;
243
+ /** Wrap a native desktop capture handle. Prefer the openScreenCapture() factory. */
244
+ constructor(handle, width, height, timeBase, ownsGpuDevice) {
245
+ this.handle = handle;
246
+ this.width = width;
247
+ this.height = height;
159
248
  this.timeBase = timeBase;
249
+ this.ownsGpuDevice = ownsGpuDevice;
160
250
  }
251
+ /** Poll the next frame; null when nothing is ready yet. Sync, never blocks.
252
+ * `data` is always empty — see the class doc. */
161
253
  pollFrame() {
162
- throw new CaptureUnavailableError(3, "Screen capture is not available from this binding today");
254
+ const raw = {};
255
+ const has = [false];
256
+ checkDevice(device.desktopPollFrame(this.handle, raw, has));
257
+ if (!has[0])
258
+ return null;
259
+ // storage_kind is always GPU (1) for Screen — the frame lives in
260
+ // gpu_buffer, a BORROWED handle released below (never freed via
261
+ // desktopFrameFree, which would double-release it; there is nothing to
262
+ // copy out of raw.data/data_len, which stay empty for GPU storage).
263
+ checkDevice(device.desktopReleaseFrame(this.handle));
264
+ return {
265
+ pts: Number(raw.pts),
266
+ duration: Number(raw.duration),
267
+ width: raw.width,
268
+ height: raw.height,
269
+ pixelFormat: "bgra8",
270
+ data: Buffer.alloc(0),
271
+ };
272
+ }
273
+ /** Joins the backend worker thread — can block up to one frame interval.
274
+ * Also closes the GPU device this session created internally (auto mode);
275
+ * a caller-supplied device (`options.gpuDevice`) is left open — the caller
276
+ * owns it and must close it themselves. */
277
+ async close() {
278
+ if (this.handle) {
279
+ checkDevice(device.desktopClose(this.handle));
280
+ this.handle = null;
281
+ }
282
+ this.ownsGpuDevice?.close();
283
+ }
284
+ /** @internal */
285
+ [NATIVE_HANDLE]() {
286
+ return this.handle;
163
287
  }
164
- async close() { }
165
288
  }
289
+ /**
290
+ * Open Screen capture for output `monitorIndex` (default the primary
291
+ * display). Creates a `GpuDevice` internally unless `options.gpuDevice` is
292
+ * supplied — pass one in to pick an explicit adapter
293
+ * (`GpuDevice.create({ adapterIndex })`) or to share one device across
294
+ * capture and encode. Throws `CaptureUnavailableError` when no usable
295
+ * GPU/Desktop-Duplication path exists.
296
+ */
166
297
  export async function openScreenCapture(options) {
167
- void options;
168
- throw new CaptureUnavailableError(3, "Screen capture needs a live GPU device handle with no CPU fallback, and its C representation is deferred — not available from this binding today");
298
+ const ownsGpuDevice = options.gpuDevice === undefined ? await GpuDevice.create() : undefined;
299
+ const gpuDevice = options.gpuDevice ?? ownsGpuDevice;
300
+ if (gpuDevice === undefined)
301
+ throw new MediawayError(9, "no GPU device available");
302
+ try {
303
+ const config = device.desktopConfigScreen(options.monitorIndex ?? 0, { num: BigInt(options.timeBase.num), den: options.timeBase.den }, gpuDevice[NATIVE_HANDLE]());
304
+ const out = [null];
305
+ checkDevice(device.desktopOpen(config, out));
306
+ if (!out[0])
307
+ throw new MediawayError(9, "capture open returned no handle");
308
+ const width = [0];
309
+ const height = [0];
310
+ checkDevice(device.desktopGeometry(out[0], width, height));
311
+ return new ScreenSession(out[0], width[0], height[0], options.timeBase, ownsGpuDevice);
312
+ }
313
+ catch (err) {
314
+ ownsGpuDevice?.close();
315
+ throw err;
316
+ }
169
317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediaway/device",
3
- "version": "0.1.5",
3
+ "version": "0.1.8",
4
4
  "description": "Rust-based, lightweight, fast — camera, microphone, and screen capture for Node.js (poll-based, never blocks)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",