@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.
package/src/ts-to-mp4.js CHANGED
@@ -51,14 +51,14 @@ function clipAccessUnits(videoAUs, audioAUs, startTime, endTime) {
51
51
  const PTS_PER_SECOND = 90000;
52
52
  const startPts = startTime * PTS_PER_SECOND;
53
53
  const endPts = endTime * PTS_PER_SECOND;
54
-
54
+
55
55
  // Find keyframe at or before startTime (needed for decoding)
56
56
  let keyframeIdx = 0;
57
57
  for (let i = 0; i < videoAUs.length; i++) {
58
58
  if (videoAUs[i].pts > startPts) break;
59
59
  if (isKeyframe(videoAUs[i])) keyframeIdx = i;
60
60
  }
61
-
61
+
62
62
  // Find first frame at or after endTime
63
63
  let endIdx = videoAUs.length;
64
64
  for (let i = keyframeIdx; i < videoAUs.length; i++) {
@@ -67,10 +67,10 @@ function clipAccessUnits(videoAUs, audioAUs, startTime, endTime) {
67
67
  break;
68
68
  }
69
69
  }
70
-
70
+
71
71
  // Clip video starting from keyframe (for proper decoding)
72
72
  const clippedVideo = videoAUs.slice(keyframeIdx, endIdx);
73
-
73
+
74
74
  if (clippedVideo.length === 0) {
75
75
  return {
76
76
  video: [],
@@ -81,35 +81,35 @@ function clipAccessUnits(videoAUs, audioAUs, startTime, endTime) {
81
81
  preroll: 0
82
82
  };
83
83
  }
84
-
84
+
85
85
  // Get PTS of keyframe and requested start
86
86
  const keyframePts = clippedVideo[0].pts;
87
87
  const lastFramePts = clippedVideo[clippedVideo.length - 1].pts;
88
-
88
+
89
89
  // Pre-roll: time between keyframe and requested start
90
90
  // This is the time the decoder needs to process but player shouldn't display
91
91
  const prerollPts = Math.max(0, startPts - keyframePts);
92
-
92
+
93
93
  // Clip audio to the REQUESTED time range (not from keyframe)
94
94
  // Audio doesn't need keyframe pre-roll
95
95
  const audioStartPts = startPts;
96
96
  const audioEndPts = Math.min(endPts, lastFramePts + 90000); // Include audio slightly past last video
97
97
  const clippedAudio = audioAUs.filter(au => au.pts >= audioStartPts && au.pts < audioEndPts);
98
-
98
+
99
99
  // Normalize video timestamps so keyframe starts at 0
100
100
  const offset = keyframePts;
101
101
  for (const au of clippedVideo) {
102
102
  au.pts -= offset;
103
103
  au.dts -= offset;
104
104
  }
105
-
105
+
106
106
  // Normalize audio timestamps so it starts at 0 (matching video playback start after preroll)
107
107
  // Audio doesn't have preroll, so it should start at PTS 0 to sync with video after edit list
108
108
  const audioOffset = audioStartPts; // Use requested start, not keyframe
109
109
  for (const au of clippedAudio) {
110
110
  au.pts -= audioOffset;
111
111
  }
112
-
112
+
113
113
  return {
114
114
  video: clippedVideo,
115
115
  audio: clippedAudio,
@@ -144,9 +144,9 @@ export function analyzeTsData(tsData) {
144
144
  const parser = new TSParser();
145
145
  parser.parse(tsData);
146
146
  parser.finalize();
147
-
147
+
148
148
  const PTS_PER_SECOND = 90000;
149
-
149
+
150
150
  // Find keyframes and their timestamps
151
151
  const keyframes = [];
152
152
  for (let i = 0; i < parser.videoAccessUnits.length; i++) {
@@ -157,15 +157,15 @@ export function analyzeTsData(tsData) {
157
157
  });
158
158
  }
159
159
  }
160
-
160
+
161
161
  // Calculate duration
162
- const videoDuration = parser.videoPts.length > 0
162
+ const videoDuration = parser.videoPts.length > 0
163
163
  ? (Math.max(...parser.videoPts) - Math.min(...parser.videoPts)) / PTS_PER_SECOND
164
164
  : 0;
165
165
  const audioDuration = parser.audioPts.length > 0
166
166
  ? (Math.max(...parser.audioPts) - Math.min(...parser.audioPts)) / PTS_PER_SECOND
167
167
  : 0;
168
-
168
+
169
169
  return {
170
170
  duration: Math.max(videoDuration, audioDuration),
171
171
  videoFrames: parser.videoAccessUnits.length,
@@ -180,17 +180,17 @@ export function analyzeTsData(tsData) {
180
180
  }
181
181
 
182
182
  export function convertTsToMp4(tsData, options = {}) {
183
- const log = options.onProgress || (() => {});
184
-
183
+ const log = options.onProgress || (() => { });
184
+
185
185
  log(`Parsing...`, { phase: 'convert', percent: 52 });
186
186
  const parser = new TSParser();
187
187
  parser.parse(tsData);
188
188
  parser.finalize();
189
-
189
+
190
190
  const debug = parser.debug;
191
191
  const videoInfo = getCodecInfo(parser.videoStreamType);
192
192
  const audioInfo = getCodecInfo(parser.audioStreamType);
193
-
193
+
194
194
  // Log parsing results
195
195
  log(`Parsed ${debug.packets} TS packets`, { phase: 'convert', percent: 55 });
196
196
  log(`PAT: ${debug.patFound ? '✓' : '✗'}, PMT: ${debug.pmtFound ? '✓' : '✗'}`);
@@ -199,16 +199,16 @@ export function convertTsToMp4(tsData, options = {}) {
199
199
  if (parser.audioSampleRate) audioDetails.push(`${parser.audioSampleRate}Hz`);
200
200
  if (parser.audioChannels) audioDetails.push(`${parser.audioChannels}ch`);
201
201
  log(`Audio: ${parser.audioPid ? `PID ${parser.audioPid}` : 'none'} → ${audioInfo.name}${audioDetails.length ? ` (${audioDetails.join(', ')})` : ''}`);
202
-
202
+
203
203
  // Check for structural issues first
204
204
  if (!debug.patFound) {
205
205
  throw new Error('Invalid MPEG-TS: No PAT (Program Association Table) found. File may be corrupted or not MPEG-TS format.');
206
206
  }
207
-
207
+
208
208
  if (!debug.pmtFound) {
209
209
  throw new Error('Invalid MPEG-TS: No PMT (Program Map Table) found. File may be corrupted or missing stream info.');
210
210
  }
211
-
211
+
212
212
  // Check for unsupported video codec BEFORE we report frame counts
213
213
  if (parser.videoStreamType && !videoInfo.supported) {
214
214
  throw new Error(
@@ -217,7 +217,7 @@ export function convertTsToMp4(tsData, options = {}) {
217
217
  `Your file needs to be transcoded to H.264 first.`
218
218
  );
219
219
  }
220
-
220
+
221
221
  // Check for unsupported audio codec
222
222
  if (parser.audioStreamType && !audioInfo.supported) {
223
223
  throw new Error(
@@ -226,7 +226,7 @@ export function convertTsToMp4(tsData, options = {}) {
226
226
  `Your file needs to be transcoded to AAC first.`
227
227
  );
228
228
  }
229
-
229
+
230
230
  // Check if we found any supported video
231
231
  if (!parser.videoPid) {
232
232
  throw new Error(
@@ -234,61 +234,61 @@ export function convertTsToMp4(tsData, options = {}) {
234
234
  'This library supports: H.264/AVC, H.265/HEVC'
235
235
  );
236
236
  }
237
-
237
+
238
238
  log(`Frames: ${parser.videoAccessUnits.length} video, ${parser.audioAccessUnits.length} audio`, { phase: 'convert', percent: 60 });
239
239
  if (debug.audioPesStarts) {
240
240
  log(`Audio: ${debug.audioPesStarts} PES starts → ${debug.audioPesCount || 0} processed → ${debug.audioFramesInPes || 0} ADTS frames${debug.audioSkipped ? ` (${debug.audioSkipped} skipped)` : ''}`);
241
241
  }
242
-
242
+
243
243
  if (parser.videoAccessUnits.length === 0) {
244
244
  throw new Error('Video stream found but no frames could be extracted. File may be corrupted.');
245
245
  }
246
-
246
+
247
247
  // Report timestamp normalization
248
248
  if (debug.timestampNormalized) {
249
249
  const offsetMs = (debug.timestampOffset / 90).toFixed(1);
250
250
  log(`Timestamps normalized: -${offsetMs}ms offset`);
251
251
  }
252
-
252
+
253
253
  log(`Processing...`, { phase: 'convert', percent: 70 });
254
-
254
+
255
255
  // Track preroll for edit list (used for precise clipping)
256
256
  let clipPreroll = 0;
257
-
257
+
258
258
  // Apply time range clipping if specified
259
259
  if (options.startTime !== undefined || options.endTime !== undefined) {
260
260
  const startTime = options.startTime || 0;
261
261
  const endTime = options.endTime !== undefined ? options.endTime : Infinity;
262
-
262
+
263
263
  const clipResult = clipAccessUnits(
264
264
  parser.videoAccessUnits,
265
265
  parser.audioAccessUnits,
266
266
  startTime,
267
267
  endTime
268
268
  );
269
-
269
+
270
270
  parser.videoAccessUnits = clipResult.video;
271
271
  parser.audioAccessUnits = clipResult.audio;
272
272
  clipPreroll = clipResult.preroll;
273
-
273
+
274
274
  // Update PTS arrays to match
275
275
  parser.videoPts = clipResult.video.map(au => au.pts);
276
276
  parser.videoDts = clipResult.video.map(au => au.dts);
277
277
  parser.audioPts = clipResult.audio.map(au => au.pts);
278
-
278
+
279
279
  const prerollMs = (clipPreroll / 90).toFixed(0);
280
280
  const endTimeStr = clipResult.requestedEndTime === Infinity ? 'end' : clipResult.requestedEndTime.toFixed(2) + 's';
281
- const clipDuration = clipResult.requestedEndTime === Infinity
281
+ const clipDuration = clipResult.requestedEndTime === Infinity
282
282
  ? (clipResult.actualEndTime - clipResult.requestedStartTime).toFixed(2)
283
283
  : (clipResult.requestedEndTime - clipResult.requestedStartTime).toFixed(2);
284
284
  log(`Clipped: ${clipResult.requestedStartTime.toFixed(2)}s - ${endTimeStr} (${clipDuration}s, ${prerollMs}ms preroll)`, { phase: 'convert', percent: 80 });
285
285
  }
286
-
286
+
287
287
  log(`Building MP4...`, { phase: 'convert', percent: 85 });
288
288
  const muxer = new MP4Muxer(parser, { preroll: clipPreroll });
289
289
  const { width, height } = muxer.getVideoDimensions();
290
290
  log(`Dimensions: ${width}x${height}`);
291
-
291
+
292
292
  const result = muxer.build();
293
293
  log(`Complete`, { phase: 'convert', percent: 100 });
294
294
  return result;
@@ -1,375 +0,0 @@
1
- /**
2
- * Fragmented MP4 to Standard MP4 Converter
3
- * Pure JavaScript - no dependencies
4
- */
5
-
6
- // ============================================
7
- // Box Utilities
8
- // ============================================
9
- function parseBoxes(data, offset = 0, end = data.byteLength) {
10
- const boxes = [];
11
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
12
- while (offset < end) {
13
- if (offset + 8 > end) break;
14
- const size = view.getUint32(offset);
15
- const type = String.fromCharCode(data[offset+4], data[offset+5], data[offset+6], data[offset+7]);
16
- if (size === 0 || size < 8) break;
17
- boxes.push({ type, offset, size, data: data.subarray(offset, offset + size) });
18
- offset += size;
19
- }
20
- return boxes;
21
- }
22
-
23
- function findBox(boxes, type) {
24
- for (const box of boxes) if (box.type === type) return box;
25
- return null;
26
- }
27
-
28
- function parseChildBoxes(box, headerSize = 8) {
29
- return parseBoxes(box.data, headerSize, box.size);
30
- }
31
-
32
- function createBox(type, ...payloads) {
33
- let size = 8;
34
- for (const p of payloads) size += p.byteLength;
35
- const result = new Uint8Array(size);
36
- const view = new DataView(result.buffer);
37
- view.setUint32(0, size);
38
- result[4] = type.charCodeAt(0); result[5] = type.charCodeAt(1); result[6] = type.charCodeAt(2); result[7] = type.charCodeAt(3);
39
- let offset = 8;
40
- for (const p of payloads) { result.set(p, offset); offset += p.byteLength; }
41
- return result;
42
- }
43
-
44
- // ============================================
45
- // trun/tfhd Parsing
46
- // ============================================
47
- function parseTrunWithOffset(trunData) {
48
- const view = new DataView(trunData.buffer, trunData.byteOffset, trunData.byteLength);
49
- const version = trunData[8];
50
- const flags = (trunData[9] << 16) | (trunData[10] << 8) | trunData[11];
51
- const sampleCount = view.getUint32(12);
52
- let offset = 16, dataOffset = 0;
53
- if (flags & 0x1) { dataOffset = view.getInt32(offset); offset += 4; }
54
- if (flags & 0x4) offset += 4;
55
- const samples = [];
56
- for (let i = 0; i < sampleCount; i++) {
57
- const sample = {};
58
- if (flags & 0x100) { sample.duration = view.getUint32(offset); offset += 4; }
59
- if (flags & 0x200) { sample.size = view.getUint32(offset); offset += 4; }
60
- if (flags & 0x400) { sample.flags = view.getUint32(offset); offset += 4; }
61
- if (flags & 0x800) { sample.compositionTimeOffset = version === 0 ? view.getUint32(offset) : view.getInt32(offset); offset += 4; }
62
- samples.push(sample);
63
- }
64
- return { samples, dataOffset };
65
- }
66
-
67
- function parseTfhd(tfhdData) {
68
- return new DataView(tfhdData.buffer, tfhdData.byteOffset, tfhdData.byteLength).getUint32(12);
69
- }
70
-
71
- // ============================================
72
- // Moov Rebuilding
73
- // ============================================
74
- function rebuildMvhd(mvhdBox, duration) {
75
- const data = new Uint8Array(mvhdBox.data);
76
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
77
- const version = data[8];
78
- const durationOffset = version === 0 ? 24 : 32;
79
- if (version === 0) view.setUint32(durationOffset, duration);
80
- else { view.setUint32(durationOffset, 0); view.setUint32(durationOffset + 4, duration); }
81
- return data;
82
- }
83
-
84
- function rebuildTkhd(tkhdBox, trackInfo, maxDuration) {
85
- const data = new Uint8Array(tkhdBox.data);
86
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
87
- const version = data[8];
88
- let trackDuration = maxDuration;
89
- if (trackInfo) { trackDuration = 0; for (const s of trackInfo.samples) trackDuration += s.duration || 0; }
90
- if (version === 0) view.setUint32(28, trackDuration);
91
- else { view.setUint32(36, 0); view.setUint32(40, trackDuration); }
92
- return data;
93
- }
94
-
95
- function rebuildMdhd(mdhdBox, trackInfo, maxDuration) {
96
- const data = new Uint8Array(mdhdBox.data);
97
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
98
- const version = data[8];
99
- let trackDuration = 0;
100
- if (trackInfo) for (const s of trackInfo.samples) trackDuration += s.duration || 0;
101
- const durationOffset = version === 0 ? 24 : 32;
102
- if (version === 0) view.setUint32(durationOffset, trackDuration);
103
- else { view.setUint32(durationOffset, 0); view.setUint32(durationOffset + 4, trackDuration); }
104
- return data;
105
- }
106
-
107
- function rebuildStbl(stblBox, trackInfo) {
108
- const stblChildren = parseChildBoxes(stblBox);
109
- const newParts = [];
110
- for (const child of stblChildren) if (child.type === 'stsd') { newParts.push(child.data); break; }
111
- const samples = trackInfo?.samples || [];
112
- const chunkOffsets = trackInfo?.chunkOffsets || [];
113
-
114
- // stts
115
- const sttsEntries = [];
116
- let curDur = null, count = 0;
117
- for (const s of samples) {
118
- const d = s.duration || 0;
119
- if (d === curDur) count++;
120
- else { if (curDur !== null) sttsEntries.push({ count, duration: curDur }); curDur = d; count = 1; }
121
- }
122
- if (curDur !== null) sttsEntries.push({ count, duration: curDur });
123
- const sttsData = new Uint8Array(8 + sttsEntries.length * 8);
124
- const sttsView = new DataView(sttsData.buffer);
125
- sttsView.setUint32(4, sttsEntries.length);
126
- let off = 8;
127
- for (const e of sttsEntries) { sttsView.setUint32(off, e.count); sttsView.setUint32(off + 4, e.duration); off += 8; }
128
- newParts.push(createBox('stts', sttsData));
129
-
130
- // stsc
131
- const stscEntries = [];
132
- if (chunkOffsets.length > 0) {
133
- let currentSampleCount = chunkOffsets[0].sampleCount, firstChunk = 1;
134
- for (let i = 1; i <= chunkOffsets.length; i++) {
135
- const sampleCount = i < chunkOffsets.length ? chunkOffsets[i].sampleCount : -1;
136
- if (sampleCount !== currentSampleCount) {
137
- stscEntries.push({ firstChunk, samplesPerChunk: currentSampleCount, sampleDescriptionIndex: 1 });
138
- firstChunk = i + 1; currentSampleCount = sampleCount;
139
- }
140
- }
141
- } else stscEntries.push({ firstChunk: 1, samplesPerChunk: samples.length, sampleDescriptionIndex: 1 });
142
- const stscData = new Uint8Array(8 + stscEntries.length * 12);
143
- const stscView = new DataView(stscData.buffer);
144
- stscView.setUint32(4, stscEntries.length);
145
- off = 8;
146
- for (const e of stscEntries) { stscView.setUint32(off, e.firstChunk); stscView.setUint32(off + 4, e.samplesPerChunk); stscView.setUint32(off + 8, e.sampleDescriptionIndex); off += 12; }
147
- newParts.push(createBox('stsc', stscData));
148
-
149
- // stsz
150
- const stszData = new Uint8Array(12 + samples.length * 4);
151
- const stszView = new DataView(stszData.buffer);
152
- stszView.setUint32(8, samples.length);
153
- off = 12;
154
- for (const s of samples) { stszView.setUint32(off, s.size || 0); off += 4; }
155
- newParts.push(createBox('stsz', stszData));
156
-
157
- // stco
158
- const numChunks = chunkOffsets.length || 1;
159
- const stcoData = new Uint8Array(8 + numChunks * 4);
160
- const stcoView = new DataView(stcoData.buffer);
161
- stcoView.setUint32(4, numChunks);
162
- for (let i = 0; i < numChunks; i++) stcoView.setUint32(8 + i * 4, chunkOffsets[i]?.offset || 0);
163
- newParts.push(createBox('stco', stcoData));
164
-
165
- // ctts
166
- const hasCtts = samples.some(s => s.compositionTimeOffset);
167
- if (hasCtts) {
168
- const cttsEntries = [];
169
- let curOff = null; count = 0;
170
- for (const s of samples) {
171
- const o = s.compositionTimeOffset || 0;
172
- if (o === curOff) count++;
173
- else { if (curOff !== null) cttsEntries.push({ count, offset: curOff }); curOff = o; count = 1; }
174
- }
175
- if (curOff !== null) cttsEntries.push({ count, offset: curOff });
176
- const cttsData = new Uint8Array(8 + cttsEntries.length * 8);
177
- const cttsView = new DataView(cttsData.buffer);
178
- cttsView.setUint32(4, cttsEntries.length);
179
- off = 8;
180
- for (const e of cttsEntries) { cttsView.setUint32(off, e.count); cttsView.setInt32(off + 4, e.offset); off += 8; }
181
- newParts.push(createBox('ctts', cttsData));
182
- }
183
-
184
- // stss
185
- const syncSamples = [];
186
- for (let i = 0; i < samples.length; i++) {
187
- const flags = samples[i].flags;
188
- if (flags !== undefined) { if (!((flags >> 16) & 0x1)) syncSamples.push(i + 1); }
189
- }
190
- if (syncSamples.length > 0 && syncSamples.length < samples.length) {
191
- const stssData = new Uint8Array(8 + syncSamples.length * 4);
192
- const stssView = new DataView(stssData.buffer);
193
- stssView.setUint32(4, syncSamples.length);
194
- off = 8;
195
- for (const n of syncSamples) { stssView.setUint32(off, n); off += 4; }
196
- newParts.push(createBox('stss', stssData));
197
- }
198
-
199
- return createBox('stbl', ...newParts);
200
- }
201
-
202
- function rebuildMinf(minfBox, trackInfo) {
203
- const minfChildren = parseChildBoxes(minfBox);
204
- const newParts = [];
205
- for (const child of minfChildren) {
206
- if (child.type === 'stbl') newParts.push(rebuildStbl(child, trackInfo));
207
- else newParts.push(child.data);
208
- }
209
- return createBox('minf', ...newParts);
210
- }
211
-
212
- function rebuildMdia(mdiaBox, trackInfo, maxDuration) {
213
- const mdiaChildren = parseChildBoxes(mdiaBox);
214
- const newParts = [];
215
- for (const child of mdiaChildren) {
216
- if (child.type === 'minf') newParts.push(rebuildMinf(child, trackInfo));
217
- else if (child.type === 'mdhd') newParts.push(rebuildMdhd(child, trackInfo, maxDuration));
218
- else newParts.push(child.data);
219
- }
220
- return createBox('mdia', ...newParts);
221
- }
222
-
223
- function rebuildTrak(trakBox, trackIdMap, maxDuration) {
224
- const trakChildren = parseChildBoxes(trakBox);
225
- let trackId = 1;
226
- for (const child of trakChildren) {
227
- if (child.type === 'tkhd') {
228
- const view = new DataView(child.data.buffer, child.data.byteOffset, child.data.byteLength);
229
- trackId = child.data[8] === 0 ? view.getUint32(20) : view.getUint32(28);
230
- }
231
- }
232
- const trackInfo = trackIdMap.get(trackId);
233
- const newParts = [];
234
- let hasEdts = false;
235
- for (const child of trakChildren) {
236
- if (child.type === 'edts') { hasEdts = true; newParts.push(child.data); }
237
- else if (child.type === 'mdia') newParts.push(rebuildMdia(child, trackInfo, maxDuration));
238
- else if (child.type === 'tkhd') newParts.push(rebuildTkhd(child, trackInfo, maxDuration));
239
- else newParts.push(child.data);
240
- }
241
- if (!hasEdts && trackInfo) {
242
- let trackDuration = 0;
243
- for (const s of trackInfo.samples) trackDuration += s.duration || 0;
244
- const elstData = new Uint8Array(20);
245
- const elstView = new DataView(elstData.buffer);
246
- elstView.setUint32(4, 1); elstView.setUint32(8, maxDuration); elstView.setInt32(12, 0); elstView.setInt16(16, 1);
247
- const elst = createBox('elst', elstData);
248
- const edts = createBox('edts', elst);
249
- const tkhdIndex = newParts.findIndex(p => p.length >= 8 && String.fromCharCode(p[4], p[5], p[6], p[7]) === 'tkhd');
250
- if (tkhdIndex >= 0) newParts.splice(tkhdIndex + 1, 0, edts);
251
- }
252
- return createBox('trak', ...newParts);
253
- }
254
-
255
- function updateStcoOffsets(output, ftypSize, moovSize) {
256
- const mdatContentOffset = ftypSize + moovSize + 8;
257
- const view = new DataView(output.buffer, output.byteOffset, output.byteLength);
258
- function scan(start, end) {
259
- let pos = start;
260
- while (pos + 8 <= end) {
261
- const size = view.getUint32(pos);
262
- if (size < 8) break;
263
- const type = String.fromCharCode(output[pos+4], output[pos+5], output[pos+6], output[pos+7]);
264
- if (type === 'stco') {
265
- const entryCount = view.getUint32(pos + 12);
266
- for (let i = 0; i < entryCount; i++) {
267
- const entryPos = pos + 16 + i * 4;
268
- view.setUint32(entryPos, mdatContentOffset + view.getUint32(entryPos));
269
- }
270
- } else if (['moov', 'trak', 'mdia', 'minf', 'stbl'].includes(type)) scan(pos + 8, pos + size);
271
- pos += size;
272
- }
273
- }
274
- scan(0, output.byteLength);
275
- }
276
-
277
- /**
278
- * Convert fragmented MP4 to standard MP4
279
- * @param {Uint8Array} fmp4Data - fMP4 data
280
- * @returns {Uint8Array} Standard MP4 data
281
- */
282
- export function convertFmp4ToMp4(fmp4Data) {
283
- const boxes = parseBoxes(fmp4Data);
284
- const ftyp = findBox(boxes, 'ftyp');
285
- const moov = findBox(boxes, 'moov');
286
- if (!ftyp || !moov) throw new Error('Invalid fMP4: missing ftyp or moov');
287
-
288
- const moovChildren = parseChildBoxes(moov);
289
- const originalTrackIds = [];
290
- for (const child of moovChildren) {
291
- if (child.type === 'trak') {
292
- const trakChildren = parseChildBoxes(child);
293
- for (const tc of trakChildren) {
294
- if (tc.type === 'tkhd') {
295
- const view = new DataView(tc.data.buffer, tc.data.byteOffset, tc.data.byteLength);
296
- originalTrackIds.push(tc.data[8] === 0 ? view.getUint32(20) : view.getUint32(28));
297
- }
298
- }
299
- }
300
- }
301
-
302
- const tracks = new Map();
303
- const mdatChunks = [];
304
- let combinedMdatOffset = 0;
305
-
306
- for (let i = 0; i < boxes.length; i++) {
307
- const box = boxes[i];
308
- if (box.type === 'moof') {
309
- const moofChildren = parseChildBoxes(box);
310
- const moofStart = box.offset;
311
- let nextMdatOffset = 0;
312
- for (let j = i + 1; j < boxes.length; j++) {
313
- if (boxes[j].type === 'mdat') { nextMdatOffset = boxes[j].offset; break; }
314
- if (boxes[j].type === 'moof') break;
315
- }
316
- for (const child of moofChildren) {
317
- if (child.type === 'traf') {
318
- const trafChildren = parseChildBoxes(child);
319
- const tfhd = findBox(trafChildren, 'tfhd');
320
- const trun = findBox(trafChildren, 'trun');
321
- if (tfhd && trun) {
322
- const trackId = parseTfhd(tfhd.data);
323
- const { samples, dataOffset } = parseTrunWithOffset(trun.data);
324
- if (!tracks.has(trackId)) tracks.set(trackId, { samples: [], chunkOffsets: [] });
325
- const track = tracks.get(trackId);
326
- const chunkOffset = combinedMdatOffset + (moofStart + dataOffset) - (nextMdatOffset + 8);
327
- track.chunkOffsets.push({ offset: chunkOffset, sampleCount: samples.length });
328
- track.samples.push(...samples);
329
- }
330
- }
331
- }
332
- } else if (box.type === 'mdat') {
333
- mdatChunks.push({ data: box.data.subarray(8), offset: combinedMdatOffset });
334
- combinedMdatOffset += box.data.subarray(8).byteLength;
335
- }
336
- }
337
-
338
- const totalMdatSize = mdatChunks.reduce((sum, c) => sum + c.data.byteLength, 0);
339
- const combinedMdat = new Uint8Array(totalMdatSize);
340
- for (const chunk of mdatChunks) combinedMdat.set(chunk.data, chunk.offset);
341
-
342
- const trackIdMap = new Map();
343
- const fmp4TrackIds = Array.from(tracks.keys()).sort((a, b) => a - b);
344
- for (let i = 0; i < fmp4TrackIds.length && i < originalTrackIds.length; i++) {
345
- trackIdMap.set(originalTrackIds[i], tracks.get(fmp4TrackIds[i]));
346
- }
347
-
348
- let maxDuration = 0;
349
- for (const [, track] of tracks) {
350
- let dur = 0;
351
- for (const s of track.samples) dur += s.duration || 0;
352
- maxDuration = Math.max(maxDuration, dur);
353
- }
354
-
355
- const newMoovParts = [];
356
- for (const child of moovChildren) {
357
- if (child.type === 'mvex') continue;
358
- if (child.type === 'trak') newMoovParts.push(rebuildTrak(child, trackIdMap, maxDuration));
359
- else if (child.type === 'mvhd') newMoovParts.push(rebuildMvhd(child, maxDuration));
360
- else newMoovParts.push(child.data);
361
- }
362
-
363
- const newMoov = createBox('moov', ...newMoovParts);
364
- const newMdat = createBox('mdat', combinedMdat);
365
- const output = new Uint8Array(ftyp.size + newMoov.byteLength + newMdat.byteLength);
366
- output.set(ftyp.data, 0);
367
- output.set(newMoov, ftyp.size);
368
- output.set(newMdat, ftyp.size + newMoov.byteLength);
369
- updateStcoOffsets(output, ftyp.size, newMoov.byteLength);
370
-
371
- return output;
372
- }
373
-
374
- export default convertFmp4ToMp4;
375
-