buninu-browser 0.5.0 → 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/inert-link.js +26 -0
- package/lib/happy-dom/mutation-observer.js +157 -0
- package/lib/happy-dom/node-iterator-removal.js +36 -0
- package/lib/happy-dom/node-replacement.js +60 -0
- package/lib/happy-dom/parser.js +6 -1
- package/lib/headless-shell.js +49 -9
- package/lib/layout/repaint.js +89 -0
- package/lib/layout/text-layout.js +4 -2
- 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 +107 -3
- 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 +577 -201
- 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/compare-node-iterator.js +42 -0
- 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/fractional-text-scale.js +38 -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/node-iterator-cases.js +59 -0
- package/test/fixtures/node-iterator-chromium.json +315 -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/fractional-text-scale.test.js +6 -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/inert-link.test.js +37 -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/node-iterator.test.js +81 -0
- package/test/observer-invalidation.test.js +12 -0
- package/test/page-scripts.test.js +151 -1
- 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/text-layout.test.js +15 -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
- package/test/window-resize.test.js +29 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { sameLayoutInput } from "../render-tree/index.js";
|
|
2
|
+
|
|
3
|
+
// These properties change raster output, not box/text metrics. Inline
|
|
4
|
+
// decoration boxes still need the guard below. Keep this list small.
|
|
5
|
+
const PAINT_PROPERTIES = new Set([
|
|
6
|
+
"opacity", "backgroundColor",
|
|
7
|
+
"backgroundSize", "backgroundPosition", "backgroundRepeat", "backgroundOrigin", "backgroundClip",
|
|
8
|
+
"transformOrigin",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
/** Retain immutable geometry when paint changes preserve layout records. */
|
|
12
|
+
export function repaintLayout(layout, previous, next) {
|
|
13
|
+
if (!sameLayoutInput(previous.root, next.root, { ignoreStyles: true })) return null;
|
|
14
|
+
const changes = new Map();
|
|
15
|
+
const styles = new Map();
|
|
16
|
+
for (const [id, node] of next.nodesById) {
|
|
17
|
+
const before = previous.nodesById.get(id)?.style, after = node.style;
|
|
18
|
+
// Anonymous fragments have no node ID and must be restyled by their old
|
|
19
|
+
// style identity. If that identity split into different new styles, only
|
|
20
|
+
// layout can determine which fragment belongs to which new owner.
|
|
21
|
+
if (styles.has(before) && !Bun.deepEquals(styles.get(before), after)) return null;
|
|
22
|
+
if (before === after) {
|
|
23
|
+
styles.set(before, before);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!before || !after) return null;
|
|
27
|
+
const paint = {};
|
|
28
|
+
for (const key of new Set([...Object.keys(before), ...Object.keys(after)])) {
|
|
29
|
+
if (Bun.deepEquals(before[key], after[key])) continue;
|
|
30
|
+
if (key === "transform") {
|
|
31
|
+
// Translation is currently baked into layout coordinates. Only scale
|
|
32
|
+
// changes within an existing transform can reuse those coordinates.
|
|
33
|
+
const a = before[key], b = after[key];
|
|
34
|
+
if (!a || !b || !Bun.deepEquals(a.x, b.x) || !Bun.deepEquals(a.y, b.y)
|
|
35
|
+
|| Object.keys(a).some(k => !["x", "y", "scaleX", "scaleY"].includes(k))
|
|
36
|
+
|| Object.keys(b).some(k => !["x", "y", "scaleX", "scaleY"].includes(k))) return null;
|
|
37
|
+
} else if (!PAINT_PROPERTIES.has(key)) return null;
|
|
38
|
+
paint[key] = after[key];
|
|
39
|
+
}
|
|
40
|
+
if (Object.keys(paint).length) {
|
|
41
|
+
// Layout only emits inline/replaced decoration boxes when a background
|
|
42
|
+
// or border is present. Adding/removing one requires those boxes to be
|
|
43
|
+
// regenerated, even though the text/image metrics have not changed.
|
|
44
|
+
if ((node.type === "inline" || node.type === "image")
|
|
45
|
+
&& hasDecoration(before) !== hasDecoration(after)) return null;
|
|
46
|
+
changes.set(id, paint);
|
|
47
|
+
styles.set(before, after);
|
|
48
|
+
} else styles.set(before, before);
|
|
49
|
+
}
|
|
50
|
+
const restyle = record => {
|
|
51
|
+
const paint = changes.get(record.nodeId);
|
|
52
|
+
let style = styles.get(record.style);
|
|
53
|
+
// Some layout records contain resolved/adjusted styles (e.g. tables).
|
|
54
|
+
// Preserve all of those metrics instead of replacing the complete style.
|
|
55
|
+
if (!style && paint && record.style) {
|
|
56
|
+
style = Object.freeze({ ...record.style, ...paint });
|
|
57
|
+
styles.set(record.style, style);
|
|
58
|
+
}
|
|
59
|
+
// Boxes, runs and anonymous line fragments can share one computed style.
|
|
60
|
+
// Keep that identity: background-clip:text finds its gradient by style.
|
|
61
|
+
return style && style !== record.style ? Object.freeze({ ...record, style }) : record;
|
|
62
|
+
};
|
|
63
|
+
return Object.freeze({ ...layout, generation: next.generation,
|
|
64
|
+
boxes: mapChanged(layout.boxes, restyle),
|
|
65
|
+
fragments: mapChanged(layout.fragments, fragment => {
|
|
66
|
+
const updated = restyle(fragment);
|
|
67
|
+
const runs = fragment.runs && mapChanged(fragment.runs, restyle);
|
|
68
|
+
return runs === fragment.runs ? updated : Object.freeze({ ...updated, runs });
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Keep in sync with the inline decoration predicate in text-layout.js.
|
|
74
|
+
function hasDecoration(style) {
|
|
75
|
+
return Boolean((style.backgroundColor && style.backgroundColor !== "rgba(0, 0, 0, 0)")
|
|
76
|
+
|| style.backgroundImage || style.borderWidths?.some(width => width > 0));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Copy immutable arrays only when a record actually changed. */
|
|
80
|
+
function mapChanged(records, update) {
|
|
81
|
+
let changed = null;
|
|
82
|
+
for (let index = 0; index < records.length; index++) {
|
|
83
|
+
const record = records[index], next = update(record);
|
|
84
|
+
if (next === record) continue;
|
|
85
|
+
changed ??= records.slice();
|
|
86
|
+
changed[index] = next;
|
|
87
|
+
}
|
|
88
|
+
return changed ? Object.freeze(changed) : records;
|
|
89
|
+
}
|
|
@@ -1858,8 +1858,10 @@ function resolveHorizontal(ctx, box, containingX, containingWidth, shrink, perce
|
|
|
1858
1858
|
return { x: containingX + marginLeft, width, marginLeft, marginRight };
|
|
1859
1859
|
}
|
|
1860
1860
|
|
|
1861
|
+
// Anonymous flex/grid items share their parent RenderNode, but measure only
|
|
1862
|
+
// their own text run. Never reuse the parent's intrinsic width for these boxes.
|
|
1861
1863
|
function maxContentWidth(ctx, box) {
|
|
1862
|
-
const cacheKey = box.node ?? box;
|
|
1864
|
+
const cacheKey = box.anonymous ? box : box.node ?? box;
|
|
1863
1865
|
const cached = ctx.maxContent.get(cacheKey);
|
|
1864
1866
|
if (cached != null) return cached;
|
|
1865
1867
|
const style = box.style;
|
|
@@ -1902,7 +1904,7 @@ function maxContentWidth(ctx, box) {
|
|
|
1902
1904
|
}
|
|
1903
1905
|
|
|
1904
1906
|
function minContentWidth(ctx, box) {
|
|
1905
|
-
const cacheKey = box.node ?? box;
|
|
1907
|
+
const cacheKey = box.anonymous ? box : box.node ?? box;
|
|
1906
1908
|
const cached = ctx.minContent.get(cacheKey);
|
|
1907
1909
|
if (cached != null) return cached;
|
|
1908
1910
|
const style = box.style;
|
|
@@ -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 };
|