@invintusmedia/tomp4 1.0.3 → 1.0.5

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 CHANGED
@@ -66,6 +66,22 @@ info.videoCodec // "H.264/AVC"
66
66
  info.audioCodec // "AAC"
67
67
  ```
68
68
 
69
+ ### progress callback
70
+
71
+ ```js
72
+ const mp4 = await toMp4(url, {
73
+ onProgress: (msg, info) => {
74
+ if (info?.percent !== undefined) {
75
+ console.log(`${info.percent}% - ${msg}`)
76
+ }
77
+ }
78
+ })
79
+ // 10% - Downloading: 10%
80
+ // 50% - Downloaded 5.2 MB
81
+ // 60% - Frames: 300 video, 450 audio
82
+ // 100% - Complete
83
+ ```
84
+
69
85
  ### use the result
70
86
 
71
87
  ```js
package/dist/tomp4.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * toMp4.js v1.0.3
2
+ * toMp4.js v1.0.5
3
3
  * Convert MPEG-TS and fMP4 to standard MP4
4
4
  * https://github.com/TVWIT/toMp4.js
5
5
  * MIT License
@@ -1214,6 +1214,7 @@
1214
1214
  function convertTsToMp4(tsData, options = {}) {
1215
1215
  const log = options.onProgress || (() => {});
1216
1216
 
1217
+ log(`Parsing...`, { phase: 'convert', percent: 52 });
1217
1218
  const parser = new TSParser();
1218
1219
  parser.parse(tsData);
1219
1220
  parser.finalize();
@@ -1223,7 +1224,7 @@
1223
1224
  const audioInfo = getCodecInfo(parser.audioStreamType);
1224
1225
 
1225
1226
  // Log parsing results
1226
- log(`Parsed ${debug.packets} TS packets`);
1227
+ log(`Parsed ${debug.packets} TS packets`, { phase: 'convert', percent: 55 });
1227
1228
  log(`PAT: ${debug.patFound ? '✓' : '✗'}, PMT: ${debug.pmtFound ? '✓' : '✗'}`);
1228
1229
  log(`Video: ${parser.videoPid ? `PID ${parser.videoPid}` : 'none'} → ${videoInfo.name}`);
1229
1230
  const audioDetails = [];
@@ -1266,7 +1267,7 @@
1266
1267
  );
1267
1268
  }
1268
1269
 
1269
- log(`Frames: ${parser.videoAccessUnits.length} video, ${parser.audioAccessUnits.length} audio`);
1270
+ log(`Frames: ${parser.videoAccessUnits.length} video, ${parser.audioAccessUnits.length} audio`, { phase: 'convert', percent: 60 });
1270
1271
  if (debug.audioPesStarts) {
1271
1272
  log(`Audio: ${debug.audioPesStarts} PES starts → ${debug.audioPesCount || 0} processed → ${debug.audioFramesInPes || 0} ADTS frames${debug.audioSkipped ? ` (${debug.audioSkipped} skipped)` : ''}`);
1272
1273
  }
@@ -1281,6 +1282,8 @@
1281
1282
  log(`Timestamps normalized: -${offsetMs}ms offset`);
1282
1283
  }
1283
1284
 
1285
+ log(`Processing...`, { phase: 'convert', percent: 70 });
1286
+
1284
1287
  // Apply time range clipping if specified
1285
1288
  if (options.startTime !== undefined || options.endTime !== undefined) {
1286
1289
  const startTime = options.startTime || 0;
@@ -1301,14 +1304,17 @@
1301
1304
  parser.videoDts = clipResult.video.map(au => au.dts);
1302
1305
  parser.audioPts = clipResult.audio.map(au => au.pts);
1303
1306
 
1304
- log(`Clipped: ${clipResult.actualStartTime.toFixed(2)}s - ${clipResult.actualEndTime.toFixed(2)}s (${clipResult.video.length} video, ${clipResult.audio.length} audio frames)`);
1307
+ log(`Clipped: ${clipResult.actualStartTime.toFixed(2)}s - ${clipResult.actualEndTime.toFixed(2)}s (${clipResult.video.length} video, ${clipResult.audio.length} audio frames)`, { phase: 'convert', percent: 80 });
1305
1308
  }
1306
1309
 
1310
+ log(`Building MP4...`, { phase: 'convert', percent: 85 });
1307
1311
  const builder = new MP4Builder(parser);
1308
1312
  const { width, height } = builder.getVideoDimensions();
1309
1313
  log(`Dimensions: ${width}x${height}`);
1310
1314
 
1311
- return builder.build();
1315
+ const result = builder.build();
1316
+ log(`Complete`, { phase: 'convert', percent: 100 });
1317
+ return result;
1312
1318
  }
1313
1319
 
1314
1320
  default convertTsToMp4;
@@ -1751,7 +1757,7 @@
1751
1757
  toMp4.isMpegTs = isMpegTs;
