@invintusmedia/tomp4 1.0.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/src/index.js ADDED
@@ -0,0 +1,311 @@
1
+ /**
2
+ * toMp4.js - Convert MPEG-TS, fMP4, and HLS to standard MP4
3
+ * Pure JavaScript, zero dependencies
4
+ *
5
+ * @example
6
+ * // From HLS URL (auto-selects highest quality)
7
+ * const mp4 = await toMp4('https://example.com/stream.m3u8');
8
+ * mp4.download('video.mp4');
9
+ *
10
+ * // From segment URL
11
+ * const mp4 = await toMp4('https://example.com/video.ts');
12
+ * video.src = mp4.toURL();
13
+ *
14
+ * // From raw data
15
+ * const mp4 = await toMp4(uint8ArrayData);
16
+ *
17
+ * // Advanced HLS: parse first, then select quality
18
+ * const hls = await toMp4.parseHls('https://example.com/stream.m3u8');
19
+ * console.log(hls.qualities); // Available qualities
20
+ * const mp4 = await toMp4(hls.select('720p'));
21
+ *
22
+ * ═══════════════════════════════════════════════════════════════
23
+ * SUPPORTED (remuxing only - no transcoding)
24
+ * ═══════════════════════════════════════════════════════════════
25
+ *
26
+ * Containers: MPEG-TS (.ts), fMP4 (.m4s), HLS (.m3u8)
27
+ * Video: H.264/AVC, H.265/HEVC
28
+ * Audio: AAC, AAC-LATM
29
+ *
30
+ * NOT SUPPORTED: MPEG-1/2 Video, MP3, AC-3 (require transcoding)
31
+ */
32
+
33
+ import { convertTsToMp4 } from './ts-to-mp4.js';
34
+ import { convertFmp4ToMp4 } from './fmp4-to-mp4.js';
35
+ import { parseHls, downloadHls, isHlsUrl, HlsStream, HlsVariant } from './hls.js';
36
+
37
+ /**
38
+ * Result object returned by toMp4()
39
+ * Provides convenient methods to use the converted MP4 data
40
+ */
41
+ class Mp4Result {
42
+ /**
43
+ * @param {Uint8Array} data - The MP4 data
44
+ * @param {string} [filename] - Optional suggested filename
45
+ */
46
+ constructor(data, filename = 'video.mp4') {
47
+ this.data = data;
48
+ this.filename = filename;
49
+ this._url = null;
50
+ this._blob = null;
51
+ }
52
+
53
+ /** Size in bytes */
54
+ get size() {
55
+ return this.data.byteLength;
56
+ }
57
+
58
+ /** Size as human-readable string */
59
+ get sizeFormatted() {
60
+ const mb = this.data.byteLength / 1024 / 1024;
61
+ return mb >= 1 ? `${mb.toFixed(2)} MB` : `${(this.data.byteLength / 1024).toFixed(1)} KB`;
62
+ }
63
+
64
+ /**
65
+ * Get as Blob
66
+ * @returns {Blob}
67
+ */
68
+ toBlob() {
69
+ if (!this._blob) {
70
+ this._blob = new Blob([this.data], { type: 'video/mp4' });
71
+ }
72
+ return this._blob;
73
+ }
74
+
75
+ /**
76
+ * Get as object URL (for video.src, etc.)
77
+ * @returns {string}
78
+ */
79
+ toURL() {
80
+ if (!this._url) {
81
+ this._url = URL.createObjectURL(this.toBlob());
82
+ }
83
+ return this._url;
84
+ }
85
+
86
+ /**
87
+ * Revoke the object URL to free memory
88
+ */
89
+ revokeURL() {
90
+ if (this._url) {
91
+ URL.revokeObjectURL(this._url);
92
+ this._url = null;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Download the MP4 file
98
+ * @param {string} [filename] - Override the default filename
99
+ */
100
+ download(filename) {
101
+ const a = document.createElement('a');
102
+ a.href = this.toURL();
103
+ a.download = filename || this.filename;
104
+ a.click();
105
+ }
106
+
107
+ /**
108
+ * Get as ArrayBuffer
109
+ * @returns {ArrayBuffer}
110
+ */
111
+ toArrayBuffer() {
112
+ return this.data.buffer.slice(this.data.byteOffset, this.data.byteOffset + this.data.byteLength);
113
+ }
114
+ }
115
+
116
+ // ============================================
117
+ // Format Detection
118
+ // ============================================
119
+
120
+ function isMpegTs(data) {
121
+ if (data.length < 4) return false;
122
+ if (data[0] === 0x47) return true;
123
+ for (let i = 0; i < Math.min(188, data.length); i++) {
124
+ if (data[i] === 0x47 && i + 188 < data.length && data[i + 188] === 0x47) return true;
125
+ }
126
+ return false;
127
+ }
128
+
129
+ function isFmp4(data) {
130
+ if (data.length < 8) return false;
131
+ const type = String.fromCharCode(data[4], data[5], data[6], data[7]);
132
+ return type === 'ftyp' || type === 'styp' || type === 'moof';
133
+ }
134
+
135
+ function isStandardMp4(data) {
136
+ if (data.length < 12) return false;
137
+ const type = String.fromCharCode(data[4], data[5], data[6], data[7]);
138
+ if (type !== 'ftyp') return false;
139
+ let offset = 0;
140
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
141
+ let hasMoov = false, hasMoof = false;
142
+ while (offset + 8 <= data.length) {
143
+ const size = view.getUint32(offset);
144
+ if (size < 8) break;
145
+ const boxType = String.fromCharCode(data[offset+4], data[offset+5], data[offset+6], data[offset+7]);
146
+ if (boxType === 'moov') hasMoov = true;
147
+ if (boxType === 'moof') hasMoof = true;
148
+ offset += size;
149
+ }
150
+ return hasMoov && !hasMoof;
151
+ }
152
+
153
+ function detectFormat(data) {
154
+ if (isMpegTs(data)) return 'mpegts';
155
+ if (isStandardMp4(data)) return 'mp4';
156
+ if (isFmp4(data)) return 'fmp4';
157
+ return 'unknown';
158
+ }
159
+
160
+ // ============================================
161
+ // Core Conversion
162
+ // ============================================
163
+
164
+ function convertData(data, options = {}) {
165
+ const uint8 = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
166
+ const format = detectFormat(uint8);
167
+
168
+ switch (format) {
169
+ case 'mpegts':
170
+ return convertTsToMp4(uint8, options);
171
+ case 'fmp4':
172
+ return convertFmp4ToMp4(uint8);
173
+ case 'mp4':
174
+ return uint8;
175
+ default:
176
+ throw new Error('Unrecognized video format. Expected MPEG-TS or fMP4.');
177
+ }
178
+ }
179
+
180
+ // ============================================
181
+ // Main API
182
+ // ============================================
183
+
184
+ /**
185
+ * Convert video to MP4
186
+ *
187
+ * @param {string | Uint8Array | ArrayBuffer | Blob | HlsStream} input - URL, HLS stream, or video data
188
+ * @param {object} [options] - Options
189
+ * @param {function} [options.onProgress] - Progress callback
190
+ * @param {string} [options.filename] - Suggested filename for downloads
191
+ * @param {string|number} [options.quality] - HLS quality: 'highest', 'lowest', or bandwidth
192
+ * @param {number} [options.maxSegments] - Max HLS segments to download (default: all)
193
+ * @returns {Promise<Mp4Result>} - Result object with download(), toURL(), etc.
194
+ *
195
+ * @example
196
+ * // From HLS URL (auto-selects highest quality)
197
+ * const mp4 = await toMp4('https://example.com/stream.m3u8');
198
+ * mp4.download('my-video.mp4');
199
+ *
200
+ * // From segment URL
201
+ * const mp4 = await toMp4('https://example.com/video.ts');
202
+ *
203
+ * // From data
204
+ * const mp4 = await toMp4(uint8Array);
205
+ * videoElement.src = mp4.toURL();
206
+ *
207
+ * // Advanced: select specific quality
208
+ * const hls = await toMp4.parseHls(url);
209
+ * const mp4 = await toMp4(hls.select('lowest'));
210
+ */
211
+ async function toMp4(input, options = {}) {
212
+ let data;
213
+ let filename = options.filename || 'video.mp4';
214
+ const log = options.onProgress || (() => {});
215
+
216
+ // Handle HlsStream object
217
+ if (input instanceof HlsStream) {
218
+ if (!options.filename) {
219
+ const urlPart = (input.masterUrl || '').split('/').pop()?.split('?')[0];
220
+ filename = urlPart ? urlPart.replace('.m3u8', '.mp4') : 'video.mp4';
221
+ }
222
+ data = await downloadHls(input, {
223
+ ...options,
224
+ quality: options.quality || 'highest'
225
+ });
226
+ }
227
+ // Handle URL strings
228
+ else if (typeof input === 'string') {
229
+ // Check if it's an HLS URL
230
+ if (isHlsUrl(input)) {
231
+ if (!options.filename) {
232
+ const urlPart = input.split('/').pop()?.split('?')[0];
233
+ filename = urlPart ? urlPart.replace('.m3u8', '.mp4') : 'video.mp4';
234
+ }
235
+ data = await downloadHls(input, {
236
+ ...options,
237
+ quality: options.quality || 'highest'
238
+ });
239
+ } else {
240
+ // Regular URL - fetch it directly
241
+ log('Fetching...');
242
+ const response = await fetch(input);
243
+ if (!response.ok) {
244
+ throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
245
+ }
246
+ data = new Uint8Array(await response.arrayBuffer());
247
+
248
+ if (!options.filename) {
249
+ const urlFilename = input.split('/').pop()?.split('?')[0];
250
+ if (urlFilename) {
251
+ filename = urlFilename.replace(/\.(ts|m4s)$/i, '') + '.mp4';
252
+ }
253
+ }
254
+ }
255
+ }
256
+ // Handle Blob
257
+ else if (input instanceof Blob) {
258
+ data = new Uint8Array(await input.arrayBuffer());
259
+ }
260
+ // Handle ArrayBuffer
261
+ else if (input instanceof ArrayBuffer) {
262
+ data = new Uint8Array(input);
263
+ }
264
+ // Handle Uint8Array
265
+ else if (input instanceof Uint8Array) {
266
+ data = input;
267
+ }
268
+ else {
269
+ throw new Error('Input must be a URL string, HlsStream, Uint8Array, ArrayBuffer, or Blob');
270
+ }
271
+
272
+ // Convert
273
+ log('Converting...');
274
+ const mp4Data = convertData(data, options);
275
+
276
+ return new Mp4Result(mp4Data, filename);
277
+ }
278
+
279
+ // Attach utilities to main function
280
+ toMp4.fromTs = (data, options) => new Mp4Result(convertTsToMp4(data instanceof ArrayBuffer ? new Uint8Array(data) : data, options));
281
+ toMp4.fromFmp4 = (data) => new Mp4Result(convertFmp4ToMp4(data instanceof ArrayBuffer ? new Uint8Array(data) : data));
282
+ toMp4.detectFormat = detectFormat;
283
+ toMp4.isMpegTs = isMpegTs;
284
+ toMp4.isFmp4 = isFmp4;
285
+ toMp4.isStandardMp4 = isStandardMp4;
286
+
287
+ // HLS utilities
288
+ toMp4.parseHls = parseHls;
289
+ toMp4.downloadHls = downloadHls;
290
+ toMp4.isHlsUrl = isHlsUrl;
291
+
292
+ // Version (injected at build time for dist, read from package.json for ESM)
293
+ toMp4.version = '1.0.0';
294
+
295
+ // Export
296
+ export {
297
+ toMp4,
298
+ Mp4Result,
299
+ convertTsToMp4,
300
+ convertFmp4ToMp4,
301
+ detectFormat,
302
+ isMpegTs,
303
+ isFmp4,
304
+ isStandardMp4,
305
+ parseHls,
306
+ downloadHls,
307
+ isHlsUrl,
308
+ HlsStream,
309
+ HlsVariant
310
+ };
311
+ export default toMp4;