@editframe/assets 0.59.4 → 0.59.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.
@@ -0,0 +1,232 @@
1
+ import { adtsToRawAac, stripSeiNalUnits } from "./bitstream.js";
2
+ import debug from "debug";
3
+ import { PassThrough, Writable } from "node:stream";
4
+ import crypto from "node:crypto";
5
+ import { registerMediabunnyServer } from "@mediabunny/server";
6
+ import { ALL_FORMATS, AppendOnlyStreamTarget, AudioSample, AudioSampleSink, AudioSampleSource, EncodedAudioPacketSource, EncodedPacketSink, EncodedVideoPacketSource, FilePathSource, Input, Mp4OutputFormat, Output, UrlSource } from "mediabunny";
7
+ //#region src/mediabunny-ingest/MediabunnyIngest.ts
8
+ const log = debug("ef:assets:mediabunny-ingest");
9
+ registerMediabunnyServer();
10
+ /** Opens a mediabunny `Input` over a URL or local file path, auto-detecting the container/HLS format. */
11
+ function openIngestInput(source) {
12
+ return new Input({
13
+ formats: ALL_FORMATS,
14
+ source: source.type === "url" ? new UrlSource(source.url) : new FilePathSource(source.path)
15
+ });
16
+ }
17
+ /**
18
+ * Selects the best video track and its best pairable audio track from an
19
+ * input (matching the "ingest only the best audio and video track" ingest
20
+ * policy). For plain progressive files this is just "the" video/audio track;
21
+ * for HLS master playlists, mediabunny's `getPrimaryVideoTrack`/
22
+ * `getPrimaryAudioTrack` already implement bitrate-based ABR selection.
23
+ *
24
+ * Throws if the source has no usable tracks, or if any selected track is
25
+ * still live (open-ended) — ingestion only supports VOD sources.
26
+ */
27
+ async function selectIngestTracks(input) {
28
+ const video = await input.getPrimaryVideoTrack();
29
+ const audio = video ? await video.getPrimaryPairableAudioTrack() ?? await input.getPrimaryAudioTrack() : await input.getPrimaryAudioTrack();
30
+ if (!video && !audio) throw new Error("No video or audio tracks found in source");
31
+ for (const track of [video, audio]) if (track && await track.isLive()) throw new Error("Live (open-ended) sources are not supported for ingestion — only VOD sources can be processed");
32
+ return {
33
+ video,
34
+ audio
35
+ };
36
+ }
37
+ function bridgeToNodeReadable() {
38
+ const pass = new PassThrough();
39
+ return {
40
+ readable: pass,
41
+ target: new AppendOnlyStreamTarget(Writable.toWeb(pass))
42
+ };
43
+ }
44
+ /**
45
+ * Zero-bases a track's packet timestamps around its own decode-order first
46
+ * packet, for two reasons:
47
+ *
48
+ * 1. Mediabunny's MP4 muxer rejects packets with negative timestamps outright.
49
+ * Negative first-packet timestamps are common and legitimate — e.g. AAC
50
+ * encoder priming samples, or B-frame reordering — and ffmpeg silently
51
+ * normalizes them by default (`-avoid_negative_ts make_zero`) when remuxing.
52
+ *
53
+ * 2. For *positive* first-packet timestamps (e.g. a source with a non-zero
54
+ * container-level `format.start_time`, such as Mux's 90kHz start-time
55
+ * offset), mediabunny's fragmented-MP4 muxer does NOT zero-base decode
56
+ * timestamps the way its non-fragmented muxer (and the old ffmpeg-CLI
57
+ * ingest's remux) does — that normalization is explicitly gated behind
58
+ * `!isFragmented` in mediabunny's isobmff muxer. Left unshifted, that
59
+ * container-level offset leaks into the produced track's absolute
60
+ * timestamps, which `generateFragmentIndex` reads assuming DTS starts at
61
+ * 0 (see its "After ffmpeg re-mux, DTS is always 0" comment) — corrupting
62
+ * its detected `startTimeOffsetMs` by exactly that leaked amount.
63
+ *
64
+ * Shifting every packet by `-firstTimestamp` handles both cases and is a
65
+ * no-op for the common case where the track already starts at t=0. It does
66
+ * NOT erase a genuine B-frame reordering offset (CTS > DTS for the first
67
+ * decode-order packet): that offset is a *relative* property between a
68
+ * packet's own timestamp and its decode-order position, which a uniform
69
+ * shift preserves.
70
+ */
71
+ async function getTimestampOffset(track) {
72
+ return -await track.getFirstTimestamp();
73
+ }
74
+ function md5Digest(hash) {
75
+ const hex = hash.digest("hex");
76
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
77
+ }
78
+ /**
79
+ * Extracts a single video track into a fragmented single-track MP4 stream via
80
+ * packet-copy transmux (no re-encoding — matches the previous ffmpeg `-c:v copy`
81
+ * behavior). SEI NAL units (type 6) are stripped from every packet, mirroring the
82
+ * previous `-bsf:v filter_units=remove_types=6` ffmpeg filter.
83
+ */
84
+ function extractVideoTrack(track) {
85
+ const { readable, target } = bridgeToNodeReadable();
86
+ const hash = crypto.createHash("md5");
87
+ const digestDeferred = createDeferred();
88
+ (async () => {
89
+ const codec = await track.getCodec();
90
+ if (!codec) throw new Error("Video track has no known codec");
91
+ const decoderConfig = await track.getDecoderConfig();
92
+ const isAnnexB = decoderConfig?.description === void 0;
93
+ const tsOffset = await getTimestampOffset(track);
94
+ const output = new Output({
95
+ format: new Mp4OutputFormat({ fastStart: "fragmented" }),
96
+ target
97
+ });
98
+ const source = new EncodedVideoPacketSource(codec);
99
+ output.addVideoTrack(source);
100
+ await output.start();
101
+ const sink = new EncodedPacketSink(track);
102
+ let isFirst = true;
103
+ for await (const packet of sink.packets()) {
104
+ hash.update(packet.data);
105
+ const strippedData = stripSeiNalUnits(packet.data, isAnnexB);
106
+ const outPacket = strippedData !== packet.data || tsOffset !== 0 ? packet.clone({
107
+ data: strippedData,
108
+ timestamp: packet.timestamp + tsOffset
109
+ }) : packet;
110
+ await source.add(outPacket, isFirst && decoderConfig ? { decoderConfig } : void 0);
111
+ isFirst = false;
112
+ }
113
+ await output.finalize();
114
+ })().then(() => digestDeferred.resolve(md5Digest(hash))).catch((error) => {
115
+ digestDeferred.reject(error);
116
+ readable.emit("error", error);
117
+ readable.destroy(error instanceof Error ? error : new Error(String(error)));
118
+ });
119
+ return {
120
+ stream: readable,
121
+ digest: digestDeferred.promise
122
+ };
123
+ }
124
+ /**
125
+ * Extracts a single audio track into a fragmented single-track MP4 stream.
126
+ *
127
+ * - AAC-raw sources (description present) are packet-copied directly.
128
+ * - AAC-ADTS sources (typical of MPEG-TS/HLS demuxed AAC) have their ADTS
129
+ * headers stripped and an AudioSpecificConfig synthesized, mirroring the
130
+ * previous `-bsf:a aac_adtstoasc` ffmpeg filter.
131
+ * - Any other codec is decoded and re-encoded to AAC (192kbps), mirroring the
132
+ * previous `-c:a aac -b:a 192k -ar 48000` ffmpeg re-encode fallback.
133
+ */
134
+ function extractAudioTrack(track) {
135
+ const { readable, target } = bridgeToNodeReadable();
136
+ const hash = crypto.createHash("md5");
137
+ const digestDeferred = createDeferred();
138
+ (async () => {
139
+ const codec = await track.getCodec();
140
+ const decoderConfig = await track.getDecoderConfig();
141
+ const isAdtsAac = codec === "aac" && decoderConfig?.description === void 0;
142
+ const needsReencode = codec !== "aac";
143
+ const tsOffset = await getTimestampOffset(track);
144
+ const output = new Output({
145
+ format: new Mp4OutputFormat({ fastStart: "fragmented" }),
146
+ target
147
+ });
148
+ if (needsReencode) {
149
+ const source = new AudioSampleSource({
150
+ codec: "aac",
151
+ bitrate: 192e3
152
+ });
153
+ output.addAudioTrack(source);
154
+ await output.start();
155
+ const hashPromise = (async () => {
156
+ const packetSink = new EncodedPacketSink(track);
157
+ for await (const packet of packetSink.packets()) hash.update(packet.data);
158
+ })();
159
+ const sampleSink = new AudioSampleSink(track);
160
+ for await (const sample of sampleSink.samples()) try {
161
+ if (tsOffset === 0) {
162
+ await source.add(sample);
163
+ continue;
164
+ }
165
+ const shifted = AudioSample.fromAudioBuffer(sample.toAudioBuffer(), sample.timestamp + tsOffset);
166
+ for (const out of shifted) try {
167
+ await source.add(out);
168
+ } finally {
169
+ out.close();
170
+ }
171
+ } finally {
172
+ sample.close();
173
+ }
174
+ await hashPromise;
175
+ await output.finalize();
176
+ return;
177
+ }
178
+ const source = new EncodedAudioPacketSource("aac");
179
+ output.addAudioTrack(source);
180
+ await output.start();
181
+ const sink = new EncodedPacketSink(track);
182
+ const [sampleRate, numberOfChannels] = await Promise.all([track.getSampleRate(), track.getNumberOfChannels()]);
183
+ let isFirst = true;
184
+ for await (const packet of sink.packets()) {
185
+ hash.update(packet.data);
186
+ if (!isAdtsAac) {
187
+ const outPacket = tsOffset !== 0 ? packet.clone({ timestamp: packet.timestamp + tsOffset }) : packet;
188
+ await source.add(outPacket, isFirst && decoderConfig ? { decoderConfig } : void 0);
189
+ isFirst = false;
190
+ continue;
191
+ }
192
+ const raw = adtsToRawAac(packet.data);
193
+ const outPacket = packet.clone({
194
+ data: raw.data,
195
+ timestamp: packet.timestamp + tsOffset
196
+ });
197
+ await source.add(outPacket, isFirst ? { decoderConfig: {
198
+ codec: raw.codec,
199
+ sampleRate,
200
+ numberOfChannels,
201
+ description: raw.audioSpecificConfig
202
+ } } : void 0);
203
+ isFirst = false;
204
+ }
205
+ await output.finalize();
206
+ })().then(() => digestDeferred.resolve(md5Digest(hash))).catch((error) => {
207
+ digestDeferred.reject(error);
208
+ readable.emit("error", error);
209
+ readable.destroy(error instanceof Error ? error : new Error(String(error)));
210
+ });
211
+ return {
212
+ stream: readable,
213
+ digest: digestDeferred.promise
214
+ };
215
+ }
216
+ function createDeferred() {
217
+ let resolve;
218
+ let reject;
219
+ return {
220
+ promise: new Promise((res, rej) => {
221
+ resolve = res;
222
+ reject = rej;
223
+ }),
224
+ resolve,
225
+ reject
226
+ };
227
+ }
228
+ log("Mediabunny ingest module loaded");
229
+ //#endregion
230
+ export { extractAudioTrack, extractVideoTrack, openIngestInput, selectIngestTracks };
231
+
232
+ //# sourceMappingURL=MediabunnyIngest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MediabunnyIngest.js","names":["NodeWritable"],"sources":["../../src/mediabunny-ingest/MediabunnyIngest.ts"],"mappings":";;;;;;;AAyBA,MAAM,MAAM,MAAM,8BAA8B;AAMhD,0BAA0B;;AAK1B,SAAgB,gBAAgB,QAA6B;AAG3D,QAAO,IAAI,MAAM;EAAE,SAAS;EAAa,QADvC,OAAO,SAAS,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG,IAAI,eAAe,OAAO,KAAK;EAC1B,CAAC;;;;;;;;;;;;AAkB9D,eAAsB,mBAAmB,OAA6C;CACpF,MAAM,QAAQ,MAAM,MAAM,sBAAsB;CAChD,MAAM,QAAQ,QACR,MAAM,MAAM,8BAA8B,IAAM,MAAM,MAAM,sBAAsB,GACpF,MAAM,MAAM,sBAAsB;AAEtC,KAAI,CAAC,SAAS,CAAC,MACb,OAAM,IAAI,MAAM,2CAA2C;AAG7D,MAAK,MAAM,SAAS,CAAC,OAAO,MAAM,CAChC,KAAI,SAAU,MAAM,MAAM,QAAQ,CAChC,OAAM,IAAI,MACR,gGACD;AAIL,QAAO;EAAE;EAAO;EAAO;;AAezB,SAAS,uBAA+E;CACtF,MAAM,OAAO,IAAI,aAAa;AAE9B,QAAO;EAAE,UAAU;EAAM,QAAQ,IAAI,uBADjBA,SAAa,MAAM,KAAK,CAC4B;EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8B5E,eAAe,mBAAmB,OAA2D;AAE3F,QAAO,CADgB,MAAM,MAAM,mBAAmB;;AAIxD,SAAS,UAAU,MAA2B;CAC5C,MAAM,MAAM,KAAK,OAAO,MAAM;AAC9B,QAAO,GAAG,IAAI,MAAM,GAAG,EAAE,CAAC,GAAG,IAAI,MAAM,GAAG,GAAG,CAAC,GAAG,IAAI,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,MAAM,IAAI,GAAG;;;;;;;;AAS9G,SAAgB,kBAAkB,OAA+C;CAC/E,MAAM,EAAE,UAAU,WAAW,sBAAsB;CACnD,MAAM,OAAO,OAAO,WAAW,MAAM;CACrC,MAAM,iBAAiB,gBAAwB;AAE/C,EAAC,YAAY;EACX,MAAM,QAAQ,MAAM,MAAM,UAAU;AACpC,MAAI,CAAC,MACH,OAAM,IAAI,MAAM,iCAAiC;EAEnD,MAAM,gBAAgB,MAAM,MAAM,kBAAkB;EACpD,MAAM,WAAW,eAAe,gBAAgB,KAAA;EAChD,MAAM,WAAW,MAAM,mBAAmB,MAAM;EAEhD,MAAM,SAAS,IAAI,OAAO;GAAE,QAAQ,IAAI,gBAAgB,EAAE,WAAW,cAAc,CAAC;GAAE;GAAQ,CAAC;EAC/F,MAAM,SAAS,IAAI,yBAAyB,MAAM;AAClD,SAAO,cAAc,OAAO;AAC5B,QAAM,OAAO,OAAO;EAEpB,MAAM,OAAO,IAAI,kBAAkB,MAAM;EACzC,IAAI,UAAU;AACd,aAAW,MAAM,UAAU,KAAK,SAAS,EAAE;AACzC,QAAK,OAAO,OAAO,KAAK;GACxB,MAAM,eAAe,iBAAiB,OAAO,MAAM,SAAS;GAE5D,MAAM,YADa,iBAAiB,OAAO,QAAQ,aAAa,IAE5D,OAAO,MAAM;IAAE,MAAM;IAAc,WAAW,OAAO,YAAY;IAAU,CAAC,GAC5E;AACJ,SAAM,OAAO,IAAI,WAAW,WAAW,gBAAgB,EAAE,eAAe,GAAG,KAAA,EAAU;AACrF,aAAU;;AAGZ,QAAM,OAAO,UAAU;KACrB,CACD,WAAW,eAAe,QAAQ,UAAU,KAAK,CAAC,CAAC,CACnD,OAAO,UAAU;AAChB,iBAAe,OAAO,MAAM;AAC5B,WAAS,KAAK,SAAS,MAAM;AAC7B,WAAS,QAAQ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC,CAAC;GAC3E;AAEJ,QAAO;EAAE,QAAQ;EAAU,QAAQ,eAAe;EAAS;;;;;;;;;;;;AAa7D,SAAgB,kBAAkB,OAA+C;CAC/E,MAAM,EAAE,UAAU,WAAW,sBAAsB;CACnD,MAAM,OAAO,OAAO,WAAW,MAAM;CACrC,MAAM,iBAAiB,gBAAwB;AAE/C,EAAC,YAAY;EACX,MAAM,QAAQ,MAAM,MAAM,UAAU;EACpC,MAAM,gBAAgB,MAAM,MAAM,kBAAkB;EACpD,MAAM,YAAY,UAAU,SAAS,eAAe,gBAAgB,KAAA;EACpE,MAAM,gBAAgB,UAAU;EAChC,MAAM,WAAW,MAAM,mBAAmB,MAAM;EAEhD,MAAM,SAAS,IAAI,OAAO;GAAE,QAAQ,IAAI,gBAAgB,EAAE,WAAW,cAAc,CAAC;GAAE;GAAQ,CAAC;AAE/F,MAAI,eAAe;GACjB,MAAM,SAAS,IAAI,kBAAkB;IAAE,OAAO;IAAO,SAAS;IAAS,CAAC;AACxE,UAAO,cAAc,OAAO;AAC5B,SAAM,OAAO,OAAO;GAOpB,MAAM,eAAe,YAAY;IAC/B,MAAM,aAAa,IAAI,kBAAkB,MAAM;AAC/C,eAAW,MAAM,UAAU,WAAW,SAAS,CAC7C,MAAK,OAAO,OAAO,KAAK;OAExB;GAEJ,MAAM,aAAa,IAAI,gBAAgB,MAAM;AAC7C,cAAW,MAAM,UAAU,WAAW,SAAS,CAC7C,KAAI;AACF,QAAI,aAAa,GAAG;AAClB,WAAM,OAAO,IAAI,OAAO;AACxB;;IAGF,MAAM,UAAU,YAAY,gBAC1B,OAAO,eAAe,EACtB,OAAO,YAAY,SACpB;AACD,SAAK,MAAM,OAAO,QAChB,KAAI;AACF,WAAM,OAAO,IAAI,IAAI;cACb;AACR,SAAI,OAAO;;aAGP;AACR,WAAO,OAAO;;AAGlB,SAAM;AAEN,SAAM,OAAO,UAAU;AACvB;;EAGF,MAAM,SAAS,IAAI,yBAAyB,MAAM;AAClD,SAAO,cAAc,OAAO;AAC5B,QAAM,OAAO,OAAO;EAEpB,MAAM,OAAO,IAAI,kBAAkB,MAAM;EACzC,MAAM,CAAC,YAAY,oBAAoB,MAAM,QAAQ,IAAI,CACvD,MAAM,eAAe,EACrB,MAAM,qBAAqB,CAC5B,CAAC;EAEF,IAAI,UAAU;AACd,aAAW,MAAM,UAAU,KAAK,SAAS,EAAE;AACzC,QAAK,OAAO,OAAO,KAAK;AAExB,OAAI,CAAC,WAAW;IACd,MAAM,YACJ,aAAa,IAAI,OAAO,MAAM,EAAE,WAAW,OAAO,YAAY,UAAU,CAAC,GAAG;AAC9E,UAAM,OAAO,IAAI,WAAW,WAAW,gBAAgB,EAAE,eAAe,GAAG,KAAA,EAAU;AACrF,cAAU;AACV;;GAGF,MAAM,MAAM,aAAa,OAAO,KAAK;GACrC,MAAM,YAAY,OAAO,MAAM;IAAE,MAAM,IAAI;IAAM,WAAW,OAAO,YAAY;IAAU,CAAC;AAC1F,SAAM,OAAO,IACX,WACA,UACI,EACE,eAAe;IACb,OAAO,IAAI;IACX;IACA;IACA,aAAa,IAAI;IAClB,EACF,GACD,KAAA,EACL;AACD,aAAU;;AAGZ,QAAM,OAAO,UAAU;KACrB,CACD,WAAW,eAAe,QAAQ,UAAU,KAAK,CAAC,CAAC,CACnD,OAAO,UAAU;AAChB,iBAAe,OAAO,MAAM;AAC5B,WAAS,KAAK,SAAS,MAAM;AAC7B,WAAS,QAAQ,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,MAAM,CAAC,CAAC;GAC3E;AAEJ,QAAO;EAAE,QAAQ;EAAU,QAAQ,eAAe;EAAS;;AAG7D,SAAS,iBAIP;CACA,IAAI;CACJ,IAAI;AAKJ,QAAO;EAAE,SAJO,IAAI,SAAY,KAAK,QAAQ;AAC3C,aAAU;AACV,YAAS;IACT;EACgB;EAAS;EAAQ;;AAGrC,IAAI,kCAAkC"}
@@ -0,0 +1,118 @@
1
+ //#region src/mediabunny-ingest/bitstream.ts
2
+ /**
3
+ * Bitstream-level helpers used by the mediabunny-based ingest pipeline.
4
+ *
5
+ * These replace two ffmpeg bitstream filters that the old ffmpeg-CLI ingest
6
+ * path relied on:
7
+ *
8
+ * -bsf:v filter_units=remove_types=6 (strip SEI NAL units — see stripSeiNalUnits)
9
+ * -bsf:a aac_adtstoasc (ADTS -> raw AAC — see adtsToRawAac)
10
+ */
11
+ const SEI_NAL_UNIT_TYPE = 6;
12
+ function concatUint8Arrays(chunks, totalLength) {
13
+ const out = new Uint8Array(totalLength);
14
+ let offset = 0;
15
+ for (const chunk of chunks) {
16
+ out.set(chunk, offset);
17
+ offset += chunk.length;
18
+ }
19
+ return out;
20
+ }
21
+ /**
22
+ * Strips SEI (Supplemental Enhancement Information, NAL unit type 6) NAL units
23
+ * from an AVC access unit. Some WebCodecs VideoDecoder implementations hang or
24
+ * crash on certain SEI payloads, so these are dropped — mirroring the
25
+ * `-bsf:v filter_units=remove_types=6` ffmpeg filter used by the previous
26
+ * ffmpeg-CLI ingest path.
27
+ *
28
+ * @param data - The packet's encoded data, as produced by mediabunny's
29
+ * `EncodedPacketSink`.
30
+ * @param isAnnexB - Whether the AVC bitstream is in Annex B (start-code
31
+ * delimited) format rather than canonical (4-byte length-prefixed) format.
32
+ * Per the Mediabunny Codec Registry, this is true iff the track's decoder
33
+ * config `description` is `undefined`.
34
+ */
35
+ function stripSeiNalUnits(data, isAnnexB) {
36
+ return isAnnexB ? stripSeiAnnexB(data) : stripSeiLengthPrefixed(data);
37
+ }
38
+ function stripSeiLengthPrefixed(data) {
39
+ const kept = [];
40
+ let keptLength = 0;
41
+ let offset = 0;
42
+ while (offset + 4 <= data.length) {
43
+ const len = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3];
44
+ const nalStart = offset + 4;
45
+ const nalEnd = nalStart + len;
46
+ if (len < 0 || nalEnd > data.length || nalStart >= data.length) return data;
47
+ if ((data[nalStart] & 31) !== SEI_NAL_UNIT_TYPE) {
48
+ const nalu = data.subarray(offset, nalEnd);
49
+ kept.push(nalu);
50
+ keptLength += nalu.length;
51
+ }
52
+ offset = nalEnd;
53
+ }
54
+ if (offset !== data.length) return data;
55
+ return concatUint8Arrays(kept, keptLength);
56
+ }
57
+ function stripSeiAnnexB(data) {
58
+ const starts = findAnnexBNaluStarts(data);
59
+ if (starts.length === 0) return data;
60
+ const kept = [];
61
+ let keptLength = 0;
62
+ for (let i = 0; i < starts.length; i++) {
63
+ const { prefixStart, nalStart } = starts[i];
64
+ const nalEnd = i + 1 < starts.length ? starts[i + 1].prefixStart : data.length;
65
+ if ((data[nalStart] & 31) !== SEI_NAL_UNIT_TYPE) {
66
+ const chunk = data.subarray(prefixStart, nalEnd);
67
+ kept.push(chunk);
68
+ keptLength += chunk.length;
69
+ }
70
+ }
71
+ return concatUint8Arrays(kept, keptLength);
72
+ }
73
+ /** Finds every Annex B start code (0x000001 or 0x00000001) and the NALU byte right after it. */
74
+ function findAnnexBNaluStarts(data) {
75
+ const starts = [];
76
+ for (let i = 0; i + 2 < data.length; i++) if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 1) {
77
+ const isFourByte = i > 0 && data[i - 1] === 0;
78
+ starts.push({
79
+ prefixStart: isFourByte ? i - 1 : i,
80
+ nalStart: i + 3
81
+ });
82
+ }
83
+ return starts;
84
+ }
85
+ /**
86
+ * Converts a single ADTS-framed AAC packet (as produced by mediabunny when
87
+ * reading AAC out of an MPEG-TS/HLS source) into raw AAC frame data plus the
88
+ * AudioSpecificConfig an MP4 muxer needs out-of-band. Mirrors ffmpeg's
89
+ * `aac_adtstoasc` bitstream filter.
90
+ *
91
+ * Only single-AAC-frame ADTS packets (`number_of_raw_data_blocks == 0`) are
92
+ * supported — this covers the overwhelming majority of real-world HLS/Mux
93
+ * output. Multi-block ADTS packets throw so the caller can fall back to a
94
+ * decode/re-encode path instead of silently mis-parsing audio.
95
+ */
96
+ function adtsToRawAac(data) {
97
+ if (data.length < 7) throw new Error("ADTS packet too short to contain a header");
98
+ if (data[0] !== 255 || (data[1] & 240) !== 240) throw new Error("Data does not start with an ADTS sync word");
99
+ const headerLength = (data[1] & 1) === 1 ? 7 : 9;
100
+ const profile = data[2] >> 6 & 3;
101
+ const samplingFrequencyIndex = data[2] >> 2 & 15;
102
+ const channelConfiguration = (data[2] & 1) << 2 | data[3] >> 6 & 3;
103
+ const numberOfRawDataBlocks = data[6] & 3;
104
+ if (numberOfRawDataBlocks !== 0) throw new Error(`Multi-block ADTS packets (number_of_raw_data_blocks=${numberOfRawDataBlocks}) are not supported`);
105
+ const audioObjectType = profile + 1;
106
+ const ascValue = audioObjectType << 11 | samplingFrequencyIndex << 7 | channelConfiguration << 3;
107
+ const audioSpecificConfig = new Uint8Array([ascValue >> 8 & 255, ascValue & 255]);
108
+ return {
109
+ data: data.subarray(headerLength),
110
+ audioSpecificConfig,
111
+ codec: `mp4a.40.${audioObjectType}`
112
+ };
113
+ }
114
+ //#endregion
115
+ exports.adtsToRawAac = adtsToRawAac;
116
+ exports.stripSeiNalUnits = stripSeiNalUnits;
117
+
118
+ //# sourceMappingURL=bitstream.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bitstream.cjs","names":[],"sources":["../../src/mediabunny-ingest/bitstream.ts"],"mappings":"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"}
@@ -0,0 +1,117 @@
1
+ //#region src/mediabunny-ingest/bitstream.ts
2
+ /**
3
+ * Bitstream-level helpers used by the mediabunny-based ingest pipeline.
4
+ *
5
+ * These replace two ffmpeg bitstream filters that the old ffmpeg-CLI ingest
6
+ * path relied on:
7
+ *
8
+ * -bsf:v filter_units=remove_types=6 (strip SEI NAL units — see stripSeiNalUnits)
9
+ * -bsf:a aac_adtstoasc (ADTS -> raw AAC — see adtsToRawAac)
10
+ */
11
+ const SEI_NAL_UNIT_TYPE = 6;
12
+ function concatUint8Arrays(chunks, totalLength) {
13
+ const out = new Uint8Array(totalLength);
14
+ let offset = 0;
15
+ for (const chunk of chunks) {
16
+ out.set(chunk, offset);
17
+ offset += chunk.length;
18
+ }
19
+ return out;
20
+ }
21
+ /**
22
+ * Strips SEI (Supplemental Enhancement Information, NAL unit type 6) NAL units
23
+ * from an AVC access unit. Some WebCodecs VideoDecoder implementations hang or
24
+ * crash on certain SEI payloads, so these are dropped — mirroring the
25
+ * `-bsf:v filter_units=remove_types=6` ffmpeg filter used by the previous
26
+ * ffmpeg-CLI ingest path.
27
+ *
28
+ * @param data - The packet's encoded data, as produced by mediabunny's
29
+ * `EncodedPacketSink`.
30
+ * @param isAnnexB - Whether the AVC bitstream is in Annex B (start-code
31
+ * delimited) format rather than canonical (4-byte length-prefixed) format.
32
+ * Per the Mediabunny Codec Registry, this is true iff the track's decoder
33
+ * config `description` is `undefined`.
34
+ */
35
+ function stripSeiNalUnits(data, isAnnexB) {
36
+ return isAnnexB ? stripSeiAnnexB(data) : stripSeiLengthPrefixed(data);
37
+ }
38
+ function stripSeiLengthPrefixed(data) {
39
+ const kept = [];
40
+ let keptLength = 0;
41
+ let offset = 0;
42
+ while (offset + 4 <= data.length) {
43
+ const len = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 | data[offset + 3];
44
+ const nalStart = offset + 4;
45
+ const nalEnd = nalStart + len;
46
+ if (len < 0 || nalEnd > data.length || nalStart >= data.length) return data;
47
+ if ((data[nalStart] & 31) !== SEI_NAL_UNIT_TYPE) {
48
+ const nalu = data.subarray(offset, nalEnd);
49
+ kept.push(nalu);
50
+ keptLength += nalu.length;
51
+ }
52
+ offset = nalEnd;
53
+ }
54
+ if (offset !== data.length) return data;
55
+ return concatUint8Arrays(kept, keptLength);
56
+ }
57
+ function stripSeiAnnexB(data) {
58
+ const starts = findAnnexBNaluStarts(data);
59
+ if (starts.length === 0) return data;
60
+ const kept = [];
61
+ let keptLength = 0;
62
+ for (let i = 0; i < starts.length; i++) {
63
+ const { prefixStart, nalStart } = starts[i];
64
+ const nalEnd = i + 1 < starts.length ? starts[i + 1].prefixStart : data.length;
65
+ if ((data[nalStart] & 31) !== SEI_NAL_UNIT_TYPE) {
66
+ const chunk = data.subarray(prefixStart, nalEnd);
67
+ kept.push(chunk);
68
+ keptLength += chunk.length;
69
+ }
70
+ }
71
+ return concatUint8Arrays(kept, keptLength);
72
+ }
73
+ /** Finds every Annex B start code (0x000001 or 0x00000001) and the NALU byte right after it. */
74
+ function findAnnexBNaluStarts(data) {
75
+ const starts = [];
76
+ for (let i = 0; i + 2 < data.length; i++) if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 1) {
77
+ const isFourByte = i > 0 && data[i - 1] === 0;
78
+ starts.push({
79
+ prefixStart: isFourByte ? i - 1 : i,
80
+ nalStart: i + 3
81
+ });
82
+ }
83
+ return starts;
84
+ }
85
+ /**
86
+ * Converts a single ADTS-framed AAC packet (as produced by mediabunny when
87
+ * reading AAC out of an MPEG-TS/HLS source) into raw AAC frame data plus the
88
+ * AudioSpecificConfig an MP4 muxer needs out-of-band. Mirrors ffmpeg's
89
+ * `aac_adtstoasc` bitstream filter.
90
+ *
91
+ * Only single-AAC-frame ADTS packets (`number_of_raw_data_blocks == 0`) are
92
+ * supported — this covers the overwhelming majority of real-world HLS/Mux
93
+ * output. Multi-block ADTS packets throw so the caller can fall back to a
94
+ * decode/re-encode path instead of silently mis-parsing audio.
95
+ */
96
+ function adtsToRawAac(data) {
97
+ if (data.length < 7) throw new Error("ADTS packet too short to contain a header");
98
+ if (data[0] !== 255 || (data[1] & 240) !== 240) throw new Error("Data does not start with an ADTS sync word");
99
+ const headerLength = (data[1] & 1) === 1 ? 7 : 9;
100
+ const profile = data[2] >> 6 & 3;
101
+ const samplingFrequencyIndex = data[2] >> 2 & 15;
102
+ const channelConfiguration = (data[2] & 1) << 2 | data[3] >> 6 & 3;
103
+ const numberOfRawDataBlocks = data[6] & 3;
104
+ if (numberOfRawDataBlocks !== 0) throw new Error(`Multi-block ADTS packets (number_of_raw_data_blocks=${numberOfRawDataBlocks}) are not supported`);
105
+ const audioObjectType = profile + 1;
106
+ const ascValue = audioObjectType << 11 | samplingFrequencyIndex << 7 | channelConfiguration << 3;
107
+ const audioSpecificConfig = new Uint8Array([ascValue >> 8 & 255, ascValue & 255]);
108
+ return {
109
+ data: data.subarray(headerLength),
110
+ audioSpecificConfig,
111
+ codec: `mp4a.40.${audioObjectType}`
112
+ };
113
+ }
114
+ //#endregion
115
+ export { adtsToRawAac, stripSeiNalUnits };
116
+
117
+ //# sourceMappingURL=bitstream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bitstream.js","names":[],"sources":["../../src/mediabunny-ingest/bitstream.ts"],"mappings":"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"}
@@ -1,9 +1,10 @@
1
1
  import { TrackFragmentIndex } from "../Probe.cjs";
