npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

memoir-node

v0.2.1

Published

Windows-native screen capture with Node.js bindings

Readme

memoir-node

Windows-native screen capture module with Node.js bindings for real-time frame analysis and deterministic replay recording.

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.

Features

  • WGC capture — continuous frame capture from any window or monitor
  • Zero-copy Buffers — BGRA frames delivered as Node.js Buffers without copying
  • Hardware-accelerated recording — lossless HEVC encoding via NVENC, AMF, or x265 fallback
  • Binary metadata.meta sidecar with per-frame keyboard state, timestamps, and frame IDs
  • Dynamic recording — start/stop recording without restarting capture
  • Frame-accurate keyboard — key state snapshot at the exact moment each frame is accepted
  • Pure TypeScript meta reader/writer — read and write .meta files without the native addon
  • Synchronous blocking API — designed for Worker threads running tick loops

Requirements

  • Windows 10 1903+
  • Node.js 18+
  • x64 architecture
  • NVIDIA GPU (NVENC), AMD GPU (AMF), or CPU-only (x265) for recording

Installation

npm install memoir-node

Quick Start

Capture frames

import { CaptureEngine } from 'memoir-node'

const engine = new CaptureEngine({
  target: { type: 'monitor', index: 0 },  // primary monitor
  maxFps: 10,
})
engine.start()

const frame = engine.getNextFrame(5000)
if (frame) {
  console.log(`Frame ${frame.frameId}: ${frame.width}x${frame.height}`)
  console.log(`Keys: ${frame.keyboardMask.toString(16)}`)
  // frame.data is a Buffer with BGRA pixels
  frame.release()
}

engine.stop()

Capture a specific window

const engine = new CaptureEngine({
  target: { type: 'windowTitle', pattern: '(?i)notepad' },
  maxFps: 30,
})

Or by executable name:

const engine = new CaptureEngine({
  target: { type: 'windowExe', pattern: 'notepad\\.exe' },
  maxFps: 30,
})

Record to MP4

const engine = new CaptureEngine({
  target: { type: 'monitor', index: 0 },
  maxFps: 10,
  recordWidth: 1920,
  recordHeight: 1080,
})
engine.start()

const info = engine.startRecording('session_001')
console.log(`Recording to ${info.videoPath}`)  // session_001.mp4

for (let i = 0; i < 100; i++) {
  const frame = engine.getNextFrame(5000)
  if (frame) frame.release()
}

engine.stopRecording()  // finalizes .mp4 + .meta
engine.stop()

Read metadata

import { readMeta, pressedKeys } from 'memoir-node'

const meta = readMeta('session_001.meta')
console.log(`Keys tracked: ${meta.keys.map(k => k.name)}`)

for (const row of meta.rows) {
  console.log(`Frame ${row.frameId}: ${pressedKeys(row, meta.keys).join(', ')}`)
}

Write metadata (for synthetic replays)

import { writeMeta, type MetaKeyEntry, type MetaRow } from 'memoir-node'

const keys: MetaKeyEntry[] = [
  { bit: 0, vk: 0x57, name: 'W' },
  { bit: 1, vk: 0x41, name: 'A' },
]

const rows: MetaRow[] = [{
  frameId: 0n, recordFrameIndex: 0n,
  captureQpc: 0n, hostAcceptQpc: 0n,
  keyboardMask: 0b01n,
  width: 1920, height: 1080, analysisStride: 7680, flags: 0,
}]

writeMeta('synthetic.meta', keys, rows)

Worker thread usage (Electron)

// tick-worker.ts — runs in Worker thread
import { CaptureEngine } from 'memoir-node'
import { parentPort } from 'worker_threads'

const engine = new CaptureEngine({
  target: { type: 'windowExe', pattern: 'myapp\\.exe' },
  maxFps: 10,
})
engine.start()

while (true) {
  const frame = engine.getNextFrame(2000)
  if (!frame) {
    const err = engine.lastError()
    if (err) { parentPort!.postMessage({ type: 'error', error: err }); break }
    continue
  }

  // frame.data is a Buffer — process it here, don't post it to main thread
  parentPort!.postMessage({
    type: 'tick',
    frameId: frame.frameId,
    width: frame.width,
    height: frame.height,
  })

  frame.release()
}

API Reference

new CaptureEngine(options)

| Option | Default | Description | |--------|---------|-------------| | target | required | { type: 'monitor', index }, { type: 'windowTitle', pattern }, or { type: 'windowExe', pattern } | | maxFps | 10 | Maximum accepted frame rate | | queueCapacity | 1 | Bounded queue size | | captureCursor | false | Include cursor in capture | | keys | 40-key gaming set | Array of { bit, vk, name } for keyboard tracking | | recordWidth | 1920 | Recording output width | | recordHeight | 1080 | Recording output height | | recordGop | 1 | GOP size (1 = all-intra) |

Methods

| Method | Returns | Description | |--------|---------|-------------| | start() | void | Initialize D3D11, create WGC session, begin capturing | | stop() | void | Stop capture and any active recording | | getNextFrame(timeoutMs?) | FramePacket \| null | Block until frame available. -1 = forever, 0 = poll | | startRecording(basePath, encoder?) | RecordingInfo | Start recording to basePath.mp4 + basePath.meta | | startRecording(opts) | RecordingInfo | Start recording with { path, videoName, metaName, encoder? } | | stopRecording() | void | Finalize recording | | isRecording() | boolean | Whether a recording session is active | | stats() | EngineStats | Live counters | | lastError() | string \| null | Last non-fatal error |

FramePacket

| Property | Type | Description | |----------|------|-------------| | frameId | number | Monotonic frame ID | | data | Buffer | BGRA pixels, length = stride * height | | keyboardMask | bigint | 64-bit key state bitmask | | captureQpc | bigint | WGC timestamp (100ns units) | | hostAcceptQpc | bigint | Host QPC when frame was accepted | | width, height, stride | number | Frame dimensions | | released | boolean | Whether pixel memory has been freed |

Call frame.release() when done to free pixel memory.

Recording

Encoding: lossless HEVC (QP=0), YUV 4:4:4. Encoder selected automatically: hevc_nvenc (NVIDIA) → hevc_amf (AMD) → libx265 (software). Force a specific encoder:

const info = engine.startRecording('session', 'libx265')
console.log(info.codec) // "libx265"

Meta utilities

import { readMeta, writeMeta, isPressed, pressedKeys, synthesizeKeyEvents } from 'memoir-node'

// Read
const meta = readMeta('session.meta')

// Check keys
isPressed(meta.rows[0], 'W', meta.keys)       // boolean
pressedKeys(meta.rows[0], meta.keys)           // string[]

// Generate key events for replay
const events = synthesizeKeyEvents(meta.rows, meta.keys)
// [{ frame: 0n, type: 'keyDown', key: 'W' }, ...]

Architecture

WGC FrameArrived (thread pool)
  │
  ├─ FPS limiter → drop if too soon
  ├─ Queue check → drop if full (drop-new policy)
  │
  ├─ Accept: assign frame_id, snapshot keyboard
  ├─ GPU→CPU: CopyResource → staging → Map → memcpy → Buffer (zero-copy)
  ├─ Recording: swscale (BGRA→YUV444) → HEVC encoder → MP4
  └─ Enqueue → Node.js consumer

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.

License

MIT. Links against FFmpeg (LGPL 2.1+).