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
@@ -0,0 +1,398 @@
1
+ import { emptyTrack } from "../result.js";
2
+ import { toFfprobeString } from "../rational.js";
3
+ import { IsoBmffError, readBoxHeader } from "./boxes.js";
4
+ import { codecNameFor, isVideoFourcc } from "./codecs.js";
5
+ import { parseMoof, parseSidx } from "./moof.js";
6
+ import { parseMoov } from "./moov.js";
7
+ import { mfraMoofOffsets, newPrefetch, prefetchNext, prefetched } from "./prefetch.js";
8
+ import { averageRates, displayRotation, editInfo, ffprobeStage1Rate, missingMandatoryAtoms, moovSamplesInFile, presentationWindow, rescaleRound, sampleTotals, timelineSegments, trackTiming, } from "./track.js";
9
+ export const MOOV_SUSPECT_CAP = 16 * 1024 * 1024;
10
+ export const MOOV_HARD_CAP = 256 * 1024 * 1024;
11
+ export const MOOF_HARD_CAP = 64 * 1024 * 1024;
12
+ const MAX_TOP_LEVEL_BOXES = 4_000_000;
13
+ const SMALL_MDAT = 64 * 1024;
14
+ const HEADER_PEEK = 16;
15
+ export function isAbort(e, reader) {
16
+ return reader.signal?.aborted === true || e?.name === 'AbortError';
17
+ }
18
+ export function containerOf(movie) {
19
+ return movie.brand === 'qt ' || (!movie.hasFtyp && movie.topLevel.length > 0) ? 'mov' : 'mp4';
20
+ }
21
+ export async function readMoov(reader, h, movie) {
22
+ const size = reader.size;
23
+ const beyond = h.start + h.size > size;
24
+ if (!beyond && h.size > MOOV_HARD_CAP)
25
+ throw new IsoBmffError('too-large', 'unsupported: moov larger than 256 MiB');
26
+ const len = beyond ? Math.min(size - h.start, MOOV_SUSPECT_CAP) : h.size;
27
+ const dv = await reader.view(h.start, len, 0);
28
+ const mh = readBoxHeader(dv, 0, dv.byteLength);
29
+ if (!mh)
30
+ throw new IsoBmffError('truncated', 'incomplete file: moov is truncated');
31
+ parseMoov(dv, mh, movie, beyond, h.start);
32
+ if (beyond)
33
+ movie.warnings.push('moov size exceeds the file; parsed what is there');
34
+ }
35
+ export async function walk(reader, opts, movie) {
36
+ const size = reader.size;
37
+ let pos = 0;
38
+ let ahead = opts.readAhead.head;
39
+ let sawMoov = false;
40
+ let boxes = 0;
41
+ let afterMdat = false;
42
+ const pf = newPrefetch();
43
+ const look = async (at, need, readAhead) => prefetched(pf, at, need) ?? reader.view(at, need, readAhead);
44
+ while (pos + 8 <= size) {
45
+ reader.checkAbort();
46
+ if (++boxes > MAX_TOP_LEVEL_BOXES) {
47
+ movie.warnings.push('too many top-level boxes');
48
+ break;
49
+ }
50
+ const tailRead = afterMdat && !sawMoov && size - pos <= MOOV_SUSPECT_CAP ? size - pos : ahead;
51
+ const hv = await look(pos, Math.min(HEADER_PEEK, size - pos), tailRead);
52
+ let rel;
53
+ try {
54
+ rel = readBoxHeader(hv, 0, size - pos, pos);
55
+ }
56
+ catch (e) {
57
+ if (!sawMoov)
58
+ throw e;
59
+ movie.warnings.push(`ignored trailing data at ${pos}: ${e.message}`);
60
+ break;
61
+ }
62
+ if (!rel)
63
+ break;
64
+ const h = { ...rel, start: pos, end: Math.min(pos + rel.size, size) };
65
+ const boxEnd = pos + rel.size;
66
+ const trailer = rel.truncated && sawMoov && h.type !== 'mdat' && h.type !== 'moof';
67
+ if (rel.truncated) {
68
+ movie.truncated = true;
69
+ movie.warnings.push(trailer ? `ignored trailing data at ${pos}: not a box` : `'${h.type}' at ${pos} is truncated`);
70
+ }
71
+ const top = { type: h.type, start: pos, end: h.end, sidx: null };
72
+ movie.topLevel.push(top);
73
+ if (trailer)
74
+ break;
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
+ movie.moov = [pos, rel.size];
91
+ await readMoov(reader, h, movie);
92
+ sawMoov = true;
93
+ }
94
+ }
95
+ else if (h.type === 'moof') {
96
+ if (!sawMoov)
97
+ throw new IsoBmffError('corrupt', 'corrupt file: movie fragment (moof) before moov');
98
+ if (h.size > MOOF_HARD_CAP)
99
+ throw new IsoBmffError('too-large', `corrupt file: moof larger than 64 MiB at ${pos}`);
100
+ const want = Math.min(h.size + HEADER_PEEK, size - pos);
101
+ const dv = await look(pos, want, 0);
102
+ const mh = readBoxHeader(dv, 0, Math.min(h.size, dv.byteLength));
103
+ if (mh)
104
+ parseMoof(dv, mh, pos, movie.moofCount, movie, opts.maxExpandedSamples);
105
+ movie.moofCount++;
106
+ let gap = 0;
107
+ try {
108
+ const next = readBoxHeader(dv, h.size, dv.byteLength);
109
+ if (next && next.type === 'mdat')
110
+ gap = next.size;
111
+ }
112
+ catch {
113
+ gap = 0;
114
+ }
115
+ ahead = gap <= SMALL_MDAT ? opts.readAhead.fragment : Math.min(65536, Math.max(1024, 2 * h.size + 64));
116
+ if (gap > SMALL_MDAT) {
117
+ if (!pf.hints.length && !pf.mfraTried && (size - pos) / (h.size + gap) > 16) {
118
+ pf.mfraTried = true;
119
+ pf.hints = await mfraMoofOffsets(reader);
120
+ }
121
+ if (pf.next < pf.hints.length && !prefetched(pf, pos + h.size + gap, HEADER_PEEK))
122
+ await prefetchNext(reader, pf, pos, ahead);
123
+ }
124
+ if (opts.onProgress && movie.moofCount % 64 === 0)
125
+ opts.onProgress({ phase: 'fragments', done: pos, total: size });
126
+ }
127
+ else if (h.type === 'sidx' && !rel.truncated) {
128
+ const dv = await look(pos, h.size, 0);
129
+ const sh = readBoxHeader(dv, 0, dv.byteLength);
130
+ if (sh)
131
+ top.sidx = parseSidx(dv, sh, pos);
132
+ if (top.sidx && top.sidx.subsegments.length > 1 && !pf.hints.length)
133
+ pf.hints = top.sidx.subsegments.map(([o]) => o);
134
+ }
135
+ else if (h.type === 'mfra') {
136
+ break;
137
+ }
138
+ afterMdat = h.type === 'mdat';
139
+ pos = boxEnd;
140
+ }
141
+ if (!sawMoov)
142
+ throw new IsoBmffError('no-header', 'incomplete file: no moov');
143
+ }
144
+ export async function emulateFfprobeExtent(reader, movie) {
145
+ const size = reader.size;
146
+ const ids = new Set(movie.tracks.map((t) => t.id));
147
+ let foundMoov = false;
148
+ let foundMdat = false;
149
+ let complete = false;
150
+ let mfraSize = null;
151
+ let moofs = 0;
152
+ for (const b of movie.topLevel) {
153
+ if (b.type === 'moof')
154
+ moofs++;
155
+ else if (b.type === 'moov')
156
+ foundMoov = true;
157
+ else if (b.type === 'mdat' && b.end - b.start > 8)
158
+ foundMdat = true;
159
+ else if (b.type === 'sidx' && foundMoov && b.sidx && !b.sidx.hierarchical && ids.has(b.sidx.referenceId)) {
160
+ let whole = b.sidx.coverageEnd === size;
161
+ if (!whole && size >= 4) {
162
+ if (mfraSize === null)
163
+ mfraSize = (await reader.view(size - 4, 4, 0)).getUint32(0);
164
+ whole = b.sidx.coverageEnd === size - mfraSize;
165
+ }
166
+ if (whole && b.sidx.firstOffset === 0)
167
+ complete = true;
168
+ }
169
+ if (foundMoov && foundMdat && (complete || b.end === size)) {
170
+ movie.ffprobeMoofLimit = moofs;
171
+ return;
172
+ }
173
+ }
174
+ }
175
+ export function layoutOf(movie) {
176
+ let mdat = Infinity;
177
+ let moof = false;
178
+ let sidx = false;
179
+ for (const b of movie.topLevel) {
180
+ if (b.type === 'mdat')
181
+ mdat = Math.min(mdat, b.start);
182
+ moof ||= b.type === 'moof';
183
+ sidx ||= !moof && b.type === 'sidx';
184
+ }
185
+ const moov = movie.moov;
186
+ const moovAt = moov ? moov[0] : Infinity;
187
+ if (moovAt === mdat)
188
+ return null;
189
+ const kind = movie.hasMvex ? 'fragmented' : mdat < moovAt ? 'moov-at-end' : 'faststart';
190
+ return {
191
+ kind,
192
+ moovOffset: moov ? moovAt : null,
193
+ moovSize: moov ? moov[1] : null,
194
+ sidx,
195
+ emptyMoov: kind === 'fragmented' && movie.tracks.every((t) => !isVideoTrack(t, movie) || !t.sampleCount),
196
+ };
197
+ }
198
+ export function isVideoTrack(tr, movie) {
199
+ if (movie.chapterTrackIds.has(tr.id))
200
+ return false;
201
+ if (tr.handler === 'vide')
202
+ return true;
203
+ if (tr.handler === 'soun')
204
+ return false;
205
+ return isVideoFourcc(tr.originalFormat ?? tr.sampleEntry);
206
+ }
207
+ const STATS_FIELDS = ['presentedFrames', 'minFps', 'maxFps', 'medianFrameInterval', 'isVfr', 'vfrRatio', 'distinctDeltas', 'duplicateTimestamps', 'nominalFps', 'dominantFps', 'timestampsQuantized'];
208
+ export function buildVideoTrack(tr, movie, opts, ctx, timeline) {
209
+ const t = emptyTrack(ctx.reportedId);
210
+ const w = t.warnings;
211
+ if (ctx.reportedId !== tr.id)
212
+ w.push(`duplicate track_ID ${tr.id}: reported as ${ctx.reportedId}`);
213
+ w.push(...tr.warnings);
214
+ const auxiliary = tr.handler === 'auxv';
215
+ if (auxiliary)
216
+ w.push("auxiliary video track (handler 'auxv')");
217
+ const broken = missingMandatoryAtoms(tr);
218
+ if (broken)
219
+ w.push('missing mandatory sample tables: ffprobe ignores the samples and timing of this track');
220
+ const ts = tr.hasMdhd ? tr.timescale : movie.timescale;
221
+ if (!tr.hasMdhd) {
222
+ w.push('no mdhd: using the movie timescale');
223
+ tr.timescale = ts;
224
+ }
225
+ t.fourccOrCodecId = tr.sampleEntry;
226
+ t.codecName = codecNameFor(tr.originalFormat ?? tr.sampleEntry, tr.esdsObjectType, tr.hasHvcc);
227
+ if (tr.originalFormat)
228
+ w.push(`encrypted: ${tr.sampleEntry} of ${tr.originalFormat}`);
229
+ if (tr.stsdCount > 1)
230
+ w.push(`${tr.stsdCount} sample descriptions, first used`);
231
+ t.width = tr.width;
232
+ t.height = tr.height;
233
+ t.rotation = displayRotation(tr.matrix, movie.matrix);
234
+ t.enabled = tr.enabled;
235
+ t.timescale = ts;
236
+ const totals = sampleTotals(tr);
237
+ const truncated = movie.truncated;
238
+ let presentFragments = 0;
239
+ for (const f of tr.fragments)
240
+ presentFragments += f.present;
241
+ const total = totals.moovSamples + totals.fragmentSamples;
242
+ const frames = totals.moovSamples + presentFragments;
243
+ t.frames = frames;
244
+ t.isStill = frames <= 1;
245
+ if (totals.moovSamples > 0 && tr.fragments.length > 0) {
246
+ w.push(`hybrid fragmented file: moov holds ${totals.moovSamples} of ${total} samples`);
247
+ }
248
+ if (frames < total)
249
+ w.push(`truncated: ${total - frames} of ${total} samples missing`);
250
+ const rates = averageRates(tr, movie);
251
+ t.avgFps = rates.avg;
252
+ t.avgFpsFfprobe = ctx.ffprobeFails ? null : rates.ffprobe;
253
+ if (t.avgFpsFfprobe !== null && t.avgFpsFfprobe !== toFfprobeString(rates.avg)) {
254
+ w.push(`ffprobe reads part of the fragments: avg_frame_rate ${t.avgFpsFfprobe}`);
255
+ }
256
+ if (rates.ffprobe === null && !ctx.ffprobeFails && total > 0)
257
+ w.push('avg_frame_rate of ffprobe is not reproduced: its header values are incomplete or overrun a box');
258
+ const r = !timeline || ctx.ffprobeFails || broken ? null : ffprobeStage1Rate(tr, movie, totals.moovSamples, opts.maxExpandedSamples, w);
259
+ t.rFpsFfprobe = r ? toFfprobeString(r) : null;
260
+ const edits = editInfo(tr, movie.timescale, totals);
261
+ if (!broken) {
262
+ const timing = trackTiming(tr, movie, totals, edits);
263
+ t.durationSec = timing.durationTs / ts;
264
+ t.startSec = timing.startPts === null ? null : timing.startPts / ts;
265
+ }
266
+ let moovKeep = totals.moovSamples;
267
+ if (tr.sampleCount !== null && tr.sampleCount < moovKeep) {
268
+ w.push(`stts describes ${moovKeep} samples but the sample size table holds ${tr.sampleCount}`);
269
+ moovKeep = tr.sampleCount;
270
+ }
271
+ if (broken || !timeline) {
272
+ t.presentedFrames = 0;
273
+ finishProvenance(t);
274
+ return { result: t, enabled: tr.enabled, auxiliary };
275
+ }
276
+ if (truncated && moovKeep > 0) {
277
+ const inFile = moovSamplesInFile(tr, movie.fileSize, totals.moovSamples);
278
+ if (inFile && inFile.complete < moovKeep) {
279
+ w.push(`truncated: ${inFile.complete} of ${moovKeep} samples complete`);
280
+ moovKeep = inFile.complete;
281
+ }
282
+ }
283
+ let window = presentationWindow(tr, edits, w);
284
+ if (window && !tr.cttsCounts && !tr.fragments.length && window.start <= 0 && (window.end <= window.start || window.end >= totals.moovDuration))
285
+ window = null;
286
+ const stats = timeline({
287
+ segments: timelineSegments(tr, moovKeep),
288
+ timescale: ts,
289
+ editWindow: window,
290
+ maxExpandedSamples: opts.maxExpandedSamples,
291
+ });
292
+ w.push(...stats.warnings);
293
+ for (const k of STATS_FIELDS)
294
+ t[k] = stats[k];
295
+ if (stats.presentedAvgFps)
296
+ t.avgFps = stats.presentedAvgFps;
297
+ if (t.duplicateTimestamps > 0)
298
+ w.push(`${t.duplicateTimestamps} duplicate timestamps`);
299
+ finishProvenance(t);
300
+ if (stats.presentedAvgFps)
301
+ t.provenance.avgFps = 'exact';
302
+ return { result: t, enabled: tr.enabled, auxiliary, profile: stats.profile };
303
+ }
304
+ export function finishProvenance(t) {
305
+ const p = t.provenance;
306
+ for (const k of ['codec', 'dimensions', 'rotation', 'frames', 'avgFps'])
307
+ p[k] = 'header';
308
+ if (t.durationSec !== null)
309
+ p.durationSec = 'header';
310
+ if (t.startSec !== null)
311
+ p.startSec = 'header';
312
+ if (t.presentedFrames !== null)
313
+ p.presentedFrames = 'exact';
314
+ if (t.isVfr !== null) {
315
+ p.minFps = 'exact';
316
+ p.maxFps = 'exact';
317
+ p.isVfr = 'exact';
318
+ p.duplicateTimestamps = 'exact';
319
+ p.medianFrameInterval = 'exact';
320
+ }
321
+ if (t.nominalFps)
322
+ p.nominalFps = 'exact';
323
+ if (t.dominantFps)
324
+ p.dominantFps = 'exact';
325
+ }
326
+ export function pickPrimary(built) {
327
+ let best = null;
328
+ const score = (b) => ((b.result.frames ?? 0) > 1 ? (b.auxiliary ? 0.5 : b.enabled ? 2 : 1) : 0);
329
+ for (const b of built) {
330
+ if (!best || score(b) > score(best) || (score(b) === score(best) && (b.result.frames ?? 0) > (best.result.frames ?? 0)))
331
+ best = b;
332
+ }
333
+ return best ? best.result.trackId : null;
334
+ }
335
+ const DEFAULT_STREAM_TIMEBASE = 90000;
336
+ export function formatDuration(movie, videoTracks) {
337
+ const NONE = Number.MAX_SAFE_INTEGER;
338
+ let start = NONE;
339
+ let startText = NONE;
340
+ let end = -NONE;
341
+ let endText = -NONE;
342
+ let dur = -NONE;
343
+ let durText = -NONE;
344
+ for (const tr of movie.tracks) {
345
+ if (!tr.hasMdhd)
346
+ continue;
347
+ const main = videoTracks.has(tr) || tr.handler === 'soun';
348
+ if (missingMandatoryAtoms(tr)) {
349
+ const d = rescaleRound(tr.mdhdDuration, 1e6, DEFAULT_STREAM_TIMEBASE);
350
+ if (main)
351
+ dur = Math.max(dur, d);
352
+ else
353
+ durText = Math.max(durText, d);
354
+ continue;
355
+ }
356
+ const totals = sampleTotals(tr);
357
+ const timing = trackTiming(tr, movie, totals, editInfo(tr, movie.timescale, totals));
358
+ const d = rescaleRound(timing.durationTs, 1e6, tr.timescale);
359
+ if (timing.startPts !== null) {
360
+ const s = rescaleRound(timing.startPts, 1e6, tr.timescale);
361
+ if (main) {
362
+ start = Math.min(start, s);
363
+ end = Math.max(end, s + d);
364
+ }
365
+ else {
366
+ startText = Math.min(startText, s);
367
+ endText = Math.max(endText, s + d);
368
+ }
369
+ }
370
+ if (main)
371
+ dur = Math.max(dur, d);
372
+ else
373
+ durText = Math.max(durText, d);
374
+ }
375
+ if (start === NONE || (start > startText && start - startText < 1e6))
376
+ start = startText;
377
+ if (end === -NONE || (end < endText && endText - end < 1e6))
378
+ end = endText;
379
+ if (dur === -NONE || (dur < durText && durText - dur < 1e6))
380
+ dur = durText;
381
+ if (start !== NONE && end !== -NONE && end >= start)
382
+ dur = Math.max(dur, end - start);
383
+ return dur > 0 ? dur / 1e6 : null;
384
+ }
385
+ export function reportedIds(tracks) {
386
+ const out = new Map();
387
+ const seen = new Set();
388
+ let next = tracks.reduce((m, t) => Math.max(m, t.id), 0) + 1;
389
+ for (const t of tracks) {
390
+ if (seen.has(t.id))
391
+ out.set(t, next++);
392
+ else {
393
+ seen.add(t.id);
394
+ out.set(t, t.id);
395
+ }
396
+ }
397
+ return out;
398
+ }