@mediaway/encoder 0.1.5 → 0.1.8

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
@@ -8,6 +8,34 @@ session you push frames into.
8
8
  Windows is the verified platform. Software fallbacks and other platforms are
9
9
  explicit follow-up work — see the [Mediaway roadmap](https://github.com/nyxways/mediaway).
10
10
 
11
+ Video/audio **decode** moved to its own peer package, [`@mediaway/decoder`](../decoder)
12
+ — it is not re-exported here anymore.
13
+
14
+ ## Capture-to-encode bridge
15
+
16
+ `EncodeSession.writeFrameFromCameraCapture(capture)` /
17
+ `.writeFrameFromDesktopCapture(capture)` poll a `CameraSession`/`ScreenSession`
18
+ (`@mediaway/device`) and push straight into the session — no intermediate
19
+ `VideoFrame`, no CPU copy for Screen's GPU frames:
20
+
21
+ ```ts
22
+ import { AutoVideoEncodeConfig, openAutoEncoder } from "@mediaway/encoder";
23
+ import { GpuDevice, openScreenCapture } from "@mediaway/device";
24
+
25
+ const gpuDevice = await GpuDevice.create();
26
+ const screen = await openScreenCapture({ timeBase: { num: 1, den: 30 }, gpuDevice });
27
+
28
+ const config = AutoVideoEncodeConfig.defaults("h264", screen.width, screen.height, screen.timeBase);
29
+ config.gpuDevice = gpuDevice; // Zero-Copy GPU input, same device as capture
30
+ config.pixelFormat = "bgra8"; // DXGI Desktop Duplication's native format
31
+ const session = await openAutoEncoder(config);
32
+
33
+ while (recording) {
34
+ await session.writeFrameFromDesktopCapture(screen); // returns false if nothing was ready yet
35
+ }
36
+ const mp4 = await session.finish();
37
+ ```
38
+
11
39
  ## Install
12
40
 
13
41
  ```bash
@@ -57,8 +85,10 @@ const mp4 = await session.finish(); // terminal: complete fMP4 bytes
57
85
  | Member | Notes |
58
86
  | --- | --- |
59
87
  | `AutoVideoEncodeConfig.defaults(codec, width, height, timeBase)` | sensible defaults for the resolution; `codec` is `"h264" \| "hevc" \| "av1" \| "vp9"` |
88
+ | `AutoVideoEncodeConfig.gpuDevice` | a `GpuDevice` (`@mediaway/device`) for Zero-Copy GPU input; pair with `pixelFormat: "bgra8"` for Screen |
60
89
  | `openAutoEncoder(config)` | resolves a hardware backend or throws `EncoderUnavailableError` |
61
90
  | `EncodeSession.writeFrame(frame)` | async; the frame is copied — reuse the buffer |
91
+ | `EncodeSession.writeFrameFromCameraCapture(capture)` / `writeFrameFromDesktopCapture(capture)` | async; polls+pushes a capture session directly, `false` if nothing was ready |
62
92
  | `EncodeSession.finish()` | terminal; returns the complete fMP4 `Buffer` |
63
93
 
64
94
  ## License
package/dist/index.d.ts CHANGED
@@ -9,6 +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
13
  export type { Rational } from "@mediaway/container";
13
14
  export { MediawayError } from "@mediaway/container";
14
15
  export type PixelFormat = "nv12" | "bgra8" | "rgba8" | "i420" | "yuy2";
@@ -33,8 +34,14 @@ export declare class AutoVideoEncodeConfig {
33
34
  timeBase: Rational;
34
35
  bitrateBps?: number;
35
36
  pixelFormat?: PixelFormat;
37
+ /** Zero-Copy GPU input: the same device a Screen capture session was opened
38
+ * with (or any `GpuDevice`) — set together with `pixelFormat: "bgra8"` for
39
+ * `writeFrameFromDesktopCapture()`'s GPU frames (DXGI delivers BGRA8, not
40
+ * the NV12 default). Leave unset for CPU input (Camera, or manual
41
+ * `writeFrame()` with CPU-storage frames). */
42
+ gpuDevice?: GpuDevice;
36
43
  private constructor();
37
- /** The ABI's defaults (backend-default bitrate, NV12 input). */
44
+ /** The ABI's defaults (backend-default bitrate, NV12 input, CPU-only). */
38
45
  static defaults(codec: VideoCodec, width: number, height: number, timeBase: Rational): AutoVideoEncodeConfig;
39
46
  toAbi(): RawRational;
40
47
  }
@@ -53,6 +60,22 @@ export declare class EncodeSession {
53
60
  private finished;
54
61
  constructor(config: AutoVideoEncodeConfig, encoderHandle: unknown);
55
62
  writeFrame(frame: VideoFrame): Promise<void>;
63
+ /**
64
+ * Poll one frame from `capture` (a `CameraSession`, `@mediaway/device`) and
65
+ * push it straight into this session — no intermediate `VideoFrame`, no
66
+ * extra copy (`adr/pipeline/0005-capture-encode-bridge-c-abi.md`). Returns
67
+ * `false` when nothing was ready yet (not an error) — mirrors
68
+ * `CameraSession.pollFrame()`'s own null-vs-error shape.
69
+ */
70
+ writeFrameFromCameraCapture(capture: CameraSession): Promise<boolean>;
71
+ /**
72
+ * Same shape as `writeFrameFromCameraCapture`, for a `ScreenSession`
73
+ * (`@mediaway/device`) instead of Camera. GPU frames pass through
74
+ * Zero-Copy: the polled frame's GPU handle moves straight into the encoder
75
+ * with no CPU copy — this is the real way to consume Screen frames (see
76
+ * `ScreenSession.pollFrame()`'s own doc: it never copies pixels out).
77
+ */
78
+ writeFrameFromDesktopCapture(capture: ScreenSession): Promise<boolean>;
56
79
  /** Flush the encoder + muxer and return the complete fMP4 bytes. Terminal. */
57
80
  finish(): Promise<Buffer>;
58
81
  /** Idempotent; no-op after finish(). Frees the native handle on error paths. */
@@ -129,4 +152,3 @@ export declare class AudioEncoder {
129
152
  /** Always safe — no handle-consumption trap on this surface (adr/0003). */
130
153
  close(): void;
131
154
  }
132
- export * from "./decode.js";
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { MwAudioEncodeConfig, MwAudioFrameView, MwAudioPacket, MwAudioStreamInfo, MwPipelineFrame, MwRational, pipeline, copyBytes, } from "@mediaway/ffi";
11
11
  import { MediawayError } from "@mediaway/container";
12
+ import { NATIVE_HANDLE } from "@mediaway/device";
12
13
  export { MediawayError } from "@mediaway/container";
13
14
  /** No encode backend is openable for this config — expected on machines
14
15
  * without a usable encoder; catch it and exit gracefully. */
@@ -21,13 +22,19 @@ export class AutoVideoEncodeConfig {
21
22
  timeBase;
22
23
  bitrateBps; // 0 / undefined = backend default
23
24
  pixelFormat;
25
+ /** Zero-Copy GPU input: the same device a Screen capture session was opened
26
+ * with (or any `GpuDevice`) — set together with `pixelFormat: "bgra8"` for
27
+ * `writeFrameFromDesktopCapture()`'s GPU frames (DXGI delivers BGRA8, not
28
+ * the NV12 default). Leave unset for CPU input (Camera, or manual
29
+ * `writeFrame()` with CPU-storage frames). */
30
+ gpuDevice;
24
31
  constructor(codec, width, height, timeBase) {
25
32
  this.codec = codec;
26
33
  this.width = width;
27
34
  this.height = height;
28
35
  this.timeBase = timeBase;
29
36
  }
30
- /** The ABI's defaults (backend-default bitrate, NV12 input). */
37
+ /** The ABI's defaults (backend-default bitrate, NV12 input, CPU-only). */
31
38
  static defaults(codec, width, height, timeBase) {
32
39
  return new AutoVideoEncodeConfig(codec, width, height, timeBase);
33
40
  }
@@ -39,6 +46,8 @@ export class AutoVideoEncodeConfig {
39
46
  const fmtMap = { nv12: 0, i420: 1, bgra8: 2, rgba8: 3, yuy2: 4 };
40
47
  if (this.pixelFormat !== undefined)
41
48
  raw.pixel_format = fmtMap[this.pixelFormat] ?? 0;
49
+ if (this.gpuDevice !== undefined)
50
+ raw.gpu_device = this.gpuDevice[NATIVE_HANDLE]();
42
51
  return raw;
43
52
  }
44
53
  }
@@ -100,6 +109,30 @@ export class EncodeSession {
100
109
  };
101
110
  checkPipeline(pipeline.sessionWriteFrame(this.handle, raw));
102
111
  }
112
+ /**
113
+ * Poll one frame from `capture` (a `CameraSession`, `@mediaway/device`) and
114
+ * push it straight into this session — no intermediate `VideoFrame`, no
115
+ * extra copy (`adr/pipeline/0005-capture-encode-bridge-c-abi.md`). Returns
116
+ * `false` when nothing was ready yet (not an error) — mirrors
117
+ * `CameraSession.pollFrame()`'s own null-vs-error shape.
118
+ */
119
+ async writeFrameFromCameraCapture(capture) {
120
+ const wrote = [false];
121
+ checkPipeline(pipeline.sessionWriteFrameFromCameraCapture(this.handle, capture[NATIVE_HANDLE](), wrote));
122
+ return wrote[0];
123
+ }
124
+ /**
125
+ * Same shape as `writeFrameFromCameraCapture`, for a `ScreenSession`
126
+ * (`@mediaway/device`) instead of Camera. GPU frames pass through
127
+ * Zero-Copy: the polled frame's GPU handle moves straight into the encoder
128
+ * with no CPU copy — this is the real way to consume Screen frames (see
129
+ * `ScreenSession.pollFrame()`'s own doc: it never copies pixels out).
130
+ */
131
+ async writeFrameFromDesktopCapture(capture) {
132
+ const wrote = [false];
133
+ checkPipeline(pipeline.sessionWriteFrameFromDesktopCapture(this.handle, capture[NATIVE_HANDLE](), wrote));
134
+ return wrote[0];
135
+ }
103
136
  /** Flush the encoder + muxer and return the complete fMP4 bytes. Terminal. */
104
137
  async finish() {
105
138
  const outData = [null];
@@ -226,4 +259,3 @@ export class AudioEncoder {
226
259
  }
227
260
  }
228
261
  }
229
- export * from "./decode.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediaway/encoder",
3
- "version": "0.1.5",
3
+ "version": "0.1.8",
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",
@@ -27,7 +27,8 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@mediaway/ffi": "^0.1.0",
30
- "@mediaway/container": "^0.1.0"
30
+ "@mediaway/container": "^0.1.0",
31
+ "@mediaway/device": "^0.1.0"
31
32
  },
32
33
  "devDependencies": {
33
34
  "typescript": "^5.6.0"
package/dist/decode.d.ts DELETED
@@ -1,106 +0,0 @@
1
- /**
2
- * @mediaway/encoder — pipeline capability: auto video decode + Opus audio decode.
3
- *
4
- * Wraps the mediaway-ffi C ABI's decode sessions (adr/0004-auto-decode-c-abi.md,
5
- * adr/pipeline/0006-audio-decode-c-abi.md) — the same "C ABI real, no
6
- * language binding wired" gap the container format series closed for
7
- * mux/demux, closed here for decode. Both sessions mirror
8
- * `openAutoEncoder()`/`AudioEncoder`'s single-step shape (the handle IS the
9
- * decoder, no consumption trap); `NO_BACKEND` throws
10
- * `DecoderUnavailableError`, an expected/graceful outcome.
11
- */
12
- import { MediawayError, type Rational } from "@mediaway/container";
13
- import { type PixelFormat, type VideoCodec } from "./index.js";
14
- /** No decoder backend is openable for this config — expected on machines
15
- * without a usable decoder; catch it and exit gracefully. */
16
- export declare class DecoderUnavailableError extends MediawayError {
17
- }
18
- export interface VideoDecodeConfig {
19
- codec: VideoCodec;
20
- /** Expected; may be refined from the bitstream. */
21
- width: number;
22
- height: number;
23
- timeBase: Rational;
24
- /** Preferred output format when the backend converts. */
25
- pixelFormat?: PixelFormat;
26
- /** AVCC / SPS-PPS codec config, required at open time — not supplied via
27
- * the first pushed packet (adr/0004 §1: the muxer-track analogy does not
28
- * hold for the wrapped decoder). */
29
- extraData?: Buffer;
30
- }
31
- /** Input to `DecodeSession.pushPacket`/`AudioDecodeSession.pushPacket` — a
32
- * pipeline-scoped packet view, distinct from `@mediaway/container`'s
33
- * `Packet` (adr/pipeline/0006 §4: shared by both video and audio decode). */
34
- export interface DecodePacket {
35
- pts: number;
36
- dts?: number;
37
- duration?: number;
38
- keyframe?: boolean;
39
- /** For audio: an empty payload is Opus's packet-loss-concealment hint for
40
- * a lost frame, not an error — pass it whenever a frame is known lost. */
41
- payload: Buffer;
42
- }
43
- export interface DecodedVideoFrame {
44
- pts: number;
45
- duration: number;
46
- width: number;
47
- height: number;
48
- pixelFormat: PixelFormat;
49
- data: Buffer;
50
- }
51
- export interface DecodedAudioFrame {
52
- pts: number;
53
- duration: number;
54
- sampleRate: number;
55
- channels: number;
56
- data: Buffer;
57
- }
58
- /**
59
- * The best available video decode session for a config — the handle IS the
60
- * decoder (single-step open, no consumption trap, mirrors `openAutoEncoder`'s
61
- * `NO_BACKEND` handling). CPU output only (GPU decode output is deferred,
62
- * adr/0004 §1/§5).
63
- */
64
- export declare class DecodeSession {
65
- private handle;
66
- private constructor();
67
- /** Open the best available video decoder for `config`. Throws
68
- * DecoderUnavailableError when no decode backend exists on this machine. */
69
- static open(config: VideoDecodeConfig): Promise<DecodeSession>;
70
- /** Push one compressed packet. May produce zero or more frames (drain via
71
- * `pollFrame()`). */
72
- pushPacket(packet: DecodePacket): Promise<void>;
73
- /** Pull the next decoded frame, if one is ready. null is a valid "nothing
74
- * ready" result, not an error. */
75
- pollFrame(): Promise<DecodedVideoFrame | null>;
76
- /** Signal end of input; drain the remaining frames with pollFrame(). */
77
- flush(): Promise<void>;
78
- /** Always safe — no handle-consumption trap on this surface. */
79
- close(): void;
80
- }
81
- /**
82
- * An Opus audio decode session — the handle IS the decoder (adr/pipeline/0006,
83
- * mirrors `DecodeSession`'s video shape; no muxer to wire, no consumption
84
- * trap). Cross-platform (mediaway-sw, no OS dependency), unlike
85
- * `DecodeSession`'s Windows-only WMF backend.
86
- */
87
- export declare class AudioDecodeSession {
88
- readonly sampleRate: number;
89
- readonly channels: number;
90
- private handle;
91
- private constructor();
92
- /** Open an Opus decode session. Throws DecoderUnavailableError when no
93
- * decode backend exists on this machine. */
94
- static open(sampleRate: number, channels: number, timeBase: Rational): Promise<AudioDecodeSession>;
95
- /** Push one compressed Opus packet. An empty payload is Opus's
96
- * packet-loss-concealment hint for a lost frame, not an error. May produce
97
- * zero or more frames (drain via pollFrame()). */
98
- pushPacket(packet: DecodePacket): Promise<void>;
99
- /** Pull the next decoded PCM frame, if one is ready. null is a valid
100
- * "nothing ready" result, not an error. */
101
- pollFrame(): Promise<DecodedAudioFrame | null>;
102
- /** Signal end of input; drain the remaining frames with pollFrame(). */
103
- flush(): Promise<void>;
104
- /** Always safe — no handle-consumption trap on this surface. */
105
- close(): void;
106
- }
package/dist/decode.js DELETED
@@ -1,165 +0,0 @@
1
- /**
2
- * @mediaway/encoder — pipeline capability: auto video decode + Opus audio decode.
3
- *
4
- * Wraps the mediaway-ffi C ABI's decode sessions (adr/0004-auto-decode-c-abi.md,
5
- * adr/pipeline/0006-audio-decode-c-abi.md) — the same "C ABI real, no
6
- * language binding wired" gap the container format series closed for
7
- * mux/demux, closed here for decode. Both sessions mirror
8
- * `openAutoEncoder()`/`AudioEncoder`'s single-step shape (the handle IS the
9
- * decoder, no consumption trap); `NO_BACKEND` throws
10
- * `DecoderUnavailableError`, an expected/graceful outcome.
11
- */
12
- import { pipeline, copyBytes, } from "@mediaway/ffi";
13
- import { MediawayError } from "@mediaway/container";
14
- import { checkPipeline } from "./index.js";
15
- /** No decoder backend is openable for this config — expected on machines
16
- * without a usable decoder; catch it and exit gracefully. */
17
- export class DecoderUnavailableError extends MediawayError {
18
- }
19
- const codecMap = { h264: 0, hevc: 1, av1: 2, vp9: 3 };
20
- const pixelFmtMap = { nv12: 0, i420: 1, bgra8: 2, rgba8: 3, yuy2: 4 };
21
- const pixelFmtRev = ["nv12", "i420", "bgra8", "rgba8", "yuy2"];
22
- /**
23
- * The best available video decode session for a config — the handle IS the
24
- * decoder (single-step open, no consumption trap, mirrors `openAutoEncoder`'s
25
- * `NO_BACKEND` handling). CPU output only (GPU decode output is deferred,
26
- * adr/0004 §1/§5).
27
- */
28
- export class DecodeSession {
29
- handle;
30
- constructor(handle) {
31
- this.handle = handle;
32
- }
33
- /** Open the best available video decoder for `config`. Throws
34
- * DecoderUnavailableError when no decode backend exists on this machine. */
35
- static async open(config) {
36
- const extraData = config.extraData ?? Buffer.alloc(0);
37
- const raw = pipeline.decodeConfigNew(codecMap[config.codec] ?? 0, config.width, config.height, { num: BigInt(config.timeBase.num), den: config.timeBase.den }, extraData.length > 0 ? extraData : null, extraData.length);
38
- if (config.pixelFormat !== undefined)
39
- raw.pixel_format = pixelFmtMap[config.pixelFormat] ?? 0;
40
- const out = [null];
41
- checkPipeline(pipeline.decodeSessionOpen(raw, out), DecoderUnavailableError);
42
- if (!out[0])
43
- throw new MediawayError(11, "decode session open returned no handle");
44
- return new DecodeSession(out[0]);
45
- }
46
- /** Push one compressed packet. May produce zero or more frames (drain via
47
- * `pollFrame()`). */
48
- async pushPacket(packet) {
49
- const raw = {
50
- stream_id: 0,
51
- pts: BigInt(packet.pts),
52
- dts: BigInt(packet.dts ?? packet.pts),
53
- duration: BigInt(packet.duration ?? 0),
54
- is_keyframe: packet.keyframe ?? false,
55
- is_discard: false,
56
- payload: packet.payload,
57
- payload_len: packet.payload.length,
58
- };
59
- checkPipeline(pipeline.decodeSessionPushPacket(this.handle, raw));
60
- }
61
- /** Pull the next decoded frame, if one is ready. null is a valid "nothing
62
- * ready" result, not an error. */
63
- async pollFrame() {
64
- const raw = {};
65
- const has = [false];
66
- checkPipeline(pipeline.decodeSessionPollFrame(this.handle, raw, has));
67
- if (!has[0])
68
- return null;
69
- const data = copyBytes(raw.data, raw.data_len);
70
- pipeline.decodedVideoFrameFree(raw);
71
- return {
72
- pts: Number(raw.pts),
73
- duration: Number(raw.duration),
74
- width: raw.width,
75
- height: raw.height,
76
- pixelFormat: pixelFmtRev[raw.pixel_format] ?? "nv12",
77
- data,
78
- };
79
- }
80
- /** Signal end of input; drain the remaining frames with pollFrame(). */
81
- async flush() {
82
- checkPipeline(pipeline.decodeSessionFlush(this.handle));
83
- }
84
- /** Always safe — no handle-consumption trap on this surface. */
85
- close() {
86
- if (this.handle) {
87
- pipeline.decodeSessionClose(this.handle);
88
- this.handle = null;
89
- }
90
- }
91
- }
92
- /**
93
- * An Opus audio decode session — the handle IS the decoder (adr/pipeline/0006,
94
- * mirrors `DecodeSession`'s video shape; no muxer to wire, no consumption
95
- * trap). Cross-platform (mediaway-sw, no OS dependency), unlike
96
- * `DecodeSession`'s Windows-only WMF backend.
97
- */
98
- export class AudioDecodeSession {
99
- sampleRate;
100
- channels;
101
- handle;
102
- constructor(handle, sampleRate, channels) {
103
- this.handle = handle;
104
- this.sampleRate = sampleRate;
105
- this.channels = channels;
106
- }
107
- /** Open an Opus decode session. Throws DecoderUnavailableError when no
108
- * decode backend exists on this machine. */
109
- static async open(sampleRate, channels, timeBase) {
110
- const raw = pipeline.audioDecodeConfigOpus(sampleRate, channels, {
111
- num: BigInt(timeBase.num),
112
- den: timeBase.den,
113
- });
114
- const out = [null];
115
- checkPipeline(pipeline.audioDecodeSessionOpen(raw, out), DecoderUnavailableError);
116
- if (!out[0])
117
- throw new MediawayError(11, "audio decode session open returned no handle");
118
- return new AudioDecodeSession(out[0], sampleRate, channels);
119
- }
120
- /** Push one compressed Opus packet. An empty payload is Opus's
121
- * packet-loss-concealment hint for a lost frame, not an error. May produce
122
- * zero or more frames (drain via pollFrame()). */
123
- async pushPacket(packet) {
124
- const raw = {
125
- stream_id: 0,
126
- pts: BigInt(packet.pts),
127
- dts: BigInt(packet.dts ?? packet.pts),
128
- duration: BigInt(packet.duration ?? 0),
129
- is_keyframe: packet.keyframe ?? false,
130
- is_discard: false,
131
- payload: packet.payload,
132
- payload_len: packet.payload.length,
133
- };
134
- checkPipeline(pipeline.audioDecodeSessionPushPacket(this.handle, raw));
135
- }
136
- /** Pull the next decoded PCM frame, if one is ready. null is a valid
137
- * "nothing ready" result, not an error. */
138
- async pollFrame() {
139
- const raw = {};
140
- const has = [false];
141
- checkPipeline(pipeline.audioDecodeSessionPollFrame(this.handle, raw, has));
142
- if (!has[0])
143
- return null;
144
- const data = copyBytes(raw.data, raw.data_len);
145
- pipeline.decodedAudioFrameFree(raw);
146
- return {
147
- pts: Number(raw.pts),
148
- duration: Number(raw.duration),
149
- sampleRate: raw.sample_rate,
150
- channels: raw.channels,
151
- data,
152
- };
153
- }
154
- /** Signal end of input; drain the remaining frames with pollFrame(). */
155
- async flush() {
156
- checkPipeline(pipeline.audioDecodeSessionFlush(this.handle));
157
- }
158
- /** Always safe — no handle-consumption trap on this surface. */
159
- close() {
160
- if (this.handle) {
161
- pipeline.audioDecodeSessionClose(this.handle);
162
- this.handle = null;
163
- }
164
- }
165
- }