buninu-browser 0.2.3 → 0.3.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 +86 -1
- package/lib/cdp-server.js +47 -14
- package/lib/clipboard.js +22 -0
- package/lib/happy-dom/document-base.js +33 -0
- package/lib/happy-dom/parser.js +15 -6
- package/lib/happy-dom/selector-matcher.js +2 -2
- package/lib/happy-dom/selector-pseudo-adapter.js +19 -0
- package/lib/happy-dom/svg-attribute-adapter.js +50 -0
- package/lib/headless-shell.js +40 -11
- package/lib/input/hit-test.js +67 -19
- package/lib/layout/text-layout.js +213 -91
- package/lib/media/byte-pipe.js +2 -2
- package/lib/media/position-audio.js +10 -1
- package/lib/media/source-snapshots.js +126 -0
- package/lib/media/video-keyframe.js +17 -4
- package/lib/media/webm-stream.js +106 -0
- package/lib/network/network-service.js +268 -120
- package/lib/paint/display-list.js +8 -28
- package/lib/paint/frame-cache.js +25 -0
- package/lib/paint/frame.js +77 -16
- package/lib/paint/paragraph-text.js +24 -0
- package/lib/paint/raster-readback.js +41 -0
- package/lib/paint/raw-frames.js +47 -0
- package/lib/paint/rounded-rect.js +82 -0
- package/lib/paint/shared-frame-pool.js +105 -0
- package/lib/render-tree/index.js +38 -6
- package/lib/render-tree/svg.js +15 -7
- package/lib/renderer/bindings.js +233 -80
- package/lib/renderer/broadcast-channel.js +76 -0
- package/lib/renderer/canvas-2d.js +257 -14
- package/lib/renderer/canvas-text.js +120 -0
- package/lib/renderer/compositor-channel.js +103 -0
- package/lib/renderer/compositor.js +34 -40
- package/lib/renderer/custom-element-reactions.js +25 -1
- package/lib/renderer/flags.js +1 -1
- package/lib/renderer/font-loading.js +227 -0
- package/lib/renderer/host.js +133 -98
- package/lib/renderer/image-resources.js +65 -0
- package/lib/renderer/interfaces.js +28 -0
- package/lib/renderer/layout-observers.js +236 -0
- package/lib/renderer/main.js +52 -5
- package/lib/renderer/media-source.js +126 -55
- package/lib/renderer/page-realm.js +287 -99
- package/lib/renderer/page-renderer.js +663 -239
- package/lib/renderer/prepared-script-cache.js +50 -0
- package/lib/renderer/rendered-text.js +108 -0
- package/lib/renderer/rewrite-worker.js +4 -3
- package/lib/renderer/script-rewrite.js +13 -2
- package/lib/renderer/scroll-into-view.js +28 -0
- package/lib/renderer/slot-assignment.js +60 -0
- package/lib/renderer/video-presentation.js +23 -4
- package/lib/renderer/workers.js +2 -2
- package/lib/style/computed-style.js +611 -104
- package/lib/style/selector-traits.js +49 -5
- package/package.json +1 -1
- package/test/broadcast-channel.test.js +26 -0
- package/test/canvas-2d.test.js +83 -2
- package/test/canvas-paint.test.js +9 -0
- package/test/canvas-text.test.js +41 -0
- package/test/cdp-stream.test.js +56 -0
- package/test/clipboard.test.js +35 -0
- package/test/computed-style-binding.test.js +151 -0
- package/test/computed-style.test.js +258 -1
- package/test/display-list.test.js +12 -0
- package/test/document-base.test.js +87 -0
- package/test/dom-platform.test.js +461 -0
- package/test/fetch-lifecycle.test.js +161 -0
- package/test/fixtures/browser-raw-demo.html +36 -0
- package/test/fixtures/canvas-paint.js +117 -0
- package/test/fixtures/canvas-text.js +42 -0
- package/test/fixtures/image-lifetime.js +90 -0
- package/test/fixtures/media-seek-snapshot.js +77 -0
- package/test/fixtures/mse-audio-chirp.webm +0 -0
- package/test/fixtures/mse-switch-large.mp4 +0 -0
- package/test/fixtures/mse-switch-small.mp4 +0 -0
- package/test/fixtures/raw-capture-benchmark.json +76 -0
- package/test/fixtures/renderer-memory.js +110 -0
- package/test/fixtures/summary-style.js +28 -0
- package/test/font-loading.test.js +97 -0
- package/test/grid.test.js +58 -0
- package/test/hit-test.test.js +21 -1
- package/test/image-lifetime.test.js +12 -0
- package/test/image-resources.test.js +94 -0
- package/test/local-media.test.js +84 -0
- package/test/media-seek-snapshot.test.js +11 -0
- package/test/media-source-config.test.js +98 -0
- package/test/media-source-webm-accounting.test.js +98 -0
- package/test/media-source-webm.test.js +108 -0
- package/test/navigation.test.js +1 -0
- package/test/network-service.test.js +388 -0
- package/test/page-scripts.test.js +1 -1
- package/test/paint-frame.test.js +103 -0
- package/test/paint-paragraph-cache.test.js +87 -0
- package/test/position-audio.test.js +24 -0
- package/test/positioning.test.js +22 -0
- package/test/prepared-script-cache.test.js +45 -0
- package/test/pulse-client.test.js +10 -3
- package/test/raster-readback.test.js +40 -0
- package/test/raw-capture.test.js +152 -0
- package/test/render-tree.test.js +28 -0
- package/test/renderer-memory.test.js +11 -0
- package/test/selector-traits.test.js +24 -0
- package/test/shared-frame-pool.test.js +63 -0
- package/test/style-sheets.test.js +51 -0
- package/test/svg.test.js +29 -0
- package/test/text-layout.test.js +106 -0
- package/test/video-keyframe.test.js +61 -0
- package/test/video-presentation.test.js +69 -0
- package/test/window-resize.test.js +36 -0
- package/tools/bench-raw-capture.js +32 -0
- package/tools/browser-lab.js +78 -0
- package/tools/browser-soak.js +75 -0
- package/tools/network-reference.js +49 -0
- package/tools/process-memory.js +38 -0
- package/tools/script-benchmark.js +25 -0
- package/ai-debug.log +0 -5537
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { escapeSesHtmlComments, rewriteClassicScriptSource } from "./script-rewrite.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reuse deterministic source preparation across documents and repeated evals.
|
|
5
|
+
* Only strings/parse flags are retained, never a document, AST, or executable
|
|
6
|
+
* closure. Evaluation still happens in the current page's SES compartment.
|
|
7
|
+
*/
|
|
8
|
+
export class PreparedScriptCache {
|
|
9
|
+
#entries = new Map();
|
|
10
|
+
#bytes = 0;
|
|
11
|
+
#maxBytes;
|
|
12
|
+
#maxEntries;
|
|
13
|
+
#prepare;
|
|
14
|
+
|
|
15
|
+
constructor({ maxBytes = 4 * 1024 * 1024, maxEntries = 128, prepare = prepareSource } = {}) {
|
|
16
|
+
this.#maxBytes = Math.max(0, maxBytes);
|
|
17
|
+
this.#maxEntries = Math.max(0, maxEntries);
|
|
18
|
+
this.#prepare = prepare;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
prepare(source) {
|
|
22
|
+
const cached = this.#entries.get(source);
|
|
23
|
+
if (cached) {
|
|
24
|
+
this.#entries.delete(source);
|
|
25
|
+
this.#entries.set(source, cached);
|
|
26
|
+
return cached.result;
|
|
27
|
+
}
|
|
28
|
+
const result = Object.freeze(this.#prepare(source));
|
|
29
|
+
// Conservative UTF-16 accounting, including both the key and rewritten
|
|
30
|
+
// source, even when the engine can share their underlying string storage.
|
|
31
|
+
const bytes = (source.length + result.code.length) * 2;
|
|
32
|
+
if (!this.#maxEntries || bytes > this.#maxBytes) return result;
|
|
33
|
+
while (this.#entries.size && (this.#entries.size >= this.#maxEntries || this.#bytes + bytes > this.#maxBytes)) {
|
|
34
|
+
const key = this.#entries.keys().next().value;
|
|
35
|
+
this.#bytes -= this.#entries.get(key).bytes;
|
|
36
|
+
this.#entries.delete(key);
|
|
37
|
+
}
|
|
38
|
+
this.#entries.set(source, { result, bytes });
|
|
39
|
+
this.#bytes += bytes;
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function prepareSource(source) {
|
|
45
|
+
const classic = rewriteClassicScriptSource(source);
|
|
46
|
+
return { code: escapeSesHtmlComments(classic.code), parsed: classic.parsed, strict: classic.strict };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const mainThreadCache = new PreparedScriptCache();
|
|
50
|
+
export const prepareClassicScript = source => mainThreadCache.prepare(source);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/** HTML innerText's rendered-text collection using the engine's computed styles. */
|
|
2
|
+
export function renderedText(element, computedStyle) {
|
|
3
|
+
if (!element.isConnected) return element.textContent;
|
|
4
|
+
const styles = new Map();
|
|
5
|
+
const styleOf = (node) => {
|
|
6
|
+
if (!styles.has(node)) styles.set(node, computedStyle(node));
|
|
7
|
+
return styles.get(node);
|
|
8
|
+
};
|
|
9
|
+
// An element outside rendering (including a display:none ancestor) returns
|
|
10
|
+
// raw descendant text. Hidden descendants of a rendered element do not.
|
|
11
|
+
for (let ancestor = element; ancestor; ancestor = ancestor.parentElement) {
|
|
12
|
+
if (styleOf(ancestor)?.display === "none") return element.textContent;
|
|
13
|
+
}
|
|
14
|
+
for (let child = element, ancestor = element.parentElement; ancestor; child = ancestor, ancestor = ancestor.parentElement) {
|
|
15
|
+
if (ancestor.tagName === "DETAILS" && !ancestor.open && child !== firstSummary(ancestor)) return "";
|
|
16
|
+
}
|
|
17
|
+
let output = "";
|
|
18
|
+
let breaks = 0;
|
|
19
|
+
let space = false;
|
|
20
|
+
const boundary = (count) => { breaks = Math.max(breaks, count); space = false; };
|
|
21
|
+
const beforeText = () => {
|
|
22
|
+
if (breaks && output) output += "\n".repeat(breaks);
|
|
23
|
+
else if (space && output && !output.endsWith("\n")) output += " ";
|
|
24
|
+
breaks = 0;
|
|
25
|
+
space = false;
|
|
26
|
+
};
|
|
27
|
+
const text = (value, style) => {
|
|
28
|
+
if (style?.visibility === "hidden" || style?.visibility === "collapse") return;
|
|
29
|
+
value = value.replace(/\r\n?/g, "\n");
|
|
30
|
+
if (style?.textTransform === "uppercase") value = value.toUpperCase();
|
|
31
|
+
else if (style?.textTransform === "lowercase") value = value.toLowerCase();
|
|
32
|
+
else if (style?.textTransform === "capitalize") value = value.replace(/(^|\s)\S/g, (part) => part.toUpperCase());
|
|
33
|
+
if (["pre", "pre-wrap", "break-spaces"].includes(style?.whiteSpace)) {
|
|
34
|
+
if (!value) return;
|
|
35
|
+
beforeText();
|
|
36
|
+
output += value;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Collapse across adjacent text nodes too: separate spans do not create
|
|
40
|
+
// extra spaces, and whitespace at block boundaries is not rendered.
|
|
41
|
+
const pattern = style?.whiteSpace === "pre-line" ? /([\t\f ]+|\n)/g : /([\t\f \n]+)/g;
|
|
42
|
+
for (const token of value.split(pattern)) {
|
|
43
|
+
if (!token) continue;
|
|
44
|
+
if (style?.whiteSpace === "pre-line" && token === "\n") {
|
|
45
|
+
beforeText();
|
|
46
|
+
output += "\n";
|
|
47
|
+
} else if (/^[\t\f \n]+$/.test(token)) space = !breaks;
|
|
48
|
+
else { beforeText(); output += token; }
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const stack = [{ node: element, root: true }];
|
|
52
|
+
while (stack.length) {
|
|
53
|
+
const item = stack.pop();
|
|
54
|
+
if (item.after) {
|
|
55
|
+
if (item.tab) { beforeText(); output += "\t"; }
|
|
56
|
+
if (item.breaks) boundary(item.breaks);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const node = item.node;
|
|
60
|
+
if (node.nodeType === 3) { text(node.data, styleOf(node.parentElement)); continue; }
|
|
61
|
+
if (node.nodeType !== 1) continue;
|
|
62
|
+
// SVG descriptive/resource elements never paint their own text, even
|
|
63
|
+
// though getComputedStyle(title).display is "inline" in browsers.
|
|
64
|
+
if (node.namespaceURI === SVG_NAMESPACE && NON_RENDERED_SVG.has(node.localName)) continue;
|
|
65
|
+
const style = styleOf(node);
|
|
66
|
+
if (style?.display === "none") continue;
|
|
67
|
+
if (node.tagName === "BR") {
|
|
68
|
+
if (style?.visibility !== "hidden" && style?.visibility !== "collapse") {
|
|
69
|
+
beforeText(); output += "\n";
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const count = item.root ? 0 : node.tagName === "P" ? 2
|
|
74
|
+
: BLOCK_DISPLAYS.has(style?.display) ? 1 : 0;
|
|
75
|
+
if (count) boundary(count);
|
|
76
|
+
const tab = style?.display === "table-cell" && node.nextElementSibling !== null;
|
|
77
|
+
if (count || tab) stack.push({ after: true, breaks: count, tab });
|
|
78
|
+
if (node.tagName === "DETAILS" && !node.open) {
|
|
79
|
+
const summary = firstSummary(node);
|
|
80
|
+
if (summary) stack.push({ node: summary });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
for (let child = node.lastChild; child; child = child.previousSibling) stack.push({ node: child });
|
|
84
|
+
}
|
|
85
|
+
return output;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function firstSummary(details) {
|
|
89
|
+
for (const child of details.children) if (child.tagName === "SUMMARY") return child;
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const BLOCK_DISPLAYS = new Set(["block", "flow-root", "flex", "grid", "list-item", "table", "table-caption", "table-row"]);
|
|
94
|
+
const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
95
|
+
const NON_RENDERED_SVG = new Set(["title", "desc", "metadata", "defs", "symbol", "clipPath", "mask", "pattern",
|
|
96
|
+
"linearGradient", "radialGradient", "filter"]);
|
|
97
|
+
|
|
98
|
+
/** innerText assignment turns CR/LF line boundaries into BR elements. */
|
|
99
|
+
export function setRenderedText(element, value) {
|
|
100
|
+
const document = element.ownerDocument;
|
|
101
|
+
const fragment = document.createDocumentFragment();
|
|
102
|
+
const lines = String(value ?? "").split(/\r\n|[\r\n]/);
|
|
103
|
+
for (let index = 0; index < lines.length; index++) {
|
|
104
|
+
if (index) fragment.appendChild(document.createElement("br"));
|
|
105
|
+
if (lines[index]) fragment.appendChild(document.createTextNode(lines[index]));
|
|
106
|
+
}
|
|
107
|
+
element.replaceChildren(fragment);
|
|
108
|
+
}
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
* Messages: { id, source } → { id, result: { code, parsed, strict } } or
|
|
8
8
|
* { id, error }. The worker only transforms strings; page code never runs here.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { PreparedScriptCache } from "./prepared-script-cache.js";
|
|
11
|
+
|
|
12
|
+
const preparedScripts = new PreparedScriptCache({ maxBytes: 16 * 1024 * 1024, maxEntries: 64 });
|
|
11
13
|
|
|
12
14
|
self.addEventListener("message", ({ data }) => {
|
|
13
15
|
const { id, source } = data ?? {};
|
|
14
16
|
try {
|
|
15
|
-
|
|
16
|
-
self.postMessage({ id, result: { code: escapeSesHtmlComments(classic.code), parsed: classic.parsed, strict: classic.strict } });
|
|
17
|
+
self.postMessage({ id, result: preparedScripts.prepare(String(source)) });
|
|
17
18
|
} catch (error) {
|
|
18
19
|
self.postMessage({ id, error: String(error?.message ?? error) });
|
|
19
20
|
}
|
|
@@ -118,8 +118,19 @@ export function rewriteClassicScript(source, parsed = null) {
|
|
|
118
118
|
const text = (start, end) => {
|
|
119
119
|
let result = "";
|
|
120
120
|
let at = start;
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
// Function declarations and initializers usually span only a handful
|
|
122
|
+
// of edits. Locate that range instead of rescanning every edit in the
|
|
123
|
+
// bundle for each declaration (quadratic for many top-level functions).
|
|
124
|
+
let low = 0, high = innerEdits.length;
|
|
125
|
+
while (low < high) {
|
|
126
|
+
const middle = (low + high) >>> 1;
|
|
127
|
+
if (innerEdits[middle].start < start) low = middle + 1;
|
|
128
|
+
else high = middle;
|
|
129
|
+
}
|
|
130
|
+
for (let index = low; index < innerEdits.length; index++) {
|
|
131
|
+
const edit = innerEdits[index];
|
|
132
|
+
if (edit.start >= end) break;
|
|
133
|
+
if (edit.end > end) continue;
|
|
123
134
|
result += `${source.slice(at, edit.start)}${edit.text}`;
|
|
124
135
|
at = edit.end;
|
|
125
136
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** CSSOM View's boolean/dictionary overload and logical alignment options. */
|
|
2
|
+
export function scrollIntoViewOptions(value = undefined) {
|
|
3
|
+
const dictionary = value !== null && (typeof value === "object" || typeof value === "function") ? value : {};
|
|
4
|
+
const { block: requestedBlock = "start", inline = "nearest", behavior = "auto", container = "all" } = dictionary;
|
|
5
|
+
const block = value !== null && value !== undefined && typeof value !== "object" && typeof value !== "function"
|
|
6
|
+
? (Boolean(value) ? "start" : "end") : requestedBlock;
|
|
7
|
+
const options = {
|
|
8
|
+
block: String(block), inline: String(inline), behavior: String(behavior), container: String(container),
|
|
9
|
+
};
|
|
10
|
+
for (const key of ["block", "inline"]) {
|
|
11
|
+
if (!["start", "center", "end", "nearest"].includes(options[key])) throw new TypeError(`Invalid scrollIntoView ${key}`);
|
|
12
|
+
}
|
|
13
|
+
if (!["auto", "instant", "smooth"].includes(options.behavior)) throw new TypeError("Invalid scrollIntoView behavior");
|
|
14
|
+
if (!["all", "nearest"].includes(options.container)) throw new TypeError("Invalid scrollIntoView container");
|
|
15
|
+
return options;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Signed scroll distance for one axis, with the target relative to the scrollport. */
|
|
19
|
+
export function scrollAlignmentDelta(start, size, viewportSize, alignment) {
|
|
20
|
+
const end = start + size;
|
|
21
|
+
if (alignment === "start") return start;
|
|
22
|
+
if (alignment === "end") return end - viewportSize;
|
|
23
|
+
if (alignment === "center") return start + (size - viewportSize) / 2;
|
|
24
|
+
// Nearest leaves both fully visible and viewport-spanning boxes alone.
|
|
25
|
+
if ((start >= 0 && end <= viewportSize) || (start <= 0 && end >= viewportSize)) return 0;
|
|
26
|
+
const endDelta = end - viewportSize;
|
|
27
|
+
return Math.abs(start) < Math.abs(endDelta) ? start : endDelta;
|
|
28
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// DOM slot assignment, shared by the page bindings and flattened-tree users.
|
|
2
|
+
// Keep only manual assignments: named slots follow the current tree directly.
|
|
3
|
+
const manualNodes = new WeakMap();
|
|
4
|
+
const manualOwner = new WeakMap();
|
|
5
|
+
const isSlottable = node => node?.nodeType === 1 || node?.nodeType === 3;
|
|
6
|
+
|
|
7
|
+
export function assignedSlot(node) {
|
|
8
|
+
const root = node?.parentElement?.shadowRoot;
|
|
9
|
+
if (!root || !isSlottable(node)) return null;
|
|
10
|
+
if (root.slotAssignment === "manual") {
|
|
11
|
+
const slot = manualOwner.get(node);
|
|
12
|
+
return slot?.getRootNode() === root ? slot : null;
|
|
13
|
+
}
|
|
14
|
+
const name = node.nodeType === 1 ? node.getAttribute("slot") ?? "" : "";
|
|
15
|
+
return [...root.querySelectorAll("slot")].find(slot => (slot.getAttribute("name") ?? "") === name) ?? null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function slotAssignedNodes(slot, flatten = false, visiting = new Set()) {
|
|
19
|
+
const root = slot.getRootNode();
|
|
20
|
+
if (!root.host || visiting.has(slot)) return [];
|
|
21
|
+
let nodes;
|
|
22
|
+
if (root.slotAssignment === "manual") {
|
|
23
|
+
nodes = (manualNodes.get(slot) ?? []).filter(node => node.parentNode === root.host);
|
|
24
|
+
} else {
|
|
25
|
+
const name = slot.getAttribute("name") ?? "";
|
|
26
|
+
const first = [...root.querySelectorAll("slot")].find(candidate => (candidate.getAttribute("name") ?? "") === name);
|
|
27
|
+
nodes = first !== slot ? [] : [...root.host.childNodes].filter(node => isSlottable(node)
|
|
28
|
+
&& (node.nodeType === 1 ? node.getAttribute("slot") ?? "" : "") === name);
|
|
29
|
+
}
|
|
30
|
+
if (!flatten) return nodes;
|
|
31
|
+
visiting.add(slot);
|
|
32
|
+
const flattened = [];
|
|
33
|
+
if (!nodes.length) nodes = [...slot.childNodes].filter(isSlottable);
|
|
34
|
+
for (const node of nodes) {
|
|
35
|
+
if (node.localName === "slot" && node.getRootNode().host) flattened.push(...slotAssignedNodes(node, true, visiting));
|
|
36
|
+
else flattened.push(node);
|
|
37
|
+
}
|
|
38
|
+
visiting.delete(slot);
|
|
39
|
+
return flattened;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function assignSlotNodes(slot, nodes, notify) {
|
|
43
|
+
if (nodes.some(node => !isSlottable(node))) throw new TypeError("Assigned nodes must be elements or text nodes");
|
|
44
|
+
if (slot.getRootNode().slotAssignment !== "manual") return;
|
|
45
|
+
const unique = [...new Set(nodes)];
|
|
46
|
+
const previous = manualNodes.get(slot) ?? [];
|
|
47
|
+
const changed = unique.length !== previous.length || unique.some((node, index) => node !== previous[index]);
|
|
48
|
+
if (!changed) return;
|
|
49
|
+
for (const node of previous) if (manualOwner.get(node) === slot) manualOwner.delete(node);
|
|
50
|
+
for (const node of unique) {
|
|
51
|
+
const old = manualOwner.get(node);
|
|
52
|
+
if (old && old !== slot) {
|
|
53
|
+
manualNodes.set(old, (manualNodes.get(old) ?? []).filter(item => item !== node));
|
|
54
|
+
notify(old);
|
|
55
|
+
}
|
|
56
|
+
manualOwner.set(node, slot);
|
|
57
|
+
}
|
|
58
|
+
manualNodes.set(slot, unique);
|
|
59
|
+
notify(slot);
|
|
60
|
+
}
|
|
@@ -20,7 +20,10 @@ export class VideoPresentation {
|
|
|
20
20
|
const decoder = await this.engine.decode(config.bytes, config.options);
|
|
21
21
|
if (epoch !== this.epoch) { decoder.close(); return; }
|
|
22
22
|
this.decoder = decoder;
|
|
23
|
-
|
|
23
|
+
const startTime = config.startTime ?? 0;
|
|
24
|
+
const endTime = config.endTime ?? Infinity;
|
|
25
|
+
this.nextTime = config.time >= endTime ? endTime
|
|
26
|
+
: config.time > startTime ? seekVideoKeyframe(decoder, config.time, startTime) : startTime;
|
|
24
27
|
this.width = Math.max(1, Math.min(480, decoder.width));
|
|
25
28
|
this.height = Math.max(1, Math.round(this.width * decoder.height / Math.max(1, decoder.width)));
|
|
26
29
|
if (this.height > 480) {
|
|
@@ -51,9 +54,17 @@ export class VideoPresentation {
|
|
|
51
54
|
// Audio stream identifiers increase monotonically within this renderer.
|
|
52
55
|
if (this.clocks.size > 32) this.clocks.delete(this.clocks.keys().next().value);
|
|
53
56
|
}
|
|
54
|
-
bind(id, audioId, audioStart, time) {
|
|
57
|
+
bind(id, audioId, audioStart, time, endTime) {
|
|
55
58
|
if (this.config?.id !== id) return;
|
|
56
|
-
|
|
59
|
+
if (audioId !== undefined) this.config.audioId = audioId;
|
|
60
|
+
if (audioStart !== undefined) this.config.audioStart = audioStart;
|
|
61
|
+
if (time !== undefined) Object.assign(this.config, { time, started: performance.now() });
|
|
62
|
+
if (endTime !== undefined) {
|
|
63
|
+
this.config.endTime = endTime;
|
|
64
|
+
// These frames may have been queued before the next init's first
|
|
65
|
+
// media segment established where the old configuration stops.
|
|
66
|
+
this.queue = this.queue.filter(frame => frame.time + 1e-6 < endTime);
|
|
67
|
+
}
|
|
57
68
|
}
|
|
58
69
|
time() {
|
|
59
70
|
const c = this.config;
|
|
@@ -65,9 +76,11 @@ export class VideoPresentation {
|
|
|
65
76
|
step() {
|
|
66
77
|
if (!this.decoder || !this.config) return;
|
|
67
78
|
const time = this.time();
|
|
79
|
+
const endTime = this.config.endTime ?? Infinity;
|
|
68
80
|
const deadline = performance.now() + 8;
|
|
69
81
|
try {
|
|
70
|
-
while (!this.eof && this.queue.length < this.limit && this.nextTime <= time + 0.75
|
|
82
|
+
while (!this.eof && this.queue.length < this.limit && this.nextTime <= time + 0.75
|
|
83
|
+
&& this.nextTime + 1e-6 < endTime) {
|
|
71
84
|
const pixels = this.decoder.nextFrame(this.width, this.height);
|
|
72
85
|
if (!pixels) { this.eof = true; break; }
|
|
73
86
|
const frame = { pixels, time: this.nextTime, width: this.width, height: this.height };
|
|
@@ -82,6 +95,12 @@ export class VideoPresentation {
|
|
|
82
95
|
// owns network/refill policy and decides whether this is EOF or an error.
|
|
83
96
|
this.eof = true;
|
|
84
97
|
}
|
|
98
|
+
// A later init can replace buffered frames before this snapshot's EOF.
|
|
99
|
+
// Finish at that media boundary; the renderer opens its next snapshot.
|
|
100
|
+
if (this.nextTime + 1e-6 >= endTime) {
|
|
101
|
+
this.eof = true;
|
|
102
|
+
this.nextTime = endTime;
|
|
103
|
+
}
|
|
85
104
|
let latest = null;
|
|
86
105
|
while (this.queue.length && this.queue[0].time <= time + 1e-6) latest = this.queue.shift();
|
|
87
106
|
if (latest) {
|
package/lib/renderer/workers.js
CHANGED
|
@@ -57,7 +57,7 @@ const hostClearTimeout = globalThis.clearTimeout;
|
|
|
57
57
|
* @returns {{ Worker: Function, dispose(): void }}
|
|
58
58
|
*/
|
|
59
59
|
export function createWorkers({
|
|
60
|
-
realm, documentUrl, fetchScript, runTask, callPage, reportError, DOMException, globals, constructorHelper, pagePorts,
|
|
60
|
+
realm, documentUrl, documentBaseUrl = documentUrl, fetchScript, runTask, callPage, reportError, DOMException, globals, constructorHelper, pagePorts,
|
|
61
61
|
}) {
|
|
62
62
|
const live = new Set();
|
|
63
63
|
|
|
@@ -94,7 +94,7 @@ export function createWorkers({
|
|
|
94
94
|
if (!new.target) throw new TypeError("Constructor Worker requires 'new'");
|
|
95
95
|
let url;
|
|
96
96
|
try {
|
|
97
|
-
url = new URL(String(scriptURL),
|
|
97
|
+
url = new URL(String(scriptURL), documentBaseUrl());
|
|
98
98
|
} catch {
|
|
99
99
|
throw new DOMException(`Failed to construct 'Worker': invalid URL '${scriptURL}'`, "SyntaxError");
|
|
100
100
|
}
|