video-fps-probe 0.3.0 → 0.4.0-beta.2
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/CHANGELOG.md +72 -0
- package/README.md +19 -11
- package/dist/analyze.js +10 -1
- package/dist/api/codes.d.ts +6 -0
- package/dist/api/codes.js +90 -0
- package/dist/aspect/index.d.ts +13 -0
- package/dist/aspect/index.js +180 -0
- package/dist/aspect/rational.js +61 -0
- package/dist/cjs/analyze.d.cts +9 -0
- package/dist/cjs/api/agreement.d.cts +18 -0
- package/dist/cjs/api/codes.d.cts +6 -0
- package/dist/cjs/api/errors.d.cts +12 -0
- package/dist/cjs/api/guards.d.cts +4 -0
- package/dist/cjs/api/summary.d.cts +22 -0
- package/dist/cjs/aspect/index.d.cts +13 -0
- package/dist/cjs/aspect.cjs +1 -0
- package/dist/cjs/check/index.d.cts +60 -0
- package/dist/cjs/check.cjs +1 -0
- package/dist/cjs/chunks/boxes-B0T2nt3e.cjs +1 -0
- package/dist/cjs/chunks/build-DhETwJLO.cjs +1 -0
- package/dist/cjs/chunks/color-xqJHSfhZ.cjs +1 -0
- package/dist/cjs/chunks/compare-B_HKNwqg.cjs +1 -0
- package/dist/cjs/chunks/config-CSbXzEqr.cjs +1 -0
- package/dist/cjs/chunks/errors-0oBg1Ai6.cjs +1 -0
- package/dist/cjs/chunks/fallback-CyPIR3RM.cjs +1 -0
- package/dist/cjs/chunks/guards-DbQNcAZS.cjs +1 -0
- package/dist/cjs/chunks/inline-Bx-TQg8f.cjs +1 -0
- package/dist/cjs/chunks/isobmff-4RrRf9N-.cjs +1 -0
- package/dist/cjs/chunks/matroska-BhgWtuPU.cjs +1 -0
- package/dist/cjs/chunks/metadata-ECdzNLkE.cjs +1 -0
- package/dist/cjs/chunks/options-B_vLHxHd.cjs +1 -0
- package/dist/cjs/chunks/probe-Cn1DtL-C.cjs +1 -0
- package/dist/cjs/chunks/rational-Dq5Vza-Q.cjs +1 -0
- package/dist/cjs/chunks/reader-Dw11ozVU.cjs +1 -0
- package/dist/cjs/chunks/rfps-C-pyKnu8.cjs +1 -0
- package/dist/cjs/chunks/runtime-BgAlQQee.cjs +1 -0
- package/dist/cjs/chunks/segments-B1H6bDz-.cjs +1 -0
- package/dist/cjs/chunks/sniff-CXwqOGwh.cjs +1 -0
- package/dist/cjs/chunks/stats-DW7dVdFa.cjs +1 -0
- package/dist/cjs/client/types.d.cts +96 -0
- package/dist/cjs/color/index.d.cts +7 -0
- package/dist/cjs/color.cjs +1 -0
- package/dist/cjs/core/reader.d.cts +47 -0
- package/dist/cjs/core/types.d.cts +194 -0
- package/dist/cjs/engines/types.d.cts +69 -0
- package/dist/cjs/header/index.d.cts +28 -0
- package/dist/cjs/header/limits.d.cts +26 -0
- package/dist/cjs/header.cjs +1 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.d.cts +13 -0
- package/dist/cjs/plugins/types.d.cts +17 -0
- package/dist/cjs/presets/index.d.cts +19 -0
- package/dist/cjs/presets.cjs +1 -0
- package/dist/cjs/probe.d.cts +3 -0
- package/dist/cjs/url/index.d.cts +47 -0
- package/dist/cjs/url.cjs +1 -0
- package/dist/cjs/worker/protocol.d.cts +47 -0
- package/dist/color/codec-string.js +121 -0
- package/dist/color/config.js +199 -0
- package/dist/color/index.d.ts +7 -0
- package/dist/color/index.js +139 -0
- package/dist/color/names.js +33 -0
- package/dist/color/sps.js +275 -0
- package/dist/core/dispatch.js +18 -29
- package/dist/core/isobmff/boxes.js +6 -5
- package/dist/core/isobmff/build.js +401 -0
- package/dist/core/isobmff/config.js +34 -0
- package/dist/core/isobmff/header.js +677 -0
- package/dist/core/isobmff/index.js +9 -381
- package/dist/core/isobmff/moof.js +2 -2
- package/dist/core/isobmff/moov.js +3 -3
- package/dist/core/matroska/header.js +10 -4
- package/dist/core/matroska/index.js +11 -5
- package/dist/core/options.js +1 -0
- package/dist/core/reader.d.ts +3 -0
- package/dist/core/reader.js +13 -7
- package/dist/core/result.js +1 -0
- package/dist/core/stats.js +3 -0
- package/dist/core/types.d.ts +60 -1
- package/dist/engines/common.js +2 -0
- package/dist/header/index.d.ts +28 -0
- package/dist/header/index.js +182 -0
- package/dist/header/limits.d.ts +26 -0
- package/dist/header/limits.js +144 -0
- package/dist/header/metadata.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/standalone/own-isobmff.worker.js +1 -1
- package/dist/standalone/own-matroska.worker.js +1 -1
- package/dist/standalone/own.worker.js +1 -1
- package/dist/url/index.d.ts +47 -0
- package/dist/url/index.js +335 -0
- package/dist/worker/single.js +2 -1
- package/package.json +117 -2
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import { MAX_EXPANDED_SAMPLES } from "../options.js";
|
|
2
|
+
import { emptyAnalysis } from "../result.js";
|
|
3
|
+
import { fourcc, whichErrorCode } from "../reader.js";
|
|
4
|
+
import { childBoxes, IsoBmffError, readBoxHeader } from "./boxes.js";
|
|
5
|
+
import { buildVideoTrack, containerOf, formatDuration, isAbort, isVideoTrack, layoutOf, MOOV_HARD_CAP, MOOV_SUSPECT_CAP, pickPrimary, readMoov, reportedIds } from "./build.js";
|
|
6
|
+
import { newMovie } from "./model.js";
|
|
7
|
+
import { parseMoof, parseSidx } from "./moof.js";
|
|
8
|
+
import { parseTfraOffsets } from "./prefetch.js";
|
|
9
|
+
import { editInfo, rescaleRound, sampleTotals, trackTiming } from "./track.js";
|
|
10
|
+
const HEADER_PEEK = 16;
|
|
11
|
+
const MFRA_TAIL = 64 * 1024;
|
|
12
|
+
const MFRA_CAP = 16 * 1024 * 1024;
|
|
13
|
+
const MOOF_HARD_CAP = 64 * 1024 * 1024;
|
|
14
|
+
const RUN_TYPES = new Set(['sidx', 'styp', 'prft', 'emsg']);
|
|
15
|
+
const RUN_WINDOW = 4096;
|
|
16
|
+
const SIDX = 0x73696478;
|
|
17
|
+
function count(w) {
|
|
18
|
+
if (++w.boxes > w.plan.maxTopLevelBoxes)
|
|
19
|
+
throw new IsoBmffError('structure-limit', `limit reached: maxTopLevelBoxes ${w.plan.maxTopLevelBoxes}`);
|
|
20
|
+
}
|
|
21
|
+
function visit(w) {
|
|
22
|
+
w.budget.checkTime();
|
|
23
|
+
count(w);
|
|
24
|
+
}
|
|
25
|
+
function ahead(w, pos, wanted) {
|
|
26
|
+
return Math.max(0, Math.min(wanted, w.budget.bytesLeft, w.reader.size - pos));
|
|
27
|
+
}
|
|
28
|
+
async function peekAt(w, pos, wanted) {
|
|
29
|
+
const need = Math.min(HEADER_PEEK, w.reader.size - pos);
|
|
30
|
+
return w.reader.peek(pos, need) ?? w.reader.view(pos, need, ahead(w, pos, wanted));
|
|
31
|
+
}
|
|
32
|
+
export function countTracks(dv, max) {
|
|
33
|
+
let moov;
|
|
34
|
+
try {
|
|
35
|
+
moov = readBoxHeader(dv, 0, dv.byteLength);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!moov)
|
|
41
|
+
return;
|
|
42
|
+
let n = 0;
|
|
43
|
+
for (let p = moov.start + moov.headerSize; p + 8 <= moov.end;) {
|
|
44
|
+
let c;
|
|
45
|
+
try {
|
|
46
|
+
c = readBoxHeader(dv, p, moov.end);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (!c)
|
|
52
|
+
return;
|
|
53
|
+
if (c.type === 'trak' && ++n > max)
|
|
54
|
+
throw new IsoBmffError('structure-limit', `limit reached: maxTracks ${max}`);
|
|
55
|
+
p = c.start + c.size;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function mehdDuration(dv) {
|
|
59
|
+
let moov;
|
|
60
|
+
try {
|
|
61
|
+
moov = readBoxHeader(dv, 0, dv.byteLength);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
if (!moov)
|
|
67
|
+
return null;
|
|
68
|
+
for (const c of childBoxes(dv, moov.start + moov.headerSize, moov.end)) {
|
|
69
|
+
if (c.type !== 'mvex')
|
|
70
|
+
continue;
|
|
71
|
+
for (const m of childBoxes(dv, c.start + c.headerSize, c.end)) {
|
|
72
|
+
const p = m.start + m.headerSize;
|
|
73
|
+
if (m.type !== 'mehd' || p + 8 > m.end)
|
|
74
|
+
continue;
|
|
75
|
+
if (dv.getUint8(p) === 1) {
|
|
76
|
+
if (p + 12 > m.end)
|
|
77
|
+
return null;
|
|
78
|
+
const d = dv.getBigUint64(p + 4);
|
|
79
|
+
return d === 0n || d > BigInt(Number.MAX_SAFE_INTEGER) ? null : Number(d);
|
|
80
|
+
}
|
|
81
|
+
const d = dv.getUint32(p + 4);
|
|
82
|
+
return d === 0 || d === 0xffffffff ? null : d;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
async function readHeaderMoov(w, h) {
|
|
88
|
+
const size = w.reader.size;
|
|
89
|
+
const beyond = h.start + h.size > size;
|
|
90
|
+
w.movie.moov = [h.start, h.size];
|
|
91
|
+
if (!beyond && h.size > MOOV_HARD_CAP)
|
|
92
|
+
throw new IsoBmffError('too-large', 'unsupported: moov larger than 256 MiB');
|
|
93
|
+
const dv = await w.reader.view(h.start, beyond ? Math.min(size - h.start, MOOV_SUSPECT_CAP) : h.size, beyond ? 0 : ahead(w, h.start, h.size + 2 * HEADER_PEEK));
|
|
94
|
+
countTracks(dv, w.plan.maxTracks);
|
|
95
|
+
await readMoov(w.reader, h, w.movie);
|
|
96
|
+
return dv;
|
|
97
|
+
}
|
|
98
|
+
async function walkTop(w) {
|
|
99
|
+
const { reader, movie } = w;
|
|
100
|
+
const size = reader.size;
|
|
101
|
+
let pos = 0;
|
|
102
|
+
let moov = null;
|
|
103
|
+
let afterMdat = false;
|
|
104
|
+
let runFrom = 0;
|
|
105
|
+
let peeked = false;
|
|
106
|
+
const walked = (firstMoof, limit = null) => ({ moov, fragments: movie.hasMvex && (firstMoof !== null || limit !== null), firstMoof, run: firstMoof ? firstMoof.start - runFrom : 0, limit });
|
|
107
|
+
try {
|
|
108
|
+
while (pos + 8 <= size) {
|
|
109
|
+
const cached = moov !== null && !movie.hasMvex;
|
|
110
|
+
let hv;
|
|
111
|
+
if (cached) {
|
|
112
|
+
hv = reader.peek(pos, Math.min(HEADER_PEEK, size - pos));
|
|
113
|
+
if (!hv) {
|
|
114
|
+
if (peeked)
|
|
115
|
+
break;
|
|
116
|
+
peeked = true;
|
|
117
|
+
try {
|
|
118
|
+
hv = await peekAt(w, pos, w.plan.head);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
if (isAbort(e, reader))
|
|
122
|
+
throw e;
|
|
123
|
+
const limit = w.budget.take();
|
|
124
|
+
if (limit)
|
|
125
|
+
throw limit;
|
|
126
|
+
movie.warnings.push(`fragments ignored: read at ${pos} after a moov without an mvex failed: ${e.message}`);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (++w.boxes > w.plan.maxTopLevelBoxes) {
|
|
131
|
+
movie.warnings.push('too many top-level boxes');
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
visit(w);
|
|
137
|
+
hv = await peekAt(w, pos, afterMdat && !moov ? w.plan.tail : w.plan.head);
|
|
138
|
+
}
|
|
139
|
+
let rel;
|
|
140
|
+
try {
|
|
141
|
+
rel = readBoxHeader(hv, 0, size - pos, pos);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
if (!moov)
|
|
145
|
+
throw e;
|
|
146
|
+
movie.warnings.push(`ignored trailing data at ${pos}: ${e.message}`);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
if (!rel)
|
|
150
|
+
break;
|
|
151
|
+
const h = { ...rel, start: pos, end: Math.min(pos + rel.size, size) };
|
|
152
|
+
const trailer = rel.truncated && moov !== null && h.type !== 'mdat' && h.type !== 'moof';
|
|
153
|
+
if (rel.truncated) {
|
|
154
|
+
movie.truncated = true;
|
|
155
|
+
movie.warnings.push(trailer ? `ignored trailing data at ${pos}: not a box` : `'${h.type}' at ${pos} is truncated`);
|
|
156
|
+
}
|
|
157
|
+
const top = { type: h.type, start: pos, end: h.end, sidx: null };
|
|
158
|
+
movie.topLevel.push(top);
|
|
159
|
+
if (cached && h.type === 'moof')
|
|
160
|
+
movie.warnings.push(`fragments ignored: moof at ${pos} after a moov without an mvex`);
|
|
161
|
+
if (trailer || (cached && h.type === 'moof') || (moov && h.type === 'mfra'))
|
|
162
|
+
break;
|
|
163
|
+
if (moov && h.type === 'moof')
|
|
164
|
+
return walked(h);
|
|
165
|
+
if (h.type === 'ftyp' && !cached) {
|
|
166
|
+
const fv = await reader.view(pos, Math.min(h.size, 1024), ahead(w, pos, w.plan.head));
|
|
167
|
+
const p = h.headerSize;
|
|
168
|
+
if (p + 4 <= fv.byteLength) {
|
|
169
|
+
movie.hasFtyp = true;
|
|
170
|
+
movie.brand = fourcc(fv, p);
|
|
171
|
+
for (let q = p + 8; q + 4 <= fv.byteLength; q += 4)
|
|
172
|
+
movie.compatibleBrands.push(fourcc(fv, q));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else if (h.type === 'moov') {
|
|
176
|
+
if (moov)
|
|
177
|
+
movie.warnings.push(`ignored a second moov at ${pos}`);
|
|
178
|
+
else
|
|
179
|
+
moov = await readHeaderMoov(w, h);
|
|
180
|
+
}
|
|
181
|
+
else if (h.type === 'moof') {
|
|
182
|
+
throw new IsoBmffError('corrupt', 'corrupt file: movie fragment (moof) before moov');
|
|
183
|
+
}
|
|
184
|
+
else if (h.type === 'sidx' && !rel.truncated) {
|
|
185
|
+
const dv = cached ? reader.peek(pos, h.size) : await reader.view(pos, h.size, ahead(w, pos, w.plan.head));
|
|
186
|
+
const sh = dv && readBoxHeader(dv, 0, dv.byteLength);
|
|
187
|
+
if (sh)
|
|
188
|
+
top.sidx = parseSidx(dv, sh, pos);
|
|
189
|
+
}
|
|
190
|
+
if (!RUN_TYPES.has(h.type))
|
|
191
|
+
runFrom = pos + rel.size;
|
|
192
|
+
afterMdat = h.type === 'mdat';
|
|
193
|
+
pos += rel.size;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
if (!moov || isAbort(e, reader))
|
|
198
|
+
throw e;
|
|
199
|
+
const limit = w.budget.take() ?? (isLimit(e) ? e : null);
|
|
200
|
+
if (!limit)
|
|
201
|
+
throw e;
|
|
202
|
+
return walked(null, limit);
|
|
203
|
+
}
|
|
204
|
+
if (!moov)
|
|
205
|
+
throw new IsoBmffError('no-header', 'incomplete file: no moov');
|
|
206
|
+
return walked(null);
|
|
207
|
+
}
|
|
208
|
+
async function readTailIndex(w) {
|
|
209
|
+
const { reader } = w;
|
|
210
|
+
const size = reader.size;
|
|
211
|
+
const tailLen = Math.min(size, MFRA_TAIL);
|
|
212
|
+
if (tailLen < 16)
|
|
213
|
+
return null;
|
|
214
|
+
const tail = reader.peek(size - tailLen, tailLen) ?? (await reader.view(size - tailLen, tailLen, 0));
|
|
215
|
+
if (fourcc(tail, tailLen - 12) !== 'mfro')
|
|
216
|
+
return null;
|
|
217
|
+
const mfraSize = tail.getUint32(tailLen - 4);
|
|
218
|
+
if (mfraSize < 24 || mfraSize > Math.min(size, MFRA_CAP))
|
|
219
|
+
return null;
|
|
220
|
+
const dv = mfraSize <= tailLen ? new DataView(tail.buffer, tail.byteOffset + tailLen - mfraSize, mfraSize) : await reader.view(size - mfraSize, mfraSize, 0);
|
|
221
|
+
let h;
|
|
222
|
+
try {
|
|
223
|
+
h = readBoxHeader(dv, 0, mfraSize);
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
if (!h || h.type !== 'mfra')
|
|
229
|
+
return null;
|
|
230
|
+
const offsets = new Map();
|
|
231
|
+
for (const t of childBoxes(dv, h.headerSize, h.end)) {
|
|
232
|
+
const p = t.start + t.headerSize;
|
|
233
|
+
if (t.type !== 'tfra' || p + 8 > t.end)
|
|
234
|
+
continue;
|
|
235
|
+
const id = dv.getUint32(p + 4);
|
|
236
|
+
const set = offsets.get(id) ?? new Set();
|
|
237
|
+
parseTfraOffsets(dv, t.start, t.end, set, size - mfraSize);
|
|
238
|
+
if (set.size > 0)
|
|
239
|
+
offsets.set(id, set);
|
|
240
|
+
}
|
|
241
|
+
return { mfraSize, offsets };
|
|
242
|
+
}
|
|
243
|
+
function parseFragment(w, dv, pos, parsed) {
|
|
244
|
+
const mh = readBoxHeader(dv, 0, dv.byteLength);
|
|
245
|
+
if (mh)
|
|
246
|
+
parseMoof(dv, mh, pos, w.movie.moofCount, w.movie, MAX_EXPANDED_SAMPLES);
|
|
247
|
+
w.movie.moofCount++;
|
|
248
|
+
parsed.add(pos);
|
|
249
|
+
}
|
|
250
|
+
async function walkFragments(w, from, end, parsed) {
|
|
251
|
+
let pos = from;
|
|
252
|
+
while (pos + 8 <= end) {
|
|
253
|
+
visit(w);
|
|
254
|
+
const hv = await peekAt(w, pos, w.plan.head);
|
|
255
|
+
let h;
|
|
256
|
+
try {
|
|
257
|
+
h = readBoxHeader(hv, 0, end - pos, pos);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
if (!h || h.truncated)
|
|
263
|
+
return false;
|
|
264
|
+
if (h.type === 'mfra')
|
|
265
|
+
return true;
|
|
266
|
+
if (h.type === 'moof' && !parsed.has(pos)) {
|
|
267
|
+
if (h.size > MOOF_HARD_CAP)
|
|
268
|
+
throw new IsoBmffError('too-large', `corrupt file: moof larger than 64 MiB at ${pos}`);
|
|
269
|
+
parseFragment(w, await w.reader.view(pos, h.size, ahead(w, pos, w.plan.head)), pos, parsed);
|
|
270
|
+
}
|
|
271
|
+
else if (h.type === 'sidx') {
|
|
272
|
+
w.movie.topLevel.push({ type: 'sidx', start: pos, end: pos + h.size, sidx: parseSidx(await w.reader.view(pos, h.size, ahead(w, pos, w.plan.head)), h, pos) });
|
|
273
|
+
}
|
|
274
|
+
pos += h.size;
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
function runTo(dv, q, at) {
|
|
279
|
+
const found = [];
|
|
280
|
+
try {
|
|
281
|
+
for (let p = q; p < dv.byteLength;) {
|
|
282
|
+
const h = readBoxHeader(dv, p, dv.byteLength);
|
|
283
|
+
if (!h || h.truncated || !RUN_TYPES.has(h.type))
|
|
284
|
+
return null;
|
|
285
|
+
found.push({ type: h.type, start: at + p, end: at + h.end, sidx: h.type === 'sidx' ? parseSidx(dv, h, at + p) : null });
|
|
286
|
+
p = h.end;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return found;
|
|
293
|
+
}
|
|
294
|
+
async function preRun(w, from, run, after) {
|
|
295
|
+
const at = Math.max(after, from - Math.max(run, RUN_WINDOW));
|
|
296
|
+
if (from - at < 8)
|
|
297
|
+
return from;
|
|
298
|
+
const dv = await w.reader.view(at, from - at, ahead(w, at, w.plan.head));
|
|
299
|
+
for (let q = 0; q + 8 <= dv.byteLength; q++) {
|
|
300
|
+
const found = dv.getUint32(q + 4) === SIDX ? runTo(dv, q, at) : null;
|
|
301
|
+
if (!found)
|
|
302
|
+
continue;
|
|
303
|
+
for (const b of found) {
|
|
304
|
+
count(w);
|
|
305
|
+
w.movie.topLevel.push(b);
|
|
306
|
+
}
|
|
307
|
+
return at + q;
|
|
308
|
+
}
|
|
309
|
+
return from;
|
|
310
|
+
}
|
|
311
|
+
function lastSidx(movie, tr, ends) {
|
|
312
|
+
let last = null;
|
|
313
|
+
for (const b of movie.topLevel) {
|
|
314
|
+
const x = b.sidx;
|
|
315
|
+
if (x && (!last || b.start > last.start) && !x.hierarchical && x.referenceId === tr.id && x.timescale === tr.timescale && (!ends || ends.includes(x.coverageEnd)))
|
|
316
|
+
last = b;
|
|
317
|
+
}
|
|
318
|
+
return last;
|
|
319
|
+
}
|
|
320
|
+
function timingOf(tr, movie, fragments) {
|
|
321
|
+
const t = { ...tr, fragments };
|
|
322
|
+
const totals = sampleTotals(t);
|
|
323
|
+
return trackTiming(t, movie, totals, editInfo(t, movie.timescale, totals));
|
|
324
|
+
}
|
|
325
|
+
function hasFragmentData(tr, movie, index) {
|
|
326
|
+
return movie.trex.has(tr.id) || (index?.offsets.has(tr.id) ?? false) || movie.topLevel.some((b) => b.sidx?.referenceId === tr.id);
|
|
327
|
+
}
|
|
328
|
+
function stillFragmented(tr, movie, index, parsed, resolved) {
|
|
329
|
+
if (!resolved)
|
|
330
|
+
return false;
|
|
331
|
+
let samples = sampleTotals({ ...tr, fragments: [] }).moovSamples;
|
|
332
|
+
for (const f of tr.fragments)
|
|
333
|
+
samples += f.count;
|
|
334
|
+
if (samples > 1)
|
|
335
|
+
return false;
|
|
336
|
+
for (const o of index?.offsets.get(tr.id) ?? [])
|
|
337
|
+
if (!parsed.has(o))
|
|
338
|
+
return false;
|
|
339
|
+
for (const b of movie.topLevel)
|
|
340
|
+
if (b.sidx?.referenceId === tr.id && b.sidx.subsegments.some(([o]) => !parsed.has(o)))
|
|
341
|
+
return false;
|
|
342
|
+
const first = tr.fragments[0];
|
|
343
|
+
const interval = movie.trex.get(tr.id)?.duration || (first ? (first.durations?.[0] ?? first.defaultDuration) : 0);
|
|
344
|
+
if (!(interval > 0))
|
|
345
|
+
return false;
|
|
346
|
+
return resolved.ticks * tr.timescale <= interval * resolved.timescale;
|
|
347
|
+
}
|
|
348
|
+
function fragmentedPrimary(built) {
|
|
349
|
+
let best = null;
|
|
350
|
+
const rank = (b) => (b.auxiliary ? 0.5 : b.enabled ? 2 : 1);
|
|
351
|
+
const area = (b) => b.result.width * b.result.height;
|
|
352
|
+
for (const b of built) {
|
|
353
|
+
if (!best)
|
|
354
|
+
best = b;
|
|
355
|
+
else if (rank(b) !== rank(best))
|
|
356
|
+
best = rank(b) > rank(best) ? b : best;
|
|
357
|
+
else if (b.result.isStill !== best.result.isStill)
|
|
358
|
+
best = best.result.isStill ? b : best;
|
|
359
|
+
else if (area(b) !== area(best))
|
|
360
|
+
best = area(b) > area(best) ? b : best;
|
|
361
|
+
else if (b.result.trackId < best.result.trackId)
|
|
362
|
+
best = b;
|
|
363
|
+
}
|
|
364
|
+
return best ? best.result.trackId : null;
|
|
365
|
+
}
|
|
366
|
+
function headerTrack(t) {
|
|
367
|
+
t.presentedFrames = null;
|
|
368
|
+
delete t.provenance.presentedFrames;
|
|
369
|
+
}
|
|
370
|
+
function isLimit(e) {
|
|
371
|
+
const code = e?.errorCode;
|
|
372
|
+
return typeof code === 'string' && code.endsWith('-limit');
|
|
373
|
+
}
|
|
374
|
+
function cachedBox(reader, pos) {
|
|
375
|
+
const size = reader.size;
|
|
376
|
+
const hv = pos + 8 <= size ? reader.peek(pos, Math.min(HEADER_PEEK, size - pos)) : null;
|
|
377
|
+
try {
|
|
378
|
+
const h = hv && readBoxHeader(hv, 0, size - pos, pos);
|
|
379
|
+
return h && !h.truncated ? h : null;
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function onlyFragment(reader, first) {
|
|
386
|
+
const after = first.start + first.size;
|
|
387
|
+
const data = cachedBox(reader, after);
|
|
388
|
+
if (after === reader.size)
|
|
389
|
+
return true;
|
|
390
|
+
if (data?.type !== 'mdat')
|
|
391
|
+
return false;
|
|
392
|
+
const end = after + data.size;
|
|
393
|
+
const next = cachedBox(reader, end);
|
|
394
|
+
return end === reader.size || (next?.type === 'mfra' && end + next.size === reader.size);
|
|
395
|
+
}
|
|
396
|
+
function lastOffset(set) {
|
|
397
|
+
let last = null;
|
|
398
|
+
for (const o of set ?? [])
|
|
399
|
+
if (last === null || o > last)
|
|
400
|
+
last = o;
|
|
401
|
+
return last;
|
|
402
|
+
}
|
|
403
|
+
async function resolveFragmented(w, walked, videos, built, analysis) {
|
|
404
|
+
const { reader, plan, movie } = w;
|
|
405
|
+
const size = reader.size;
|
|
406
|
+
const isMain = (t) => videos.includes(t) || t.handler === 'soun';
|
|
407
|
+
const tracks = movie.tracks.filter((t) => isMain(t) || t.hasMdhd);
|
|
408
|
+
const resolved = new Map();
|
|
409
|
+
const early = new Map();
|
|
410
|
+
const uncovered = new Set();
|
|
411
|
+
const parsed = new Set();
|
|
412
|
+
const firstCount = new Map();
|
|
413
|
+
const first = walked.firstMoof;
|
|
414
|
+
let index = null;
|
|
415
|
+
let limit = walked.limit;
|
|
416
|
+
let whole = false;
|
|
417
|
+
let low = size;
|
|
418
|
+
const open = () => tracks.filter((t) => !resolved.has(t) && (videos.includes(t) || hasFragmentData(t, movie, index)));
|
|
419
|
+
const settle = (tr, ends, fragments, into = resolved) => {
|
|
420
|
+
const x = lastSidx(movie, tr, ends)?.sidx;
|
|
421
|
+
const end = x ? x.earliestPts + x.durationSum : 0;
|
|
422
|
+
const ticks = timingOf(tr, movie, fragments).durationTs;
|
|
423
|
+
if (ends ? !x : !whole && x && x.start < low && x.coverageEnd <= low && end < ticks)
|
|
424
|
+
return;
|
|
425
|
+
into.set(tr, { ticks: Math.max(ticks, end), timescale: tr.timescale, provenance: 'header' });
|
|
426
|
+
};
|
|
427
|
+
const bySidx = (ends) => {
|
|
428
|
+
const list = open();
|
|
429
|
+
const own = list.filter((tr) => lastSidx(movie, tr, ends) !== null);
|
|
430
|
+
uncovered.clear();
|
|
431
|
+
for (const tr of list) {
|
|
432
|
+
if (own.includes(tr))
|
|
433
|
+
settle(tr, ends, tr.fragments, own.length === list.length ? resolved : early);
|
|
434
|
+
else
|
|
435
|
+
uncovered.add(tr);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
try {
|
|
439
|
+
const cut = first ? reader.peek(first.start, first.size) === null : false;
|
|
440
|
+
if (first && !limit && (plan.index || !cut || open().some((tr) => lastSidx(movie, tr, [size]) !== null))) {
|
|
441
|
+
if (first.size > MOOF_HARD_CAP)
|
|
442
|
+
throw new IsoBmffError('too-large', `corrupt file: moof larger than 64 MiB at ${first.start}`);
|
|
443
|
+
parseFragment(w, reader.peek(first.start, first.size) ?? (await reader.view(first.start, first.size, ahead(w, first.start, plan.head))), first.start, parsed);
|
|
444
|
+
whole = onlyFragment(reader, first);
|
|
445
|
+
if (whole)
|
|
446
|
+
for (const tr of open())
|
|
447
|
+
settle(tr, null, tr.fragments);
|
|
448
|
+
else
|
|
449
|
+
bySidx([size]);
|
|
450
|
+
}
|
|
451
|
+
for (const tr of tracks)
|
|
452
|
+
firstCount.set(tr, tr.fragments.length);
|
|
453
|
+
if (plan.index && !limit && open().length > 0) {
|
|
454
|
+
index = await readTailIndex(w);
|
|
455
|
+
if (index) {
|
|
456
|
+
const stop = size - index.mfraSize;
|
|
457
|
+
bySidx([size, stop]);
|
|
458
|
+
low = stop;
|
|
459
|
+
let reached = true;
|
|
460
|
+
for (const main of [true, false]) {
|
|
461
|
+
let from = null;
|
|
462
|
+
for (const tr of open()) {
|
|
463
|
+
const last = isMain(tr) === main ? lastOffset(index.offsets.get(tr.id)) : null;
|
|
464
|
+
if (last !== null && last < low && (from === null || last < from))
|
|
465
|
+
from = last;
|
|
466
|
+
}
|
|
467
|
+
if (from === null)
|
|
468
|
+
continue;
|
|
469
|
+
const before = tracks.map((t) => t.fragments.length);
|
|
470
|
+
const seen = main && first ? await preRun(w, from, walked.run, first.start + first.size) : from;
|
|
471
|
+
reached = (await walkFragments(w, from, low, parsed)) && reached;
|
|
472
|
+
tracks.forEach((t, i) => {
|
|
473
|
+
const f = t.fragments;
|
|
474
|
+
const a = firstCount.get(t) ?? 0;
|
|
475
|
+
t.fragments = [...f.slice(0, a), ...f.slice(before[i]), ...f.slice(a, before[i])];
|
|
476
|
+
});
|
|
477
|
+
low = seen;
|
|
478
|
+
whole = reached && first !== null && from <= first.start;
|
|
479
|
+
for (const tr of open()) {
|
|
480
|
+
const tail = tr.fragments.slice(firstCount.get(tr));
|
|
481
|
+
if (whole || (tail.length > 0 && tail[0].baseDts !== null))
|
|
482
|
+
settle(tr, null, tr.fragments);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
catch (e) {
|
|
489
|
+
if (isAbort(e, reader))
|
|
490
|
+
throw e;
|
|
491
|
+
limit = w.budget.take() ?? (isLimit(e) ? e : null);
|
|
492
|
+
if (!limit)
|
|
493
|
+
throw e;
|
|
494
|
+
for (const tr of tracks)
|
|
495
|
+
tr.fragments.length = firstCount.get(tr) ?? 0;
|
|
496
|
+
}
|
|
497
|
+
const mehd = walked.moov ? mehdDuration(walked.moov) : null;
|
|
498
|
+
const unread = open().length > 0 ? limit : null;
|
|
499
|
+
const kept = [...early.keys()].filter((tr) => !resolved.has(tr));
|
|
500
|
+
for (const tr of kept)
|
|
501
|
+
resolved.set(tr, early.get(tr));
|
|
502
|
+
for (const tr of open())
|
|
503
|
+
if (mehd !== null)
|
|
504
|
+
resolved.set(tr, { ticks: mehd, timescale: movie.timescale, provenance: 'estimated' });
|
|
505
|
+
for (const tr of tracks) {
|
|
506
|
+
if (!resolved.has(tr) && !videos.includes(tr) && !hasFragmentData(tr, movie, index))
|
|
507
|
+
resolved.set(tr, { ticks: timingOf(tr, movie, []).durationTs, timescale: tr.timescale, provenance: 'header' });
|
|
508
|
+
}
|
|
509
|
+
videos.forEach((tr, i) => {
|
|
510
|
+
const t = built[i].result;
|
|
511
|
+
const r = resolved.get(tr) ?? null;
|
|
512
|
+
const p = t.provenance;
|
|
513
|
+
t.frames = null;
|
|
514
|
+
t.avgFps = null;
|
|
515
|
+
t.avgFpsFfprobe = null;
|
|
516
|
+
t.startSec = null;
|
|
517
|
+
t.durationSec = r ? r.ticks / r.timescale : null;
|
|
518
|
+
t.isStill = stillFragmented(tr, movie, index, parsed, r);
|
|
519
|
+
delete p.frames;
|
|
520
|
+
delete p.avgFps;
|
|
521
|
+
delete p.startSec;
|
|
522
|
+
if (r)
|
|
523
|
+
p.durationSec = r.provenance;
|
|
524
|
+
else
|
|
525
|
+
delete p.durationSec;
|
|
526
|
+
});
|
|
527
|
+
const unresolved = open();
|
|
528
|
+
const failed = videos.length > 0 ? !videos.some((tr) => resolved.has(tr)) : ![...resolved.keys()].some(isMain) && unresolved.some(isMain);
|
|
529
|
+
if (unread && !failed)
|
|
530
|
+
analysis.warnings.push(`fragment index not read: ${unread.message}`);
|
|
531
|
+
const estimate = [...resolved].some(([tr, r]) => isMain(tr) && tr.hasMdhd && r.provenance === 'estimated');
|
|
532
|
+
const loose = (tr) => !estimate && !isMain(tr) && tracks.some((t) => isMain(t) && t.hasMdhd);
|
|
533
|
+
const dropped = unresolved.filter(loose);
|
|
534
|
+
const missing = unresolved.filter((tr) => !loose(tr));
|
|
535
|
+
const unstarted = [];
|
|
536
|
+
const NONE = Number.MAX_SAFE_INTEGER;
|
|
537
|
+
const av = { start: NONE, end: -NONE, dur: -NONE };
|
|
538
|
+
const text = { ...av };
|
|
539
|
+
let span = true;
|
|
540
|
+
let counted = 0;
|
|
541
|
+
for (const [tr, r] of resolved) {
|
|
542
|
+
if (!tr.hasMdhd)
|
|
543
|
+
continue;
|
|
544
|
+
const s = timingOf(tr, movie, whole ? tr.fragments : tr.fragments.slice(0, firstCount.get(tr) ?? 0)).startPts;
|
|
545
|
+
const exact = r.provenance !== 'estimated';
|
|
546
|
+
const started = s !== null || whole || !hasFragmentData(tr, movie, index);
|
|
547
|
+
if (loose(tr) && !(exact && started)) {
|
|
548
|
+
dropped.push(tr);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
counted++;
|
|
552
|
+
const m = isMain(tr) ? av : text;
|
|
553
|
+
const d = rescaleRound(r.ticks, 1e6, r.timescale);
|
|
554
|
+
m.dur = Math.max(m.dur, d);
|
|
555
|
+
if (!exact)
|
|
556
|
+
span = false;
|
|
557
|
+
else if (s !== null) {
|
|
558
|
+
const at = rescaleRound(s, 1e6, tr.timescale);
|
|
559
|
+
m.start = Math.min(m.start, at);
|
|
560
|
+
m.end = Math.max(m.end, at + d);
|
|
561
|
+
}
|
|
562
|
+
else if (!started)
|
|
563
|
+
unstarted.push(tr);
|
|
564
|
+
}
|
|
565
|
+
if (av.start === NONE || (av.start > text.start && av.start - text.start < 1e6))
|
|
566
|
+
av.start = text.start;
|
|
567
|
+
if (av.end === -NONE || (av.end < text.end && text.end - av.end < 1e6))
|
|
568
|
+
av.end = text.end;
|
|
569
|
+
let dur = av.dur === -NONE || (av.dur < text.dur && text.dur - av.dur < 1e6) ? text.dur : av.dur;
|
|
570
|
+
if (span && av.start !== NONE && av.end !== -NONE && av.end >= av.start)
|
|
571
|
+
dur = Math.max(dur, av.end - av.start);
|
|
572
|
+
if (!span || counted < 2)
|
|
573
|
+
unstarted.length = 0;
|
|
574
|
+
const unsure = kept.length > 0 ? [...uncovered].filter((tr) => resolved.get(tr)?.provenance !== 'header') : [];
|
|
575
|
+
if (kept.length > 0 && unsure.length === 0)
|
|
576
|
+
unsure.push(...kept);
|
|
577
|
+
const flagged = new Set([...missing, ...unstarted, ...dropped, ...unsure]);
|
|
578
|
+
if (!failed)
|
|
579
|
+
for (const tr of tracks)
|
|
580
|
+
if (flagged.has(tr))
|
|
581
|
+
analysis.warnings.push(`format duration unresolved: track ${tr.id}`);
|
|
582
|
+
analysis.formatDurationSec = missing.length + unstarted.length + unsure.length === 0 && dur > 0 ? dur / 1e6 : null;
|
|
583
|
+
analysis.primaryTrackId = fragmentedPrimary(built);
|
|
584
|
+
if (failed) {
|
|
585
|
+
const err = limit ?? new IsoBmffError('no-duration', 'incomplete header: fragmented file without a sidx, mfra or mehd duration');
|
|
586
|
+
analysis.error = err.message;
|
|
587
|
+
analysis.errorCode = whichErrorCode(err);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function collapse(list) {
|
|
591
|
+
const seen = new Map();
|
|
592
|
+
for (const s of list)
|
|
593
|
+
seen.set(s, (seen.get(s) ?? 0) + 1);
|
|
594
|
+
if (seen.size === list.length)
|
|
595
|
+
return;
|
|
596
|
+
list.length = 0;
|
|
597
|
+
for (const [s, n] of seen)
|
|
598
|
+
list.push(n > 1 ? `${s} (${n} times)` : s);
|
|
599
|
+
}
|
|
600
|
+
let capture;
|
|
601
|
+
let captureLoad;
|
|
602
|
+
function loadCapture() {
|
|
603
|
+
captureLoad ??= import("./config.js").then((m) => (capture = m), (e) => {
|
|
604
|
+
captureLoad = undefined;
|
|
605
|
+
throw e;
|
|
606
|
+
});
|
|
607
|
+
return captureLoad;
|
|
608
|
+
}
|
|
609
|
+
export async function analyzeIsoBmffHeader(reader, budget, plan) {
|
|
610
|
+
const movie = newMovie(reader.size);
|
|
611
|
+
const w = { reader, budget, plan, movie, boxes: 0 };
|
|
612
|
+
const analysis = emptyAnalysis('mp4');
|
|
613
|
+
analysis.scan = 'header';
|
|
614
|
+
analysis.warnings = movie.warnings;
|
|
615
|
+
analysis.errorCode = null;
|
|
616
|
+
try {
|
|
617
|
+
const walked = await walkTop(w);
|
|
618
|
+
analysis.container = containerOf(movie);
|
|
619
|
+
analysis.brand = movie.brand;
|
|
620
|
+
const reject = movie.ffprobeReject;
|
|
621
|
+
const ffprobeFails = reject !== null && reject.moof < movie.ffprobeMoofLimit;
|
|
622
|
+
if (ffprobeFails)
|
|
623
|
+
movie.warnings.push(`ffprobe 8.1.1 ${reject.reason}`);
|
|
624
|
+
const ids = reportedIds(movie.tracks);
|
|
625
|
+
if ([...ids].some(([t, id]) => t.id !== id))
|
|
626
|
+
movie.warnings.push('duplicate track_ID values');
|
|
627
|
+
const videos = movie.tracks.filter((t) => isVideoTrack(t, movie));
|
|
628
|
+
const built = videos.map((t) => buildVideoTrack(t, movie, { maxExpandedSamples: 0 }, { reportedId: ids.get(t) ?? t.id, ffprobeFails }));
|
|
629
|
+
for (const b of built)
|
|
630
|
+
headerTrack(b.result);
|
|
631
|
+
let loadError = null;
|
|
632
|
+
if (plan.codecConfig) {
|
|
633
|
+
try {
|
|
634
|
+
const { isoCodecConfig } = capture ?? (await budget.race(loadCapture()));
|
|
635
|
+
videos.forEach((t, i) => (built[i].result.codecConfig = isoCodecConfig(movie, t)));
|
|
636
|
+
}
|
|
637
|
+
catch (e) {
|
|
638
|
+
if (isAbort(e, reader))
|
|
639
|
+
throw e;
|
|
640
|
+
loadError = e;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
analysis.videoTracks = built.map((b) => b.result);
|
|
644
|
+
analysis.audioTrackCount = movie.tracks.filter((t) => t.handler === 'soun').length;
|
|
645
|
+
if (walked.fragments)
|
|
646
|
+
await resolveFragmented(w, walked, videos, built, analysis);
|
|
647
|
+
else {
|
|
648
|
+
analysis.primaryTrackId = pickPrimary(built);
|
|
649
|
+
analysis.formatDurationSec = formatDuration(movie, new Set(videos));
|
|
650
|
+
if (walked.limit) {
|
|
651
|
+
analysis.error = walked.limit.message;
|
|
652
|
+
analysis.errorCode = whichErrorCode(walked.limit);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (loadError !== null)
|
|
656
|
+
analysis.warnings.push(`codec configuration unavailable: ${loadError instanceof Error ? loadError.message : String(loadError)}`);
|
|
657
|
+
}
|
|
658
|
+
catch (e) {
|
|
659
|
+
if (isAbort(e, reader))
|
|
660
|
+
throw e;
|
|
661
|
+
const limit = budget.take();
|
|
662
|
+
const message = limit ? limit.message : e instanceof Error ? e.message : String(e);
|
|
663
|
+
analysis.container = containerOf(movie);
|
|
664
|
+
analysis.brand = movie.brand;
|
|
665
|
+
analysis.error = limit || e?.errorCode ? message : `corrupt file: ${message}`;
|
|
666
|
+
analysis.errorCode = whichErrorCode(limit ?? e);
|
|
667
|
+
analysis.videoTracks = [];
|
|
668
|
+
analysis.audioTrackCount = 0;
|
|
669
|
+
analysis.primaryTrackId = null;
|
|
670
|
+
analysis.formatDurationSec = null;
|
|
671
|
+
}
|
|
672
|
+
analysis.layout = layoutOf(movie);
|
|
673
|
+
collapse(analysis.warnings);
|
|
674
|
+
for (const t of analysis.videoTracks)
|
|
675
|
+
collapse(t.warnings);
|
|
676
|
+
return analysis;
|
|
677
|
+
}
|