video-fps-probe 0.2.0 → 0.4.0-beta.1

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 (110) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +33 -15
  3. package/dist/api/codes.d.ts +6 -0
  4. package/dist/api/codes.js +88 -0
  5. package/dist/api/errors.js +1 -1
  6. package/dist/api/guards.d.ts +2 -0
  7. package/dist/api/guards.js +2 -1
  8. package/dist/check/index.js +12 -2
  9. package/dist/cjs/analyze.d.cts +9 -0
  10. package/dist/cjs/api/agreement.d.cts +18 -0
  11. package/dist/cjs/api/codes.d.cts +6 -0
  12. package/dist/cjs/api/errors.d.cts +12 -0
  13. package/dist/cjs/api/guards.d.cts +4 -0
  14. package/dist/cjs/api/summary.d.cts +22 -0
  15. package/dist/cjs/check/index.d.cts +60 -0
  16. package/dist/cjs/check.cjs +1 -0
  17. package/dist/cjs/chunks/build-CT5g6_bs.cjs +1 -0
  18. package/dist/cjs/chunks/compare-D7RVqlqf.cjs +1 -0
  19. package/dist/cjs/chunks/errors-0oBg1Ai6.cjs +1 -0
  20. package/dist/cjs/chunks/fallback-D9Ak2zFv.cjs +1 -0
  21. package/dist/cjs/chunks/guards-Dqpjtkdn.cjs +1 -0
  22. package/dist/cjs/chunks/inline-Bx-TQg8f.cjs +1 -0
  23. package/dist/cjs/chunks/isobmff-B_bb5rS5.cjs +1 -0
  24. package/dist/cjs/chunks/matroska-CLoGzfKA.cjs +1 -0
  25. package/dist/cjs/chunks/probe-DDLUfr23.cjs +1 -0
  26. package/dist/cjs/chunks/rational-Dq5Vza-Q.cjs +1 -0
  27. package/dist/cjs/chunks/reader-DNLR-_tm.cjs +1 -0
  28. package/dist/cjs/chunks/rfps-C-pyKnu8.cjs +1 -0
  29. package/dist/cjs/chunks/runtime-DtgT0xzU.cjs +1 -0
  30. package/dist/cjs/chunks/segments-i-hPN7H3.cjs +1 -0
  31. package/dist/cjs/chunks/sniff-CXwqOGwh.cjs +1 -0
  32. package/dist/cjs/chunks/stats-D68frkk2.cjs +1 -0
  33. package/dist/cjs/client/types.d.cts +96 -0
  34. package/dist/cjs/core/reader.d.cts +47 -0
  35. package/dist/cjs/core/types.d.cts +149 -0
  36. package/dist/cjs/engines/types.d.cts +69 -0
  37. package/dist/cjs/header/index.d.cts +25 -0
  38. package/dist/cjs/header/limits.d.cts +25 -0
  39. package/dist/cjs/header.cjs +1 -0
  40. package/dist/cjs/index.cjs +1 -0
  41. package/dist/cjs/index.d.cts +13 -0
  42. package/dist/cjs/plugins/types.d.cts +17 -0
  43. package/dist/cjs/presets/index.d.cts +19 -0
  44. package/dist/cjs/presets.cjs +1 -0
  45. package/dist/cjs/probe.d.cts +3 -0
  46. package/dist/cjs/url/index.d.cts +47 -0
  47. package/dist/cjs/url.cjs +1 -0
  48. package/dist/cjs/worker/protocol.d.cts +47 -0
  49. package/dist/client/compare.js +2 -2
  50. package/dist/client/create.d.ts +3 -0
  51. package/dist/client/create.js +8 -0
  52. package/dist/client/fallback.js +2 -2
  53. package/dist/client/index.d.ts +1 -1
  54. package/dist/client/index.js +1 -1
  55. package/dist/client/inline.js +1 -1
  56. package/dist/client/lazy.js +1 -1
  57. package/dist/client/probe-client.js +4 -0
  58. package/dist/client/probe.js +23 -22
  59. package/dist/client/route.js +20 -0
  60. package/dist/client/split.js +9 -0
  61. package/dist/client/stub.js +2 -2
  62. package/dist/client/types.d.ts +1 -0
  63. package/dist/core/analyze.js +4 -78
  64. package/dist/core/dispatch.js +65 -0
  65. package/dist/core/isobmff/boxes.js +6 -8
  66. package/dist/core/isobmff/build.js +398 -0
  67. package/dist/core/isobmff/header.js +654 -0
  68. package/dist/core/isobmff/index.js +9 -378
  69. package/dist/core/isobmff/model.js +1 -1
  70. package/dist/core/isobmff/moof.js +2 -2
  71. package/dist/core/isobmff/moov.js +14 -9
  72. package/dist/core/isobmff/track.js +4 -2
  73. package/dist/core/matroska/ebml.js +1 -1
  74. package/dist/core/matroska/header.js +9 -4
  75. package/dist/core/matroska/index.js +7 -3
  76. package/dist/core/reader.d.ts +3 -12
  77. package/dist/core/reader.js +21 -32
  78. package/dist/core/result.js +1 -0
  79. package/dist/core/stats.js +3 -0
  80. package/dist/core/types.d.ts +15 -1
  81. package/dist/engines/common.js +8 -0
  82. package/dist/engines/mediabunny.js +17 -4
  83. package/dist/engines/mp4box.js +2 -1
  84. package/dist/header/index.d.ts +25 -0
  85. package/dist/header/index.js +146 -0
  86. package/dist/header/limits.d.ts +25 -0
  87. package/dist/header/limits.js +140 -0
  88. package/dist/index.d.ts +1 -0
  89. package/dist/index.js +1 -0
  90. package/dist/plugins/own.js +5 -2
  91. package/dist/presets/index.d.ts +1 -0
  92. package/dist/presets/sources.json +8 -0
  93. package/dist/probe.js +2 -1
  94. package/dist/react/store.js +1 -1
  95. package/dist/standalone/own-isobmff.worker.js +1 -0
  96. package/dist/standalone/own-matroska.worker.js +1 -0
  97. package/dist/standalone/own.worker.js +1 -1
  98. package/dist/url/index.d.ts +47 -0
  99. package/dist/url/index.js +335 -0
  100. package/dist/worker/own-isobmff.worker.js +4 -0
  101. package/dist/worker/own-matroska.worker.js +4 -0
  102. package/dist/worker/runtime.d.ts +2 -3
  103. package/dist/worker/runtime.js +2 -97
  104. package/dist/worker/serve.d.ts +3 -0
  105. package/dist/worker/serve.js +97 -0
  106. package/dist/worker/single.js +14 -0
  107. package/package.json +85 -2
  108. package/dist/client/inline.d.ts +0 -19
  109. package/dist/client/pool.d.ts +0 -29
  110. package/dist/client/probe.d.ts +0 -29
