image-to-pixel 3.0.0 → 3.1.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.
Files changed (71) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +2 -2
  3. package/dist/editor.css +1 -1
  4. package/dist/editor.css.map +2 -2
  5. package/dist/editor.esm.js +60 -60
  6. package/dist/editor.esm.js.map +4 -4
  7. package/dist/fonts/README.md +6 -6
  8. package/dist/fonts/geist-LICENSE.txt +93 -93
  9. package/dist/image-to-pixel.bundle.esm.js +16 -16
  10. package/dist/image-to-pixel.bundle.esm.js.map +3 -3
  11. package/dist/image-to-pixel.cjs +2 -2
  12. package/dist/image-to-pixel.cjs.map +2 -2
  13. package/dist/image-to-pixel.esm.js +2 -2
  14. package/dist/image-to-pixel.esm.js.map +2 -2
  15. package/dist/image-to-pixel.js +2 -2
  16. package/dist/image-to-pixel.js.map +2 -2
  17. package/dist/image-to-pixel.min.js +11 -11
  18. package/dist/image-to-pixel.min.js.map +3 -3
  19. package/dist/video.esm.js +2 -2
  20. package/dist/video.esm.js.map +4 -4
  21. package/lib/core/color.js +69 -69
  22. package/lib/core/cpu/pipeline.js +21 -21
  23. package/lib/core/cpu/stages.js +227 -227
  24. package/lib/core/dither/bluenoise-data.js +3 -3
  25. package/lib/core/dither/kernels.js +27 -27
  26. package/lib/core/dither/matrices.js +95 -95
  27. package/lib/core/engine.js +158 -158
  28. package/lib/core/export/canvas.js +22 -22
  29. package/lib/core/export/crc32.js +15 -15
  30. package/lib/core/export/gif.js +153 -153
  31. package/lib/core/export/png.js +94 -94
  32. package/lib/core/export/scale.js +103 -103
  33. package/lib/core/export/sheet.js +95 -95
  34. package/lib/core/export/zip.js +55 -55
  35. package/lib/core/gpu/backend.js +159 -159
  36. package/lib/core/gpu/shaders.js +258 -258
  37. package/lib/core/palette/builtin.js +36 -36
  38. package/lib/core/palette/extract.js +69 -69
  39. package/lib/core/palette/lospec.js +24 -24
  40. package/lib/core/palette/parse.js +112 -112
  41. package/lib/core/plan.js +58 -58
  42. package/lib/core/presets.js +42 -42
  43. package/lib/core/settings.js +274 -274
  44. package/lib/core/source.js +89 -89
  45. package/lib/core/video/audio.js +159 -0
  46. package/lib/core/video/frames.js +38 -0
  47. package/lib/core/video/mux-mp4.js +213 -24
  48. package/lib/core/video/mux-webm.js +63 -22
  49. package/lib/editor/controls.js +139 -139
  50. package/lib/editor/dom.js +65 -65
  51. package/lib/editor/exporter.js +43 -3
  52. package/lib/editor/icons.js +4 -0
  53. package/lib/editor/index.js +218 -9
  54. package/lib/editor/inspector.js +19 -4
  55. package/lib/editor/library.js +8 -2
  56. package/lib/editor/persist.js +18 -18
  57. package/lib/editor/samples.js +90 -90
  58. package/lib/editor/stage.js +239 -239
  59. package/lib/editor/store.js +80 -80
  60. package/lib/editor/styles/editor.css +34 -1
  61. package/lib/editor/styles/fonts/README.md +6 -6
  62. package/lib/editor/styles/fonts/geist-LICENSE.txt +93 -93
  63. package/lib/editor/styles/tokens.css +89 -89
  64. package/lib/editor/ui.js +97 -97
  65. package/lib/editor/workspace.js +62 -0
  66. package/lib/index.js +31 -29
  67. package/lib/video.js +37 -7
  68. package/package.json +1 -1
  69. package/types/editor.d.ts +34 -34
  70. package/types/index.d.ts +196 -196
  71. package/types/video.d.ts +61 -5
