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.
- package/CHANGELOG.md +94 -0
- package/README.md +33 -15
- package/dist/api/codes.d.ts +6 -0
- package/dist/api/codes.js +88 -0
- package/dist/api/errors.js +1 -1
- package/dist/api/guards.d.ts +2 -0
- package/dist/api/guards.js +2 -1
- package/dist/check/index.js +12 -2
- 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/check/index.d.cts +60 -0
- package/dist/cjs/check.cjs +1 -0
- package/dist/cjs/chunks/build-CT5g6_bs.cjs +1 -0
- package/dist/cjs/chunks/compare-D7RVqlqf.cjs +1 -0
- package/dist/cjs/chunks/errors-0oBg1Ai6.cjs +1 -0
- package/dist/cjs/chunks/fallback-D9Ak2zFv.cjs +1 -0
- package/dist/cjs/chunks/guards-Dqpjtkdn.cjs +1 -0
- package/dist/cjs/chunks/inline-Bx-TQg8f.cjs +1 -0
- package/dist/cjs/chunks/isobmff-B_bb5rS5.cjs +1 -0
- package/dist/cjs/chunks/matroska-CLoGzfKA.cjs +1 -0
- package/dist/cjs/chunks/probe-DDLUfr23.cjs +1 -0
- package/dist/cjs/chunks/rational-Dq5Vza-Q.cjs +1 -0
- package/dist/cjs/chunks/reader-DNLR-_tm.cjs +1 -0
- package/dist/cjs/chunks/rfps-C-pyKnu8.cjs +1 -0
- package/dist/cjs/chunks/runtime-DtgT0xzU.cjs +1 -0
- package/dist/cjs/chunks/segments-i-hPN7H3.cjs +1 -0
- package/dist/cjs/chunks/sniff-CXwqOGwh.cjs +1 -0
- package/dist/cjs/chunks/stats-D68frkk2.cjs +1 -0
- package/dist/cjs/client/types.d.cts +96 -0
- package/dist/cjs/core/reader.d.cts +47 -0
- package/dist/cjs/core/types.d.cts +149 -0
- package/dist/cjs/engines/types.d.cts +69 -0
- package/dist/cjs/header/index.d.cts +25 -0
- package/dist/cjs/header/limits.d.cts +25 -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/client/compare.js +2 -2
- package/dist/client/create.d.ts +3 -0
- package/dist/client/create.js +8 -0
- package/dist/client/fallback.js +2 -2
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/inline.js +1 -1
- package/dist/client/lazy.js +1 -1
- package/dist/client/probe-client.js +4 -0
- package/dist/client/probe.js +23 -22
- package/dist/client/route.js +20 -0
- package/dist/client/split.js +9 -0
- package/dist/client/stub.js +2 -2
- package/dist/client/types.d.ts +1 -0
- package/dist/core/analyze.js +4 -78
- package/dist/core/dispatch.js +65 -0
- package/dist/core/isobmff/boxes.js +6 -8
- package/dist/core/isobmff/build.js +398 -0
- package/dist/core/isobmff/header.js +654 -0
- package/dist/core/isobmff/index.js +9 -378
- package/dist/core/isobmff/model.js +1 -1
- package/dist/core/isobmff/moof.js +2 -2
- package/dist/core/isobmff/moov.js +14 -9
- package/dist/core/isobmff/track.js +4 -2
- package/dist/core/matroska/ebml.js +1 -1
- package/dist/core/matroska/header.js +9 -4
- package/dist/core/matroska/index.js +7 -3
- package/dist/core/reader.d.ts +3 -12
- package/dist/core/reader.js +21 -32
- package/dist/core/result.js +1 -0
- package/dist/core/stats.js +3 -0
- package/dist/core/types.d.ts +15 -1
- package/dist/engines/common.js +8 -0
- package/dist/engines/mediabunny.js +17 -4
- package/dist/engines/mp4box.js +2 -1
- package/dist/header/index.d.ts +25 -0
- package/dist/header/index.js +146 -0
- package/dist/header/limits.d.ts +25 -0
- package/dist/header/limits.js +140 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plugins/own.js +5 -2
- package/dist/presets/index.d.ts +1 -0
- package/dist/presets/sources.json +8 -0
- package/dist/probe.js +2 -1
- package/dist/react/store.js +1 -1
- package/dist/standalone/own-isobmff.worker.js +1 -0
- package/dist/standalone/own-matroska.worker.js +1 -0
- 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/own-isobmff.worker.js +4 -0
- package/dist/worker/own-matroska.worker.js +4 -0
- package/dist/worker/runtime.d.ts +2 -3
- package/dist/worker/runtime.js +2 -97
- package/dist/worker/serve.d.ts +3 -0
- package/dist/worker/serve.js +97 -0
- package/dist/worker/single.js +14 -0
- package/package.json +85 -2
- package/dist/client/inline.d.ts +0 -19
- package/dist/client/pool.d.ts +0 -29
- package/dist/client/probe.d.ts +0 -29
|
@@ -1,389 +1,19 @@
|
|
|
1
|
-
import { emptyAnalysis
|
|
1
|
+
import { emptyAnalysis } from "../result.js";
|
|
2
2
|
import { analyzeTimeline, rateProfileOf } from "../stats.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (;;) {
|