1752
1758
  toMp4.isFmp4 = isFmp4;
1753
1759
  toMp4.isStandardMp4 = isStandardMp4;
1754
- toMp4.version = '1.0.3';
1760
+ toMp4.version = '1.0.5';
1755
1761
 
1756
1762
  return toMp4;
1757
1763
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@invintusmedia/tomp4",
3
- "version": "1.0.3",
4
- "description": "Convert MPEG-TS and fMP4 streams to standard MP4 - pure JavaScript, zero dependencies",
3
+ "version": "1.0.5",
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",
7
7
  "types": "src/index.d.ts",
@@ -39,7 +39,10 @@
39
39
  "converter",
40
40
  "transmux",
41
41
  "hls",
42
- "streaming"
42
+ "streaming",
43
+ "clip",
44
+ "trim",
45
+ "cut"
43
46
  ],
44
47
  "author": "Invintus Media",
45
48
  "license": "MIT",
package/src/hls.js CHANGED
@@ -280,9 +280,11 @@ async function downloadHls(source, options = {}) {
280
280
  toDownload = toDownload.slice(0, options.maxSegments);
281
281
  }
282
282
 
283
- log(`Downloading ${toDownload.length} segment${toDownload.length > 1 ? 's' : ''}...`);
283
+ const totalSegments = toDownload.length;
284
+ log(`Downloading ${totalSegments} segment${totalSegments > 1 ? 's' : ''}...`);
284
285
 
