@mediaway/encoder 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
@@ -80,6 +80,35 @@ const mp4 = await session.finish(); // terminal: complete fMP4 bytes
80
80
  // -> write `mp4` to a file / stream
81
81
  ```
82
82
 
83
+ ## Streaming a long recording
84
+
85
+ `finish()` alone holds the whole recording in RAM. Call `pollBytes()` as you go and append each
86
+ chunk to a file: memory is then bounded by the poll cadence, not the recording's length.
87
+ `finish()` afterwards returns only the tail that was not polled yet.
88
+
89
+ ```ts
90
+ const out = openSync("recording.mp4", "w");
91
+ for (let i = 0; i < frames; i++) {
92
+ await session.writeFrame(frame(i));
93
+ writeSync(out, await session.pollBytes()); // empty buffer when nothing is ready
94
+ }
95
+ writeSync(out, await session.finish()); // the tail — concatenated, the file is complete
96
+ ```
97
+
98
+ ## Probing backends
99
+
100
+ `encoderSupport(codec, width, height)` reports every backend's availability **at the size you will
101
+ encode** — hardware encoders have minimum and maximum dimensions, so there is no resolution-free
102
+ form. Each row is `{ backend, state, pathClass }` (`pathClass` is `null` unless `state` is
103
+ `"supported"`). **It is costly**: it opens a throwaway session per backend, so call it when a
104
+ settings screen opens, never per frame.
105
+
106
+ ```ts
107
+ for (const row of await encoderSupport("h264", 1920, 1080)) {
108
+ console.log(row.backend, row.state, row.pathClass); // "nvenc" "supported" "cpu-upload"
109
+ }
110
+ ```
111
+
83
112
  ## API
84
113
 
85
114
  | Member | Notes |
@@ -89,7 +118,9 @@ const mp4 = await session.finish(); // terminal: complete fMP4 bytes
89
118
  | `openAutoEncoder(config)` | resolves a hardware backend or throws `EncoderUnavailableError` |
90
119
  | `EncodeSession.writeFrame(frame)` | async; the frame is copied — reuse the buffer |
91
120
  | `EncodeSession.writeFrameFromCameraCapture(capture)` / `writeFrameFromDesktopCapture(capture)` | async; polls+pushes a capture session directly, `false` if nothing was ready |
92
- | `EncodeSession.finish()` | terminal; returns the complete fMP4 `Buffer` |
121
+ | `EncodeSession.pollBytes()` | async; the fMP4 bytes ready now (empty `Buffer` when none) — the streaming exit; never ends the session |
122
+ | `EncodeSession.finish()` | terminal; returns the fMP4 `Buffer` **not yet taken by `pollBytes()`** — the whole file if never polled, only the tail if it was |
123
+ | `encoderSupport(codec, width, height)` | async, **costly**; per-backend `{ backend, state, pathClass }` at that resolution |
93
124
 
94
125
  ## License
95
126
 
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { type RawRational } from "@mediaway/ffi";
11
11
  import { MediawayError, type Rational } from "@mediaway/container";
12
- import { type CameraSession, type GpuDevice, type ScreenSession } from "@mediaway/device";
12
+ import { type CameraSession, type DesktopCaptureSession, type GpuDevice } from "@mediaway/device";
13
13
  export type { Rational } from "@mediaway/container";
14
14
  export { MediawayError } from "@mediaway/container";
15
15
  export type PixelFormat = "nv12" | "bgra8" | "rgba8" | "i420" | "yuy2";
@@ -69,14 +69,29 @@ export declare class EncodeSession {
69
69
  */
70
70
  writeFrameFromCameraCapture(capture: CameraSession): Promise<boolean>;
71
71
  /**
72
- * Same shape as `writeFrameFromCameraCapture`, for a `ScreenSession`
72
+ * Same shape as `writeFrameFromCameraCapture`, for a `ScreenSession` or `WindowSession`
73
73
  * (`@mediaway/device`) instead of Camera. GPU frames pass through
74
74
  * Zero-Copy: the polled frame's GPU handle moves straight into the encoder
75
75
  * with no CPU copy — this is the real way to consume Screen frames (see
76
76
  * `ScreenSession.pollFrame()`'s own doc: it never copies pixels out).
77
77
  */
78
- writeFrameFromDesktopCapture(capture: ScreenSession): Promise<boolean>;
79
- /** Flush the encoder + muxer and return the complete fMP4 bytes. Terminal. */
78
+ writeFrameFromDesktopCapture(capture: DesktopCaptureSession): Promise<boolean>;
79
+ /**
80
+ * Take the fMP4 bytes that are ready now, without ending the session — the streaming exit
81
+ * (`adr/pipeline/0007` §1). Call it as often as you like (after every `writeFrame`, on a timer,
82
+ * or never) and the session's memory stays bounded by the poll cadence instead of growing with
83
+ * the recording: a long capture polled into a file holds a fragment, not the whole file.
84
+ *
85
+ * Resolves to an empty buffer when nothing is ready, which is indistinguishable from "already
86
+ * drained" — track your own running total if you need the difference. Polling never finishes
87
+ * the stream: the last fragments only appear in `finish()`'s result.
88
+ */
89
+ pollBytes(): Promise<Buffer>;
90
+ /**
91
+ * Flush the encoder + muxer and return the fMP4 bytes **not yet taken by `pollBytes()`**:
92
+ * the complete file for a session that was never polled, only its tail for one that was.
93
+ * A streaming caller appends this to what it already wrote. Terminal.
94
+ */
80
95
  finish(): Promise<Buffer>;
81
96
  /** Idempotent; no-op after finish(). Frees the native handle on error paths. */
82
97
  close(): void;
@@ -84,6 +99,37 @@ export declare class EncodeSession {
84
99
  /** Pick the best available encoder for `config` and open a session on it.
85
100
  * Throws EncoderUnavailableError when no backend exists on this machine. */
86
101
  export declare function openAutoEncoder(config: AutoVideoEncodeConfig): Promise<EncodeSession>;
102
+ /** A codec the encoder probe accepts. */
103
+ export type ProbeCodec = VideoCodec | "aac" | "opus";
104
+ /** Which encode backend a probe row describes. */
105
+ export type EncodeBackend = "os" | "nvenc" | "quicksync" | "amf" | "vulkan" | "software" | "unknown";
106
+ /** Whether a backend or codec is usable right now. */
107
+ export type SupportState = "supported" | "not-implemented" | "no-device" | "unknown";
108
+ /** The cheapest data path a supported encoder reached. */
109
+ export type EncodePathClass = "zero-copy" | "gpu-copy" | "cpu-upload" | "readback" | "software" | "unknown";
110
+ /** One backend row of `encoderSupport()`. */
111
+ export interface EncoderCapability {
112
+ backend: EncodeBackend;
113
+ state: SupportState;
114
+ /** The data path cost — `null` unless `state === "supported"`. */
115
+ pathClass: EncodePathClass | null;
116
+ }
117
+ /** Map a `mediaway_support_state_t` value to its name (shared with `@mediaway/decoder`). */
118
+ export declare function supportStateName(state: number): SupportState;
119
+ /**
120
+ * Probe every encode backend for `codec` **at `width` x `height`**.
121
+ *
122
+ * Encoder support is resolution-dependent — a hardware encoder has minimum and maximum
123
+ * dimensions, so a backend that works at one size says nothing about another — which is why there
124
+ * is deliberately no resolution-free form. Pass the size you will encode.
125
+ *
126
+ * **Costly:** it opens a throwaway session per backend (a real MFT / VA-API / VideoToolbox
127
+ * session each). Call it when a settings screen opens, never per frame or in a loop. A platform
128
+ * with no per-backend selection reports an empty list.
129
+ *
130
+ * @throws MediawayError status 5 for a zero width or height.
131
+ */
132
+ export declare function encoderSupport(codec: ProbeCodec, width: number, height: number): Promise<EncoderCapability[]>;
87
133
  export type AudioCodec = "aac";
88
134
  export type SampleFormat = "s16" | "s32" | "f32";
89
135
  export interface AudioEncodeConfig {
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * `EncodeSession` directly; `finish()` is terminal. Video only — there is no
8
8
  * audio encoder in the ABI.
9
9
  */
10
- import { MwAudioEncodeConfig, MwAudioFrameView, MwAudioPacket, MwAudioStreamInfo, MwPipelineFrame, MwRational, pipeline, copyBytes, } from "@mediaway/ffi";
10
+ import { MwAudioEncodeConfig, MwAudioFrameView, MwAudioPacket, MwAudioStreamInfo, MwEncoderCapability, MwPipelineFrame, MwRational, PIPELINE_CODEC, pipeline, copyBytes, decodeArray, } from "@mediaway/ffi";
11
11
  import { MediawayError } from "@mediaway/container";
12
12
  import { NATIVE_HANDLE } from "@mediaway/device";
13
13
  export { MediawayError } from "@mediaway/container";
@@ -122,7 +122,7 @@ export class EncodeSession {
122
122
  return wrote[0];
123
123
  }
124
124
  /**
125
- * Same shape as `writeFrameFromCameraCapture`, for a `ScreenSession`
125
+ * Same shape as `writeFrameFromCameraCapture`, for a `ScreenSession` or `WindowSession`
126
126
  * (`@mediaway/device`) instead of Camera. GPU frames pass through
127
127
  * Zero-Copy: the polled frame's GPU handle moves straight into the encoder
128
128
  * with no CPU copy — this is the real way to consume Screen frames (see
@@ -133,7 +133,33 @@ export class EncodeSession {
133
133
  checkPipeline(pipeline.sessionWriteFrameFromDesktopCapture(this.handle, capture[NATIVE_HANDLE](), wrote));
134
134
  return wrote[0];
135
135
  }
136
- /** Flush the encoder + muxer and return the complete fMP4 bytes. Terminal. */
136
+ /**
137
+ * Take the fMP4 bytes that are ready now, without ending the session — the streaming exit
138
+ * (`adr/pipeline/0007` §1). Call it as often as you like (after every `writeFrame`, on a timer,
139
+ * or never) and the session's memory stays bounded by the poll cadence instead of growing with
140
+ * the recording: a long capture polled into a file holds a fragment, not the whole file.
141
+ *
142
+ * Resolves to an empty buffer when nothing is ready, which is indistinguishable from "already
143
+ * drained" — track your own running total if you need the difference. Polling never finishes
144
+ * the stream: the last fragments only appear in `finish()`'s result.
145
+ */
146
+ async pollBytes() {
147
+ if (!this.handle)
148
+ throw new MediawayError(7, "session already finished or not open");
149
+ const outData = [null];
150
+ const outLen = [0];
151
+ checkPipeline(pipeline.sessionPollBytes(this.handle, outData, outLen));
152
+ if (outLen[0] === 0)
153
+ return Buffer.alloc(0);
154
+ const data = copyBytes(outData[0], outLen[0]);
155
+ pipeline.bufferFree(outData[0], outLen[0]);
156
+ return data;
157
+ }
158
+ /**
159
+ * Flush the encoder + muxer and return the fMP4 bytes **not yet taken by `pollBytes()`**:
160
+ * the complete file for a session that was never polled, only its tail for one that was.
161
+ * A streaming caller appends this to what it already wrote. Terminal.
162
+ */
137
163
  async finish() {
138
164
  const outData = [null];
139
165
  const outLen = [0];
@@ -166,6 +192,59 @@ export async function openAutoEncoder(config) {
166
192
  throw new MediawayError(11, "encoder open returned no handle");
167
193
  return new EncodeSession(config, outEncoder[0]);
168
194
  }
195
+ const BACKENDS = {
196
+ 0: "os",
197
+ 1: "nvenc",
198
+ 2: "quicksync",
199
+ 3: "amf",
200
+ 4: "vulkan",
201
+ 5: "software",
202
+ };
203
+ const PATH_CLASSES = {
204
+ 1: "zero-copy",
205
+ 2: "gpu-copy",
206
+ 3: "cpu-upload",
207
+ 4: "readback",
208
+ 5: "software",
209
+ };
210
+ const SUPPORT_STATES = {
211
+ 0: "supported",
212
+ 1: "not-implemented",
213
+ 2: "no-device",
214
+ };
215
+ /** Map a `mediaway_support_state_t` value to its name (shared with `@mediaway/decoder`). */
216
+ export function supportStateName(state) {
217
+ return SUPPORT_STATES[state] ?? "unknown";
218
+ }
219
+ /**
220
+ * Probe every encode backend for `codec` **at `width` x `height`**.
221
+ *
222
+ * Encoder support is resolution-dependent — a hardware encoder has minimum and maximum
223
+ * dimensions, so a backend that works at one size says nothing about another — which is why there
224
+ * is deliberately no resolution-free form. Pass the size you will encode.
225
+ *
226
+ * **Costly:** it opens a throwaway session per backend (a real MFT / VA-API / VideoToolbox
227
+ * session each). Call it when a settings screen opens, never per frame or in a loop. A platform
228
+ * with no per-backend selection reports an empty list.
229
+ *
230
+ * @throws MediawayError status 5 for a zero width or height.
231
+ */
232
+ export async function encoderSupport(codec, width, height) {
233
+ const outRows = [null];
234
+ const outCount = [0];
235
+ checkPipeline(pipeline.encoderSupportAt(PIPELINE_CODEC[codec], width, height, outRows, outCount));
236
+ const count = outCount[0];
237
+ if (count === 0)
238
+ return [];
239
+ const rows = decodeArray(outRows[0], MwEncoderCapability, count);
240
+ const result = rows.map((row) => ({
241
+ backend: BACKENDS[row.backend] ?? "unknown",
242
+ state: supportStateName(row.state),
243
+ pathClass: row.state === 0 ? (PATH_CLASSES[row.path_class] ?? "unknown") : null,
244
+ }));
245
+ pipeline.encoderSupportFree(outRows[0], count);
246
+ return result;
247
+ }
169
248
  /**
170
249
  * An opened auto audio encoder — the session IS the encoder (ABI v2,
171
250
  * adr/0003): single-step open, no intermediate handle, no consumption trap;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediaway/encoder",
3
- "version": "0.1.6",
3
+ "version": "0.2.1",
4
4
  "description": "Rust-based, lightweight, fast — automatic hardware video encoding to fragmented MP4 for Node.js (NVENC / Vulkan Video / QuickSync)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",