@aglyn/aglyn 1.0.0-beta.197 → 1.0.0-beta.199

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.
Files changed (80) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
  3. package/src/lib/app-utils/docs-help.generated.js +1 -0
  4. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  5. package/src/lib/app-utils/docs-index.generated.js +3 -3
  6. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  7. package/src/lib/app-utils/media-embedded-fields.d.ts +471 -0
  8. package/src/lib/app-utils/media-embedded-fields.js +619 -0
  9. package/src/lib/app-utils/media-embedded-fields.js.map +1 -0
  10. package/src/lib/app-utils/media-embedded-metadata/exif.d.ts +160 -0
  11. package/src/lib/app-utils/media-embedded-metadata/exif.js +1267 -0
  12. package/src/lib/app-utils/media-embedded-metadata/exif.js.map +1 -0
  13. package/src/lib/app-utils/media-embedded-metadata/gif.d.ts +44 -0
  14. package/src/lib/app-utils/media-embedded-metadata/gif.js +134 -0
  15. package/src/lib/app-utils/media-embedded-metadata/gif.js.map +1 -0
  16. package/src/lib/app-utils/media-embedded-metadata/heif.d.ts +23 -0
  17. package/src/lib/app-utils/media-embedded-metadata/heif.js +354 -0
  18. package/src/lib/app-utils/media-embedded-metadata/heif.js.map +1 -0
  19. package/src/lib/app-utils/media-embedded-metadata/icc.d.ts +22 -0
  20. package/src/lib/app-utils/media-embedded-metadata/icc.js +85 -0
  21. package/src/lib/app-utils/media-embedded-metadata/icc.js.map +1 -0
  22. package/src/lib/app-utils/media-embedded-metadata/image-blocks.d.ts +145 -0
  23. package/src/lib/app-utils/media-embedded-metadata/image-blocks.js +200 -0
  24. package/src/lib/app-utils/media-embedded-metadata/image-blocks.js.map +1 -0
  25. package/src/lib/app-utils/media-embedded-metadata/index.d.ts +93 -0
  26. package/src/lib/app-utils/media-embedded-metadata/index.js +744 -0
  27. package/src/lib/app-utils/media-embedded-metadata/index.js.map +1 -0
  28. package/src/lib/app-utils/media-embedded-metadata/iptc.d.ts +59 -0
  29. package/src/lib/app-utils/media-embedded-metadata/iptc.js +529 -0
  30. package/src/lib/app-utils/media-embedded-metadata/iptc.js.map +1 -0
  31. package/src/lib/app-utils/media-embedded-metadata/jpeg.d.ts +43 -0
  32. package/src/lib/app-utils/media-embedded-metadata/jpeg.js +456 -0
  33. package/src/lib/app-utils/media-embedded-metadata/jpeg.js.map +1 -0
  34. package/src/lib/app-utils/media-embedded-metadata/mp4.d.ts +37 -0
  35. package/src/lib/app-utils/media-embedded-metadata/mp4.js +621 -0
  36. package/src/lib/app-utils/media-embedded-metadata/mp4.js.map +1 -0
  37. package/src/lib/app-utils/media-embedded-metadata/ooxml.d.ts +131 -0
  38. package/src/lib/app-utils/media-embedded-metadata/ooxml.js +1058 -0
  39. package/src/lib/app-utils/media-embedded-metadata/ooxml.js.map +1 -0
  40. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.d.ts +223 -0
  41. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js +936 -0
  42. package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js.map +1 -0
  43. package/src/lib/app-utils/media-embedded-metadata/pdf.d.ts +82 -0
  44. package/src/lib/app-utils/media-embedded-metadata/pdf.js +1167 -0
  45. package/src/lib/app-utils/media-embedded-metadata/pdf.js.map +1 -0
  46. package/src/lib/app-utils/media-embedded-metadata/png.d.ts +41 -0
  47. package/src/lib/app-utils/media-embedded-metadata/png.js +512 -0
  48. package/src/lib/app-utils/media-embedded-metadata/png.js.map +1 -0
  49. package/src/lib/app-utils/media-embedded-metadata/svg.d.ts +30 -0
  50. package/src/lib/app-utils/media-embedded-metadata/svg.js +154 -0
  51. package/src/lib/app-utils/media-embedded-metadata/svg.js.map +1 -0
  52. package/src/lib/app-utils/media-embedded-metadata/tiff.d.ts +40 -0
  53. package/src/lib/app-utils/media-embedded-metadata/tiff.js +158 -0
  54. package/src/lib/app-utils/media-embedded-metadata/tiff.js.map +1 -0
  55. package/src/lib/app-utils/media-embedded-metadata/types.d.ts +60 -0
  56. package/src/lib/app-utils/media-embedded-metadata/types.js +32 -0
  57. package/src/lib/app-utils/media-embedded-metadata/types.js.map +1 -0
  58. package/src/lib/app-utils/media-embedded-metadata/webm.d.ts +26 -0
  59. package/src/lib/app-utils/media-embedded-metadata/webm.js +417 -0
  60. package/src/lib/app-utils/media-embedded-metadata/webm.js.map +1 -0
  61. package/src/lib/app-utils/media-embedded-metadata/webp.d.ts +37 -0
  62. package/src/lib/app-utils/media-embedded-metadata/webp.js +272 -0
  63. package/src/lib/app-utils/media-embedded-metadata/webp.js.map +1 -0
  64. package/src/lib/app-utils/media-embedded-metadata/xml.d.ts +231 -0
  65. package/src/lib/app-utils/media-embedded-metadata/xml.js +598 -0
  66. package/src/lib/app-utils/media-embedded-metadata/xml.js.map +1 -0
  67. package/src/lib/app-utils/media-embedded-metadata/xmp.d.ts +100 -0
  68. package/src/lib/app-utils/media-embedded-metadata/xmp.js +1475 -0
  69. package/src/lib/app-utils/media-embedded-metadata/xmp.js.map +1 -0
  70. package/src/lib/app-utils/media-embedded-metadata/zip.d.ts +93 -0
  71. package/src/lib/app-utils/media-embedded-metadata/zip.js +531 -0
  72. package/src/lib/app-utils/media-embedded-metadata/zip.js.map +1 -0
  73. package/src/lib/app-utils/name-search.d.ts +12 -0
  74. package/src/lib/app-utils/name-search.js +11 -0
  75. package/src/lib/app-utils/name-search.js.map +1 -1
  76. package/src/lib/foundation/definitions/platform.types.d.ts +11 -0
  77. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  78. package/src/server.d.ts +1 -0
  79. package/src/server.js +7 -0
  80. package/src/server.js.map +1 -1
