gesso-core 0.1.0 → 0.2.0
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 +147 -0
- package/README.md +1 -1
- package/dist/{CanvasSurface-CrG-uJ5o.js → CanvasSurface-5dU-U3qQ.js} +34 -5
- package/dist/{CanvasSurface-CrG-uJ5o.js.map → CanvasSurface-5dU-U3qQ.js.map} +1 -1
- package/dist/{index-DuZEAhyZ.d.ts → index-DAdauEn9.d.ts} +634 -53
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1439 -117
- package/dist/index.js.map +1 -1
- package/dist/testing.d.ts +163 -2
- package/dist/testing.js +325 -2
- package/dist/testing.js.map +1 -1
- package/package.json +2 -2
package/dist/testing.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $
|
|
1
|
+
import { $f as UiInputDispatcher, Cn as CanvasHost, Cs as Canvas2DGradient, Ga as UiPointerController, Jp as UiNode, Ka as ScrollContainerState, Lp as Constraints, Mi as LayoutEngine, Rp as LayoutBox, Ss as Canvas2DContext, Ta as UiTouchScroller, Ua as UiKeyboardController, Wa as PointerControllerOptions, Xa as UiWheelController, Xc as UiGraph, Ym as LayoutRecord, ao as UiFocusManager, eh as UiNodeType, eo as UiGestureRecognizer, fn as Canvas2DRenderer, ha as UiPlatformAdapter, ma as PlatformSurface, pa as PlatformEventTarget, qa as ScrollSink, rc as TextMeasurer, wa as TouchScrollerOptions, wn as CanvasSurface, xo as UiHitTester, zp as LayoutResult } from "./index-DAdauEn9.js";
|
|
2
2
|
//#region src/layout/LayoutTestUtils.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Shared helpers for layout specs: builds UiNode trees through
|
|
@@ -228,5 +228,166 @@ declare class RenderHarness {
|
|
|
228
228
|
box(node: UiNode): LayoutBox;
|
|
229
229
|
}
|
|
230
230
|
//#endregion
|
|
231
|
-
|
|
231
|
+
//#region src/media/Mp4TestUtils.d.ts
|
|
232
|
+
/**
|
|
233
|
+
* A very small MP4 writer, and a decoder that decodes nothing.
|
|
234
|
+
*
|
|
235
|
+
* Both exist for the same reason the demuxer's spec gives for building
|
|
236
|
+
* its files by hand: a fixture `.mp4` is a better test of reality and
|
|
237
|
+
* a worse test of this code, because when it fails you do not know
|
|
238
|
+
* which of forty boxes was misread. Building the file means a spec can
|
|
239
|
+
* name the exact field it is about, and construct the awkward cases —
|
|
240
|
+
* a chunk holding several samples, composition offsets reordering
|
|
241
|
+
* them, a track with no `stss` — rather than hunting for a file that
|
|
242
|
+
* has one.
|
|
243
|
+
*
|
|
244
|
+
* In `gesso-core/testing` rather than beside a spec because two specs
|
|
245
|
+
* now want it: the demuxer's, which reads these files, and the
|
|
246
|
+
* resolver's, which plays them.
|
|
247
|
+
*/
|
|
248
|
+
/** `size type payload`, which is every box in the format. */
|
|
249
|
+
declare function box(type: string, ...payload: number[][]): number[];
|
|
250
|
+
/** A full box: a version and three flag bytes before the payload. */
|
|
251
|
+
declare function fullBox(type: string, version: number, ...payload: number[][]): number[];
|
|
252
|
+
/** An AAC audio track, for a file that has sound as well as pictures. */
|
|
253
|
+
interface AudioTrackSpec {
|
|
254
|
+
timescale: number;
|
|
255
|
+
deltas: {
|
|
256
|
+
count: number;
|
|
257
|
+
delta: number;
|
|
258
|
+
}[];
|
|
259
|
+
sizes: number[];
|
|
260
|
+
chunkOffsets: number[];
|
|
261
|
+
chunkRuns: {
|
|
262
|
+
firstChunk: number;
|
|
263
|
+
samplesPerChunk: number;
|
|
264
|
+
}[];
|
|
265
|
+
channels: number;
|
|
266
|
+
sampleRate: number;
|
|
267
|
+
/**
|
|
268
|
+
* The `AudioSpecificConfig`, whose top five bits are the audio
|
|
269
|
+
* object type. The default is AAC-LC at 44.1kHz stereo, which is
|
|
270
|
+
* `mp4a.40.2` and what nearly every file on the web carries.
|
|
271
|
+
*/
|
|
272
|
+
config?: number[];
|
|
273
|
+
}
|
|
274
|
+
interface TrackSpec {
|
|
275
|
+
timescale: number;
|
|
276
|
+
/** One `stts` run per entry. */
|
|
277
|
+
deltas: {
|
|
278
|
+
count: number;
|
|
279
|
+
delta: number;
|
|
280
|
+
}[];
|
|
281
|
+
compositionOffsets?: {
|
|
282
|
+
count: number;
|
|
283
|
+
offset: number;
|
|
284
|
+
}[];
|
|
285
|
+
sizes: number[];
|
|
286
|
+
chunkOffsets: number[];
|
|
287
|
+
chunkRuns: {
|
|
288
|
+
firstChunk: number;
|
|
289
|
+
samplesPerChunk: number;
|
|
290
|
+
}[];
|
|
291
|
+
/** Omitted means no `stss` at all, which means every sample is a sync sample. */
|
|
292
|
+
syncSamples?: number[];
|
|
293
|
+
width: number;
|
|
294
|
+
height: number;
|
|
295
|
+
handler?: string;
|
|
296
|
+
/** The sample entry's four character code. `avc1` unless said otherwise. */
|
|
297
|
+
codec?: 'avc1' | 'av01' | 'vp09';
|
|
298
|
+
/** The bytes of the configuration record after any fixed header; see `configurationBox`. */
|
|
299
|
+
configBytes?: number[];
|
|
300
|
+
}
|
|
301
|
+
declare function buildMp4(spec: TrackSpec, options?: {
|
|
302
|
+
fragmented?: boolean;
|
|
303
|
+
audio?: AudioTrackSpec;
|
|
304
|
+
}): ArrayBuffer;
|
|
305
|
+
/** One run of samples inside a fragment. */
|
|
306
|
+
interface FragmentSpec {
|
|
307
|
+
/** Decode time the fragment starts at, in the track's timescale. Omitted means "carry on". */
|
|
308
|
+
baseMediaDecodeTime?: number;
|
|
309
|
+
samples: {
|
|
310
|
+
duration: number;
|
|
311
|
+
size: number;
|
|
312
|
+
isKey: boolean;
|
|
313
|
+
}[];
|
|
314
|
+
}
|
|
315
|
+
interface FragmentedSpec {
|
|
316
|
+
trackId: number;
|
|
317
|
+
timescale: number;
|
|
318
|
+
width: number;
|
|
319
|
+
height: number;
|
|
320
|
+
fragments: FragmentSpec[];
|
|
321
|
+
/** Written into `trex`, and used by any sample whose `trun` omits the field. */
|
|
322
|
+
defaultSampleDuration?: number;
|
|
323
|
+
defaultSampleSize?: number;
|
|
324
|
+
/** Leave a field out of the `trun` so the default has to supply it. */
|
|
325
|
+
omit?: {
|
|
326
|
+
duration?: boolean;
|
|
327
|
+
size?: boolean;
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* A fragmented MP4: a `moov` that describes the track and promises
|
|
332
|
+
* fragments, then the fragments.
|
|
333
|
+
*
|
|
334
|
+
* The `stbl` is a shell — `stsd` and four empty tables — which is what
|
|
335
|
+
* a fragmenter writes and is exactly the shape that used to make this
|
|
336
|
+
* demuxer return zero samples for a file the browser played.
|
|
337
|
+
*/
|
|
338
|
+
declare function buildFragmentedMp4(spec: FragmentedSpec): ArrayBuffer;
|
|
339
|
+
/** The offset `mdat`'s payload lands at, for a table that must point into it. */
|
|
340
|
+
declare function mdatStart(data: ArrayBuffer): number;
|
|
341
|
+
/** One chunk as the fake decoder saw it. */
|
|
342
|
+
interface DecodedChunk {
|
|
343
|
+
readonly timestamp: number;
|
|
344
|
+
readonly type: 'key' | 'delta';
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* A `VideoDecoder` that records what it was asked to decode and hands
|
|
348
|
+
* back a frame for each.
|
|
349
|
+
*
|
|
350
|
+
* What the resolver's spec is about is *which samples are submitted*:
|
|
351
|
+
* a seek that starts at the wrong keyframe, or one that decodes the
|
|
352
|
+
* whole clip to get somewhere, is a bug in arithmetic that has nothing
|
|
353
|
+
* to do with pixels. So this decodes nothing and the frames it emits
|
|
354
|
+
* carry a timestamp and a `close` and no image at all.
|
|
355
|
+
*
|
|
356
|
+
* Emission is synchronous, which a real decoder's never is. That makes
|
|
357
|
+
* the specs readable and is the one way this lies: the ordering
|
|
358
|
+
* hazards a real decoder creates — a frame arriving after the position
|
|
359
|
+
* has moved past it — are `Mp4Playback`'s to handle and are asserted
|
|
360
|
+
* by pushing frames in by hand rather than by waiting for a thread.
|
|
361
|
+
*/
|
|
362
|
+
declare class FakeVideoDecoder {
|
|
363
|
+
static readonly submitted: DecodedChunk[];
|
|
364
|
+
/** How many times a decoder was configured: a seek costs one. */
|
|
365
|
+
static configures: number;
|
|
366
|
+
static resets: number;
|
|
367
|
+
/** Set to stop frames being emitted, for a spec about a queue filling up. */
|
|
368
|
+
static emit: boolean;
|
|
369
|
+
static install(): () => void;
|
|
370
|
+
static reset(): void;
|
|
371
|
+
/** Every timestamp submitted since the last configure, in order. */
|
|
372
|
+
static get sinceConfigure(): number[];
|
|
373
|
+
static isConfigSupported(): Promise<{
|
|
374
|
+
supported: boolean;
|
|
375
|
+
}>;
|
|
376
|
+
state: 'unconfigured' | 'configured' | 'closed';
|
|
377
|
+
decodeQueueSize: number;
|
|
378
|
+
private readonly output;
|
|
379
|
+
constructor(init: {
|
|
380
|
+
output: (frame: {
|
|
381
|
+
timestamp: number;
|
|
382
|
+
close(): void;
|
|
383
|
+
}) => void;
|
|
384
|
+
error: (e: unknown) => void;
|
|
385
|
+
});
|
|
386
|
+
configure(): void;
|
|
387
|
+
decode(chunk: DecodedChunk): void;
|
|
388
|
+
reset(): void;
|
|
389
|
+
close(): void;
|
|
390
|
+
}
|
|
391
|
+
//#endregion
|
|
392
|
+
export { type AudioTrackSpec, type DecodedChunk, FakeCanvasHost, FakeEventTarget, FakePlatformSurface, FakeVideoDecoder, type FragmentSpec, type FragmentedSpec, InputTestHarness, type RecordedCall, RecordingCanvasContext, RenderHarness, type TrackSpec, box, buildFragmentedMp4, buildMp4, callArgs, callNames, callsOf, fullBox, mdatStart, savedDepth };
|
|
232
393
|
//# sourceMappingURL=testing.d.ts.map
|
package/dist/testing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { N as Constraints, X as UiPlatformAdapter, _t as UiInputDispatcher, b as LayoutEngine, cr as UiGraph, ct as UiFocusManager, it as UiWheelController, lt as UiHitTester, nt as UiGestureRecognizer, ot as UiKeyboardController, r as Canvas2DRenderer, st as UiPointerController, t as CanvasSurface, tt as UiTouchScroller, wt as CharacterCountTextMeasurer } from "./CanvasSurface-
|
|
1
|
+
import { N as Constraints, X as UiPlatformAdapter, _t as UiInputDispatcher, b as LayoutEngine, cr as UiGraph, ct as UiFocusManager, it as UiWheelController, lt as UiHitTester, nt as UiGestureRecognizer, ot as UiKeyboardController, r as Canvas2DRenderer, st as UiPointerController, t as CanvasSurface, tt as UiTouchScroller, wt as CharacterCountTextMeasurer } from "./CanvasSurface-5dU-U3qQ.js";
|
|
2
2
|
//#region src/layout/LayoutTestUtils.ts
|
|
3
3
|
/**
|
|
4
4
|
* Shared helpers for layout specs: builds UiNode trees through
|
|
@@ -555,6 +555,329 @@ var RenderHarness = class {
|
|
|
555
555
|
}
|
|
556
556
|
};
|
|
557
557
|
//#endregion
|
|
558
|
-
|
|
558
|
+
//#region src/media/Mp4TestUtils.ts
|
|
559
|
+
/**
|
|
560
|
+
* A very small MP4 writer, and a decoder that decodes nothing.
|
|
561
|
+
*
|
|
562
|
+
* Both exist for the same reason the demuxer's spec gives for building
|
|
563
|
+
* its files by hand: a fixture `.mp4` is a better test of reality and
|
|
564
|
+
* a worse test of this code, because when it fails you do not know
|
|
565
|
+
* which of forty boxes was misread. Building the file means a spec can
|
|
566
|
+
* name the exact field it is about, and construct the awkward cases —
|
|
567
|
+
* a chunk holding several samples, composition offsets reordering
|
|
568
|
+
* them, a track with no `stss` — rather than hunting for a file that
|
|
569
|
+
* has one.
|
|
570
|
+
*
|
|
571
|
+
* In `gesso-core/testing` rather than beside a spec because two specs
|
|
572
|
+
* now want it: the demuxer's, which reads these files, and the
|
|
573
|
+
* resolver's, which plays them.
|
|
574
|
+
*/
|
|
575
|
+
function u32(value) {
|
|
576
|
+
return [
|
|
577
|
+
value >>> 24 & 255,
|
|
578
|
+
value >>> 16 & 255,
|
|
579
|
+
value >>> 8 & 255,
|
|
580
|
+
value & 255
|
|
581
|
+
];
|
|
582
|
+
}
|
|
583
|
+
function u16(value) {
|
|
584
|
+
return [value >>> 8 & 255, value & 255];
|
|
585
|
+
}
|
|
586
|
+
function ascii(text) {
|
|
587
|
+
return [...text].map((character) => character.charCodeAt(0));
|
|
588
|
+
}
|
|
589
|
+
/** `size type payload`, which is every box in the format. */
|
|
590
|
+
function box(type, ...payload) {
|
|
591
|
+
const body = payload.flat();
|
|
592
|
+
return [
|
|
593
|
+
...u32(body.length + 8),
|
|
594
|
+
...ascii(type),
|
|
595
|
+
...body
|
|
596
|
+
];
|
|
597
|
+
}
|
|
598
|
+
/** A full box: a version and three flag bytes before the payload. */
|
|
599
|
+
function fullBox(type, version, ...payload) {
|
|
600
|
+
return box(type, [
|
|
601
|
+
version,
|
|
602
|
+
0,
|
|
603
|
+
0,
|
|
604
|
+
0
|
|
605
|
+
], ...payload);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* The codec configuration box, which differs per codec and is where a
|
|
609
|
+
* codec string comes from.
|
|
610
|
+
*/
|
|
611
|
+
function configurationBox(spec) {
|
|
612
|
+
if (spec.codec === "av01") return box("av1C", [129, ...spec.configBytes ?? [4, 0]]);
|
|
613
|
+
if (spec.codec === "vp09") return fullBox("vpcC", 1, spec.configBytes ?? [
|
|
614
|
+
0,
|
|
615
|
+
31,
|
|
616
|
+
128
|
|
617
|
+
]);
|
|
618
|
+
return box("avcC", [
|
|
619
|
+
1,
|
|
620
|
+
100,
|
|
621
|
+
0,
|
|
622
|
+
31,
|
|
623
|
+
255,
|
|
624
|
+
225
|
|
625
|
+
]);
|
|
626
|
+
}
|
|
627
|
+
function visualSampleEntry(spec) {
|
|
628
|
+
return box(spec.codec ?? "avc1", [
|
|
629
|
+
0,
|
|
630
|
+
0,
|
|
631
|
+
0,
|
|
632
|
+
0,
|
|
633
|
+
0,
|
|
634
|
+
0
|
|
635
|
+
], u16(1), Array.from({ length: 16 }, () => 0), u16(spec.width), u16(spec.height), u32(4718592), u32(4718592), u32(0), u16(1), Array.from({ length: 32 }, () => 0), u16(24), [255, 255], configurationBox(spec));
|
|
636
|
+
}
|
|
637
|
+
/** `esds`: the descriptor tree down to the two numbers a codec string needs. */
|
|
638
|
+
function esds(spec) {
|
|
639
|
+
const config = spec.config ?? [18, 16];
|
|
640
|
+
const dsi = [
|
|
641
|
+
5,
|
|
642
|
+
config.length,
|
|
643
|
+
...config
|
|
644
|
+
];
|
|
645
|
+
const dcd = [
|
|
646
|
+
4,
|
|
647
|
+
13 + dsi.length,
|
|
648
|
+
64,
|
|
649
|
+
21,
|
|
650
|
+
0,
|
|
651
|
+
0,
|
|
652
|
+
0,
|
|
653
|
+
0,
|
|
654
|
+
0,
|
|
655
|
+
0,
|
|
656
|
+
0,
|
|
657
|
+
0,
|
|
658
|
+
0,
|
|
659
|
+
0,
|
|
660
|
+
0,
|
|
661
|
+
...dsi
|
|
662
|
+
];
|
|
663
|
+
return fullBox("esds", 0, [
|
|
664
|
+
3,
|
|
665
|
+
3 + dcd.length,
|
|
666
|
+
0,
|
|
667
|
+
1,
|
|
668
|
+
0,
|
|
669
|
+
...dcd
|
|
670
|
+
]);
|
|
671
|
+
}
|
|
672
|
+
function audioSampleEntry(spec) {
|
|
673
|
+
return box("mp4a", [
|
|
674
|
+
0,
|
|
675
|
+
0,
|
|
676
|
+
0,
|
|
677
|
+
0,
|
|
678
|
+
0,
|
|
679
|
+
0
|
|
680
|
+
], u16(1), u16(0), [
|
|
681
|
+
0,
|
|
682
|
+
0,
|
|
683
|
+
0,
|
|
684
|
+
0,
|
|
685
|
+
0,
|
|
686
|
+
0
|
|
687
|
+
], u16(spec.channels), u16(16), u16(0), u16(0), u16(spec.sampleRate), u16(0), esds(spec));
|
|
688
|
+
}
|
|
689
|
+
function audioSampleTable(spec) {
|
|
690
|
+
return box("stbl", fullBox("stsd", 0, u32(1), audioSampleEntry(spec)), fullBox("stts", 0, u32(spec.deltas.length), ...spec.deltas.map((run) => [...u32(run.count), ...u32(run.delta)])), fullBox("stsc", 0, u32(spec.chunkRuns.length), ...spec.chunkRuns.map((run) => [
|
|
691
|
+
...u32(run.firstChunk),
|
|
692
|
+
...u32(run.samplesPerChunk),
|
|
693
|
+
...u32(1)
|
|
694
|
+
])), fullBox("stsz", 0, u32(0), u32(spec.sizes.length), ...spec.sizes.map((size) => u32(size))), fullBox("stco", 0, u32(spec.chunkOffsets.length), ...spec.chunkOffsets.map((offset) => u32(offset))));
|
|
695
|
+
}
|
|
696
|
+
function audioTrak(spec) {
|
|
697
|
+
return box("trak", box("mdia", fullBox("mdhd", 0, u32(0), u32(0), u32(spec.timescale), u32(1e3)), fullBox("hdlr", 0, u32(0), ascii("soun"), Array.from({ length: 12 }, () => 0), [0]), box("minf", audioSampleTable(spec))));
|
|
698
|
+
}
|
|
699
|
+
function sampleTable(spec) {
|
|
700
|
+
const boxes = [
|
|
701
|
+
fullBox("stsd", 0, u32(1), visualSampleEntry(spec)),
|
|
702
|
+
fullBox("stts", 0, u32(spec.deltas.length), ...spec.deltas.map((run) => [...u32(run.count), ...u32(run.delta)])),
|
|
703
|
+
fullBox("stsc", 0, u32(spec.chunkRuns.length), ...spec.chunkRuns.map((run) => [
|
|
704
|
+
...u32(run.firstChunk),
|
|
705
|
+
...u32(run.samplesPerChunk),
|
|
706
|
+
...u32(1)
|
|
707
|
+
])),
|
|
708
|
+
fullBox("stsz", 0, u32(0), u32(spec.sizes.length), ...spec.sizes.map((size) => u32(size))),
|
|
709
|
+
fullBox("stco", 0, u32(spec.chunkOffsets.length), ...spec.chunkOffsets.map((offset) => u32(offset)))
|
|
710
|
+
];
|
|
711
|
+
if (spec.compositionOffsets !== void 0) boxes.splice(2, 0, fullBox("ctts", 0, u32(spec.compositionOffsets.length), ...spec.compositionOffsets.map((run) => [...u32(run.count), ...u32(run.offset)])));
|
|
712
|
+
if (spec.syncSamples !== void 0) boxes.push(fullBox("stss", 0, u32(spec.syncSamples.length), ...spec.syncSamples.map((number) => u32(number))));
|
|
713
|
+
return box("stbl", ...boxes);
|
|
714
|
+
}
|
|
715
|
+
function buildMp4(spec, options = {}) {
|
|
716
|
+
const trak = box("trak", box("mdia", fullBox("mdhd", 0, u32(0), u32(0), u32(spec.timescale), u32(1e3)), fullBox("hdlr", 0, u32(0), ascii(spec.handler ?? "vide"), Array.from({ length: 12 }, () => 0), [0]), box("minf", sampleTable(spec))));
|
|
717
|
+
const bytes = [
|
|
718
|
+
...box("ftyp", ascii("isom"), u32(512), ascii("isomiso2avc1mp41")),
|
|
719
|
+
...box("moov", fullBox("mvhd", 0, Array.from({ length: 100 }, () => 0)), trak, ...options.audio === void 0 ? [] : [audioTrak(options.audio)]),
|
|
720
|
+
...options.fragmented === true ? box("moof", box("mfhd", u32(1))) : [],
|
|
721
|
+
...box("mdat", Array.from({ length: 512 }, () => 65))
|
|
722
|
+
];
|
|
723
|
+
return new Uint8Array(bytes).buffer;
|
|
724
|
+
}
|
|
725
|
+
function tkhd(trackId) {
|
|
726
|
+
return fullBox("tkhd", 0, u32(0), u32(0), u32(trackId), Array.from({ length: 68 }, () => 0));
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* A fragmented MP4: a `moov` that describes the track and promises
|
|
730
|
+
* fragments, then the fragments.
|
|
731
|
+
*
|
|
732
|
+
* The `stbl` is a shell — `stsd` and four empty tables — which is what
|
|
733
|
+
* a fragmenter writes and is exactly the shape that used to make this
|
|
734
|
+
* demuxer return zero samples for a file the browser played.
|
|
735
|
+
*/
|
|
736
|
+
function buildFragmentedMp4(spec) {
|
|
737
|
+
const shellSpec = {
|
|
738
|
+
timescale: spec.timescale,
|
|
739
|
+
deltas: [],
|
|
740
|
+
sizes: [],
|
|
741
|
+
chunkOffsets: [],
|
|
742
|
+
chunkRuns: [],
|
|
743
|
+
width: spec.width,
|
|
744
|
+
height: spec.height
|
|
745
|
+
};
|
|
746
|
+
const trak = box("trak", tkhd(spec.trackId), box("mdia", fullBox("mdhd", 0, u32(0), u32(0), u32(spec.timescale), u32(1e3)), fullBox("hdlr", 0, u32(0), ascii("vide"), Array.from({ length: 12 }, () => 0), [0]), box("minf", sampleTable(shellSpec))));
|
|
747
|
+
const mvex = box("mvex", fullBox("trex", 0, u32(spec.trackId), u32(1), u32(spec.defaultSampleDuration ?? 0), u32(spec.defaultSampleSize ?? 0), u32(0)));
|
|
748
|
+
const moov = box("moov", fullBox("mvhd", 0, Array.from({ length: 100 }, () => 0)), trak, mvex);
|
|
749
|
+
const bytes = [...box("ftyp", ascii("isom"), u32(512), ascii("isomiso6avc1mp41")), ...moov];
|
|
750
|
+
for (const fragment of spec.fragments) {
|
|
751
|
+
const payloadSize = fragment.samples.reduce((total, sample) => total + sample.size, 0);
|
|
752
|
+
const build = (dataOffset) => {
|
|
753
|
+
const trunFlags = 1 | (spec.omit?.duration === true ? 0 : 256) | (spec.omit?.size === true ? 0 : 512) | 1024;
|
|
754
|
+
const entries = fragment.samples.flatMap((sample) => [
|
|
755
|
+
...spec.omit?.duration === true ? [] : u32(sample.duration),
|
|
756
|
+
...spec.omit?.size === true ? [] : u32(sample.size),
|
|
757
|
+
...u32(sample.isKey ? 0 : 65536)
|
|
758
|
+
]);
|
|
759
|
+
const trun = box("trun", [
|
|
760
|
+
0,
|
|
761
|
+
trunFlags >> 16 & 255,
|
|
762
|
+
trunFlags >> 8 & 255,
|
|
763
|
+
trunFlags & 255
|
|
764
|
+
], u32(fragment.samples.length), u32(dataOffset), entries);
|
|
765
|
+
const traf = box("traf", box("tfhd", [
|
|
766
|
+
0,
|
|
767
|
+
2,
|
|
768
|
+
0,
|
|
769
|
+
0
|
|
770
|
+
], u32(spec.trackId)), ...fragment.baseMediaDecodeTime === void 0 ? [] : [fullBox("tfdt", 0, u32(fragment.baseMediaDecodeTime))], trun);
|
|
771
|
+
return box("moof", fullBox("mfhd", 0, u32(1)), traf);
|
|
772
|
+
};
|
|
773
|
+
const moof = build(build(0).length + 8);
|
|
774
|
+
bytes.push(...moof, ...box("mdat", Array.from({ length: payloadSize }, () => 66)));
|
|
775
|
+
}
|
|
776
|
+
return new Uint8Array(bytes).buffer;
|
|
777
|
+
}
|
|
778
|
+
/** The offset `mdat`'s payload lands at, for a table that must point into it. */
|
|
779
|
+
function mdatStart(data) {
|
|
780
|
+
const view = new DataView(data);
|
|
781
|
+
let at = 0;
|
|
782
|
+
while (at + 8 <= data.byteLength) {
|
|
783
|
+
const size = view.getUint32(at);
|
|
784
|
+
if (String.fromCharCode(view.getUint8(at + 4), view.getUint8(at + 5), view.getUint8(at + 6), view.getUint8(at + 7)) === "mdat") return at + 8;
|
|
785
|
+
at += size;
|
|
786
|
+
}
|
|
787
|
+
throw new Error("no mdat");
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* A `VideoDecoder` that records what it was asked to decode and hands
|
|
791
|
+
* back a frame for each.
|
|
792
|
+
*
|
|
793
|
+
* What the resolver's spec is about is *which samples are submitted*:
|
|
794
|
+
* a seek that starts at the wrong keyframe, or one that decodes the
|
|
795
|
+
* whole clip to get somewhere, is a bug in arithmetic that has nothing
|
|
796
|
+
* to do with pixels. So this decodes nothing and the frames it emits
|
|
797
|
+
* carry a timestamp and a `close` and no image at all.
|
|
798
|
+
*
|
|
799
|
+
* Emission is synchronous, which a real decoder's never is. That makes
|
|
800
|
+
* the specs readable and is the one way this lies: the ordering
|
|
801
|
+
* hazards a real decoder creates — a frame arriving after the position
|
|
802
|
+
* has moved past it — are `Mp4Playback`'s to handle and are asserted
|
|
803
|
+
* by pushing frames in by hand rather than by waiting for a thread.
|
|
804
|
+
*/
|
|
805
|
+
var FakeVideoDecoder = class FakeVideoDecoder {
|
|
806
|
+
static submitted = [];
|
|
807
|
+
/** How many times a decoder was configured: a seek costs one. */
|
|
808
|
+
static configures = 0;
|
|
809
|
+
static resets = 0;
|
|
810
|
+
/** Set to stop frames being emitted, for a spec about a queue filling up. */
|
|
811
|
+
static emit = true;
|
|
812
|
+
static install() {
|
|
813
|
+
const scope = globalThis;
|
|
814
|
+
const previous = {
|
|
815
|
+
VideoDecoder: scope.VideoDecoder,
|
|
816
|
+
EncodedVideoChunk: scope.EncodedVideoChunk,
|
|
817
|
+
VideoFrame: scope.VideoFrame,
|
|
818
|
+
createImageBitmap: scope.createImageBitmap
|
|
819
|
+
};
|
|
820
|
+
FakeVideoDecoder.reset();
|
|
821
|
+
scope.VideoDecoder = FakeVideoDecoder;
|
|
822
|
+
scope.EncodedVideoChunk = class {
|
|
823
|
+
timestamp;
|
|
824
|
+
type;
|
|
825
|
+
constructor(init) {
|
|
826
|
+
this.timestamp = init.timestamp;
|
|
827
|
+
this.type = init.type;
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
scope.VideoFrame = class {};
|
|
831
|
+
scope.createImageBitmap = void 0;
|
|
832
|
+
return () => Object.assign(scope, previous);
|
|
833
|
+
}
|
|
834
|
+
static reset() {
|
|
835
|
+
FakeVideoDecoder.submitted.length = 0;
|
|
836
|
+
FakeVideoDecoder.configures = 0;
|
|
837
|
+
FakeVideoDecoder.resets = 0;
|
|
838
|
+
FakeVideoDecoder.emit = true;
|
|
839
|
+
}
|
|
840
|
+
/** Every timestamp submitted since the last configure, in order. */
|
|
841
|
+
static get sinceConfigure() {
|
|
842
|
+
return FakeVideoDecoder.submitted.map((chunk) => chunk.timestamp);
|
|
843
|
+
}
|
|
844
|
+
static isConfigSupported() {
|
|
845
|
+
return Promise.resolve({ supported: true });
|
|
846
|
+
}
|
|
847
|
+
state = "unconfigured";
|
|
848
|
+
decodeQueueSize = 0;
|
|
849
|
+
output;
|
|
850
|
+
constructor(init) {
|
|
851
|
+
this.output = init.output;
|
|
852
|
+
}
|
|
853
|
+
configure() {
|
|
854
|
+
this.state = "configured";
|
|
855
|
+
FakeVideoDecoder.configures++;
|
|
856
|
+
}
|
|
857
|
+
decode(chunk) {
|
|
858
|
+
FakeVideoDecoder.submitted.push({
|
|
859
|
+
timestamp: chunk.timestamp,
|
|
860
|
+
type: chunk.type
|
|
861
|
+
});
|
|
862
|
+
if (!FakeVideoDecoder.emit) {
|
|
863
|
+
this.decodeQueueSize++;
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
this.output({
|
|
867
|
+
timestamp: chunk.timestamp,
|
|
868
|
+
close: () => {}
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
reset() {
|
|
872
|
+
FakeVideoDecoder.resets++;
|
|
873
|
+
this.decodeQueueSize = 0;
|
|
874
|
+
this.state = "unconfigured";
|
|
875
|
+
}
|
|
876
|
+
close() {
|
|
877
|
+
this.state = "closed";
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
//#endregion
|
|
881
|
+
export { FakeCanvasHost, FakeEventTarget, FakePlatformSurface, FakeVideoDecoder, InputTestHarness, RecordingCanvasContext, RenderHarness, box, buildFragmentedMp4, buildMp4, callArgs, callNames, callsOf, fullBox, mdatStart, savedDepth };
|
|
559
882
|
|
|
560
883
|
//# sourceMappingURL=testing.js.map
|