@mediaway/encoder 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 +32 -1
- package/dist/index.d.ts +50 -4
- package/dist/index.js +82 -3
- package/package.json +1 -1
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.
|
|
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
|
|
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:
|
|
79
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|