@mediaway/encoder 0.1.5 → 0.1.6
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 +30 -0
- package/dist/index.d.ts +24 -2
- package/dist/index.js +34 -2
- package/package.json +3 -2
- package/dist/decode.d.ts +0 -106
- package/dist/decode.js +0 -165
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.
|
|
3
|
+
"version": "0.1.6",
|
|
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
|
-
}
|