@invintusmedia/tomp4 1.0.9 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tomp4.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * toMp4.js v1.0.9
2
+ * toMp4.js v1.1.1
3
3
  * Convert MPEG-TS and fMP4 to standard MP4
4
4
  * https://github.com/TVWIT/toMp4.js
5
5
  * MIT License
@@ -72,14 +72,14 @@
72
72
  const PTS_PER_SECOND = 90000;
73
73
  const startPts = startTime * PTS_PER_SECOND;
74
74
  const endPts = endTime * PTS_PER_SECOND;
75
-
75
+
76
76
  // Find keyframe at or before startTime (needed for decoding)
77
77
  let keyframeIdx = 0;
78
78
  for (let i = 0; i < videoAUs.length; i++) {
79
79
  if (videoAUs[i].pts > startPts) break;
80
80
  if (isKeyframe(videoAUs[i])) keyframeIdx = i;
81
81
  }
82
-
82
+
83
83
  // Find first frame at or after endTime
84
84
  let endIdx = videoAUs.length;
85
85
  for (let i = keyframeIdx; i < videoAUs.length; i++) {
@@ -88,10 +88,10 @@
88
88
  break;
89
89
  }
90
90
  }
91
-
91
+
92
92
  // Clip video starting from keyframe (for proper decoding)
93
93
  const clippedVideo = videoAUs.slice(keyframeIdx, endIdx);
94
-
94
+
95
95
  if (clippedVideo.length === 0) {
96
96
  return {
97
97
  video: [],
@@ -102,35 +102,35 @@
102
102
  preroll: 0
103
103
  };
104
104
  }
105
-
105
+
106
106
  // Get PTS of keyframe and requested start
107
107
  const keyframePts = clippedVideo[0].pts;
108
108
  const lastFramePts = clippedVideo[clippedVideo.length - 1].pts;
109
-
109
+
110
110
  // Pre-roll: time between keyframe and requested start
111
111
  // This is the time the decoder needs to process but player shouldn't display
112
112
  const prerollPts = Math.max(0, startPts - keyframePts);
113
-
113
+
114
114
  // Clip audio to the REQUESTED time range (not from keyframe)
115
115
  // Audio doesn't need keyframe pre-roll
116
116
  const audioStartPts = startPts;
117
117
  const audioEndPts = Math.min(endPts, lastFramePts + 90000); // Include audio slightly past last video
118
118
  const clippedAudio = audioAUs.filter(au => au.pts >= audioStartPts && au.pts < audioEndPts);
119
-
119
+
120
120
  // Normalize video timestamps so keyframe starts at 0
121
121
  const offset = keyframePts;
122
122
  for (const au of clippedVideo) {
123
123
  au.pts -= offset;
124
124
  au.dts -= offset;
125
125
  }
126
-
126
+
127
127
  // Normalize audio timestamps so it starts at 0 (matching video playback start after preroll)
128
128
  // Audio doesn't have preroll, so it should start at PTS 0 to sync with video after edit list
129
129
  const audioOffset = audioStartPts; // Use requested start, not keyframe
130
130
  for (const au of clippedAudio) {
131
131
  au.pts -= audioOffset;
132
132
  }
133
-
133
+
134
134
  return {
135
135
  video: clippedVideo,
136
136
  audio: clippedAudio,
@@ -165,9 +165,9 @@
165
165
  const parser = new TSParser();
166
166
  parser.parse(tsData);
167
167
  parser.finalize();
168
-
168
+
169
169
  const PTS_PER_SECOND = 90000;
170
-
170
+
171
171
  // Find keyframes and their timestamps
172
172
  const keyframes = [];
173
173
  for (let i = 0; i < parser.videoAccessUnits.length; i++) {
@@ -178,15 +178,15 @@
178
178
  });
179
179
  }
180
180
  }
181
-
181
+
182
182
  // Calculate duration
183
- const videoDuration = parser.videoPts.length > 0
183
+ const videoDuration = parser.videoPts.length > 0
184
184
  ? (Math.max(...parser.videoPts) - Math.min(...parser.videoPts)) / PTS_PER_SECOND
185
185
  : 0;
186
186
  const audioDuration = parser.audioPts.length > 0
187
187
  ? (Math.max(...parser.audioPts) - Math.min(...parser.audioPts)) / PTS_PER_SECOND
188
188
  : 0;
189
-
189
+
190
190
  return {
191
191
  duration: Math.max(videoDuration, audioDuration),
192
192
  videoFrames: parser.videoAccessUnits.length,
@@ -201,17 +201,17 @@
201
201
  }
202
202
 
