@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/LICENSE +22 -0
- package/README.md +95 -0
- package/dist/tomp4.js +1617 -0
- package/package.json +43 -0
- package/src/fmp4-to-mp4.js +375 -0
- package/src/hls.js +280 -0
- package/src/index.js +311 -0
- package/src/ts-to-mp4.js +1154 -0
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;
|