2
2
  import { TaskResult } from "../idempotentTask.cjs";
3
+ import { Readable } from "node:stream";
3
4
 
4
5
  //#region src/tasks/generateScrubTrack.d.ts
5
6
  declare const generateScrubTrackFromPath: (absolutePath: string) => Promise<{
6
- stream: any;
7
+ stream: Readable;
7
8
  fragmentIndex: Promise<Record<number, TrackFragmentIndex>>;
8
9
  }>;
9
10
  declare const generateScrubTrack: (cacheRoot: string, absolutePath: string) => Promise<TaskResult>;
@@ -1,9 +1,10 @@
1
1
  import { TrackFragmentIndex } from "../Probe.js";
2
2
  import { TaskResult } from "../idempotentTask.js";
3
+ import { Readable } from "node:stream";
3
4
 
4
5
  //#region src/tasks/generateScrubTrack.d.ts
5
6
  declare const generateScrubTrackFromPath: (absolutePath: string) => Promise<{
6
- stream: any;
7
+ stream: Readable;
7
8
  fragmentIndex: Promise<Record<number, TrackFragmentIndex>>;
8
9
  }>;
9
10
  declare const generateScrubTrack: (cacheRoot: string, absolutePath: string) => Promise<TaskResult>;
@@ -1,7 +1,8 @@
1
1
  import { TaskResult } from "../idempotentTask.cjs";
