@mediaway/decoder 0.1.5

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 ADDED
@@ -0,0 +1,65 @@
1
+ # @mediaway/decoder
2
+
3
+ Automatic hardware video decode + Opus audio decode over Mediaway's C ABI.
4
+ Split out of `@mediaway/encoder` (previously buried as an undiscoverable
5
+ `decode.ts`) into its own peer package — decode and encode are siblings,
6
+ mirroring the Rust `mediaway-decoder`/`mediaway-encoder` crate split.
7
+
8
+ Windows is the verified platform for `DecodeSession` (WMF backend). Opus
9
+ audio decode (`AudioDecodeSession`) is cross-platform — `mediaway-sw`, no OS
10
+ dependency.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @mediaway/decoder
16
+ ```
17
+
18
+ ## Example
19
+
20
+ ```ts
21
+ import { DecodeSession, DecoderUnavailableError, type DecodedVideoFrame } from "@mediaway/decoder";
22
+ import type { Rational } from "@mediaway/container";
23
+
24
+ const TIME_BASE: Rational = { num: 1, den: 30 };
25
+
26
+ let session;
27
+ try {
28
+ session = await DecodeSession.open({
29
+ codec: "h264",
30
+ width: 640,
31
+ height: 480,
32
+ timeBase: TIME_BASE,
33
+ extraData: avccBytes, // AVCC / SPS-PPS, required at open time
34
+ });
35
+ } catch (err) {
36
+ if (err instanceof DecoderUnavailableError) {
37
+ console.log("no hardware decoder available on this machine");
38
+ process.exit(1);
39
+ }
40
+ throw err;
41
+ }
42
+
43
+ for (const packet of compressedPackets) {
44
+ await session.pushPacket(packet);
45
+ let frame: DecodedVideoFrame | null;
46
+ while ((frame = await session.pollFrame()) !== null) {
47
+ // consume frame.data (frame.pixelFormat, width, height)
48
+ }
49
+ }
50
+ await session.flush();
51
+ session.close(); // always safe — no consumption trap
52
+ ```
53
+
54
+ ## API
55
+
56
+ | Member | Notes |
57
+ | --- | --- |
58
+ | `DecodeSession.open(config)` | resolves a hardware video decoder or throws `DecoderUnavailableError` |
59
+ | `DecodeSession.pushPacket(packet)` / `pollFrame()` | may produce zero or more frames per pushed packet |
60
+ | `AudioDecodeSession.open(sampleRate, channels, timeBase)` | Opus only today; cross-platform |
61
+
62
+ ## License
63
+
64
+ MIT OR Apache-2.0. Part of the [Mediaway](https://github.com/nyxways/mediaway)
65
+ project — pre-1.0, APIs may change.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @mediaway/decoder — pipeline capability: auto video decode + Opus audio decode.
3
+ *
4
+ * Implements the DX contract in bindings/nodejs/README.md over the
5
+ * mediaway-ffi C ABI (via @mediaway/ffi). Split out of @mediaway/encoder into
6
+ * its own package (previously buried as encoder/src/decode.ts, undiscoverable
7
+ * and undocumented) — decode and encode are peer capabilities, mirroring the
8
+ * Rust crate split (`mediaway-decoder`/`mediaway-encoder`), not one depending
9
+ * on the other. Both sessions mirror `openAutoEncoder()`/`AudioEncoder`'s
10
+ * single-step shape (the handle IS the decoder, no consumption trap);
11
+ * `NO_BACKEND` throws `DecoderUnavailableError`, an expected/graceful outcome.
12
+ */
13
+ import { MediawayError, type Rational } from "@mediaway/container";
14
+ export type { Rational } from "@mediaway/container";
15
+ export { MediawayError } from "@mediaway/container";
16
+ export type PixelFormat = "nv12" | "bgra8" | "rgba8" | "i420" | "yuy2";
17
+ export type VideoCodec = "h264" | "hevc" | "av1" | "vp9";
18
+ /** No decoder backend is openable for this config — expected on machines
19
+ * without a usable decoder; catch it and exit gracefully. */
20
+ export declare class DecoderUnavailableError extends MediawayError {
21
+ }
22
+ export interface VideoDecodeConfig {
23
+ codec: VideoCodec;
24
+ /** Expected; may be refined from the bitstream. */
25
+ width: number;
26
+ height: number;
27
+ timeBase: Rational;
28
+ /** Preferred output format when the backend converts. */
29
+ pixelFormat?: PixelFormat;
30
+ /** AVCC / SPS-PPS codec config, required at open time — not supplied via
31
+ * the first pushed packet (adr/0004 §1: the muxer-track analogy does not
32
+ * hold for the wrapped decoder). */
33
+ extraData?: Buffer;
34
+ }
35
+ /** Input to `DecodeSession.pushPacket`/`AudioDecodeSession.pushPacket` — a
36
+ * pipeline-scoped packet view, distinct from `@mediaway/container`'s
37
+ * `Packet` (adr/pipeline/0006 §4: shared by both video and audio decode). */
38
+ export interface DecodePacket {
39
+ pts: number;
40
+ dts?: number;
41
+ duration?: number;
42
+ keyframe?: boolean;
43
+ /** For audio: an empty payload is Opus's packet-loss-concealment hint for
44
+ * a lost frame, not an error — pass it whenever a frame is known lost. */
45
+ payload: Buffer;
46
+ }
47
+ export interface DecodedVideoFrame {
48
+ pts: number;
49
+ duration: number;
50
+ width: number;
51
+ height: number;
52
+ pixelFormat: PixelFormat;
53
+ data: Buffer;
54
+ }
55
+ export interface DecodedAudioFrame {
56
+ pts: number;
57
+ duration: number;
58
+ sampleRate: number;
59
+ channels: number;
60
+ data: Buffer;
61
+ }
62
+ /**
63
+ * The best available video decode session for a config — the handle IS the
64
+ * decoder (single-step open, no consumption trap, mirrors `openAutoEncoder`'s
65
+ * `NO_BACKEND` handling). CPU output only (GPU decode output is deferred,
66
+ * adr/0004 §1/§5).
67
+ */
68
+ export declare class DecodeSession {
69
+ private handle;
70
+ private constructor();
71
+ /** Open the best available video decoder for `config`. Throws
72
+ * DecoderUnavailableError when no decode backend exists on this machine. */
73
+ static open(config: VideoDecodeConfig): Promise<DecodeSession>;
74
+ /** Push one compressed packet. May produce zero or more frames (drain via
75
+ * `pollFrame()`). */
76
+ pushPacket(packet: DecodePacket): Promise<void>;
77
+ /** Pull the next decoded frame, if one is ready. null is a valid "nothing
78
+ * ready" result, not an error. */
79
+ pollFrame(): Promise<DecodedVideoFrame | null>;
80
+ /** Signal end of input; drain the remaining frames with pollFrame(). */
81
+ flush(): Promise<void>;
82
+ /** Always safe — no handle-consumption trap on this surface. */
83
+ close(): void;
84
+ }
85
+ /**
86
+ * An Opus audio decode session — the handle IS the decoder (adr/pipeline/0006,
87
+ * mirrors `DecodeSession`'s video shape; no muxer to wire, no consumption
88
+ * trap). Cross-platform (mediaway-sw, no OS dependency), unlike
89
+ * `DecodeSession`'s Windows-only WMF backend.
90
+ */
91
+ export declare class AudioDecodeSession {
92
+ readonly sampleRate: number;
93
+ readonly channels: number;
94
+ private handle;
95
+ private constructor();
96
+ /** Open an Opus decode session. Throws DecoderUnavailableError when no
97
+ * decode backend exists on this machine. */
98
+ static open(sampleRate: number, channels: number, timeBase: Rational): Promise<AudioDecodeSession>;
99
+ /** Push one compressed Opus packet. An empty payload is Opus's
100
+ * packet-loss-concealment hint for a lost frame, not an error. May produce
101
+ * zero or more frames (drain via pollFrame()). */
102
+ pushPacket(packet: DecodePacket): Promise<void>;
103
+ /** Pull the next decoded PCM frame, if one is ready. null is a valid
104
+ * "nothing ready" result, not an error. */
105
+ pollFrame(): Promise<DecodedAudioFrame | null>;
106
+ /** Signal end of input; drain the remaining frames with pollFrame(). */
107
+ flush(): Promise<void>;
108
+ /** Always safe — no handle-consumption trap on this surface. */
109
+ close(): void;
110
+ }
package/dist/index.js ADDED
@@ -0,0 +1,188 @@
1
+ /**
2
+ * @mediaway/decoder — pipeline capability: auto video decode + Opus audio decode.
3
+ *
4
+ * Implements the DX contract in bindings/nodejs/README.md over the
5
+ * mediaway-ffi C ABI (via @mediaway/ffi). Split out of @mediaway/encoder into
6
+ * its own package (previously buried as encoder/src/decode.ts, undiscoverable
7
+ * and undocumented) — decode and encode are peer capabilities, mirroring the
8
+ * Rust crate split (`mediaway-decoder`/`mediaway-encoder`), not one depending
9
+ * on the other. Both sessions mirror `openAutoEncoder()`/`AudioEncoder`'s
10
+ * single-step shape (the handle IS the decoder, no consumption trap);
11
+ * `NO_BACKEND` throws `DecoderUnavailableError`, an expected/graceful outcome.
12
+ */
13
+ import { pipeline, copyBytes, } from "@mediaway/ffi";
14
+ import { MediawayError } from "@mediaway/container";
15
+ export { MediawayError } from "@mediaway/container";
16
+ /** No decoder backend is openable for this config — expected on machines
17
+ * without a usable decoder; catch it and exit gracefully. */
18
+ export class DecoderUnavailableError extends MediawayError {
19
+ }
20
+ function checkPipeline(status, noBackendError = DecoderUnavailableError) {
21
+ if (status === 0)
22
+ return;
23
+ if (status === 3)
24
+ throw new noBackendError(status, "no backend compiled in or openable");
25
+ const names = {
26
+ 1: "invalid argument",
27
+ 2: "handle poisoned by an earlier panic",
28
+ 4: "codec/pixel-format/geometry not supported",
29
+ 5: "bad dimensions, rates, or frame metadata",
30
+ 6: "encoder backend OS/API failure",
31
+ 7: "session already finished or not open",
32
+ 8: "muxer rejected the encoder's stream info",
33
+ 9: "packet does not match the registered track",
34
+ 10: "malformed container data",
35
+ 11: "unknown error",
36
+ 12: "internal panic (handle poisoned)",
37
+ 13: "decoder backend OS/API failure",
38
+ 14: "decode session already finished or not open",
39
+ };
40
+ throw new MediawayError(status, names[status] ?? "unknown pipeline error");
41
+ }
42
+ const codecMap = { h264: 0, hevc: 1, av1: 2, vp9: 3 };
43
+ const pixelFmtMap = { nv12: 0, i420: 1, bgra8: 2, rgba8: 3, yuy2: 4 };
44
+ const pixelFmtRev = ["nv12", "i420", "bgra8", "rgba8", "yuy2"];
45
+ /**
46
+ * The best available video decode session for a config — the handle IS the
47
+ * decoder (single-step open, no consumption trap, mirrors `openAutoEncoder`'s
48
+ * `NO_BACKEND` handling). CPU output only (GPU decode output is deferred,
49
+ * adr/0004 §1/§5).
50
+ */
51
+ export class DecodeSession {
52
+ handle;
53
+ constructor(handle) {
54
+ this.handle = handle;
55
+ }
56
+ /** Open the best available video decoder for `config`. Throws
57
+ * DecoderUnavailableError when no decode backend exists on this machine. */
58
+ static async open(config) {
59
+ const extraData = config.extraData ?? Buffer.alloc(0);
60
+ 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);
61
+ if (config.pixelFormat !== undefined)
62
+ raw.pixel_format = pixelFmtMap[config.pixelFormat] ?? 0;
63
+ const out = [null];
64
+ checkPipeline(pipeline.decodeSessionOpen(raw, out), DecoderUnavailableError);
65
+ if (!out[0])
66
+ throw new MediawayError(11, "decode session open returned no handle");
67
+ return new DecodeSession(out[0]);
68
+ }
69
+ /** Push one compressed packet. May produce zero or more frames (drain via
70
+ * `pollFrame()`). */
71
+ async pushPacket(packet) {
72
+ const raw = {
73
+ stream_id: 0,
74
+ pts: BigInt(packet.pts),
75
+ dts: BigInt(packet.dts ?? packet.pts),
76
+ duration: BigInt(packet.duration ?? 0),
77
+ is_keyframe: packet.keyframe ?? false,
78
+ is_discard: false,
79
+ payload: packet.payload,
80
+ payload_len: packet.payload.length,
81
+ };
82
+ checkPipeline(pipeline.decodeSessionPushPacket(this.handle, raw));
83
+ }
84
+ /** Pull the next decoded frame, if one is ready. null is a valid "nothing
85
+ * ready" result, not an error. */
86
+ async pollFrame() {
87
+ const raw = {};
88
+ const has = [false];
89
+ checkPipeline(pipeline.decodeSessionPollFrame(this.handle, raw, has));
90
+ if (!has[0])
91
+ return null;
92
+ const data = copyBytes(raw.data, raw.data_len);
93
+ pipeline.decodedVideoFrameFree(raw);
94
+ return {
95
+ pts: Number(raw.pts),
96
+ duration: Number(raw.duration),
97
+ width: raw.width,
98
+ height: raw.height,
99
+ pixelFormat: pixelFmtRev[raw.pixel_format] ?? "nv12",
100
+ data,
101
+ };
102
+ }
103
+ /** Signal end of input; drain the remaining frames with pollFrame(). */
104
+ async flush() {
105
+ checkPipeline(pipeline.decodeSessionFlush(this.handle));
106
+ }
107
+ /** Always safe — no handle-consumption trap on this surface. */
108
+ close() {
109
+ if (this.handle) {
110
+ pipeline.decodeSessionClose(this.handle);
111
+ this.handle = null;
112
+ }
113
+ }
114
+ }
115
+ /**
116
+ * An Opus audio decode session — the handle IS the decoder (adr/pipeline/0006,
117
+ * mirrors `DecodeSession`'s video shape; no muxer to wire, no consumption
118
+ * trap). Cross-platform (mediaway-sw, no OS dependency), unlike
119
+ * `DecodeSession`'s Windows-only WMF backend.
120
+ */
121
+ export class AudioDecodeSession {
122
+ sampleRate;
123
+ channels;
124
+ handle;
125
+ constructor(handle, sampleRate, channels) {
126
+ this.handle = handle;
127
+ this.sampleRate = sampleRate;
128
+ this.channels = channels;
129
+ }
130
+ /** Open an Opus decode session. Throws DecoderUnavailableError when no
131
+ * decode backend exists on this machine. */
132
+ static async open(sampleRate, channels, timeBase) {
133
+ const raw = pipeline.audioDecodeConfigOpus(sampleRate, channels, {
134
+ num: BigInt(timeBase.num),
135
+ den: timeBase.den,
136
+ });
137
+ const out = [null];
138
+ checkPipeline(pipeline.audioDecodeSessionOpen(raw, out), DecoderUnavailableError);
139
+ if (!out[0])
140
+ throw new MediawayError(11, "audio decode session open returned no handle");
141
+ return new AudioDecodeSession(out[0], sampleRate, channels);
142
+ }
143
+ /** Push one compressed Opus packet. An empty payload is Opus's
144
+ * packet-loss-concealment hint for a lost frame, not an error. May produce
145
+ * zero or more frames (drain via pollFrame()). */
146
+ async pushPacket(packet) {
147
+ const raw = {
148
+ stream_id: 0,
149
+ pts: BigInt(packet.pts),
150
+ dts: BigInt(packet.dts ?? packet.pts),
151
+ duration: BigInt(packet.duration ?? 0),
152
+ is_keyframe: packet.keyframe ?? false,
153
+ is_discard: false,
154
+ payload: packet.payload,
155
+ payload_len: packet.payload.length,
156
+ };
157
+ checkPipeline(pipeline.audioDecodeSessionPushPacket(this.handle, raw));
158
+ }
159
+ /** Pull the next decoded PCM frame, if one is ready. null is a valid
160
+ * "nothing ready" result, not an error. */
161
+ async pollFrame() {
162
+ const raw = {};
163
+ const has = [false];
164
+ checkPipeline(pipeline.audioDecodeSessionPollFrame(this.handle, raw, has));
165
+ if (!has[0])
166
+ return null;
167
+ const data = copyBytes(raw.data, raw.data_len);
168
+ pipeline.decodedAudioFrameFree(raw);
169
+ return {
170
+ pts: Number(raw.pts),
171
+ duration: Number(raw.duration),
172
+ sampleRate: raw.sample_rate,
173
+ channels: raw.channels,
174
+ data,
175
+ };
176
+ }
177
+ /** Signal end of input; drain the remaining frames with pollFrame(). */
178
+ async flush() {
179
+ checkPipeline(pipeline.audioDecodeSessionFlush(this.handle));
180
+ }
181
+ /** Always safe — no handle-consumption trap on this surface. */
182
+ close() {
183
+ if (this.handle) {
184
+ pipeline.audioDecodeSessionClose(this.handle);
185
+ this.handle = null;
186
+ }
187
+ }
188
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@mediaway/decoder",
3
+ "version": "0.1.5",
4
+ "description": "Rust-based, lightweight, fast — automatic hardware video decode + Opus audio decode for Node.js",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "license": "MIT OR Apache-2.0",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/nyxways/mediaway.git"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json"
27
+ },
28
+ "dependencies": {
29
+ "@mediaway/ffi": "^0.1.0",
30
+ "@mediaway/container": "^0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.6.0"
34
+ }
35
+ }