@mediaway/decoder 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
@@ -1,13 +1,40 @@
1
1
  # @mediaway/decoder
2
2
 
3
- Automatic hardware video decode + Opus audio decode over Mediaway's C ABI.
3
+ Automatic hardware video decode + Opus and AAC audio decode over Mediaway's C ABI.
4
4
  Split out of `@mediaway/encoder` (previously buried as an undiscoverable
5
5
  `decode.ts`) into its own peer package — decode and encode are siblings,
6
6
  mirroring the Rust `mediaway-decoder`/`mediaway-encoder` crate split.
7
7
 
8
8
  Windows is the verified platform for `DecodeSession` (WMF backend). Opus
9
9
  audio decode (`AudioDecodeSession`) is cross-platform — `mediaway-sw`, no OS
10
- dependency.
10
+ dependency, identical output on every host.
11
+
12
+ **AAC decode** (`AudioDecodeSession.open({ codec: "aac", ... })`, ABI v7) is the
13
+ *operating system's* decoder: Media Foundation on Windows (run-verified) and
14
+ AudioToolbox on macOS/iOS (**compile-checked only — not run yet**); other platforms
15
+ answer status 4 (unsupported). Unlike Opus, its samples **can differ between hosts and OS
16
+ versions**. It needs the raw `AudioSpecificConfig` as `extraData` (an MP4's `esds`
17
+ DecoderSpecificInfo) — an empty one is refused (`MediawayError` status 5) rather than guessed,
18
+ because a synthesised default would decode SBR/PS streams to quietly wrong output. Raw AAC
19
+ only: strip ADTS headers first. An empty packet is also refused for AAC (it is Opus's
20
+ loss-concealment hint, and means nothing here).
21
+
22
+ ```ts
23
+ import { AudioDecodeSession, decoderSupport } from "@mediaway/decoder";
24
+
25
+ if ((await decoderSupport("aac")) === "supported") {
26
+ const aac = await AudioDecodeSession.open({
27
+ codec: "aac",
28
+ sampleRate: 48_000,
29
+ channels: 2,
30
+ extraData: audioSpecificConfig, // required; timeBase defaults to 1/sampleRate
31
+ });
32
+ for (const packet of aacPackets) await aac.pushPacket({ pts: packet.pts, payload: packet.data });
33
+ await aac.flush();
34
+ for (let f; (f = await aac.pollFrame()); ) consume(f.data); // interleaved f32le PCM
35
+ aac.close();
36
+ }
37
+ ```
11
38
 
12
39
  ## Install
13
40
 
@@ -57,7 +84,9 @@ session.close(); // always safe — no consumption trap
57
84
  | --- | --- |
58
85
  | `DecodeSession.open(config)` | resolves a hardware video decoder or throws `DecoderUnavailableError` |
59
86
  | `DecodeSession.pushPacket(packet)` / `pollFrame()` | may produce zero or more frames per pushed packet |
60
- | `AudioDecodeSession.open(sampleRate, channels, timeBase)` | Opus only today; cross-platform |
87
+ | `AudioDecodeSession.open(sampleRate, channels, timeBase)` | Opus (the original positional form); cross-platform |
88
+ | `AudioDecodeSession.open({ codec, sampleRate, channels, timeBase?, extraData? })` | `"opus"` (needs `timeBase`) or `"aac"` (needs `extraData`; OS decoder) |
89
+ | `decoderSupport(codec)` | async, **costly** (opens a throwaway session); `"supported" \| "not-implemented" \| "no-device"` — how to learn whether AAC decode exists here |
61
90
 
62
91
  ## License
63
92
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mediaway/decoder — pipeline capability: auto video decode + Opus audio decode.
2
+ * @mediaway/decoder — pipeline capability: auto video decode + Opus and AAC audio decode.
3
3
  *
4
4
  * Implements the DX contract in bindings/nodejs/README.md over the
5
5
  * mediaway-ffi C ABI (via @mediaway/ffi). Split out of @mediaway/encoder into
@@ -40,8 +40,10 @@ export interface DecodePacket {
40
40
  dts?: number;
41
41
  duration?: number;
42
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. */
43
+ /** For Opus audio: an empty payload is the packet-loss-concealment hint for
44
+ * a lost frame, not an error — pass it whenever a frame is known lost. For
45
+ * AAC an empty payload is refused (`MediawayError` status 5): AAC has no
46
+ * such convention. */
45
47
  payload: Buffer;
46
48
  }
