memoir-node 0.1.7 → 0.2.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/LICENSE +27 -27
- package/README.md +249 -249
- package/build/Release/avcodec-62.dll +0 -0
- package/build/Release/avdevice-62.dll +0 -0
- package/build/Release/avfilter-11.dll +0 -0
- package/build/Release/avformat-62.dll +0 -0
- package/build/Release/avutil-60.dll +0 -0
- package/build/Release/libx265.dll +0 -0
- package/build/Release/memoir_node.node +0 -0
- package/build/Release/swresample-6.dll +0 -0
- package/build/Release/swscale-9.dll +0 -0
- package/package.json +40 -42
package/LICENSE
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 lunavod
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
This software links against FFmpeg, which is licensed under the
|
|
26
|
-
GNU Lesser General Public License (LGPL) version 2.1 or later.
|
|
27
|
-
See https://www.ffmpeg.org/legal.html for details.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lunavod
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This software links against FFmpeg, which is licensed under the
|
|
26
|
+
GNU Lesser General Public License (LGPL) version 2.1 or later.
|
|
27
|
+
See https://www.ffmpeg.org/legal.html for details.
|
package/README.md
CHANGED
|
@@ -1,249 +1,249 @@
|
|
|
1
|
-
# memoir-node
|
|
2
|
-
|
|
3
|
-
Windows-native screen capture module with Node.js bindings for real-time frame analysis and deterministic replay recording.
|
|
4
|
-
|
|
5
|
-
Memoir captures frames from a window or monitor using Windows Graphics Capture (WGC), delivers them as Buffers, and optionally records them to HEVC video with per-frame metadata.
|
|
6
|
-
|
|
7
|
-
## Features
|
|
8
|
-
|
|
9
|
-
- **WGC capture** — continuous frame capture from any window or monitor
|
|
10
|
-
- **Zero-copy Buffers** — BGRA frames delivered as Node.js Buffers without copying
|
|
11
|
-
- **Hardware-accelerated recording** — lossless HEVC encoding via NVENC, AMF, or x265 fallback
|
|
12
|
-
- **Binary metadata** — `.meta` sidecar with per-frame keyboard state, timestamps, and frame IDs
|
|
13
|
-
- **Dynamic recording** — start/stop recording without restarting capture
|
|
14
|
-
- **Frame-accurate keyboard** — key state snapshot at the exact moment each frame is accepted
|
|
15
|
-
- **Pure TypeScript meta reader/writer** — read and write `.meta` files without the native addon
|
|
16
|
-
- **Synchronous blocking API** — designed for Worker threads running tick loops
|
|
17
|
-
|
|
18
|
-
## Requirements
|
|
19
|
-
|
|
20
|
-
- Windows 10 1903+
|
|
21
|
-
- Node.js 18+
|
|
22
|
-
- x64 architecture
|
|
23
|
-
- NVIDIA GPU (NVENC), AMD GPU (AMF), or CPU-only (x265) for recording
|
|
24
|
-
|
|
25
|
-
## Installation
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
npm install memoir-node
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Quick Start
|
|
32
|
-
|
|
33
|
-
### Capture frames
|
|
34
|
-
|
|
35
|
-
```typescript
|
|
36
|
-
import { CaptureEngine } from 'memoir-node'
|
|
37
|
-
|
|
38
|
-
const engine = new CaptureEngine({
|
|
39
|
-
target: { type: 'monitor', index: 0 }, // primary monitor
|
|
40
|
-
maxFps: 10,
|
|
41
|
-
})
|
|
42
|
-
engine.start()
|
|
43
|
-
|
|
44
|
-
const frame = engine.getNextFrame(5000)
|
|
45
|
-
if (frame) {
|
|
46
|
-
console.log(`Frame ${frame.frameId}: ${frame.width}x${frame.height}`)
|
|
47
|
-
console.log(`Keys: ${frame.keyboardMask.toString(16)}`)
|
|
48
|
-
// frame.data is a Buffer with BGRA pixels
|
|
49
|
-
frame.release()
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
engine.stop()
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### Capture a specific window
|
|
56
|
-
|
|
57
|
-
```typescript
|
|
58
|
-
const engine = new CaptureEngine({
|
|
59
|
-
target: { type: 'windowTitle', pattern: '(?i)notepad' },
|
|
60
|
-
maxFps: 30,
|
|
61
|
-
})
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Or by executable name:
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
const engine = new CaptureEngine({
|
|
68
|
-
target: { type: 'windowExe', pattern: 'notepad\\.exe' },
|
|
69
|
-
maxFps: 30,
|
|
70
|
-
})
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Record to MP4
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
const engine = new CaptureEngine({
|
|
77
|
-
target: { type: 'monitor', index: 0 },
|
|
78
|
-
maxFps: 10,
|
|
79
|
-
recordWidth: 1920,
|
|
80
|
-
recordHeight: 1080,
|
|
81
|
-
})
|
|
82
|
-
engine.start()
|
|
83
|
-
|
|
84
|
-
const info = engine.startRecording('session_001')
|
|
85
|
-
console.log(`Recording to ${info.videoPath}`) // session_001.mp4
|
|
86
|
-
|
|
87
|
-
for (let i = 0; i < 100; i++) {
|
|
88
|
-
const frame = engine.getNextFrame(5000)
|
|
89
|
-
if (frame) frame.release()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
engine.stopRecording() // finalizes .mp4 + .meta
|
|
93
|
-
engine.stop()
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### Read metadata
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
import { readMeta, pressedKeys } from 'memoir-node'
|
|
100
|
-
|
|
101
|
-
const meta = readMeta('session_001.meta')
|
|
102
|
-
console.log(`Keys tracked: ${meta.keys.map(k => k.name)}`)
|
|
103
|
-
|
|
104
|
-
for (const row of meta.rows) {
|
|
105
|
-
console.log(`Frame ${row.frameId}: ${pressedKeys(row, meta.keys).join(', ')}`)
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### Write metadata (for synthetic replays)
|
|
110
|
-
|
|
111
|
-
```typescript
|
|
112
|
-
import { writeMeta, type MetaKeyEntry, type MetaRow } from 'memoir-node'
|
|
113
|
-
|
|
114
|
-
const keys: MetaKeyEntry[] = [
|
|
115
|
-
{ bit: 0, vk: 0x57, name: 'W' },
|
|
116
|
-
{ bit: 1, vk: 0x41, name: 'A' },
|
|
117
|
-
]
|
|
118
|
-
|
|
119
|
-
const rows: MetaRow[] = [{
|
|
120
|
-
frameId: 0n, recordFrameIndex: 0n,
|
|
121
|
-
captureQpc: 0n, hostAcceptQpc: 0n,
|
|
122
|
-
keyboardMask: 0b01n,
|
|
123
|
-
width: 1920, height: 1080, analysisStride: 7680, flags: 0,
|
|
124
|
-
}]
|
|
125
|
-
|
|
126
|
-
writeMeta('synthetic.meta', keys, rows)
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### Worker thread usage (Electron)
|
|
130
|
-
|
|
131
|
-
```typescript
|
|
132
|
-
// tick-worker.ts — runs in Worker thread
|
|
133
|
-
import { CaptureEngine } from 'memoir-node'
|
|
134
|
-
import { parentPort } from 'worker_threads'
|
|
135
|
-
|
|
136
|
-
const engine = new CaptureEngine({
|
|
137
|
-
target: { type: 'windowExe', pattern: 'myapp\\.exe' },
|
|
138
|
-
maxFps: 10,
|
|
139
|
-
})
|
|
140
|
-
engine.start()
|
|
141
|
-
|
|
142
|
-
while (true) {
|
|
143
|
-
const frame = engine.getNextFrame(2000)
|
|
144
|
-
if (!frame) {
|
|
145
|
-
const err = engine.lastError()
|
|
146
|
-
if (err) { parentPort!.postMessage({ type: 'error', error: err }); break }
|
|
147
|
-
continue
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// frame.data is a Buffer — process it here, don't post it to main thread
|
|
151
|
-
parentPort!.postMessage({
|
|
152
|
-
type: 'tick',
|
|
153
|
-
frameId: frame.frameId,
|
|
154
|
-
width: frame.width,
|
|
155
|
-
height: frame.height,
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
frame.release()
|
|
159
|
-
}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
## API Reference
|
|
163
|
-
|
|
164
|
-
### `new CaptureEngine(options)`
|
|
165
|
-
|
|
166
|
-
| Option | Default | Description |
|
|
167
|
-
|--------|---------|-------------|
|
|
168
|
-
| `target` | required | `{ type: 'monitor', index }`, `{ type: 'windowTitle', pattern }`, or `{ type: 'windowExe', pattern }` |
|
|
169
|
-
| `maxFps` | `10` | Maximum accepted frame rate |
|
|
170
|
-
| `queueCapacity` | `1` | Bounded queue size |
|
|
171
|
-
| `captureCursor` | `false` | Include cursor in capture |
|
|
172
|
-
| `keys` | 40-key gaming set | Array of `{ bit, vk, name }` for keyboard tracking |
|
|
173
|
-
| `recordWidth` | `1920` | Recording output width |
|
|
174
|
-
| `recordHeight` | `1080` | Recording output height |
|
|
175
|
-
| `recordGop` | `1` | GOP size (1 = all-intra) |
|
|
176
|
-
|
|
177
|
-
### Methods
|
|
178
|
-
|
|
179
|
-
| Method | Returns | Description |
|
|
180
|
-
|--------|---------|-------------|
|
|
181
|
-
| `start()` | `void` | Initialize D3D11, create WGC session, begin capturing |
|
|
182
|
-
| `stop()` | `void` | Stop capture and any active recording |
|
|
183
|
-
| `getNextFrame(timeoutMs?)` | `FramePacket \| null` | Block until frame available. -1 = forever, 0 = poll |
|
|
184
|
-
| `startRecording(basePath, encoder?)` | `RecordingInfo` | Start recording to `basePath.mp4` + `basePath.meta` |
|
|
185
|
-
| `startRecording(opts)` | `RecordingInfo` | Start recording with `{ path, videoName, metaName, encoder? }` |
|
|
186
|
-
| `stopRecording()` | `void` | Finalize recording |
|
|
187
|
-
| `isRecording()` | `boolean` | Whether a recording session is active |
|
|
188
|
-
| `stats()` | `EngineStats` | Live counters |
|
|
189
|
-
| `lastError()` | `string \| null` | Last non-fatal error |
|
|
190
|
-
|
|
191
|
-
### `FramePacket`
|
|
192
|
-
|
|
193
|
-
| Property | Type | Description |
|
|
194
|
-
|----------|------|-------------|
|
|
195
|
-
| `frameId` | `number` | Monotonic frame ID |
|
|
196
|
-
| `data` | `Buffer` | BGRA pixels, length = stride * height |
|
|
197
|
-
| `keyboardMask` | `bigint` | 64-bit key state bitmask |
|
|
198
|
-
| `captureQpc` | `bigint` | WGC timestamp (100ns units) |
|
|
199
|
-
| `hostAcceptQpc` | `bigint` | Host QPC when frame was accepted |
|
|
200
|
-
| `width`, `height`, `stride` | `number` | Frame dimensions |
|
|
201
|
-
| `released` | `boolean` | Whether pixel memory has been freed |
|
|
202
|
-
|
|
203
|
-
Call `frame.release()` when done to free pixel memory.
|
|
204
|
-
|
|
205
|
-
### Recording
|
|
206
|
-
|
|
207
|
-
Encoding: lossless HEVC (QP=0), YUV 4:4:4. Encoder selected automatically: `hevc_nvenc` (NVIDIA) → `hevc_amf` (AMD) → `libx265` (software). Force a specific encoder:
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
const info = engine.startRecording('session', 'libx265')
|
|
211
|
-
console.log(info.codec) // "libx265"
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
### Meta utilities
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
import { readMeta, writeMeta, isPressed, pressedKeys, synthesizeKeyEvents } from 'memoir-node'
|
|
218
|
-
|
|
219
|
-
// Read
|
|
220
|
-
const meta = readMeta('session.meta')
|
|
221
|
-
|
|
222
|
-
// Check keys
|
|
223
|
-
isPressed(meta.rows[0], 'W', meta.keys) // boolean
|
|
224
|
-
pressedKeys(meta.rows[0], meta.keys) // string[]
|
|
225
|
-
|
|
226
|
-
// Generate key events for replay
|
|
227
|
-
const events = synthesizeKeyEvents(meta.rows, meta.keys)
|
|
228
|
-
// [{ frame: 0n, type: 'keyDown', key: 'W' }, ...]
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
## Architecture
|
|
232
|
-
|
|
233
|
-
```
|
|
234
|
-
WGC FrameArrived (thread pool)
|
|
235
|
-
│
|
|
236
|
-
├─ FPS limiter → drop if too soon
|
|
237
|
-
├─ Queue check → drop if full (drop-new policy)
|
|
238
|
-
│
|
|
239
|
-
├─ Accept: assign frame_id, snapshot keyboard
|
|
240
|
-
├─ GPU→CPU: CopyResource → staging → Map → memcpy → Buffer (zero-copy)
|
|
241
|
-
├─ Recording: swscale (BGRA→YUV444) → HEVC encoder → MP4
|
|
242
|
-
└─ Enqueue → Node.js consumer
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
`getNextFrame()` is a synchronous blocking call that waits on a condition variable. No GIL concerns (unlike Python) — the calling thread simply sleeps until a frame arrives. Use in a Worker thread to avoid blocking the main V8 thread.
|
|
246
|
-
|
|
247
|
-
## License
|
|
248
|
-
|
|
249
|
-
MIT. Links against FFmpeg (LGPL 2.1+).
|
|
1
|
+
# memoir-node
|
|
2
|
+
|
|
3
|
+
Windows-native screen capture module with Node.js bindings for real-time frame analysis and deterministic replay recording.
|
|
4
|
+
|
|
5
|
+
Memoir captures frames from a window or monitor using Windows Graphics Capture (WGC), delivers them as Buffers, and optionally records them to HEVC video with per-frame metadata.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **WGC capture** — continuous frame capture from any window or monitor
|
|
10
|
+
- **Zero-copy Buffers** — BGRA frames delivered as Node.js Buffers without copying
|
|
11
|
+
- **Hardware-accelerated recording** — lossless HEVC encoding via NVENC, AMF, or x265 fallback
|
|
12
|
+
- **Binary metadata** — `.meta` sidecar with per-frame keyboard state, timestamps, and frame IDs
|
|
13
|
+
- **Dynamic recording** — start/stop recording without restarting capture
|
|
14
|
+
- **Frame-accurate keyboard** — key state snapshot at the exact moment each frame is accepted
|
|
15
|
+
- **Pure TypeScript meta reader/writer** — read and write `.meta` files without the native addon
|
|
16
|
+
- **Synchronous blocking API** — designed for Worker threads running tick loops
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Windows 10 1903+
|
|
21
|
+
- Node.js 18+
|
|
22
|
+
- x64 architecture
|
|
23
|
+
- NVIDIA GPU (NVENC), AMD GPU (AMF), or CPU-only (x265) for recording
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
npm install memoir-node
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
### Capture frames
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { CaptureEngine } from 'memoir-node'
|
|
37
|
+
|
|
38
|
+
const engine = new CaptureEngine({
|
|
39
|
+
target: { type: 'monitor', index: 0 }, // primary monitor
|
|
40
|
+
maxFps: 10,
|
|
41
|
+
})
|
|
42
|
+
engine.start()
|
|
43
|
+
|
|
44
|
+
const frame = engine.getNextFrame(5000)
|
|
45
|
+
if (frame) {
|
|
46
|
+
console.log(`Frame ${frame.frameId}: ${frame.width}x${frame.height}`)
|
|
47
|
+
console.log(`Keys: ${frame.keyboardMask.toString(16)}`)
|
|
48
|
+
// frame.data is a Buffer with BGRA pixels
|
|
49
|
+
frame.release()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
engine.stop()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Capture a specific window
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const engine = new CaptureEngine({
|
|
59
|
+
target: { type: 'windowTitle', pattern: '(?i)notepad' },
|
|
60
|
+
maxFps: 30,
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or by executable name:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const engine = new CaptureEngine({
|
|
68
|
+
target: { type: 'windowExe', pattern: 'notepad\\.exe' },
|
|
69
|
+
maxFps: 30,
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Record to MP4
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const engine = new CaptureEngine({
|
|
77
|
+
target: { type: 'monitor', index: 0 },
|
|
78
|
+
maxFps: 10,
|
|
79
|
+
recordWidth: 1920,
|
|
80
|
+
recordHeight: 1080,
|
|
81
|
+
})
|
|
82
|
+
engine.start()
|
|
83
|
+
|
|
84
|
+
const info = engine.startRecording('session_001')
|
|
85
|
+
console.log(`Recording to ${info.videoPath}`) // session_001.mp4
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < 100; i++) {
|
|
88
|
+
const frame = engine.getNextFrame(5000)
|
|
89
|
+
if (frame) frame.release()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
engine.stopRecording() // finalizes .mp4 + .meta
|
|
93
|
+
engine.stop()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Read metadata
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { readMeta, pressedKeys } from 'memoir-node'
|
|
100
|
+
|
|
101
|
+
const meta = readMeta('session_001.meta')
|
|
102
|
+
console.log(`Keys tracked: ${meta.keys.map(k => k.name)}`)
|
|
103
|
+
|
|
104
|
+
for (const row of meta.rows) {
|
|
105
|
+
console.log(`Frame ${row.frameId}: ${pressedKeys(row, meta.keys).join(', ')}`)
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Write metadata (for synthetic replays)
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { writeMeta, type MetaKeyEntry, type MetaRow } from 'memoir-node'
|
|
113
|
+
|
|
114
|
+
const keys: MetaKeyEntry[] = [
|
|
115
|
+
{ bit: 0, vk: 0x57, name: 'W' },
|
|
116
|
+
{ bit: 1, vk: 0x41, name: 'A' },
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
const rows: MetaRow[] = [{
|
|
120
|
+
frameId: 0n, recordFrameIndex: 0n,
|
|
121
|
+
captureQpc: 0n, hostAcceptQpc: 0n,
|
|
122
|
+
keyboardMask: 0b01n,
|
|
123
|
+
width: 1920, height: 1080, analysisStride: 7680, flags: 0,
|
|
124
|
+
}]
|
|
125
|
+
|
|
126
|
+
writeMeta('synthetic.meta', keys, rows)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Worker thread usage (Electron)
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// tick-worker.ts — runs in Worker thread
|
|
133
|
+
import { CaptureEngine } from 'memoir-node'
|
|
134
|
+
import { parentPort } from 'worker_threads'
|
|
135
|
+
|
|
136
|
+
const engine = new CaptureEngine({
|
|
137
|
+
target: { type: 'windowExe', pattern: 'myapp\\.exe' },
|
|
138
|
+
maxFps: 10,
|
|
139
|
+
})
|
|
140
|
+
engine.start()
|
|
141
|
+
|
|
142
|
+
while (true) {
|
|
143
|
+
const frame = engine.getNextFrame(2000)
|
|
144
|
+
if (!frame) {
|
|
145
|
+
const err = engine.lastError()
|
|
146
|
+
if (err) { parentPort!.postMessage({ type: 'error', error: err }); break }
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// frame.data is a Buffer — process it here, don't post it to main thread
|
|
151
|
+
parentPort!.postMessage({
|
|
152
|
+
type: 'tick',
|
|
153
|
+
frameId: frame.frameId,
|
|
154
|
+
width: frame.width,
|
|
155
|
+
height: frame.height,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
frame.release()
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## API Reference
|
|
163
|
+
|
|
164
|
+
### `new CaptureEngine(options)`
|
|
165
|
+
|
|
166
|
+
| Option | Default | Description |
|
|
167
|
+
|--------|---------|-------------|
|
|
168
|
+
| `target` | required | `{ type: 'monitor', index }`, `{ type: 'windowTitle', pattern }`, or `{ type: 'windowExe', pattern }` |
|
|
169
|
+
| `maxFps` | `10` | Maximum accepted frame rate |
|
|
170
|
+
| `queueCapacity` | `1` | Bounded queue size |
|
|
171
|
+
| `captureCursor` | `false` | Include cursor in capture |
|
|
172
|
+
| `keys` | 40-key gaming set | Array of `{ bit, vk, name }` for keyboard tracking |
|
|
173
|
+
| `recordWidth` | `1920` | Recording output width |
|
|
174
|
+
| `recordHeight` | `1080` | Recording output height |
|
|
175
|
+
| `recordGop` | `1` | GOP size (1 = all-intra) |
|
|
176
|
+
|
|
177
|
+
### Methods
|
|
178
|
+
|
|
179
|
+
| Method | Returns | Description |
|
|
180
|
+
|--------|---------|-------------|
|
|
181
|
+
| `start()` | `void` | Initialize D3D11, create WGC session, begin capturing |
|
|
182
|
+
| `stop()` | `void` | Stop capture and any active recording |
|
|
183
|
+
| `getNextFrame(timeoutMs?)` | `FramePacket \| null` | Block until frame available. -1 = forever, 0 = poll |
|
|
184
|
+
| `startRecording(basePath, encoder?)` | `RecordingInfo` | Start recording to `basePath.mp4` + `basePath.meta` |
|
|
185
|
+
| `startRecording(opts)` | `RecordingInfo` | Start recording with `{ path, videoName, metaName, encoder? }` |
|
|
186
|
+
| `stopRecording()` | `void` | Finalize recording |
|
|
187
|
+
| `isRecording()` | `boolean` | Whether a recording session is active |
|
|
188
|
+
| `stats()` | `EngineStats` | Live counters |
|
|
189
|
+
| `lastError()` | `string \| null` | Last non-fatal error |
|
|
190
|
+
|
|
191
|
+
### `FramePacket`
|
|
192
|
+
|
|
193
|
+
| Property | Type | Description |
|
|
194
|
+
|----------|------|-------------|
|
|
195
|
+
| `frameId` | `number` | Monotonic frame ID |
|
|
196
|
+
| `data` | `Buffer` | BGRA pixels, length = stride * height |
|
|
197
|
+
| `keyboardMask` | `bigint` | 64-bit key state bitmask |
|
|
198
|
+
| `captureQpc` | `bigint` | WGC timestamp (100ns units) |
|
|
199
|
+
| `hostAcceptQpc` | `bigint` | Host QPC when frame was accepted |
|
|
200
|
+
| `width`, `height`, `stride` | `number` | Frame dimensions |
|
|
201
|
+
| `released` | `boolean` | Whether pixel memory has been freed |
|
|
202
|
+
|
|
203
|
+
Call `frame.release()` when done to free pixel memory.
|
|
204
|
+
|
|
205
|
+
### Recording
|
|
206
|
+
|
|
207
|
+
Encoding: lossless HEVC (QP=0), YUV 4:4:4. Encoder selected automatically: `hevc_nvenc` (NVIDIA) → `hevc_amf` (AMD) → `libx265` (software). Force a specific encoder:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
const info = engine.startRecording('session', 'libx265')
|
|
211
|
+
console.log(info.codec) // "libx265"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Meta utilities
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
import { readMeta, writeMeta, isPressed, pressedKeys, synthesizeKeyEvents } from 'memoir-node'
|
|
218
|
+
|
|
219
|
+
// Read
|
|
220
|
+
const meta = readMeta('session.meta')
|
|
221
|
+
|
|
222
|
+
// Check keys
|
|
223
|
+
isPressed(meta.rows[0], 'W', meta.keys) // boolean
|
|
224
|
+
pressedKeys(meta.rows[0], meta.keys) // string[]
|
|
225
|
+
|
|
226
|
+
// Generate key events for replay
|
|
227
|
+
const events = synthesizeKeyEvents(meta.rows, meta.keys)
|
|
228
|
+
// [{ frame: 0n, type: 'keyDown', key: 'W' }, ...]
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Architecture
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
WGC FrameArrived (thread pool)
|
|
235
|
+
│
|
|
236
|
+
├─ FPS limiter → drop if too soon
|
|
237
|
+
├─ Queue check → drop if full (drop-new policy)
|
|
238
|
+
│
|
|
239
|
+
├─ Accept: assign frame_id, snapshot keyboard
|
|
240
|
+
├─ GPU→CPU: CopyResource → staging → Map → memcpy → Buffer (zero-copy)
|
|
241
|
+
├─ Recording: swscale (BGRA→YUV444) → HEVC encoder → MP4
|
|
242
|
+
└─ Enqueue → Node.js consumer
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`getNextFrame()` is a synchronous blocking call that waits on a condition variable. No GIL concerns (unlike Python) — the calling thread simply sleeps until a frame arrives. Use in a Worker thread to avoid blocking the main V8 thread.
|
|
246
|
+
|
|
247
|
+
## License
|
|
248
|
+
|
|
249
|
+
MIT. Links against FFmpeg (LGPL 2.1+).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,42 +1,40 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "memoir-node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Windows-native screen capture with Node.js bindings",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "https://github.com/lunavod/memoir-capture.git"
|
|
9
|
-
},
|
|
10
|
-
"os": ["win32"],
|
|
11
|
-
"cpu": ["x64"],
|
|
12
|
-
"engines": {
|
|
13
|
-
"node": ">=18"
|
|
14
|
-
},
|
|
15
|
-
"keywords": ["screen-capture", "windows", "native", "napi", "wgc", "ffmpeg", "recording"],
|
|
16
|
-
"main": "dist/index.js",
|
|
17
|
-
"types": "dist/index.d.ts",
|
|
18
|
-
"scripts": {
|
|
19
|
-
"build:native": "cmake-js build --CDMEMOIR_BUILD_NODE=ON --CDMEMOIR_BUILD_PYTHON=OFF --CDCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake --CDVCPKG_OVERLAY_TRIPLETS=cmake/triplets --CDVCPKG_TARGET_TRIPLET=x64-windows-release",
|
|
20
|
-
"build:ts": "tsc",
|
|
21
|
-
"build": "npm run build:native && tsc",
|
|
22
|
-
"rebuild": "cmake-js rebuild --CDMEMOIR_BUILD_NODE=ON --CDMEMOIR_BUILD_PYTHON=OFF --CDCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake --CDVCPKG_OVERLAY_TRIPLETS=cmake/triplets --CDVCPKG_TARGET_TRIPLET=x64-windows-release",
|
|
23
|
-
"test": "vitest run"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
]
|
|
42
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "memoir-node",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Windows-native screen capture with Node.js bindings",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/lunavod/memoir-capture.git"
|
|
9
|
+
},
|
|
10
|
+
"os": ["win32"],
|
|
11
|
+
"cpu": ["x64"],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"keywords": ["screen-capture", "windows", "native", "napi", "wgc", "ffmpeg", "recording"],
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build:native": "cmake-js build --CDMEMOIR_BUILD_NODE=ON --CDMEMOIR_BUILD_PYTHON=OFF --CDCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake --CDVCPKG_OVERLAY_TRIPLETS=cmake/triplets --CDVCPKG_TARGET_TRIPLET=x64-windows-release",
|
|
20
|
+
"build:ts": "tsc",
|
|
21
|
+
"build": "npm run build:native && tsc",
|
|
22
|
+
"rebuild": "cmake-js rebuild --CDMEMOIR_BUILD_NODE=ON --CDMEMOIR_BUILD_PYTHON=OFF --CDCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake --CDVCPKG_OVERLAY_TRIPLETS=cmake/triplets --CDVCPKG_TARGET_TRIPLET=x64-windows-release",
|
|
23
|
+
"test": "vitest run"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.0.0",
|
|
28
|
+
"cmake-js": "^7.0.0",
|
|
29
|
+
"node-addon-api": "^7.0.0",
|
|
30
|
+
"typescript": "^5.5.0",
|
|
31
|
+
"vitest": "^3.0.0"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist/*.js",
|
|
35
|
+
"dist/*.d.ts",
|
|
36
|
+
"build/Release/memoir_node.node",
|
|
37
|
+
"build/Release/*.dll",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
]
|
|
40
|
+
}
|