@invintusmedia/tomp4 1.0.9 → 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,7 +31,8 @@
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';
@@ -148,7 +149,7 @@ function isStandardMp4(data) {
148
149
  while (offset + 8 <= data.length) {
149
150
  const size = view.getUint32(offset);
150
151
  if (size < 8) break;
151
- 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]);
152
153
  if (boxType === 'moov') hasMoov = true;
153
154
  if (boxType === 'moof') hasMoof = true;
154
155
  offset += size;
@@ -170,7 +171,7 @@ function detectFormat(data) {
170
171
  function convertData(data, options = {}) {
171
172
  const uint8 = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
172
173
  const format = detectFormat(uint8);
173
-
174
+
174
175
  switch (format) {
175
176
  case 'mpegts':
176
177
  return convertTsToMp4(uint8, options);
@@ -217,15 +218,15 @@ function convertData(data, options = {}) {
217
218
  async function toMp4(input, options = {}) {
218
219
  let data;
219
220
  let filename = options.filename || 'video.mp4';
220
- const log = options.onProgress || (() => {});
221
-
221
+ const log = options.onProgress || (() => { });
222
+
222
223
  // Handle HlsStream object
223
224
  if (input instanceof HlsStream) {
224
225
  if (!options.filename) {
225
226
  const urlPart = (input.masterUrl || '').split('/').pop()?.split('?')[0];
226
227
  filename = urlPart ? urlPart.replace('.m3u8', '.mp4') : 'video.mp4';
227
228
  }
228
- data = await downloadHls(input, {
229
+ data = await downloadHls(input, {
229
230
  ...options,
230
231
  quality: options.quality || 'highest'
231
232
  });
@@ -250,7 +251,7 @@ async function toMp4(input, options = {}) {
250
251
  throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
251
252
  }
252
253
  data = new Uint8Array(await response.arrayBuffer());
253
-
254
+
254
255
  if (!options.filename) {
255
256
  const urlFilename = input.split('/').pop()?.split('?')[0];
256
257
  if (urlFilename) {
@@ -274,7 +275,7 @@ async function toMp4(input, options = {}) {
274
275
  else {
275
276
  throw new Error('Input must be a URL string, HlsStream, Uint8Array, ArrayBuffer, or Blob');
276
277
  }
277
-
278
+
278
279
  // Adjust clip times if we downloaded HLS with a time range
279
280
  // The downloaded segments have been normalized to start at 0,
280
281
  // so we need to adjust the requested clip times accordingly
@@ -289,17 +290,20 @@ async function toMp4(input, options = {}) {
289
290
  }
290
291
  log(`Adjusted clip: ${convertOptions.startTime?.toFixed(2) || 0}s - ${convertOptions.endTime?.toFixed(2) || '∞'}s (offset: -${segmentStart.toFixed(2)}s)`);
291
292
  }
292
-
293
+
293
294
  // Convert
294
295
  log('Converting...');
295
296
  const mp4Data = convertData(data, convertOptions);
296
-
297
+
297
298
  return new Mp4Result(mp4Data, filename);
298
299
  }
299
300
 
300
301
  // Attach utilities to main function
301
302
  toMp4.fromTs = (data, options) => new Mp4Result(convertTsToMp4(data instanceof ArrayBuffer ? new Uint8Array(data) : data, options));
302
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;
303
307
  toMp4.detectFormat = detectFormat;
304
308
  toMp4.isMpegTs = isMpegTs;
305
309
  toMp4.isFmp4 = isFmp4;
@@ -325,18 +329,21 @@ toMp4.TSParser = TSParser;
325
329
  toMp4.RemoteMp4 = RemoteMp4;
326
330
 
327
331
  // Version (injected at build time for dist, read from package.json for ESM)
328
- toMp4.version = '1.0.9';
332
+ toMp4.version = '1.1.0';
329
333
 
330
334
  // Export
331
- export {
332
- toMp4,
333
- Mp4Result,
334
- convertTsToMp4,
335
- convertFmp4ToMp4,
335
+ export {
336
+ toMp4,
337
+ Mp4Result,
338
+ convertTsToMp4,
339
+ convertFmp4ToMp4,
340
+ stitchFmp4,
341
+ stitchTs,
342
+ concatTs,
336
343
  analyzeTsData,
337
- detectFormat,
338
- isMpegTs,
339
- isFmp4,
344
+ detectFormat,
345
+ isMpegTs,
346
+ isFmp4,
340
347
  isStandardMp4,
341
348
  parseHls,
342
349
  downloadHls,
@@ -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;