@invintusmedia/tomp4 1.0.6 → 1.0.8
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 +59 -23
- package/package.json +2 -1
- package/src/index.js +1 -1
- package/src/muxers/mp4.js +38 -15
- package/src/muxers/mpegts.js +1 -0
- package/src/ts-to-mp4.js +57 -21
package/dist/tomp4.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* toMp4.js v1.0.
|
|
2
|
+
* toMp4.js v1.0.8
|
|
3
3
|
* Convert MPEG-TS and fMP4 to standard MP4
|
|
4
4
|
* https://github.com/TVWIT/toMp4.js
|
|
5
5
|
* MIT License
|
|
@@ -59,7 +59,9 @@
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* Clip access units to a time range, snapping to keyframes
|
|
62
|
+
* Clip access units to a time range, snapping to keyframes for decode
|
|
63
|
+
* but using edit list for precise playback timing
|
|
64
|
+
*
|
|
63
65
|
* @param {Array} videoAUs - Video access units
|
|
64
66
|
* @param {Array} audioAUs - Audio access units
|
|
65
67
|
* @param {number} startTime - Start time in seconds
|
|
@@ -71,48 +73,73 @@
|
|
|
71
73
|
const startPts = startTime * PTS_PER_SECOND;
|
|
72
74
|
const endPts = endTime * PTS_PER_SECOND;
|
|
73
75
|
|
|
74
|
-
// Find keyframe at or before startTime
|
|
75
|
-
let
|
|
76
|
+
// Find keyframe at or before startTime (needed for decoding)
|
|
77
|
+
let keyframeIdx = 0;
|
|
76
78
|
for (let i = 0; i < videoAUs.length; i++) {
|
|
77
79
|
if (videoAUs[i].pts > startPts) break;
|
|
78
|
-
if (isKeyframe(videoAUs[i]))
|
|
80
|
+
if (isKeyframe(videoAUs[i])) keyframeIdx = i;
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
// Find first frame after endTime
|
|
83
|
+
// Find first frame at or after endTime
|
|
82
84
|
let endIdx = videoAUs.length;
|
|
83
|
-
for (let i =
|
|
85
|
+
for (let i = keyframeIdx; i < videoAUs.length; i++) {
|
|
84
86
|
if (videoAUs[i].pts >= endPts) {
|
|
85
87
|
endIdx = i;
|
|
86
88
|
break;
|
|
87
89
|
}
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
// Clip video
|
|
91
|
-
const clippedVideo = videoAUs.slice(
|
|
92
|
+
// Clip video starting from keyframe (for proper decoding)
|
|
93
|
+
const clippedVideo = videoAUs.slice(keyframeIdx, endIdx);
|
|
94
|
+
|
|
95
|
+
if (clippedVideo.length === 0) {
|
|
96
|
+
return {
|
|
97
|
+
video: [],
|
|
98
|
+
audio: [],
|
|
99
|
+
actualStartTime: startTime,
|
|
100
|
+
actualEndTime: endTime,
|
|
101
|
+
offset: 0,
|
|
102
|
+
preroll: 0
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Get PTS of keyframe and requested start
|
|
107
|
+
const keyframePts = clippedVideo[0].pts;
|
|
108
|
+
const lastFramePts = clippedVideo[clippedVideo.length - 1].pts;
|
|
92
109
|
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
const
|
|
110
|
+
// Pre-roll: time between keyframe and requested start
|
|
111
|
+
// This is the time the decoder needs to process but player shouldn't display
|
|
112
|
+
const prerollPts = Math.max(0, startPts - keyframePts);
|
|
96
113
|
|
|
97
|
-
// Clip audio to
|
|
98
|
-
|
|
114
|
+
// Clip audio to the REQUESTED time range (not from keyframe)
|
|
115
|
+
// Audio doesn't need keyframe pre-roll
|
|
116
|
+
const audioStartPts = startPts;
|
|
117
|
+
const audioEndPts = Math.min(endPts, lastFramePts + 90000); // Include audio slightly past last video
|
|
118
|
+
const clippedAudio = audioAUs.filter(au => au.pts >= audioStartPts && au.pts < audioEndPts);
|
|
99
119
|
|
|
100
|
-
// Normalize timestamps so
|
|
101
|
-
const offset =
|
|
120
|
+
// Normalize video timestamps so keyframe starts at 0
|
|
121
|
+
const offset = keyframePts;
|
|
102
122
|
for (const au of clippedVideo) {
|
|
103
123
|
au.pts -= offset;
|
|
104
124
|
au.dts -= offset;
|
|
105
125
|
}
|
|
126
|
+
|
|
127
|
+
// Normalize audio timestamps so it starts at 0 (matching video playback start after preroll)
|
|
128
|
+
// Audio doesn't have preroll, so it should start at PTS 0 to sync with video after edit list
|
|
129
|
+
const audioOffset = audioStartPts; // Use requested start, not keyframe
|
|
106
130
|
for (const au of clippedAudio) {
|
|
107
|
-
au.pts -=
|
|
131
|
+
au.pts -= audioOffset;
|
|
108
132
|
}
|
|
109
133
|
|
|
110
134
|
return {
|
|
111
135
|
video: clippedVideo,
|
|
112
136
|
audio: clippedAudio,
|
|
113
|
-
actualStartTime:
|
|
114
|
-
actualEndTime:
|
|
115
|
-
|
|
137
|
+
actualStartTime: keyframePts / PTS_PER_SECOND, // Where decode starts (keyframe)
|
|
138
|
+
actualEndTime: lastFramePts / PTS_PER_SECOND,
|
|
139
|
+
requestedStartTime: startTime, // Where playback should start
|
|
140
|
+
requestedEndTime: endTime,
|
|
141
|
+
offset,
|
|
142
|
+
preroll: prerollPts // Edit list will use this to skip pre-roll frames during playback
|
|
116
143
|
};
|
|
117
144
|
}
|
|
118
145
|
|
|
@@ -246,6 +273,9 @@
|
|
|
246
273
|
|
|
247
274
|
log(`Processing...`, { phase: 'convert', percent: 70 });
|
|
248
275
|
|
|
276
|
+
// Track preroll for edit list (used for precise clipping)
|
|
277
|
+
let clipPreroll = 0;
|
|
278
|
+
|
|
249
279
|
// Apply time range clipping if specified
|
|
250
280
|
if (options.startTime !== undefined || options.endTime !== undefined) {
|
|
251
281
|
const startTime = options.startTime || 0;
|
|
@@ -260,17 +290,23 @@
|
|
|
260
290
|
|
|
261
291
|
parser.videoAccessUnits = clipResult.video;
|
|
262
292
|
parser.audioAccessUnits = clipResult.audio;
|
|
293
|
+
clipPreroll = clipResult.preroll;
|
|
263
294
|
|
|
264
295
|
// Update PTS arrays to match
|
|
265
296
|
parser.videoPts = clipResult.video.map(au => au.pts);
|
|
266
297
|
parser.videoDts = clipResult.video.map(au => au.dts);
|
|
267
298
|
parser.audioPts = clipResult.audio.map(au => au.pts);
|
|
268
299
|
|
|
269
|
-
|
|
300
|
+
const prerollMs = (clipPreroll / 90).toFixed(0);
|
|
301
|
+
const endTimeStr = clipResult.requestedEndTime === Infinity ? 'end' : clipResult.requestedEndTime.toFixed(2) + 's';
|
|
302
|
+
const clipDuration = clipResult.requestedEndTime === Infinity
|
|
303
|
+
? (clipResult.actualEndTime - clipResult.requestedStartTime).toFixed(2)
|
|
304
|
+
: (clipResult.requestedEndTime - clipResult.requestedStartTime).toFixed(2);
|
|
305
|
+
log(`Clipped: ${clipResult.requestedStartTime.toFixed(2)}s - ${endTimeStr} (${clipDuration}s, ${prerollMs}ms preroll)`, { phase: 'convert', percent: 80 });
|
|
270
306
|
}
|
|
271
307
|
|
|
272
308
|
log(`Building MP4...`, { phase: 'convert', percent: 85 });
|
|
273
|
-
const muxer = new MP4Muxer(parser);
|
|
309
|
+
const muxer = new MP4Muxer(parser, { preroll: clipPreroll });
|
|
274
310
|
const { width, height } = muxer.getVideoDimensions();
|
|
275
311
|
log(`Dimensions: ${width}x${height}`);
|
|
276
312
|
|
|
@@ -720,7 +756,7 @@
|
|
|
720
756
|
toMp4.isMpegTs = isMpegTs;
|
|
721
757
|
toMp4.isFmp4 = isFmp4;
|
|
722
758
|
toMp4.isStandardMp4 = isStandardMp4;
|
|
723
|
-
toMp4.version = '1.0.
|
|
759
|
+
toMp4.version = '1.0.8';
|
|
724
760
|
|
|
725
761
|
return toMp4;
|
|
726
762
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@invintusmedia/tomp4",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Convert MPEG-TS, fMP4, and HLS streams to MP4 with clipping support - pure JavaScript, zero dependencies",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "node build.js",
|
|
22
22
|
"dev": "npx serve . -p 3000",
|
|
23
|
+
"test": "node tests/clip.test.js",
|
|
23
24
|
"release": "npm run build && git add -A && git commit -m \"v$(node -p \"require('./package.json').version\")\" && git tag v$(node -p \"require('./package.json').version\") && git push && git push --tags",
|
|
24
25
|
"release:patch": "npm version patch --no-git-tag-version && npm run release",
|
|
25
26
|
"release:minor": "npm version minor --no-git-tag-version && npm run release",
|
package/src/index.js
CHANGED
|
@@ -316,7 +316,7 @@ toMp4.transcode = transcode;
|
|
|
316
316
|
toMp4.isWebCodecsSupported = isWebCodecsSupported;
|
|
317
317
|
|
|
318
318
|
// Version (injected at build time for dist, read from package.json for ESM)
|
|
319
|
-
toMp4.version = '1.0.
|
|
319
|
+
toMp4.version = '1.0.8';
|
|
320
320
|
|
|
321
321
|
// Export
|
|
322
322
|
export {
|
package/src/muxers/mp4.js
CHANGED
|
@@ -149,13 +149,16 @@ export function parseSPS(sps) {
|
|
|
149
149
|
export class MP4Muxer {
|
|
150
150
|
/**
|
|
151
151
|
* @param {TSParser} parser - Parser with video/audio access units
|
|
152
|
+
* @param {Object} [options] - Muxer options
|
|
153
|
+
* @param {number} [options.preroll=0] - Pre-roll time in 90kHz ticks (for edit list)
|
|
152
154
|
*/
|
|
153
|
-
constructor(parser) {
|
|
155
|
+
constructor(parser, options = {}) {
|
|
154
156
|
this.parser = parser;
|
|
155
157
|
this.videoTimescale = 90000;
|
|
156
158
|
this.audioTimescale = parser.audioSampleRate || 48000;
|
|
157
159
|
this.audioSampleDuration = 1024;
|
|
158
160
|
this.videoDimensions = null;
|
|
161
|
+
this.preroll = options.preroll || 0; // Pre-roll for precise clipping
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
getVideoDimensions() {
|
|
@@ -265,7 +268,7 @@ export class MP4Muxer {
|
|
|
265
268
|
const data = new Uint8Array(96);
|
|
266
269
|
const view = new DataView(data.buffer);
|
|
267
270
|
view.setUint32(8, this.videoTimescale);
|
|
268
|
-
view.setUint32(12, this.
|
|
271
|
+
view.setUint32(12, this.calculatePlaybackDuration()); // Use playback duration for movie header
|
|
269
272
|
view.setUint32(16, 0x00010000);
|
|
270
273
|
view.setUint16(20, 0x0100);
|
|
271
274
|
view.setUint32(32, 0x00010000);
|
|
@@ -283,6 +286,15 @@ export class MP4Muxer {
|
|
|
283
286
|
return Math.round(lastDts - firstDts + avgDuration);
|
|
284
287
|
}
|
|
285
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Calculate playback duration (total duration minus pre-roll)
|
|
291
|
+
* This is what the player should report as the video length
|
|
292
|
+
*/
|
|
293
|
+
calculatePlaybackDuration() {
|
|
294
|
+
const totalDuration = this.calculateVideoDuration();
|
|
295
|
+
return this.preroll > 0 ? Math.max(0, totalDuration - this.preroll) : totalDuration;
|
|
296
|
+
}
|
|
297
|
+
|
|
286
298
|
buildVideoTrak() {
|
|
287
299
|
const edts = this.buildVideoEdts();
|
|
288
300
|
if (edts) {
|
|
@@ -297,18 +309,25 @@ export class MP4Muxer {
|
|
|
297
309
|
const firstAU = this.parser.videoAccessUnits[0];
|
|
298
310
|
const firstVideoPts = firstAU.pts;
|
|
299
311
|
|
|
300
|
-
|
|
312
|
+
// Use preroll for precise clipping, or firstVideoPts for timestamp normalization
|
|
313
|
+
const mediaTime = this.preroll > 0 ? this.preroll : firstVideoPts;
|
|
314
|
+
|
|
315
|
+
// If no offset needed, skip edit list
|
|
316
|
+
if (mediaTime === 0) return null;
|
|
301
317
|
|
|
302
|
-
|
|
303
|
-
const
|
|
318
|
+
// Calculate playback duration (total duration minus pre-roll)
|
|
319
|
+
const totalDuration = this.calculateVideoDuration();
|
|
320
|
+
const playbackDuration = this.preroll > 0
|
|
321
|
+
? Math.max(0, totalDuration - this.preroll)
|
|
322
|
+
: totalDuration;
|
|
304
323
|
|
|
305
324
|
const elstData = new Uint8Array(16);
|
|
306
325
|
const view = new DataView(elstData.buffer);
|
|
307
|
-
view.setUint32(0, 1);
|
|
308
|
-
view.setUint32(4,
|
|
309
|
-
view.setInt32(8, mediaTime);
|
|
310
|
-
view.setUint16(12, 1);
|
|
311
|
-
view.setUint16(14, 0);
|
|
326
|
+
view.setUint32(0, 1); // entry_count
|
|
327
|
+
view.setUint32(4, playbackDuration); // segment_duration (what player reports)
|
|
328
|
+
view.setInt32(8, mediaTime); // media_time (where to start in media)
|
|
329
|
+
view.setUint16(12, 1); // media_rate_integer
|
|
330
|
+
view.setUint16(14, 0); // media_rate_fraction
|
|
312
331
|
|
|
313
332
|
const elst = createFullBox('elst', 0, 0, elstData);
|
|
314
333
|
return createBox('edts', elst);
|
|
@@ -319,7 +338,7 @@ export class MP4Muxer {
|
|
|
319
338
|
const data = new Uint8Array(80);
|
|
320
339
|
const view = new DataView(data.buffer);
|
|
321
340
|
view.setUint32(8, 256);
|
|
322
|
-
view.setUint32(16, this.
|
|
341
|
+
view.setUint32(16, this.calculatePlaybackDuration()); // Use playback duration for track header
|
|
323
342
|
view.setUint16(32, 0);
|
|
324
343
|
view.setUint32(36, 0x00010000);
|
|
325
344
|
view.setUint32(52, 0x00010000);
|
|
@@ -494,15 +513,19 @@ export class MP4Muxer {
|
|
|
494
513
|
if (this.parser.audioPts.length === 0) return null;
|
|
495
514
|
|
|
496
515
|
const firstAudioPts = this.parser.audioPts[0];
|
|
516
|
+
|
|
517
|
+
// When clipping with preroll, audio is normalized to start at PTS 0
|
|
518
|
+
// (matching video playback start after edit list), so no edit list needed
|
|
497
519
|
if (firstAudioPts === 0) return null;
|
|
498
520
|
|
|
521
|
+
// For non-clipped content, handle any timestamp offset
|
|
499
522
|
const mediaTime = Math.round(firstAudioPts * this.audioTimescale / 90000);
|
|
500
|
-
const
|
|
523
|
+
const audioDuration = this.audioSampleSizes.length * this.audioSampleDuration;
|
|
501
524
|
|
|
502
525
|
const elstData = new Uint8Array(16);
|
|
503
526
|
const view = new DataView(elstData.buffer);
|
|
504
527
|
view.setUint32(0, 1);
|
|
505
|
-
view.setUint32(4, Math.round(
|
|
528
|
+
view.setUint32(4, Math.round(audioDuration * this.videoTimescale / this.audioTimescale));
|
|
506
529
|
view.setInt32(8, mediaTime);
|
|
507
530
|
view.setUint16(12, 1);
|
|
508
531
|
view.setUint16(14, 0);
|
|
@@ -515,8 +538,8 @@ export class MP4Muxer {
|
|
|
515
538
|
const data = new Uint8Array(80);
|
|
516
539
|
const view = new DataView(data.buffer);
|
|
517
540
|
view.setUint32(8, 257);
|
|
518
|
-
|
|
519
|
-
view.setUint32(16,
|
|
541
|
+
// Use playback duration to match video track (for proper sync with preroll)
|
|
542
|
+
view.setUint32(16, this.calculatePlaybackDuration());
|
|
520
543
|
view.setUint16(32, 0x0100);
|
|
521
544
|
view.setUint32(36, 0x00010000); view.setUint32(52, 0x00010000); view.setUint32(68, 0x40000000);
|
|
522
545
|
return createFullBox('tkhd', 0, 3, data);
|
package/src/muxers/mpegts.js
CHANGED
package/src/ts-to-mp4.js
CHANGED
|
@@ -38,7 +38,9 @@ function isKeyframe(accessUnit) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* Clip access units to a time range, snapping to keyframes
|
|
41
|
+
* Clip access units to a time range, snapping to keyframes for decode
|
|
42
|
+
* but using edit list for precise playback timing
|
|
43
|
+
*
|
|
42
44
|
* @param {Array} videoAUs - Video access units
|
|
43
45
|
* @param {Array} audioAUs - Audio access units
|
|
44
46
|
* @param {number} startTime - Start time in seconds
|
|
@@ -50,48 +52,73 @@ function clipAccessUnits(videoAUs, audioAUs, startTime, endTime) {
|
|
|
50
52
|
const startPts = startTime * PTS_PER_SECOND;
|
|
51
53
|
const endPts = endTime * PTS_PER_SECOND;
|
|
52
54
|
|
|
53
|
-
// Find keyframe at or before startTime
|
|
54
|
-
let
|
|
55
|
+
// Find keyframe at or before startTime (needed for decoding)
|
|
56
|
+
let keyframeIdx = 0;
|
|
55
57
|
for (let i = 0; i < videoAUs.length; i++) {
|
|
56
58
|
if (videoAUs[i].pts > startPts) break;
|
|
57
|
-
if (isKeyframe(videoAUs[i]))
|
|
59
|
+
if (isKeyframe(videoAUs[i])) keyframeIdx = i;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
// Find first frame after endTime
|
|
62
|
+
// Find first frame at or after endTime
|
|
61
63
|
let endIdx = videoAUs.length;
|
|
62
|
-
for (let i =
|
|
64
|
+
for (let i = keyframeIdx; i < videoAUs.length; i++) {
|
|
63
65
|
if (videoAUs[i].pts >= endPts) {
|
|
64
66
|
endIdx = i;
|
|
65
67
|
break;
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
// Clip video
|
|
70
|
-
const clippedVideo = videoAUs.slice(
|
|
71
|
+
// Clip video starting from keyframe (for proper decoding)
|
|
72
|
+
const clippedVideo = videoAUs.slice(keyframeIdx, endIdx);
|
|
73
|
+
|
|
74
|
+
if (clippedVideo.length === 0) {
|
|
75
|
+
return {
|
|
76
|
+
video: [],
|
|
77
|
+
audio: [],
|
|
78
|
+
actualStartTime: startTime,
|
|
79
|
+
actualEndTime: endTime,
|
|
80
|
+
offset: 0,
|
|
81
|
+
preroll: 0
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Get PTS of keyframe and requested start
|
|
86
|
+
const keyframePts = clippedVideo[0].pts;
|
|
87
|
+
const lastFramePts = clippedVideo[clippedVideo.length - 1].pts;
|
|
71
88
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
const
|
|
89
|
+
// Pre-roll: time between keyframe and requested start
|
|
90
|
+
// This is the time the decoder needs to process but player shouldn't display
|
|
91
|
+
const prerollPts = Math.max(0, startPts - keyframePts);
|
|
75
92
|
|
|
76
|
-
// Clip audio to
|
|
77
|
-
|
|
93
|
+
// Clip audio to the REQUESTED time range (not from keyframe)
|
|
94
|
+
// Audio doesn't need keyframe pre-roll
|
|
95
|
+
const audioStartPts = startPts;
|
|
96
|
+
const audioEndPts = Math.min(endPts, lastFramePts + 90000); // Include audio slightly past last video
|
|
97
|
+
const clippedAudio = audioAUs.filter(au => au.pts >= audioStartPts && au.pts < audioEndPts);
|
|
78
98
|
|
|
79
|
-
// Normalize timestamps so
|
|
80
|
-
const offset =
|
|
99
|
+
// Normalize video timestamps so keyframe starts at 0
|
|
100
|
+
const offset = keyframePts;
|
|
81
101
|
for (const au of clippedVideo) {
|
|
82
102
|
au.pts -= offset;
|
|
83
103
|
au.dts -= offset;
|
|
84
104
|
}
|
|
105
|
+
|
|
106
|
+
// Normalize audio timestamps so it starts at 0 (matching video playback start after preroll)
|
|
107
|
+
// Audio doesn't have preroll, so it should start at PTS 0 to sync with video after edit list
|
|
108
|
+
const audioOffset = audioStartPts; // Use requested start, not keyframe
|
|
85
109
|
for (const au of clippedAudio) {
|
|
86
|
-
au.pts -=
|
|
110
|
+
au.pts -= audioOffset;
|
|
87
111
|
}
|
|
88
112
|
|
|
89
113
|
return {
|
|
90
114
|
video: clippedVideo,
|
|
91
115
|
audio: clippedAudio,
|
|
92
|
-
actualStartTime:
|
|
93
|
-
actualEndTime:
|
|
94
|
-
|
|
116
|
+
actualStartTime: keyframePts / PTS_PER_SECOND, // Where decode starts (keyframe)
|
|
117
|
+
actualEndTime: lastFramePts / PTS_PER_SECOND,
|
|
118
|
+
requestedStartTime: startTime, // Where playback should start
|
|
119
|
+
requestedEndTime: endTime,
|
|
120
|
+
offset,
|
|
121
|
+
preroll: prerollPts // Edit list will use this to skip pre-roll frames during playback
|
|
95
122
|
};
|
|
96
123
|
}
|
|
97
124
|
|
|
@@ -225,6 +252,9 @@ export function convertTsToMp4(tsData, options = {}) {
|
|
|
225
252
|
|
|
226
253
|
log(`Processing...`, { phase: 'convert', percent: 70 });
|
|
227
254
|
|
|
255
|
+
// Track preroll for edit list (used for precise clipping)
|
|
256
|
+
let clipPreroll = 0;
|
|
257
|
+
|
|
228
258
|
// Apply time range clipping if specified
|
|
229
259
|
if (options.startTime !== undefined || options.endTime !== undefined) {
|
|
230
260
|
const startTime = options.startTime || 0;
|
|
@@ -239,17 +269,23 @@ export function convertTsToMp4(tsData, options = {}) {
|
|
|
239
269
|
|
|
240
270
|
parser.videoAccessUnits = clipResult.video;
|
|
241
271
|
parser.audioAccessUnits = clipResult.audio;
|
|
272
|
+
clipPreroll = clipResult.preroll;
|
|
242
273
|
|
|
243
274
|
// Update PTS arrays to match
|
|
244
275
|
parser.videoPts = clipResult.video.map(au => au.pts);
|
|
245
276
|
parser.videoDts = clipResult.video.map(au => au.dts);
|
|
246
277
|
parser.audioPts = clipResult.audio.map(au => au.pts);
|
|
247
278
|
|
|
248
|
-
|
|
279
|
+
const prerollMs = (clipPreroll / 90).toFixed(0);
|
|
280
|
+
const endTimeStr = clipResult.requestedEndTime === Infinity ? 'end' : clipResult.requestedEndTime.toFixed(2) + 's';
|
|
281
|
+
const clipDuration = clipResult.requestedEndTime === Infinity
|
|
282
|
+
? (clipResult.actualEndTime - clipResult.requestedStartTime).toFixed(2)
|
|
283
|
+
: (clipResult.requestedEndTime - clipResult.requestedStartTime).toFixed(2);
|
|
284
|
+
log(`Clipped: ${clipResult.requestedStartTime.toFixed(2)}s - ${endTimeStr} (${clipDuration}s, ${prerollMs}ms preroll)`, { phase: 'convert', percent: 80 });
|
|
249
285
|
}
|
|
250
286
|
|
|
251
287
|
log(`Building MP4...`, { phase: 'convert', percent: 85 });
|
|
252
|
-
const muxer = new MP4Muxer(parser);
|
|
288
|
+
const muxer = new MP4Muxer(parser, { preroll: clipPreroll });
|
|
253
289
|
const { width, height } = muxer.getVideoDimensions();
|
|
254
290
|
log(`Dimensions: ${width}x${height}`);
|
|
255
291
|
|