2
+ import { Readable } from "node:stream";
2
3
 
3
4
  //#region src/tasks/generateTrack.d.ts
4
- declare const generateTrackFromPath: (absolutePath: string, trackId: number) => Promise<any>;
5
+ declare const generateTrackFromPath: (absolutePath: string, trackId: number) => Promise<Readable>;
5
6
  declare const generateTrack: (cacheRoot: string, absolutePath: string, url: string) => Promise<TaskResult>;
6
7
  //#endregion
7
8
  export { generateTrack, generateTrackFromPath };
@@ -1,7 +1,8 @@
1
1
  import { TaskResult } from "../idempotentTask.js";
2
+ import { Readable } from "node:stream";
2
3
 
3
4
  //#region src/tasks/generateTrack.d.ts
4
- declare const generateTrackFromPath: (absolutePath: string, trackId: number) => Promise<any>;
5
+ declare const generateTrackFromPath: (absolutePath: string, trackId: number) => Promise<Readable>;
5
6
  declare const generateTrack: (cacheRoot: string, absolutePath: string, url: string) => Promise<TaskResult>;
6
7
  //#endregion
7
8
  export { generateTrack, generateTrackFromPath };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@editframe/assets",
3
- "version": "0.59.4",
3
+ "version": "0.59.6",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,7 +17,9 @@
17
17
  "author": "",
18
18
  "license": "SEE LICENSE IN LICENSE-FULL.md",
19
19
  "dependencies": {
20
+ "@mediabunny/server": "^1.52.0",
20
21
  "debug": "^4.3.5",
22
+ "mediabunny": "^1.50.8",
21
23
  "ora": "^8.0.1",
22
24
  "zod": "^3.23.8"
23
25
  },