@h3l1os/mp4vault 2.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/MP4.ts ADDED
@@ -0,0 +1,363 @@
1
+ import { Atom } from './Atom.js';
2
+ import { Embed } from './Embed.js';
3
+ import { Pack } from './Pack.js';
4
+ import { MAX_INT32 } from './constants.js';
5
+ import { Readable } from './node/Readable.js';
6
+ import { Writable } from './node/Writable.js';
7
+ import debug from 'debug';
8
+ import type { IReadable, IWritable, FileRecord } from './types.js';
9
+
10
+ const log = debug('mp4vault');
11
+
12
+ export class MP4 {
13
+ private _analyzed = false;
14
+ _readable: IReadable | null = null;
15
+ private _embed: Embed | null = null;
16
+ private _key: Buffer | null = null;
17
+ private _password: string | null = null;
18
+ _atoms: Atom[] = [];
19
+ private _initialMdatStart: number | null = null;
20
+ private _initialEmbed: Embed | null = null;
21
+
22
+ getEmbedFiles(): FileRecord[] {
23
+ if (this._initialEmbed) {
24
+ return this._initialEmbed.getFilesToExtract();
25
+ }
26
+ return [];
27
+ }
28
+
29
+ setKey(key: Buffer): void {
30
+ this._key = key;
31
+ this._password = null;
32
+ }
33
+
34
+ setPassword(password: string): void {
35
+ this._password = password;
36
+ this._key = null;
37
+ }
38
+
39
+ async loadFile(params: { filename: string }): Promise<void> {
40
+ if (!params || !params.filename) {
41
+ throw new Error('filename is required');
42
+ }
43
+ this._readable = new Readable(params);
44
+ await this.analyzeFile();
45
+ await this._readable.close();
46
+ }
47
+
48
+ async embedFile(params: {
49
+ filename?: string;
50
+ file?: { name?: string };
51
+ meta?: Record<string, unknown>;
52
+ key?: Buffer | null;
53
+ password?: string | null;
54
+ }): Promise<void> {
55
+ if (!this._embed) {
56
+ this._embed = new Embed({ mp4: this, key: this._key, password: this._password });
57
+ }
58
+ await this._embed.addFile(params);
59
+ }
60
+
61
+ async getExpectedSize(): Promise<number> {
62
+ let expectedSize = 0;
63
+
64
+ const ftyp = this.findAtom('ftyp');
65
+ const mdat = this.findAtom('mdat');
66
+ const moov = this.findAtom('moov');
67
+
68
+ if (!ftyp || !mdat || !moov) {
69
+ throw new Error('ftyp, mdat and moov atoms required');
70
+ }
71
+
72
+ const extendOffset = this._embed ? (await this._embed.getExpectedSize()) : 0;
73
+ let mdatOffset = 0;
74
+ const mdatNewSize = mdat.size - mdat.header_size;
75
+
76
+ mdatOffset += ftyp.size;
77
+ expectedSize += ftyp.size;
78
+
79
+ const tempH = mdat.header_size;
80
+ const tempL = mdat.size;
81
+
82
+ if (mdatNewSize <= MAX_INT32) {
83
+ const freeAtom = new Atom({
84
+ name: 'free',
85
+ start: 0,
86
+ size: 8,
87
+ header_size: 8,
88
+ });
89
+ expectedSize += freeAtom.size;
90
+ mdat.size += extendOffset;
91
+ expectedSize += mdat.header_size;
92
+ mdatOffset += 8;
93
+ mdatOffset += 8;
94
+ } else {
95
+ mdat.size += extendOffset;
96
+ mdat.header_size = 16;
97
+ expectedSize += mdat.header_size;
98
+ mdatOffset += 16;
99
+ }
100
+
101
+ mdat.header_size = tempH;
102
+ mdat.size = tempL;
103
+
104
+ if (this._embed) {
105
+ expectedSize += await this._embed.getExpectedSize();
106
+ }
107
+
108
+ expectedSize += (mdat.size - mdat.header_size);
109
+
110
+ const shiftOffsets = extendOffset + (mdatOffset - this._initialMdatStart!);
111
+ await this.adjustSampleOffsets(shiftOffsets);
112
+
113
+ expectedSize += moov.size;
114
+
115
+ return expectedSize;
116
+ }
117
+
118
+ async adjustSampleOffsets(offset: number): Promise<void> {
119
+ const sampleAtoms = this.findAtoms(null, 'stco').concat(this.findAtoms(null, 'co64'));
120
+
121
+ log('adjusting sample offsets by', offset, 'stco co64 atoms count:', sampleAtoms.length);
122
+ for (const atom of sampleAtoms) {
123
+ const data = await this._readable!.getSlice(atom.start + atom.header_size, 8);
124
+ const unpacked = Pack.unpack('>II', data);
125
+ const verFlags = unpacked[0];
126
+ const count = unpacked[1];
127
+
128
+ const sampleOffsets: number[] = [];
129
+
130
+ if (atom.name === 'stco') {
131
+ for (let i = 0; i < count; i += 1024) {
132
+ const cToRead = Math.min(1024, count - i);
133
+ const readOffsets = Pack.unpack(
134
+ '>' + 'I'.repeat(cToRead),
135
+ await this._readable!.getSlice(atom.start + atom.header_size + 8 + i * 4, cToRead * 4),
136
+ );
137
+ sampleOffsets.push(...readOffsets);
138
+ }
139
+ } else if (atom.name === 'co64') {
140
+ for (let i = 0; i < count; i += 1024) {
141
+ const cToRead = Math.min(1024, count - i);
142
+ const readOffsets = Pack.unpack(
143
+ '>' + 'Q'.repeat(cToRead),
144
+ await this._readable!.getSlice(atom.start + atom.header_size + 8 + i * 8, cToRead * 8),
145
+ );
146
+ sampleOffsets.push(...readOffsets);
147
+ }
148
+ }
149
+
150
+ for (let i = 0; i < sampleOffsets.length; i++) {
151
+ sampleOffsets[i] = sampleOffsets[i] + offset;
152
+ if (atom.name === 'stco' && sampleOffsets[i] >= MAX_INT32) {
153
+ atom.name = 'co64';
154
+ }
155
+ }
156
+
157
+ if (atom.name === 'stco') {
158
+ atom.contents = Pack.pack('>II', [verFlags, count]).concat(
159
+ Pack.pack('>' + 'I'.repeat(count), sampleOffsets),
160
+ );
161
+ atom.size = atom.contents.length + 8;
162
+ } else {
163
+ atom.contents = Pack.pack('>II', [verFlags, count]).concat(
164
+ Pack.pack('>' + 'Q'.repeat(count), sampleOffsets),
165
+ );
166
+ atom.size = atom.contents.length + 8;
167
+ }
168
+
169
+ atom.readable = null;
170
+ }
171
+ }
172
+
173
+ async extractEmbedHeader(): Promise<void> {
174
+ const mdat = this.findAtom('mdat')!;
175
+ const offset = mdat.start + mdat.header_size;
176
+
177
+ this._initialEmbed = new Embed({ mp4: this });
178
+ await this._initialEmbed.restoreFromReadable(
179
+ this._readable!,
180
+ { key: this._key || undefined, password: this._password },
181
+ offset,
182
+ );
183
+ }
184
+
185
+ async extractFile(n: number, writable: IWritable | null = null): Promise<IWritable> {
186
+ const mdat = this.findAtom('mdat')!;
187
+ const offset = mdat.start + mdat.header_size;
188
+
189
+ return await this._initialEmbed!.restoreBinary(
190
+ this._readable!,
191
+ { key: this._key || undefined, password: this._password || undefined },
192
+ n,
193
+ offset,
194
+ writable,
195
+ );
196
+ }
197
+
198
+ async embed(writable?: IWritable): Promise<IWritable> {
199
+ const ftyp = this.findAtom('ftyp');
200
+ const mdat = this.findAtom('mdat');
201
+ const moov = this.findAtom('moov');
202
+
203
+ if (!ftyp || !mdat || !moov) {
204
+ throw new Error('ftyp, mdat and moov atoms required');
205
+ }
206
+
207
+ if (!writable) {
208
+ writable = new Writable();
209
+ }
210
+
211
+ const extendOffset = this._embed ? (await this._embed.getExpectedSize()) : 0;
212
+ let mdatOffset = 0;
213
+ const mdatNewSize = mdat.size - mdat.header_size;
214
+
215
+ await ftyp.write(writable);
216
+ mdatOffset += ftyp.size;
217
+
218
+ const tempH = mdat.header_size;
219
+ const tempL = mdat.size;
220
+
221
+ if (mdatNewSize <= MAX_INT32) {
222
+ const freeAtom = new Atom({
223
+ name: 'free',
224
+ start: 0,
225
+ size: 8,
226
+ header_size: 8,
227
+ });
228
+ await freeAtom.write(writable);
229
+ mdat.size += extendOffset;
230
+ await mdat.writeHeader(writable);
231
+ mdatOffset += 8;
232
+ mdatOffset += 8;
233
+ } else {
234
+ mdat.size += extendOffset;
235
+ mdat.header_size = 16;
236
+ await mdat.writeHeader(writable);
237
+ mdatOffset += 16;
238
+ }
239
+
240
+ log('writing mdat atom start', mdatOffset);
241
+
242
+ mdat.header_size = tempH;
243
+ mdat.size = tempL;
244
+
245
+ if (this._embed) {
246
+ await this._embed.writeTo(writable);
247
+ }
248
+
249
+ await mdat.writePayload(writable);
250
+
251
+ const shiftOffsets = extendOffset + (mdatOffset - this._initialMdatStart!);
252
+ await this.adjustSampleOffsets(shiftOffsets);
253
+
254
+ await moov.write(writable);
255
+
256
+ await this._readable!.close();
257
+ await writable.close();
258
+
259
+ return writable;
260
+ }
261
+
262
+ async analyzeFile(): Promise<Atom[]> {
263
+ this._atoms = [];
264
+
265
+ const size = await this._readable!.size();
266
+ await this.parseAtoms(0, size, null);
267
+
268
+ this._analyzed = true;
269
+
270
+ const mdat = this.findAtom('mdat')!;
271
+ this._initialMdatStart = mdat.start + mdat.header_size;
272
+ log('initial mdat atom start', this._initialMdatStart);
273
+
274
+ try {
275
+ await this.extractEmbedHeader();
276
+ } catch {
277
+ // no embedded data
278
+ }
279
+
280
+ return this._atoms;
281
+ }
282
+
283
+ printAtoms(atoms?: Atom[], level = 0): void {
284
+ atoms = atoms || this._atoms;
285
+ for (const a of atoms) {
286
+ console.log(a.start, ''.padStart(level, '-'), a.name, a.size, a.header_size);
287
+ if (a.children.length) {
288
+ this.printAtoms(a.children, level + 1);
289
+ }
290
+ }
291
+ }
292
+
293
+ findAtom(name: string): Atom | null {
294
+ if (!this._analyzed) {
295
+ throw new Error('Run await analyzeFile() first');
296
+ }
297
+ const atoms = this.findAtoms(null, name);
298
+ return atoms.length ? atoms[0] : null;
299
+ }
300
+
301
+ findAtoms(atoms: Atom[] | null, name: string): Atom[] {
302
+ if (!this._analyzed) {
303
+ throw new Error('Run await analyzeFile() first');
304
+ }
305
+
306
+ atoms = atoms || this._atoms;
307
+
308
+ let ret: Atom[] = [];
309
+ for (const a of atoms) {
310
+ if (a.name === name) {
311
+ ret.push(a);
312
+ }
313
+ if (a.children.length) {
314
+ ret = ret.concat(this.findAtoms(a.children, name));
315
+ }
316
+ }
317
+
318
+ return ret;
319
+ }
320
+
321
+ async parseAtoms(start: number, end: number, mother: Atom | null): Promise<Atom[]> {
322
+ let offset = start;
323
+
324
+ while (offset < end) {
325
+ let atomSize = Pack.unpack('>I', await this._readable!.getSlice(offset, 4))[0];
326
+ const atomType = Pack.unpack('>4s', await this._readable!.getSlice(offset + 4, 4))[0];
327
+ let atomHeaderSize: number;
328
+
329
+ if (atomSize === 1) {
330
+ atomSize = Pack.unpack('>Q', await this._readable!.getSlice(offset + 8, 8))[0];
331
+ atomHeaderSize = 16;
332
+ } else {
333
+ atomHeaderSize = 8;
334
+ if (atomSize === 0) {
335
+ atomSize = end - offset;
336
+ }
337
+ }
338
+
339
+ const atom = new Atom({
340
+ readable: this._readable!,
341
+ name: String(atomType),
342
+ start: offset,
343
+ size: atomSize,
344
+ header_size: atomHeaderSize,
345
+ mother: mother,
346
+ });
347
+
348
+ if (mother) {
349
+ mother.children.push(atom);
350
+ } else {
351
+ this._atoms.push(atom);
352
+ }
353
+
354
+ if (['moov', 'trak', 'mdia', 'minf', 'stbl', 'edts', 'udta'].includes(String(atomType))) {
355
+ await this.parseAtoms(offset + atomHeaderSize, offset + atomSize, atom);
356
+ }
357
+
358
+ offset = offset + atomSize;
359
+ }
360
+
361
+ return this._atoms;
362
+ }
363
+ }
package/src/Pack.ts ADDED
@@ -0,0 +1,11 @@
1
+ import jspack from './jspack.js';
2
+
3
+ export class Pack {
4
+ static pack(format: string, values: unknown[]): number[] {
5
+ return jspack.Pack(format, values);
6
+ }
7
+
8
+ static unpack(format: string, buffer: Uint8Array | number[]): number[] {
9
+ return jspack.Unpack(format, buffer, 0);
10
+ }
11
+ }
@@ -0,0 +1,3 @@
1
+ export const BUFFER_SIZE = 100000;
2
+ export const MAX_HEADER_SIZE = 10485760; // 10MB
3
+ export const MAX_INT32 = 4294967295;
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { MP4 } from './MP4.js';
2
+ export { AES } from './AES.js';
3
+ export { Convert } from './Convert.js';
4
+ export { Atom } from './Atom.js';
5
+ export { Embed } from './Embed.js';
6
+ export { EmbedBinary } from './EmbedBinary.js';
7
+ export { EmbedObject } from './EmbedObject.js';
8
+ export { Pack } from './Pack.js';
9
+ export { Readable } from './node/Readable.js';
10
+ export { Writable } from './node/Writable.js';
11
+ export type { IReadable, IWritable, FileRecord } from './types.js';
@@ -0,0 +1,10 @@
1
+ interface JSPack {
2
+ Pack(fmt: string, values: unknown[]): number[];
3
+ Unpack(fmt: string, values: Uint8Array | number[], offset?: number): number[];
4
+ CalcLength(fmt: string): number;
5
+ PackTo(fmt: string, a: number[], p: number, values: unknown[], allowLessData?: boolean): number[] | false;
6
+ UnpackTo(fmt: string, a: Uint8Array | number[], p: number, allowLessData?: boolean): number[] | undefined;
7
+ }
8
+
9
+ declare const jspack: JSPack;
10
+ export = jspack;