@invintusmedia/tomp4 1.0.8 → 1.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,201 @@
1
+ /**
2
+ * fMP4 Box Utilities
3
+ *
4
+ * Shared utilities for parsing and creating MP4 boxes
5
+ * Used by both converter and stitcher modules
6
+ *
7
+ * @module fmp4/utils
8
+ */
9
+
10
+ // ============================================
11
+ // Box Parsing
12
+ // ============================================
13
+
14
+ /**
15
+ * Parse top-level or nested boxes from MP4 data
16
+ * @param {Uint8Array} data - Data buffer
17
+ * @param {number} offset - Start offset
18
+ * @param {number} end - End offset
19
+ * @returns {Array<{type: string, offset: number, size: number, data: Uint8Array}>}
20
+ */
21
+ export function parseBoxes(data, offset = 0, end = data.byteLength) {
22
+ const boxes = [];
23
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
24
+ while (offset < end) {
25
+ if (offset + 8 > end) break;
26
+ const size = view.getUint32(offset);
27
+ const type = String.fromCharCode(data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7]);
28
+ if (size === 0 || size < 8) break;
29
+ boxes.push({ type, offset, size, data: data.subarray(offset, offset + size) });
30
+ offset += size;
31
+ }
32
+ return boxes;
33
+ }
34
+
35
+ /**
36
+ * Find a box by type in an array of boxes
37
+ * @param {Array} boxes - Array of parsed boxes
38
+ * @param {string} type - 4-character box type
39
+ * @returns {object|null} Box object or null
40
+ */
41
+ export function findBox(boxes, type) {
42
+ for (const box of boxes) if (box.type === type) return box;
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * Parse child boxes within a container box
48
+ * @param {object} box - Parent box
49
+ * @param {number} headerSize - Header size (8 for regular, 12 for fullbox)
50
+ * @returns {Array} Array of child boxes
51
+ */
52
+ export function parseChildBoxes(box, headerSize = 8) {
53
+ return parseBoxes(box.data, headerSize, box.size);
54
+ }
55
+
56
+ /**
57
+ * Create an MP4 box with the given type and payloads
58
+ * @param {string} type - 4-character box type
59
+ * @param {...Uint8Array} payloads - Box content
60
+ * @returns {Uint8Array} Complete box
61
+ */
62
+ export function createBox(type, ...payloads) {
63
+ let size = 8;
64
+ for (const p of payloads) size += p.byteLength;
65
+ const result = new Uint8Array(size);
66
+ const view = new DataView(result.buffer);
67
+ view.setUint32(0, size);
68
+ result[4] = type.charCodeAt(0);
69
+ result[5] = type.charCodeAt(1);
70
+ result[6] = type.charCodeAt(2);
71
+ result[7] = type.charCodeAt(3);
72
+ let offset = 8;
73
+ for (const p of payloads) {
74
+ result.set(p, offset);
75
+ offset += p.byteLength;
76
+ }
77
+ return result;
78
+ }
79
+
80
+ // ============================================
81
+ // Fragment Box Parsing
82
+ // ============================================
83
+
84
+ /**
85
+ * Parse tfhd (track fragment header) box
86
+ * Extracts track ID and default sample values
87
+ * @param {Uint8Array} tfhdData - tfhd box data
88
+ * @returns {{trackId: number, defaultSampleDuration: number, defaultSampleSize: number, defaultSampleFlags: number}}
89
+ */
90
+ export function parseTfhd(tfhdData) {
91
+ const view = new DataView(tfhdData.buffer, tfhdData.byteOffset, tfhdData.byteLength);
92
+ const flags = (tfhdData[9] << 16) | (tfhdData[10] << 8) | tfhdData[11];
93
+ const trackId = view.getUint32(12);
94
+ let offset = 16;
95
+ let defaultSampleDuration = 0, defaultSampleSize = 0, defaultSampleFlags = 0;
96
+
97
+ if (flags & 0x1) offset += 8; // base-data-offset
98
+ if (flags & 0x2) offset += 4; // sample-description-index
99
+ if (flags & 0x8) { defaultSampleDuration = view.getUint32(offset); offset += 4; }
100
+ if (flags & 0x10) { defaultSampleSize = view.getUint32(offset); offset += 4; }
101
+ if (flags & 0x20) { defaultSampleFlags = view.getUint32(offset); offset += 4; }
102
+
103
+ return { trackId, defaultSampleDuration, defaultSampleSize, defaultSampleFlags };
104
+ }
105
+
106
+ /**
107
+ * Parse tfdt (track fragment decode time) box
108
+ * @param {Uint8Array} tfdtData - tfdt box data
109
+ * @returns {number} Base media decode time
110
+ */
111
+ export function parseTfdt(tfdtData) {
112
+ const view = new DataView(tfdtData.buffer, tfdtData.byteOffset, tfdtData.byteLength);
113
+ const version = tfdtData[8];
114
+ if (version === 1) {
115
+ return Number(view.getBigUint64(12));
116
+ }
117
+ return view.getUint32(12);
118
+ }
119
+
120
+ /**
121
+ * Parse trun (track run) box
122
+ * @param {Uint8Array} trunData - trun box data
123
+ * @param {{defaultSampleDuration?: number, defaultSampleSize?: number, defaultSampleFlags?: number}} defaults - Default values from tfhd
124
+ * @returns {{samples: Array, dataOffset: number, flags: number}}
125
+ */
126
+ export function parseTrun(trunData, defaults = {}) {
127
+ const view = new DataView(trunData.buffer, trunData.byteOffset, trunData.byteLength);
128
+ const version = trunData[8];
129
+ const flags = (trunData[9] << 16) | (trunData[10] << 8) | trunData[11];
130
+ const sampleCount = view.getUint32(12);
131
+ let offset = 16;
132
+ let dataOffset = 0;
133
+ let firstSampleFlags = null;
134
+
135
+ if (flags & 0x1) { dataOffset = view.getInt32(offset); offset += 4; }
136
+ if (flags & 0x4) { firstSampleFlags = view.getUint32(offset); offset += 4; }
137
+
138
+ const samples = [];
139
+ for (let i = 0; i < sampleCount; i++) {
140
+ const sample = {
141
+ duration: defaults.defaultSampleDuration || 0,
142
+ size: defaults.defaultSampleSize || 0,
143
+ flags: (i === 0 && firstSampleFlags !== null) ? firstSampleFlags : (defaults.defaultSampleFlags || 0),
144
+ compositionTimeOffset: 0
145
+ };
146
+ if (flags & 0x100) { sample.duration = view.getUint32(offset); offset += 4; }
147
+ if (flags & 0x200) { sample.size = view.getUint32(offset); offset += 4; }
148
+ if (flags & 0x400) { sample.flags = view.getUint32(offset); offset += 4; }
149
+ if (flags & 0x800) {
150
+ sample.compositionTimeOffset = version === 0 ? view.getUint32(offset) : view.getInt32(offset);
151
+ offset += 4;
152
+ }
153
+ samples.push(sample);
154
+ }
155
+
156
+ return { samples, dataOffset, flags };
157
+ }
158
+
159
+ // ============================================
160
+ // Track ID Extraction
161
+ // ============================================
162
+
163
+ /**
164
+ * Extract track IDs from moov box
165
+ * @param {object} moovBox - Parsed moov box
166
+ * @returns {number[]} Array of track IDs
167
+ */
168
+ export function extractTrackIds(moovBox) {
169
+ const trackIds = [];
170
+ const moovChildren = parseChildBoxes(moovBox);
171
+ for (const child of moovChildren) {
172
+ if (child.type === 'trak') {
173
+ const trakChildren = parseChildBoxes(child);
174
+ for (const tc of trakChildren) {
175
+ if (tc.type === 'tkhd') {
176
+ const view = new DataView(tc.data.buffer, tc.data.byteOffset, tc.data.byteLength);
177
+ const version = tc.data[8];
178
+ trackIds.push(version === 0 ? view.getUint32(20) : view.getUint32(28));
179
+ }
180
+ }
181
+ }
182
+ }
183
+ return trackIds;
184
+ }
185
+
186
+ /**
187
+ * Extract movie timescale from mvhd box
188
+ * @param {object} moovBox - Parsed moov box
189
+ * @returns {number} Movie timescale (default: 1000)
190
+ */
191
+ export function getMovieTimescale(moovBox) {
192
+ const moovChildren = parseChildBoxes(moovBox);
193
+ for (const child of moovChildren) {
194
+ if (child.type === 'mvhd') {
195
+ const view = new DataView(child.data.buffer, child.data.byteOffset, child.data.byteLength);
196
+ const version = child.data[8];
197
+ return version === 0 ? view.getUint32(20) : view.getUint32(28);
198
+ }
199
+ }
200
+ return 1000;
201
+ }
package/src/index.js CHANGED
@@ -31,12 +31,15 @@
31
31
  */
32
32
 
33
33
  import { convertTsToMp4, analyzeTsData } from './ts-to-mp4.js';
34
- import { convertFmp4ToMp4 } from './fmp4-to-mp4.js';
34
+ import { convertFmp4ToMp4, stitchFmp4 } from './fmp4/index.js';
35
+ import { stitchTs, concatTs } from './mpegts/index.js';
35
36
  import { parseHls, downloadHls, isHlsUrl, HlsStream, HlsVariant } from './hls.js';
36
37
  import { transcode, isWebCodecsSupported } from './transcode.js';
37
38
  import { TSMuxer } from './muxers/mpegts.js';
38
39
  import { MP4Muxer } from './muxers/mp4.js';
39
40
  import { TSParser } from './parsers/mpegts.js';
41
+ import { MP4Parser } from './parsers/mp4.js';
42
+ import { RemoteMp4 } from './remote/index.js';
40
43
 
41
44
  /**
42
45
  * Result object returned by toMp4()
@@ -146,7 +149,7 @@ function isStandardMp4(data) {
146
149
  while (offset + 8 <= data.length) {
147
150
  const size = view.getUint32(offset);
148
151
  if (size < 8) break;
149
- const boxType = String.fromCharCode(data[offset+4], data[offset+5], data[offset+6], data[offset+7]);
152
+ const boxType = String.fromCharCode(data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7]);
150
153
  if (boxType === 'moov') hasMoov = true;
151
154
  if (boxType === 'moof') hasMoof = true;
152
155
  offset += size;
@@ -168,7 +171,7 @@ function detectFormat(data) {
168
171
  function convertData(data, options = {}) {
169
172
  const uint8 = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
170
173
  const format = detectFormat(uint8);
171
-
174
+
172
175
  switch (format) {
173
176
  case 'mpegts':
174
177
  return convertTsToMp4(uint8, options);
@@ -215,15 +218,15 @@ function convertData(data, options = {}) {
215
218
  async function toMp4(input, options = {}) {
216
219
  let data;
217
220
  let filename = options.filename || 'video.mp4';
218
- const log = options.onProgress || (() => {});
219
-
221
+ const log = options.onProgress || (() => { });
222
+
220
223
  // Handle HlsStream object
221
224
  if (input instanceof HlsStream) {
222
225
  if (!options.filename) {
223
226
  const urlPart = (input.masterUrl || '').split('/').pop()?.split('?')[0];
224
227
  filename = urlPart ? urlPart.replace('.m3u8', '.mp4') : 'video.mp4';
225
228
  }
226
- data = await downloadHls(input, {
229
+ data = await downloadHls(input, {
227
230
  ...options,
228
231
  quality: options.quality || 'highest'
229
232
  });
@@ -248,7 +251,7 @@ async function toMp4(input, options = {}) {
248
251
  throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
249
252
  }
250
253
  data = new Uint8Array(await response.arrayBuffer());
251
-
254
+
252
255
  if (!options.filename) {
253
256
  const urlFilename = input.split('/').pop()?.split('?')[0];
254
257
  if (urlFilename) {
@@ -272,7 +275,7 @@ async function toMp4(input, options = {}) {
272
275
  else {
273
276
  throw new Error('Input must be a URL string, HlsStream, Uint8Array, ArrayBuffer, or Blob');
274
277
  }
275
-
278
+
276
279
  // Adjust clip times if we downloaded HLS with a time range
277
280
  // The downloaded segments have been normalized to start at 0,
278
281
  // so we need to adjust the requested clip times accordingly
@@ -287,17 +290,20 @@ async function toMp4(input, options = {}) {
287
290
  }
288
291
  log(`Adjusted clip: ${convertOptions.startTime?.toFixed(2) || 0}s - ${convertOptions.endTime?.toFixed(2) || '∞'}s (offset: -${segmentStart.toFixed(2)}s)`);
289
292
  }
290
-
293
+
291
294
  // Convert
292
295
  log('Converting...');
293
296
  const mp4Data = convertData(data, convertOptions);
294
-
297
+
295
298
  return new Mp4Result(mp4Data, filename);
296
299
  }
297
300
 
298
301
  // Attach utilities to main function
299
302
  toMp4.fromTs = (data, options) => new Mp4Result(convertTsToMp4(data instanceof ArrayBuffer ? new Uint8Array(data) : data, options));
300
303
  toMp4.fromFmp4 = (data) => new Mp4Result(convertFmp4ToMp4(data instanceof ArrayBuffer ? new Uint8Array(data) : data));
304
+ toMp4.stitchFmp4 = (segments, options) => new Mp4Result(stitchFmp4(segments, options));
305
+ toMp4.stitchTs = (segments) => new Mp4Result(stitchTs(segments));
306
+ toMp4.concatTs = concatTs;
301
307
  toMp4.detectFormat = detectFormat;
302
308
  toMp4.isMpegTs = isMpegTs;
303
309
  toMp4.isFmp4 = isFmp4;
@@ -315,19 +321,29 @@ toMp4.analyze = analyzeTsData;
315
321
  toMp4.transcode = transcode;
316
322
  toMp4.isWebCodecsSupported = isWebCodecsSupported;
317
323
 
324
+ // Parsers
325
+ toMp4.MP4Parser = MP4Parser;
326
+ toMp4.TSParser = TSParser;
327
+
328
+ // Remote MP4 (on-demand HLS from remote MP4)
329
+ toMp4.RemoteMp4 = RemoteMp4;
330
+
318
331
  // Version (injected at build time for dist, read from package.json for ESM)
319
- toMp4.version = '1.0.8';
332
+ toMp4.version = '1.1.0';
320
333
 
321
334
  // Export
322
- export {
323
- toMp4,
324
- Mp4Result,
325
- convertTsToMp4,
326
- convertFmp4ToMp4,
335
+ export {
336
+ toMp4,
337
+ Mp4Result,
338
+ convertTsToMp4,
339
+ convertFmp4ToMp4,
340
+ stitchFmp4,
341
+ stitchTs,
342
+ concatTs,
327
343
  analyzeTsData,
328
- detectFormat,
329
- isMpegTs,
330
- isFmp4,
344
+ detectFormat,
345
+ isMpegTs,
346
+ isFmp4,
331
347
  isStandardMp4,
332
348
  parseHls,
333
349
  downloadHls,
@@ -336,9 +352,14 @@ export {
336
352
  HlsVariant,
337
353
  // Transcoding (browser-only)
338
354
  transcode,
355
+ isWebCodecsSupported,
356
+ // Muxers
339
357
  TSMuxer,
340
358
  MP4Muxer,
359
+ // Parsers
341
360
  TSParser,
342
- isWebCodecsSupported
361
+ MP4Parser,
362
+ // Remote MP4 (on-demand HLS)
363
+ RemoteMp4
343
364
  };
344
365
  export default toMp4;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * MPEG-TS Stitching Module
3
+ * Combine multiple MPEG-TS segments into MP4 or continuous TS
4
+ */
5
+
6
+ export { stitchTs, concatTs, parseAndCombineSegments, isKeyframe, extractSpsPps } from './stitcher.js';
7
+ export { default } from './stitcher.js';
@@ -0,0 +1,251 @@
1
+ /**
2
+ * MPEG-TS Segment Stitching
3
+ * Combine multiple MPEG-TS segments into a single MP4 or continuous TS stream
4
+ * Pure JavaScript - no dependencies
5
+ */
6
+
7
+ import { TSParser } from '../parsers/mpegts.js';
8
+ import { MP4Muxer } from '../muxers/mp4.js';
9
+ import { TSMuxer } from '../muxers/mpegts.js';
10
+
11
+ // ============================================
12
+ // Utilities
13
+ // ============================================
14
+
15
+ /**
16
+ * Normalize input to Uint8Array
17
+ */
18
+ function normalizeInput(input) {
19
+ if (input instanceof ArrayBuffer) {
20
+ return new Uint8Array(input);
21
+ }
22
+ return input;
23
+ }
24
+
25
+ /**
26
+ * Check if a video access unit contains a keyframe (IDR NAL unit)
27
+ */
28
+ function isKeyframe(accessUnit) {
29
+ for (const nalUnit of accessUnit.nalUnits) {
30
+ const nalType = nalUnit[0] & 0x1F;
31
+ if (nalType === 5) return true; // IDR slice
32
+ }
33
+ return false;
34
+ }
35
+
36
+ /**
37
+ * Calculate segment duration from timestamps
38
+ * Returns duration in PTS ticks (90kHz)
39
+ */
40
+ function getSegmentDuration(timestamps) {
41
+ if (!timestamps || timestamps.length < 2) return 0;
42
+
43
+ const first = timestamps[0];
44
+ const last = timestamps[timestamps.length - 1];
45
+
46
+ // Estimate last frame duration as average
47
+ const avgDuration = (last - first) / (timestamps.length - 1);
48
+
49
+ return Math.round(last - first + avgDuration);
50
+ }
51
+
52
+ /**
53
+ * Build ADTS header for AAC frame
54
+ * @param {number} dataLength - Length of AAC data (without header)
55
+ * @param {number} sampleRate - Audio sample rate
56
+ * @param {number} channels - Number of audio channels
57
+ * @returns {Uint8Array} 7-byte ADTS header
58
+ */
59
+ function buildAdtsHeader(dataLength, sampleRate, channels) {
60
+ const SAMPLE_RATES = [96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350];
61
+ const samplingFreqIndex = SAMPLE_RATES.indexOf(sampleRate);
62
+ const freqIndex = samplingFreqIndex >= 0 ? samplingFreqIndex : 3; // Default to 48000
63
+
64
+ const frameLength = dataLength + 7; // ADTS header is 7 bytes
65
+
66
+ const header = new Uint8Array(7);
67
+ header[0] = 0xFF; // Sync word
68
+ header[1] = 0xF1; // MPEG-4, Layer 0, no CRC
69
+ header[2] = (1 << 6) | (freqIndex << 2) | ((channels >> 2) & 0x01); // AAC-LC, freq index, channel config high bit
70
+ header[3] = ((channels & 0x03) << 6) | ((frameLength >> 11) & 0x03);
71
+ header[4] = (frameLength >> 3) & 0xFF;
72
+ header[5] = ((frameLength & 0x07) << 5) | 0x1F;
73
+ header[6] = 0xFC;
74
+
75
+ return header;
76
+ }
77
+
78
+ /**
79
+ * Extract SPS and PPS from video access units
80
+ */
81
+ function extractSpsPps(videoAccessUnits) {
82
+ let sps = null;
83
+ let pps = null;
84
+
85
+ for (const au of videoAccessUnits) {
86
+ for (const nalUnit of au.nalUnits) {
87
+ const nalType = nalUnit[0] & 0x1F;
88
+ if (nalType === 7 && !sps) sps = nalUnit;
89
+ if (nalType === 8 && !pps) pps = nalUnit;
90
+ if (sps && pps) return { sps, pps };
91
+ }
92
+ }
93
+
94
+ return { sps, pps };
95
+ }
96
+
97
+ // ============================================
98
+ // Core Parsing Logic
99
+ // ============================================
100
+
101
+ /**
102
+ * Parse multiple TS segments and combine with continuous timestamps
103
+ *
104
+ * @param {Uint8Array[]} segments - Array of TS segment data
105
+ * @returns {object} Combined parser-like object compatible with MP4Muxer
106
+ */
107
+ function parseAndCombineSegments(segments) {
108
+ if (!segments || segments.length === 0) {
109
+ throw new Error('stitchTs: At least one segment is required');
110
+ }
111
+
112
+ let runningVideoPts = 0;
113
+ let runningAudioPts = 0;
114
+
115
+ const combined = {
116
+ videoAccessUnits: [],
117
+ audioAccessUnits: [],
118
+ videoPts: [],
119
+ videoDts: [],
120
+ audioPts: [],
121
+ // Metadata from first segment with data
122
+ audioSampleRate: null,
123
+ audioChannels: null,
124
+ videoStreamType: null,
125
+ audioStreamType: null
126
+ };
127
+
128
+ for (let i = 0; i < segments.length; i++) {
129
+ const segmentData = normalizeInput(segments[i]);
130
+
131
+ const parser = new TSParser();
132
+ parser.parse(segmentData);
133
+ parser.finalize();
134
+
135
+ // Skip empty segments
136
+ if (parser.videoAccessUnits.length === 0 && parser.audioAccessUnits.length === 0) {
137
+ continue;
138
+ }
139
+
140
+ // Capture metadata from first segment with data
141
+ if (combined.audioSampleRate === null && parser.audioSampleRate) {
142
+ combined.audioSampleRate = parser.audioSampleRate;
143
+ combined.audioChannels = parser.audioChannels;
144
+ }
145
+ if (combined.videoStreamType === null && parser.videoStreamType) {
146
+ combined.videoStreamType = parser.videoStreamType;
147
+ }
148
+ if (combined.audioStreamType === null && parser.audioStreamType) {
149
+ combined.audioStreamType = parser.audioStreamType;
150
+ }
151
+
152
+ // Calculate this segment's duration for next offset
153
+ const segmentVideoDuration = getSegmentDuration(parser.videoDts);
154
+ const segmentAudioDuration = getSegmentDuration(parser.audioPts);
155
+
156
+ // Offset and append video access units
157
+ for (const au of parser.videoAccessUnits) {
158
+ combined.videoAccessUnits.push({
159
+ nalUnits: au.nalUnits,
160
+ pts: au.pts + runningVideoPts,
161
+ dts: au.dts + runningVideoPts
162
+ });
163
+ combined.videoPts.push(au.pts + runningVideoPts);
164
+ combined.videoDts.push(au.dts + runningVideoPts);
165
+ }
166
+
167
+ // Offset and append audio access units
168
+ for (const au of parser.audioAccessUnits) {
169
+ combined.audioAccessUnits.push({
170
+ data: au.data,
171
+ pts: au.pts + runningAudioPts
172
+ });
173
+ combined.audioPts.push(au.pts + runningAudioPts);
174
+ }
175
+
176
+ // Advance running offsets for next segment
177
+ runningVideoPts += segmentVideoDuration;
178
+ runningAudioPts += segmentAudioDuration;
179
+ }
180
+
181
+ if (combined.videoAccessUnits.length === 0) {
182
+ throw new Error('stitchTs: No video frames found in any segment');
183
+ }
184
+
185
+ return combined;
186
+ }
187
+
188
+ // ============================================
189
+ // Public API
190
+ // ============================================
191
+
192
+ /**
193
+ * Stitch multiple MPEG-TS segments into a single standard MP4
194
+ *
195
+ * @param {(Uint8Array | ArrayBuffer)[]} segments - Array of TS segment data
196
+ * @returns {Uint8Array} MP4 data
197
+ *
198
+ * @example
199
+ * const mp4Data = stitchTs([segment1, segment2, segment3]);
200
+ */
201
+ export function stitchTs(segments) {
202
+ const combined = parseAndCombineSegments(segments);
203
+ const muxer = new MP4Muxer(combined);
204
+ return muxer.build();
205
+ }
206
+
207
+ /**
208
+ * Concatenate multiple MPEG-TS segments into a single continuous TS stream
209
+ *
210
+ * @param {(Uint8Array | ArrayBuffer)[]} segments - Array of TS segment data
211
+ * @returns {Uint8Array} Combined MPEG-TS data with continuous timestamps
212
+ *
213
+ * @example
214
+ * const tsData = concatTs([segment1, segment2, segment3]);
215
+ */
216
+ export function concatTs(segments) {
217
+ const combined = parseAndCombineSegments(segments);
218
+ const { sps, pps } = extractSpsPps(combined.videoAccessUnits);
219
+
220
+ const muxer = new TSMuxer();
221
+
222
+ if (sps && pps) {
223
+ muxer.setSpsPps(sps, pps);
224
+ }
225
+ muxer.setHasAudio(combined.audioAccessUnits.length > 0);
226
+
227
+ // Queue all audio samples (need to wrap raw AAC in ADTS)
228
+ const sampleRate = combined.audioSampleRate || 48000;
229
+ const channels = combined.audioChannels || 2;
230
+
231
+ for (const au of combined.audioAccessUnits) {
232
+ // Build ADTS frame from raw AAC data
233
+ const header = buildAdtsHeader(au.data.length, sampleRate, channels);
234
+ const adtsFrame = new Uint8Array(header.length + au.data.length);
235
+ adtsFrame.set(header, 0);
236
+ adtsFrame.set(au.data, header.length);
237
+ muxer.addAudioSample(adtsFrame, au.pts);
238
+ }
239
+
240
+ // Add video samples using NAL units directly
241
+ for (const au of combined.videoAccessUnits) {
242
+ const isKey = isKeyframe(au);
243
+ muxer.addVideoNalUnits(au.nalUnits, isKey, au.pts, au.dts);
244
+ }
245
+
246
+ muxer.flush();
247
+ return muxer.build();
248
+ }
249
+
250
+ export { parseAndCombineSegments, isKeyframe, extractSpsPps };
251
+ export default stitchTs;