@invintusmedia/tomp4 1.0.7 → 1.0.9

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.7
2
+ * toMp4.js v1.0.9
3
3
  * Convert MPEG-TS and fMP4 to standard MP4
4
4
  * https://github.com/TVWIT/toMp4.js
5
5
  * MIT License
@@ -114,17 +114,21 @@
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
- const audioEndPts = Math.min(endPts, lastFramePts);
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
- // Normalize all timestamps so keyframe starts at 0
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
+
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
126
130
  for (const au of clippedAudio) {
127
- au.pts -= offset;
131
+ au.pts -= audioOffset;
128
132
  }
129
133
 
130
134
  return {
@@ -752,7 +756,7 @@
752
756
  toMp4.isMpegTs = isMpegTs;
753
757
  toMp4.isFmp4 = isFmp4;
754
758
  toMp4.isStandardMp4 = isStandardMp4;
755
- toMp4.version = '1.0.7';
759
+ toMp4.version = '1.0.9';
756
760
 
757
761
  return toMp4;
758
762
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invintusmedia/tomp4",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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,8 +20,11 @@
20
20
  "scripts": {
21
21
  "build": "node build.js",
22
22
  "dev": "npx serve . -p 3000",
23
- "test": "node tests/clip.test.js",
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",
23
+ "test": "npm run test:clip && npm run test:mp4",
24
+ "test:clip": "node tests/clip.test.js",
25
+ "test:mp4": "node tests/mp4-parser.test.js",
26
+ "test:all": "npm run test",
27
+ "release": "npm test && 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",
25
28
  "release:patch": "npm version patch --no-git-tag-version && npm run release",
26
29
  "release:minor": "npm version minor --no-git-tag-version && npm run release",
27
30
  "release:major": "npm version major --no-git-tag-version && npm run release",
package/src/index.js CHANGED
@@ -37,6 +37,8 @@ import { transcode, isWebCodecsSupported } from './transcode.js';
37
37
  import { TSMuxer } from './muxers/mpegts.js';
38
38
  import { MP4Muxer } from './muxers/mp4.js';
39
39
  import { TSParser } from './parsers/mpegts.js';
40
+ import { MP4Parser } from './parsers/mp4.js';
41
+ import { RemoteMp4 } from './remote/index.js';
40
42
 
41
43
  /**
42
44
  * Result object returned by toMp4()
@@ -315,8 +317,15 @@ toMp4.analyze = analyzeTsData;
315
317
  toMp4.transcode = transcode;
316
318
  toMp4.isWebCodecsSupported = isWebCodecsSupported;
317
319
 
320
+ // Parsers
321
+ toMp4.MP4Parser = MP4Parser;
322
+ toMp4.TSParser = TSParser;
323
+
324
+ // Remote MP4 (on-demand HLS from remote MP4)
325
+ toMp4.RemoteMp4 = RemoteMp4;
326
+
318
327
  // Version (injected at build time for dist, read from package.json for ESM)
319
- toMp4.version = '1.0.7';
328
+ toMp4.version = '1.0.9';
320
329
 
321
330
  // Export
322
331
  export {
@@ -336,9 +345,14 @@ export {
336
345
  HlsVariant,
337
346
  // Transcoding (browser-only)
338
347
  transcode,
348
+ isWebCodecsSupported,
349
+ // Muxers
339
350
  TSMuxer,
340
351
  MP4Muxer,
352
+ // Parsers
341
353
  TSParser,
342
- isWebCodecsSupported
354
+ MP4Parser,
355
+ // Remote MP4 (on-demand HLS)
356
+ RemoteMp4
343
357
  };
344
358
  export default toMp4;
package/src/muxers/mp4.js CHANGED
@@ -513,15 +513,19 @@ export class MP4Muxer {
513
513
  if (this.parser.audioPts.length === 0) return null;
514
514
 
515
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
516
519
  if (firstAudioPts === 0) return null;
517
520
 
521
+ // For non-clipped content, handle any timestamp offset
518
522
  const mediaTime = Math.round(firstAudioPts * this.audioTimescale / 90000);
519
- const duration = this.audioSampleSizes.length * this.audioSampleDuration;
523
+ const audioDuration = this.audioSampleSizes.length * this.audioSampleDuration;
520
524
 
521
525
  const elstData = new Uint8Array(16);
522
526
  const view = new DataView(elstData.buffer);
523
527
  view.setUint32(0, 1);
524
- view.setUint32(4, Math.round(duration * this.videoTimescale / this.audioTimescale));
528
+ view.setUint32(4, Math.round(audioDuration * this.videoTimescale / this.audioTimescale));
525
529
  view.setInt32(8, mediaTime);
526
530
  view.setUint16(12, 1);
527
531
  view.setUint16(14, 0);
@@ -534,8 +538,8 @@ export class MP4Muxer {
534
538
  const data = new Uint8Array(80);
535
539
  const view = new DataView(data.buffer);
536
540
  view.setUint32(8, 257);
537
- const audioDuration = this.audioSampleSizes.length * this.audioSampleDuration;
538
- view.setUint32(16, Math.round(audioDuration * this.videoTimescale / this.audioTimescale));
541
+ // Use playback duration to match video track (for proper sync with preroll)
542
+ view.setUint32(16, this.calculatePlaybackDuration());
539
543
  view.setUint16(32, 0x0100);
540
544
  view.setUint32(36, 0x00010000); view.setUint32(52, 0x00010000); view.setUint32(68, 0x40000000);
541
545
  return createFullBox('tkhd', 0, 3, data);
@@ -85,8 +85,9 @@ export class TSMuxer {
85
85
  * @param {Uint8Array} avccData - AVCC-formatted NAL units
86
86
  * @param {boolean} isKey - Is this a keyframe
87
87
  * @param {number} pts90k - Presentation timestamp in 90kHz ticks
88
+ * @param {number} [dts90k] - Decode timestamp in 90kHz ticks (defaults to pts90k)
88
89
  */
89
- addVideoSample(avccData, isKey, pts90k) {
90
+ addVideoSample(avccData, isKey, pts90k, dts90k = pts90k) {
90
91
  const nalUnits = [];
91
92
 
92
93
  // Add AUD (Access Unit Delimiter) at start of each access unit
@@ -102,20 +103,28 @@ export class TSMuxer {
102
103
 
103
104
  // Parse AVCC NALs and convert to Annex B
104
105
  let offset = 0;
105
- while (offset < avccData.length - 4) {
106
+ let iterations = 0;
107
+ const maxIterations = 10000;
108
+
109
+ while (offset + 4 <= avccData.length && iterations < maxIterations) {
110
+ iterations++;
106
111
  const len = (avccData[offset] << 24) | (avccData[offset + 1] << 16) |
107
112
  (avccData[offset + 2] << 8) | avccData[offset + 3];
108
113
  offset += 4;
109
- if (len > 0 && offset + len <= avccData.length) {
110
- nalUnits.push(new Uint8Array([0, 0, 0, 1]));
111
- nalUnits.push(avccData.slice(offset, offset + len));
114
+
115
+ // Safety: bail on invalid NAL length
116
+ if (len <= 0 || len > avccData.length - offset) {
117
+ break;
112
118
  }
119
+
120
+ nalUnits.push(new Uint8Array([0, 0, 0, 1]));
121
+ nalUnits.push(avccData.slice(offset, offset + len));
113
122
  offset += len;
114
123
  }
115
124
 
116
- // Build PES packet
125
+ // Build PES packet with both PTS and DTS
117
126
  const annexB = concat(nalUnits);
118
- const pes = this._buildVideoPES(annexB, pts90k);
127
+ const pes = this._buildVideoPES(annexB, pts90k, dts90k);
119
128
 
120
129
  // Write PAT/PMT before keyframes
121
130
  if (isKey) {
@@ -130,8 +139,8 @@ export class TSMuxer {
130
139
  this._packetizePES(audioPes, 0x102, false, audio.pts, 'audio');
131
140
  }
132
141
 
133
- // Packetize video PES into 188-byte TS packets
134
- this._packetizePES(pes, 0x101, isKey, pts90k, 'video');
142
+ // Packetize video PES into 188-byte TS packets (use DTS for PCR)
143
+ this._packetizePES(pes, 0x101, isKey, dts90k, 'video');
135
144
  }
136
145
 
137
146
  /**
@@ -160,16 +169,32 @@ export class TSMuxer {
160
169
 
161
170
  // --- Private methods ---
162
171
 
163
- _buildVideoPES(payload, pts90k) {
164
- const pes = new Uint8Array(14 + payload.length);
172
+ _buildVideoPES(payload, pts90k, dts90k) {
173
+ // If PTS == DTS, only write PTS (saves 5 bytes per frame)
174
+ const hasDts = pts90k !== dts90k;
175
+ const headerLen = hasDts ? 10 : 5;
176
+ const pes = new Uint8Array(9 + headerLen + payload.length);
177
+
165
178
  pes[0] = 0; pes[1] = 0; pes[2] = 1; // Start code
166
179
  pes[3] = 0xE0; // Stream ID (video)
167
180
  pes[4] = 0; pes[5] = 0; // Length = 0 (unbounded)
168
- pes[6] = 0x80; // Flags
169
- pes[7] = 0x80; // PTS present
170
- pes[8] = 5; // Header length
171
- this._writePTS(pes, 9, pts90k, 0x21);
172
- pes.set(payload, 14);
181
+ pes[6] = 0x80; // Flags: data_alignment
182
+
183
+ if (hasDts) {
184
+ // PTS + DTS present
185
+ pes[7] = 0xC0; // PTS_DTS_flags = 11
186
+ pes[8] = 10; // Header length: 5 (PTS) + 5 (DTS)
187
+ this._writePTS(pes, 9, pts90k, 0x31); // PTS marker = 0011
188
+ this._writePTS(pes, 14, dts90k, 0x11); // DTS marker = 0001
189
+ pes.set(payload, 19);
190
+ } else {
191
+ // PTS only
192
+ pes[7] = 0x80; // PTS_DTS_flags = 10
193
+ pes[8] = 5; // Header length: 5 (PTS)
194
+ this._writePTS(pes, 9, pts90k, 0x21); // PTS marker = 0010
195
+ pes.set(payload, 14);
196
+ }
197
+
173
198
  return pes;
174
199
  }
175
200
 
@@ -220,14 +245,17 @@ export class TSMuxer {
220
245
 
221
246
  pkt[3] = 0x30 | (this.cc[cc] & 0x0F);
222
247
  pkt[4] = afLen;
223
- pkt[5] = 0x50; // PCR + random_access
248
+ pkt[5] = 0x50; // PCR flag + random_access_indicator
249
+
250
+ // PCR = 33-bit base (90kHz) + 6 reserved bits + 9-bit extension (27MHz)
251
+ // We only use the base, extension = 0
224
252
  const pcrBase = BigInt(pts90k);
225
253
  pkt[6] = Number((pcrBase >> 25n) & 0xFFn);
226
254
  pkt[7] = Number((pcrBase >> 17n) & 0xFFn);
227
255
  pkt[8] = Number((pcrBase >> 9n) & 0xFFn);
228
256
  pkt[9] = Number((pcrBase >> 1n) & 0xFFn);
229
- pkt[10] = (Number(pcrBase & 1n) << 7) | 0x7E;
230
- pkt[11] = 0;
257
+ pkt[10] = (Number(pcrBase & 1n) << 7) | 0x7E; // LSB of base + 6 reserved (111111)
258
+ pkt[11] = 0; // 9-bit extension = 0
231
259
 
232
260
  pkt.set(pes.slice(offset, offset + payloadLen), 12);
233
261
  offset += payloadLen;