@mediaway/device 0.1.6 → 0.2.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 CHANGED
@@ -75,6 +75,35 @@ readback path for Screen). For real pixel data, feed the session straight
75
75
  into an encoder with `EncodeSession.writeFrameFromDesktopCapture()`
76
76
  (`@mediaway/encoder`), Zero-Copy end to end — see that package's README.
77
77
 
78
+ ## Window capture
79
+
80
+ One window by `HWND` (Windows, WGC) — GPU-only and Zero-Copy exactly like Screen, and
81
+ `pollFrame()`'s `data` is likewise always empty (`mediaway-ffi` ADR-0005). `WindowSession` also
82
+ feeds `EncodeSession.writeFrameFromDesktopCapture()`.
83
+
84
+ ```ts
85
+ import { openWindowCapture } from "@mediaway/device";
86
+
87
+ const win = await openWindowCapture({
88
+ hwnd, // bigint | number, caller-owned; 0 is rejected
89
+ timeBase: { num: 1, den: 30 },
90
+ cursor: "included", // default "excluded"
91
+ border: "hidden", // default "shown"; Windows 11 build 22000+
92
+ dimensions: "even-cropped", // default "native"; any hardware encoder accepts even frames
93
+ region: { x: 0, y: 0, width: 640, height: 360 }, // optional
94
+ });
95
+ console.log(win.width, win.height, win.borderHidden);
96
+ await win.close();
97
+ ```
98
+
99
+ - `borderHidden` reports what Windows *did*: a refusal is not an error, so check it after
100
+ asking for `border: "hidden"`.
101
+ - A `region` at the window's origin is free; one anywhere else costs one GPU copy per frame
102
+ (not Zero-Copy). A window that is or becomes smaller than the region throws
103
+ `RegionOutOfBoundsError` — distinct from the bad-config error, so it can be retried with a
104
+ smaller region.
105
+ - Off Windows, `CaptureUnavailableError`.
106
+
78
107
  ## GPU device factory
79
108
 
