@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.
- package/package.json +11 -11
- package/src/lib/app-utils/docs-help.generated.d.ts +1 -1
- package/src/lib/app-utils/docs-help.generated.js +1 -0
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +3 -3
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/media-embedded-fields.d.ts +471 -0
- package/src/lib/app-utils/media-embedded-fields.js +619 -0
- package/src/lib/app-utils/media-embedded-fields.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/exif.d.ts +160 -0
- package/src/lib/app-utils/media-embedded-metadata/exif.js +1267 -0
- package/src/lib/app-utils/media-embedded-metadata/exif.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/gif.d.ts +44 -0
- package/src/lib/app-utils/media-embedded-metadata/gif.js +134 -0
- package/src/lib/app-utils/media-embedded-metadata/gif.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/heif.d.ts +23 -0
- package/src/lib/app-utils/media-embedded-metadata/heif.js +354 -0
- package/src/lib/app-utils/media-embedded-metadata/heif.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/icc.d.ts +22 -0
- package/src/lib/app-utils/media-embedded-metadata/icc.js +85 -0
- package/src/lib/app-utils/media-embedded-metadata/icc.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/image-blocks.d.ts +145 -0
- package/src/lib/app-utils/media-embedded-metadata/image-blocks.js +200 -0
- package/src/lib/app-utils/media-embedded-metadata/image-blocks.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/index.d.ts +93 -0
- package/src/lib/app-utils/media-embedded-metadata/index.js +744 -0
- package/src/lib/app-utils/media-embedded-metadata/index.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/iptc.d.ts +59 -0
- package/src/lib/app-utils/media-embedded-metadata/iptc.js +529 -0
- package/src/lib/app-utils/media-embedded-metadata/iptc.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/jpeg.d.ts +43 -0
- package/src/lib/app-utils/media-embedded-metadata/jpeg.js +456 -0
- package/src/lib/app-utils/media-embedded-metadata/jpeg.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/mp4.d.ts +37 -0
- package/src/lib/app-utils/media-embedded-metadata/mp4.js +621 -0
- package/src/lib/app-utils/media-embedded-metadata/mp4.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/ooxml.d.ts +131 -0
- package/src/lib/app-utils/media-embedded-metadata/ooxml.js +1058 -0
- package/src/lib/app-utils/media-embedded-metadata/ooxml.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf-objects.d.ts +223 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js +936 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf-objects.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf.d.ts +82 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf.js +1167 -0
- package/src/lib/app-utils/media-embedded-metadata/pdf.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/png.d.ts +41 -0
- package/src/lib/app-utils/media-embedded-metadata/png.js +512 -0
- package/src/lib/app-utils/media-embedded-metadata/png.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/svg.d.ts +30 -0
- package/src/lib/app-utils/media-embedded-metadata/svg.js +154 -0
- package/src/lib/app-utils/media-embedded-metadata/svg.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/tiff.d.ts +40 -0
- package/src/lib/app-utils/media-embedded-metadata/tiff.js +158 -0
- package/src/lib/app-utils/media-embedded-metadata/tiff.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/types.d.ts +60 -0
- package/src/lib/app-utils/media-embedded-metadata/types.js +32 -0
- package/src/lib/app-utils/media-embedded-metadata/types.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/webm.d.ts +26 -0
- package/src/lib/app-utils/media-embedded-metadata/webm.js +417 -0
- package/src/lib/app-utils/media-embedded-metadata/webm.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/webp.d.ts +37 -0
- package/src/lib/app-utils/media-embedded-metadata/webp.js +272 -0
- package/src/lib/app-utils/media-embedded-metadata/webp.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/xml.d.ts +231 -0
- package/src/lib/app-utils/media-embedded-metadata/xml.js +598 -0
- package/src/lib/app-utils/media-embedded-metadata/xml.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/xmp.d.ts +100 -0
- package/src/lib/app-utils/media-embedded-metadata/xmp.js +1475 -0
- package/src/lib/app-utils/media-embedded-metadata/xmp.js.map +1 -0
- package/src/lib/app-utils/media-embedded-metadata/zip.d.ts +93 -0
- package/src/lib/app-utils/media-embedded-metadata/zip.js +531 -0
- package/src/lib/app-utils/media-embedded-metadata/zip.js.map +1 -0
- package/src/lib/app-utils/name-search.d.ts +12 -0
- package/src/lib/app-utils/name-search.js +11 -0
- package/src/lib/app-utils/name-search.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +11 -0
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/server.d.ts +1 -0
- package/src/server.js +7 -0
- 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
|