@@ -1,389 +1,19 @@
1
- import { emptyAnalysis, emptyTrack } from "../result.js";
1
+ import { emptyAnalysis } from "../result.js";
2
2
  import { analyzeTimeline, rateProfileOf } from "../stats.js";
3
- import { toFfprobeString } from "../rational.js";
4
- import { IsoBmffError, readBoxHeader } from "./boxes.js";
5
- import { codecNameFor, isVideoFourcc } from "./codecs.js";
3
+ import { buildVideoTrack, containerOf, emulateFfprobeExtent, formatDuration, isAbort, isVideoTrack, layoutOf, pickPrimary, reportedIds, walk } from "./build.js";
4
+ import { whichErrorCode } from "../reader.js";
6
5
  import { newMovie } from "./model.js";
7
- import { parseMoof, parseSidx } from "./moof.js";
8
- import { parseMoov } from "./moov.js";
9
- import { mfraMoofOffsets, newPrefetch, prefetchNext, prefetched } from "./prefetch.js";
10
- import { averageRates, displayRotation, editInfo, ffprobeStage1Rate, missingMandatoryAtoms, moovSamplesInFile, presentationWindow, rescaleRound, sampleTotals, timelineSegments, trackTiming, } from "./track.js";
11
6
  export { IsoBmffError } from "./boxes.js";
