@mediaway/container 0.1.3 → 0.1.5
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/adts.d.ts +35 -0
- package/dist/adts.js +100 -0
- package/dist/flv.d.ts +39 -0
- package/dist/flv.js +156 -0
- package/dist/index.d.ts +20 -5
- package/dist/index.js +37 -9
- package/dist/mp3.d.ts +45 -0
- package/dist/mp3.js +97 -0
- package/dist/ogg.d.ts +34 -0
- package/dist/ogg.js +102 -0
- package/dist/ts.d.ts +46 -0
- package/dist/ts.js +139 -0
- package/dist/wav.d.ts +41 -0
- package/dist/wav.js +100 -0
- package/package.json +3 -3
package/dist/adts.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mediaway/container — ADTS mux + demux (adr/container/0004-ogg-adts-c-abi.md).
|
|
3
|
+
*
|
|
4
|
+
* Same dedicated-handle reasoning as `ogg.ts`: ADTS has no track-registration
|
|
5
|
+
* step and no Open/Live typestate.
|
|
6
|
+
*/
|
|
7
|
+
import { type Packet } from "./index.js";
|
|
8
|
+
export interface AdtsStreamInfo {
|
|
9
|
+
sampleRate: number;
|
|
10
|
+
channels: number;
|
|
11
|
+
extraData: Buffer;
|
|
12
|
+
}
|
|
13
|
+
export declare class AdtsMuxer {
|
|
14
|
+
private handle;
|
|
15
|
+
/** `sampleRate` must be a standard ADTS sample rate. */
|
|
16
|
+
constructor(sampleRate: number, channels: number);
|
|
17
|
+
/** Append one AAC frame (raw, ADTS header added). Fails with INVALID_PACKET
|
|
18
|
+
* if the payload is too large for ADTS's 13-bit frame-length field. */
|
|
19
|
+
push(packet: Packet): void;
|
|
20
|
+
/** No-op — ADTS frames are independently appendable. */
|
|
21
|
+
flush(): void;
|
|
22
|
+
pollBytes(): Buffer;
|
|
23
|
+
close(): void;
|
|
24
|
+
}
|
|
25
|
+
export declare class AdtsDemuxer {
|
|
26
|
+
private handle;
|
|
27
|
+
constructor();
|
|
28
|
+
pushBytes(bytes: Buffer): void;
|
|
29
|
+
/** Streams discovered so far — 0 or 1 (ADTS carries a single implicit stream). */
|
|
30
|
+
streams(): AdtsStreamInfo[];
|
|
31
|
+
/** pts/duration are synthesized from a running 1024-samples-per-frame
|
|
32
|
+
* count — ADTS carries no per-frame timing of its own. */
|
|
33
|
+
pollPacket(): Packet | null;
|
|
34
|
+
close(): void;
|
|
35
|
+
}
|
package/dist/adts.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mediaway/container — ADTS mux + demux (adr/container/0004-ogg-adts-c-abi.md).
|
|
3
|
+
*
|
|
4
|
+
* Same dedicated-handle reasoning as `ogg.ts`: ADTS has no track-registration
|
|
5
|
+
* step and no Open/Live typestate.
|
|
6
|
+
*/
|
|
7
|
+
import { container, copyBytes } from "@mediaway/ffi";
|
|
8
|
+
import { MediawayError, check } from "./index.js";
|
|
9
|
+
export class AdtsMuxer {
|
|
10
|
+
handle;
|
|
11
|
+
/** `sampleRate` must be a standard ADTS sample rate. */
|
|
12
|
+
constructor(sampleRate, channels) {
|
|
13
|
+
this.handle = container.adtsMuxerCreate(sampleRate, channels);
|
|
14
|
+
if (!this.handle) {
|
|
15
|
+
throw new MediawayError(1, `non-standard ADTS sample rate (${sampleRate} Hz), or the native call panicked`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Append one AAC frame (raw, ADTS header added). Fails with INVALID_PACKET
|
|
19
|
+
* if the payload is too large for ADTS's 13-bit frame-length field. */
|
|
20
|
+
push(packet) {
|
|
21
|
+
const raw = {
|
|
22
|
+
stream_id: packet.trackIndex,
|
|
23
|
+
pts: BigInt(packet.pts),
|
|
24
|
+
dts: BigInt(packet.pts),
|
|
25
|
+
duration: BigInt(packet.duration),
|
|
26
|
+
is_keyframe: packet.key ?? false,
|
|
27
|
+
is_discard: false,
|
|
28
|
+
payload: packet.data,
|
|
29
|
+
payload_len: packet.data.length,
|
|
30
|
+
};
|
|
31
|
+
check(container.adtsMuxerPushPacket(this.handle, raw));
|
|
32
|
+
}
|
|
33
|
+
/** No-op — ADTS frames are independently appendable. */
|
|
34
|
+
flush() {
|
|
35
|
+
check(container.adtsMuxerFlush(this.handle));
|
|
36
|
+
}
|
|
37
|
+
pollBytes() {
|
|
38
|
+
const outData = [null];
|
|
39
|
+
const outLen = [0];
|
|
40
|
+
check(container.adtsMuxerPollBytes(this.handle, outData, outLen));
|
|
41
|
+
const data = copyBytes(outData[0], outLen[0]);
|
|
42
|
+
if (outLen[0] > 0)
|
|
43
|
+
container.bufferFree(outData[0], outLen[0]);
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
close() {
|
|
47
|
+
if (this.handle) {
|
|
48
|
+
container.adtsMuxerClose(this.handle);
|
|
49
|
+
this.handle = null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export class AdtsDemuxer {
|
|
54
|
+
handle;
|
|
55
|
+
constructor() {
|
|
56
|
+
this.handle = container.adtsDemuxerCreate();
|
|
57
|
+
if (!this.handle)
|
|
58
|
+
throw new MediawayError(7, "ADTS demuxer creation panicked");
|
|
59
|
+
}
|
|
60
|
+
pushBytes(bytes) {
|
|
61
|
+
check(container.adtsDemuxerPushBytes(this.handle, bytes, bytes.length));
|
|
62
|
+
}
|
|
63
|
+
/** Streams discovered so far — 0 or 1 (ADTS carries a single implicit stream). */
|
|
64
|
+
streams() {
|
|
65
|
+
const count = container.adtsDemuxerStreamCount(this.handle);
|
|
66
|
+
const out = [];
|
|
67
|
+
for (let i = 0; i < count; i++) {
|
|
68
|
+
const raw = {};
|
|
69
|
+
check(container.adtsDemuxerStreamAt(this.handle, i, raw));
|
|
70
|
+
const extraData = copyBytes(raw.extra_data, raw.extra_data_len);
|
|
71
|
+
container.streamInfoFree(raw);
|
|
72
|
+
out.push({ sampleRate: raw.sample_rate, channels: raw.channels, extraData });
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
/** pts/duration are synthesized from a running 1024-samples-per-frame
|
|
77
|
+
* count — ADTS carries no per-frame timing of its own. */
|
|
78
|
+
pollPacket() {
|
|
79
|
+
const raw = {};
|
|
80
|
+
const has = [false];
|
|
81
|
+
check(container.adtsDemuxerPollPacket(this.handle, raw, has));
|
|
82
|
+
if (!has[0])
|
|
83
|
+
return null;
|
|
84
|
+
const payload = copyBytes(raw.payload, raw.payload_len);
|
|
85
|
+
container.packetFree(raw);
|
|
86
|
+
return {
|
|
87
|
+
trackIndex: raw.stream_id,
|
|
88
|
+
data: payload,
|
|
89
|
+
pts: Number(raw.pts),
|
|
90
|
+
duration: Number(raw.duration),
|
|
91
|
+
key: raw.is_keyframe,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
close() {
|
|
95
|
+
if (this.handle) {
|
|
96
|
+
container.adtsDemuxerClose(this.handle);
|
|
97
|
+
this.handle = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
package/dist/flv.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mediaway/container — FLV mux + demux (adr/container/0005-flv-c-abi.md).
|
|
3
|
+
*
|
|
4
|
+
* Unlike `Muxer`, every write method here returns its own freshly allocated
|
|
5
|
+
* output buffer directly — there is no separate `pollBytes` step. FLV has
|
|
6
|
+
* exactly one video and one audio slot (no track-id field in the format
|
|
7
|
+
* itself); `addVideoTrack`/`addAudioTrack` ignore the info's own id and the
|
|
8
|
+
* fixed `VIDEO_TRACK_ID`/`AUDIO_TRACK_ID` are used instead.
|
|
9
|
+
*/
|
|
10
|
+
import { type AudioTrackInfo, type Packet, type TrackInfo, type VideoTrackInfo } from "./index.js";
|
|
11
|
+
export declare const VIDEO_TRACK_ID = 0;
|
|
12
|
+
export declare const AUDIO_TRACK_ID = 1;
|
|
13
|
+
export declare class FlvMuxer {
|
|
14
|
+
private handle;
|
|
15
|
+
constructor();
|
|
16
|
+
/** Write the FLV file header. Call before any track registration or packet. */
|
|
17
|
+
writeHeader(hasAudio: boolean, hasVideo: boolean): Buffer;
|
|
18
|
+
/** Register the video track. Only H264 is recognized (UNSUPPORTED_CODEC otherwise). */
|
|
19
|
+
addVideoTrack(info: VideoTrackInfo): number;
|
|
20
|
+
/** Register the audio track. AAC and MP3 are recognized (UNSUPPORTED_CODEC otherwise). */
|
|
21
|
+
addAudioTrack(info: AudioTrackInfo): number;
|
|
22
|
+
/** Mux one packet: writes the track's sequence-header tag first (once,
|
|
23
|
+
* only for codecs that have one) then the data tag. `packet.trackIndex`
|
|
24
|
+
* selects `VIDEO_TRACK_ID`/`AUDIO_TRACK_ID` and must have a matching
|
|
25
|
+
* `addVideoTrack`/`addAudioTrack` call already made, else UNKNOWN_STREAM. */
|
|
26
|
+
push(packet: Packet): Buffer;
|
|
27
|
+
close(): void;
|
|
28
|
+
}
|
|
29
|
+
export declare class FlvDemuxer {
|
|
30
|
+
private handle;
|
|
31
|
+
constructor();
|
|
32
|
+
pushBytes(bytes: Buffer): void;
|
|
33
|
+
/** Streams recognized so far — 0, 1, or 2 (fixed video-then-audio slots). */
|
|
34
|
+
streams(): TrackInfo[];
|
|
35
|
+
/** Sequence-header tags (AVC/AAC config) update the matching stream's
|
|
36
|
+
* extra data internally and are not themselves returned as packets. */
|
|
37
|
+
pollPacket(): Packet | null;
|
|
38
|
+
close(): void;
|
|
39
|
+
}
|
package/dist/flv.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mediaway/container — FLV mux + demux (adr/container/0005-flv-c-abi.md).
|
|
3
|
+
*
|
|
4
|
+
* Unlike `Muxer`, every write method here returns its own freshly allocated
|
|
5
|
+
* output buffer directly — there is no separate `pollBytes` step. FLV has
|
|
6
|
+
* exactly one video and one audio slot (no track-id field in the format
|
|
7
|
+
* itself); `addVideoTrack`/`addAudioTrack` ignore the info's own id and the
|
|
8
|
+
* fixed `VIDEO_TRACK_ID`/`AUDIO_TRACK_ID` are used instead.
|
|
9
|
+
*/
|
|
10
|
+
import { container, copyBytes, } from "@mediaway/ffi";
|
|
11
|
+
import { ABI_TO_CODEC, CODEC_TO_ABI, MediawayError, check, } from "./index.js";
|
|
12
|
+
export const VIDEO_TRACK_ID = 0;
|
|
13
|
+
export const AUDIO_TRACK_ID = 1;
|
|
14
|
+
export class FlvMuxer {
|
|
15
|
+
handle;
|
|
16
|
+
constructor() {
|
|
17
|
+
this.handle = container.flvMuxerCreate();
|
|
18
|
+
if (!this.handle)
|
|
19
|
+
throw new MediawayError(7, "FLV muxer creation panicked");
|
|
20
|
+
}
|
|
21
|
+
/** Write the FLV file header. Call before any track registration or packet. */
|
|
22
|
+
writeHeader(hasAudio, hasVideo) {
|
|
23
|
+
const outData = [null];
|
|
24
|
+
const outLen = [0];
|
|
25
|
+
check(container.flvMuxerWriteHeader(this.handle, hasAudio, hasVideo, outData, outLen));
|
|
26
|
+
const data = copyBytes(outData[0], outLen[0]);
|
|
27
|
+
if (outLen[0] > 0)
|
|
28
|
+
container.bufferFree(outData[0], outLen[0]);
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
/** Register the video track. Only H264 is recognized (UNSUPPORTED_CODEC otherwise). */
|
|
32
|
+
addVideoTrack(info) {
|
|
33
|
+
const raw = {
|
|
34
|
+
id: 0,
|
|
35
|
+
codec: CODEC_TO_ABI[info.codec] ?? 0,
|
|
36
|
+
time_base: { num: BigInt(info.timeBase.num), den: info.timeBase.den },
|
|
37
|
+
width: info.width,
|
|
38
|
+
height: info.height,
|
|
39
|
+
extra_data: info.extraData ?? null,
|
|
40
|
+
extra_data_len: info.extraData?.length ?? 0,
|
|
41
|
+
};
|
|
42
|
+
check(container.flvMuxerAddVideoTrack(this.handle, raw));
|
|
43
|
+
return VIDEO_TRACK_ID;
|
|
44
|
+
}
|
|
45
|
+
/** Register the audio track. AAC and MP3 are recognized (UNSUPPORTED_CODEC otherwise). */
|
|
46
|
+
addAudioTrack(info) {
|
|
47
|
+
const raw = {
|
|
48
|
+
id: 0,
|
|
49
|
+
codec: CODEC_TO_ABI[info.codec] ?? 4,
|
|
50
|
+
time_base: { num: BigInt(info.timeBase.num), den: info.timeBase.den },
|
|
51
|
+
sample_rate: info.sampleRate,
|
|
52
|
+
channels: info.channels,
|
|
53
|
+
extra_data: info.extraData ?? null,
|
|
54
|
+
extra_data_len: info.extraData?.length ?? 0,
|
|
55
|
+
};
|
|
56
|
+
check(container.flvMuxerAddAudioTrack(this.handle, raw));
|
|
57
|
+
return AUDIO_TRACK_ID;
|
|
58
|
+
}
|
|
59
|
+
/** Mux one packet: writes the track's sequence-header tag first (once,
|
|
60
|
+
* only for codecs that have one) then the data tag. `packet.trackIndex`
|
|
61
|
+
* selects `VIDEO_TRACK_ID`/`AUDIO_TRACK_ID` and must have a matching
|
|
62
|
+
* `addVideoTrack`/`addAudioTrack` call already made, else UNKNOWN_STREAM. */
|
|
63
|
+
push(packet) {
|
|
64
|
+
const raw = {
|
|
65
|
+
stream_id: packet.trackIndex,
|
|
66
|
+
pts: BigInt(packet.pts),
|
|
67
|
+
dts: BigInt(packet.pts),
|
|
68
|
+
duration: BigInt(packet.duration),
|
|
69
|
+
is_keyframe: packet.key ?? false,
|
|
70
|
+
is_discard: false,
|
|
71
|
+
payload: packet.data,
|
|
72
|
+
payload_len: packet.data.length,
|
|
73
|
+
};
|
|
74
|
+
const outData = [null];
|
|
75
|
+
const outLen = [0];
|
|
76
|
+
check(container.flvMuxerPushPacket(this.handle, raw, outData, outLen));
|
|
77
|
+
const data = copyBytes(outData[0], outLen[0]);
|
|
78
|
+
if (outLen[0] > 0)
|
|
79
|
+
container.bufferFree(outData[0], outLen[0]);
|
|
80
|
+
return data;
|
|
81
|
+
}
|
|
82
|
+
close() {
|
|
83
|
+
if (this.handle) {
|
|
84
|
+
container.flvMuxerClose(this.handle);
|
|
85
|
+
this.handle = null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export class FlvDemuxer {
|
|
90
|
+
handle;
|
|
91
|
+
constructor() {
|
|
92
|
+
this.handle = container.flvDemuxerCreate();
|
|
93
|
+
if (!this.handle)
|
|
94
|
+
throw new MediawayError(7, "FLV demuxer creation panicked");
|
|
95
|
+
}
|
|
96
|
+
pushBytes(bytes) {
|
|
97
|
+
check(container.flvDemuxerPushBytes(this.handle, bytes, bytes.length));
|
|
98
|
+
}
|
|
99
|
+
/** Streams recognized so far — 0, 1, or 2 (fixed video-then-audio slots). */
|
|
100
|
+
streams() {
|
|
101
|
+
const count = container.flvDemuxerStreamCount(this.handle);
|
|
102
|
+
const out = [];
|
|
103
|
+
for (let i = 0; i < count; i++) {
|
|
104
|
+
const raw = {};
|
|
105
|
+
check(container.flvDemuxerStreamAt(this.handle, i, raw));
|
|
106
|
+
const extraData = copyBytes(raw.extra_data, raw.extra_data_len);
|
|
107
|
+
container.streamInfoFree(raw);
|
|
108
|
+
const timeBase = { num: Number(raw.time_base.num), den: raw.time_base.den };
|
|
109
|
+
if (raw.has_geometry) {
|
|
110
|
+
out.push({
|
|
111
|
+
type: "video",
|
|
112
|
+
codec: (ABI_TO_CODEC[raw.codec] ?? "h264"),
|
|
113
|
+
width: raw.width,
|
|
114
|
+
height: raw.height,
|
|
115
|
+
extraData,
|
|
116
|
+
timeBase,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
out.push({
|
|
121
|
+
type: "audio",
|
|
122
|
+
codec: (ABI_TO_CODEC[raw.codec] ?? "aac"),
|
|
123
|
+
sampleRate: raw.sample_rate,
|
|
124
|
+
channels: raw.channels,
|
|
125
|
+
extraData,
|
|
126
|
+
timeBase,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
/** Sequence-header tags (AVC/AAC config) update the matching stream's
|
|
133
|
+
* extra data internally and are not themselves returned as packets. */
|
|
134
|
+
pollPacket() {
|
|
135
|
+
const raw = {};
|
|
136
|
+
const has = [false];
|
|
137
|
+
check(container.flvDemuxerPollPacket(this.handle, raw, has));
|
|
138
|
+
if (!has[0])
|
|
139
|
+
return null;
|
|
140
|
+
const payload = copyBytes(raw.payload, raw.payload_len);
|
|
141
|
+
container.packetFree(raw);
|
|
142
|
+
return {
|
|
143
|
+
trackIndex: raw.stream_id,
|
|
144
|
+
data: payload,
|
|
145
|
+
pts: Number(raw.pts),
|
|
146
|
+
duration: Number(raw.duration),
|
|
147
|
+
key: raw.is_keyframe,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
close() {
|
|
151
|
+
if (this.handle) {
|
|
152
|
+
container.flvDemuxerClose(this.handle);
|
|
153
|
+
this.handle = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,9 +10,13 @@ export interface Rational {
|
|
|
10
10
|
num: number;
|
|
11
11
|
den: number;
|
|
12
12
|
}
|
|
13
|
-
export type VideoCodec = "h264" | "av1" | "vp9";
|
|
14
|
-
export type AudioCodec = "aac" | "opus";
|
|
13
|
+
export type VideoCodec = "h264" | "hevc" | "av1" | "vp9" | "vp8";
|
|
14
|
+
export type AudioCodec = "aac" | "opus" | "mp3" | "raw_audio";
|
|
15
15
|
export type PixelFormat = "nv12" | "bgra8" | "rgba8" | "i420" | "yuy2";
|
|
16
|
+
/** Which format `Muxer`/`Demuxer` open — mirrors mediaway_container_format_t.
|
|
17
|
+
* Only formats sharing MP4's multi-track, typestated shape are reachable
|
|
18
|
+
* here; Ogg/ADTS/FLV/MPEG-TS/MP3/WAV have their own dedicated classes. */
|
|
19
|
+
export type ContainerFormat = "mp4" | "webm";
|
|
16
20
|
export interface VideoTrackInfo {
|
|
17
21
|
codec: VideoCodec;
|
|
18
22
|
width: number;
|
|
@@ -52,15 +56,20 @@ export declare class MediawayError extends Error {
|
|
|
52
56
|
readonly status: number;
|
|
53
57
|
constructor(status: number, message?: string);
|
|
54
58
|
}
|
|
59
|
+
export declare const CODEC_TO_ABI: Record<string, number>;
|
|
60
|
+
export declare const ABI_TO_CODEC: Record<number, string>;
|
|
61
|
+
export declare function check(status: number): void;
|
|
55
62
|
/**
|
|
56
63
|
* A muxer in the track-registration (Open) state. Stream indices are assigned
|
|
57
|
-
* in registration order: the first `add*Track` call returns
|
|
64
|
+
* in registration order starting at 1: the first `add*Track` call returns 1,
|
|
65
|
+
* the second 2 — not 0, since WebM/Matroska's TrackNumber element must not
|
|
66
|
+
* be 0 (MP4 tolerates 0, but there is no reason to special-case it).
|
|
58
67
|
* `begin()` makes the muxer live; after it, track registration fails.
|
|
59
68
|
*/
|
|
60
69
|
export declare class Muxer {
|
|
61
70
|
private handle;
|
|
62
71
|
private nextIndex;
|
|
63
|
-
constructor();
|
|
72
|
+
constructor(format?: ContainerFormat);
|
|
64
73
|
addVideoTrack(info: VideoTrackInfo): number;
|
|
65
74
|
addAudioTrack(info: AudioTrackInfo): number;
|
|
66
75
|
/**
|
|
@@ -84,7 +93,7 @@ export declare class Muxer {
|
|
|
84
93
|
export declare class Demuxer {
|
|
85
94
|
private handle;
|
|
86
95
|
private streamIndexById;
|
|
87
|
-
constructor();
|
|
96
|
+
constructor(format?: ContainerFormat);
|
|
88
97
|
pushBytes(bytes: Buffer): void;
|
|
89
98
|
/** Streams discovered so far; empty until the init segment has been parsed. */
|
|
90
99
|
streams(): TrackInfo[];
|
|
@@ -92,3 +101,9 @@ export declare class Demuxer {
|
|
|
92
101
|
pollPacket(): Packet | null;
|
|
93
102
|
close(): void;
|
|
94
103
|
}
|
|
104
|
+
export * from "./ogg.js";
|
|
105
|
+
export * from "./adts.js";
|
|
106
|
+
export * from "./flv.js";
|
|
107
|
+
export * from "./ts.js";
|
|
108
|
+
export * from "./mp3.js";
|
|
109
|
+
export * from "./wav.js";
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* `pts`/`duration` are integer ticks of each track's `timeBase`.
|
|
8
8
|
*/
|
|
9
9
|
import { MwPacket, MwPacketView, MwRational, MwStreamInfo, MwVideoTrackInfo, MwAudioTrackInfo, container, copyBytes, } from "@mediaway/ffi";
|
|
10
|
+
const FORMAT_TO_ABI = { mp4: 0, webm: 1 };
|
|
10
11
|
/** Raw per-crate ABI status from mediaway-ffi (mediaway_status_t). */
|
|
11
12
|
export class MediawayError extends Error {
|
|
12
13
|
status;
|
|
@@ -15,9 +16,20 @@ export class MediawayError extends Error {
|
|
|
15
16
|
this.status = status;
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
|
-
const CODEC_TO_ABI = {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
export const CODEC_TO_ABI = {
|
|
20
|
+
h264: 0,
|
|
21
|
+
hevc: 1,
|
|
22
|
+
av1: 2,
|
|
23
|
+
vp9: 3,
|
|
24
|
+
aac: 4,
|
|
25
|
+
opus: 5,
|
|
26
|
+
mp3: 6,
|
|
27
|
+
vorbis: 7,
|
|
28
|
+
raw_audio: 11,
|
|
29
|
+
vp8: 12,
|
|
30
|
+
};
|
|
31
|
+
export const ABI_TO_CODEC = Object.fromEntries(Object.entries(CODEC_TO_ABI).map(([k, v]) => [v, k]));
|
|
32
|
+
export function check(status) {
|
|
21
33
|
if (status === 0)
|
|
22
34
|
return;
|
|
23
35
|
const names = {
|
|
@@ -29,20 +41,25 @@ function check(status) {
|
|
|
29
41
|
6: "unknown error",
|
|
30
42
|
7: "internal panic (handle poisoned)",
|
|
31
43
|
8: "handle poisoned by an earlier panic",
|
|
44
|
+
9: "codec has no encoding in this container format",
|
|
45
|
+
10: "packet's stream id matches no registered track",
|
|
32
46
|
};
|
|
33
47
|
throw new MediawayError(status, names[status] ?? "unknown container error");
|
|
34
48
|
}
|
|
35
49
|
// ── Muxer ──────────────────────────────────────────────────────────────────────
|
|
36
50
|
/**
|
|
37
51
|
* A muxer in the track-registration (Open) state. Stream indices are assigned
|
|
38
|
-
* in registration order: the first `add*Track` call returns
|
|
52
|
+
* in registration order starting at 1: the first `add*Track` call returns 1,
|
|
53
|
+
* the second 2 — not 0, since WebM/Matroska's TrackNumber element must not
|
|
54
|
+
* be 0 (MP4 tolerates 0, but there is no reason to special-case it).
|
|
39
55
|
* `begin()` makes the muxer live; after it, track registration fails.
|
|
40
56
|
*/
|
|
41
57
|
export class Muxer {
|
|
42
58
|
handle;
|
|
43
|
-
nextIndex =
|
|
44
|
-
constructor() {
|
|
45
|
-
this.handle =
|
|
59
|
+
nextIndex = 1;
|
|
60
|
+
constructor(format = "mp4") {
|
|
61
|
+
this.handle =
|
|
62
|
+
format === "mp4" ? container.muxerCreate() : container.muxerCreateForFormat(FORMAT_TO_ABI[format]);
|
|
46
63
|
if (!this.handle)
|
|
47
64
|
throw new MediawayError(7, "muxer creation panicked");
|
|
48
65
|
}
|
|
@@ -126,8 +143,9 @@ export class Muxer {
|
|
|
126
143
|
export class Demuxer {
|
|
127
144
|
handle;
|
|
128
145
|
streamIndexById = new Map();
|
|
129
|
-
constructor() {
|
|
130
|
-
this.handle =
|
|
146
|
+
constructor(format = "mp4") {
|
|
147
|
+
this.handle =
|
|
148
|
+
format === "mp4" ? container.demuxerCreate() : container.demuxerCreateForFormat(FORMAT_TO_ABI[format]);
|
|
131
149
|
if (!this.handle)
|
|
132
150
|
throw new MediawayError(7, "demuxer creation panicked");
|
|
133
151
|
}
|
|
@@ -196,3 +214,13 @@ export class Demuxer {
|
|
|
196
214
|
}
|
|
197
215
|
}
|
|
198
216
|
}
|
|
217
|
+
// ── The 6 dedicated-handle formats ──────────────────────────────────────────
|
|
218
|
+
// Ogg/ADTS/FLV/MPEG-TS/MP3/WAV each need a genuinely different C ABI shape
|
|
219
|
+
// from MP4/WebM's Muxer/Demuxer (no track registration, out-buffer-per-call
|
|
220
|
+
// mux, or a construction-time stream list) — see each module's own top comment.
|
|
221
|
+
export * from "./ogg.js";
|
|
222
|
+
export * from "./adts.js";
|
|
223
|
+
export * from "./flv.js";
|
|
224
|
+
export * from "./ts.js";
|
|
225
|
+
export * from "./mp3.js";
|
|
226
|
+
export * from "./wav.js";
|
package/dist/mp3.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mediaway/container — MP3 (MPEG Layer III) mux + demux (adr/container/0007-mp3-c-abi.md).
|
|
3
|
+
*
|
|
4
|
+
* A fixed header for the mux session's lifetime (no track registration at
|
|
5
|
+
* all); `writeFrame` takes an explicit `padding` bit no `Packet` has a slot for.
|
|
6
|
+
*/
|
|
7
|
+
import { type Packet } from "./index.js";
|
|
8
|
+
export type MpegVersion = "mpeg1" | "mpeg2" | "mpeg2_5";
|
|
9
|
+
export type ChannelMode = "stereo" | "joint_stereo" | "dual_channel" | "mono";
|
|
10
|
+
/** Fixed Layer III frame header for `Mp3Muxer` — bitrate/sample rate/
|
|
11
|
+
* channel mode stay constant for the whole mux session's lifetime. */
|
|
12
|
+
export interface Mp3FrameHeader {
|
|
13
|
+
version: MpegVersion;
|
|
14
|
+
/** Must be one of the 14 standard Layer III rates for `version`. */
|
|
15
|
+
bitrateKbps: number;
|
|
16
|
+
/** Must be one of the 3 standard rates for `version`. */
|
|
17
|
+
sampleRate: number;
|
|
18
|
+
channelMode: ChannelMode;
|
|
19
|
+
}
|
|
20
|
+
export interface Mp3StreamInfo {
|
|
21
|
+
sampleRate: number;
|
|
22
|
+
channels: number;
|
|
23
|
+
extraData: Buffer;
|
|
24
|
+
}
|
|
25
|
+
export declare class Mp3Muxer {
|
|
26
|
+
private handle;
|
|
27
|
+
/** `header` must be a standard Layer III bitrate/sample-rate combination for its version. */
|
|
28
|
+
constructor(header: Mp3FrameHeader);
|
|
29
|
+
/** Append one already-encoded Layer III frame body. Fails with
|
|
30
|
+
* INVALID_PACKET when `frameBody`'s length doesn't match what the
|
|
31
|
+
* header's bitrate/sample-rate/padding combination requires. */
|
|
32
|
+
writeFrame(frameBody: Buffer, padding: boolean): Buffer;
|
|
33
|
+
close(): void;
|
|
34
|
+
}
|
|
35
|
+
export declare class Mp3Demuxer {
|
|
36
|
+
private handle;
|
|
37
|
+
constructor();
|
|
38
|
+
pushBytes(bytes: Buffer): void;
|
|
39
|
+
/** Streams discovered so far — 0 or 1 (MP3 carries a single implicit stream). */
|
|
40
|
+
streams(): Mp3StreamInfo[];
|
|
41
|
+
/** pts/duration are synthesized from a running samples-per-frame count —
|
|
42
|
+
* MPEG audio carries no per-frame timing of its own. */
|
|
43
|
+
pollPacket(): Packet | null;
|
|
44
|
+
close(): void;
|
|
45
|
+
}
|
package/dist/mp3.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mediaway/container — MP3 (MPEG Layer III) mux + demux (adr/container/0007-mp3-c-abi.md).
|
|
3
|
+
*
|
|
4
|
+
* A fixed header for the mux session's lifetime (no track registration at
|
|
5
|
+
* all); `writeFrame` takes an explicit `padding` bit no `Packet` has a slot for.
|
|
6
|
+
*/
|
|
7
|
+
import { container, copyBytes } from "@mediaway/ffi";
|
|
8
|
+
import { MediawayError, check } from "./index.js";
|
|
9
|
+
const MPEG_VERSION_TO_ABI = { mpeg1: 0, mpeg2: 1, mpeg2_5: 2 };
|
|
10
|
+
const CHANNEL_MODE_TO_ABI = {
|
|
11
|
+
stereo: 0,
|
|
12
|
+
joint_stereo: 1,
|
|
13
|
+
dual_channel: 2,
|
|
14
|
+
mono: 3,
|
|
15
|
+
};
|
|
16
|
+
export class Mp3Muxer {
|
|
17
|
+
handle;
|
|
18
|
+
/** `header` must be a standard Layer III bitrate/sample-rate combination for its version. */
|
|
19
|
+
constructor(header) {
|
|
20
|
+
const raw = {
|
|
21
|
+
version: MPEG_VERSION_TO_ABI[header.version],
|
|
22
|
+
bitrate_kbps: header.bitrateKbps,
|
|
23
|
+
sample_rate: header.sampleRate,
|
|
24
|
+
channel_mode: CHANNEL_MODE_TO_ABI[header.channelMode],
|
|
25
|
+
};
|
|
26
|
+
this.handle = container.mp3MuxerCreate(raw);
|
|
27
|
+
if (!this.handle) {
|
|
28
|
+
throw new MediawayError(1, "non-standard MP3 bitrate/sample-rate combination, or the native call panicked");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Append one already-encoded Layer III frame body. Fails with
|
|
32
|
+
* INVALID_PACKET when `frameBody`'s length doesn't match what the
|
|
33
|
+
* header's bitrate/sample-rate/padding combination requires. */
|
|
34
|
+
writeFrame(frameBody, padding) {
|
|
35
|
+
const outData = [null];
|
|
36
|
+
const outLen = [0];
|
|
37
|
+
check(container.mp3MuxerWriteFrame(this.handle, frameBody, frameBody.length, padding, outData, outLen));
|
|
38
|
+
const data = copyBytes(outData[0], outLen[0]);
|
|
39
|
+
if (outLen[0] > 0)
|
|
40
|
+
container.bufferFree(outData[0], outLen[0]);
|
|
41
|
+
return data;
|
|
42
|
+
}
|
|
43
|
+
close() {
|
|
44
|
+
if (this.handle) {
|
|
45
|
+
container.mp3MuxerClose(this.handle);
|
|
46
|
+
this.handle = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export class Mp3Demuxer {
|
|
51
|
+
handle;
|
|
52
|
+
constructor() {
|
|
53
|
+
this.handle = container.mp3DemuxerCreate();
|
|
54
|
+
if (!this.handle)
|
|
55
|
+
throw new MediawayError(7, "MP3 demuxer creation panicked");
|
|
56
|
+
}
|
|
57
|
+
pushBytes(bytes) {
|
|
58
|
+
check(container.mp3DemuxerPushBytes(this.handle, bytes, bytes.length));
|
|
59
|
+
}
|
|
60
|
+
/** Streams discovered so far — 0 or 1 (MP3 carries a single implicit stream). */
|
|
61
|
+
streams() {
|
|
62
|
+
const count = container.mp3DemuxerStreamCount(this.handle);
|
|
63
|
+
const out = [];
|
|
64
|
+
for (let i = 0; i < count; i++) {
|
|
65
|
+
const raw = {};
|
|
66
|
+
check(container.mp3DemuxerStreamAt(this.handle, i, raw));
|
|
67
|
+
const extraData = copyBytes(raw.extra_data, raw.extra_data_len);
|
|
68
|
+
container.streamInfoFree(raw);
|
|
69
|
+
out.push({ sampleRate: raw.sample_rate, channels: raw.channels, extraData });
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
/** pts/duration are synthesized from a running samples-per-frame count —
|
|
74
|
+
* MPEG audio carries no per-frame timing of its own. */
|
|
75
|
+
pollPacket() {
|
|
76
|
+
const raw = {};
|
|
77
|
+
const has = [false];
|
|
78
|
+
check(container.mp3DemuxerPollPacket(this.handle, raw, has));
|
|
79
|
+
if (!has[0])
|
|
80
|
+
return null;
|
|
81
|
+
const payload = copyBytes(raw.payload, raw.payload_len);
|
|
82
|
+
container.packetFree(raw);
|
|
83
|
+
return {
|
|
84
|
+
trackIndex: raw.stream_id,
|
|
85
|
+
data: payload,
|
|
86
|
+
pts: Number(raw.pts),
|
|
87
|
+
duration: Number(raw.duration),
|
|
88
|
+
key: raw.is_keyframe,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
close() {
|
|
92
|
+
if (this.handle) {
|
|
93
|
+
container.mp3DemuxerClose(this.handle);
|
|
94
|
+
this.handle = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
package/dist/ogg.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mediaway/container — Ogg mux + demux (adr/container/0004-ogg-adts-c-abi.md).
|
|
3
|
+
*
|
|
4
|
+
* Dedicated handles, not `Muxer`/`Demuxer`: Ogg has no track-registration
|
|
5
|
+
* step and no Open/Live typestate — `OggMuxer` is immediately ready for
|
|
6
|
+
* `push`. Reuses the shared `Packet`/`TrackInfo` shapes and `MediawayError`.
|
|
7
|
+
*/
|
|
8
|
+
import { type AudioCodec, type Packet, type VideoCodec } from "./index.js";
|
|
9
|
+
export interface OggStreamInfo {
|
|
10
|
+
codec: AudioCodec | VideoCodec;
|
|
11
|
+
sampleRate: number;
|
|
12
|
+
channels: number;
|
|
13
|
+
extraData: Buffer;
|
|
14
|
+
}
|
|
15
|
+
export declare class OggMuxer {
|
|
16
|
+
private handle;
|
|
17
|
+
constructor(serial: number);
|
|
18
|
+
/** Write one Ogg page. `packet.pts` becomes the page's granule position;
|
|
19
|
+
* fails with INVALID_DATA when the payload exceeds a single page. */
|
|
20
|
+
push(packet: Packet): void;
|
|
21
|
+
/** No-op — every `push` call already wrote a complete, independently valid Ogg page. */
|
|
22
|
+
flush(): void;
|
|
23
|
+
pollBytes(): Buffer;
|
|
24
|
+
close(): void;
|
|
25
|
+
}
|
|
26
|
+
export declare class OggDemuxer {
|
|
27
|
+
private handle;
|
|
28
|
+
constructor();
|
|
29
|
+
pushBytes(bytes: Buffer): void;
|
|
30
|
+
/** Streams discovered so far — 0 or 1 (Ogg carries a single logical bitstream). */
|
|
31
|
+
streams(): OggStreamInfo[];
|
|
32
|
+
pollPacket(): Packet | null;
|
|
33
|
+
close(): void;
|
|
34
|
+
}
|
package/dist/ogg.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mediaway/container — Ogg mux + demux (adr/container/0004-ogg-adts-c-abi.md).
|
|
3
|
+
*
|
|
4
|
+
* Dedicated handles, not `Muxer`/`Demuxer`: Ogg has no track-registration
|
|
5
|
+
* step and no Open/Live typestate — `OggMuxer` is immediately ready for
|
|
6
|
+
* `push`. Reuses the shared `Packet`/`TrackInfo` shapes and `MediawayError`.
|
|
7
|
+
*/
|
|
8
|
+
import { container, copyBytes } from "@mediaway/ffi";
|
|
9
|
+
import { ABI_TO_CODEC, MediawayError, check } from "./index.js";
|
|
10
|
+
export class OggMuxer {
|
|
11
|
+
handle;
|
|
12
|
+
constructor(serial) {
|
|
13
|
+
this.handle = container.oggMuxerCreate(serial);
|
|
14
|
+
if (!this.handle)
|
|
15
|
+
throw new MediawayError(7, "Ogg muxer creation panicked");
|
|
16
|
+
}
|
|
17
|
+
/** Write one Ogg page. `packet.pts` becomes the page's granule position;
|
|
18
|
+
* fails with INVALID_DATA when the payload exceeds a single page. */
|
|
19
|
+
push(packet) {
|
|
20
|
+
const raw = {
|
|
21
|
+
stream_id: packet.trackIndex,
|
|
22
|
+
pts: BigInt(packet.pts),
|
|
23
|
+
dts: BigInt(packet.pts),
|
|
24
|
+
duration: BigInt(packet.duration),
|
|
25
|
+
is_keyframe: packet.key ?? false,
|
|
26
|
+
is_discard: false,
|
|
27
|
+
payload: packet.data,
|
|
28
|
+
payload_len: packet.data.length,
|
|
29
|
+
};
|
|
30
|
+
check(container.oggMuxerPushPacket(this.handle, raw));
|
|
31
|
+
}
|
|
32
|
+
/** No-op — every `push` call already wrote a complete, independently valid Ogg page. */
|
|
33
|
+
flush() {
|
|
34
|
+
check(container.oggMuxerFlush(this.handle));
|
|
35
|
+
}
|
|
36
|
+
pollBytes() {
|
|
37
|
+
const outData = [null];
|
|
38
|
+
const outLen = [0];
|
|
39
|
+
check(container.oggMuxerPollBytes(this.handle, outData, outLen));
|
|
40
|
+
const data = copyBytes(outData[0], outLen[0]);
|
|
41
|
+
if (outLen[0] > 0)
|
|
42
|
+
container.bufferFree(outData[0], outLen[0]);
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
close() {
|
|
46
|
+
if (this.handle) {
|
|
47
|
+
container.oggMuxerClose(this.handle);
|
|
48
|
+
this.handle = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export class OggDemuxer {
|
|
53
|
+
handle;
|
|
54
|
+
constructor() {
|
|
55
|
+
this.handle = container.oggDemuxerCreate();
|
|
56
|
+
if (!this.handle)
|
|
57
|
+
throw new MediawayError(7, "Ogg demuxer creation panicked");
|
|
58
|
+
}
|
|
59
|
+
pushBytes(bytes) {
|
|
60
|
+
check(container.oggDemuxerPushBytes(this.handle, bytes, bytes.length));
|
|
61
|
+
}
|
|
62
|
+
/** Streams discovered so far — 0 or 1 (Ogg carries a single logical bitstream). */
|
|
63
|
+
streams() {
|
|
64
|
+
const count = container.oggDemuxerStreamCount(this.handle);
|
|
65
|
+
const out = [];
|
|
66
|
+
for (let i = 0; i < count; i++) {
|
|
67
|
+
const raw = {};
|
|
68
|
+
check(container.oggDemuxerStreamAt(this.handle, i, raw));
|
|
69
|
+
const extraData = copyBytes(raw.extra_data, raw.extra_data_len);
|
|
70
|
+
container.streamInfoFree(raw);
|
|
71
|
+
out.push({
|
|
72
|
+
codec: (ABI_TO_CODEC[raw.codec] ?? "opus"),
|
|
73
|
+
sampleRate: raw.sample_rate,
|
|
74
|
+
channels: raw.channels,
|
|
75
|
+
extraData,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
pollPacket() {
|
|
81
|
+
const raw = {};
|
|
82
|
+
const has = [false];
|
|
83
|
+
check(container.oggDemuxerPollPacket(this.handle, raw, has));
|
|
84
|
+
if (!has[0])
|
|
85
|
+
return null;
|
|
86
|
+
const payload = copyBytes(raw.payload, raw.payload_len);
|
|
87
|
+
container.packetFree(raw);
|
|
88
|
+
return {
|
|
89
|
+
trackIndex: raw.stream_id,
|
|
90
|
+
data: payload,
|
|
91
|
+
pts: Number(raw.pts),
|
|
92
|
+
duration: Number(raw.duration),
|
|
93
|
+
key: raw.is_keyframe,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
close() {
|
|
97
|
+
if (this.handle) {
|
|
98
|
+
container.oggDemuxerClose(this.handle);
|
|
99
|
+
this.handle = null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
package/dist/ts.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mediaway/container — MPEG-TS mux + demux (adr/container/0006-mpeg-ts-c-abi.md).
|
|
3
|
+
*
|
|
4
|
+
* The full elementary-stream list is fixed at construction (no `addTrack`
|
|
5
|
+
* after); `writePatPmt`/`writeAccessUnit` write directly into a freshly
|
|
6
|
+
* allocated output buffer with explicit 90 kHz `pts`/`dts` clock values —
|
|
7
|
+
* not a track-relative time base.
|
|
8
|
+
*/
|
|
9
|
+
import { type AudioCodec, type Packet, type TrackInfo, type VideoCodec } from "./index.js";
|
|
10
|
+
export interface TsElementaryStream {
|
|
11
|
+
/** TS packet identifier, must be in `2..=0x1FFF` (0/1 are reserved for PAT/CAT). */
|
|
12
|
+
pid: number;
|
|
13
|
+
/** Must be H264, HEVC, AAC, or MP3. */
|
|
14
|
+
codec: VideoCodec | AudioCodec;
|
|
15
|
+
}
|
|
16
|
+
export declare class TsMuxer {
|
|
17
|
+
private handle;
|
|
18
|
+
/** `pmtPid` and every stream's `pid` must be in `2..=0x1FFF`; every
|
|
19
|
+
* stream's codec must be H264/HEVC/AAC/MP3. */
|
|
20
|
+
constructor(programNumber: number, pmtPid: number, streams: TsElementaryStream[]);
|
|
21
|
+
/** Write PAT + PMT packets. Call once at the start and periodically
|
|
22
|
+
* thereafter — real players expect PAT/PMT to repeat. */
|
|
23
|
+
writePatPmt(): Buffer;
|
|
24
|
+
/** Packetize one access unit for `pid` into PES + TS packets. `pts90k`/
|
|
25
|
+
* `dts90k` are the real MPEG-TS 90 kHz clock values; `dts90k === null`
|
|
26
|
+
* means "no DTS". */
|
|
27
|
+
writeAccessUnit(pid: number, data: Buffer, pts90k: number, dts90k: number | null, randomAccess: boolean): Buffer;
|
|
28
|
+
close(): void;
|
|
29
|
+
}
|
|
30
|
+
export declare class TsDemuxer {
|
|
31
|
+
private handle;
|
|
32
|
+
constructor();
|
|
33
|
+
/** Feed bytes — need not be 188-byte aligned across calls. */
|
|
34
|
+
pushBytes(bytes: Buffer): void;
|
|
35
|
+
/** Streams whose stream_type maps to a recognized codec (H264/HEVC/AAC/MP3).
|
|
36
|
+
* Empty until `pollPacket` has actually consumed the PMT (lazy PSI parsing). */
|
|
37
|
+
streams(): TrackInfo[];
|
|
38
|
+
/** A PID with no recognized codec mapping is silently skipped. */
|
|
39
|
+
pollPacket(): Packet | null;
|
|
40
|
+
/** Force-emit whatever is still accumulating per PID — call once at the
|
|
41
|
+
* end of a stream so the very last access unit per PID isn't lost
|
|
42
|
+
* (MPEG-TS only confirms a PES boundary once the next packet on the same
|
|
43
|
+
* PID starts). */
|
|
44
|
+
finish(): Packet[];
|
|
45
|
+
close(): void;
|
|
46
|
+
}
|
package/dist/ts.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mediaway/container — MPEG-TS mux + demux (adr/container/0006-mpeg-ts-c-abi.md).
|
|
3
|
+
*
|
|
4
|
+
* The full elementary-stream list is fixed at construction (no `addTrack`
|
|
5
|
+
* after); `writePatPmt`/`writeAccessUnit` write directly into a freshly
|
|
6
|
+
* allocated output buffer with explicit 90 kHz `pts`/`dts` clock values —
|
|
7
|
+
* not a track-relative time base.
|
|
8
|
+
*/
|
|
9
|
+
import { MwPacket, container, copyBytes, decodeArray, } from "@mediaway/ffi";
|
|
10
|
+
import { ABI_TO_CODEC, CODEC_TO_ABI, MediawayError, check } from "./index.js";
|
|
11
|
+
export class TsMuxer {
|
|
12
|
+
handle;
|
|
13
|
+
/** `pmtPid` and every stream's `pid` must be in `2..=0x1FFF`; every
|
|
14
|
+
* stream's codec must be H264/HEVC/AAC/MP3. */
|
|
15
|
+
constructor(programNumber, pmtPid, streams) {
|
|
16
|
+
const raw = streams.map((s) => ({ pid: s.pid, codec: CODEC_TO_ABI[s.codec] ?? 0 }));
|
|
17
|
+
this.handle = container.tsMuxerCreate(programNumber, pmtPid, raw, raw.length);
|
|
18
|
+
if (!this.handle) {
|
|
19
|
+
throw new MediawayError(1, "invalid PMT/elementary-stream PID, an unsupported elementary-stream codec, or the native call panicked");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Write PAT + PMT packets. Call once at the start and periodically
|
|
23
|
+
* thereafter — real players expect PAT/PMT to repeat. */
|
|
24
|
+
writePatPmt() {
|
|
25
|
+
const outData = [null];
|
|
26
|
+
const outLen = [0];
|
|
27
|
+
check(container.tsMuxerWritePatPmt(this.handle, outData, outLen));
|
|
28
|
+
const data = copyBytes(outData[0], outLen[0]);
|
|
29
|
+
if (outLen[0] > 0)
|
|
30
|
+
container.bufferFree(outData[0], outLen[0]);
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
/** Packetize one access unit for `pid` into PES + TS packets. `pts90k`/
|
|
34
|
+
* `dts90k` are the real MPEG-TS 90 kHz clock values; `dts90k === null`
|
|
35
|
+
* means "no DTS". */
|
|
36
|
+
writeAccessUnit(pid, data, pts90k, dts90k, randomAccess) {
|
|
37
|
+
const outData = [null];
|
|
38
|
+
const outLen = [0];
|
|
39
|
+
check(container.tsMuxerWriteAccessUnit(this.handle, pid, data, data.length, BigInt(pts90k), dts90k !== null, BigInt(dts90k ?? 0), randomAccess, outData, outLen));
|
|
40
|
+
const result = copyBytes(outData[0], outLen[0]);
|
|
41
|
+
if (outLen[0] > 0)
|
|
42
|
+
container.bufferFree(outData[0], outLen[0]);
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
close() {
|
|
46
|
+
if (this.handle) {
|
|
47
|
+
container.tsMuxerClose(this.handle);
|
|
48
|
+
this.handle = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function streamInfoToManaged(raw, extraData) {
|
|
53
|
+
const timeBase = { num: Number(raw.time_base.num), den: raw.time_base.den };
|
|
54
|
+
if (raw.has_geometry) {
|
|
55
|
+
return {
|
|
56
|
+
type: "video",
|
|
57
|
+
codec: (ABI_TO_CODEC[raw.codec] ?? "h264"),
|
|
58
|
+
width: raw.width,
|
|
59
|
+
height: raw.height,
|
|
60
|
+
extraData,
|
|
61
|
+
timeBase,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
type: "audio",
|
|
66
|
+
codec: (ABI_TO_CODEC[raw.codec] ?? "aac"),
|
|
67
|
+
sampleRate: raw.sample_rate,
|
|
68
|
+
channels: raw.channels,
|
|
69
|
+
extraData,
|
|
70
|
+
timeBase,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function packetToManaged(raw) {
|
|
74
|
+
return {
|
|
75
|
+
trackIndex: raw.stream_id,
|
|
76
|
+
data: copyBytes(raw.payload, raw.payload_len),
|
|
77
|
+
pts: Number(raw.pts),
|
|
78
|
+
duration: Number(raw.duration),
|
|
79
|
+
key: raw.is_keyframe,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export class TsDemuxer {
|
|
83
|
+
handle;
|
|
84
|
+
constructor() {
|
|
85
|
+
this.handle = container.tsDemuxerCreate();
|
|
86
|
+
if (!this.handle)
|
|
87
|
+
throw new MediawayError(7, "MPEG-TS demuxer creation panicked");
|
|
88
|
+
}
|
|
89
|
+
/** Feed bytes — need not be 188-byte aligned across calls. */
|
|
90
|
+
pushBytes(bytes) {
|
|
91
|
+
check(container.tsDemuxerPushBytes(this.handle, bytes, bytes.length));
|
|
92
|
+
}
|
|
93
|
+
/** Streams whose stream_type maps to a recognized codec (H264/HEVC/AAC/MP3).
|
|
94
|
+
* Empty until `pollPacket` has actually consumed the PMT (lazy PSI parsing). */
|
|
95
|
+
streams() {
|
|
96
|
+
const count = container.tsDemuxerStreamCount(this.handle);
|
|
97
|
+
const out = [];
|
|
98
|
+
for (let i = 0; i < count; i++) {
|
|
99
|
+
const raw = {};
|
|
100
|
+
check(container.tsDemuxerStreamAt(this.handle, i, raw));
|
|
101
|
+
const extraData = copyBytes(raw.extra_data, raw.extra_data_len);
|
|
102
|
+
container.streamInfoFree(raw);
|
|
103
|
+
out.push(streamInfoToManaged(raw, extraData));
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/** A PID with no recognized codec mapping is silently skipped. */
|
|
108
|
+
pollPacket() {
|
|
109
|
+
const raw = {};
|
|
110
|
+
const has = [false];
|
|
111
|
+
check(container.tsDemuxerPollPacket(this.handle, raw, has));
|
|
112
|
+
if (!has[0])
|
|
113
|
+
return null;
|
|
114
|
+
const packet = packetToManaged(raw);
|
|
115
|
+
container.packetFree(raw);
|
|
116
|
+
return packet;
|
|
117
|
+
}
|
|
118
|
+
/** Force-emit whatever is still accumulating per PID — call once at the
|
|
119
|
+
* end of a stream so the very last access unit per PID isn't lost
|
|
120
|
+
* (MPEG-TS only confirms a PES boundary once the next packet on the same
|
|
121
|
+
* PID starts). */
|
|
122
|
+
finish() {
|
|
123
|
+
const outPackets = [null];
|
|
124
|
+
const outCount = [0];
|
|
125
|
+
check(container.tsDemuxerFinish(this.handle, outPackets, outCount));
|
|
126
|
+
const count = outCount[0];
|
|
127
|
+
const decoded = decodeArray(outPackets[0], MwPacket, count);
|
|
128
|
+
const result = decoded.map(packetToManaged);
|
|
129
|
+
if (count > 0)
|
|
130
|
+
container.tsDemuxerFinishFree(outPackets[0], count);
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
close() {
|
|
134
|
+
if (this.handle) {
|
|
135
|
+
container.tsDemuxerClose(this.handle);
|
|
136
|
+
this.handle = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
package/dist/wav.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mediaway/container — WAV mux + one-shot parse (adr/container/0008-wav-c-abi.md).
|
|
3
|
+
*
|
|
4
|
+
* `wav::Muxer::finish` consumes `self` by value on the Rust side (RIFF chunk
|
|
5
|
+
* sizes must be known up front), so there is no `pollBytes` step — `finish`
|
|
6
|
+
* returns the complete byte stream directly. Demux has NO handle at all:
|
|
7
|
+
* `parseWav` is a one-shot whole-buffer function, unlike every other format
|
|
8
|
+
* in this package.
|
|
9
|
+
*/
|
|
10
|
+
import { type Packet, type TrackInfo } from "./index.js";
|
|
11
|
+
export type WavSampleFormat = "pcm" | "float";
|
|
12
|
+
/** Explicit RIFF/WAVE fmt chunk for `WavMuxer`. */
|
|
13
|
+
export interface WaveFormat {
|
|
14
|
+
sampleFormat: WavSampleFormat;
|
|
15
|
+
channels: number;
|
|
16
|
+
sampleRate: number;
|
|
17
|
+
bitsPerSample: number;
|
|
18
|
+
}
|
|
19
|
+
export interface WavParseResult {
|
|
20
|
+
info: TrackInfo;
|
|
21
|
+
packet: Packet;
|
|
22
|
+
}
|
|
23
|
+
export declare class WavMuxer {
|
|
24
|
+
private handle;
|
|
25
|
+
private finished;
|
|
26
|
+
/** Start an integer-PCM mux session, or pass a `WaveFormat` for an
|
|
27
|
+
* explicit format (e.g. IEEE float PCM). */
|
|
28
|
+
constructor(sampleRateOrFormat: number | WaveFormat, channels?: number, bitsPerSample?: number);
|
|
29
|
+
/** Append raw interleaved PCM bytes, already encoded per the session's format. */
|
|
30
|
+
push(packet: Packet): void;
|
|
31
|
+
/** Finalize the mux session and return the complete RIFF/WAVE byte stream.
|
|
32
|
+
* Only the native-side internal state is consumed — this `WavMuxer` stays
|
|
33
|
+
* usable for `close()` afterward. A second call fails with INVALID_STATE
|
|
34
|
+
* rather than re-finalizing. */
|
|
35
|
+
finish(): Buffer;
|
|
36
|
+
get isFinished(): boolean;
|
|
37
|
+
close(): void;
|
|
38
|
+
}
|
|
39
|
+
/** Parse a complete RIFF/WAVE buffer into its single track's stream info
|
|
40
|
+
* and one packet holding the whole PCM payload. */
|
|
41
|
+
export declare function parseWav(data: Buffer): WavParseResult;
|
package/dist/wav.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mediaway/container — WAV mux + one-shot parse (adr/container/0008-wav-c-abi.md).
|
|
3
|
+
*
|
|
4
|
+
* `wav::Muxer::finish` consumes `self` by value on the Rust side (RIFF chunk
|
|
5
|
+
* sizes must be known up front), so there is no `pollBytes` step — `finish`
|
|
6
|
+
* returns the complete byte stream directly. Demux has NO handle at all:
|
|
7
|
+
* `parseWav` is a one-shot whole-buffer function, unlike every other format
|
|
8
|
+
* in this package.
|
|
9
|
+
*/
|
|
10
|
+
import { container, copyBytes } from "@mediaway/ffi";
|
|
11
|
+
import { ABI_TO_CODEC, MediawayError, check } from "./index.js";
|
|
12
|
+
const WAV_SAMPLE_FORMAT_TO_ABI = { pcm: 0, float: 1 };
|
|
13
|
+
export class WavMuxer {
|
|
14
|
+
handle;
|
|
15
|
+
finished = false;
|
|
16
|
+
/** Start an integer-PCM mux session, or pass a `WaveFormat` for an
|
|
17
|
+
* explicit format (e.g. IEEE float PCM). */
|
|
18
|
+
constructor(sampleRateOrFormat, channels, bitsPerSample) {
|
|
19
|
+
if (typeof sampleRateOrFormat === "number") {
|
|
20
|
+
this.handle = container.wavMuxerCreate(sampleRateOrFormat, channels, bitsPerSample);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
const raw = {
|
|
24
|
+
sample_format: WAV_SAMPLE_FORMAT_TO_ABI[sampleRateOrFormat.sampleFormat],
|
|
25
|
+
channels: sampleRateOrFormat.channels,
|
|
26
|
+
sample_rate: sampleRateOrFormat.sampleRate,
|
|
27
|
+
bits_per_sample: sampleRateOrFormat.bitsPerSample,
|
|
28
|
+
};
|
|
29
|
+
this.handle = container.wavMuxerCreateWithFormat(raw);
|
|
30
|
+
}
|
|
31
|
+
if (!this.handle)
|
|
32
|
+
throw new MediawayError(7, "WAV muxer creation panicked");
|
|
33
|
+
}
|
|
34
|
+
/** Append raw interleaved PCM bytes, already encoded per the session's format. */
|
|
35
|
+
push(packet) {
|
|
36
|
+
const raw = {
|
|
37
|
+
stream_id: packet.trackIndex,
|
|
38
|
+
pts: BigInt(packet.pts),
|
|
39
|
+
dts: BigInt(packet.pts),
|
|
40
|
+
duration: BigInt(packet.duration),
|
|
41
|
+
is_keyframe: packet.key ?? false,
|
|
42
|
+
is_discard: false,
|
|
43
|
+
payload: packet.data,
|
|
44
|
+
payload_len: packet.data.length,
|
|
45
|
+
};
|
|
46
|
+
check(container.wavMuxerPushPacket(this.handle, raw));
|
|
47
|
+
}
|
|
48
|
+
/** Finalize the mux session and return the complete RIFF/WAVE byte stream.
|
|
49
|
+
* Only the native-side internal state is consumed — this `WavMuxer` stays
|
|
50
|
+
* usable for `close()` afterward. A second call fails with INVALID_STATE
|
|
51
|
+
* rather than re-finalizing. */
|
|
52
|
+
finish() {
|
|
53
|
+
const outData = [null];
|
|
54
|
+
const outLen = [0];
|
|
55
|
+
check(container.wavMuxerFinish(this.handle, outData, outLen));
|
|
56
|
+
this.finished = true;
|
|
57
|
+
const data = copyBytes(outData[0], outLen[0]);
|
|
58
|
+
if (outLen[0] > 0)
|
|
59
|
+
container.bufferFree(outData[0], outLen[0]);
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
get isFinished() {
|
|
63
|
+
return this.finished;
|
|
64
|
+
}
|
|
65
|
+
close() {
|
|
66
|
+
if (this.handle) {
|
|
67
|
+
container.wavMuxerClose(this.handle);
|
|
68
|
+
this.handle = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Parse a complete RIFF/WAVE buffer into its single track's stream info
|
|
73
|
+
* and one packet holding the whole PCM payload. */
|
|
74
|
+
export function parseWav(data) {
|
|
75
|
+
const outInfo = {};
|
|
76
|
+
const outPacket = {};
|
|
77
|
+
check(container.wavParse(data, data.length, outInfo, outPacket));
|
|
78
|
+
const extraData = copyBytes(outInfo.extra_data, outInfo.extra_data_len);
|
|
79
|
+
const timeBase = { num: Number(outInfo.time_base.num), den: outInfo.time_base.den };
|
|
80
|
+
// WAV always parses to an audio track (RIFF/WAVE has no video concept).
|
|
81
|
+
const info = {
|
|
82
|
+
type: "audio",
|
|
83
|
+
codec: (ABI_TO_CODEC[outInfo.codec] ?? "raw_audio"),
|
|
84
|
+
sampleRate: outInfo.sample_rate,
|
|
85
|
+
channels: outInfo.channels,
|
|
86
|
+
extraData,
|
|
87
|
+
timeBase,
|
|
88
|
+
};
|
|
89
|
+
container.streamInfoFree(outInfo);
|
|
90
|
+
const payload = copyBytes(outPacket.payload, outPacket.payload_len);
|
|
91
|
+
const packet = {
|
|
92
|
+
trackIndex: outPacket.stream_id,
|
|
93
|
+
data: payload,
|
|
94
|
+
pts: Number(outPacket.pts),
|
|
95
|
+
duration: Number(outPacket.duration),
|
|
96
|
+
key: outPacket.is_keyframe,
|
|
97
|
+
};
|
|
98
|
+
container.packetFree(outPacket);
|
|
99
|
+
return { info, packet };
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mediaway/container",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Rust-based, lightweight, fast — sans-IO fragmented MP4 (fMP4) mux and demux for Node.js — push packets in, poll bytes out",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"build": "tsc -p tsconfig.json"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@mediaway/ffi": "0.1.0"
|
|
29
|
+
"@mediaway/ffi": "^0.1.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"typescript": "^5.6.0"
|