@mediaway/container 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.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @mediaway/container — container capability: sans-io fragmented-MP4 mux + demux.
3
+ *
4
+ * Implements the DX contract in bindings/nodejs/README.md over the
5
+ * mediaway-ffi C ABI (via @mediaway/ffi). Typed structs in, typed
6
+ * structs out; the muxer never touches files — the caller owns byte I/O.
7
+ * `pts`/`duration` are integer ticks of each track's `timeBase`.
8
+ */
9
+ export interface Rational {
10
+ num: number;
11
+ den: number;
12
+ }
13
+ export type VideoCodec = "h264" | "av1" | "vp9";
14
+ export type AudioCodec = "aac" | "opus";
15
+ export type PixelFormat = "nv12" | "bgra8" | "rgba8" | "i420" | "yuy2";
16
+ export interface VideoTrackInfo {
17
+ codec: VideoCodec;
18
+ width: number;
19
+ height: number;
20
+ timeBase: Rational;
21
+ pixelFormat?: PixelFormat;
22
+ /** Codec config (avcC etc.); empty = placeholder written. */
23
+ extraData?: Buffer;
24
+ }
25
+ export interface AudioTrackInfo {
26
+ codec: AudioCodec;
27
+ sampleRate: number;
28
+ channels: number;
29
+ timeBase: Rational;
30
+ bitDepth?: number;
31
+ /** Codec config (AudioSpecificConfig); empty = placeholder written. */
32
+ extraData?: Buffer;
33
+ }
34
+ export type TrackInfo = ({
35
+ type: "video";
36
+ } & VideoTrackInfo) | ({
37
+ type: "audio";
38
+ } & AudioTrackInfo);
39
+ export interface Packet {
40
+ /** Index into `Demuxer.streams()` (demux side) / track index (mux side). */
41
+ trackIndex: number;
42
+ data: Buffer;
43
+ /** Ticks of the track's timeBase. */
44
+ pts: number;
45
+ /** Ticks of the track's timeBase. */
46
+ duration: number;
47
+ /** Sync sample / keyframe. */
48
+ key?: boolean;
49
+ }
50
+ /** Raw per-crate ABI status from mediaway-ffi (mediaway_status_t). */
51
+ export declare class MediawayError extends Error {
52
+ readonly status: number;
53
+ constructor(status: number, message?: string);
54
+ }
55
+ /**
56
+ * A muxer in the track-registration (Open) state. Stream indices are assigned
57
+ * in registration order: the first `add*Track` call returns 0, the second 1.
58
+ * `begin()` makes the muxer live; after it, track registration fails.
59
+ */
60
+ export declare class Muxer {
61
+ private handle;
62
+ private nextIndex;
63
+ constructor();
64
+ addVideoTrack(info: VideoTrackInfo): number;
65
+ addAudioTrack(info: AudioTrackInfo): number;
66
+ /**
67
+ * Make the muxer live and return the bytes queued so far (the init segment,
68
+ * ftyp + moov — may be empty until the first media bytes are flushed; the
69
+ * ABI emits the init segment lazily with the first fragment).
70
+ */
71
+ begin(): Buffer;
72
+ /** Push one packet (sync; the muxer never blocks on I/O). */
73
+ push(packet: Packet): void;
74
+ /** End of input — finalizes the last fragment. */
75
+ flush(): void;
76
+ /** Drain bytes queued since the last call; empty Buffer when nothing is pending. */
77
+ pollBytes(): Buffer;
78
+ close(): void;
79
+ }
80
+ /**
81
+ * A streaming demuxer: feed container bytes, poll streams and packets.
82
+ * `packet.trackIndex` indexes the `streams()` list.
83
+ */
84
+ export declare class Demuxer {
85
+ private handle;
86
+ private streamIndexById;
87
+ constructor();
88
+ pushBytes(bytes: Buffer): void;
89
+ /** Streams discovered so far; empty until the init segment has been parsed. */
90
+ streams(): TrackInfo[];
91
+ /** The next demuxed packet, or null when the stream is exhausted. */
92
+ pollPacket(): Packet | null;
93
+ close(): void;
94
+ }
package/dist/index.js ADDED
@@ -0,0 +1,198 @@
1
+ /**
2
+ * @mediaway/container — container capability: sans-io fragmented-MP4 mux + demux.
3
+ *
4
+ * Implements the DX contract in bindings/nodejs/README.md over the
5
+ * mediaway-ffi C ABI (via @mediaway/ffi). Typed structs in, typed
6
+ * structs out; the muxer never touches files — the caller owns byte I/O.
7
+ * `pts`/`duration` are integer ticks of each track's `timeBase`.
8
+ */
9
+ import { MwPacket, MwPacketView, MwRational, MwStreamInfo, MwVideoTrackInfo, MwAudioTrackInfo, container, copyBytes, } from "@mediaway/ffi";
10
+ /** Raw per-crate ABI status from mediaway-ffi (mediaway_status_t). */
11
+ export class MediawayError extends Error {
12
+ status;
13
+ constructor(status, message) {
14
+ super(message ?? `mediaway error (status ${status})`);
15
+ this.status = status;
16
+ }
17
+ }
18
+ const CODEC_TO_ABI = { h264: 0, hevc: 1, av1: 2, vp9: 3, aac: 4, opus: 5 };
19
+ const ABI_TO_CODEC = Object.fromEntries(Object.entries(CODEC_TO_ABI).map(([k, v]) => [v, k]));
20
+ function check(status) {
21
+ if (status === 0)
22
+ return;
23
+ const names = {
24
+ 1: "invalid argument",
25
+ 2: "invalid state (typestate violation)",
26
+ 3: "invalid or duplicate track id",
27
+ 4: "packet does not match a registered track",
28
+ 5: "truncated or malformed container data",
29
+ 6: "unknown error",
30
+ 7: "internal panic (handle poisoned)",
31
+ 8: "handle poisoned by an earlier panic",
32
+ };
33
+ throw new MediawayError(status, names[status] ?? "unknown container error");
34
+ }
35
+ // ── Muxer ──────────────────────────────────────────────────────────────────────
36
+ /**
37
+ * A muxer in the track-registration (Open) state. Stream indices are assigned
38
+ * in registration order: the first `add*Track` call returns 0, the second 1.
39
+ * `begin()` makes the muxer live; after it, track registration fails.
40
+ */
41
+ export class Muxer {
42
+ handle;
43
+ nextIndex = 0;
44
+ constructor() {
45
+ this.handle = container.muxerCreate();
46
+ if (!this.handle)
47
+ throw new MediawayError(7, "muxer creation panicked");
48
+ }
49
+ addVideoTrack(info) {
50
+ const index = this.nextIndex++;
51
+ const raw = {
52
+ id: index,
53
+ codec: CODEC_TO_ABI[info.codec] ?? 0,
54
+ time_base: { num: BigInt(info.timeBase.num), den: info.timeBase.den },
55
+ width: info.width,
56
+ height: info.height,
57
+ extra_data: info.extraData ?? null,
58
+ extra_data_len: info.extraData?.length ?? 0,
59
+ };
60
+ check(container.muxerAddVideoTrack(this.handle, raw));
61
+ return index;
62
+ }
63
+ addAudioTrack(info) {
64
+ const index = this.nextIndex++;
65
+ const raw = {
66
+ id: index,
67
+ codec: CODEC_TO_ABI[info.codec] ?? 4,
68
+ time_base: { num: BigInt(info.timeBase.num), den: info.timeBase.den },
69
+ sample_rate: info.sampleRate,
70
+ channels: info.channels,
71
+ extra_data: info.extraData ?? null,
72
+ extra_data_len: info.extraData?.length ?? 0,
73
+ };
74
+ check(container.muxerAddAudioTrack(this.handle, raw));
75
+ return index;
76
+ }
77
+ /**
78
+ * Make the muxer live and return the bytes queued so far (the init segment,
79
+ * ftyp + moov — may be empty until the first media bytes are flushed; the
80
+ * ABI emits the init segment lazily with the first fragment).
81
+ */
82
+ begin() {
83
+ check(container.muxerBegin(this.handle));
84
+ return this.pollBytes();
85
+ }
86
+ /** Push one packet (sync; the muxer never blocks on I/O). */
87
+ push(packet) {
88
+ const raw = {
89
+ stream_id: packet.trackIndex,
90
+ pts: BigInt(packet.pts),
91
+ dts: BigInt(packet.pts), // dts defaults to pts (no B-frames in the DX contract)
92
+ duration: BigInt(packet.duration),
93
+ is_keyframe: packet.key ?? false,
94
+ is_discard: false,
95
+ payload: packet.data,
96
+ payload_len: packet.data.length,
97
+ };
98
+ check(container.muxerPushPacket(this.handle, raw));
99
+ }
100
+ /** End of input — finalizes the last fragment. */
101
+ flush() {
102
+ check(container.muxerFlush(this.handle));
103
+ }
104
+ /** Drain bytes queued since the last call; empty Buffer when nothing is pending. */
105
+ pollBytes() {
106
+ const outData = [null];
107
+ const outLen = [0];
108
+ check(container.muxerPollBytes(this.handle, outData, outLen));
109
+ const data = copyBytes(outData[0], outLen[0]);
110
+ if (outLen[0] > 0)
111
+ container.bufferFree(outData[0], outLen[0]);
112
+ return data;
113
+ }
114
+ close() {
115
+ if (this.handle) {
116
+ container.muxerClose(this.handle);
117
+ this.handle = null;
118
+ }
119
+ }
120
+ }
121
+ // ── Demuxer ────────────────────────────────────────────────────────────────────
122
+ /**
123
+ * A streaming demuxer: feed container bytes, poll streams and packets.
124
+ * `packet.trackIndex` indexes the `streams()` list.
125
+ */
126
+ export class Demuxer {
127
+ handle;
128
+ streamIndexById = new Map();
129
+ constructor() {
130
+ this.handle = container.demuxerCreate();
131
+ if (!this.handle)
132
+ throw new MediawayError(7, "demuxer creation panicked");
133
+ }
134
+ pushBytes(bytes) {
135
+ check(container.demuxerPushBytes(this.handle, bytes, bytes.length));
136
+ }
137
+ /** Streams discovered so far; empty until the init segment has been parsed. */
138
+ streams() {
139
+ const count = container.demuxerStreamCount(this.handle);
140
+ const out = [];
141
+ this.streamIndexById.clear();
142
+ for (let i = 0; i < count; i++) {
143
+ const raw = {};
144
+ check(container.demuxerStreamAt(this.handle, i, raw));
145
+ const extra = copyBytes(raw.extra_data, raw.extra_data_len);
146
+ container.streamInfoFree(raw);
147
+ this.streamIndexById.set(raw.id, i);
148
+ const common = {
149
+ timeBase: { num: Number(raw.time_base.num), den: raw.time_base.den },
150
+ };
151
+ if (raw.has_geometry) {
152
+ out.push({
153
+ type: "video",
154
+ codec: (ABI_TO_CODEC[raw.codec] ?? "h264"),
155
+ width: raw.width,
156
+ height: raw.height,
157
+ extraData: extra,
158
+ ...common,
159
+ });
160
+ }
161
+ else {
162
+ out.push({
163
+ type: "audio",
164
+ codec: (ABI_TO_CODEC[raw.codec] ?? "aac"),
165
+ sampleRate: raw.sample_rate,
166
+ channels: raw.channels,
167
+ extraData: extra,
168
+ ...common,
169
+ });
170
+ }
171
+ }
172
+ return out;
173
+ }
174
+ /** The next demuxed packet, or null when the stream is exhausted. */
175
+ pollPacket() {
176
+ const raw = {};
177
+ const has = [false];
178
+ check(container.demuxerPollPacket(this.handle, raw, has));
179
+ if (!has[0])
180
+ return null;
181
+ const payload = copyBytes(raw.payload, raw.payload_len);
182
+ container.packetFree(raw);
183
+ const trackIndex = this.streamIndexById.get(raw.stream_id) ?? raw.stream_id;
184
+ return {
185
+ trackIndex,
186
+ data: payload,
187
+ pts: Number(raw.pts),
188
+ duration: Number(raw.duration),
189
+ key: raw.is_keyframe,
190
+ };
191
+ }
192
+ close() {
193
+ if (this.handle) {
194
+ container.demuxerClose(this.handle);
195
+ this.handle = null;
196
+ }
197
+ }
198
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@mediaway/container",
3
+ "version": "0.1.0",
4
+ "description": "Mediaway fMP4 mux/demux 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
+ },
31
+ "devDependencies": {
32
+ "typescript": "^5.6.0"
33
+ }
34
+ }