@@ -0,0 +1,621 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ /**
17
+ * The tags of an MP4, M4V or QuickTime film (AGL-3331). Read-only: a
18
+ * change to `moov` moves every sample offset in the file, which is not an
19
+ * edit this module will make to a customer's video.
20
+ *
21
+ * ## Reading without downloading the film
22
+ *
23
+ * Everything this reads lives in the `moov` box, which may sit before or
24
+ * after the media data. The top level is walked one box HEADER at a time
25
+ * through the random-access reader — eight bytes, sixteen for a 64-bit size
26
+ * (ISO/IEC 14496-12 §4.2) — and `mdat` is skipped by its size, never read.
27
+ * Then `moov` is read whole, up to {@link MP4_MOOV_MAX_BYTES}.
28
+ *
29
+ * ## Where a film keeps its tags
30
+ *
31
+ * - `moov/udta/meta/ilst` — iTunes-style items (`©nam`, `©ART`, `desc`, …),
32
+ * each holding a `data` atom whose type indicator says how to decode it
33
+ * (QuickTime File Format, "Well-known types"). `meta` is a full box in
34
+ * ISO files and a plain one in QuickTime files; both are handled.
35
+ * - `moov/meta` with `keys` + `ilst` — the `mdta` handler Apple's cameras
36
+ * use (`com.apple.quicktime.title`, `.creationdate`, `.location.ISO6709`,
37
+ * …), where an item's type is a 1-based index into `keys`.
38
+ * - `moov/udta` ©-atoms — QuickTime user-data text (`©nam`, `©day`, `©xyz`,
39
+ * …): a 16-bit length and a 16-bit language code, then the text.
40
+ * - `moov/udta` 3GPP asset boxes (`titl`, `dscp`, `auth`, `perf`, `cprt`;
41
+ * 3GPP TS 26.244 §8) — what AVFoundation writes for an `.mp4`.
42
+ * - `mvhd` creation time, as the recording date of last resort.
43
+ * - XMP, in the `uuid` box `BE7ACFCB-97A9-42E8-9C71-999491E3AFAC` (XMP
44
+ * Specification Part 3 §1.2.7) at the top level or in `moov/udta`, or in
45
+ * a QuickTime `udta/XMP_` atom — returned for the XMP reader.
46
+ *
47
+ * When the same idea is written in several places, the first found in the
48
+ * order above wins, except the XMP (merged by the dispatcher) and `mvhd`
49
+ * (used only when no tag gave a date).
50
+ */ import { otherEmbeddedKey } from "../media-embedded-fields.js";
51
+ /** Above this, `moov` is not read and the film reads as having no tags. */ export const MP4_MOOV_MAX_BYTES = 32 * 1024 * 1024;
52
+ const XMP_MAX_BYTES = 8 * 1024 * 1024;
53
+ /** A fragmented film has thousands of top-level boxes; tags come early. */ const MAX_TOP_LEVEL_BOXES = 1024;
54
+ const MAX_CHILDREN = 4096;
55
+ const MAX_KEYS = 4096;
56
+ const XMP_UUID = [
57
+ 0xbe,
58
+ 0x7a,
59
+ 0xcf,
60
+ 0xcb,
61
+ 0x97,
62
+ 0xa9,
63
+ 0x42,
64
+ 0xe8,
65
+ 0x9c,
66
+ 0x71,
67
+ 0x99,
68
+ 0x94,
69
+ 0x91,
70
+ 0xe3,
71
+ 0xaf,
72
+ 0xac
73
+ ];
74
+ /** Seconds from 1904-01-01T00:00:00Z, the ISO BMFF epoch, to 1970's. */ const EPOCH_1904 = 2082844800;
75
+ const view = (bytes)=>new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
76
+ /** A four-character code, one Latin-1 character per byte (`©` is 0xA9). */ const fourcc = (bytes, at)=>String.fromCharCode(...bytes.subarray(at, at + 4));
77
+ /** A plausible box type: printable ASCII or `©`. */ const plausibleType = (type)=>type.length === 4 && /^[\x20-\x7e\xa9]{4}$/.test(type);
78
+ const utf8 = (bytes)=>new TextDecoder('utf-8').decode(bytes).replace(/\0+$/, '');
79
+ /** UTF-8 when the bytes are valid UTF-8, otherwise Mac OS Roman. */ function macText(bytes) {
80
+ try {
81
+ return new TextDecoder('utf-8', {
82
+ fatal: true
83
+ }).decode(bytes).replace(/\0+$/, '');
84
+ } catch (unused) {
85
+ try {
86
+ return new TextDecoder('macintosh').decode(bytes).replace(/\0+$/, '');
87
+ } catch (unused) {
88
+ return new TextDecoder('latin1').decode(bytes).replace(/\0+$/, '');
89
+ }
90
+ }
91
+ }
92
+ /**
93
+ * A string that may open with a UTF-16 byte-order mark (3GPP TS 26.244
94
+ * §8.1); a stray odd byte — half a terminator — is dropped.
95
+ */ function bomText(bytes) {
96
+ const even = bytes.subarray(2, 2 + (bytes.length - 2 & ~1));
97
+ if (bytes[0] === 0xfe && bytes[1] === 0xff) return new TextDecoder('utf-16be').decode(even).replace(/\0+$/, '');
98
+ if (bytes[0] === 0xff && bytes[1] === 0xfe) return new TextDecoder('utf-16le').decode(even).replace(/\0+$/, '');
99
+ return utf8(bytes);
100
+ }
101
+ /**
102
+ * The boxes inside `bytes[start, end)` (ISO/IEC 14496-12 §4.2). A box that
103
+ * claims to run past its parent is cut at the parent's end and ends the
104
+ * walk; a size smaller than its own header ends it too.
105
+ */ function children(bytes, start, end) {
106
+ const data = view(bytes);
107
+ const out = [];
108
+ let at = start;
109
+ while(at + 8 <= end && out.length < MAX_CHILDREN){
110
+ let size = data.getUint32(at);
111
+ let body = at + 8;
112
+ if (size === 1) {
113
+ if (at + 16 > end) break;
114
+ size = data.getUint32(at + 8) * 0x100000000 + data.getUint32(at + 12);
115
+ body = at + 16;
116
+ } else if (size === 0) {
117
+ size = end - at;
118
+ }
119
+ if (size < body - at) break;
120
+ const boxEnd = Math.min(end, at + size);
121
+ out.push({
122
+ type: fourcc(bytes, at + 4),
123
+ start: at,
124
+ body,
125
+ end: boxEnd
126
+ });
127
+ if (at + size > end) break;
128
+ at = boxEnd;
129
+ }
130
+ return out;
131
+ }
132
+ const child = (bytes, box, type)=>children(bytes, box.body, box.end).find((item)=>item.type === type);
133
+ /**
134
+ * Normalize a tag's date to the catalog's form. QuickTime writes the zone
135
+ * as `-0700`, iTunes as `Z`, some tools not at all; a year alone is kept.
136
+ */ function normalizeDate(raw) {
137
+ const value = raw.trim();
138
+ const match = /^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}(?::\d{2})?)(?:[.,](\d+))?\s*(Z|[+-]\d{2}(?::?\d{2})?)?$/.exec(value);
139
+ if (!match) return value;
140
+ const [, date, time, fraction, zone] = match;
141
+ let offset = zone != null ? zone : '';
142
+ if (offset && offset !== 'Z') {
143
+ const digits = offset.slice(1).replace(':', '');
144
+ offset = `${offset[0]}${digits.slice(0, 2)}:${digits.slice(2, 4) || '00'}`;
145
+ }
146
+ return `${date}T${time}${fraction ? `.${fraction.slice(0, 3)}` : ''}${offset}`;
147
+ }
148
+ /**
149
+ * An ISO 6709 point (`+37.7749-122.4194+010.000/`) as `"lat,lon"` with six
150
+ * places. Latitude has 2, 4 or 6 integer digits (degrees, degrees and
151
+ * minutes, or degrees minutes seconds); longitude 3, 5 or 7 (ISO 6709
152
+ * Annex H). Null for anything else or out of range.
153
+ */ export function parseIso6709(raw) {
154
+ const match = /^([+-])(\d+)(\.\d+)?([+-])(\d+)(\.\d+)?/.exec(raw.trim());
155
+ if (!match) return null;
156
+ const [, latSign, latInt, latFraction, lonSign, lonInt, lonFraction] = match;
157
+ const part = (digits, fraction, degreeDigits)=>{
158
+ const whole = digits.length - degreeDigits;
159
+ if (whole !== 0 && whole !== 2 && whole !== 4) return NaN;
160
+ const frac = Number(`0${fraction != null ? fraction : ''}`);
161
+ const degrees = Number(digits.slice(0, degreeDigits));
162
+ if (whole === 0) return degrees + frac;
163
+ const minutes = Number(digits.slice(degreeDigits, degreeDigits + 2));
164
+ if (whole === 2) return degrees + (minutes + frac) / 60;
165
+ const seconds = Number(digits.slice(degreeDigits + 2, degreeDigits + 4));
166
+ return degrees + minutes / 60 + (seconds + frac) / 3600;
167
+ };
168
+ const lat = part(latInt != null ? latInt : '', latFraction, 2) * (latSign === '-' ? -1 : 1);
169
+ const lon = part(lonInt != null ? lonInt : '', lonFraction, 3) * (lonSign === '-' ? -1 : 1);
170
+ if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
171
+ if (Math.abs(lat) > 90 || Math.abs(lon) > 180) return null;
172
+ const fixed = (value)=>(value === 0 ? 0 : value).toFixed(6);
173
+ return `${fixed(lat)},${fixed(lon)}`;
174
+ }
175
+ /** A big-endian integer of 1 to 8 bytes, as decimal text. */ function integerText(bytes, signed) {
176
+ var _bytes_;
177
+ if (bytes.length < 1 || bytes.length > 8) return null;
178
+ let value = 0n;
179
+ for (const byte of bytes)value = value << 8n | BigInt(byte);
180
+ if (signed && bytes.length && ((_bytes_ = bytes[0]) != null ? _bytes_ : 0) & 0x80) {
181
+ value -= 1n << BigInt(bytes.length * 8);
182
+ }
183
+ return value.toString();
184
+ }
185
+ /**
186
+ * A `data` atom's value (QuickTime File Format, "Well-known types"): text
187
+ * and numbers only. Images, binary and the "implicit" type 0 (track and
188
+ * disc numbers, genre ids) are not shown.
189
+ */ function dataValue(type, value) {
190
+ const data = view(value);
191
+ switch(type){
192
+ case 1:
193
+ case 4:
194
+ return utf8(value);
195
+ case 2:
196
+ case 5:
197
+ return new TextDecoder('utf-16be').decode(value).replace(/\0+$/, '');
198
+ case 21:
199
+ return integerText(value, true);
200
+ case 22:
201
+ return integerText(value, false);
202
+ case 23:
203
+ return value.length === 4 ? String(data.getFloat32(0)) : null;
204
+ case 24:
205
+ return value.length === 8 ? String(data.getFloat64(0)) : null;
206
+ case 65:
207
+ case 66:
208
+ case 67:
209
+ case 74:
210
+ return integerText(value, true);
211
+ case 75:
212
+ case 76:
213
+ case 77:
214
+ case 78:
215
+ return integerText(value, false);
216
+ default:
217
+ return null;
218
+ }
219
+ }
220
+ /** The first decodable `data` atom of an item: its value and whether it is text. */ function itemValue(bytes, item) {
221
+ for (const atom of children(bytes, item.body, item.end)){
222
+ if (atom.type !== 'data' || atom.body + 8 > atom.end) continue;
223
+ // 1 reserved byte + 3-byte type indicator, then a 4-byte locale.
224
+ const type = view(bytes).getUint32(atom.body) & 0xffffff;
225
+ const text = dataValue(type, bytes.subarray(atom.body + 8, atom.end));
226
+ if (text !== null) return {
227
+ text,
228
+ numeric: ![
229
+ 1,
230
+ 2,
231
+ 4,
232
+ 5
233
+ ].includes(type)
234
+ };
235
+ }
236
+ return null;
237
+ }
238
+ /** `mean` and `name` are full boxes holding a UTF-8 string. */ const fullBoxText = (bytes, box)=>box && box.body + 4 <= box.end ? utf8(bytes.subarray(box.body + 4, box.end)) : '';
239
+ /**
240
+ * iTunes item atoms and QuickTime user-data atoms. `ldes` (long
241
+ * description) outranks `desc` when a file has both.
242
+ */ const ATOM_KEYS = {
243
+ '©nam': 'title',
244
+ '©ART': 'creator',
245
+ '©aut': 'creator',
246
+ ldes: 'description',
247
+ desc: 'description',
248
+ '©des': 'description',
249
+ '©cmt': 'comment',
250
+ '©day': 'createdAt',
251
+ cprt: 'copyright',
252
+ '©cpy': 'copyright',
253
+ '©too': 'encoder',
254
+ '©swr': 'encoder',
255
+ '©mak': 'make',
256
+ '©mod': 'model',
257
+ keyw: 'keywords',
258
+ '©xyz': 'gps'
259
+ };
260
+ /** Apple's `mdta` keys (QuickTime File Format, "QuickTime metadata keys"). */ const MDTA_KEYS = {
261
+ 'com.apple.quicktime.title': 'title',
262
+ 'com.apple.quicktime.description': 'description',
263
+ 'com.apple.quicktime.artist': 'creator',
264
+ 'com.apple.quicktime.author': 'creator',
265
+ 'com.apple.quicktime.comment': 'comment',
266
+ 'com.apple.quicktime.creationdate': 'createdAt',
267
+ 'com.apple.quicktime.location.ISO6709': 'gps',
268
+ 'com.apple.quicktime.make': 'make',
269
+ 'com.apple.quicktime.model': 'model',
270
+ 'com.apple.quicktime.software': 'software',
271
+ 'com.apple.quicktime.keywords': 'keywords',
272
+ 'com.apple.quicktime.copyright': 'copyright'
273
+ };
274
+ /** Labels for the atoms outside the catalog that a person would recognize. */ const ATOM_LABELS = {
275
+ '©alb': 'Album',
276
+ aART: 'Album artist',
277
+ '©gen': 'Genre',
278
+ '©wrt': 'Composer',
279
+ '©grp': 'Grouping',
280
+ '©lyr': 'Lyrics',
281
+ '©dir': 'Director',
282
+ '©prd': 'Producer',
283
+ '©PRD': 'Product',
284
+ '©enc': 'Encoded by',
285
+ '©inf': 'Information',
286
+ '©req': 'Requirements',
287
+ '©src': 'Source credits',
288
+ '©fmt': 'Format',
289
+ '©prf': 'Performers',
290
+ '©st3': 'Subtitle',
291
+ tvsh: 'TV show',
292
+ tvnn: 'TV network',
293
+ tven: 'TV episode',
294
+ catg: 'Category',
295
+ tmpo: 'Tempo'
296
+ };
297
+ /**
298
+ * Candidates by key, the first of equal rank kept: a lower `rank` is a
299
+ * stronger place for the same idea.
300
+ */ let Collector = class Collector {
301
+ add(key, value, rank, label) {
302
+ const text = value.trim();
303
+ if (!text) return;
304
+ let candidate;
305
+ if (key === 'gps') {
306
+ const point = parseIso6709(text);
307
+ if (!point) return;
308
+ candidate = {
309
+ key,
310
+ value: point,
311
+ source: 'mp4',
312
+ editable: false
313
+ };
314
+ } else if (key === 'keywords') {
315
+ const items = text.split(/[,;]/).map((item)=>item.trim()).filter(Boolean);
316
+ if (!items.length) return;
317
+ candidate = {
318
+ key,
319
+ value: items,
320
+ source: 'mp4',
321
+ editable: false
322
+ };
323
+ } else if (key === 'creator') {
324
+ candidate = {
325
+ key,
326
+ value: [
327
+ text
328
+ ],
329
+ source: 'mp4',
330
+ editable: false
331
+ };
332
+ } else if (key === 'createdAt') {
333
+ candidate = {
334
+ key,
335
+ value: normalizeDate(text),
336
+ source: 'mp4',
337
+ editable: false
338
+ };
339
+ } else {
340
+ candidate = {
341
+ key,
342
+ value: text,
343
+ source: 'mp4',
344
+ editable: false
345
+ };
346
+ if (label) candidate.label = label;
347
+ }
348
+ const held = this.byKey.get(key);
349
+ if (!held || rank < held.rank) this.byKey.set(key, {
350
+ candidate,
351
+ rank
352
+ });
353
+ }
354
+ has(key) {
355
+ return this.byKey.has(key);
356
+ }
357
+ candidates() {
358
+ return [
359
+ ...this.byKey.values()
360
+ ].map(({ candidate })=>candidate);
361
+ }
362
+ constructor(){
363
+ this.byKey = new Map();
364
+ }
365
+ };
366
+ /** Ranks: Apple keys, then iTunes items, then user-data atoms, then 3GPP. */ const RANK_MDTA = 0;
367
+ const RANK_ILST = 10;
368
+ const RANK_UDTA = 20;
369
+ const RANK_3GPP = 30;
370
+ /**
371
+ * The two ways a tag is named: a four-character atom (iTunes items,
372
+ * QuickTime user data) or an Apple `mdta` key. Only a labeled atom's number
373
+ * is worth showing — `stik` or `rtng` alone means nothing to a person — and
374
+ * an atom whose name is not printable is noise from a damaged file.
375
+ */ const ATOMS = {
376
+ keys: ATOM_KEYS,
377
+ labels: ATOM_LABELS,
378
+ mdta: false
379
+ };
380
+ const MDTA = {
381
+ keys: MDTA_KEYS,
382
+ labels: {},
383
+ mdta: true
384
+ };
385
+ /** An item under a known key or atom, or an "other" field. */ function addTag(out, name, value, rank, naming) {
386
+ const key = naming.keys[name];
387
+ if (key) {
388
+ // `ldes` outranks `desc` within one container.
389
+ out.add(key, value.text, rank + (name === 'desc' ? 1 : 0));
390
+ return;
391
+ }
392
+ const label = naming.labels[name];
393
+ if (!naming.mdta && (!plausibleType(name) || value.numeric && !label)) return;
394
+ out.add(otherEmbeddedKey('mp4', name), value.text, rank, label != null ? label : name);
395
+ }
396
+ /**
397
+ * A `meta` box: `hdlr`, optional `keys`, `ilst`. ISO/IEC 14496-12 §8.11.1
398
+ * makes it a full box; QuickTime's is a plain one — whichever layout puts
399
+ * `hdlr` (or failing that, any plausible box) first is the one used.
400
+ */ function readMeta(bytes, meta, rank, out) {
401
+ const hdlrAt = (at)=>at + 8 <= meta.end && fourcc(bytes, at + 4) === 'hdlr';
402
+ let body = meta.body;
403
+ if (hdlrAt(meta.body + 4)) body = meta.body + 4;
404
+ else if (!hdlrAt(meta.body) && meta.body + 4 <= meta.end && view(bytes).getUint32(meta.body) === 0) body = meta.body + 4;
405
+ const boxes = children(bytes, body, meta.end);
406
+ const hdlr = boxes.find((box)=>box.type === 'hdlr');
407
+ // hdlr: version+flags, pre_defined, then handler_type (§8.4.3).
408
+ const handler = hdlr && hdlr.body + 12 <= hdlr.end ? fourcc(bytes, hdlr.body + 8) : '';
409
+ const keys = [];
410
+ const keysBox = boxes.find((box)=>box.type === 'keys');
411
+ if (keysBox && keysBox.body + 8 <= keysBox.end) {
412
+ const data = view(bytes);
413
+ const count = Math.min(MAX_KEYS, data.getUint32(keysBox.body + 4));
414
+ let at = keysBox.body + 8;
415
+ for(let index = 0; index < count && at + 8 <= keysBox.end; index++){
416
+ const size = data.getUint32(at);
417
+ if (size < 8 || at + size > keysBox.end) break;
418
+ keys.push(utf8(bytes.subarray(at + 8, at + size)));
419
+ at += size;
420
+ }
421
+ }
422
+ const ilst = boxes.find((box)=>box.type === 'ilst');
423
+ if (!ilst) return;
424
+ const indexed = handler === 'mdta' || keys.length > 0;
425
+ for (const item of children(bytes, ilst.body, ilst.end)){
426
+ if (indexed) {
427
+ // The item's type is a 1-based index into `keys`.
428
+ const name = keys[view(bytes).getUint32(item.start + 4) - 1];
429
+ const value = itemValue(bytes, item);
430
+ if (name && value) addTag(out, name, value, RANK_MDTA, MDTA);
431
+ continue;
432
+ }
433
+ if (item.type === '----') {
434
+ const parts = children(bytes, item.body, item.end);
435
+ const name = fullBoxText(bytes, parts.find((box)=>box.type === 'name'));
436
+ const value = itemValue(bytes, item);
437
+ if (name && value) out.add(otherEmbeddedKey('mp4', name), value.text, rank, name);
438
+ continue;
439
+ }
440
+ const value = itemValue(bytes, item);
441
+ if (value) addTag(out, item.type, value, rank, ATOMS);
442
+ }
443
+ }
444
+ /**
445
+ * A QuickTime user-data text atom: one or more `[length][language][text]`
446
+ * items; the first is used. A language code under 0x400 is a Macintosh
447
+ * one, whose text is Mac OS Roman; above it, packed ISO 639-2 with UTF-8.
448
+ * Some writers put an iTunes `data` atom here instead, which is honored.
449
+ */ function userDataText(bytes, atom) {
450
+ if (atom.body + 8 <= atom.end && fourcc(bytes, atom.body + 4) === 'data') {
451
+ return itemValue(bytes, atom);
452
+ }
453
+ if (atom.body + 4 > atom.end) return null;
454
+ const data = view(bytes);
455
+ const length = data.getUint16(atom.body);
456
+ const language = data.getUint16(atom.body + 2);
457
+ const end = atom.body + 4 + length;
458
+ if (end > atom.end) return null;
459
+ const raw = bytes.subarray(atom.body + 4, end);
460
+ return {
461
+ text: language < 0x400 ? macText(raw) : bomText(raw),
462
+ numeric: false
463
+ };
464
+ }
465
+ /**
466
+ * 3GPP asset boxes: full box, 16-bit pad+language, then a string. The
467
+ * performer stands in for the author when there is none — AVFoundation
468
+ * writes an MP4's artist as `perf`.
469
+ */ const THREE_GPP_KEYS = {
470
+ titl: {
471
+ key: 'title',
472
+ rank: 0
473
+ },
474
+ dscp: {
475
+ key: 'description',
476
+ rank: 0
477
+ },
478
+ auth: {
479
+ key: 'creator',
480
+ rank: 0
481
+ },
482
+ perf: {
483
+ key: 'creator',
484
+ rank: 1
485
+ },
486
+ cprt: {
487
+ key: 'copyright',
488
+ rank: 0
489
+ }
490
+ };
491
+ function isXmpUuid(bytes, at) {
492
+ return XMP_UUID.every((byte, index)=>bytes[at + index] === byte);
493
+ }
494
+ function readUserData(bytes, udta, out, xmp) {
495
+ for (const atom of children(bytes, udta.body, udta.end)){
496
+ if (atom.type === 'meta') {
497
+ readMeta(bytes, atom, RANK_ILST, out);
498
+ } else if (atom.type === 'XMP_') {
499
+ var _xmp, _value;
500
+ (_value = (_xmp = xmp).value) != null ? _value : _xmp.value = utf8(bytes.subarray(atom.body, atom.end));
501
+ } else if (atom.type === 'uuid') {
502
+ if (atom.body + 16 <= atom.end && isXmpUuid(bytes, atom.body)) {
503
+ var _xmp1, _value1;
504
+ (_value1 = (_xmp1 = xmp).value) != null ? _value1 : _xmp1.value = utf8(bytes.subarray(atom.body + 16, atom.end));
505
+ }
506
+ } else if (atom.type.startsWith('©')) {
507
+ const value = userDataText(bytes, atom);
508
+ if (value) addTag(out, atom.type, value, RANK_UDTA, ATOMS);
509
+ } else {
510
+ const asset = THREE_GPP_KEYS[atom.type];
511
+ if (asset && atom.body + 6 <= atom.end) {
512
+ const text = bomText(bytes.subarray(atom.body + 6, atom.end));
513
+ out.add(asset.key, text, RANK_3GPP + asset.rank);
514
+ }
515
+ }
516
+ }
517
+ }
518
+ /** `mvhd` creation time (§8.2.2): seconds since 1904 in UTC; 0 is unset. */ function movieCreated(bytes, mvhd) {
519
+ var _bytes_mvhd_body;
520
+ const data = view(bytes);
521
+ if (mvhd.body + 4 > mvhd.end) return null;
522
+ const version = (_bytes_mvhd_body = bytes[mvhd.body]) != null ? _bytes_mvhd_body : 0;
523
+ let seconds;
524
+ if (version === 1) {
525
+ if (mvhd.body + 12 > mvhd.end) return null;
526
+ seconds = data.getUint32(mvhd.body + 4) * 0x100000000 + data.getUint32(mvhd.body + 8);
527
+ } else {
528
+ if (mvhd.body + 8 > mvhd.end) return null;
529
+ seconds = data.getUint32(mvhd.body + 4);
530
+ }
531
+ if (!seconds) return null;
532
+ const date = new Date((seconds - EPOCH_1904) * 1000);
533
+ const year = date.getUTCFullYear();
534
+ if (Number.isNaN(date.getTime()) || year < 1904 || year > 9999) return null;
535
+ return date.toISOString().replace(/\.\d{3}Z$/, 'Z');
536
+ }
537
+ /** A top-level box header, read on its own (§4.2). Null past the end. */ async function topLevelBox(reader, at) {
538
+ if (at + 8 > reader.size) return null;
539
+ const head = await reader.read(at, at + 8);
540
+ if (head.length < 8) return null;
541
+ let size = view(head).getUint32(0);
542
+ let body = at + 8;
543
+ if (size === 1) {
544
+ if (at + 16 > reader.size) return null;
545
+ const large = await reader.read(at + 8, at + 16);
546
+ if (large.length < 8) return null;
547
+ size = view(large).getUint32(0) * 0x100000000 + view(large).getUint32(4);
548
+ if (!Number.isSafeInteger(size)) return null;
549
+ body = at + 16;
550
+ } else if (size === 0) {
551
+ size = reader.size - at;
552
+ }
553
+ if (size < body - at) return null;
554
+ return {
555
+ type: fourcc(head, 4),
556
+ start: at,
557
+ body,
558
+ end: Math.min(reader.size, at + size),
559
+ declaredEnd: at + size
560
+ };
561
+ }
562
+ /**
563
+ * Read a film's tags through random access.
564
+ *
565
+ * Null when the bytes do not open like an ISO BMFF or QuickTime file, have
566
+ * no `moov`, or have a `moov` over {@link MP4_MOOV_MAX_BYTES}. Every
567
+ * candidate is `editable: false`.
568
+ */ export async function readMp4(reader) {
569
+ let moov = null;
570
+ const xmp = {
571
+ value: null
572
+ };
573
+ let at = 0;
574
+ for(let count = 0; count < MAX_TOP_LEVEL_BOXES && at < reader.size; count++){
575
+ const box = await topLevelBox(reader, at);
576
+ if (!box || !plausibleType(box.type)) {
577
+ if (count === 0) return null;
578
+ break;
579
+ }
580
+ if (box.type === 'moov') {
581
+ moov != null ? moov : moov = box;
582
+ } else if (box.type === 'uuid' && xmp.value === null) {
583
+ const length = box.end - box.body - 16;
584
+ if (length > 0 && length <= XMP_MAX_BYTES) {
585
+ const id = await reader.read(box.body, box.body + 16);
586
+ if (isXmpUuid(id, 0)) {
587
+ xmp.value = utf8(await reader.read(box.body + 16, box.end));
588
+ }
589
+ }
590
+ } else if (box.type === 'moof' && moov) {
591
+ break;
592
+ }
593
+ at = box.declaredEnd;
594
+ }
595
+ if (!moov) return null;
596
+ if (moov.declaredEnd - moov.start > MP4_MOOV_MAX_BYTES) return null;
597
+ const bytes = await reader.read(moov.start, moov.end);
598
+ const box = {
599
+ type: 'moov',
600
+ start: 0,
601
+ body: moov.body - moov.start,
602
+ end: bytes.length
603
+ };
604
+ const out = new Collector();
605
+ const boxes = children(bytes, box.body, box.end);
606
+ const meta = boxes.find((item)=>item.type === 'meta');
607
+ if (meta) readMeta(bytes, meta, RANK_MDTA, out);
608
+ const udta = boxes.find((item)=>item.type === 'udta');
609
+ if (udta) readUserData(bytes, udta, out, xmp);
610
+ const mvhd = child(bytes, box, 'mvhd');
611
+ if (mvhd && !out.has('createdAt')) {
612
+ const created = movieCreated(bytes, mvhd);
613
+ if (created) out.add('createdAt', created, Number.MAX_SAFE_INTEGER);
614
+ }
615
+ return {
616
+ candidates: out.candidates(),
617
+ xmp: xmp.value
618
+ };
619
+ }
620
+
621
+ //# sourceMappingURL=mp4.js.map