@mediaway/decoder 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 -3
- package/dist/index.d.ts +56 -11
- package/dist/index.js +51 -19
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* `
|
|
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
|
|
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
|
|
100
|
-
* packet-loss-concealment hint for a lost frame, not an error
|
|
101
|
-
*
|
|
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
|
|
117
|
-
* mirrors `DecodeSession`'s video shape; no muxer to wire, no consumption
|
|
118
|
-
* trap).
|
|
119
|
-
* `
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
144
|
-
* packet-loss-concealment hint for a lost frame, not an error
|
|
145
|
-
*
|
|
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