@@ -1,89 +1,89 @@
1
- // Turning whatever the caller has into a "frame": { width, height, canvas?, data }.
2
- // Accepts URLs, Blobs/Files, <img>, <canvas>, OffscreenCanvas, ImageBitmap, <video> (current frame),
3
- // VideoFrame, ImageData or any { width, height, data } RGBA object, plus p5 / q5 images.
4
-
5
- export const MAX_SOURCE = 4096;
6
- const hasDOM = typeof document !== 'undefined';
7
-
8
- const isRGBA = s => s && typeof s === 'object' && 'data' in s && 'width' in s && 'height' in s && s.data && s.data.length === s.width * s.height * 4;
9
-
10
- /** Load a URL or Blob into something drawable. */
11
- export async function loadImage(src) {
12
- if (typeof Blob !== 'undefined' && src instanceof Blob) {
13
- if (typeof createImageBitmap === 'function') {
14
- try { return await createImageBitmap(src); } catch { /* fall through (e.g. SVG) */ }
15
- }
16
- src = URL.createObjectURL(src);
17
- }
18
- if (!hasDOM) throw new Error('Loading images by URL needs a browser; pass { width, height, data } in Node.');
19
- const img = new Image();
20
- img.crossOrigin = 'anonymous';
21
- img.decoding = 'async';
22
- img.src = src;
23
- await img.decode();
24
- return img;
25
- }
26
-
27
- /** Natural pixel size of a drawable. */
28
- export function sizeOf(el) {
29
- return {
30
- width: el.naturalWidth || el.videoWidth || el.displayWidth || el.codedWidth || el.width || 0,
31
- height: el.naturalHeight || el.videoHeight || el.displayHeight || el.codedHeight || el.height || 0,
32
- };
33
- }
34
-
35
- function unwrap(src) {
36
- // p5.Image / q5 image / p5.Graphics / p5.Renderer
37
- if (src && !isRGBA(src) && typeof src === 'object') {
38
- if (src.canvas && typeof src.canvas === 'object' && 'getContext' in src.canvas) return src.canvas;
39
- if (src.elt && typeof src.elt === 'object' && 'getContext' in src.elt) return src.elt;
40
- }
41
- return src;
42
- }
43
-
44
- /**
45
- * @param {any} source
46
- * @param {{ maxSize?: number, canvas?: HTMLCanvasElement }} [opts] canvas: scratch canvas to reuse
47
- */
48
- export async function toFrame(source, opts = {}) {
49
- const maxSize = opts.maxSize || MAX_SOURCE;
50
- let src = unwrap(source);
51
- if (typeof src === 'string' || (typeof Blob !== 'undefined' && src instanceof Blob)) src = await loadImage(src);
52
- if (src && typeof src.complete === 'boolean' && !src.complete && src.decode) await src.decode();
53
-
54
- if (isRGBA(src)) {
55
- const frame = { width: src.width, height: src.height, data: src.data, canvas: null };
56
- if (hasDOM) {
57
- Object.defineProperty(frame, 'canvas', {
58
- configurable: true,
59
- get() {
60
- const c = document.createElement('canvas'); c.width = src.width; c.height = src.height;
61
- const img = new ImageData(new Uint8ClampedArray(src.data), src.width, src.height);
62
- c.getContext('2d').putImageData(img, 0, 0);
63
- Object.defineProperty(frame, 'canvas', { value: c });
64
- return c;
65
- },
66
- });
67
- }
68
- return frame;
69
- }
70
- if (!hasDOM) throw new Error('Unsupported source in a non-browser environment. Pass { width, height, data }.');
71
-
72
- let { width, height } = sizeOf(src);
73
- if (!width || !height) throw new Error('Source has no size yet (is the image or video loaded?)');
74
- const k = Math.min(1, maxSize / Math.max(width, height));
75
- width = Math.max(1, Math.round(width * k)); height = Math.max(1, Math.round(height * k));
76
-
77
- const canvas = opts.canvas || document.createElement('canvas');
78
- canvas.width = width; canvas.height = height;
79
- const ctx = canvas.getContext('2d', { willReadFrequently: true });
80
- ctx.clearRect(0, 0, width, height);
81
- ctx.imageSmoothingEnabled = k < 1;
82
- ctx.imageSmoothingQuality = 'high';
83
- ctx.drawImage(src, 0, 0, width, height);
84
- let cached = null;
85
- return {
86
- width, height, canvas, dirty: true,
87
- get data() { return (cached ||= ctx.getImageData(0, 0, width, height).data); },
88
- };
89
- }
1
+ // Turning whatever the caller has into a "frame": { width, height, canvas?, data }.
2
+ // Accepts URLs, Blobs/Files, <img>, <canvas>, OffscreenCanvas, ImageBitmap, <video> (current frame),
3
+ // VideoFrame, ImageData or any { width, height, data } RGBA object, plus p5 / q5 images.
4
+
5
+ export const MAX_SOURCE = 4096;
6
+ const hasDOM = typeof document !== 'undefined';
7
+
8
+ const isRGBA = s => s && typeof s === 'object' && 'data' in s && 'width' in s && 'height' in s && s.data && s.data.length === s.width * s.height * 4;
9
+
10
+ /** Load a URL or Blob into something drawable. */
11
+ export async function loadImage(src) {
12
+ if (typeof Blob !== 'undefined' && src instanceof Blob) {
13
+ if (typeof createImageBitmap === 'function') {
14
+ try { return await createImageBitmap(src); } catch { /* fall through (e.g. SVG) */ }
15
+ }
16
+ src = URL.createObjectURL(src);
17
+ }
18
+ if (!hasDOM) throw new Error('Loading images by URL needs a browser; pass { width, height, data } in Node.');
19
+ const img = new Image();
20
+ img.crossOrigin = 'anonymous';
21
+ img.decoding = 'async';
22
+ img.src = src;
23
+ await img.decode();
24
+ return img;
25
+ }
26
+
27
+ /** Natural pixel size of a drawable. */
28
+ export function sizeOf(el) {
29
+ return {
30
+ width: el.naturalWidth || el.videoWidth || el.displayWidth || el.codedWidth || el.width || 0,
31
+ height: el.naturalHeight || el.videoHeight || el.displayHeight || el.codedHeight || el.height || 0,
32
+ };
33
+ }
34
+
35
+ function unwrap(src) {
36
+ // p5.Image / q5 image / p5.Graphics / p5.Renderer
37
+ if (src && !isRGBA(src) && typeof src === 'object') {
38
+ if (src.canvas && typeof src.canvas === 'object' && 'getContext' in src.canvas) return src.canvas;
39
+ if (src.elt && typeof src.elt === 'object' && 'getContext' in src.elt) return src.elt;
40
+ }
41
+ return src;
42
+ }
43
+
44
+ /**
45
+ * @param {any} source
46
+ * @param {{ maxSize?: number, canvas?: HTMLCanvasElement }} [opts] canvas: scratch canvas to reuse
47
+ */
48
+ export async function toFrame(source, opts = {}) {
49
+ const maxSize = opts.maxSize || MAX_SOURCE;
50
+ let src = unwrap(source);
51
+ if (typeof src === 'string' || (typeof Blob !== 'undefined' && src instanceof Blob)) src = await loadImage(src);
52
+ if (src && typeof src.complete === 'boolean' && !src.complete && src.decode) await src.decode();
53
+
54
+ if (isRGBA(src)) {
55
+ const frame = { width: src.width, height: src.height, data: src.data, canvas: null };
56
+ if (hasDOM) {
57
+ Object.defineProperty(frame, 'canvas', {
58
+ configurable: true,
59
+ get() {
60
+ const c = document.createElement('canvas'); c.width = src.width; c.height = src.height;
61
+ const img = new ImageData(new Uint8ClampedArray(src.data), src.width, src.height);
62
+ c.getContext('2d').putImageData(img, 0, 0);
63
+ Object.defineProperty(frame, 'canvas', { value: c });
64
+ return c;
65
+ },
66
+ });
67
+ }
68
+ return frame;
69
+ }
70
+ if (!hasDOM) throw new Error('Unsupported source in a non-browser environment. Pass { width, height, data }.');
71
+
72
+ let { width, height } = sizeOf(src);
73
+ if (!width || !height) throw new Error('Source has no size yet (is the image or video loaded?)');
74
+ const k = Math.min(1, maxSize / Math.max(width, height));
75
+ width = Math.max(1, Math.round(width * k)); height = Math.max(1, Math.round(height * k));
76
+
77
+ const canvas = opts.canvas || document.createElement('canvas');
78
+ canvas.width = width; canvas.height = height;
79
+ const ctx = canvas.getContext('2d', { willReadFrequently: true });
80
+ ctx.clearRect(0, 0, width, height);
81
+ ctx.imageSmoothingEnabled = k < 1;
82
+ ctx.imageSmoothingQuality = 'high';
83
+ ctx.drawImage(src, 0, 0, width, height);
84
+ let cached = null;
85
+ return {
86
+ width, height, canvas, dirty: true,
87
+ get data() { return (cached ||= ctx.getImageData(0, 0, width, height).data); },
88
+ };
89
+ }
@@ -0,0 +1,159 @@
1
+ // Audio for video export: read a source file's soundtrack as PCM, and encode PCM with WebCodecs.
2
+ // The muxers (mux-mp4.js, mux-webm.js) take the encoded chunks as a second track next to the video.
3
+
4
+ /** WebCodecs codec strings for the audio formats the muxers can store. */
5
+ export const AUDIO_CODECS = { aac: 'mp4a.40.2', opus: 'opus' };
6
+
7
+ const DEFAULT_RATE = 48000; // Opus only encodes at 48 kHz (and lower); AAC is fine with it too
8
+
9
+ async function toArrayBuffer(source) {
10
+ if (source instanceof ArrayBuffer) return source.slice(0);
11
+ if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
12
+ if (typeof Blob !== 'undefined' && source instanceof Blob) return source.arrayBuffer();
13
+ const url = typeof source === 'string' ? source : source && (source.currentSrc || source.src);
14
+ if (!url) throw new TypeError('decodeAudio() takes a File, Blob, ArrayBuffer, URL or <video>/<audio> element');
15
+ const res = await fetch(url);
16
+ if (!res.ok) throw new Error(`Couldn't load the audio: ${res.status} ${res.statusText}`);
17
+ return res.arrayBuffer();
18
+ }
19
+
20
+ /**
21
+ * Decode the audio track of a video (or audio) file into PCM, resampled and trimmed.
22
+ * Resolves with null when there is no audio track, it can't be decoded, or Web Audio isn't available (Node).
23
+ * More than two channels are mixed down to stereo with the standard Web Audio speaker rules.
24
+ * @param {File|Blob|ArrayBuffer|ArrayBufferView|string|HTMLMediaElement} source
25
+ * @param {{ start?: number, end?: number, sampleRate?: number, channels?: 1 | 2 }} [opts] start/end in seconds
26
+ * @returns {Promise<null | { sampleRate: number, numberOfChannels: number, channels: Float32Array[], duration: number }>}
27
+ */
28
+ export async function decodeAudio(source, opts = {}) {
29
+ if (typeof OfflineAudioContext === 'undefined') return null;
30
+ const sampleRate = opts.sampleRate || DEFAULT_RATE;
31
+ const bytes = await toArrayBuffer(source);
32
+ let decoded;
33
+ try {
34
+ decoded = await new OfflineAudioContext(1, 1, sampleRate).decodeAudioData(bytes);
35
+ } catch (_) {
36
+ return null; // no audio track, or a codec the browser can't decode
37
+ }
38
+ const start = Math.max(0, opts.start || 0);
39
+ const end = Math.min(decoded.duration, opts.end > start ? opts.end : decoded.duration);
40
+ const length = Math.floor((end - start) * sampleRate);
41
+ if (length < 1) return null;
42
+ const numberOfChannels = Math.min(opts.channels || 2, decoded.numberOfChannels);
43
+ // Rendering the decoded buffer through an offline graph trims it and mixes 5.1 etc. down properly.
44
+ const ctx = new OfflineAudioContext(numberOfChannels, length, sampleRate);
45
+ const node = ctx.createBufferSource();
46
+ node.buffer = decoded;
47
+ node.connect(ctx.destination);
48
+ node.start(0, start, length / sampleRate);
49
+ const out = await ctx.startRendering();
50
+ const channels = [];
51
+ for (let c = 0; c < numberOfChannels; c++) channels.push(out.getChannelData(c));
52
+ return { sampleRate, numberOfChannels, channels, duration: length / sampleRate };
53
+ }
54
+
55
+ /**
56
+ * Find an AudioEncoder configuration this browser accepts for `codec`, or null.
57
+ * Platform AAC encoders (Windows Media Foundation, macOS AudioToolbox) are picky about sample rate and
58
+ * bitrate, so a few common combinations are tried: the requested rate first, then 48 and 44.1 kHz.
59
+ * @returns {Promise<AudioEncoderConfig | null>} the config to pass to encodeAudio({ config })
60
+ */
61
+ export async function findAudioConfig(codec, sampleRate = DEFAULT_RATE, numberOfChannels = 2, bitrate = 128000) {
62
+ if (typeof AudioEncoder === 'undefined' || !AUDIO_CODECS[codec]) return null;
63
+ const rates = codec === 'opus' ? [48000] : [...new Set([sampleRate, 48000, 44100])];
64
+ const bitrates = [...new Set([bitrate, 128000, 192000, 160000, 96000, 0])];
65
+ for (const rate of rates) {
66
+ for (const br of bitrates) {
67
+ const config = {
68
+ codec: AUDIO_CODECS[codec], sampleRate: rate, numberOfChannels,
69
+ ...(br ? { bitrate: br } : {}),
70
+ ...(codec === 'aac' ? { aac: { format: 'aac' } } : {}), // raw frames + AudioSpecificConfig, which MP4 needs
71
+ };
72
+ const r = await AudioEncoder.isConfigSupported(config).catch(() => ({}));
73
+ if (r.supported) return config;
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ /** Can this browser encode `codec` ('aac' | 'opus') with these parameters (or a close variant)? */
80
+ export async function audioCodecSupported(codec, sampleRate = DEFAULT_RATE, numberOfChannels = 2) {
81
+ return !!(await findAudioConfig(codec, sampleRate, numberOfChannels));
82
+ }
83
+
84
+ /** Resample PCM with Web Audio (browser only). */
85
+ export async function resamplePcm(pcm, sampleRate) {
86
+ if (pcm.sampleRate === sampleRate) return pcm;
87
+ const { numberOfChannels, channels } = pcm;
88
+ const length = Math.max(1, Math.round(channels[0].length * sampleRate / pcm.sampleRate));
89
+ const ctx = new OfflineAudioContext(numberOfChannels, length, sampleRate);
90
+ const buf = ctx.createBuffer(numberOfChannels, channels[0].length, pcm.sampleRate);
91
+ for (let c = 0; c < numberOfChannels; c++) buf.copyToChannel(channels[c], c);
92
+ const node = ctx.createBufferSource();
93
+ node.buffer = buf;
94
+ node.connect(ctx.destination);
95
+ node.start();
96
+ const out = await ctx.startRendering();
97
+ const res = [];
98
+ for (let c = 0; c < numberOfChannels; c++) res.push(out.getChannelData(c));
99
+ return { sampleRate, numberOfChannels, channels: res, duration: length / sampleRate };
100
+ }
101
+
102
+ /**
103
+ * Encode PCM (as returned by decodeAudio) with WebCodecs' AudioEncoder.
104
+ * @param {{ sampleRate: number, numberOfChannels: number, channels: Float32Array[] }} pcm
105
+ * @param {{ codec?: 'aac' | 'opus', config?: AudioEncoderConfig, bitrate?: number, maxDuration?: number,
106
+ * onChunk: (chunk: EncodedAudioChunk, meta?: EncodedAudioChunkMetadata) => void }} opts
107
+ * config: from findAudioConfig(); otherwise built from codec + bitrate. PCM at a different rate is resampled.
108
+ * maxDuration: seconds; longer audio is cut (used to match the video's length)
109
+ */
110
+ export async function encodeAudio(pcm, opts) {
111
+ if (typeof AudioEncoder === 'undefined') throw new Error('This browser has no WebCodecs AudioEncoder');
112
+ const config = opts.config || {
113
+ codec: AUDIO_CODECS[opts.codec], sampleRate: pcm.sampleRate, numberOfChannels: pcm.numberOfChannels,
114
+ bitrate: opts.bitrate || 128000,
115
+ ...(opts.codec === 'aac' ? { aac: { format: 'aac' } } : {}),
116
+ };
117
+ if (config.sampleRate !== pcm.sampleRate) pcm = await resamplePcm(pcm, config.sampleRate);
118
+ const { sampleRate, numberOfChannels, channels } = pcm;
119
+ let error = null;
120
+ const encoder = new AudioEncoder({ output: (chunk, meta) => opts.onChunk(chunk, meta), error: e => { error = e; } });
121
+ encoder.configure(config);
122
+ const limit = Number.isFinite(opts.maxDuration) ? Math.floor(opts.maxDuration * sampleRate) : Infinity;
123
+ const total = Math.min(channels[0].length, limit);
124
+ const BLOCK = Math.round(sampleRate / 10); // 100 ms per AudioData
125
+ for (let o = 0; o < total; o += BLOCK) {
126
+ if (error) break;
127
+ const n = Math.min(BLOCK, total - o);
128
+ const data = new Float32Array(n * numberOfChannels);
129
+ for (let c = 0; c < numberOfChannels; c++) data.set(channels[c].subarray(o, o + n), c * n);
130
+ const frame = new AudioData({ format: 'f32-planar', sampleRate, numberOfFrames: n, numberOfChannels, timestamp: Math.round(o * 1e6 / sampleRate), data });
131
+ encoder.encode(frame);
132
+ frame.close();
133
+ if (encoder.encodeQueueSize > 16) await new Promise(r => setTimeout(r, 0));
134
+ }
135
+ if (!error) await encoder.flush().catch(e => { error = e; });
136
+ if (encoder.state !== 'closed') encoder.close();
137
+ if (error) throw error;
138
+ }
139
+
140
+ /** OpusHead (RFC 7845), little-endian: what Matroska's CodecPrivate holds for Opus. */
141
+ export function opusHead(numberOfChannels, sampleRate, preSkip = 312) {
142
+ const b = new Uint8Array(19);
143
+ b.set([0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64]); // 'OpusHead'
144
+ const dv = new DataView(b.buffer);
145
+ dv.setUint8(8, 1);
146
+ dv.setUint8(9, numberOfChannels);
147
+ dv.setUint16(10, preSkip, true);
148
+ dv.setUint32(12, sampleRate, true);
149
+ dv.setInt16(16, 0, true);
150
+ dv.setUint8(18, 0);
151
+ return b;
152
+ }
153
+
154
+ /** Read { channels, preSkip, sampleRate } from an OpusHead, or null if it isn't one. */
155
+ export function parseOpusHead(b) {
156
+ if (!b || b.length < 19 || String.fromCharCode(...b.subarray(0, 8)) !== 'OpusHead') return null;
157
+ const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
158
+ return { channels: dv.getUint8(9), preSkip: dv.getUint16(10, true), sampleRate: dv.getUint32(12, true) };
159
+ }
@@ -67,6 +67,44 @@ export async function sniffVideoCodec(file) {
67
67
  return '';
68
68
  }
69
69
 
70
+ /**
71
+ * Does this MP4/MOV/WebM file have an audio track? Reads only the container headers: for MP4/MOV the
72
+ * top-level boxes are walked to 'moov' (which phones often write at the end) and only that box is read.
73
+ * @param {Blob} file
74
+ * @returns {Promise<boolean | null>} null when it can't tell
75
+ */
76
+ export async function hasAudioTrack(file) {
77
+ try {
78
+ const head = new Uint8Array(await file.slice(0, 12).arrayBuffer());
79
+ if (head[0] === 0x1a && head[1] === 0x45 && head[2] === 0xdf && head[3] === 0xa3) {
80
+ // WebM/Matroska: Tracks sit near the start; audio CodecIDs all begin with A_
81
+ const text = new TextDecoder('latin1').decode(await file.slice(0, 4 << 20).arrayBuffer());
82
+ return /A_(OPUS|VORBIS|AAC|MPEG|AC3|EAC3|FLAC|PCM|DTS|TRUEHD|ALAC)/.test(text);
83
+ }
84
+ for (let o = 0; o + 8 <= file.size;) {
85
+ const dv = new DataView(await file.slice(o, o + 16).arrayBuffer());
86
+ let size = dv.getUint32(0);
87
+ const type = String.fromCharCode(dv.getUint8(4), dv.getUint8(5), dv.getUint8(6), dv.getUint8(7));
88
+ if (size === 1) size = Number(dv.getBigUint64(8));
89
+ else if (size === 0) size = file.size - o;
90
+ if (size < 8) return null;
91
+ if (type === 'moov') {
92
+ const moov = new Uint8Array(await file.slice(o, o + size).arrayBuffer());
93
+ // each track's 'hdlr' box names its kind 12 bytes after the box type: 'vide', 'soun', …
94
+ for (let i = 4; i + 16 <= moov.length; i++) {
95
+ if (moov[i] === 0x68 && moov[i + 1] === 0x64 && moov[i + 2] === 0x6c && moov[i + 3] === 0x72
96
+ && moov[i + 12] === 0x73 && moov[i + 13] === 0x6f && moov[i + 14] === 0x75 && moov[i + 15] === 0x6e) return true;
97
+ }
98
+ return false;
99
+ }
100
+ o += size;
101
+ }
102
+ return null;
103
+ } catch (_) {
104
+ return null;
105
+ }
106
+ }
107
+
70
108
  /** Seek and resolve once the frame at `time` is decoded and paintable. */
71
109
  export async function seekTo(video, time) {
72
110
  time = Math.max(0, Math.min(time, (video.duration || 0) - 1e-4));