@mediaway/encoder 0.1.0
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/dist/index.d.ts +130 -0
- package/dist/index.js +226 -0
- package/package.json +35 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mediaway/encoder — pipeline capability: auto video encode -> fragmented MP4.
|
|
3
|
+
*
|
|
4
|
+
* Implements the DX contract in bindings/nodejs/README.md over the
|
|
5
|
+
* mediaway-ffi C ABI (via @mediaway/ffi). `openAutoEncoder()` picks
|
|
6
|
+
* the best available OS/GPU encoder for the config and returns an
|
|
7
|
+
* `EncodeSession` directly; `finish()` is terminal. Video only — there is no
|
|
8
|
+
* audio encoder in the ABI.
|
|
9
|
+
*/
|
|
10
|
+
import { type RawRational } from "@mediaway/ffi";
|
|
11
|
+
import { MediawayError, type Rational } from "@mediaway/container";
|
|
12
|
+
export type { Rational } from "@mediaway/container";
|
|
13
|
+
export { MediawayError } from "@mediaway/container";
|
|
14
|
+
export type PixelFormat = "nv12" | "bgra8" | "rgba8" | "i420" | "yuy2";
|
|
15
|
+
export type VideoCodec = "h264" | "hevc" | "av1" | "vp9";
|
|
16
|
+
export interface VideoFrame {
|
|
17
|
+
/** Ticks of the session's timeBase. */
|
|
18
|
+
pts: number;
|
|
19
|
+
duration: number;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
pixelFormat: PixelFormat;
|
|
23
|
+
data: Buffer;
|
|
24
|
+
}
|
|
25
|
+
/** No encode backend is openable for this config — expected on machines
|
|
26
|
+
* without a usable encoder; catch it and exit gracefully. */
|
|
27
|
+
export declare class EncoderUnavailableError extends MediawayError {
|
|
28
|
+
}
|
|
29
|
+
export declare class AutoVideoEncodeConfig {
|
|
30
|
+
codec: VideoCodec;
|
|
31
|
+
width: number;
|
|
32
|
+
height: number;
|
|
33
|
+
timeBase: Rational;
|
|
34
|
+
bitrateBps?: number;
|
|
35
|
+
pixelFormat?: PixelFormat;
|
|
36
|
+
private constructor();
|
|
37
|
+
/** The ABI's defaults (backend-default bitrate, NV12 input). */
|
|
38
|
+
static defaults(codec: VideoCodec, width: number, height: number, timeBase: Rational): AutoVideoEncodeConfig;
|
|
39
|
+
toAbi(): RawRational;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A single-use encode session. `finish()` is terminal — it consumes the
|
|
43
|
+
* session; `close()` after it is a no-op (idempotent), but is required to free
|
|
44
|
+
* the native handle when an error aborts the encode mid-way.
|
|
45
|
+
*/
|
|
46
|
+
export declare class EncodeSession {
|
|
47
|
+
readonly codec: VideoCodec;
|
|
48
|
+
readonly width: number;
|
|
49
|
+
readonly height: number;
|
|
50
|
+
readonly timeBase: Rational;
|
|
51
|
+
private handle;
|
|
52
|
+
private finished;
|
|
53
|
+
constructor(config: AutoVideoEncodeConfig, encoderHandle: unknown);
|
|
54
|
+
writeFrame(frame: VideoFrame): Promise<void>;
|
|
55
|
+
/** Flush the encoder + muxer and return the complete fMP4 bytes. Terminal. */
|
|
56
|
+
finish(): Promise<Buffer>;
|
|
57
|
+
/** Idempotent; no-op after finish(). Frees the native handle on error paths. */
|
|
58
|
+
close(): void;
|
|
59
|
+
}
|
|
60
|
+
/** Pick the best available encoder for `config` and open a session on it.
|
|
61
|
+
* Throws EncoderUnavailableError when no backend exists on this machine. */
|
|
62
|
+
export declare function openAutoEncoder(config: AutoVideoEncodeConfig): Promise<EncodeSession>;
|
|
63
|
+
export type AudioCodec = "aac";
|
|
64
|
+
export type SampleFormat = "s16" | "s32" | "f32";
|
|
65
|
+
export interface AudioEncodeConfig {
|
|
66
|
+
/** Output codec — "aac" today (the only real backend codec). */
|
|
67
|
+
codec?: AudioCodec;
|
|
68
|
+
/** Input sample rate in Hz — must match the pushed PCM frames. */
|
|
69
|
+
sampleRate: number;
|
|
70
|
+
/** Input channel count — must match the pushed PCM frames (a mono mic is
|
|
71
|
+
* not the AAC sugar's default stereo). */
|
|
72
|
+
channels: number;
|
|
73
|
+
/** Input PCM format — "f32" today. */
|
|
74
|
+
sampleFormat?: SampleFormat;
|
|
75
|
+
/** Sample clock: { num: 1, den: sampleRate } = tick per sample. */
|
|
76
|
+
timeBase?: Rational;
|
|
77
|
+
/** Target bitrate; 0 / undefined = backend default (128 kbps). */
|
|
78
|
+
bitrateBps?: number;
|
|
79
|
+
}
|
|
80
|
+
export interface AudioStreamInfo {
|
|
81
|
+
codec: AudioCodec;
|
|
82
|
+
sampleRate: number;
|
|
83
|
+
channels: number;
|
|
84
|
+
/** AudioSpecificConfig — register it on the muxer's audio track. */
|
|
85
|
+
extraData: Buffer;
|
|
86
|
+
}
|
|
87
|
+
export interface AudioPcmFrame {
|
|
88
|
+
/** Sample index in the stream timeBase (frame i starts at i * samplesPerFrame). */
|
|
89
|
+
pts: number;
|
|
90
|
+
/** Sample count; undefined = derived from the chunk length. */
|
|
91
|
+
duration?: number;
|
|
92
|
+
/** Interleaved f32le PCM bytes. */
|
|
93
|
+
data: Buffer;
|
|
94
|
+
}
|
|
95
|
+
export interface EncodedAudioPacket {
|
|
96
|
+
pts: number;
|
|
97
|
+
dts: number;
|
|
98
|
+
duration: number;
|
|
99
|
+
keyframe: boolean;
|
|
100
|
+
/** Owned AAC bytes; freed by this wrapper inside pollPacket(). */
|
|
101
|
+
data: Buffer;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* An opened auto audio encoder — the session IS the encoder (ABI v2,
|
|
105
|
+
* adr/0003): single-step open, no intermediate handle, no consumption trap;
|
|
106
|
+
* `close()` is always safe (idempotent).
|
|
107
|
+
*/
|
|
108
|
+
export declare class AudioEncoder {
|
|
109
|
+
readonly sampleRate: number;
|
|
110
|
+
readonly channels: number;
|
|
111
|
+
readonly timeBase: Rational;
|
|
112
|
+
private handle;
|
|
113
|
+
private constructor();
|
|
114
|
+
/** Open the best available audio encoder. Throws EncoderUnavailableError
|
|
115
|
+
* when no audio backend exists on this machine. */
|
|
116
|
+
static open(config: AudioEncodeConfig): Promise<AudioEncoder>;
|
|
117
|
+
/** Push one interleaved f32le PCM chunk (borrowed — copied synchronously). */
|
|
118
|
+
pushPcm(frame: AudioPcmFrame): Promise<void>;
|
|
119
|
+
/** Pull the next encoded packet, if one is ready. null is a valid "nothing
|
|
120
|
+
* ready" result, not an error. */
|
|
121
|
+
pollPacket(): Promise<EncodedAudioPacket | null>;
|
|
122
|
+
/** Signal end of input; drain the remaining packets with pollPacket(). */
|
|
123
|
+
flush(): Promise<void>;
|
|
124
|
+
/** Codec config (AudioSpecificConfig) + negotiated rates — available after
|
|
125
|
+
* the first pushed frame (adr/0003 call order: push, then streamInfo, then
|
|
126
|
+
* mux). */
|
|
127
|
+
streamInfo(): Promise<AudioStreamInfo>;
|
|
128
|
+
/** Always safe — no handle-consumption trap on this surface (adr/0003). */
|
|
129
|
+
close(): void;
|
|
130
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mediaway/encoder — pipeline capability: auto video encode -> fragmented MP4.
|
|
3
|
+
*
|
|
4
|
+
* Implements the DX contract in bindings/nodejs/README.md over the
|
|
5
|
+
* mediaway-ffi C ABI (via @mediaway/ffi). `openAutoEncoder()` picks
|
|
6
|
+
* the best available OS/GPU encoder for the config and returns an
|
|
7
|
+
* `EncodeSession` directly; `finish()` is terminal. Video only — there is no
|
|
8
|
+
* audio encoder in the ABI.
|
|
9
|
+
*/
|
|
10
|
+
import { MwAudioEncodeConfig, MwAudioFrameView, MwAudioPacket, MwAudioStreamInfo, MwPipelineFrame, MwRational, pipeline, copyBytes, } from "@mediaway/ffi";
|
|
11
|
+
import { MediawayError } from "@mediaway/container";
|
|
12
|
+
export { MediawayError } from "@mediaway/container";
|
|
13
|
+
/** No encode backend is openable for this config — expected on machines
|
|
14
|
+
* without a usable encoder; catch it and exit gracefully. */
|
|
15
|
+
export class EncoderUnavailableError extends MediawayError {
|
|
16
|
+
}
|
|
17
|
+
export class AutoVideoEncodeConfig {
|
|
18
|
+
codec;
|
|
19
|
+
width;
|
|
20
|
+
height;
|
|
21
|
+
timeBase;
|
|
22
|
+
bitrateBps; // 0 / undefined = backend default
|
|
23
|
+
pixelFormat;
|
|
24
|
+
constructor(codec, width, height, timeBase) {
|
|
25
|
+
this.codec = codec;
|
|
26
|
+
this.width = width;
|
|
27
|
+
this.height = height;
|
|
28
|
+
this.timeBase = timeBase;
|
|
29
|
+
}
|
|
30
|
+
/** The ABI's defaults (backend-default bitrate, NV12 input). */
|
|
31
|
+
static defaults(codec, width, height, timeBase) {
|
|
32
|
+
return new AutoVideoEncodeConfig(codec, width, height, timeBase);
|
|
33
|
+
}
|
|
34
|
+
toAbi() {
|
|
35
|
+
const codecMap = { h264: 0, hevc: 1, av1: 2, vp9: 3 };
|
|
36
|
+
const raw = pipeline.encConfigNew(codecMap[this.codec] ?? 0, this.width, this.height, { num: BigInt(this.timeBase.num), den: this.timeBase.den });
|
|
37
|
+
if (this.bitrateBps !== undefined)
|
|
38
|
+
raw.bitrate_bps = this.bitrateBps;
|
|
39
|
+
const fmtMap = { nv12: 0, i420: 1, bgra8: 2, rgba8: 3, yuy2: 4 };
|
|
40
|
+
if (this.pixelFormat !== undefined)
|
|
41
|
+
raw.pixel_format = fmtMap[this.pixelFormat] ?? 0;
|
|
42
|
+
return raw;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function checkPipeline(status) {
|
|
46
|
+
if (status === 0)
|
|
47
|
+
return;
|
|
48
|
+
if (status === 3)
|
|
49
|
+
throw new EncoderUnavailableError(status, "no encode backend compiled in or openable");
|
|
50
|
+
const names = {
|
|
51
|
+
1: "invalid argument",
|
|
52
|
+
2: "handle poisoned by an earlier panic",
|
|
53
|
+
4: "codec/pixel-format/geometry not supported",
|
|
54
|
+
5: "bad dimensions, rates, or frame metadata",
|
|
55
|
+
6: "encoder backend OS/API failure",
|
|
56
|
+
7: "session already finished or not open",
|
|
57
|
+
8: "muxer rejected the encoder's stream info",
|
|
58
|
+
9: "packet does not match the registered track",
|
|
59
|
+
10: "malformed container data",
|
|
60
|
+
11: "unknown error",
|
|
61
|
+
12: "internal panic (handle poisoned)",
|
|
62
|
+
};
|
|
63
|
+
throw new MediawayError(status, names[status] ?? "unknown pipeline error");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A single-use encode session. `finish()` is terminal — it consumes the
|
|
67
|
+
* session; `close()` after it is a no-op (idempotent), but is required to free
|
|
68
|
+
* the native handle when an error aborts the encode mid-way.
|
|
69
|
+
*/
|
|
70
|
+
export class EncodeSession {
|
|
71
|
+
codec;
|
|
72
|
+
width;
|
|
73
|
+
height;
|
|
74
|
+
timeBase;
|
|
75
|
+
handle;
|
|
76
|
+
finished = false;
|
|
77
|
+
constructor(config, encoderHandle) {
|
|
78
|
+
this.codec = config.codec;
|
|
79
|
+
this.width = config.width;
|
|
80
|
+
this.height = config.height;
|
|
81
|
+
this.timeBase = config.timeBase;
|
|
82
|
+
// mediaway_encode_session_open consumes the encoder unconditionally.
|
|
83
|
+
const out = [null];
|
|
84
|
+
checkPipeline(pipeline.sessionOpen(encoderHandle, out));
|
|
85
|
+
this.handle = out[0];
|
|
86
|
+
}
|
|
87
|
+
async writeFrame(frame) {
|
|
88
|
+
const raw = {
|
|
89
|
+
pts: BigInt(frame.pts),
|
|
90
|
+
duration: BigInt(frame.duration),
|
|
91
|
+
width: frame.width,
|
|
92
|
+
height: frame.height,
|
|
93
|
+
pixel_format: { nv12: 0, i420: 1, bgra8: 2, rgba8: 3, yuy2: 4 }[frame.pixelFormat] ?? 0,
|
|
94
|
+
storage_kind: 0, // CPU
|
|
95
|
+
raw_bytes: frame.data,
|
|
96
|
+
raw_bytes_len: frame.data.length,
|
|
97
|
+
gpu_buffer: { kind: 255, native_a: 0, native_b: 0, subresource: 0, webgpu_texture_id: 0 },
|
|
98
|
+
};
|
|
99
|
+
checkPipeline(pipeline.sessionWriteFrame(this.handle, raw));
|
|
100
|
+
}
|
|
101
|
+
/** Flush the encoder + muxer and return the complete fMP4 bytes. Terminal. */
|
|
102
|
+
async finish() {
|
|
103
|
+
const outData = [null];
|
|
104
|
+
const outLen = [0];
|
|
105
|
+
// finish consumes the session UNCONDITIONALLY (even on failure) — null
|
|
106
|
+
// the handle before checking so close() cannot double-free it.
|
|
107
|
+
const status = pipeline.sessionFinish(this.handle, outData, outLen);
|
|
108
|
+
this.handle = null;
|
|
109
|
+
this.finished = true;
|
|
110
|
+
checkPipeline(status);
|
|
111
|
+
const data = copyBytes(outData[0], outLen[0]);
|
|
112
|
+
if (outLen[0] > 0)
|
|
113
|
+
pipeline.bufferFree(outData[0], outLen[0]);
|
|
114
|
+
return data;
|
|
115
|
+
}
|
|
116
|
+
/** Idempotent; no-op after finish(). Frees the native handle on error paths. */
|
|
117
|
+
close() {
|
|
118
|
+
if (this.handle) {
|
|
119
|
+
pipeline.sessionClose(this.handle);
|
|
120
|
+
this.handle = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Pick the best available encoder for `config` and open a session on it.
|
|
125
|
+
* Throws EncoderUnavailableError when no backend exists on this machine. */
|
|
126
|
+
export async function openAutoEncoder(config) {
|
|
127
|
+
const raw = config.toAbi();
|
|
128
|
+
const outEncoder = [null];
|
|
129
|
+
checkPipeline(pipeline.autoEncoderOpen(raw, outEncoder));
|
|
130
|
+
if (!outEncoder[0])
|
|
131
|
+
throw new MediawayError(11, "encoder open returned no handle");
|
|
132
|
+
return new EncodeSession(config, outEncoder[0]);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* An opened auto audio encoder — the session IS the encoder (ABI v2,
|
|
136
|
+
* adr/0003): single-step open, no intermediate handle, no consumption trap;
|
|
137
|
+
* `close()` is always safe (idempotent).
|
|
138
|
+
*/
|
|
139
|
+
export class AudioEncoder {
|
|
140
|
+
sampleRate;
|
|
141
|
+
channels;
|
|
142
|
+
timeBase;
|
|
143
|
+
handle;
|
|
144
|
+
constructor(handle, config, timeBase) {
|
|
145
|
+
this.handle = handle;
|
|
146
|
+
this.sampleRate = config.sampleRate;
|
|
147
|
+
this.channels = config.channels;
|
|
148
|
+
this.timeBase = timeBase;
|
|
149
|
+
}
|
|
150
|
+
/** Open the best available audio encoder. Throws EncoderUnavailableError
|
|
151
|
+
* when no audio backend exists on this machine. */
|
|
152
|
+
static async open(config) {
|
|
153
|
+
const tb = config.timeBase ?? { num: 1, den: config.sampleRate };
|
|
154
|
+
const raw = {
|
|
155
|
+
codec: { aac: 4 }[config.codec ?? "aac"] ?? 4,
|
|
156
|
+
sample_rate: config.sampleRate,
|
|
157
|
+
channels: config.channels,
|
|
158
|
+
sample_format: { s16: 0, s32: 1, f32: 2 }[config.sampleFormat ?? "f32"] ?? 2,
|
|
159
|
+
time_base: { num: BigInt(tb.num), den: tb.den },
|
|
160
|
+
bitrate_bps: config.bitrateBps ?? 0,
|
|
161
|
+
};
|
|
162
|
+
const out = [null];
|
|
163
|
+
checkPipeline(pipeline.audioEncoderOpen(raw, out));
|
|
164
|
+
if (!out[0])
|
|
165
|
+
throw new MediawayError(11, "audio encoder open returned no handle");
|
|
166
|
+
return new AudioEncoder(out[0], config, tb);
|
|
167
|
+
}
|
|
168
|
+
/** Push one interleaved f32le PCM chunk (borrowed — copied synchronously). */
|
|
169
|
+
async pushPcm(frame) {
|
|
170
|
+
const samples = frame.duration ?? frame.data.length / 4 / this.channels;
|
|
171
|
+
const raw = {
|
|
172
|
+
pts: BigInt(frame.pts),
|
|
173
|
+
duration: BigInt(Math.round(samples)),
|
|
174
|
+
sample_rate: this.sampleRate,
|
|
175
|
+
channels: this.channels,
|
|
176
|
+
sample_format: 2, // F32
|
|
177
|
+
data: frame.data,
|
|
178
|
+
data_len: frame.data.length,
|
|
179
|
+
};
|
|
180
|
+
checkPipeline(pipeline.audioPushPcm(this.handle, raw));
|
|
181
|
+
}
|
|
182
|
+
/** Pull the next encoded packet, if one is ready. null is a valid "nothing
|
|
183
|
+
* ready" result, not an error. */
|
|
184
|
+
async pollPacket() {
|
|
185
|
+
const raw = {};
|
|
186
|
+
const has = [false];
|
|
187
|
+
checkPipeline(pipeline.audioPollPacket(this.handle, raw, has));
|
|
188
|
+
if (!has[0])
|
|
189
|
+
return null;
|
|
190
|
+
const data = copyBytes(raw.payload, raw.payload_len);
|
|
191
|
+
pipeline.pipelinePacketFree(raw);
|
|
192
|
+
return {
|
|
193
|
+
pts: Number(raw.pts),
|
|
194
|
+
dts: Number(raw.dts),
|
|
195
|
+
duration: Number(raw.duration),
|
|
196
|
+
keyframe: raw.is_keyframe,
|
|
197
|
+
data,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
/** Signal end of input; drain the remaining packets with pollPacket(). */
|
|
201
|
+
async flush() {
|
|
202
|
+
checkPipeline(pipeline.audioFlush(this.handle));
|
|
203
|
+
}
|
|
204
|
+
/** Codec config (AudioSpecificConfig) + negotiated rates — available after
|
|
205
|
+
* the first pushed frame (adr/0003 call order: push, then streamInfo, then
|
|
206
|
+
* mux). */
|
|
207
|
+
async streamInfo() {
|
|
208
|
+
const raw = {};
|
|
209
|
+
checkPipeline(pipeline.audioStreamInfo(this.handle, raw));
|
|
210
|
+
const extraData = copyBytes(raw.extra_data, raw.extra_data_len);
|
|
211
|
+
pipeline.pipelineStreamInfoFree(raw);
|
|
212
|
+
return {
|
|
213
|
+
codec: ["aac"][raw.codec - 4] ?? "aac",
|
|
214
|
+
sampleRate: raw.sample_rate,
|
|
215
|
+
channels: raw.channels,
|
|
216
|
+
extraData,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/** Always safe — no handle-consumption trap on this surface (adr/0003). */
|
|
220
|
+
close() {
|
|
221
|
+
if (this.handle) {
|
|
222
|
+
pipeline.audioSessionClose(this.handle);
|
|
223
|
+
this.handle = null;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mediaway/encoder",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Mediaway auto video/audio encode to fMP4 over the C ABI (mediaway-ffi)",
|
|
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
|
+
}
|