video-fps-probe 0.3.0 → 0.4.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/README.md +19 -11
- package/dist/analyze.js +10 -1
- package/dist/api/codes.d.ts +6 -0
- package/dist/api/codes.js +90 -0
- package/dist/aspect/index.d.ts +13 -0
- package/dist/aspect/index.js +180 -0
- package/dist/aspect/rational.js +61 -0
- package/dist/cjs/analyze.d.cts +9 -0
- package/dist/cjs/api/agreement.d.cts +18 -0
- package/dist/cjs/api/codes.d.cts +6 -0
- package/dist/cjs/api/errors.d.cts +12 -0
- package/dist/cjs/api/guards.d.cts +4 -0
- package/dist/cjs/api/summary.d.cts +22 -0
- package/dist/cjs/aspect/index.d.cts +13 -0
- package/dist/cjs/aspect.cjs +1 -0
- package/dist/cjs/check/index.d.cts +60 -0
- package/dist/cjs/check.cjs +1 -0
- package/dist/cjs/chunks/boxes-B0T2nt3e.cjs +1 -0
- package/dist/cjs/chunks/build-DhETwJLO.cjs +1 -0
- package/dist/cjs/chunks/color-xqJHSfhZ.cjs +1 -0
- package/dist/cjs/chunks/compare-B_HKNwqg.cjs +1 -0
- package/dist/cjs/chunks/config-CSbXzEqr.cjs +1 -0
- package/dist/cjs/chunks/errors-0oBg1Ai6.cjs +1 -0
- package/dist/cjs/chunks/fallback-CyPIR3RM.cjs +1 -0
- package/dist/cjs/chunks/guards-DbQNcAZS.cjs +1 -0
- package/dist/cjs/chunks/inline-Bx-TQg8f.cjs +1 -0
- package/dist/cjs/chunks/isobmff-4RrRf9N-.cjs +1 -0
- package/dist/cjs/chunks/matroska-BhgWtuPU.cjs +1 -0
- package/dist/cjs/chunks/metadata-ECdzNLkE.cjs +1 -0
- package/dist/cjs/chunks/options-B_vLHxHd.cjs +1 -0
- package/dist/cjs/chunks/probe-Cn1DtL-C.cjs +1 -0
- package/dist/cjs/chunks/rational-Dq5Vza-Q.cjs +1 -0
- package/dist/cjs/chunks/reader-Dw11ozVU.cjs +1 -0
- package/dist/cjs/chunks/rfps-C-pyKnu8.cjs +1 -0
- package/dist/cjs/chunks/runtime-BgAlQQee.cjs +1 -0
- package/dist/cjs/chunks/segments-B1H6bDz-.cjs +1 -0
- package/dist/cjs/chunks/sniff-CXwqOGwh.cjs +1 -0
- package/dist/cjs/chunks/stats-DW7dVdFa.cjs +1 -0
- package/dist/cjs/client/types.d.cts +96 -0
- package/dist/cjs/color/index.d.cts +7 -0
- package/dist/cjs/color.cjs +1 -0
- package/dist/cjs/core/reader.d.cts +47 -0
- package/dist/cjs/core/types.d.cts +194 -0
- package/dist/cjs/engines/types.d.cts +69 -0
- package/dist/cjs/header/index.d.cts +28 -0
- package/dist/cjs/header/limits.d.cts +26 -0
- package/dist/cjs/header.cjs +1 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.d.cts +13 -0
- package/dist/cjs/plugins/types.d.cts +17 -0
- package/dist/cjs/presets/index.d.cts +19 -0
- package/dist/cjs/presets.cjs +1 -0
- package/dist/cjs/probe.d.cts +3 -0
- package/dist/cjs/url/index.d.cts +47 -0
- package/dist/cjs/url.cjs +1 -0
- package/dist/cjs/worker/protocol.d.cts +47 -0
- package/dist/color/codec-string.js +121 -0
- package/dist/color/config.js +199 -0
- package/dist/color/index.d.ts +7 -0
- package/dist/color/index.js +139 -0
- package/dist/color/names.js +33 -0
- package/dist/color/sps.js +275 -0
- package/dist/core/dispatch.js +18 -29
- package/dist/core/isobmff/boxes.js +6 -5
- package/dist/core/isobmff/build.js +401 -0
- package/dist/core/isobmff/config.js +34 -0
- package/dist/core/isobmff/header.js +677 -0
- package/dist/core/isobmff/index.js +9 -381
- package/dist/core/isobmff/moof.js +2 -2
- package/dist/core/isobmff/moov.js +3 -3
- package/dist/core/matroska/header.js +10 -4
- package/dist/core/matroska/index.js +11 -5
- package/dist/core/options.js +1 -0
- package/dist/core/reader.d.ts +3 -0
- package/dist/core/reader.js +13 -7
- package/dist/core/result.js +1 -0
- package/dist/core/stats.js +3 -0
- package/dist/core/types.d.ts +60 -1
- package/dist/engines/common.js +2 -0
- package/dist/header/index.d.ts +28 -0
- package/dist/header/index.js +182 -0
- package/dist/header/limits.d.ts +26 -0
- package/dist/header/limits.js +144 -0
- package/dist/header/metadata.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/standalone/own-isobmff.worker.js +1 -1
- package/dist/standalone/own-matroska.worker.js +1 -1
- package/dist/standalone/own.worker.js +1 -1
- package/dist/url/index.d.ts +47 -0
- package/dist/url/index.js +335 -0
- package/dist/worker/single.js +2 -1
- package/package.json +117 -2
|
@@ -1,392 +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 || 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
|
-
const trailer = rel.truncated && sawMoov && h.type !== 'mdat' && h.type !== 'moof';
|
|
70
|
-
if (rel.truncated) {
|
|
71
|
-
movie.truncated = true;
|
|
72
|
-
movie.warnings.push(trailer ? `ignored trailing data at ${pos}: not a box` : `'${h.type}' at ${pos} is truncated`);
|
|
73
|
-
}
|
|
74
|
-
const top = { type: h.type, start: pos, end: h.end, sidx: null };
|
|
75
|
-
movie.topLevel.push(top);
|
|
76
|
-
if (trailer)
|
|
77
|
-
break;
|
|
78
|
-
if (h.type === 'ftyp') {
|
|
79
|
-
const fv = await reader.view(pos, Math.min(h.size, 1024));
|
|
80
|
-
const p = h.headerSize;
|
|
81
|
-
if (p + 4 <= fv.byteLength) {
|
|
82
|
-
movie.hasFtyp = true;
|
|
83
|
-
movie.brand = String.fromCharCode(fv.getUint8(p), fv.getUint8(p + 1), fv.getUint8(p + 2), fv.getUint8(p + 3));
|
|
84
|
-
for (let q = p + 8; q + 4 <= fv.byteLength; q += 4) {
|
|
85
|
-
movie.compatibleBrands.push(String.fromCharCode(fv.getUint8(q), fv.getUint8(q + 1), fv.getUint8(q + 2), fv.getUint8(q + 3)));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
else if (h.type === 'moov') {
|
|
90
|
-
if (sawMoov)
|
|
91
|
-
movie.warnings.push(`ignored a second moov at ${pos}`);
|
|
92
|
-
else {
|
|
93
|
-
await readMoov(reader, h, movie);
|
|
94
|
-
sawMoov = true;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
else if (h.type === 'moof') {
|
|
98
|
-
if (!sawMoov)
|
|
99
|
-
throw new IsoBmffError('corrupt file: movie fragment (moof) before moov');
|
|
100
|
-
if (h.size > MOOF_HARD_CAP)
|
|
101
|
-
throw new IsoBmffError(`corrupt file: moof larger than 64 MiB at ${pos}`);
|
|
102
|
-
const want = Math.min(h.size + HEADER_PEEK, size - pos);
|
|
103
|
-
const dv = await look(pos, want, 0);
|
|
104
|
-
const mh = readBoxHeader(dv, 0, Math.min(h.size, dv.byteLength));
|
|
105
|
-
if (mh)
|
|
106
|
-
parseMoof(dv, mh, pos, movie.moofCount, movie, opts.maxExpandedSamples);
|
|
107
|
-
movie.moofCount++;
|
|
108
|
-
let gap = 0;
|
|
109
|
-
try {
|
|
110
|
-
const next = readBoxHeader(dv, h.size, dv.byteLength);
|
|
111
|
-
if (next && next.type === 'mdat')
|
|
112
|
-
gap = next.size;
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
gap = 0;
|
|
116
|
-
}
|
|
117
|
-
ahead = gap <= SMALL_MDAT ? opts.readAhead.fragment : Math.min(65536, Math.max(1024, 2 * h.size + 64));
|
|
118
|
-
if (gap > SMALL_MDAT) {
|
|
119
|
-
if (!pf.hints.length && !pf.mfraTried && (size - pos) / (h.size + gap) > 16) {
|
|
120
|
-
pf.mfraTried = true;
|
|
121
|
-
pf.hints = await mfraMoofOffsets(reader);
|
|
122
|
-
}
|
|
123
|
-
if (pf.next < pf.hints.length && !prefetched(pf, pos + h.size + gap, HEADER_PEEK))
|
|
124
|
-
await prefetchNext(reader, pf, pos, ahead);
|
|
125
|
-
}
|
|
126
|
-
if (opts.onProgress && movie.moofCount % 64 === 0)
|
|
127
|
-
opts.onProgress({ phase: 'fragments', done: pos, total: size });
|
|
128
|
-
}
|
|
129
|
-
else if (h.type === 'sidx' && !rel.truncated) {
|
|
130
|
-
const dv = await look(pos, h.size, 0);
|
|
131
|
-
const sh = readBoxHeader(dv, 0, dv.byteLength);
|
|
132
|
-
if (sh)
|
|
133
|
-
top.sidx = parseSidx(dv, sh, pos);
|
|
134
|
-
if (top.sidx && top.sidx.subsegments.length > 1 && !pf.hints.length)
|
|
135
|
-
pf.hints = top.sidx.subsegments.map(([o]) => o);
|
|
136
|
-
}
|
|
137
|
-
else if (h.type === 'mfra') {
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
afterMdat = h.type === 'mdat';
|
|
141
|
-
pos = boxEnd;
|
|
142
|
-
}
|
|
143
|
-
if (!sawMoov)
|
|
144
|
-
throw new IsoBmffError('incomplete file: no moov');
|
|
145
|
-
}
|
|
146
|
-
async function emulateFfprobeExtent(reader, movie) {
|
|
147
|
-
const size = reader.size;
|
|
148
|
-
const ids = new Set(movie.tracks.map((t) => t.id));
|
|
149
|
-
let foundMoov = false;
|
|
150
|
-
let foundMdat = false;
|
|
151
|
-
let complete = false;
|
|
152
|
-
let mfraSize = null;
|
|
153
|
-
let moofs = 0;
|
|
154
|
-
for (const b of movie.topLevel) {
|
|
155
|
-
if (b.type === 'moof')
|
|
156
|
-
moofs++;
|
|
157
|
-
else if (b.type === 'moov')
|
|
158
|
-
foundMoov = true;
|
|
159
|
-
else if (b.type === 'mdat' && b.end - b.start > 8)
|
|
160
|
-
foundMdat = true;
|
|
161
|
-
else if (b.type === 'sidx' && foundMoov && b.sidx && !b.sidx.hierarchical && ids.has(b.sidx.referenceId)) {
|
|
162
|
-
let whole = b.sidx.coverageEnd === size;
|
|
163
|
-
if (!whole && size >= 4) {
|
|
164
|
-
if (mfraSize === null)
|
|
165
|
-
mfraSize = (await reader.view(size - 4, 4, 0)).getUint32(0);
|
|
166
|
-
whole = b.sidx.coverageEnd === size - mfraSize;
|
|
167
|
-
}
|
|
168
|
-
if (whole && b.sidx.firstOffset === 0)
|
|
169
|
-
complete = true;
|
|
170
|
-
}
|
|
171
|
-
if (foundMoov && foundMdat && (complete || b.end === size)) {
|
|
172
|
-
movie.ffprobeMoofLimit = moofs;
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
function isVideoTrack(tr, movie) {
|
|
178
|
-
if (movie.chapterTrackIds.has(tr.id))
|
|
179
|
-
return false;
|
|
180
|
-
if (tr.handler === 'vide')
|
|
181
|
-
return true;
|
|
182
|
-
if (tr.handler === 'soun')
|
|
183
|
-
return false;
|
|
184
|
-
return isVideoFourcc(tr.originalFormat ?? tr.sampleEntry);
|
|
185
|
-
}
|
|
186
|
-
function buildVideoTrack(tr, movie, opts, ctx) {
|
|
187
|
-
const t = emptyTrack(ctx.reportedId);
|
|
188
|
-
const w = t.warnings;
|
|
189
|
-
if (ctx.reportedId !== tr.id)
|
|
190
|
-
w.push(`duplicate track_ID ${tr.id}: reported as ${ctx.reportedId}`);
|
|
191
|
-
w.push(...tr.warnings);
|
|
192
|
-
const auxiliary = tr.handler === 'auxv';
|
|
193
|
-
if (auxiliary)
|
|
194
|
-
w.push("auxiliary video track (handler 'auxv')");
|
|
195
|
-
const broken = missingMandatoryAtoms(tr);
|
|
196
|
-
if (broken)
|
|
197
|
-
w.push('missing mandatory sample tables: ffprobe ignores the samples and timing of this track');
|
|
198
|
-
const ts = tr.hasMdhd ? tr.timescale : movie.timescale;
|
|
199
|
-
if (!tr.hasMdhd) {
|
|
200
|
-
w.push('no mdhd: using the movie timescale');
|
|
201
|
-
tr.timescale = ts;
|
|
202
|
-
}
|
|
203
|
-
t.fourccOrCodecId = tr.sampleEntry;
|
|
204
|
-
t.codecName = codecNameFor(tr.originalFormat ?? tr.sampleEntry, tr.esdsObjectType, tr.hasHvcc);
|
|
205
|
-
if (tr.originalFormat)
|
|
206
|
-
w.push(`encrypted: ${tr.sampleEntry} of ${tr.originalFormat}`);
|
|
207
|
-
if (tr.stsdCount > 1)
|
|
208
|
-
w.push(`${tr.stsdCount} sample descriptions, first used`);
|
|
209
|
-
t.width = tr.width;
|
|
210
|
-
t.height = tr.height;
|
|
211
|
-
t.rotation = displayRotation(tr.matrix, movie.matrix);
|
|
212
|
-
t.timescale = ts;
|
|
213
|
-
const totals = sampleTotals(tr);
|
|
214
|
-
const truncated = movie.truncated;
|
|
215
|
-
let presentFragments = 0;
|
|
216
|
-
for (const f of tr.fragments)
|
|
217
|
-
presentFragments += f.present;
|
|
218
|
-
const total = totals.moovSamples + totals.fragmentSamples;
|
|
219
|
-
const frames = totals.moovSamples + presentFragments;
|
|
220
|
-
t.frames = frames;
|
|
221
|
-
t.isStill = frames <= 1;
|
|
222
|
-
if (totals.moovSamples > 0 && tr.fragments.length > 0) {
|
|
223
|
-
w.push(`hybrid fragmented file: moov holds ${totals.moovSamples} of ${total} samples`);
|
|
224
|
-
}
|
|
225
|
-
if (frames < total)
|
|
226
|
-
w.push(`truncated: ${total - frames} of ${total} samples missing`);
|
|
227
|
-
const rates = averageRates(tr, movie);
|
|
228
|
-
t.avgFps = rates.avg;
|
|
229
|
-
t.avgFpsFfprobe = ctx.ffprobeFails ? null : rates.ffprobe;
|
|
230
|
-
if (t.avgFpsFfprobe !== null && t.avgFpsFfprobe !== toFfprobeString(rates.avg)) {
|
|
231
|
-
w.push(`ffprobe reads part of the fragments: avg_frame_rate ${t.avgFpsFfprobe}`);
|
|
232
|
-
}
|
|
233
|
-
if (rates.ffprobe === null && !ctx.ffprobeFails && total > 0)
|
|
234
|
-
w.push('avg_frame_rate of ffprobe is not reproduced: its header values are incomplete or overrun a box');
|
|
235
|
-
const r = ctx.ffprobeFails || broken ? null : ffprobeStage1Rate(tr, movie, totals.moovSamples, opts.maxExpandedSamples, w);
|
|
236
|
-
t.rFpsFfprobe = r ? toFfprobeString(r) : null;
|
|
237
|
-
const edits = editInfo(tr, movie.timescale, totals);
|
|
238
|
-
if (!broken) {
|
|
239
|
-
const timing = trackTiming(tr, movie, totals, edits);
|
|
240
|
-
t.durationSec = timing.durationTs / ts;
|
|
241
|
-
t.startSec = timing.startPts === null ? null : timing.startPts / ts;
|
|
242
|
-
}
|
|
243
|
-
let moovKeep = totals.moovSamples;
|
|
244
|
-
if (tr.sampleCount !== null && tr.sampleCount < moovKeep) {
|
|
245
|
-
w.push(`stts describes ${moovKeep} samples but the sample size table holds ${tr.sampleCount}`);
|
|
246
|
-
moovKeep = tr.sampleCount;
|
|
247
|
-
}
|
|
248
|
-
if (broken) {
|
|
249
|
-
t.presentedFrames = 0;
|
|
250
|
-
finishProvenance(t);
|
|
251
|
-
return { result: t, enabled: tr.enabled, auxiliary };
|
|
252
|
-
}
|
|
253
|
-
if (truncated && moovKeep > 0) {
|
|
254
|
-
const inFile = moovSamplesInFile(tr, movie.fileSize, totals.moovSamples);
|
|
255
|
-
if (inFile && inFile.complete < moovKeep) {
|
|
256
|
-
w.push(`truncated: ${inFile.complete} of ${moovKeep} samples complete`);
|
|
257
|
-
moovKeep = inFile.complete;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
let window = presentationWindow(tr, edits, w);
|
|
261
|
-
if (window && !tr.cttsCounts && !tr.fragments.length && window.start <= 0 && (window.end <= window.start || window.end >= totals.moovDuration))
|
|
262
|
-
window = null;
|
|
263
|
-
const stats = analyzeTimeline({
|
|
264
|
-
segments: timelineSegments(tr, moovKeep),
|
|
265
|
-
timescale: ts,
|
|
266
|
-
editWindow: window,
|
|
267
|
-
maxExpandedSamples: opts.maxExpandedSamples,
|
|
268
|
-
});
|
|
269
|
-
w.push(...stats.warnings);
|
|
270
|
-
t.presentedFrames = stats.presentedFrames;
|
|
271
|
-
t.minFps = stats.minFps;
|
|
272
|
-
t.maxFps = stats.maxFps;
|
|
273
|
-
t.isVfr = stats.isVfr;
|
|
274
|
-
t.vfrRatio = stats.vfrRatio;
|
|
275
|
-
t.distinctDeltas = stats.distinctDeltas;
|
|
276
|
-
t.duplicateTimestamps = stats.duplicateTimestamps;
|
|
277
|
-
t.nominalFps = stats.nominalFps;
|
|
278
|
-
t.dominantFps = stats.dominantFps;
|
|
279
|
-
t.timestampsQuantized = stats.timestampsQuantized;
|
|
280
|
-
if (stats.presentedAvgFps)
|
|
281
|
-
t.avgFps = stats.presentedAvgFps;
|
|
282
|
-
if (t.duplicateTimestamps > 0)
|
|
283
|
-
w.push(`${t.duplicateTimestamps} duplicate timestamps`);
|
|
284
|
-
finishProvenance(t);
|
|
285
|
-
if (stats.presentedAvgFps)
|
|
286
|
-
t.provenance.avgFps = 'exact';
|
|
287
|
-
return { result: t, enabled: tr.enabled, auxiliary, profile: stats.profile };
|
|
288
|
-
}
|
|
289
|
-
function finishProvenance(t) {
|
|
290
|
-
const p = t.provenance;
|
|
291
|
-
for (const k of ['codec', 'dimensions', 'rotation', 'frames', 'avgFps'])
|
|
292
|
-
p[k] = 'header';
|
|
293
|
-
if (t.durationSec !== null)
|
|
294
|
-
p.durationSec = 'header';
|
|
295
|
-
if (t.startSec !== null)
|
|
296
|
-
p.startSec = 'header';
|
|
297
|
-
if (t.presentedFrames !== null)
|
|
298
|
-
p.presentedFrames = 'exact';
|
|
299
|
-
if (t.isVfr !== null) {
|
|
300
|
-
p.minFps = 'exact';
|
|
301
|
-
p.maxFps = 'exact';
|
|
302
|
-
p.isVfr = 'exact';
|
|
303
|
-
p.duplicateTimestamps = 'exact';
|
|
304
|
-
}
|
|
305
|
-
if (t.nominalFps)
|
|
306
|
-
p.nominalFps = 'exact';
|
|
307
|
-
if (t.dominantFps)
|
|
308
|
-
p.dominantFps = 'exact';
|
|
309
|
-
}
|
|
310
|
-
function pickPrimary(built) {
|
|
311
|
-
let best = null;
|
|
312
|
-
const score = (b) => ((b.result.frames ?? 0) > 1 ? (b.auxiliary ? 0.5 : b.enabled ? 2 : 1) : 0);
|
|
313
|
-
for (const b of built) {
|
|
314
|
-
if (!best || score(b) > score(best) || (score(b) === score(best) && (b.result.frames ?? 0) > (best.result.frames ?? 0)))
|
|
315
|
-
best = b;
|
|
316
|
-
}
|
|
317
|
-
return best ? best.result.trackId : null;
|
|
318
|
-
}
|
|
319
|
-
const DEFAULT_STREAM_TIMEBASE = 90000;
|
|
320
|
-
function formatDuration(movie, videoTracks) {
|
|
321
|
-
const NONE = Number.MAX_SAFE_INTEGER;
|
|
322
|
-
let start = NONE;
|
|
323
|
-
let startText = NONE;
|
|
324
|
-
let end = -NONE;
|
|
325
|
-
let endText = -NONE;
|
|
326
|
-
let dur = -NONE;
|
|
327
|
-
let durText = -NONE;
|
|
328
|
-
for (const tr of movie.tracks) {
|
|
329
|
-
if (!tr.hasMdhd)
|
|
330
|
-
continue;
|
|
331
|
-
const main = videoTracks.has(tr) || tr.handler === 'soun';
|
|
332
|
-
if (missingMandatoryAtoms(tr)) {
|
|
333
|
-
const d = rescaleRound(tr.mdhdDuration, 1e6, DEFAULT_STREAM_TIMEBASE);
|
|
334
|
-
if (main)
|
|
335
|
-
dur = Math.max(dur, d);
|
|
336
|
-
else
|
|
337
|
-
durText = Math.max(durText, d);
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
const totals = sampleTotals(tr);
|
|
341
|
-
const timing = trackTiming(tr, movie, totals, editInfo(tr, movie.timescale, totals));
|
|
342
|
-
const d = rescaleRound(timing.durationTs, 1e6, tr.timescale);
|
|
343
|
-
if (timing.startPts !== null) {
|
|
344
|
-
const s = rescaleRound(timing.startPts, 1e6, tr.timescale);
|
|
345
|
-
if (main) {
|
|
346
|
-
start = Math.min(start, s);
|
|
347
|
-
end = Math.max(end, s + d);
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
startText = Math.min(startText, s);
|
|
351
|
-
endText = Math.max(endText, s + d);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
if (main)
|
|
355
|
-
dur = Math.max(dur, d);
|
|
356
|
-
else
|
|
357
|
-
durText = Math.max(durText, d);
|
|
358
|
-
}
|
|
359
|
-
if (start === NONE || (start > startText && start - startText < 1e6))
|
|
360
|
-
start = startText;
|
|
361
|
-
if (end === -NONE || (end < endText && endText - end < 1e6))
|
|
362
|
-
end = endText;
|
|
363
|
-
if (dur === -NONE || (dur < durText && durText - dur < 1e6))
|
|
364
|
-
dur = durText;
|
|
365
|
-
if (start !== NONE && end !== -NONE && end >= start)
|
|
366
|
-
dur = Math.max(dur, end - start);
|
|
367
|
-
return dur > 0 ? dur / 1e6 : null;
|
|
368
|
-
}
|
|
369
|
-
function reportedIds(tracks) {
|
|
370
|
-
const out = new Map();
|
|
371
|
-
const seen = new Set();
|
|
372
|
-
let next = tracks.reduce((m, t) => Math.max(m, t.id), 0) + 1;
|
|
373
|
-
for (const t of tracks) {
|
|
374
|
-
if (seen.has(t.id))
|
|
375
|
-
out.set(t, next++);
|
|
376
|
-
else {
|
|
377
|
-
seen.add(t.id);
|
|
378
|
-
out.set(t, t.id);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
return out;
|
|
382
|
-
}
|
|
7
|
+
export { MOOF_HARD_CAP, MOOV_HARD_CAP, MOOV_SUSPECT_CAP } from "./build.js";
|
|
383
8
|
export async function analyzeIsoBmffDetailed(reader, opts) {
|
|
384
9
|
const movie = newMovie(reader.size);
|
|
385
10
|
const fail = (e) => {
|
|
386
11
|
if (isAbort(e, reader))
|
|
387
12
|
throw e;
|
|
388
13
|
const message = e instanceof Error ? e.message : String(e);
|
|
389
|
-
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);
|
|
390
17
|
analysis.brand = movie.brand;
|
|
391
18
|
analysis.warnings = movie.warnings;
|
|
392
19
|
return { analysis, movie };
|
|
@@ -406,13 +33,14 @@ export async function analyzeIsoBmffDetailed(reader, opts) {
|
|
|
406
33
|
if ([...ids].some(([t, id]) => t.id !== id))
|
|
407
34
|
movie.warnings.push('duplicate track_ID values');
|
|
408
35
|
const videos = movie.tracks.filter((t) => isVideoTrack(t, movie));
|
|
409
|
-
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));
|
|
410
37
|
for (const b of built)
|
|
411
38
|
b.result.rateProfile = await rateProfileOf(b.profile ?? null);
|
|
412
39
|
analysis.videoTracks = built.map((b) => b.result);
|
|
413
40
|
analysis.audioTrackCount = movie.tracks.filter((t) => t.handler === 'soun').length;
|
|
414
41
|
analysis.primaryTrackId = pickPrimary(built);
|
|
415
42
|
analysis.formatDurationSec = formatDuration(movie, new Set(videos));
|
|
43
|
+
analysis.layout = layoutOf(movie);
|
|
416
44
|
return { analysis, movie };
|
|
417
45
|
}
|
|
418
46
|
catch (e) {
|
|
@@ -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);
|
|
@@ -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;
|
|
@@ -207,7 +207,6 @@ function parseStsd(dv, h, tr, ctx) {
|
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
function parseStbl(dv, h, tr, ctx) {
|
|
210
|
-
tr.dv = dv;
|
|
211
210
|
for (const c of children(dv, h, ctx)) {
|
|
212
211
|
const p = payload(c);
|
|
213
212
|
switch (c.type) {
|
|
@@ -281,6 +280,7 @@ function parseMdia(dv, h, tr, ctx) {
|
|
|
281
280
|
}
|
|
282
281
|
function parseTrak(dv, h, movie, ctx) {
|
|
283
282
|
const tr = newTrack();
|
|
283
|
+
tr.dv = dv;
|
|
284
284
|
for (const c of children(dv, h, ctx)) {
|
|
285
285
|
const p = payload(c);
|
|
286
286
|
if (c.type === 'tkhd' && p + 84 <= c.end) {
|
|
@@ -340,7 +340,7 @@ export function parseMoov(dv, h, movie, strict, base = 0) {
|
|
|
340
340
|
}
|
|
341
341
|
break;
|
|
342
342
|
case 'cmov':
|
|
343
|
-
throw new IsoBmffError('unsupported: compressed movie header (cmov)');
|
|
343
|
+
throw new IsoBmffError('unsupported', 'unsupported: compressed movie header (cmov)');
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
346
|
}
|
|
@@ -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]],
|
|
@@ -74,6 +76,7 @@ export function parseMaster(h, b, start, end, parent, cues = null, entry = null)
|
|
|
74
76
|
switch (c.id) {
|
|
75
77
|
case ID_TRACK_ENTRY:
|
|
76
78
|
t = newTrack();
|
|
79
|
+
t.entry = b.slice(p, p + n);
|
|
77
80
|
nest();
|
|
78
81
|
finishTrack(h, t);
|
|
79
82
|
break;
|
|
@@ -111,6 +114,9 @@ export function parseMaster(h, b, start, end, parent, cues = null, entry = null)
|
|
|
111
114
|
case ID_TRACK_TYPE:
|
|
112
115
|
t.type = uint();
|
|
113
116
|
break;
|
|
117
|
+
case ID_FLAG_ENABLED:
|
|
118
|
+
t.disabled = !uint();
|
|
119
|
+
break;
|
|
114
120
|
case ID_CODEC_ID:
|
|
115
121
|
t.codecId = readString(b, p, n);
|
|
116
122
|
break;
|
|
@@ -294,7 +300,7 @@ export async function readMatroskaHeader(src) {
|
|
|
294
300
|
};
|
|
295
301
|
const ebml = await src.header(0);
|
|
296
302
|
if (!ebml || ebml.id !== ID_EBML || ebml.size === UNKNOWN_SIZE || ebml.dataStart + ebml.size > src.size)
|
|
297
|
-
throw
|
|
303
|
+
throw withErrorCode('corrupt', 'invalid EBML header');
|
|
298
304
|
await src.load(ebml.dataStart, Math.min(ebml.size, 4096));
|
|
299
305
|
const s0 = ebml.dataStart - src.base;
|
|
300
306
|
for (const c of children(src.buf, s0, s0 + ebml.size))
|
|
@@ -305,7 +311,7 @@ export async function readMatroskaHeader(src) {
|
|
|
305
311
|
if (!seg) {
|
|
306
312
|
const q = await findLevel1Forward(src, ebml.dataStart + ebml.size, src.size, src.size, { canLoad });
|
|
307
313
|
if (q < 0)
|
|
308
|
-
throw
|
|
314
|
+
throw withErrorCode('no-header', 'no Segment element');
|
|
309
315
|
h.warnings.push(`no valid Segment header: reading level-1 elements from ${q}`);
|
|
310
316
|
h.segmentExtended = true;
|
|
311
317
|
seg = { id: ID_SEGMENT, pos: q, size: UNKNOWN_SIZE, dataStart: q };
|
|
@@ -375,6 +381,6 @@ export async function readMatroskaHeader(src) {
|
|
|
375
381
|
if (!seen.ids.has(ID_INFO))
|
|
376
382
|
h.warnings.push('no Info element: TimestampScale 1000000 assumed');
|
|
377
383
|
if (!h.tracks.length)
|
|
378
|
-
throw
|
|
384
|
+
throw seen.ids.has(ID_TRACKS) ? withErrorCode('truncated', 'no complete TrackEntry in Tracks') : withErrorCode('no-header', 'no Tracks element found');
|
|
379
385
|
return h;
|
|
380
386
|
}
|
|
@@ -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,15 +82,19 @@ 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
|
-
function headerTrack(h, t) {
|
|
89
|
+
function headerTrack(h, t, opts) {
|
|
87
90
|
const r = emptyTrack(t.number);
|
|
91
|
+
if (opts.codecConfig)
|
|
92
|
+
r.codecConfig = { family: 'matroska', trackEntry: t.entry ?? null };
|
|
88
93
|
r.fourccOrCodecId = t.codecId;
|
|
89
94
|
r.codecName = codecNameOf(t.codecId);
|
|
90
95
|
r.width = t.width;
|
|
91
96
|
r.height = t.height;
|
|
97
|
+
r.enabled = !t.disabled;
|
|
92
98
|
r.timescale = 1e9 / (Math.trunc(h.timestampScale * t.trackTimestampScale) || h.timestampScale);
|
|
93
99
|
r.provenance.codec = r.provenance.dimensions = 'header';
|
|
94
100
|
const d = declaredRate(t.defaultDuration);
|
|
@@ -260,7 +266,7 @@ function pickEvenly(list, k) {
|
|
|
260
266
|
return [...new Set(Array.from({ length: k }, (_, i) => list[Math.round((i * (list.length - 1)) / (k - 1))]))];
|
|
261
267
|
}
|
|
262
268
|
async function tailScan(src, h, video, first, opts) {
|
|
263
|
-
let size = Math.max(opts.readAhead.tail, 4096);
|
|
269
|
+
let size = Math.max(opts.readAhead.tail || 0, 4096);
|
|
264
270
|
let hi = h.segmentEnd;
|
|
265
271
|
let maxAnyTicks = -Infinity;
|
|
266
272
|
for (;;) {
|
|
@@ -393,7 +399,7 @@ export async function analyzeMatroska(reader, opts) {
|
|
|
393
399
|
return a;
|
|
394
400
|
}
|
|
395
401
|
a.primaryTrackId = video[0].number;
|
|
396
|
-
a.videoTracks = video.map((t) => headerTrack(h, t));
|
|
402
|
+
a.videoTracks = video.map((t) => headerTrack(h, t, opts));
|
|
397
403
|
let mode = opts.matroskaMode;
|
|
398
404
|
let first = null;
|
|
399
405
|
if (mode !== 'header') {
|
package/dist/core/options.js
CHANGED
package/dist/core/reader.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FileErrorCode } from './types.js';
|
|
1
2
|
export interface BlobSlice {
|
|
2
3
|
arrayBuffer(): Promise<ArrayBuffer>;
|
|
3
4
|
}
|
|
@@ -42,3 +43,5 @@ export declare function u64Big(dv: DataView, p: number): bigint;
|
|
|
42
43
|
export declare function fixed16_16(dv: DataView, p: number): number;
|
|
43
44
|
export declare function ascii(dv: DataView, p: number, length: number): string;
|
|
44
45
|
export declare function fourcc(dv: DataView, p: number): string;
|
|
46
|
+
export declare function withErrorCode(code: FileErrorCode, message: string): Error;
|
|
47
|
+
export declare function whichErrorCode(e: unknown, fallback?: FileErrorCode): FileErrorCode;
|
package/dist/core/reader.js
CHANGED
|
@@ -2,9 +2,9 @@ import { throwIfAborted } from "./abort.js";
|
|
|
2
2
|
export const DEFAULT_HEAD_WINDOW = 64 * 1024;
|
|
3
3
|
export const DEFAULT_MAX_WINDOWS = 4;
|
|
4
4
|
export const MAX_SAFE_OFFSET = Number.MAX_SAFE_INTEGER;
|
|
5
|
-
function assertSafeCount(value, what) {
|
|
5
|
+
function assertSafeCount(value, what, kind = RangeError) {
|
|
6
6
|
if (!Number.isSafeInteger(value) || value < 0)
|
|
7
|
-
throw new
|
|
7
|
+
throw new kind(`${what} must be a non-negative safe integer, got ${value}`);
|
|
8
8
|
}
|
|
9
9
|
export function toSafeNumber(value, what = 'value') {
|
|
10
10
|
if (value > BigInt(MAX_SAFE_OFFSET) || value < -BigInt(MAX_SAFE_OFFSET))
|
|
@@ -22,13 +22,13 @@ export class BlobReader {
|
|
|
22
22
|
#reads = 0;
|
|
23
23
|
#unique = 0;
|
|
24
24
|
constructor(source, options = {}) {
|
|
25
|
-
assertSafeCount(source.size, 'blob size');
|
|
25
|
+
assertSafeCount(source.size, 'blob size', TypeError);
|
|
26
26
|
this.#source = source;
|
|
27
27
|
this.size = source.size;
|
|
28
28
|
this.headWindow = options.headWindow ?? DEFAULT_HEAD_WINDOW;
|
|
29
29
|
this.maxWindows = Math.max(1, options.maxWindows ?? DEFAULT_MAX_WINDOWS);
|
|
30
30
|
this.signal = options.signal ?? null;
|
|
31
|
-
assertSafeCount(this.headWindow, 'headWindow');
|
|
31
|
+
assertSafeCount(this.headWindow, 'headWindow', TypeError);
|
|
32
32
|
}
|
|
33
33
|
get reads() {
|
|
34
34
|
return this.#reads;
|
|
@@ -54,12 +54,12 @@ export class BlobReader {
|
|
|
54
54
|
}
|
|
55
55
|
catch (e) {
|
|
56
56
|
this.checkAbort();
|
|
57
|
-
throw e?.name === 'AbortError' ? e :
|
|
57
|
+
throw e?.name === 'AbortError' ? e : withErrorCode('read-failed', `file read failed: ${e}`);
|
|
58
58
|
}
|
|
59
59
|
this.checkAbort();
|
|
60
60
|
this.#reads++;
|
|
61
61
|
if (ab.byteLength !== end - offset) {
|
|
62
|
-
throw
|
|
62
|
+
throw withErrorCode('read-failed', `file read failed: short read at ${offset}: expected ${end - offset} bytes, got ${ab.byteLength}`);
|
|
63
63
|
}
|
|
64
64
|
this.#cover(offset, end);
|
|
65
65
|
return new Uint8Array(ab);
|
|
@@ -98,7 +98,7 @@ export class BlobReader {
|
|
|
98
98
|
async view(offset, length, readAhead = this.headWindow) {
|
|
99
99
|
assertSafeCount(offset, 'offset');
|
|
100
100
|
assertSafeCount(length, 'length');
|
|
101
|
-
assertSafeCount(readAhead, 'readAhead');
|
|
101
|
+
assertSafeCount(readAhead, 'readAhead', TypeError);
|
|
102
102
|
if (offset > this.size)
|
|
103
103
|
throw new RangeError(`offset ${offset} is beyond the end of the file (${this.size})`);
|
|
104
104
|
const end = Math.min(this.size, offset + length);
|
|
@@ -198,3 +198,9 @@ export function ascii(dv, p, length) {
|
|
|
198
198
|
export function fourcc(dv, p) {
|
|
199
199
|
return ascii(dv, p, 4).replace(/[^ -~]/g, '?');
|
|
200
200
|
}
|
|
201
|
+
export function withErrorCode(code, message) {
|
|
202
|
+
return Object.assign(Error(message), { errorCode: code });
|
|
203
|
+
}
|
|
204
|
+
export function whichErrorCode(e, fallback = 'internal') {
|
|
205
|
+
return e?.errorCode ?? (e instanceof RangeError ? 'corrupt' : fallback);
|
|
206
|
+
}
|