buninu-browser 0.5.1 → 0.6.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/README.md +9 -3
- package/lib/audio/pcm.js +11 -0
- package/lib/audio/pulse-client.js +7 -2
- package/lib/cdp-server.js +31 -7
- package/lib/happy-dom/document-base.js +29 -1
- package/lib/happy-dom/mutation-observer.js +157 -0
- package/lib/happy-dom/parser.js +2 -0
- package/lib/headless-shell.js +49 -9
- package/lib/layout/repaint.js +89 -0
- package/lib/media/mp4-keyframes.js +144 -0
- package/lib/media/video-keyframe.js +29 -1
- package/lib/media/webm-snapshots.js +282 -0
- package/lib/network/cookie-jar.js +18 -2
- package/lib/paint/display-list.js +4 -3
- package/lib/paint/frame.js +12 -5
- package/lib/render-tree/index.js +3 -0
- package/lib/renderer/animation-frames.js +76 -0
- package/lib/renderer/bindings.js +2 -0
- package/lib/renderer/compositor-channel.js +95 -10
- package/lib/renderer/compositor.js +54 -18
- package/lib/renderer/document-cookies.js +87 -0
- package/lib/renderer/font-loading.js +60 -16
- package/lib/renderer/host.js +48 -19
- package/lib/renderer/idle-callbacks.js +109 -0
- package/lib/renderer/main.js +5 -1
- package/lib/renderer/media-source.js +4 -0
- package/lib/renderer/page-realm.js +159 -49
- package/lib/renderer/page-renderer.js +567 -197
- package/lib/renderer/resource-elements.js +88 -0
- package/lib/renderer/script-rewrite.js +18 -1
- package/lib/renderer/seccomp.js +6 -4
- package/lib/renderer/timers.js +71 -0
- package/lib/renderer/video-channel.js +94 -0
- package/lib/renderer/video-mask.js +115 -0
- package/lib/renderer/video-presentation.js +304 -39
- package/lib/renderer/video-worker.js +46 -0
- package/lib/renderer/workers.js +7 -17
- package/lib/style/computed-style.js +257 -61
- package/package.json +1 -1
- package/test/animation-frames.test.js +67 -0
- package/test/canvas-style-snapshot.test.js +18 -0
- package/test/cdp-stream.test.js +96 -3
- package/test/compositor-channel-ownership.test.js +194 -0
- package/test/compositor.test.js +27 -1
- package/test/computed-style-binding.test.js +49 -0
- package/test/computed-style-connectivity.test.js +13 -0
- package/test/computed-style.test.js +86 -0
- package/test/cookie-sync.test.js +82 -0
- package/test/document-base-cache.test.js +103 -0
- package/test/document-cookies.test.js +117 -0
- package/test/duplicate-style-keys.test.js +11 -0
- package/test/fixtures/animation-frames.js +56 -0
- package/test/fixtures/canvas-style-snapshot.js +81 -0
- package/test/fixtures/computed-style-connectivity.js +101 -0
- package/test/fixtures/computed-style-lazy.js +135 -0
- package/test/fixtures/duplicate-style-keys.js +97 -0
- package/test/fixtures/fake-renderer.js +12 -0
- package/test/fixtures/font-definition-invalidation.js +97 -0
- package/test/fixtures/font-metrics-invalidation.js +87 -0
- package/test/fixtures/full-style-identity.js +36 -0
- package/test/fixtures/hidden-style-engine.js +260 -0
- package/test/fixtures/hidden-style-reentry.js +232 -0
- package/test/fixtures/hidden-style-renderer.js +203 -0
- package/test/fixtures/layout-repaint.js +69 -0
- package/test/fixtures/media-presentation-continuation.js +141 -0
- package/test/fixtures/media-presentation-refill.js +113 -0
- package/test/fixtures/media-probe.js +148 -0
- package/test/fixtures/media-refill-cancel.js +79 -0
- package/test/fixtures/media-repaint.js +48 -0
- package/test/fixtures/media-resource-reselection.js +94 -0
- package/test/fixtures/media-source-attachment.js +136 -0
- package/test/fixtures/media-source-no-decoder.js +17 -0
- package/test/fixtures/media-track-refill.js +132 -0
- package/test/fixtures/media-webm-seek.js +51 -0
- package/test/fixtures/mutation-observer.js +201 -0
- package/test/fixtures/observer-invalidation.js +105 -0
- package/test/fixtures/prepaint-scheduling.js +73 -0
- package/test/fixtures/resource-discovery.js +32 -0
- package/test/fixtures/spare-renderer-scheduling.js +124 -0
- package/test/fixtures/style-propagation.js +26 -0
- package/test/fixtures/svg-node-reuse.js +83 -0
- package/test/fixtures/video-black-replay.js +140 -0
- package/test/fixtures/video-controls-invalidation.js +71 -0
- package/test/fixtures/viewport-bounds.js +148 -0
- package/test/font-loading.test.js +77 -0
- package/test/font-metrics-invalidation.test.js +60 -0
- package/test/full-style-identity.test.js +191 -0
- package/test/hidden-style-lazy.test.js +31 -0
- package/test/hidden-style-reentry.test.js +20 -0
- package/test/idle-callback.test.js +89 -0
- package/test/idle-scheduler.test.js +148 -0
- package/test/inline-style-cascade.test.js +63 -0
- package/test/layout-repaint-renderer.test.js +9 -0
- package/test/layout-repaint.test.js +222 -0
- package/test/media-presentation-continuation.test.js +14 -0
- package/test/media-presentation-refill.test.js +8 -0
- package/test/media-probe.test.js +16 -0
- package/test/media-repaint.test.js +8 -0
- package/test/media-source-attachment.test.js +34 -0
- package/test/media-track-refill.test.js +26 -0
- package/test/media-webm-seek.test.js +11 -0
- package/test/mp4-keyframes.test.js +36 -0
- package/test/mutation-observer.test.js +91 -0
- package/test/observer-invalidation.test.js +12 -0
- package/test/page-scripts.test.js +35 -0
- package/test/page-timers.test.js +67 -0
- package/test/paint-frame.test.js +18 -1
- package/test/pcm.test.js +63 -0
- package/test/prepaint-scheduling.test.js +13 -0
- package/test/pulse-client.test.js +165 -0
- package/test/raw-capture.test.js +34 -5
- package/test/renderer-host.test.js +51 -0
- package/test/resource-discovery.test.js +12 -0
- package/test/resource-elements.test.js +234 -0
- package/test/root-self-style.test.js +104 -0
- package/test/script-rewrite.test.js +35 -0
- package/test/seccomp.test.js +45 -0
- package/test/spare-renderer-scheduling.test.js +8 -0
- package/test/style-propagation.test.js +225 -0
- package/test/style-sheets.test.js +19 -0
- package/test/svg.test.js +45 -0
- package/test/timers.test.js +58 -0
- package/test/video-black-replay.test.js +34 -0
- package/test/video-capture.test.js +170 -0
- package/test/video-channel.test.js +253 -0
- package/test/video-continuation.test.js +57 -0
- package/test/video-controls-invalidation.test.js +12 -0
- package/test/video-keyframe.test.js +104 -4
- package/test/video-mask.test.js +94 -0
- package/test/video-presentation.test.js +239 -1
- package/test/video-timing-presentation.test.js +115 -0
- package/test/viewport-bounds.test.js +13 -0
- package/test/webm-snapshots.test.js +372 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Fragmented MP4 sync-sample decode times are seek candidates, not decoded
|
|
2
|
+
// presentation timestamps. The caller must verify them against its decoder.
|
|
3
|
+
// Unsupported layouts return null and retain the ordinary seek search.
|
|
4
|
+
const cache = new WeakMap();
|
|
5
|
+
const MAX_BOXES = 32768, MAX_SAMPLES = 250000, MAX_KEYS = 16384;
|
|
6
|
+
|
|
7
|
+
export function mp4KeyframeBefore(bytes, seconds) {
|
|
8
|
+
if (!(bytes instanceof Uint8Array) || !Number.isFinite(seconds)) return null;
|
|
9
|
+
let index = cache.get(bytes);
|
|
10
|
+
if (index === undefined) {
|
|
11
|
+
try { index = inspect(bytes); } catch { index = null; }
|
|
12
|
+
cache.set(bytes, index);
|
|
13
|
+
}
|
|
14
|
+
if (!index?.length) return null;
|
|
15
|
+
let low = 0, high = index.length;
|
|
16
|
+
while (low < high) {
|
|
17
|
+
const mid = (low + high) >>> 1;
|
|
18
|
+
if (index[mid] <= seconds) low = mid + 1;
|
|
19
|
+
else high = mid;
|
|
20
|
+
}
|
|
21
|
+
return low ? index[low - 1] : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function inspect(bytes) {
|
|
25
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
26
|
+
let boxesSeen = 0, samplesSeen = 0;
|
|
27
|
+
const u32 = (at, end) => {
|
|
28
|
+
if (at < 0 || at + 4 > end) throw Error('short MP4 field');
|
|
29
|
+
return view.getUint32(at);
|
|
30
|
+
};
|
|
31
|
+
const u64 = (at, end) => {
|
|
32
|
+
const value = u32(at, end) * 0x100000000 + u32(at + 4, end);
|
|
33
|
+
if (!Number.isSafeInteger(value)) throw Error('large MP4 timestamp');
|
|
34
|
+
return value;
|
|
35
|
+
};
|
|
36
|
+
const boxes = (from, end) => {
|
|
37
|
+
const result = [];
|
|
38
|
+
for (let at = from; at < end;) {
|
|
39
|
+
if (++boxesSeen > MAX_BOXES) throw Error('too many MP4 boxes');
|
|
40
|
+
let size = u32(at, end), header = 8;
|
|
41
|
+
const type = String.fromCharCode(...bytes.subarray(at + 4, at + 8));
|
|
42
|
+
if (at + header > end) throw Error('short MP4 box');
|
|
43
|
+
if (size === 1) { size = u64(at + 8, end); header = 16; }
|
|
44
|
+
else if (size === 0) size = end - at;
|
|
45
|
+
if (size < header || size > end - at) throw Error('invalid MP4 box size');
|
|
46
|
+
result.push({ type, data: at + header, end: at + size });
|
|
47
|
+
at += size;
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
const children = box => boxes(box.data, box.end);
|
|
52
|
+
const one = (list, type) => {
|
|
53
|
+
const matches = list.filter(box => box.type === type);
|
|
54
|
+
if (matches.length !== 1) throw Error(`missing/duplicate ${type}`);
|
|
55
|
+
return matches[0];
|
|
56
|
+
};
|
|
57
|
+
const version = box => {
|
|
58
|
+
u32(box.data, box.end);
|
|
59
|
+
if (bytes[box.data] > 1) throw Error('unknown MP4 version');
|
|
60
|
+
return bytes[box.data];
|
|
61
|
+
};
|
|
62
|
+
const flags = box => u32(box.data, box.end) & 0xffffff;
|
|
63
|
+
const top = boxes(0, bytes.length), movie = children(one(top, 'moov'));
|
|
64
|
+
const defaults = new Map();
|
|
65
|
+
for (const box of children(one(movie, 'mvex'))) if (box.type === 'trex') {
|
|
66
|
+
if (version(box) !== 0 || defaults.size >= 32) return null;
|
|
67
|
+
const id = u32(box.data + 4, box.end);
|
|
68
|
+
if (defaults.has(id) || u32(box.data + 8, box.end) !== 1) return null;
|
|
69
|
+
defaults.set(id, { duration: u32(box.data + 12, box.end), flags: u32(box.data + 20, box.end) });
|
|
70
|
+
}
|
|
71
|
+
let track = null;
|
|
72
|
+
for (const box of movie) if (box.type === 'trak') {
|
|
73
|
+
const nodes = children(box), media = children(one(nodes, 'mdia'));
|
|
74
|
+
const handler = one(media, 'hdlr');
|
|
75
|
+
if (u32(handler.data + 8, handler.end) !== 0x76696465) continue; // vide
|
|
76
|
+
if (track) return null;
|
|
77
|
+
const edits = nodes.filter(box => box.type === 'edts');
|
|
78
|
+
if (edits.length > 1) return null;
|
|
79
|
+
if (edits.length) {
|
|
80
|
+
const edit = one(children(edits[0]), 'elst'), wide = version(edit) === 1;
|
|
81
|
+
if (u32(edit.data + 4, edit.end) !== 1) return null;
|
|
82
|
+
const mediaTimeAt = edit.data + (wide ? 16 : 12), rateAt = edit.data + (wide ? 24 : 16);
|
|
83
|
+
if (u32(mediaTimeAt, edit.end) & 0x80000000 || u32(rateAt, edit.end) !== 0x00010000) return null;
|
|
84
|
+
// Only a single forward unit-rate edit is considered. Its offset is
|
|
85
|
+
// deliberately not presented as a decoder timestamp: seek boundary
|
|
86
|
+
// verification must still succeed before this candidate can be used.
|
|
87
|
+
}
|
|
88
|
+
const header = one(nodes, 'tkhd'), timing = one(media, 'mdhd');
|
|
89
|
+
const id = u32(header.data + (version(header) ? 20 : 12), header.end);
|
|
90
|
+
const timescale = u32(timing.data + (version(timing) ? 20 : 12), timing.end);
|
|
91
|
+
if (!timescale || !defaults.has(id)) return null;
|
|
92
|
+
track = { id, timescale, ...defaults.get(id) };
|
|
93
|
+
}
|
|
94
|
+
if (!track) return null;
|
|
95
|
+
const keys = [];
|
|
96
|
+
let lastDecodeTime = -1;
|
|
97
|
+
for (const moof of top) if (moof.type === 'moof') {
|
|
98
|
+
for (const traf of children(moof)) if (traf.type === 'traf') {
|
|
99
|
+
const nodes = children(traf), header = one(nodes, 'tfhd');
|
|
100
|
+
if (u32(header.data + 4, header.end) !== track.id) continue;
|
|
101
|
+
if (version(header) !== 0 || nodes.some(box => box.type === 'sbgp' || box.type === 'sgpd')) return null;
|
|
102
|
+
const hf = flags(header);
|
|
103
|
+
if (hf & ~0x02003b) return null;
|
|
104
|
+
let cursor = header.data + 8, duration = track.duration, sampleFlags = track.flags;
|
|
105
|
+
if (hf & 1) cursor += 8;
|
|
106
|
+
if (hf & 2) { if (u32(cursor, header.end) !== 1) return null; cursor += 4; }
|
|
107
|
+
if (hf & 8) { duration = u32(cursor, header.end); cursor += 4; }
|
|
108
|
+
if (hf & 16) cursor += 4;
|
|
109
|
+
if (hf & 32) { sampleFlags = u32(cursor, header.end); cursor += 4; }
|
|
110
|
+
if (cursor > header.end) return null;
|
|
111
|
+
const timing = one(nodes, 'tfdt');
|
|
112
|
+
let time = version(timing) ? u64(timing.data + 4, timing.end) : u32(timing.data + 4, timing.end);
|
|
113
|
+
for (const run of nodes) if (run.type === 'trun') {
|
|
114
|
+
version(run);
|
|
115
|
+
const rf = flags(run), count = u32(run.data + 4, run.end);
|
|
116
|
+
if (rf & ~0xf05 || (rf & 4) && (rf & 0x400)) return null;
|
|
117
|
+
samplesSeen += count;
|
|
118
|
+
if (samplesSeen > MAX_SAMPLES) return null;
|
|
119
|
+
cursor = run.data + 8;
|
|
120
|
+
if (rf & 1) cursor += 4;
|
|
121
|
+
let firstFlags = sampleFlags;
|
|
122
|
+
if (rf & 4) { firstFlags = u32(cursor, run.end); cursor += 4; }
|
|
123
|
+
for (let i = 0; i < count; i++) {
|
|
124
|
+
let step = duration, sf = i === 0 ? firstFlags : sampleFlags;
|
|
125
|
+
if (rf & 0x100) { step = u32(cursor, run.end); cursor += 4; }
|
|
126
|
+
if (rf & 0x200) cursor += 4;
|
|
127
|
+
if (rf & 0x400) { sf = u32(cursor, run.end); cursor += 4; }
|
|
128
|
+
if (rf & 0x800) cursor += 4; // composition offset does not change decode time
|
|
129
|
+
if (cursor > run.end || !step || time < lastDecodeTime) return null;
|
|
130
|
+
const depends = (sf >>> 24) & 3;
|
|
131
|
+
if (depends === 3) return null;
|
|
132
|
+
if (!(sf & 0x10000) && depends !== 1) {
|
|
133
|
+
if (keys.length >= MAX_KEYS) return null;
|
|
134
|
+
keys.push(time / track.timescale);
|
|
135
|
+
}
|
|
136
|
+
lastDecodeTime = time;
|
|
137
|
+
time += step;
|
|
138
|
+
if (!Number.isSafeInteger(time)) return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return keys;
|
|
144
|
+
}
|
|
@@ -1,20 +1,48 @@
|
|
|
1
1
|
// wasmpeg seeks to a keyframe without returning its timestamp. Locate it by
|
|
2
2
|
// comparing tiny decoded probes, then rewind before delivering full frames.
|
|
3
|
-
|
|
3
|
+
import { mp4KeyframeBefore } from './mp4-keyframes.js';
|
|
4
|
+
|
|
5
|
+
export function seekVideoKeyframe(decoder, seconds, startTime = 0, bytes = null) {
|
|
4
6
|
const start = Math.max(0, startTime);
|
|
5
7
|
seconds = Math.max(start, seconds);
|
|
6
8
|
const seek = time => decoder.seek(Math.max(1, time * 1000));
|
|
7
9
|
const frame = 1 / (decoder.fps || 30);
|
|
8
10
|
if (!(seconds >= start + frame)) { seek(start); return start; }
|
|
11
|
+
let lastPixels = null;
|
|
9
12
|
const probe = time => {
|
|
10
13
|
seek(time);
|
|
11
14
|
const pixels = decoder.nextFrame(32, 18);
|
|
15
|
+
lastPixels = pixels;
|
|
12
16
|
if (!pixels) return null;
|
|
13
17
|
let hash = 0x811c9dc5;
|
|
14
18
|
for (let i = 0; i < pixels.length; i += 3) hash = Math.imul(hash ^ pixels[i], 0x01000193);
|
|
15
19
|
return hash >>> 0;
|
|
16
20
|
};
|
|
17
21
|
const target = probe(seconds);
|
|
22
|
+
const candidate = bytes && mp4KeyframeBefore(bytes, seconds);
|
|
23
|
+
if (target !== null && candidate !== null && candidate >= start && frame >= 0.001) {
|
|
24
|
+
// A container index narrows the search; it cannot establish what a
|
|
25
|
+
// decoder actually returned. Require the same complete tiny picture at
|
|
26
|
+
// the candidate and a different one just before its seek boundary.
|
|
27
|
+
// This preserves the existing decode-time convention, not guessed PTS.
|
|
28
|
+
const targetPixels = lastPixels.slice();
|
|
29
|
+
const samePicture = () => lastPixels?.length === targetPixels.length
|
|
30
|
+
&& targetPixels.every((byte, i) => byte === lastPixels[i]);
|
|
31
|
+
// The decoder's seek API rounds to integer milliseconds; do not round
|
|
32
|
+
// a fractional keyframe time down into the preceding GOP.
|
|
33
|
+
const candidateSeek = Math.ceil(candidate * 1000) / 1000;
|
|
34
|
+
try {
|
|
35
|
+
probe(candidateSeek);
|
|
36
|
+
if (samePicture()) {
|
|
37
|
+
const atStart = candidate === start;
|
|
38
|
+
if (!atStart) probe(Math.max(start, candidate - frame));
|
|
39
|
+
if (atStart || (lastPixels && !samePicture())) { seek(candidateSeek); return candidate; }
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// An optional index hint must not turn a working decoder search into
|
|
43
|
+
// an open failure. Errors from the original search still propagate.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
18
46
|
let keyframe = seconds;
|
|
19
47
|
if (target !== null) {
|
|
20
48
|
let distance = 10;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { ebmlHeader } from './webm-stream.js';
|
|
2
|
+
|
|
3
|
+
const CLUSTER = 0x1f43b675;
|
|
4
|
+
const MAX_RECORDS = 250_000;
|
|
5
|
+
const OPUS_RATE = 48_000;
|
|
6
|
+
const join = parts => parts.length === 1 ? parts[0] : new Uint8Array(Bun.concatArrayBuffers(parts));
|
|
7
|
+
|
|
8
|
+
// Decode-time snapshots for a single Opus track. A PCM-only decoder cannot
|
|
9
|
+
// represent holes in a WebM timeline: concatenating 0..2s and 6..8s produces
|
|
10
|
+
// four seconds of samples, not eight. Keep each contiguous run independently
|
|
11
|
+
// decodable, without changing any packet or inventing samples for the hole.
|
|
12
|
+
export function createWebMAudioSnapshots() {
|
|
13
|
+
const headers = [], records = [];
|
|
14
|
+
let scale = 1_000_000, track = null, valid = true, runs = null, serial = 0, ordered = true;
|
|
15
|
+
let preroll = 1, cached = null, initHeaders = null;
|
|
16
|
+
const uint = (bytes, from, size) => {
|
|
17
|
+
if (size < 1 || size > 6 || from + size > bytes.length) throw Error('invalid integer');
|
|
18
|
+
let value = 0;
|
|
19
|
+
for (let i = 0; i < size; i++) value = value * 256 + bytes[from + i];
|
|
20
|
+
return value;
|
|
21
|
+
};
|
|
22
|
+
const children = (bytes, from, end) => {
|
|
23
|
+
const result = [];
|
|
24
|
+
for (let at = from; at < end;) {
|
|
25
|
+
if (result.length >= 65_536) throw Error('too many elements');
|
|
26
|
+
const head = ebmlHeader(bytes, at);
|
|
27
|
+
if (!head || !Number.isFinite(head.size) || head.size > end - at - head.length) throw Error('incomplete element');
|
|
28
|
+
const data = at + head.length;
|
|
29
|
+
result.push({ id: head.id, start: at, data, size: head.size, end: data + head.size });
|
|
30
|
+
at = data + head.size;
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
const payload = bytes => {
|
|
35
|
+
const head = ebmlHeader(bytes);
|
|
36
|
+
if (!head || head.size !== bytes.length - head.length) throw Error('incomplete element');
|
|
37
|
+
return children(bytes, head.length, bytes.length);
|
|
38
|
+
};
|
|
39
|
+
const opusSamples = (bytes, at, end) => {
|
|
40
|
+
if (at >= end) throw Error('empty packet');
|
|
41
|
+
const toc = bytes[at], config = toc >> 3;
|
|
42
|
+
const frame = config >= 16 ? 2.5 * 2 ** (config & 3)
|
|
43
|
+
: config >= 12 ? 10 * 2 ** (config & 1) : [10, 20, 40, 60][config & 3];
|
|
44
|
+
const code = toc & 3;
|
|
45
|
+
if (code === 3 && at + 1 >= end) throw Error('incomplete packet');
|
|
46
|
+
const count = code === 0 ? 1 : code === 3 ? bytes[at + 1] & 63 : 2;
|
|
47
|
+
const samples = frame * count * 48;
|
|
48
|
+
if (!count || samples > 5760) throw Error('invalid Opus duration');
|
|
49
|
+
return samples;
|
|
50
|
+
};
|
|
51
|
+
const blockRange = (bytes, block, origin) => {
|
|
52
|
+
const from = block.data;
|
|
53
|
+
if (!bytes[from]) throw Error('invalid track');
|
|
54
|
+
let length = 1, marker = 0x80;
|
|
55
|
+
while (!(bytes[from] & marker)) { length++; marker >>= 1; }
|
|
56
|
+
if (length > 6 || from + length + 3 > block.end) throw Error('invalid block');
|
|
57
|
+
let number = bytes[from] & (marker - 1);
|
|
58
|
+
for (let i = 1; i < length; i++) number = number * 256 + bytes[from + i];
|
|
59
|
+
if (number !== track) throw Error('another track');
|
|
60
|
+
const at = from + length;
|
|
61
|
+
// Lacing requires a separate packet table; leave that format on the
|
|
62
|
+
// ordinary decoder path until its timing can be proved as well.
|
|
63
|
+
if (bytes[at + 2] & 6) throw Error('laced block');
|
|
64
|
+
const relative = new DataView(bytes.buffer, bytes.byteOffset + at, 2).getInt16(0);
|
|
65
|
+
const start = (origin + relative) * scale / 1e9;
|
|
66
|
+
if (!Number.isFinite(start) || start < 0) throw Error('invalid timestamp');
|
|
67
|
+
const samples = opusSamples(bytes, at + 3, block.end);
|
|
68
|
+
return { start, end: start + samples / OPUS_RATE, samples };
|
|
69
|
+
};
|
|
70
|
+
const record = (element) => {
|
|
71
|
+
const { bytes, cluster } = element;
|
|
72
|
+
const items = cluster ? children(bytes, 0, bytes.length) : payload(bytes);
|
|
73
|
+
if (!cluster) {
|
|
74
|
+
// A finite Cluster can contain many seconds of Opus. Index its complete
|
|
75
|
+
// packets just like a live Cluster, so reopening keeps the requested
|
|
76
|
+
// preroll instead of decoding everything from the Cluster's beginning.
|
|
77
|
+
// Only the size field changes; timestamps and encoded Blocks stay intact.
|
|
78
|
+
const head = ebmlHeader(bytes), header = bytes.slice(0, head.length);
|
|
79
|
+
const width = head.length - 4; // Cluster ID is always four bytes.
|
|
80
|
+
header[4] = (1 << (9 - width)) - 1;
|
|
81
|
+
header.fill(0xff, 5);
|
|
82
|
+
const prefix = [header], blocks = [];
|
|
83
|
+
let time = null;
|
|
84
|
+
for (const item of items) {
|
|
85
|
+
if (item.id === 0xa3 || item.id === 0xa0) {
|
|
86
|
+
blocks.push(bytes.subarray(item.start, item.end));
|
|
87
|
+
} else if (!blocks.length && (item.id === 0xec || (item.id === 0xe7 && time == null))) {
|
|
88
|
+
if (item.id === 0xe7) time = uint(bytes, item.data, item.size);
|
|
89
|
+
prefix.push(bytes.subarray(item.start, item.end));
|
|
90
|
+
} else {
|
|
91
|
+
// CRC/Position/PrevSize describe the original complete Cluster;
|
|
92
|
+
// encrypted or unknown metadata cannot safely survive this cut.
|
|
93
|
+
throw Error('unsupported finite cluster metadata');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (time == null) throw Error('missing cluster time');
|
|
97
|
+
// This prefix cannot grow, unlike a live Cluster's mutable metadata.
|
|
98
|
+
// Share it across packet records instead of copying metadata N times.
|
|
99
|
+
Object.freeze(prefix);
|
|
100
|
+
const token = {};
|
|
101
|
+
for (const block of blocks) record({ bytes: block, cluster: { token, time, prefix, sharedPrefix: true } });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
let origin = cluster?.time;
|
|
105
|
+
if (origin == null) {
|
|
106
|
+
const time = items.find(item => item.id === 0xe7);
|
|
107
|
+
if (!time) throw Error('missing cluster time');
|
|
108
|
+
origin = uint(bytes, time.data, time.size);
|
|
109
|
+
}
|
|
110
|
+
let start = null, end = null, samples = 0, padded = false;
|
|
111
|
+
for (const item of items) {
|
|
112
|
+
let block = item, padding = 0;
|
|
113
|
+
if (item.id === 0xa0) {
|
|
114
|
+
const group = children(bytes, item.data, item.end);
|
|
115
|
+
block = group.find(child => child.id === 0xa1);
|
|
116
|
+
const discard = group.find(child => child.id === 0x75a2);
|
|
117
|
+
if (discard) {
|
|
118
|
+
if (bytes[discard.data] & 0x80) throw Error('negative discard padding');
|
|
119
|
+
padding = uint(bytes, discard.data, discard.size) / 1e9;
|
|
120
|
+
}
|
|
121
|
+
} else if (item.id !== 0xa3) continue;
|
|
122
|
+
if (!block) throw Error('missing block');
|
|
123
|
+
const range = blockRange(bytes, block, origin);
|
|
124
|
+
range.end -= padding;
|
|
125
|
+
if (range.end <= range.start) throw Error('invalid padding');
|
|
126
|
+
if (end != null && Math.abs(range.start - end) > scale / 1e9 + 1e-6) throw Error('discontinuous cluster');
|
|
127
|
+
start ??= range.start;
|
|
128
|
+
end = range.end;
|
|
129
|
+
samples += range.samples;
|
|
130
|
+
padded ||= padding > 0;
|
|
131
|
+
}
|
|
132
|
+
if (start == null) return;
|
|
133
|
+
if (records.length >= MAX_RECORDS) throw Error('too many blocks');
|
|
134
|
+
if ((cluster?.prefix.length ?? 0) > 65_536) throw Error('too much cluster metadata');
|
|
135
|
+
initHeaders ??= headers.slice();
|
|
136
|
+
const inOrder = !records.length || start >= records.at(-1).start;
|
|
137
|
+
if (!inOrder) ordered = false;
|
|
138
|
+
const entry = { id: ++serial, start, end, samples, padded, bytes, token: cluster?.token,
|
|
139
|
+
prefix: cluster && (cluster.sharedPrefix ? cluster.prefix : cluster.prefix.slice()) };
|
|
140
|
+
records.push(entry);
|
|
141
|
+
// Ordered appends update only the final run, even for live Clusters which
|
|
142
|
+
// emit one record per packet. An out-of-order append rebuilds once on use.
|
|
143
|
+
if (runs && inOrder && start >= runs.at(-1).end - scale / 1e9 - 1e-6) addEntry(runs, entry);
|
|
144
|
+
else runs = null;
|
|
145
|
+
cached = null;
|
|
146
|
+
};
|
|
147
|
+
const addEntry = (result, entry) => {
|
|
148
|
+
let run = result.at(-1);
|
|
149
|
+
const tolerance = scale / 1e9 + 1e-6;
|
|
150
|
+
if (run && entry.start < run.end - tolerance) throw Error('overlapping clusters');
|
|
151
|
+
if (!run || entry.start > run.end + tolerance) {
|
|
152
|
+
run = { id: entry.id, startTime: entry.start, end: entry.end,
|
|
153
|
+
startSample: Math.round(entry.start * OPUS_RATE), headers: initHeaders, entries: [], samples: 0, exact: true };
|
|
154
|
+
result.push(run);
|
|
155
|
+
}
|
|
156
|
+
// Packet sample counts are exact; millisecond Block timestamps are not.
|
|
157
|
+
// Reopening reapplies Opus pre-skip, cancelling the initial decoder's
|
|
158
|
+
// pre-skip, so the new origin advances by encoded samples, not rounded PTS.
|
|
159
|
+
const timestamp = run.startTime + run.samples / OPUS_RATE;
|
|
160
|
+
// Round the initial cursor once, as openAudioDecoder does, then add integer
|
|
161
|
+
// samples. Rounding (fractional origin + elapsed time) can move a sample.
|
|
162
|
+
entry.origin = (run.startSample + run.samples) / OPUS_RATE;
|
|
163
|
+
entry.seekable = run.exact && Math.abs(entry.start - timestamp) <= tolerance;
|
|
164
|
+
run.samples += entry.samples;
|
|
165
|
+
run.exact &&= entry.seekable && !entry.padded && Number.isSafeInteger(run.samples);
|
|
166
|
+
run.entries.push(entry);
|
|
167
|
+
run.end = entry.end;
|
|
168
|
+
};
|
|
169
|
+
const buildRuns = () => {
|
|
170
|
+
const sorted = ordered ? records : records.slice().sort((a, b) => a.start - b.start);
|
|
171
|
+
const result = [];
|
|
172
|
+
for (const entry of sorted) addEntry(result, entry);
|
|
173
|
+
return result;
|
|
174
|
+
};
|
|
175
|
+
return {
|
|
176
|
+
// Called only after SourceBuffer's existing retry/deduplication checks.
|
|
177
|
+
append(element) {
|
|
178
|
+
if (!valid) return;
|
|
179
|
+
try {
|
|
180
|
+
const { id, bytes } = element;
|
|
181
|
+
if (id === CLUSTER) {
|
|
182
|
+
if (track == null) throw Error('missing supported track');
|
|
183
|
+
record(element);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (records.length) {
|
|
187
|
+
// Metadata at EOF does not belong in a run; a new initialization
|
|
188
|
+
// needs a separate codec epoch, which this small index declines.
|
|
189
|
+
if ([0x1a45dfa3, 0x18538067, 0x1549a966, 0x1654ae6b].includes(id)) throw Error('new initialization');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (id === 0x1549a966) {
|
|
193
|
+
const time = payload(bytes).find(item => item.id === 0x2ad7b1);
|
|
194
|
+
if (time) scale = uint(bytes, time.data, time.size);
|
|
195
|
+
if (!(scale > 0)) throw Error('invalid timestamp scale');
|
|
196
|
+
} else if (id === 0x1654ae6b) {
|
|
197
|
+
const tracks = payload(bytes).filter(item => item.id === 0xae);
|
|
198
|
+
if (tracks.length !== 1 || track != null) throw Error('multiple tracks');
|
|
199
|
+
const fields = children(bytes, tracks[0].data, tracks[0].end);
|
|
200
|
+
const field = id => fields.find(item => item.id === id);
|
|
201
|
+
if (field(0x6d80)) throw Error('encoded or encrypted track');
|
|
202
|
+
const number = field(0xd7), type = field(0x83), codec = field(0x86), priv = field(0x63a2), delay = field(0x56aa);
|
|
203
|
+
if (!number || !type || !codec || !priv || !delay || uint(bytes, type.data, type.size) !== 2
|
|
204
|
+
|| new TextDecoder().decode(bytes.subarray(codec.data, codec.end)) !== 'A_OPUS'
|
|
205
|
+
|| priv.size < 19 || new TextDecoder().decode(bytes.subarray(priv.data, priv.data + 8)) !== 'OpusHead') throw Error('unsupported audio track');
|
|
206
|
+
const skip = bytes[priv.data + 10] | (bytes[priv.data + 11] << 8);
|
|
207
|
+
// Opening a new run reapplies Opus pre-skip. CodecDelay must match
|
|
208
|
+
// that skip, making the first decoded sample's origin its original
|
|
209
|
+
// Block timestamp; later cuts advance by exact packet sample counts.
|
|
210
|
+
if (Math.abs(uint(bytes, delay.data, delay.size) - skip * 1e9 / 48000) > 1) throw Error('inconsistent codec delay');
|
|
211
|
+
const seek = field(0x56bb);
|
|
212
|
+
// Matroska's Opus SeekPreRoll recommends 80ms. Keep at least one
|
|
213
|
+
// second to restore decoder state before returning audible PCM.
|
|
214
|
+
if (seek) preroll = Math.max(preroll, uint(bytes, seek.data, seek.size) / 1e9);
|
|
215
|
+
track = uint(bytes, number.data, number.size);
|
|
216
|
+
if (!(track > 0)) throw Error('invalid track');
|
|
217
|
+
}
|
|
218
|
+
if (headers.length >= 65_536) throw Error('too many headers');
|
|
219
|
+
headers.push(bytes);
|
|
220
|
+
} catch {
|
|
221
|
+
valid = false;
|
|
222
|
+
headers.length = records.length = 0;
|
|
223
|
+
runs = cached = initHeaders = null;
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
at(seconds, fallback) {
|
|
227
|
+
const ordinary = () => ({ id: 0, startTime: 0, endTime: Infinity, bytes: fallback() });
|
|
228
|
+
if (!valid || !records.length) return ordinary();
|
|
229
|
+
try {
|
|
230
|
+
runs ??= buildRuns();
|
|
231
|
+
let low = 0, high = runs.length;
|
|
232
|
+
while (low + 1 < high) {
|
|
233
|
+
const mid = (low + high) >>> 1;
|
|
234
|
+
if (runs[mid].startTime <= seconds + 1e-6) low = mid;
|
|
235
|
+
else high = mid;
|
|
236
|
+
}
|
|
237
|
+
const selected = runs[low], endTime = runs[low + 1]?.startTime ?? Infinity;
|
|
238
|
+
const entries = selected.entries, count = entries.length;
|
|
239
|
+
let first = 0;
|
|
240
|
+
high = count;
|
|
241
|
+
const latest = seconds - preroll;
|
|
242
|
+
while (first + 1 < high) {
|
|
243
|
+
const mid = (first + high) >>> 1;
|
|
244
|
+
if (entries[mid].origin <= latest) first = mid;
|
|
245
|
+
else high = mid;
|
|
246
|
+
}
|
|
247
|
+
if (!entries[first].seekable) first = 0;
|
|
248
|
+
if (!first && runs.length === 1 && ordered) {
|
|
249
|
+
// Keep the initial full-file path, pre-skip and trailing metadata.
|
|
250
|
+
return { id: selected.id, startTime: selected.startTime, endTime, bytes: fallback() };
|
|
251
|
+
}
|
|
252
|
+
if (cached?.run === selected && cached.first === first && cached.count === count && cached.endTime === endTime) return cached.snapshot;
|
|
253
|
+
let bytes;
|
|
254
|
+
const snapshot = { id: selected.id, startTime: first ? entries[first].origin : selected.startTime, endTime,
|
|
255
|
+
get bytes() {
|
|
256
|
+
if (!bytes) {
|
|
257
|
+
const parts = selected.headers.slice();
|
|
258
|
+
let token = null;
|
|
259
|
+
for (let i = first; i < count; i++) {
|
|
260
|
+
const entry = entries[i];
|
|
261
|
+
if (entry.token && entry.token !== token) for (const part of entry.prefix) parts.push(part);
|
|
262
|
+
parts.push(entry.bytes);
|
|
263
|
+
token = entry.token;
|
|
264
|
+
}
|
|
265
|
+
bytes = join(parts);
|
|
266
|
+
}
|
|
267
|
+
return bytes;
|
|
268
|
+
} };
|
|
269
|
+
// Metadata notifications only read id/endTime; do not copy the audio
|
|
270
|
+
// there. The getter's fixed record count keeps its snapshot immutable
|
|
271
|
+
// if more packets append before decoding. Cache only the latest view.
|
|
272
|
+
cached = { run: selected, first, count, endTime, snapshot };
|
|
273
|
+
return snapshot;
|
|
274
|
+
} catch {
|
|
275
|
+
valid = false;
|
|
276
|
+
headers.length = records.length = 0;
|
|
277
|
+
runs = cached = initHeaders = null;
|
|
278
|
+
return ordinary();
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
@@ -16,10 +16,10 @@ export class CookieJar {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** Stores Set-Cookie header values received for `url`. `fromScript` rejects HttpOnly (document.cookie). */
|
|
19
|
-
store(url, setCookies, { fromScript = false } = {}) {
|
|
19
|
+
store(url, setCookies, { fromScript = false, receivedAt = this.#now() } = {}) {
|
|
20
20
|
const target = new URL(url);
|
|
21
21
|
for (const header of setCookies) {
|
|
22
|
-
const cookie = parseSetCookie(header, target,
|
|
22
|
+
const cookie = parseSetCookie(header, target, receivedAt);
|
|
23
23
|
if (!cookie) continue;
|
|
24
24
|
if (fromScript && cookie.httpOnly) continue;
|
|
25
25
|
if (cookie.secure && target.protocol !== "https:" && !isLocalhost(target.hostname)) continue;
|
|
@@ -69,6 +69,22 @@ export class CookieJar {
|
|
|
69
69
|
return this.header(url, { includeHttpOnly: false });
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/** Non-HttpOnly records a renderer of this origin may mirror, including all paths. */
|
|
73
|
+
scriptSnapshot(url) {
|
|
74
|
+
const target = new URL(url), now = this.#now();
|
|
75
|
+
return this.#cookies.filter(cookie => !cookie.httpOnly && cookie.expires > now
|
|
76
|
+
&& domainMatches(cookie, target.hostname)
|
|
77
|
+
&& (!cookie.secure || target.protocol === "https:" || isLocalhost(target.hostname)))
|
|
78
|
+
.map(cookie => ({ ...cookie }));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Replace a renderer's script-only mirror with a trusted controller snapshot. */
|
|
82
|
+
replaceScriptSnapshot(cookies) {
|
|
83
|
+
const now = this.#now();
|
|
84
|
+
this.#cookies = cookies.filter(cookie => !cookie.httpOnly && cookie.expires > now)
|
|
85
|
+
.map(cookie => ({ ...cookie, httpOnly: false }));
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
get size() {
|
|
73
89
|
return this.#cookies.length;
|
|
74
90
|
}
|
|
@@ -405,7 +405,8 @@ function paintEffects(layout, tree, scrollOffsetOf = null) {
|
|
|
405
405
|
if (!style) continue;
|
|
406
406
|
if (style.position === "fixed") fixed = true;
|
|
407
407
|
opacity *= style.opacity ?? 1;
|
|
408
|
-
|
|
408
|
+
const shape = shapeClip(node);
|
|
409
|
+
if (shape) clips.push([node, shape]);
|
|
409
410
|
const ownContent = kind === "text" || node !== owner;
|
|
410
411
|
if (ownContent && escaping !== "fixed") {
|
|
411
412
|
const positioned = style.position && style.position !== "static";
|
|
@@ -415,7 +416,7 @@ function paintEffects(layout, tree, scrollOffsetOf = null) {
|
|
|
415
416
|
if (radii) {
|
|
416
417
|
radii = insetRadii(radii, style.borderWidths ?? [0, 0, 0, 0]);
|
|
417
418
|
}
|
|
418
|
-
clips.push([node, clip, radii]);
|
|
419
|
+
if (clip) clips.push([node, clip, radii]);
|
|
419
420
|
if (isScroller(style) && boxes.has(node.id)) {
|
|
420
421
|
if (!scrolledBy.size) outOfFlow = positionedOut;
|
|
421
422
|
scrolledBy.add(node);
|
|
@@ -435,7 +436,7 @@ function paintEffects(layout, tree, scrollOffsetOf = null) {
|
|
|
435
436
|
const node = chain[index];
|
|
436
437
|
const own = ownTransform(node);
|
|
437
438
|
if (own) transform = transform ? composeTransforms(transform, own) : own;
|
|
438
|
-
transforms.set(node, transform);
|
|
439
|
+
if (transform) transforms.set(node, transform);
|
|
439
440
|
const offset = scrolledBy.has(node) ? scrollOffsetOf?.(node.domNode) : null;
|
|
440
441
|
if (offset && (offset.x || offset.y)) {
|
|
441
442
|
const scrolled = { a: 1, d: 1, e: 0 - offset.x, f: 0 - offset.y };
|
package/lib/paint/frame.js
CHANGED
|
@@ -117,11 +117,18 @@ export function backgroundBytes(color) {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/** Raw screenshot payload, owned independently of a recyclable raster tile. */
|
|
120
|
-
export function rawFrame(pixels, width, height, format) {
|
|
121
|
-
if (!['rgba', 'bgra'].includes(format) || width * height > 16777216) throw Error('Invalid raw screenshot');
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
export function rawFrame(pixels, width, height, format, { takeOwnership = false, inputFormat = "rgba" } = {}) {
|
|
121
|
+
if (!['rgba', 'bgra'].includes(format) || !['rgba', 'bgra'].includes(inputFormat) || width * height > 16777216) throw Error('Invalid raw screenshot');
|
|
122
|
+
// Only private composition/crop snapshots may be consumed in place. The
|
|
123
|
+
// default keeps raw captures independent of immutable/recyclable tiles.
|
|
124
|
+
const data = takeOwnership && pixels.buffer instanceof ArrayBuffer && pixels.byteOffset % 4 === 0 ? pixels : new Uint8Array(pixels);
|
|
125
|
+
if (format !== inputFormat) {
|
|
126
|
+
// Swap R/B as packed pixels, preserving G and alpha (Bun targets are little-endian).
|
|
127
|
+
const words = new Uint32Array(data.buffer, data.byteOffset, Math.floor(data.byteLength / 4));
|
|
128
|
+
for (let i = 0; i < words.length; i++) {
|
|
129
|
+
const p = words[i];
|
|
130
|
+
words[i] = (p & 0xff00ff00) | ((p & 0xff) << 16) | ((p >>> 16) & 0xff);
|
|
131
|
+
}
|
|
125
132
|
}
|
|
126
133
|
return {format, width, height, pixels:data};
|
|
127
134
|
}
|
package/lib/render-tree/index.js
CHANGED
|
@@ -315,6 +315,9 @@ function sameNodeProperties(previous, type, properties) {
|
|
|
315
315
|
for (const [key, value] of Object.entries(properties)) {
|
|
316
316
|
const old = previous[key];
|
|
317
317
|
if (old === value) continue;
|
|
318
|
+
// Extraction makes fresh immutable SVG values even when their paint is
|
|
319
|
+
// unchanged. Retain the node so its ancestors can reuse layout results.
|
|
320
|
+
if (key === "svg" && Bun.deepEquals(old, value, true)) continue;
|
|
318
321
|
if (key !== "children" || old.length !== value.length) return false;
|
|
319
322
|
for (let index = 0; index < old.length; index++) if (old[index] !== value[index]) return false;
|
|
320
323
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { drainMicrotasks } from "bun:jsc";
|
|
2
|
+
import { createTimers } from "./timers.js";
|
|
3
|
+
|
|
4
|
+
// Older Bun checkpoints also dispatched queued event-loop tasks. Although
|
|
5
|
+
// 1.4.3 stable fixed this, Bun.version hides the suffix of unsafe 1.4.3
|
|
6
|
+
// canaries. Require the later 1.4.4 branch and keep unknown channels on timers.
|
|
7
|
+
export function supportsFrameCheckpoint(version = Bun.version) {
|
|
8
|
+
const match = typeof version === "string"
|
|
9
|
+
&& /^((?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))(?:-canary(?:\.(?:0|[1-9]\d*))?)?$/.exec(version);
|
|
10
|
+
return !!match && Bun.semver.order(match[1], "1.4.4") >= 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** One callback collection per document, with a checkpoint between callbacks. */
|
|
14
|
+
export function createAnimationFrames({ invoke, now, checkpoint = drainMicrotasks,
|
|
15
|
+
frameTimers = null }) {
|
|
16
|
+
let nextWakeId = 1;
|
|
17
|
+
// A private timer collection keeps frame handles separate from Window timers.
|
|
18
|
+
// timerTask=false also prevents a frame inheriting a timer's nesting clamp.
|
|
19
|
+
const timers = frameTimers ?? createTimers({ invoke: callback => callback(), nextId: () => nextWakeId++ });
|
|
20
|
+
const callbacks = new Map();
|
|
21
|
+
let nextHandle = 0, wake = null, running = false, disposed = false;
|
|
22
|
+
|
|
23
|
+
function arm(delay) {
|
|
24
|
+
if (!disposed && wake === null && !running && callbacks.size) {
|
|
25
|
+
wake = timers.schedule(deliver, delay, false, false);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function deliver() {
|
|
29
|
+
wake = null;
|
|
30
|
+
if (disposed) return;
|
|
31
|
+
running = true;
|
|
32
|
+
const timestamp = now(), handles = [...callbacks.keys()];
|
|
33
|
+
try {
|
|
34
|
+
for (const handle of handles) {
|
|
35
|
+
if (disposed) break;
|
|
36
|
+
const callback = callbacks.get(handle);
|
|
37
|
+
if (callback === undefined) continue;
|
|
38
|
+
callbacks.delete(handle);
|
|
39
|
+
// invoke reports page exceptions. Drain the complete promise/microtask
|
|
40
|
+
// queue here, without yielding unrelated timers or I/O between callbacks.
|
|
41
|
+
try { invoke(callback, timestamp); }
|
|
42
|
+
finally { checkpoint(); }
|
|
43
|
+
}
|
|
44
|
+
} finally {
|
|
45
|
+
running = false;
|
|
46
|
+
// Callbacks registered during delivery, including its microtasks, belong
|
|
47
|
+
// to the next frame. Long callbacks do not add another fixed 16 ms delay.
|
|
48
|
+
arm(Math.max(0, Math.ceil(16 - (now() - timestamp))));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
request(callback) {
|
|
53
|
+
if (typeof callback !== "function") throw new TypeError("requestAnimationFrame callback must be a function");
|
|
54
|
+
do { nextHandle = (nextHandle + 1) >>> 0; } while (callbacks.has(nextHandle));
|
|
55
|
+
const handle = nextHandle;
|
|
56
|
+
if (!disposed) {
|
|
57
|
+
callbacks.set(handle, callback);
|
|
58
|
+
arm(16);
|
|
59
|
+
}
|
|
60
|
+
return handle;
|
|
61
|
+
},
|
|
62
|
+
cancel(handle) {
|
|
63
|
+
callbacks.delete(+handle >>> 0);
|
|
64
|
+
if (!callbacks.size && wake !== null) {
|
|
65
|
+
timers.cancel(wake);
|
|
66
|
+
wake = null;
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
dispose() {
|
|
70
|
+
disposed = true;
|
|
71
|
+
callbacks.clear();
|
|
72
|
+
timers.dispose();
|
|
73
|
+
wake = null;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
package/lib/renderer/bindings.js
CHANGED
|
@@ -777,6 +777,7 @@ class ElementBinding extends ParentBinding {
|
|
|
777
777
|
load() {
|
|
778
778
|
if (!isMedia(this)) return;
|
|
779
779
|
realmOf(this).hooks.pauseMedia?.(nodeOf(this));
|
|
780
|
+
realmOf(this).hooks.reselectMedia?.(nodeOf(this));
|
|
780
781
|
void realmOf(this).hooks.seekMedia?.(nodeOf(this), 0);
|
|
781
782
|
}
|
|
782
783
|
|
|
@@ -918,6 +919,7 @@ class ElementBinding extends ParentBinding {
|
|
|
918
919
|
if (resetsCanvas) resetCanvas(node);
|
|
919
920
|
touch(this);
|
|
920
921
|
}
|
|
922
|
+
if (attribute.toLowerCase() === "src" && isMedia(this)) realmOf(this).hooks.attachMediaSrc?.(node, node.src);
|
|
921
923
|
}
|
|
922
924
|
|
|
923
925
|
removeAttribute(name) {
|