@mediaway/device 0.1.8 → 0.2.2
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 +30 -0
- package/dist/index.d.ts +74 -11
- package/dist/index.js +96 -21
- package/package.json +1 -1
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
|
-
*
|
|
134
|
-
*
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
46
|
+
return new CaptureUnavailableError(status, "no capture backend or device available");
|
|
35
47
|
}
|
|
36
48
|
if (status === 3) {
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
228
|
-
*
|
|
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
|
|
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.
|
|
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)
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
//
|
|
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