203
203
  function convertTsToMp4(tsData, options = {}) {
204
- const log = options.onProgress || (() => {});
205
-
204
+ const log = options.onProgress || (() => { });
205
+
206
206
  log(`Parsing...`, { phase: 'convert', percent: 52 });
207
207
  const parser = new TSParser();
208
208
  parser.parse(tsData);
209
209
  parser.finalize();
210
-
210
+
211
211
  const debug = parser.debug;
212
212
  const videoInfo = getCodecInfo(parser.videoStreamType);
213
213
  const audioInfo = getCodecInfo(parser.audioStreamType);
214
-
214
+
215
215
  // Log parsing results
216
216
  log(`Parsed ${debug.packets} TS packets`, { phase: 'convert', percent: 55 });
217
217
  log(`PAT: ${debug.patFound ? '✓' : '✗'}, PMT: ${debug.pmtFound ? '✓' : '✗'}`);
@@ -220,16 +220,16 @@
220
220
  if (parser.audioSampleRate) audioDetails.push(`${parser.audioSampleRate}Hz`);
221
221
  if (parser.audioChannels) audioDetails.push(`${parser.audioChannels}ch`);
222
222
  log(`Audio: ${parser.audioPid ? `PID ${parser.audioPid}` : 'none'} → ${audioInfo.name}${audioDetails.length ? ` (${audioDetails.join(', ')})` : ''}`);
223
-
223
+
224
224
  // Check for structural issues first
225
225
  if (!debug.patFound) {
226
226
  throw new Error('Invalid MPEG-TS: No PAT (Program Association Table) found. File may be corrupted or not MPEG-TS format.');
227
227
  }
228
-
228
+
229
229
  if (!debug.pmtFound) {
230
230
  throw new Error('Invalid MPEG-TS: No PMT (Program Map Table) found. File may be corrupted or missing stream info.');
231
231
  }
232
-
232
+
233
233
  // Check for unsupported video codec BEFORE we report frame counts
234
234
  if (parser.videoStreamType && !videoInfo.supported) {
235
235
  throw new Error(
@@ -238,7 +238,7 @@
238
238
  `Your file needs to be transcoded to H.264 first.`
239
239
  );
240
240
  }
241
-
241
+
242
242
  // Check for unsupported audio codec
243
243
  if (parser.audioStreamType && !audioInfo.supported) {
244
244
  throw new Error(
@@ -247,7 +247,7 @@
247
247
  `Your file needs to be transcoded to AAC first.`
248
248
  );
249
249
  }
250
-
250
+
251
251
  // Check if we found any supported video
252
252
  if (!parser.videoPid) {
253
253
  throw new Error(
@@ -255,61 +255,61 @@
255
255
  'This library supports: H.264/AVC, H.265/HEVC'
256
256
  );
257
257
  }
258
-
258
+
259
259
  log(`Frames: ${parser.videoAccessUnits.length} video, ${parser.audioAccessUnits.length} audio`, { phase: 'convert', percent: 60 });
260
260
  if (debug.audioPesStarts) {
261
261
  log(`Audio: ${debug.audioPesStarts} PES starts → ${debug.audioPesCount || 0} processed → ${debug.audioFramesInPes || 0} ADTS frames${debug.audioSkipped ? ` (${debug.audioSkipped} skipped)` : ''}`);
262
262
  }
263
-
263
+
264
264
  if (parser.videoAccessUnits.length === 0) {
265
265
  throw new Error('Video stream found but no frames could be extracted. File may be corrupted.');
266
266
  }
267
-
267
+
268
268
  // Report timestamp normalization
269
269
  if (debug.timestampNormalized) {
270
270
  const offsetMs = (debug.timestampOffset / 90).toFixed(1);
271
271
  log(`Timestamps normalized: -${offsetMs}ms offset`);
272
272
  }
273
-
273
+
274
274
  log(`Processing...`, { phase: 'convert', percent: 70 });
275
-
275
+
276
276
  // Track preroll for edit list (used for precise clipping)
277
277
  let clipPreroll = 0;
278
-
278
+
279
279
  // Apply time range clipping if specified
280
280
  if (options.startTime !== undefined || options.endTime !== undefined) {
281
281
  const startTime = options.startTime || 0;
282
282
  const endTime = options.endTime !== undefined ? options.endTime : Infinity;
283
-
283
+
284
284
  const clipResult = clipAccessUnits(
285
285
  parser.videoAccessUnits,
286
286
  parser.audioAccessUnits,
287
287
  startTime,
288
288
  endTime
289
289
  );
290
-
290
+
291
291
  parser.videoAccessUnits = clipResult.video;
292
292
  parser.audioAccessUnits = clipResult.audio;
293
293
  clipPreroll = clipResult.preroll;
294
-
294
+
295
295
  // Update PTS arrays to match
296
296
  parser.videoPts = clipResult.video.map(au => au.pts);
297
297
  parser.videoDts = clipResult.video.map(au => au.dts);
298
298
  parser.audioPts = clipResult.audio.map(au => au.pts);
299
-
299
+
300
300
  const prerollMs = (clipPreroll / 90).toFixed(0);
301
301
  const endTimeStr = clipResult.requestedEndTime === Infinity ? 'end' : clipResult.requestedEndTime.toFixed(2) + 's';
302
- const clipDuration = clipResult.requestedEndTime === Infinity
302
+ const clipDuration = clipResult.requestedEndTime === Infinity
303
303
  ? (clipResult.actualEndTime - clipResult.requestedStartTime).toFixed(2)
304
304
  : (clipResult.requestedEndTime - clipResult.requestedStartTime).toFixed(2);
305
305
  log(`Clipped: ${clipResult.requestedStartTime.toFixed(2)}s - ${endTimeStr} (${clipDuration}s, ${prerollMs}ms preroll)`, { phase: 'convert', percent: 80 });
306
306
  }
307
-
307
+
308
308
  log(`Building MP4...`, { phase: 'convert', percent: 85 });
309
309
  const muxer = new MP4Muxer(parser, { preroll: clipPreroll });
310
310
  const { width, height } = muxer.getVideoDimensions();
311
311
  log(`Dimensions: ${width}x${height}`);
312
-
312
+
313
313
  const result = muxer.build();
314
314
  log(`Complete`, { phase: 'convert', percent: 100 });
315
315
  return result;
@@ -322,376 +322,325 @@
322
322
  // fMP4 to MP4 Converter
323
323
  // ============================================
324
324
  /**
325
- * Fragmented MP4 to Standard MP4 Converter
326
- * Pure JavaScript - no dependencies
325
+ * fMP4 to Standard MP4 Converter
326
+ *
327
+ * Converts a fragmented MP4 file to a standard MP4 container
328
+ * by extracting samples from fragments and rebuilding the moov box.
329
+ *
330
+ * @module fmp4/converter
327
331
  */
328
332
 
329
- // ============================================
330
- // Box Utilities
331
- // ============================================
332
- function parseBoxes(data, offset = 0, end = data.byteLength) {
333
- const boxes = [];
334
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
335
- while (offset < end) {
336
- if (offset + 8 > end) break;
337
- const size = view.getUint32(offset);
338
- const type = String.fromCharCode(data[offset+4], data[offset+5], data[offset+6], data[offset+7]);
339
- if (size === 0 || size < 8) break;
340
- boxes.push({ type, offset, size, data: data.subarray(offset, offset + size) });
341
- offset += size;
342
- }
343
- return boxes;
344
- }
345
-
346
- function findBox(boxes, type) {
347
- for (const box of boxes) if (box.type === type) return box;
348
- return null;
349
- }
350
-
351
- function parseChildBoxes(box, headerSize = 8) {
352
- return parseBoxes(box.data, headerSize, box.size);
353
- }
354
-
355
- function createBox(type, ...payloads) {
356
- let size = 8;
357
- for (const p of payloads) size += p.byteLength;
358
- const result = new Uint8Array(size);
359
- const view = new DataView(result.buffer);
360
- view.setUint32(0, size);
361
- result[4] = type.charCodeAt(0); result[5] = type.charCodeAt(1); result[6] = type.charCodeAt(2); result[7] = type.charCodeAt(3);
362
- let offset = 8;
363
- for (const p of payloads) { result.set(p, offset); offset += p.byteLength; }
364
- return result;
365
- }
333
+ import {
334
+ parseBoxes, findBox, parseChildBoxes, createBox,
335
+ parseTfhd, parseTrun
336
+ } from './utils.js';
366
337
 
367
338
  // ============================================
368
- // trun/tfhd Parsing
339
+ // Moov Rebuilding Functions
369
340
  // ============================================
370
- function parseTrunWithOffset(trunData) {
371
- const view = new DataView(trunData.buffer, trunData.byteOffset, trunData.byteLength);
372
- const version = trunData[8];
373
- const flags = (trunData[9] << 16) | (trunData[10] << 8) | trunData[11];
374
- const sampleCount = view.getUint32(12);
375
- let offset = 16, dataOffset = 0;
376
- if (flags & 0x1) { dataOffset = view.getInt32(offset); offset += 4; }
377
- if (flags & 0x4) offset += 4;
378
- const samples = [];
379
- for (let i = 0; i < sampleCount; i++) {
380
- const sample = {};
381
- if (flags & 0x100) { sample.duration = view.getUint32(offset); offset += 4; }
382
- if (flags & 0x200) { sample.size = view.getUint32(offset); offset += 4; }
383
- if (flags & 0x400) { sample.flags = view.getUint32(offset); offset += 4; }
384
- if (flags & 0x800) { sample.compositionTimeOffset = version === 0 ? view.getUint32(offset) : view.getInt32(offset); offset += 4; }
385
- samples.push(sample);
386
- }
387
- return { samples, dataOffset };
388
- }
389
-
390
- function parseTfhd(tfhdData) {
391
- return new DataView(tfhdData.buffer, tfhdData.byteOffset, tfhdData.byteLength).getUint32(12);
392
- }
393
341
 
394
- // ============================================
395
- // Moov Rebuilding
396
- // ============================================
397
342
  function rebuildMvhd(mvhdBox, duration) {
398
- const data = new Uint8Array(mvhdBox.data);
399
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
400
- const version = data[8];
401
- const durationOffset = version === 0 ? 24 : 32;
402
- if (version === 0) view.setUint32(durationOffset, duration);
403
- else { view.setUint32(durationOffset, 0); view.setUint32(durationOffset + 4, duration); }
404
- return data;
343
+ const data = new Uint8Array(mvhdBox.data);
344
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
345
+ const version = data[8];
346
+ const durationOffset = version === 0 ? 24 : 32;
347
+ if (version === 0) view.setUint32(durationOffset, duration);
348
+ else { view.setUint32(durationOffset, 0); view.setUint32(durationOffset + 4, duration); }
349
+ return data;
405
350
  }
406
351
 
407
352
  function rebuildTkhd(tkhdBox, trackInfo, maxDuration) {
408
- const data = new Uint8Array(tkhdBox.data);
409
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
410
- const version = data[8];
411
- let trackDuration = maxDuration;
412
- if (trackInfo) { trackDuration = 0; for (const s of trackInfo.samples) trackDuration += s.duration || 0; }
413
- if (version === 0) view.setUint32(28, trackDuration);
414
- else { view.setUint32(36, 0); view.setUint32(40, trackDuration); }
415
- return data;
353
+ const data = new Uint8Array(tkhdBox.data);
354
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
355
+ const version = data[8];
356
+ let trackDuration = maxDuration;
357
+ if (trackInfo) { trackDuration = 0; for (const s of trackInfo.samples) trackDuration += s.duration || 0; }
358
+ if (version === 0) view.setUint32(28, trackDuration);
359
+ else { view.setUint32(36, 0); view.setUint32(40, trackDuration); }
360
+ return data;
416
361
  }
417
362
 
418
363
  function rebuildMdhd(mdhdBox, trackInfo, maxDuration) {
419
- const data = new Uint8Array(mdhdBox.data);
420
- const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
421
- const version = data[8];
422
- let trackDuration = 0;
423
- if (trackInfo) for (const s of trackInfo.samples) trackDuration += s.duration || 0;
424
- const durationOffset = version === 0 ? 24 : 32;
425
- if (version === 0) view.setUint32(durationOffset, trackDuration);
426
- else { view.setUint32(durationOffset, 0); view.setUint32(durationOffset + 4, trackDuration); }
427
- return data;
364
+ const data = new Uint8Array(mdhdBox.data);
365
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
366
+ const version = data[8];
367
+ let trackDuration = 0;
368
+ if (trackInfo) for (const s of trackInfo.samples) trackDuration += s.duration || 0;
369
+ const durationOffset = version === 0 ? 24 : 32;
370
+ if (version === 0) view.setUint32(durationOffset, trackDuration);
371
+ else { view.setUint32(durationOffset, 0); view.setUint32(durationOffset + 4, trackDuration); }
372
+ return data;
428
373
  }
429
374
 
430
375
  function rebuildStbl(stblBox, trackInfo) {
431
- const stblChildren = parseChildBoxes(stblBox);
432
- const newParts = [];
433
- for (const child of stblChildren) if (child.type === 'stsd') { newParts.push(child.data); break; }
434
- const samples = trackInfo?.samples || [];
435
- const chunkOffsets = trackInfo?.chunkOffsets || [];
436
-
437
- // stts
438
- const sttsEntries = [];
439
- let curDur = null, count = 0;
440
- for (const s of samples) {
441
- const d = s.duration || 0;
442
- if (d === curDur) count++;
443
- else { if (curDur !== null) sttsEntries.push({ count, duration: curDur }); curDur = d; count = 1; }
444
- }
445
- if (curDur !== null) sttsEntries.push({ count, duration: curDur });
446
- const sttsData = new Uint8Array(8 + sttsEntries.length * 8);
447
- const sttsView = new DataView(sttsData.buffer);
448
- sttsView.setUint32(4, sttsEntries.length);
449
- let off = 8;
450
- for (const e of sttsEntries) { sttsView.setUint32(off, e.count); sttsView.setUint32(off + 4, e.duration); off += 8; }
451
- newParts.push(createBox('stts', sttsData));
452
-
453
- // stsc
454
- const stscEntries = [];
455
- if (chunkOffsets.length > 0) {
456
- let currentSampleCount = chunkOffsets[0].sampleCount, firstChunk = 1;
457
- for (let i = 1; i <= chunkOffsets.length; i++) {
458
- const sampleCount = i < chunkOffsets.length ? chunkOffsets[i].sampleCount : -1;
459
- if (sampleCount !== currentSampleCount) {
460
- stscEntries.push({ firstChunk, samplesPerChunk: currentSampleCount, sampleDescriptionIndex: 1 });
461
- firstChunk = i + 1; currentSampleCount = sampleCount;
462
- }
463
- }
464
- } else stscEntries.push({ firstChunk: 1, samplesPerChunk: samples.length, sampleDescriptionIndex: 1 });
465
- const stscData = new Uint8Array(8 + stscEntries.length * 12);
466
- const stscView = new DataView(stscData.buffer);
467
- stscView.setUint32(4, stscEntries.length);
468
- off = 8;
469
- for (const e of stscEntries) { stscView.setUint32(off, e.firstChunk); stscView.setUint32(off + 4, e.samplesPerChunk); stscView.setUint32(off + 8, e.sampleDescriptionIndex); off += 12; }
470
- newParts.push(createBox('stsc', stscData));
471
-
472
- // stsz
473
- const stszData = new Uint8Array(12 + samples.length * 4);
474
- const stszView = new DataView(stszData.buffer);
475
- stszView.setUint32(8, samples.length);
476
- off = 12;
477
- for (const s of samples) { stszView.setUint32(off, s.size || 0); off += 4; }
478
- newParts.push(createBox('stsz', stszData));
479
-
480
- // stco
481
- const numChunks = chunkOffsets.length || 1;
482
- const stcoData = new Uint8Array(8 + numChunks * 4);
483
- const stcoView = new DataView(stcoData.buffer);
484
- stcoView.setUint32(4, numChunks);
485
- for (let i = 0; i < numChunks; i++) stcoView.setUint32(8 + i * 4, chunkOffsets[i]?.offset || 0);
486
- newParts.push(createBox('stco', stcoData));
487
-
488
- // ctts
489
- const hasCtts = samples.some(s => s.compositionTimeOffset);
490
- if (hasCtts) {
491
- const cttsEntries = [];
492
- let curOff = null; count = 0;
376
+ const stblChildren = parseChildBoxes(stblBox);
377
+ const newParts = [];
378
+ for (const child of stblChildren) if (child.type === 'stsd') { newParts.push(child.data); break; }
379
+ const samples = trackInfo?.samples || [];
380
+ const chunkOffsets = trackInfo?.chunkOffsets || [];
381
+
382
+ // stts
383
+ const sttsEntries = [];
384
+ let curDur = null, count = 0;
493
385
  for (const s of samples) {
494
- const o = s.compositionTimeOffset || 0;
495
- if (o === curOff) count++;
496
- else { if (curOff !== null) cttsEntries.push({ count, offset: curOff }); curOff = o; count = 1; }
386
+ const d = s.duration || 0;
387
+ if (d === curDur) count++;
388
+ else { if (curDur !== null) sttsEntries.push({ count, duration: curDur }); curDur = d; count = 1; }
497
389
  }
498
- if (curOff !== null) cttsEntries.push({ count, offset: curOff });
499
- const cttsData = new Uint8Array(8 + cttsEntries.length * 8);
500
- const cttsView = new DataView(cttsData.buffer);
501
- cttsView.setUint32(4, cttsEntries.length);
502
- off = 8;
503
- for (const e of cttsEntries) { cttsView.setUint32(off, e.count); cttsView.setInt32(off + 4, e.offset); off += 8; }
504
- newParts.push(createBox('ctts', cttsData));
505
- }
506
-
507
- // stss
508
- const syncSamples = [];
509
- for (let i = 0; i < samples.length; i++) {
510
- const flags = samples[i].flags;
511
- if (flags !== undefined) { if (!((flags >> 16) & 0x1)) syncSamples.push(i + 1); }
512
- }
513
- if (syncSamples.length > 0 && syncSamples.length < samples.length) {
514
- const stssData = new Uint8Array(8 + syncSamples.length * 4);
515
- const stssView = new DataView(stssData.buffer);
516
- stssView.setUint32(4, syncSamples.length);
390
+ if (curDur !== null) sttsEntries.push({ count, duration: curDur });
391
+ const sttsData = new Uint8Array(8 + sttsEntries.length * 8);
392
+ const sttsView = new DataView(sttsData.buffer);
393
+ sttsView.setUint32(4, sttsEntries.length);
394
+ let off = 8;
395
+ for (const e of sttsEntries) { sttsView.setUint32(off, e.count); sttsView.setUint32(off + 4, e.duration); off += 8; }
396
+ newParts.push(createBox('stts', sttsData));
397
+
398
+ // stsc
399
+ const stscEntries = [];
400
+ if (chunkOffsets.length > 0) {
401
+ let currentSampleCount = chunkOffsets[0].sampleCount, firstChunk = 1;
402
+ for (let i = 1; i <= chunkOffsets.length; i++) {
403
+ const sampleCount = i < chunkOffsets.length ? chunkOffsets[i].sampleCount : -1;
404
+ if (sampleCount !== currentSampleCount) {
405
+ stscEntries.push({ firstChunk, samplesPerChunk: currentSampleCount, sampleDescriptionIndex: 1 });
406
+ firstChunk = i + 1; currentSampleCount = sampleCount;
407
+ }
408
+ }
409
+ } else stscEntries.push({ firstChunk: 1, samplesPerChunk: samples.length, sampleDescriptionIndex: 1 });
410
+ const stscData = new Uint8Array(8 + stscEntries.length * 12);
411
+ const stscView = new DataView(stscData.buffer);
412
+ stscView.setUint32(4, stscEntries.length);
517
413
  off = 8;
518
- for (const n of syncSamples) { stssView.setUint32(off, n); off += 4; }
519
- newParts.push(createBox('stss', stssData));
520
- }
521
-
522
- return createBox('stbl', ...newParts);
414
+ for (const e of stscEntries) { stscView.setUint32(off, e.firstChunk); stscView.setUint32(off + 4, e.samplesPerChunk); stscView.setUint32(off + 8, e.sampleDescriptionIndex); off += 12; }
415
+ newParts.push(createBox('stsc', stscData));
416
+
417
+ // stsz
418
+ const stszData = new Uint8Array(12 + samples.length * 4);
419
+ const stszView = new DataView(stszData.buffer);
420
+ stszView.setUint32(8, samples.length);
421
+ off = 12;
422
+ for (const s of samples) { stszView.setUint32(off, s.size || 0); off += 4; }
423
+ newParts.push(createBox('stsz', stszData));
424
+
425
+ // stco
426
+ const numChunks = chunkOffsets.length || 1;
427
+ const stcoData = new Uint8Array(8 + numChunks * 4);
428
+ const stcoView = new DataView(stcoData.buffer);
429
+ stcoView.setUint32(4, numChunks);
430
+ for (let i = 0; i < numChunks; i++) stcoView.setUint32(8 + i * 4, chunkOffsets[i]?.offset || 0);
431
+ newParts.push(createBox('stco', stcoData));
432
+
433
+ // ctts
434
+ const hasCtts = samples.some(s => s.compositionTimeOffset);
435
+ if (hasCtts) {
436
+ const cttsEntries = [];
437
+ let curOff = null; count = 0;
438
+ for (const s of samples) {
439
+ const o = s.compositionTimeOffset || 0;
440
+ if (o === curOff) count++;
441
+ else { if (curOff !== null) cttsEntries.push({ count, offset: curOff }); curOff = o; count = 1; }
442
+ }
443
+ if (curOff !== null) cttsEntries.push({ count, offset: curOff });
444
+ const cttsData = new Uint8Array(8 + cttsEntries.length * 8);
445
+ const cttsView = new DataView(cttsData.buffer);
446
+ cttsView.setUint32(4, cttsEntries.length);
447
+ off = 8;
448
+ for (const e of cttsEntries) { cttsView.setUint32(off, e.count); cttsView.setInt32(off + 4, e.offset); off += 8; }
449
+ newParts.push(createBox('ctts', cttsData));
450
+ }
451
+
452
+ // stss
453
+ const syncSamples = [];
454
+ for (let i = 0; i < samples.length; i++) {
455
+ const flags = samples[i].flags;
456
+ if (flags !== undefined) { if (!((flags >> 16) & 0x1)) syncSamples.push(i + 1); }
457
+ }
458
+ if (syncSamples.length > 0 && syncSamples.length < samples.length) {
459
+ const stssData = new Uint8Array(8 + syncSamples.length * 4);
460
+ const stssView = new DataView(stssData.buffer);
461
+ stssView.setUint32(4, syncSamples.length);
462
+ off = 8;
463
+ for (const n of syncSamples) { stssView.setUint32(off, n); off += 4; }
464
+ newParts.push(createBox('stss', stssData));
465
+ }
466
+
467
+ return createBox('stbl', ...newParts);
523
468
  }
524
469
 
525
470
  function rebuildMinf(minfBox, trackInfo) {
526
- const minfChildren = parseChildBoxes(minfBox);
527
- const newParts = [];
528
- for (const child of minfChildren) {
529
- if (child.type === 'stbl') newParts.push(rebuildStbl(child, trackInfo));
530
- else newParts.push(child.data);
531
- }
532
- return createBox('minf', ...newParts);
471
+ const minfChildren = parseChildBoxes(minfBox);
472
+ const newParts = [];
473
+ for (const child of minfChildren) {
474
+ if (child.type === 'stbl') newParts.push(rebuildStbl(child, trackInfo));
475
+ else newParts.push(child.data);
476
+ }
477
+ return createBox('minf', ...newParts);
533
478
  }
534
479
 
535
480
  function rebuildMdia(mdiaBox, trackInfo, maxDuration) {
536
- const mdiaChildren = parseChildBoxes(mdiaBox);
537
- const newParts = [];
538
- for (const child of mdiaChildren) {
539
- if (child.type === 'minf') newParts.push(rebuildMinf(child, trackInfo));
540
- else if (child.type === 'mdhd') newParts.push(rebuildMdhd(child, trackInfo, maxDuration));
541
- else newParts.push(child.data);
542
- }
543
- return createBox('mdia', ...newParts);
481
+ const mdiaChildren = parseChildBoxes(mdiaBox);
482
+ const newParts = [];
483
+ for (const child of mdiaChildren) {
484
+ if (child.type === 'minf') newParts.push(rebuildMinf(child, trackInfo));
485
+ else if (child.type === 'mdhd') newParts.push(rebuildMdhd(child, trackInfo, maxDuration));
486
+ else newParts.push(child.data);
487
+ }
488
+ return createBox('mdia', ...newParts);
544
489
  }
545
490
 
546
491
  function rebuildTrak(trakBox, trackIdMap, maxDuration) {
547
- const trakChildren = parseChildBoxes(trakBox);
548
- let trackId = 1;
549
- for (const child of trakChildren) {
550
- if (child.type === 'tkhd') {
551
- const view = new DataView(child.data.buffer, child.data.byteOffset, child.data.byteLength);
552
- trackId = child.data[8] === 0 ? view.getUint32(20) : view.getUint32(28);
492
+ const trakChildren = parseChildBoxes(trakBox);
493
+ let trackId = 1;
494
+ for (const child of trakChildren) {
495
+ if (child.type === 'tkhd') {
496
+ const view = new DataView(child.data.buffer, child.data.byteOffset, child.data.byteLength);
497
+ trackId = child.data[8] === 0 ? view.getUint32(20) : view.getUint32(28);
498
+ }
553
499
  }
554
- }
555
- const trackInfo = trackIdMap.get(trackId);
556
- const newParts = [];
557
- let hasEdts = false;
558
- for (const child of trakChildren) {
559
- if (child.type === 'edts') { hasEdts = true; newParts.push(child.data); }
560
- else if (child.type === 'mdia') newParts.push(rebuildMdia(child, trackInfo, maxDuration));
561
- else if (child.type === 'tkhd') newParts.push(rebuildTkhd(child, trackInfo, maxDuration));
562
- else newParts.push(child.data);
563
- }
564
- if (!hasEdts && trackInfo) {
565
- let trackDuration = 0;
566
- for (const s of trackInfo.samples) trackDuration += s.duration || 0;
567
- const elstData = new Uint8Array(20);
568
- const elstView = new DataView(elstData.buffer);
569
- elstView.setUint32(4, 1); elstView.setUint32(8, maxDuration); elstView.setInt32(12, 0); elstView.setInt16(16, 1);
570
- const elst = createBox('elst', elstData);
571
- const edts = createBox('edts', elst);
572
- const tkhdIndex = newParts.findIndex(p => p.length >= 8 && String.fromCharCode(p[4], p[5], p[6], p[7]) === 'tkhd');
573
- if (tkhdIndex >= 0) newParts.splice(tkhdIndex + 1, 0, edts);
574
- }
575
- return createBox('trak', ...newParts);
500
+ const trackInfo = trackIdMap.get(trackId);
501
+ const newParts = [];
502
+ let hasEdts = false;
503
+ for (const child of trakChildren) {
504
+ if (child.type === 'edts') { hasEdts = true; newParts.push(child.data); }
505
+ else if (child.type === 'mdia') newParts.push(rebuildMdia(child, trackInfo, maxDuration));
506
+ else if (child.type === 'tkhd') newParts.push(rebuildTkhd(child, trackInfo, maxDuration));
507
+ else newParts.push(child.data);
508
+ }
509
+ if (!hasEdts && trackInfo) {
510
+ let trackDuration = 0;
511
+ for (const s of trackInfo.samples) trackDuration += s.duration || 0;
512
+ const elstData = new Uint8Array(20);
513
+ const elstView = new DataView(elstData.buffer);
514
+ elstView.setUint32(4, 1); elstView.setUint32(8, maxDuration); elstView.setInt32(12, 0); elstView.setInt16(16, 1);
515
+ const elst = createBox('elst', elstData);
516
+ const edts = createBox('edts', elst);
517
+ const tkhdIndex = newParts.findIndex(p => p.length >= 8 && String.fromCharCode(p[4], p[5], p[6], p[7]) === 'tkhd');
518
+ if (tkhdIndex >= 0) newParts.splice(tkhdIndex + 1, 0, edts);
519
+ }
520
+ return createBox('trak', ...newParts);
576
521
  }
577
522
 
578
523
  function updateStcoOffsets(output, ftypSize, moovSize) {
579
- const mdatContentOffset = ftypSize + moovSize + 8;
580
- const view = new DataView(output.buffer, output.byteOffset, output.byteLength);
581
- function scan(start, end) {
582
- let pos = start;
583
- while (pos + 8 <= end) {
584
- const size = view.getUint32(pos);
585
- if (size < 8) break;
586
- const type = String.fromCharCode(output[pos+4], output[pos+5], output[pos+6], output[pos+7]);
587
- if (type === 'stco') {
588
- const entryCount = view.getUint32(pos + 12);
589
- for (let i = 0; i < entryCount; i++) {
590
- const entryPos = pos + 16 + i * 4;
591
- view.setUint32(entryPos, mdatContentOffset + view.getUint32(entryPos));
524
+ const mdatContentOffset = ftypSize + moovSize + 8;
525
+ const view = new DataView(output.buffer, output.byteOffset, output.byteLength);
526
+ function scan(start, end) {
527
+ let pos = start;
528
+ while (pos + 8 <= end) {
529
+ const size = view.getUint32(pos);
530
+ if (size < 8) break;
531
+ const type = String.fromCharCode(output[pos + 4], output[pos + 5], output[pos + 6], output[pos + 7]);
532
+ if (type === 'stco') {
533
+ const entryCount = view.getUint32(pos + 12);
534
+ for (let i = 0; i < entryCount; i++) {
535
+ const entryPos = pos + 16 + i * 4;
536
+ view.setUint32(entryPos, mdatContentOffset + view.getUint32(entryPos));
537
+ }
538
+ } else if (['moov', 'trak', 'mdia', 'minf', 'stbl'].includes(type)) scan(pos + 8, pos + size);
539
+ pos += size;
592
540
  }
593
- } else if (['moov', 'trak', 'mdia', 'minf', 'stbl'].includes(type)) scan(pos + 8, pos + size);
594
- pos += size;
595
541
  }
596
- }
597
- scan(0, output.byteLength);
542
+ scan(0, output.byteLength);
598
543
  }
599
544
 
545
+ // ============================================
546
+ // Main Converter Function
547
+ // ============================================
548
+
600
549
  /**
601
550
  * Convert fragmented MP4 to standard MP4
602
551
  * @param {Uint8Array} fmp4Data - fMP4 data
603
552
  * @returns {Uint8Array} Standard MP4 data
604
553
  */
605
554
  function convertFmp4ToMp4(fmp4Data) {
606
- const boxes = parseBoxes(fmp4Data);
607
- const ftyp = findBox(boxes, 'ftyp');
608
- const moov = findBox(boxes, 'moov');
609
- if (!ftyp || !moov) throw new Error('Invalid fMP4: missing ftyp or moov');
610
-
611
- const moovChildren = parseChildBoxes(moov);
612
- const originalTrackIds = [];
613
- for (const child of moovChildren) {
614
- if (child.type === 'trak') {
615
- const trakChildren = parseChildBoxes(child);
616
- for (const tc of trakChildren) {
617
- if (tc.type === 'tkhd') {
618
- const view = new DataView(tc.data.buffer, tc.data.byteOffset, tc.data.byteLength);
619
- originalTrackIds.push(tc.data[8] === 0 ? view.getUint32(20) : view.getUint32(28));
555
+ const boxes = parseBoxes(fmp4Data);
556
+ const ftyp = findBox(boxes, 'ftyp');
557
+ const moov = findBox(boxes, 'moov');
558
+ if (!ftyp || !moov) throw new Error('Invalid fMP4: missing ftyp or moov');
559
+
560
+ const moovChildren = parseChildBoxes(moov);
561
+ const originalTrackIds = [];
562
+ for (const child of moovChildren) {
563
+ if (child.type === 'trak') {
564
+ const trakChildren = parseChildBoxes(child);
565
+ for (const tc of trakChildren) {
566
+ if (tc.type === 'tkhd') {
567
+ const view = new DataView(tc.data.buffer, tc.data.byteOffset, tc.data.byteLength);
568
+ originalTrackIds.push(tc.data[8] === 0 ? view.getUint32(20) : view.getUint32(28));
569
+ }
570
+ }
620
571
  }
621
- }
622
572
  }
623
- }
624
-
625
- const tracks = new Map();
626
- const mdatChunks = [];
627
- let combinedMdatOffset = 0;
628
-
629
- for (let i = 0; i < boxes.length; i++) {
630
- const box = boxes[i];
631
- if (box.type === 'moof') {
632
- const moofChildren = parseChildBoxes(box);
633
- const moofStart = box.offset;
634
- let nextMdatOffset = 0;
635
- for (let j = i + 1; j < boxes.length; j++) {
636
- if (boxes[j].type === 'mdat') { nextMdatOffset = boxes[j].offset; break; }
637
- if (boxes[j].type === 'moof') break;
638
- }
639
- for (const child of moofChildren) {
640
- if (child.type === 'traf') {
641
- const trafChildren = parseChildBoxes(child);
642
- const tfhd = findBox(trafChildren, 'tfhd');
643
- const trun = findBox(trafChildren, 'trun');
644
- if (tfhd && trun) {
645
- const trackId = parseTfhd(tfhd.data);
646
- const { samples, dataOffset } = parseTrunWithOffset(trun.data);
647
- if (!tracks.has(trackId)) tracks.set(trackId, { samples: [], chunkOffsets: [] });
648
- const track = tracks.get(trackId);
649
- const chunkOffset = combinedMdatOffset + (moofStart + dataOffset) - (nextMdatOffset + 8);
650
- track.chunkOffsets.push({ offset: chunkOffset, sampleCount: samples.length });
651
- track.samples.push(...samples);
652
- }
573
+
574
+ const tracks = new Map();
575
+ const mdatChunks = [];
576
+ let combinedMdatOffset = 0;
577
+
578
+ for (let i = 0; i < boxes.length; i++) {
579
+ const box = boxes[i];
580
+ if (box.type === 'moof') {
581
+ const moofChildren = parseChildBoxes(box);
582
+ const moofStart = box.offset;
583
+ let nextMdatOffset = 0;
584
+ for (let j = i + 1; j < boxes.length; j++) {
585
+ if (boxes[j].type === 'mdat') { nextMdatOffset = boxes[j].offset; break; }
586
+ if (boxes[j].type === 'moof') break;
587
+ }
588
+ for (const child of moofChildren) {
589
+ if (child.type === 'traf') {
590
+ const trafChildren = parseChildBoxes(child);
591
+ const tfhd = findBox(trafChildren, 'tfhd');
592
+ const trun = findBox(trafChildren, 'trun');
593
+ if (tfhd && trun) {
594
+ const tfhdInfo = parseTfhd(tfhd.data);
595
+ const { samples, dataOffset } = parseTrun(trun.data, tfhdInfo);
596
+ if (!tracks.has(tfhdInfo.trackId)) tracks.set(tfhdInfo.trackId, { samples: [], chunkOffsets: [] });
597
+ const track = tracks.get(tfhdInfo.trackId);
598
+ const chunkOffset = combinedMdatOffset + (moofStart + dataOffset) - (nextMdatOffset + 8);
599
+ track.chunkOffsets.push({ offset: chunkOffset, sampleCount: samples.length });
600
+ track.samples.push(...samples);
601
+ }
602
+ }
603
+ }
604
+ } else if (box.type === 'mdat') {
605
+ mdatChunks.push({ data: box.data.subarray(8), offset: combinedMdatOffset });
606
+ combinedMdatOffset += box.data.subarray(8).byteLength;
653
607
  }
654
- }
655
- } else if (box.type === 'mdat') {
656
- mdatChunks.push({ data: box.data.subarray(8), offset: combinedMdatOffset });
657
- combinedMdatOffset += box.data.subarray(8).byteLength;
658
608
  }
659
- }
660
-
661
- const totalMdatSize = mdatChunks.reduce((sum, c) => sum + c.data.byteLength, 0);
662
- const combinedMdat = new Uint8Array(totalMdatSize);
663
- for (const chunk of mdatChunks) combinedMdat.set(chunk.data, chunk.offset);
664
-
665
- const trackIdMap = new Map();
666
- const fmp4TrackIds = Array.from(tracks.keys()).sort((a, b) => a - b);
667
- for (let i = 0; i < fmp4TrackIds.length && i < originalTrackIds.length; i++) {
668
- trackIdMap.set(originalTrackIds[i], tracks.get(fmp4TrackIds[i]));
669
- }
670
-
671
- let maxDuration = 0;
672
- for (const [, track] of tracks) {
673
- let dur = 0;
674
- for (const s of track.samples) dur += s.duration || 0;
675
- maxDuration = Math.max(maxDuration, dur);
676
- }
677
-
678
- const newMoovParts = [];
679
- for (const child of moovChildren) {
680
- if (child.type === 'mvex') continue;
681
- if (child.type === 'trak') newMoovParts.push(rebuildTrak(child, trackIdMap, maxDuration));
682
- else if (child.type === 'mvhd') newMoovParts.push(rebuildMvhd(child, maxDuration));
683
- else newMoovParts.push(child.data);
684
- }
685
-
686
- const newMoov = createBox('moov', ...newMoovParts);
687
- const newMdat = createBox('mdat', combinedMdat);
688
- const output = new Uint8Array(ftyp.size + newMoov.byteLength + newMdat.byteLength);
689
- output.set(ftyp.data, 0);
690
- output.set(newMoov, ftyp.size);
691
- output.set(newMdat, ftyp.size + newMoov.byteLength);
692
- updateStcoOffsets(output, ftyp.size, newMoov.byteLength);
693
-
694
- return output;
609
+
610
+ const totalMdatSize = mdatChunks.reduce((sum, c) => sum + c.data.byteLength, 0);
611
+ const combinedMdat = new Uint8Array(totalMdatSize);
612
+ for (const chunk of mdatChunks) combinedMdat.set(chunk.data, chunk.offset);
613
+
614
+ const trackIdMap = new Map();
615
+ const fmp4TrackIds = Array.from(tracks.keys()).sort((a, b) => a - b);
616
+ for (let i = 0; i < fmp4TrackIds.length && i < originalTrackIds.length; i++) {
617
+ trackIdMap.set(originalTrackIds[i], tracks.get(fmp4TrackIds[i]));
618
+ }
619
+
620
+ let maxDuration = 0;
621
+ for (const [, track] of tracks) {
622
+ let dur = 0;
623
+ for (const s of track.samples) dur += s.duration || 0;
624
+ maxDuration = Math.max(maxDuration, dur);
625
+ }
626
+
627
+ const newMoovParts = [];
628
+ for (const child of moovChildren) {
629
+ if (child.type === 'mvex') continue;
630
+ if (child.type === 'trak') newMoovParts.push(rebuildTrak(child, trackIdMap, maxDuration));
631
+ else if (child.type === 'mvhd') newMoovParts.push(rebuildMvhd(child, maxDuration));
632
+ else newMoovParts.push(child.data);
633
+ }
634
+
635
+ const newMoov = createBox('moov', ...newMoovParts);
636
+ const newMdat = createBox('mdat', combinedMdat);
637
+ const output = new Uint8Array(ftyp.size + newMoov.byteLength + newMdat.byteLength);
638
+ output.set(ftyp.data, 0);
639
+ output.set(newMoov, ftyp.size);
640
+ output.set(newMdat, ftyp.size + newMoov.byteLength);
641
+ updateStcoOffsets(output, ftyp.size, newMoov.byteLength);
642
+
643
+ return output;
695
644
  }
696
645
 
697
646
  default convertFmp4ToMp4;
@@ -756,7 +705,7 @@
756
705
  toMp4.isMpegTs = isMpegTs;
757
706
  toMp4.isFmp4 = isFmp4;
758
707
  toMp4.isStandardMp4 = isStandardMp4;
759
- toMp4.version = '1.0.9';
708
+ toMp4.version = '1.1.1';
760
709
 
761
710
  return toMp4;
762
711
  });