@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/README.md +25 -1
- package/dist/tomp4.js +312 -363
- package/package.json +6 -3
- package/src/fmp4/converter.js +323 -0
- package/src/fmp4/index.js +25 -0
- package/src/fmp4/stitcher.js +615 -0
- package/src/fmp4/utils.js +201 -0
- package/src/index.js +41 -20
- package/src/mpegts/index.js +7 -0
- package/src/mpegts/stitcher.js +251 -0
- package/src/muxers/mp4.js +85 -85
- package/src/muxers/mpegts.js +101 -19
- package/src/parsers/mp4.js +691 -0
- package/src/parsers/mpegts.js +42 -42
- package/src/remote/index.js +444 -0
- package/src/transcode.js +20 -36
- package/src/ts-to-mp4.js +37 -37
- package/src/fmp4-to-mp4.js +0 -375
package/dist/tomp4.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* toMp4.js v1.0
|
|
2
|
+
* toMp4.js v1.1.0
|
|
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
|
-
*
|
|
326
|
-
*
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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 (
|
|
499
|
-
const
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
off = 8;
|
|
503
|
-
for (const e of
|
|
504
|
-
newParts.push(createBox('
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
|
519
|
-
newParts.push(createBox('
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
|
708
|
+
toMp4.version = '1.1.0';
|
|
760
709
|
|
|
761
710
|
return toMp4;
|
|
762
711
|
});
|