47
49
  export interface DecodedVideoFrame {
@@ -82,23 +84,52 @@ export declare class DecodeSession {
82
84
  /** Always safe — no handle-consumption trap on this surface. */
83
85
  close(): void;
84
86
  }
87
+ /** Audio codecs `AudioDecodeSession` opens. */
88
+ export type AudioDecodeCodec = "opus" | "aac";
85
89
  /**
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
+ * Config for `AudioDecodeSession.open()`.
91
+ *
92
+ * **Opus** is the software decoder: identical output on every host, no `extraData`, and
93
+ * `timeBase` (the frame duration, also the per-frame sample cap) is required.
94
+ *
95
+ * **AAC** is the *operating system's* decoder (Media Foundation on Windows, AudioToolbox on
96
+ * macOS/iOS; `DecoderUnavailableError` elsewhere), so its samples **can differ between hosts and
97
+ * OS versions**. `extraData` — the raw AudioSpecificConfig, e.g. an MP4's `esds`
98
+ * DecoderSpecificInfo — is **required**: an empty one is refused (`MediawayError` status 5)
99
+ * because a synthesised default would decode SBR/PS streams to quietly wrong output. Raw AAC
100
+ * only; strip ADTS headers first (`@mediaway/container`'s ADTS demuxer). `timeBase` defaults to
101
+ * `1 / sampleRate`, so packet and frame `pts`/`duration` are sample counts. The Apple decode path
102
+ * is compile-checked but has not been run yet.
103
+ */
104
+ export interface AudioDecodeConfig {
105
+ codec: AudioDecodeCodec;
106
+ sampleRate: number;
107
+ channels: number;
108
+ timeBase?: Rational;
109
+ /** AAC: the AudioSpecificConfig. Ignored for Opus. */
110
+ extraData?: Buffer;
111
+ }
112
+ /**
113
+ * An audio decode session (Opus or AAC) — the handle IS the decoder (adr/pipeline/0006 and
114
+ * adr/pipeline/0007, mirrors `DecodeSession`'s video shape; no muxer to wire, no consumption
115
+ * trap). Opus is cross-platform (mediaway-sw, no OS dependency); AAC needs the OS decoder — see
116
+ * `AudioDecodeConfig`, and `decoderSupport("aac")` to learn whether it exists here.
90
117
  */
91
118
  export declare class AudioDecodeSession {
119
+ readonly codec: AudioDecodeCodec;
92
120
  readonly sampleRate: number;
93
121
  readonly channels: number;
94
122
  private handle;
95
123
  private constructor();
96
- /** Open an Opus decode session. Throws DecoderUnavailableError when no
124
+ /** Open an audio decode session. Throws DecoderUnavailableError when no
97
125
  * decode backend exists on this machine. */
126
+ static open(config: AudioDecodeConfig): Promise<AudioDecodeSession>;
127
+ /** Open an Opus decode session (the original positional form). */
98
128
  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()). */
129
+ /** Push one compressed packet. For Opus an empty payload is the
130
+ * packet-loss-concealment hint for a lost frame, not an error; for AAC an
131
+ * empty payload is refused (status 5). May produce zero or more frames
132
+ * (drain via pollFrame()). */
102
133
  pushPacket(packet: DecodePacket): Promise<void>;
103
134
  /** Pull the next decoded PCM frame, if one is ready. null is a valid
104
135
  * "nothing ready" result, not an error. */
@@ -108,3 +139,17 @@ export declare class AudioDecodeSession {
108
139
  /** Always safe — no handle-consumption trap on this surface. */
109
140
  close(): void;
110
141
  }
142
+ /** A codec `decoderSupport()` accepts. */
143
+ export type DecodeProbeCodec = VideoCodec | "aac" | "opus";
144
+ /** Whether decoding a codec is usable right now. */
145
+ export type DecodeSupportState = "supported" | "not-implemented" | "no-device" | "unknown";
146
+ /**
147
+ * Whether decoding `codec` is usable on this machine right now.
148
+ *
149
+ * Decode has one implementation per platform, so this is one state, not a list. It is how a
150
+ * caller learns whether AAC decode exists here (`"aac"`) without opening a session.
151
+ *
152
+ * **Costly:** it opens a throwaway session. Call it when a settings screen opens, never per
153
+ * frame or in a loop.
154
+ */
155
+ export declare function decoderSupport(codec: DecodeProbeCodec): Promise<DecodeSupportState>;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @mediaway/decoder — pipeline capability: auto video decode + Opus audio decode.
2
+ * @mediaway/decoder — pipeline capability: auto video decode + Opus and AAC audio decode.
3
3
  *
4
4
  * Implements the DX contract in bindings/nodejs/README.md over the
5
5
  * mediaway-ffi C ABI (via @mediaway/ffi). Split out of @mediaway/encoder into
@@ -10,7 +10,7 @@
10
10
  * single-step shape (the handle IS the decoder, no consumption trap);
11
11
  * `NO_BACKEND` throws `DecoderUnavailableError`, an expected/graceful outcome.
12
12
  */
13
- import { pipeline, copyBytes, } from "@mediaway/ffi";
13
+ import { PIPELINE_CODEC, pipeline, copyBytes, } from "@mediaway/ffi";
14
14
  import { MediawayError } from "@mediaway/container";
15
15
  export { MediawayError } from "@mediaway/container";
16
16
  /** No decoder backend is openable for this config — expected on machines
@@ -26,7 +26,7 @@ function checkPipeline(status, noBackendError = DecoderUnavailableError) {
26
26
  1: "invalid argument",
27
27
  2: "handle poisoned by an earlier panic",
28
28
  4: "codec/pixel-format/geometry not supported",
29
- 5: "bad dimensions, rates, or frame metadata",
29
+ 5: "bad dimensions, rates, codec config (e.g. an empty AAC AudioSpecificConfig) or frame metadata",
30
30
  6: "encoder backend OS/API failure",
31
31
  7: "session already finished or not open",
32
32
  8: "muxer rejected the encoder's stream info",
@@ -113,36 +113,49 @@ export class DecodeSession {
113
113
  }
114
114
  }
115
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.
116
+ * An audio decode session (Opus or AAC) — the handle IS the decoder (adr/pipeline/0006 and
117
+ * adr/pipeline/0007, mirrors `DecodeSession`'s video shape; no muxer to wire, no consumption
118
+ * trap). Opus is cross-platform (mediaway-sw, no OS dependency); AAC needs the OS decoder — see
119
+ * `AudioDecodeConfig`, and `decoderSupport("aac")` to learn whether it exists here.
120
120
  */
121
121
  export class AudioDecodeSession {
122
+ codec;
122
123
  sampleRate;
123
124
  channels;
124
125
  handle;
125
- constructor(handle, sampleRate, channels) {
126
+ constructor(handle, codec, sampleRate, channels) {
126
127
  this.handle = handle;
128
+ this.codec = codec;
127
129
  this.sampleRate = sampleRate;
128
130
  this.channels = channels;
129
131
  }
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
- });
132
+ static async open(first, channels, timeBase) {
133
+ const config = typeof first === "number"
134
+ ? { codec: "opus", sampleRate: first, channels: channels ?? 0, timeBase }
135
+ : first;
136
+ const tb = config.timeBase ?? (config.codec === "aac" ? { num: 1, den: config.sampleRate } : undefined);
137
+ if (!tb)
138
+ throw new MediawayError(5, "Opus decode needs a timeBase (the frame duration)");
139
+ const rawTimeBase = { num: BigInt(tb.num), den: tb.den };
140
+ const raw = config.codec === "aac"
141
+ ? pipeline.audioDecodeConfigAac(config.sampleRate, config.channels, rawTimeBase, null, 0)
142
+ : pipeline.audioDecodeConfigOpus(config.sampleRate, config.channels, rawTimeBase);
143
+ if (config.codec === "aac" && config.extraData && config.extraData.length > 0) {
144
+ // The ASC is BORROWED by native code for the open() call only. Assigning the Buffer here
145
+ // (rather than passing it to the constructor) lets koffi pin it for exactly that call.
146
+ raw.extra_data = config.extraData;
147
+ raw.extra_data_len = config.extraData.length;
148
+ }
137
149
  const out = [null];
138
150
  checkPipeline(pipeline.audioDecodeSessionOpen(raw, out), DecoderUnavailableError);
139
151
  if (!out[0])
140
152
  throw new MediawayError(11, "audio decode session open returned no handle");
141
- return new AudioDecodeSession(out[0], sampleRate, channels);
153
+ return new AudioDecodeSession(out[0], config.codec, config.sampleRate, config.channels);
142
154
  }
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()). */
155
+ /** Push one compressed packet. For Opus an empty payload is the
156
+ * packet-loss-concealment hint for a lost frame, not an error; for AAC an
157
+ * empty payload is refused (status 5). May produce zero or more frames
158
+ * (drain via pollFrame()). */
146
159
  async pushPacket(packet) {
147
160
  const raw = {
148
161
  stream_id: 0,
@@ -186,3 +199,22 @@ export class AudioDecodeSession {
186
199
  }
187
200
  }
188
201
  }
202
+ const SUPPORT_STATES = {
203
+ 0: "supported",
204
+ 1: "not-implemented",
205
+ 2: "no-device",
206
+ };
207
+ /**
208
+ * Whether decoding `codec` is usable on this machine right now.
209
+ *
210
+ * Decode has one implementation per platform, so this is one state, not a list. It is how a
211
+ * caller learns whether AAC decode exists here (`"aac"`) without opening a session.
212
+ *
213
+ * **Costly:** it opens a throwaway session. Call it when a settings screen opens, never per
214
+ * frame or in a loop.
215
+ */
216
+ export async function decoderSupport(codec) {
217
+ const out = [255];
218
+ checkPipeline(pipeline.decoderSupport(PIPELINE_CODEC[codec], out));
219
+ return SUPPORT_STATES[out[0]] ?? "unknown";
220
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediaway/decoder",
3
- "version": "0.1.6",
3
+ "version": "0.2.1",
4
4
  "description": "Rust-based, lightweight, fast — automatic hardware video decode + Opus audio decode for Node.js",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",