285
- // Download all segments in parallel
286
+ // Download segments with progress tracking
287
+ let completedSegments = 0;
286
288
  const buffers = await Promise.all(
287
289
  toDownload.map(async (seg, i) => {
288
290
  const url = seg.url || seg; // Handle both HlsSegment objects and plain URLs
@@ -290,7 +292,11 @@ async function downloadHls(source, options = {}) {
290
292
  if (!resp.ok) {
291
293
  throw new Error(`Segment ${i + 1} failed: ${resp.status}`);
292
294
  }
293
- return new Uint8Array(await resp.arrayBuffer());
295
+ const buffer = new Uint8Array(await resp.arrayBuffer());
296
+ completedSegments++;
297
+ const percent = Math.round((completedSegments / totalSegments) * 50); // Download is 0-50%
298
+ log(`Downloading: ${percent}%`, { phase: 'download', percent, segment: completedSegments, totalSegments });
299
+ return buffer;
294
300
  })
295
301
  );
296
302
 
@@ -303,7 +309,7 @@ async function downloadHls(source, options = {}) {
303
309
  offset += buf.length;
304
310
  }
305
311
 
306
- log(`Downloaded ${(totalSize / 1024 / 1024).toFixed(2)} MB`);
312
+ log(`Downloaded ${(totalSize / 1024 / 1024).toFixed(2)} MB`, { phase: 'download', percent: 50 });
307
313
 
308
314
  // Return with metadata for precise clipping
309
315
  combined._hlsTimeRange = hasTimeRange ? {
package/src/index.d.ts CHANGED
@@ -37,9 +37,20 @@ declare module '@invintusmedia/tomp4' {
37
37
  segments: string[];
38
38
  }
39
39
 
40
+ export interface ProgressInfo {
41
+ /** Current phase: 'download' or 'convert' */
42
+ phase: 'download' | 'convert';
43
+ /** Progress percentage (0-100) */
44
+ percent: number;
45
+ /** Current segment (download phase only) */
46
+ segment?: number;
47
+ /** Total segments (download phase only) */
48
+ totalSegments?: number;
49
+ }
50
+
40
51
  export interface ToMp4Options {
41
- /** Progress callback */
42
- onProgress?: (message: string) => void;
52
+ /** Progress callback - receives message string and optional progress info */
53
+ onProgress?: (message: string, info?: ProgressInfo) => void;
43
54
  /** Suggested filename for downloads */
44
55
  filename?: string;
45
56
  /** HLS quality: 'highest', 'lowest', or bandwidth number */
package/src/index.js CHANGED
@@ -269,9 +269,24 @@ async function toMp4(input, options = {}) {
269
269
  throw new Error('Input must be a URL string, HlsStream, Uint8Array, ArrayBuffer, or Blob');
270
270
  }
271
271
 
272
+ // Adjust clip times if we downloaded HLS with a time range
273
+ // The downloaded segments have been normalized to start at 0,
274
+ // so we need to adjust the requested clip times accordingly
275
+ let convertOptions = { ...options };
276
+ if (data._hlsTimeRange && (options.startTime !== undefined || options.endTime !== undefined)) {
277
+ const segmentStart = data._hlsTimeRange.actualStart;
278
+ if (options.startTime !== undefined) {
279
+ convertOptions.startTime = Math.max(0, options.startTime - segmentStart);
280
+ }
281
+ if (options.endTime !== undefined) {
282
+ convertOptions.endTime = options.endTime - segmentStart;
283
+ }
284
+ log(`Adjusted clip: ${convertOptions.startTime?.toFixed(2) || 0}s - ${convertOptions.endTime?.toFixed(2) || '∞'}s (offset: -${segmentStart.toFixed(2)}s)`);
285
+ }
286
+
272
287
  // Convert
273
288
  log('Converting...');
274
- const mp4Data = convertData(data, options);
289
+ const mp4Data = convertData(data, convertOptions);
275
290
 
276
291
  return new Mp4Result(mp4Data, filename);
277
292
  }
@@ -293,7 +308,7 @@ toMp4.isHlsUrl = isHlsUrl;
293
308
  toMp4.analyze = analyzeTsData;
294
309
 
295
310
  // Version (injected at build time for dist, read from package.json for ESM)
296
- toMp4.version = '1.0.3';
311
+ toMp4.version = '1.0.5';
297
312
 
298
313
  // Export
299
314
  export {
package/src/ts-to-mp4.js CHANGED
@@ -1193,6 +1193,7 @@ export function analyzeTsData(tsData) {
1193
1193
  export function convertTsToMp4(tsData, options = {}) {
1194
1194
  const log = options.onProgress || (() => {});
1195
1195
 
1196
+ log(`Parsing...`, { phase: 'convert', percent: 52 });
1196
1197
  const parser = new TSParser();
1197
1198
  parser.parse(tsData);
1198
1199
  parser.finalize();
@@ -1202,7 +1203,7 @@ export function convertTsToMp4(tsData, options = {}) {
1202
1203
  const audioInfo = getCodecInfo(parser.audioStreamType);
1203
1204
 
1204
1205
  // Log parsing results
1205
- log(`Parsed ${debug.packets} TS packets`);
1206
+ log(`Parsed ${debug.packets} TS packets`, { phase: 'convert', percent: 55 });
1206
1207
  log(`PAT: ${debug.patFound ? '✓' : '✗'}, PMT: ${debug.pmtFound ? '✓' : '✗'}`);
1207
1208
  log(`Video: ${parser.videoPid ? `PID ${parser.videoPid}` : 'none'} → ${videoInfo.name}`);
1208
1209
  const audioDetails = [];
@@ -1245,7 +1246,7 @@ export function convertTsToMp4(tsData, options = {}) {
1245
1246
  );
1246
1247
  }
1247
1248
 
1248
- log(`Frames: ${parser.videoAccessUnits.length} video, ${parser.audioAccessUnits.length} audio`);
1249
+ log(`Frames: ${parser.videoAccessUnits.length} video, ${parser.audioAccessUnits.length} audio`, { phase: 'convert', percent: 60 });
1249
1250
  if (debug.audioPesStarts) {
1250
1251
  log(`Audio: ${debug.audioPesStarts} PES starts → ${debug.audioPesCount || 0} processed → ${debug.audioFramesInPes || 0} ADTS frames${debug.audioSkipped ? ` (${debug.audioSkipped} skipped)` : ''}`);
1251
1252
  }
@@ -1260,6 +1261,8 @@ export function convertTsToMp4(tsData, options = {}) {
1260
1261
  log(`Timestamps normalized: -${offsetMs}ms offset`);
1261
1262
  }
1262
1263
 
1264
+ log(`Processing...`, { phase: 'convert', percent: 70 });
1265
+
1263
1266
  // Apply time range clipping if specified
1264
1267
  if (options.startTime !== undefined || options.endTime !== undefined) {
1265
1268
  const startTime = options.startTime || 0;
@@ -1280,14 +1283,17 @@ export function convertTsToMp4(tsData, options = {}) {
1280
1283
  parser.videoDts = clipResult.video.map(au => au.dts);
1281
1284
  parser.audioPts = clipResult.audio.map(au => au.pts);
1282
1285
 
1283
- log(`Clipped: ${clipResult.actualStartTime.toFixed(2)}s - ${clipResult.actualEndTime.toFixed(2)}s (${clipResult.video.length} video, ${clipResult.audio.length} audio frames)`);
1286
+ log(`Clipped: ${clipResult.actualStartTime.toFixed(2)}s - ${clipResult.actualEndTime.toFixed(2)}s (${clipResult.video.length} video, ${clipResult.audio.length} audio frames)`, { phase: 'convert', percent: 80 });
1284
1287
  }
1285
1288
 
1289
+ log(`Building MP4...`, { phase: 'convert', percent: 85 });
1286
1290
  const builder = new MP4Builder(parser);
1287
1291
  const { width, height } = builder.getVideoDimensions();
1288
1292
  log(`Dimensions: ${width}x${height}`);
1289
1293
 
1290
- return builder.build();
1294
+ const result = builder.build();
1295
+ log(`Complete`, { phase: 'convert', percent: 100 });
1296
+ return result;
1291
1297
  }
1292
1298
 
1293
1299
  export default convertTsToMp4;