12
- export const MOOV_SUSPECT_CAP = 16 * 1024 * 1024;
13
- export const MOOV_HARD_CAP = 256 * 1024 * 1024;
14
- export const MOOF_HARD_CAP = 64 * 1024 * 1024;
15
- const MAX_TOP_LEVEL_BOXES = 4_000_000;
16
- const SMALL_MDAT = 64 * 1024;
17
- const HEADER_PEEK = 16;
18
- function isAbort(e, reader) {
19
- return reader.signal?.aborted === true || (typeof e === 'object' && e !== null && e.name === 'AbortError');
20
- }
21
- function containerOf(movie) {
22
- return movie.brand === 'qt ' || (!movie.hasFtyp && movie.topLevel.length > 0) ? 'mov' : 'mp4';
23
- }
24
- async function readMoov(reader, h, movie) {
25
- const size = reader.size;
26
- const beyond = h.start + h.size > size;
27
- if (!beyond && h.size > MOOV_HARD_CAP)
28
- throw new IsoBmffError('unsupported: moov larger than 256 MiB');
29
- const len = beyond ? Math.min(size - h.start, MOOV_SUSPECT_CAP) : h.size;
30
- const dv = await reader.view(h.start, len, 0);
31
- const mh = readBoxHeader(dv, 0, dv.byteLength);
32
- if (!mh)
33
- throw new IsoBmffError('incomplete file: moov is truncated');
34
- parseMoov(dv, mh, movie, beyond, h.start);
35
- if (beyond)
36
- movie.warnings.push('moov size exceeds the file; parsed what is there');
37
- }
38
- async function walk(reader, opts, movie) {
39
- const size = reader.size;
40
- let pos = 0;
41
- let ahead = opts.readAhead.head;
42
- let sawMoov = false;
43
- let boxes = 0;
44
- let afterMdat = false;
45
- const pf = newPrefetch();
46
- const look = async (at, need, readAhead) => prefetched(pf, at, need) ?? reader.view(at, need, readAhead);
47
- while (pos + 8 <= size) {
48
- reader.checkAbort();
49
- if (++boxes > MAX_TOP_LEVEL_BOXES) {
50
- movie.warnings.push('too many top-level boxes');
51
- break;
52
- }
53
- const tailRead = afterMdat && !sawMoov && size - pos <= MOOV_SUSPECT_CAP ? size - pos : ahead;
54
- const hv = await look(pos, Math.min(HEADER_PEEK, size - pos), tailRead);
55
- let rel;
56
- try {
57
- rel = readBoxHeader(hv, 0, size - pos, pos);
58
- }
59
- catch (e) {
60
- if (!sawMoov)
61
- throw e;
62
- movie.warnings.push(`ignored trailing data at ${pos}: ${e.message}`);
63
- break;
64
- }
65
- if (!rel)
66
- break;
67
- const h = { ...rel, start: pos, end: Math.min(pos + rel.size, size) };
68
- const boxEnd = pos + rel.size;
69
- if (rel.truncated) {
70
- movie.truncated = true;
71
- movie.warnings.push(`'${h.type}' at ${pos} is truncated`);
72
- }
73
- const top = { type: h.type, start: pos, end: h.end, sidx: null };
74
- movie.topLevel.push(top);
75
- if (h.type === 'ftyp') {
76
- const fv = await reader.view(pos, Math.min(h.size, 1024));
77
- const p = h.headerSize;
78
- if (p + 4 <= fv.byteLength) {
79
- movie.hasFtyp = true;
80
- movie.brand = String.fromCharCode(fv.getUint8(p), fv.getUint8(p + 1), fv.getUint8(p + 2), fv.getUint8(p + 3));
81
- for (let q = p + 8; q + 4 <= fv.byteLength; q += 4) {
82
- movie.compatibleBrands.push(String.fromCharCode(fv.getUint8(q), fv.getUint8(q + 1), fv.getUint8(q + 2), fv.getUint8(q + 3)));
83
- }
84
- }
85
- }
86
- else if (h.type === 'moov') {
87
- if (sawMoov)
88
- movie.warnings.push(`ignored a second moov at ${pos}`);
89
- else {
90
- await readMoov(reader, h, movie);
91
- sawMoov = true;
92
- }
93
- }
94
- else if (h.type === 'moof') {
95
- if (!sawMoov)
96
- throw new IsoBmffError('corrupt file: movie fragment (moof) before moov');
97
- if (h.size > MOOF_HARD_CAP)
98
- throw new IsoBmffError(`corrupt file: moof larger than 64 MiB at ${pos}`);
99
- const want = Math.min(h.size + HEADER_PEEK, size - pos);
100
- const dv = await look(pos, want, 0);
101
- const mh = readBoxHeader(dv, 0, Math.min(h.size, dv.byteLength));
102
- if (mh)
103
- parseMoof(dv, mh, pos, movie.moofCount, movie, opts.maxExpandedSamples);
104
- movie.moofCount++;
105
- let gap = 0;
106
- try {
107
- const next = readBoxHeader(dv, h.size, dv.byteLength);
108
- if (next && next.type === 'mdat')
109
- gap = next.size;
110
- }
111
- catch {
112
- gap = 0;
113
- }
114
- ahead = gap <= SMALL_MDAT ? opts.readAhead.fragment : Math.min(65536, Math.max(1024, 2 * h.size + 64));
115
- if (gap > SMALL_MDAT) {
116
- if (!pf.hints.length && !pf.mfraTried && (size - pos) / (h.size + gap) > 16) {
117
- pf.mfraTried = true;
118
- pf.hints = await mfraMoofOffsets(reader);
119
- }
120
- if (pf.next < pf.hints.length && !prefetched(pf, pos + h.size + gap, HEADER_PEEK))
121
- await prefetchNext(reader, pf, pos, ahead);
122
- }
123
- if (opts.onProgress && movie.moofCount % 64 === 0)
124
- opts.onProgress({ phase: 'fragments', done: pos, total: size });
125
- }
126
- else if (h.type === 'sidx' && !rel.truncated) {
127
- const dv = await look(pos, h.size, 0);
128
- const sh = readBoxHeader(dv, 0, dv.byteLength);
129
- if (sh)
130
- top.sidx = parseSidx(dv, sh, pos);
131
- if (top.sidx && top.sidx.subsegments.length > 1 && !pf.hints.length)
132
- pf.hints = top.sidx.subsegments.map(([o]) => o);
133
- }
134
- else if (h.type === 'mfra') {
135
- break;
136
- }
137
- afterMdat = h.type === 'mdat';
138
- pos = boxEnd;
139
- }
140
- if (!sawMoov)
141
- throw new IsoBmffError('incomplete file: no moov');
142
- }
143
- async function emulateFfprobeExtent(reader, movie) {
144
- const size = reader.size;
145
- const ids = new Set(movie.tracks.map((t) => t.id));
146
- let foundMoov = false;
147
- let foundMdat = false;
148
- let complete = false;
149
- let mfraSize = null;
150
- let moofs = 0;
151
- for (const b of movie.topLevel) {
152
- if (b.type === 'moof')
153
- moofs++;
154
- else if (b.type === 'moov')
155
- foundMoov = true;
156
- else if (b.type === 'mdat' && b.end - b.start > 8)
157
- foundMdat = true;
158
- else if (b.type === 'sidx' && foundMoov && b.sidx && !b.sidx.hierarchical && ids.has(b.sidx.referenceId)) {
159
- let whole = b.sidx.coverageEnd === size;
160
- if (!whole && size >= 4) {
161
- if (mfraSize === null)
162
- mfraSize = (await reader.view(size - 4, 4, 0)).getUint32(0);
163
- whole = b.sidx.coverageEnd === size - mfraSize;
164
- }
165
- if (whole && b.sidx.firstOffset === 0)
166
- complete = true;
167
- }
168
- if (foundMoov && foundMdat && (complete || b.end === size)) {
169
- movie.ffprobeMoofLimit = moofs;
170
- return;
171
- }
172
- }
173
- }
174
- function isVideoTrack(tr, movie) {
175
- if (movie.chapterTrackIds.has(tr.id))
176
- return false;
177
- if (tr.handler === 'vide')
178
- return true;
179
- if (tr.handler === 'soun')
180
- return false;
181
- return isVideoFourcc(tr.originalFormat ?? tr.sampleEntry);
182
- }
183
- function buildVideoTrack(tr, movie, opts, ctx) {
184
- const t = emptyTrack(ctx.reportedId);
185
- const w = t.warnings;
186
- if (ctx.reportedId !== tr.id)
187
- w.push(`duplicate track_ID ${tr.id}: reported as ${ctx.reportedId}`);
188
- w.push(...tr.warnings);
189
- const auxiliary = tr.handler === 'auxv';
190
- if (auxiliary)
191
- w.push("auxiliary video track (handler 'auxv')");
192
- const broken = missingMandatoryAtoms(tr);
193
- if (broken)
194
- w.push('missing mandatory sample tables: ffprobe ignores the samples and timing of this track');
195
- const ts = tr.hasMdhd ? tr.timescale : movie.timescale;
196
- if (!tr.hasMdhd) {
197
- w.push('no mdhd: using the movie timescale');
198
- tr.timescale = ts;
199
- }
200
- t.fourccOrCodecId = tr.sampleEntry;
201
- t.codecName = codecNameFor(tr.originalFormat ?? tr.sampleEntry, tr.esdsObjectType, tr.hasHvcc);
202
- if (tr.originalFormat)
203
- w.push(`encrypted: ${tr.sampleEntry} of ${tr.originalFormat}`);
204
- if (tr.stsdCount > 1)
205
- w.push(`${tr.stsdCount} sample descriptions, first used`);
206
- t.width = tr.width;
207
- t.height = tr.height;
208
- t.rotation = displayRotation(tr.matrix, movie.matrix);
209
- t.timescale = ts;
210
- const totals = sampleTotals(tr);
211
- const truncated = movie.truncated;
212
- let presentFragments = 0;
213
- for (const f of tr.fragments)
214
- presentFragments += f.present;
215
- const total = totals.moovSamples + totals.fragmentSamples;
216
- const frames = totals.moovSamples + presentFragments;
217
- t.frames = frames;
218
- t.isStill = frames <= 1;
219
- if (totals.moovSamples > 0 && tr.fragments.length > 0) {
220
- w.push(`hybrid fragmented file: moov holds ${totals.moovSamples} of ${total} samples`);
221
- }
222
- if (frames < total)
223
- w.push(`truncated: ${total - frames} of ${total} samples missing`);
224
- const rates = averageRates(tr, movie);
225
- t.avgFps = rates.avg;
226
- t.avgFpsFfprobe = ctx.ffprobeFails ? null : rates.ffprobe;
227
- if (t.avgFpsFfprobe !== null && t.avgFpsFfprobe !== toFfprobeString(rates.avg)) {
228
- w.push(`ffprobe reads part of the fragments: avg_frame_rate ${t.avgFpsFfprobe}`);
229
- }
230
- if (rates.ffprobe === null && !ctx.ffprobeFails && total > 0)
231
- w.push('avg_frame_rate of ffprobe is not reproduced: its header values are incomplete or overrun a box');
232
- const r = ctx.ffprobeFails || broken ? null : ffprobeStage1Rate(tr, movie, totals.moovSamples, opts.maxExpandedSamples, w);
233
- t.rFpsFfprobe = r ? toFfprobeString(r) : null;
234
- const edits = editInfo(tr, movie.timescale);
235
- if (!broken) {
236
- const timing = trackTiming(tr, movie, totals, edits);
237
- t.durationSec = timing.durationTs / ts;
238
- t.startSec = timing.startPts === null ? null : timing.startPts / ts;
239
- }
240
- let moovKeep = totals.moovSamples;
241
- if (tr.sampleCount !== null && tr.sampleCount < moovKeep) {
242
- w.push(`stts describes ${moovKeep} samples but the sample size table holds ${tr.sampleCount}`);
243
- moovKeep = tr.sampleCount;
244
- }
245
- if (broken) {
246
- t.presentedFrames = 0;
247
- finishProvenance(t);
248
- return { result: t, enabled: tr.enabled, auxiliary };
249
- }
250
- if (truncated && moovKeep > 0) {
251
- const inFile = moovSamplesInFile(tr, movie.fileSize, totals.moovSamples);
252
- if (inFile && inFile.complete < moovKeep) {
253
- w.push(`truncated: ${inFile.complete} of ${moovKeep} samples complete`);
254
- moovKeep = inFile.complete;
255
- }
256
- }
257
- let window = presentationWindow(tr, edits, w);
258
- if (window && !tr.cttsCounts && !tr.fragments.length && window.start <= 0 && (window.end <= window.start || window.end >= totals.moovDuration))
259
- window = null;
260
- const stats = analyzeTimeline({
261
- segments: timelineSegments(tr, moovKeep),
262
- timescale: ts,
263
- editWindow: window,
264
- maxExpandedSamples: opts.maxExpandedSamples,
265
- });
266
- w.push(...stats.warnings);
267
- t.presentedFrames = stats.presentedFrames;
268
- t.minFps = stats.minFps;
269
- t.maxFps = stats.maxFps;
270
- t.isVfr = stats.isVfr;
271
- t.vfrRatio = stats.vfrRatio;
272
- t.distinctDeltas = stats.distinctDeltas;
273
- t.duplicateTimestamps = stats.duplicateTimestamps;
274
- t.nominalFps = stats.nominalFps;
275
- t.dominantFps = stats.dominantFps;
276
- t.timestampsQuantized = stats.timestampsQuantized;
277
- if (stats.presentedAvgFps)
278
- t.avgFps = stats.presentedAvgFps;
279
- if (t.duplicateTimestamps > 0)
280
- w.push(`${t.duplicateTimestamps} duplicate timestamps`);
281
- finishProvenance(t);
282
- if (stats.presentedAvgFps)
283
- t.provenance.avgFps = 'exact';
284
- return { result: t, enabled: tr.enabled, auxiliary, profile: stats.profile };
285
- }
286
- function finishProvenance(t) {
287
- const p = t.provenance;
288
- for (const k of ['codec', 'dimensions', 'rotation', 'frames', 'avgFps'])
289
- p[k] = 'header';
290
- if (t.durationSec !== null)
291
- p.durationSec = 'header';
292
- if (t.startSec !== null)
293
- p.startSec = 'header';
294
- if (t.presentedFrames !== null)
295
- p.presentedFrames = 'exact';
296
- if (t.isVfr !== null) {
297
- p.minFps = 'exact';
298
- p.maxFps = 'exact';
299
- p.isVfr = 'exact';
300
- p.duplicateTimestamps = 'exact';
301
- }
302
- if (t.nominalFps)
303
- p.nominalFps = 'exact';
304
- if (t.dominantFps)
305
- p.dominantFps = 'exact';
306
- }
307
- function pickPrimary(built) {
308
- let best = null;
309
- const score = (b) => ((b.result.frames ?? 0) > 1 ? (b.auxiliary ? 0.5 : b.enabled ? 2 : 1) : 0);
310
- for (const b of built) {
311
- if (!best || score(b) > score(best) || (score(b) === score(best) && (b.result.frames ?? 0) > (best.result.frames ?? 0)))
312
- best = b;
313
- }
314
- return best ? best.result.trackId : null;
315
- }
316
- const DEFAULT_STREAM_TIMEBASE = 90000;
317
- function formatDuration(movie, videoTracks) {
318
- const NONE = Number.MAX_SAFE_INTEGER;
319
- let start = NONE;
320
- let startText = NONE;
321
- let end = -NONE;
322
- let endText = -NONE;
323
- let dur = -NONE;
324
- let durText = -NONE;
325
- for (const tr of movie.tracks) {
326
- if (!tr.hasMdhd)
327
- continue;
328
- const main = videoTracks.has(tr) || tr.handler === 'soun';
329
- if (missingMandatoryAtoms(tr)) {
330
- const d = rescaleRound(tr.mdhdDuration, 1e6, DEFAULT_STREAM_TIMEBASE);
331
- if (main)
332
- dur = Math.max(dur, d);
333
- else
334
- durText = Math.max(durText, d);
335
- continue;
336
- }
337
- const totals = sampleTotals(tr);
338
- const timing = trackTiming(tr, movie, totals, editInfo(tr, movie.timescale));
339
- const d = rescaleRound(timing.durationTs, 1e6, tr.timescale);
340
- if (timing.startPts !== null) {
341
- const s = rescaleRound(timing.startPts, 1e6, tr.timescale);
342
- if (main) {
343
- start = Math.min(start, s);
344
- end = Math.max(end, s + d);
345
- }
346
- else {
347
- startText = Math.min(startText, s);
348
- endText = Math.max(endText, s + d);
349
- }
350
- }
351
- if (main)
352
- dur = Math.max(dur, d);
353
- else
354
- durText = Math.max(durText, d);
355
- }
356
- if (start === NONE || (start > startText && start - startText < 1e6))
357
- start = startText;
358
- if (end === -NONE || (end < endText && endText - end < 1e6))
359
- end = endText;
360
- if (dur === -NONE || (dur < durText && durText - dur < 1e6))
361
- dur = durText;
362
- if (start !== NONE && end !== -NONE && end >= start)
363
- dur = Math.max(dur, end - start);
364
- return dur > 0 ? dur / 1e6 : null;
365
- }
366
- function reportedIds(tracks) {
367
- const out = new Map();
368
- const seen = new Set();
369
- let next = tracks.reduce((m, t) => Math.max(m, t.id), 0) + 1;
370
- for (const t of tracks) {
371
- if (seen.has(t.id))
372
- out.set(t, next++);
373
- else {
374
- seen.add(t.id);
375
- out.set(t, t.id);
376
- }
377
- }
378
- return out;
379
- }
7
+ export { MOOF_HARD_CAP, MOOV_HARD_CAP, MOOV_SUSPECT_CAP } from "./build.js";
380
8
  export async function analyzeIsoBmffDetailed(reader, opts) {
381
9
  const movie = newMovie(reader.size);
382
10
  const fail = (e) => {
383
11
  if (isAbort(e, reader))
384
12
  throw e;
385
13
  const message = e instanceof Error ? e.message : String(e);
386
- const analysis = emptyAnalysis(containerOf(movie), e instanceof IsoBmffError ? message : `corrupt file: ${message}`);
14
+ const analysis = emptyAnalysis(containerOf(movie), e?.errorCode ? message : `corrupt file: ${message}`);
15
+ analysis.errorCode = whichErrorCode(e);
16
+ analysis.layout = layoutOf(movie);
387
17
  analysis.brand = movie.brand;
388
18
  analysis.warnings = movie.warnings;
389
19
  return { analysis, movie };
@@ -403,13 +33,14 @@ export async function analyzeIsoBmffDetailed(reader, opts) {
403
33
  if ([...ids].some(([t, id]) => t.id !== id))
404
34
  movie.warnings.push('duplicate track_ID values');
405
35
  const videos = movie.tracks.filter((t) => isVideoTrack(t, movie));
406
- const built = videos.map((t) => buildVideoTrack(t, movie, opts, { reportedId: ids.get(t) ?? t.id, ffprobeFails }));
36
+ const built = videos.map((t) => buildVideoTrack(t, movie, opts, { reportedId: ids.get(t) ?? t.id, ffprobeFails }, analyzeTimeline));
407
37
  for (const b of built)
408
38
  b.result.rateProfile = await rateProfileOf(b.profile ?? null);
409
39
  analysis.videoTracks = built.map((b) => b.result);
410
40
  analysis.audioTrackCount = movie.tracks.filter((t) => t.handler === 'soun').length;
411
41
  analysis.primaryTrackId = pickPrimary(built);
412
42
  analysis.formatDurationSec = formatDuration(movie, new Set(videos));
43
+ analysis.layout = layoutOf(movie);
413
44
  return { analysis, movie };
414
45
  }
415
46
  catch (e) {
@@ -1,3 +1,4 @@
1
+ export const TO_MEDIA_END = Number.MAX_SAFE_INTEGER;
1
2
  export const IDENTITY_MATRIX = [65536, 0, 0, 0, 65536, 0, 0, 0, 1 << 30];
2
3
  export function newMovie(fileSize) {
3
4
  return {
@@ -13,7 +14,6 @@ export function newMovie(fileSize) {
13
14
  chapterTrackIds: new Set(),
14
15
  hasMvex: false,
15
16
  trex: new Map(),
16
- mehdDuration: null,
17
17
  moofCount: 0,
18
18
  topLevel: [],
19
19
  truncated: false,
@@ -77,7 +77,7 @@ export function parseTrun(dv, p, end, tfhd, dataStartDefault, fileSize, warnings
77
77
  const overrun = record > 0 && count < declared;
78
78
  const hasDuration = (flags & TRUN_SAMPLE_DURATION) !== 0;
79
79
  if (!hasDuration && tfhd.defaultDuration === null && count > 0) {
80
- throw new IsoBmffError(`corrupt file: no sample duration for track ${tfhd.trackId}`);
80
+ throw new IsoBmffError('corrupt', `corrupt file: no sample duration for track ${tfhd.trackId}`);
81
81
  }
82
82
  const defaultDuration = tfhd.defaultDuration ?? 0;
83
83
  const durations = hasDuration ? new Uint32Array(count) : null;
@@ -196,7 +196,7 @@ export function parseSidx(dv, box, fileStart) {
196
196
  const p = box.start + box.headerSize;
197
197
  const v1 = p < box.end && dv.getUint8(p) === 1;
198
198
  if (p + (v1 ? 32 : 24) > box.end)
199
- throw new IsoBmffError(`corrupt file: box 'sidx' at ${fileStart} is truncated`);
199
+ throw new IsoBmffError('corrupt', `corrupt file: box 'sidx' at ${fileStart} is truncated`);
200
200
  let o = p + 12;
201
201
  const earliestPts = v1 ? boxU64(dv, o, 'sidx', fileStart) : dv.getUint32(o);
202
202
  const firstOffset = v1 ? boxU64(dv, o + 8, 'sidx', fileStart) : dv.getUint32(o + 4);
@@ -1,6 +1,6 @@
1
1
  import { fourcc } from "../reader.js";
2
2
  import { boundEntries, boxI64, boxU64, childBoxes, IsoBmffError, readBoxHeader } from "./boxes.js";
3
- import { newTrack } from "./model.js";
3
+ import { newTrack, TO_MEDIA_END } from "./model.js";
4
4
  export const MAX_STTS_DELTA = 4294487295;
5
5
  const UINT32_ALL_ONES = 0xffffffff;
6
6
  function children(dv, h, ctx, skip = 0) {
@@ -10,7 +10,7 @@ function* guard(gen, ctx, parent) {
10
10
  for (const c of gen) {
11
11
  if (c.truncated) {
12
12
  if (ctx.strict)
13
- throw new IsoBmffError('incomplete file: moov is truncated');
13
+ throw new IsoBmffError('truncated', 'incomplete file: moov is truncated');
14
14
  ctx.warnings.push(`'${c.type}' overruns '${parent}'`);
15
15
  }
16
16
  yield c;
@@ -105,7 +105,7 @@ export function parseCtts(dv, h, tr) {
105
105
  tr.cttsOffsets = Int32Array.from(offsets);
106
106
  tr.dtsShift = shift;
107
107
  }
108
- function parseElst(dv, h, tr, ctx) {
108
+ function parseElst(dv, h, tr, ctx, movie) {
109
109
  const p = payload(h);
110
110
  if (p + 8 > h.end)
111
111
  return;
@@ -125,8 +125,15 @@ function parseElst(dv, h, tr, ctx) {
125
125
  const at = ctx.base + h.start;
126
126
  for (let i = 0; i < n; i++) {
127
127
  const o = p + 8 + i * size;
128
- if (v === 1)
129
- tr.edits.push({ duration: boxU64(dv, o, 'elst', at), mediaTime: boxI64(dv, o + 8, 'elst', at), rate: dv.getInt32(o + 16) / 65536 });
128
+ if (v === 1) {
129
+ const mediaTime = boxI64(dv, o + 8, 'elst', at);
130
+ const unknown = dv.getBigInt64(o) === -1n;
131
+ if (unknown) {
132
+ tr.warnings.push(`unknown elst duration: ${mediaTime === -1 ? 'the empty edit is ignored' : 'the edit lasts to the end'}`);
133
+ movie.ffprobeReject ??= { moof: -1, reason: 'rejects this file: an elst duration of -1' };
134
+ }
135
+ tr.edits.push({ duration: !unknown ? boxU64(dv, o, 'elst', at) : mediaTime === -1 ? 0 : TO_MEDIA_END, mediaTime, rate: dv.getInt32(o + 16) / 65536 });
136
+ }
130
137
  else
131
138
  tr.edits.push({ duration: dv.getUint32(o), mediaTime: dv.getInt32(o + 4), rate: dv.getInt32(o + 8) / 65536 });
132
139
  }
@@ -287,7 +294,7 @@ function parseTrak(dv, h, movie, ctx) {
287
294
  else if (c.type === 'edts') {
288
295
  for (const e of children(dv, c, ctx))
289
296
  if (e.type === 'elst')
290
- parseElst(dv, e, tr, ctx);
297
+ parseElst(dv, e, tr, ctx, movie);
291
298
  }
292
299
  else if (c.type === 'mdia') {
293
300
  parseMdia(dv, c, tr, ctx);
@@ -330,12 +337,10 @@ export function parseMoov(dv, h, movie, strict, base = 0) {
330
337
  const q = payload(m);
331
338
  if (m.type === 'trex' && q + 24 <= m.end)
332
339
  movie.trex.set(dv.getUint32(q + 4), { duration: dv.getUint32(q + 12), size: dv.getUint32(q + 16) });
333
- else if (m.type === 'mehd' && q + 8 <= m.end)
334
- movie.mehdDuration = dv.getUint8(q) === 1 && q + 12 <= m.end ? boxU64(dv, q + 4, 'mehd', base + m.start) : dv.getUint32(q + 4);
335
340
  }
336
341
  break;
337
342
  case 'cmov':
338
- throw new IsoBmffError('unsupported: compressed movie header (cmov)');
343
+ throw new IsoBmffError('unsupported', 'unsupported: compressed movie header (cmov)');
339
344
  }
340
345
  }
341
346
  }
@@ -1,4 +1,5 @@
1
1
  import { INT_MAX, reduceRational, toFfprobeString } from "../rational.js";
2
+ import { TO_MEDIA_END } from "./model.js";
2
3
  export function rescaleRound(a, b, c) {
3
4
  if (!(c > 0))
4
5
  return 0;
@@ -62,13 +63,14 @@ export function missingMandatoryAtoms(tr) {
62
63
  const samples = tr.sampleCount ?? 0;
63
64
  return (chunks > 0 && (!stts || declared(tr.stsc) === 0 || (tr.sampleSize === 0 && samples === 0))) || (samples > 0 && chunks === 0);
64
65
  }
65
- export function editInfo(tr, movieTimescale) {
66
+ export function editInfo(tr, movieTimescale, totals) {
66
67
  let emptyBefore = 0;
67
68
  let editSum = 0;
68
69
  let mediaEdits = 0;
69
70
  let firstMedia = null;
71
+ const mediaEnd = (tr.cttsOffsets?.[0] ?? 0) + totals.moovDuration;
70
72
  for (const e of tr.edits) {
71
- const d = rescaleRound(e.duration, tr.timescale, movieTimescale);
73
+ const d = e.duration === TO_MEDIA_END ? Math.max(0, mediaEnd - e.mediaTime) : rescaleRound(e.duration, tr.timescale, movieTimescale);
72
74
  editSum += d;
73
75
  if (e.mediaTime === -1) {
74
76
  if (!firstMedia)
@@ -146,7 +146,7 @@ export function readString(b, p, n) {
146
146
  let e = p;
147
147
  while (e < p + n && b[e] !== 0)
148
148
  e++;
149
- return utf8.decode(b.subarray(p, e));
149
+ return utf8.decode(b.subarray(p, e)).replace(/[^\x20-\x7e\xa0-\uffff]/g, '?');
150
150
  }
151
151
  export function* children(b, start, end) {
152
152
  let p = start;
@@ -1,10 +1,12 @@
1
1
  import { childUint, children, ID_ATTACHED_FILE, ID_ATTACHMENTS, ID_CLUSTER, ID_CODEC_DELAY, ID_CODEC_ID, ID_CODEC_PRIVATE, ID_CONTENT_ENCODINGS, ID_CRC32, ID_CUE_CLUSTER_POSITION, ID_CUE_POINT, ID_CUE_TRACK_POSITIONS, ID_CUES, ID_DEFAULT_DURATION, ID_DOC_TYPE, ID_DURATION, ID_EBML, ID_FILE_MEDIA_TYPE, ID_FRAME_RATE, ID_INFO, ID_PIXEL_HEIGHT, ID_PIXEL_WIDTH, ID_SEEK, ID_SEEK_HEAD, ID_SEEK_ID, ID_SEEK_POSITION, ID_SEGMENT, ID_SIMPLE_TAG, ID_TAG, ID_TAG_NAME, ID_TAG_STRING, ID_TAG_TRACK_UID, ID_TAGS, ID_TARGETS, ID_TIMESTAMP_SCALE, ID_TRACK_ENTRY, ID_TRACK_NUMBER, ID_TRACK_TIMESTAMP_SCALE, ID_TRACK_TYPE, ID_TRACK_UID, ID_TRACKS, ID_VIDEO, ID_VOID, LEVEL1_IDS, readFloat, readString, readUintBig, UNKNOWN_SIZE, vintLength, } from "./ebml.js";
2
+ import { withErrorCode } from "../reader.js";
2
3
  import { findLevel1Forward } from "./source.js";
3
4
  export const HEADER_MAX_READS = 5;
4
5
  export const HEADER_MAX_BYTES = 1024 * 1024;
5
6
  export const MAX_DEPTH = 16;
6
7
  const SPARSE_CHILD_MAX = 64 * 1024;
7
8
  const CODEC_PRIVATE_MAX = 4096;
9
+ const ID_FLAG_ENABLED = 0xb9;
8
10
  const SEGMENT_SEARCH_HOPS = 16;
9
11
  const SKIPPED_NAMES = new Map([
10
12
  [ID_SEEK_HEAD, 'SeekHead'],
@@ -17,7 +19,7 @@ const CHILDREN = new Map([
17
19
  [ID_SEEK, [ID_SEEK_ID, ID_SEEK_POSITION]],
18
20
  [ID_INFO, [ID_TIMESTAMP_SCALE, ID_DURATION]],
19
21
  [ID_TRACKS, [ID_TRACK_ENTRY]],
20
- [ID_TRACK_ENTRY, [ID_TRACK_NUMBER, ID_TRACK_UID, ID_TRACK_TYPE, ID_CODEC_ID, ID_CODEC_PRIVATE, ID_CONTENT_ENCODINGS, ID_DEFAULT_DURATION, ID_TRACK_TIMESTAMP_SCALE, ID_CODEC_DELAY, ID_VIDEO]],
22
+ [ID_TRACK_ENTRY, [ID_TRACK_NUMBER, ID_TRACK_UID, ID_TRACK_TYPE, ID_FLAG_ENABLED, ID_CODEC_ID, ID_CODEC_PRIVATE, ID_CONTENT_ENCODINGS, ID_DEFAULT_DURATION, ID_TRACK_TIMESTAMP_SCALE, ID_CODEC_DELAY, ID_VIDEO]],
21
23
  [ID_VIDEO, [ID_PIXEL_WIDTH, ID_PIXEL_HEIGHT, ID_FRAME_RATE]],
22
24
  [ID_TAGS, [ID_TAG]],
23
25
  [ID_TAG, [ID_TARGETS, ID_SIMPLE_TAG]],
@@ -111,6 +113,9 @@ export function parseMaster(h, b, start, end, parent, cues = null, entry = null)
111
113
  case ID_TRACK_TYPE:
112
114
  t.type = uint();
113
115
  break;
116
+ case ID_FLAG_ENABLED:
117
+ t.disabled = !uint();
118
+ break;
114
119
  case ID_CODEC_ID:
115
120
  t.codecId = readString(b, p, n);
116
121
  break;
@@ -294,7 +299,7 @@ export async function readMatroskaHeader(src) {
294
299
  };
295
300
  const ebml = await src.header(0);
296
301
  if (!ebml || ebml.id !== ID_EBML || ebml.size === UNKNOWN_SIZE || ebml.dataStart + ebml.size > src.size)
297
- throw new Error('invalid EBML header');
302
+ throw withErrorCode('corrupt', 'invalid EBML header');
298
303
  await src.load(ebml.dataStart, Math.min(ebml.size, 4096));
299
304
  const s0 = ebml.dataStart - src.base;
300
305
  for (const c of children(src.buf, s0, s0 + ebml.size))
@@ -305,7 +310,7 @@ export async function readMatroskaHeader(src) {
305
310
  if (!seg) {
306
311
  const q = await findLevel1Forward(src, ebml.dataStart + ebml.size, src.size, src.size, { canLoad });
307
312
  if (q < 0)
308
- throw new Error('no Segment element');
313
+ throw withErrorCode('no-header', 'no Segment element');
309
314
  h.warnings.push(`no valid Segment header: reading level-1 elements from ${q}`);
310
315
  h.segmentExtended = true;
311
316
  seg = { id: ID_SEGMENT, pos: q, size: UNKNOWN_SIZE, dataStart: q };
@@ -375,6 +380,6 @@ export async function readMatroskaHeader(src) {
375
380
  if (!seen.ids.has(ID_INFO))
376
381
  h.warnings.push('no Info element: TimestampScale 1000000 assumed');
377
382
  if (!h.tracks.length)
378
- throw new Error(seen.ids.has(ID_TRACKS) ? 'no complete TrackEntry in Tracks' : 'no Tracks element found');
383
+ throw seen.ids.has(ID_TRACKS) ? withErrorCode('truncated', 'no complete TrackEntry in Tracks') : withErrorCode('no-header', 'no Tracks element found');
379
384
  return h;
380
385
  }
@@ -44,7 +44,7 @@ export function declaredRate(defaultDuration) {
44
44
  }
45
45
  export function mergeRunStats(timescale, runs) {
46
46
  const hist = new Map();
47
- const out = { minFps: null, maxFps: null, isVfr: null, vfrRatio: 0, distinctDeltas: 0, duplicateTimestamps: 0, timestampsQuantized: false, medianDelta: null };
47
+ const out = { minFps: null, maxFps: null, isVfr: null, vfrRatio: 0, distinctDeltas: 0, duplicateTimestamps: 0, timestampsQuantized: false, medianDelta: null, medianFrameInterval: null };
48
48
  let total = 0;
49
49
  for (const r of runs) {
50
50
  out.duplicateTimestamps += r.duplicateTimestamps;
@@ -65,6 +65,8 @@ export function mergeRunStats(timescale, runs) {
65
65
  if (Math.abs(k - median) > tol)
66
66
  off += hist.get(k) ?? 0;
67
67
  out.medianDelta = median;
68
+ const rate = rateFromTicks(timescale, median);
69
+ out.medianFrameInterval = rate && { num: rate.den, den: rate.num, value: rate.den / rate.num };
68
70
  out.minFps = rateFromTicks(timescale, keys[keys.length - 1]);
69
71
  out.maxFps = rateFromTicks(timescale, keys[0]);
70
72
  out.isVfr = total >= 2 && off > 0;
@@ -80,7 +82,8 @@ function fillRates(r, s, prov) {
80
82
  r.distinctDeltas = s.distinctDeltas;
81
83
  r.duplicateTimestamps = s.duplicateTimestamps;
82
84
  r.timestampsQuantized = s.timestampsQuantized;
83
- for (const f of ['minFps', 'maxFps', 'isVfr', 'duplicateTimestamps'])
85
+ r.medianFrameInterval = s.medianFrameInterval;
86
+ for (const f of ['minFps', 'maxFps', 'isVfr', 'duplicateTimestamps', 'medianFrameInterval'])
84
87
  r.provenance[f] = prov;
85
88
  }
86
89
  function headerTrack(h, t) {
@@ -89,6 +92,7 @@ function headerTrack(h, t) {
89
92
  r.codecName = codecNameOf(t.codecId);
90
93
  r.width = t.width;
91
94
  r.height = t.height;
95
+ r.enabled = !t.disabled;
92
96
  r.timescale = 1e9 / (Math.trunc(h.timestampScale * t.trackTimestampScale) || h.timestampScale);
93
97
  r.provenance.codec = r.provenance.dimensions = 'header';
94
98
  const d = declaredRate(t.defaultDuration);
@@ -260,7 +264,7 @@ function pickEvenly(list, k) {
260
264
  return [...new Set(Array.from({ length: k }, (_, i) => list[Math.round((i * (list.length - 1)) / (k - 1))]))];
261
265
  }
262
266
  async function tailScan(src, h, video, first, opts) {
263
- let size = Math.max(opts.readAhead.tail, 4096);
267
+ let size = Math.max(opts.readAhead.tail || 0, 4096);
264
268
  let hi = h.segmentEnd;
265
269
  let maxAnyTicks = -Infinity;
266
270
  for (;;) {