80
109
  ```ts
@@ -100,6 +129,7 @@ adapter DXGI reports.
100
129
  | `openCamera({ index, timeBase })` | `CameraSession` (`width`, `height`, `pixelFormat` = `"nv12"`) | `pollFrame(): VideoFrame \| null` |
101
130
  | `openMicrophone({ sampleRate, channels })` | `MicSession` (`sampleRate`, `channels`) | `pollFrame(): AudioFrame \| null` |
102
131
  | `openScreenCapture({ timeBase, monitorIndex?, gpuDevice? })` | `ScreenSession` | `pollFrame(): VideoFrame \| null` (`data` always empty) |
132
+ | `openWindowCapture({ hwnd, timeBase, gpuDevice?, cursor?, border?, dimensions?, region? })` | `WindowSession` (`ScreenSession`'s fields + `borderHidden`) | `pollFrame(): VideoFrame \| null` (`data` always empty) |
103
133
  | `listGpuAdapters()` | — | `GpuAdapterInfo[]` |
104
134
  | `GpuDevice.create({ adapterIndex?, videoSupport?, debugLayer? })` | `GpuDevice` | — |
105
135
 
package/dist/index.d.ts CHANGED
@@ -42,6 +42,16 @@ export interface AudioFrame {
42
42
  * not hard failures. */
43
43
  export declare class CaptureUnavailableError extends MediawayError {
44
44
  }
45
+ /** A window capture region does not fit the captured window — at open, or because the
46
+ * window later shrank. A runtime condition a caller can retry with a smaller region,
47
+ * so it is distinct from the bad-config error (`status` 5). */
48
+ export declare class RegionOutOfBoundsError extends MediawayError {
49
+ }
50
+ /**
51
+ * @internal The error a non-zero device status maps to, or `undefined` for `0`. Exported
52
+ * only so the status mapping is testable without provoking every native failure.
53
+ */
54
+ export declare function deviceStatusError(status: number): MediawayError | undefined;
45
55
  /**
46
56
  * A Camera capture session. Negotiated geometry/format are authoritative over
47
57
  * the request — read them from the session properties.
@@ -130,26 +140,20 @@ export declare class GpuDevice {
130
140
  [NATIVE_HANDLE](): RawGpuDeviceHandle;
131
141
  }
132
142
  /**
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.
143
+ * State and behaviour shared by Screen and Window capture sessions: both are the same
144
+ * `mediaway_desktop_capture_t*` handle, GPU-only and Zero-Copy.
141
145
  */
142
- export declare class ScreenSession {
146
+ export declare abstract class DesktopSession {
143
147
  readonly width: number;
144
148
  readonly height: number;
145
149
  readonly pixelFormat: "bgra8";
146
150
  readonly timeBase: Rational;
147
151
  private handle;
148
152
  private readonly ownsGpuDevice;
149
- /** Wrap a native desktop capture handle. Prefer the openScreenCapture() factory. */
153
+ /** @internal Wrap a native desktop capture handle. Use the open*Capture() factories. */
150
154
  constructor(handle: unknown, width: number, height: number, timeBase: Rational, ownsGpuDevice: GpuDevice | undefined);
151
155
  /** Poll the next frame; null when nothing is ready yet. Sync, never blocks.
152
- * `data` is always empty — see the class doc. */
156
+ * `data` is always empty — see the `ScreenSession` class doc. */
153
157
  pollFrame(): VideoFrame | null;
154
158
  /** Joins the backend worker thread — can block up to one frame interval.
155
159
  * Also closes the GPU device this session created internally (auto mode);
@@ -159,6 +163,20 @@ export declare class ScreenSession {
159
163
  /** @internal */
160
164
  [NATIVE_HANDLE](): unknown;
161
165
  }
166
+ /** Either kind of desktop capture session — what `EncodeSession.writeFrameFromDesktopCapture` takes. */
167
+ export type DesktopCaptureSession = ScreenSession | WindowSession;
168
+ /**
169
+ * A Screen capture session — GPU-only, Zero-Copy (`adr/0003-gpu-handle-c-abi.md`
170
+ * §4: no CPU fallback exists in the wrapped Rust backend, so unlike Camera
171
+ * there is no CPU pixel readback path here either). `pollFrame()` proves
172
+ * frames are genuinely arriving (real `pts`/geometry) but its `VideoFrame.data`
173
+ * is always empty — it does not copy pixels out. For real pixel data, feed
174
+ * the session straight into the encoder with
175
+ * `EncodeSession.writeFrameFromDesktopCapture()` (`@mediaway/encoder`), which
176
+ * moves the GPU texture Zero-Copy with no CPU round trip at all.
177
+ */
178
+ export declare class ScreenSession extends DesktopSession {
179
+ }
162
180
  /**
163
181
  * Open Screen capture for output `monitorIndex` (default the primary
164
182
  * display). Creates a `GpuDevice` internally unless `options.gpuDevice` is
@@ -172,3 +190,48 @@ export declare function openScreenCapture(options: {
172
190
  monitorIndex?: number;
173
191
  gpuDevice?: GpuDevice;
174
192
  }): Promise<ScreenSession>;
193
+ /** A rectangle of the captured window to record instead of all of it, in window pixels. */
194
+ export interface CaptureRegion {
195
+ x: number;
196
+ y: number;
197
+ width: number;
198
+ height: number;
199
+ }
200
+ export interface WindowCaptureOptions {
201
+ /** The window to capture, as its `HWND` bits. Caller-owned: the session does not
202
+ * extend its lifetime. `0` is rejected as a bad config (`status` 5). */
203
+ hwnd: bigint | number;
204
+ timeBase: Rational;
205
+ /** Share a device with an encoder, or pick an adapter; omitted, one is created and
206
+ * closed with the session (same rule as `openScreenCapture`). */
207
+ gpuDevice?: GpuDevice;
208
+ /** Draw the mouse pointer into frames. Default `"excluded"`. */
209
+ cursor?: "excluded" | "included";
210
+ /** Ask Windows 11 (build 22000+) not to draw the capture border. Default `"shown"`.
211
+ * A refusal is not an error — read `WindowSession.borderHidden` for what happened. */
212
+ border?: "shown" | "hidden";
213
+ /** `"even-cropped"` drops an odd axis's last column/row so any hardware encoder
214
+ * accepts the frames; free on WGC. Default `"native"`. */
215
+ dimensions?: "native" | "even-cropped";
216
+ /** Record only this rectangle. A region at the window's origin is free; one anywhere
217
+ * else costs one GPU copy per frame (not Zero-Copy). A window that shrinks below it
218
+ * throws `RegionOutOfBoundsError`. */
219
+ region?: CaptureRegion;
220
+ }
221
+ /**
222
+ * A Window capture session (Windows, WGC) — GPU-only, Zero-Copy, exactly like
223
+ * `ScreenSession`; see its doc for why `pollFrame()` returns no pixel data.
224
+ */
225
+ export declare class WindowSession extends DesktopSession {
226
+ /** Whether Windows actually hid the capture border. Meaningful with
227
+ * `border: "hidden"`; `false` otherwise, and on builds before Windows 11 22000. */
228
+ readonly borderHidden: boolean;
229
+ /** @internal Use openWindowCapture(). */
230
+ constructor(handle: unknown, width: number, height: number, timeBase: Rational, ownsGpuDevice: GpuDevice | undefined, borderHidden: boolean);
231
+ }
232
+ /**
233
+ * Open Window capture for `options.hwnd`. Creates a `GpuDevice` internally unless
234
+ * `options.gpuDevice` is supplied. Throws `CaptureUnavailableError` off Windows or with
235
+ * no usable GPU, `RegionOutOfBoundsError` when the region does not fit the window.
236
+ */
237
+ export declare function openWindowCapture(options: WindowCaptureOptions): Promise<WindowSession>;
package/dist/index.js CHANGED
@@ -27,14 +27,26 @@ export const NATIVE_HANDLE = Symbol("mediaway.device.nativeHandle");
27
27
  * not hard failures. */
28
28
  export class CaptureUnavailableError extends MediawayError {
29
29
  }
30
- function checkDevice(status) {
30
+ /** A window capture region does not fit the captured window — at open, or because the
31
+ * window later shrank. A runtime condition a caller can retry with a smaller region,
32
+ * so it is distinct from the bad-config error (`status` 5). */
33
+ export class RegionOutOfBoundsError extends MediawayError {
34
+ }
35
+ /**
36
+ * @internal The error a non-zero device status maps to, or `undefined` for `0`. Exported
37
+ * only so the status mapping is testable without provoking every native failure.
38
+ */
39
+ export function deviceStatusError(status) {
31
40
  if (status === 0)
32
- return;
41
+ return undefined;
42
+ if (status === 14) {
43
+ return new RegionOutOfBoundsError(status, "capture region does not fit the window");
44
+ }
33
45
  if (status === 4 || status === 6 || status === 8) {
34
- throw new CaptureUnavailableError(status, "no capture backend or device available");
46
+ return new CaptureUnavailableError(status, "no capture backend or device available");
35
47
  }
36
48
  if (status === 3) {
37
- throw new CaptureUnavailableError(status, "this capture configuration is unsupported by the ABI");
49
+ return new CaptureUnavailableError(status, "this capture configuration is unsupported by the ABI");
38
50
  }
39
51
  const names = {
40
52
  1: "invalid argument",
@@ -47,7 +59,12 @@ function checkDevice(status) {
47
59
  12: "callback mode active (poll disabled)",
48
60
  13: "timed out waiting for a frame",
49
61
  };
50
- throw new MediawayError(status, names[status] ?? "unknown device error");
62
+ return new MediawayError(status, names[status] ?? "unknown device error");
63
+ }
64
+ function checkDevice(status) {
65
+ const err = deviceStatusError(status);
66
+ if (err !== undefined)
67
+ throw err;
51
68
  }
52
69
  // ── Camera ─────────────────────────────────────────────────────────────────────
53
70
  /**
@@ -222,25 +239,19 @@ export class GpuDevice {
222
239
  return out;
223
240
  }
224
241
  }
225
- // ── Screen ───────────────────────────────────────────────────────────────────
242
+ // ── Desktop capture (Screen + Window) ────────────────────────────────────────
226
243
  /**
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.
244
+ * State and behaviour shared by Screen and Window capture sessions: both are the same
245
+ * `mediaway_desktop_capture_t*` handle, GPU-only and Zero-Copy.
235
246
  */
236
- export class ScreenSession {
247
+ export class DesktopSession {
237
248
  width;
238
249
  height;
239
250
  pixelFormat = "bgra8";
240
251
  timeBase;
241
252
  handle;
242
253
  ownsGpuDevice;
243
- /** Wrap a native desktop capture handle. Prefer the openScreenCapture() factory. */
254
+ /** @internal Wrap a native desktop capture handle. Use the open*Capture() factories. */
244
255
  constructor(handle, width, height, timeBase, ownsGpuDevice) {
245
256
  this.handle = handle;
246
257
  this.width = width;
@@ -249,17 +260,17 @@ export class ScreenSession {
249
260
  this.ownsGpuDevice = ownsGpuDevice;
250
261
  }
251
262
  /** Poll the next frame; null when nothing is ready yet. Sync, never blocks.
252
- * `data` is always empty — see the class doc. */
263
+ * `data` is always empty — see the `ScreenSession` class doc. */
253
264
  pollFrame() {
254
265
  const raw = {};
255
266
  const has = [false];
256
267
  checkDevice(device.desktopPollFrame(this.handle, raw, has));
257
268
  if (!has[0])
258
269
  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).
270
+ // storage_kind is always GPU (1) — the frame lives in gpu_buffer, a BORROWED
271
+ // handle released below (never freed via desktopFrameFree, which would
272
+ // double-release it; there is nothing to copy out of raw.data/data_len, which
273
+ // stay empty for GPU storage).
263
274
  checkDevice(device.desktopReleaseFrame(this.handle));
264
275
  return {
265
276
  pts: Number(raw.pts),
@@ -286,6 +297,18 @@ export class ScreenSession {
286
297
  return this.handle;
287
298
  }
288
299
  }
300
+ /**
301
+ * A Screen capture session — GPU-only, Zero-Copy (`adr/0003-gpu-handle-c-abi.md`
302
+ * §4: no CPU fallback exists in the wrapped Rust backend, so unlike Camera
303
+ * there is no CPU pixel readback path here either). `pollFrame()` proves
304
+ * frames are genuinely arriving (real `pts`/geometry) but its `VideoFrame.data`
305
+ * is always empty — it does not copy pixels out. For real pixel data, feed
306
+ * the session straight into the encoder with
307
+ * `EncodeSession.writeFrameFromDesktopCapture()` (`@mediaway/encoder`), which
308
+ * moves the GPU texture Zero-Copy with no CPU round trip at all.
309
+ */
310
+ export class ScreenSession extends DesktopSession {
311
+ }
289
312
  /**
290
313
  * Open Screen capture for output `monitorIndex` (default the primary
291
314
  * display). Creates a `GpuDevice` internally unless `options.gpuDevice` is
@@ -315,3 +338,55 @@ export async function openScreenCapture(options) {
315
338
  throw err;
316
339
  }
317
340
  }
341
+ /**
342
+ * A Window capture session (Windows, WGC) — GPU-only, Zero-Copy, exactly like
343
+ * `ScreenSession`; see its doc for why `pollFrame()` returns no pixel data.
344
+ */
345
+ export class WindowSession extends DesktopSession {
346
+ /** Whether Windows actually hid the capture border. Meaningful with
347
+ * `border: "hidden"`; `false` otherwise, and on builds before Windows 11 22000. */
348
+ borderHidden;
349
+ /** @internal Use openWindowCapture(). */
350
+ constructor(handle, width, height, timeBase, ownsGpuDevice, borderHidden) {
351
+ super(handle, width, height, timeBase, ownsGpuDevice);
352
+ this.borderHidden = borderHidden;
353
+ }
354
+ }
355
+ /**
356
+ * Open Window capture for `options.hwnd`. Creates a `GpuDevice` internally unless
357
+ * `options.gpuDevice` is supplied. Throws `CaptureUnavailableError` off Windows or with
358
+ * no usable GPU, `RegionOutOfBoundsError` when the region does not fit the window.
359
+ */
360
+ export async function openWindowCapture(options) {
361
+ const ownsGpuDevice = options.gpuDevice === undefined ? await GpuDevice.create() : undefined;
362
+ const gpuDevice = options.gpuDevice ?? ownsGpuDevice;
363
+ if (gpuDevice === undefined)
364
+ throw new MediawayError(9, "no GPU device available");
365
+ try {
366
+ const config = device.desktopConfigWindow(BigInt(options.hwnd), { num: BigInt(options.timeBase.num), den: options.timeBase.den }, gpuDevice[NATIVE_HANDLE]());
367
+ config.cursor = options.cursor === "included" ? 1 : 0;
368
+ config.border = options.border === "hidden" ? 1 : 0;
369
+ config.dimensions = options.dimensions === "even-cropped" ? 1 : 0;
370
+ if (options.region !== undefined) {
371
+ config.region_enabled = true;
372
+ config.region_x = options.region.x;
373
+ config.region_y = options.region.y;
374
+ config.region_width = options.region.width;
375
+ config.region_height = options.region.height;
376
+ }
377
+ const out = [null];
378
+ checkDevice(device.desktopOpen(config, out));
379
+ if (!out[0])
380
+ throw new MediawayError(9, "capture open returned no handle");
381
+ const width = [0];
382
+ const height = [0];
383
+ checkDevice(device.desktopGeometry(out[0], width, height));
384
+ const hidden = [false];
385
+ checkDevice(device.desktopBorderHidden(out[0], hidden));
386
+ return new WindowSession(out[0], width[0], height[0], options.timeBase, ownsGpuDevice, hidden[0]);
387
+ }
388
+ catch (err) {
389
+ ownsGpuDevice?.close();
390
+ throw err;
391
+ }
392
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediaway/device",
3
- "version": "0.1.6",
3
+ "version": "0.2.1",
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",