buninu-browser 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/lib/audio/playback-buffer.js +27 -0
  2. package/lib/audio/pulse-client.js +18 -8
  3. package/lib/cdp-server.js +53 -14
  4. package/lib/happy-dom/document-base.js +33 -0
  5. package/lib/happy-dom/parser.js +20 -6
  6. package/lib/happy-dom/selector-matcher.js +2 -2
  7. package/lib/happy-dom/selector-pseudo-adapter.js +33 -0
  8. package/lib/happy-dom/svg-attribute-adapter.js +50 -0
  9. package/lib/headless-shell.js +12 -6
  10. package/lib/input/hit-test.js +67 -19
  11. package/lib/layout/text-layout.js +289 -131
  12. package/lib/media/byte-pipe.js +2 -2
  13. package/lib/media/position-audio.js +10 -1
  14. package/lib/media/source-snapshots.js +126 -0
  15. package/lib/media/video-keyframe.js +42 -0
  16. package/lib/media/webm-stream.js +106 -0
  17. package/lib/network/network-service.js +268 -120
  18. package/lib/paint/display-list.js +22 -27
  19. package/lib/paint/frame-cache.js +25 -0
  20. package/lib/paint/frame.js +42 -13
  21. package/lib/paint/paragraph-text.js +24 -0
  22. package/lib/paint/raster-readback.js +41 -0
  23. package/lib/paint/rounded-rect.js +82 -0
  24. package/lib/paint/shared-frame-pool.js +105 -0
  25. package/lib/render-tree/index.js +40 -6
  26. package/lib/render-tree/svg.js +15 -7
  27. package/lib/renderer/bindings.js +244 -82
  28. package/lib/renderer/broadcast-channel.js +76 -0
  29. package/lib/renderer/canvas-2d.js +257 -14
  30. package/lib/renderer/canvas-text.js +120 -0
  31. package/lib/renderer/compositor.js +27 -10
  32. package/lib/renderer/custom-element-reactions.js +42 -0
  33. package/lib/renderer/font-loading.js +227 -0
  34. package/lib/renderer/history.js +42 -0
  35. package/lib/renderer/host.js +36 -5
  36. package/lib/renderer/image-loading.js +27 -0
  37. package/lib/renderer/image-resources.js +65 -0
  38. package/lib/renderer/interfaces.js +28 -0
  39. package/lib/renderer/keyboard-input.js +12 -0
  40. package/lib/renderer/layout-observers.js +236 -0
  41. package/lib/renderer/main.js +33 -5
  42. package/lib/renderer/media-playback-events.js +13 -0
  43. package/lib/renderer/media-source.js +126 -55
  44. package/lib/renderer/page-realm.js +278 -147
  45. package/lib/renderer/page-renderer.js +840 -301
  46. package/lib/renderer/piano-input.js +22 -0
  47. package/lib/renderer/pointer-move.js +29 -0
  48. package/lib/renderer/prepared-script-cache.js +50 -0
  49. package/lib/renderer/rendered-text.js +108 -0
  50. package/lib/renderer/rewrite-worker.js +4 -3
  51. package/lib/renderer/script-rewrite.js +13 -2
  52. package/lib/renderer/scroll-into-view.js +28 -0
  53. package/lib/renderer/slot-assignment.js +60 -0
  54. package/lib/renderer/video-presentation.js +145 -0
  55. package/lib/renderer/workers.js +2 -2
  56. package/lib/style/computed-style.js +632 -108
  57. package/lib/style/selector-traits.js +49 -5
  58. package/package.json +1 -1
  59. package/test/broadcast-channel.test.js +26 -0
  60. package/test/canvas-2d.test.js +83 -2
  61. package/test/canvas-paint.test.js +9 -0
  62. package/test/canvas-text.test.js +41 -0
  63. package/test/cdp-stream.test.js +56 -0
  64. package/test/compositor.test.js +38 -0
  65. package/test/computed-style-binding.test.js +151 -0
  66. package/test/computed-style.test.js +279 -1
  67. package/test/display-list.test.js +26 -0
  68. package/test/document-base.test.js +87 -0
  69. package/test/dom-platform.test.js +461 -0
  70. package/test/download.test.js +8 -0
  71. package/test/fetch-lifecycle.test.js +161 -0
  72. package/test/fixtures/canvas-paint.js +117 -0
  73. package/test/fixtures/canvas-text.js +42 -0
  74. package/test/fixtures/image-lifetime.js +90 -0
  75. package/test/fixtures/media-seek-snapshot.js +77 -0
  76. package/test/fixtures/mse-audio-chirp.webm +0 -0
  77. package/test/fixtures/mse-switch-large.mp4 +0 -0
  78. package/test/fixtures/mse-switch-small.mp4 +0 -0
  79. package/test/fixtures/renderer-memory.js +110 -0
  80. package/test/fixtures/summary-style.js +28 -0
  81. package/test/font-loading.test.js +97 -0
  82. package/test/grid.test.js +58 -0
  83. package/test/headless-shell.test.js +22 -0
  84. package/test/hit-test.test.js +21 -1
  85. package/test/image-lifetime.test.js +12 -0
  86. package/test/image-resources.test.js +94 -0
  87. package/test/local-media.test.js +84 -0
  88. package/test/media-playback-events.test.js +22 -0
  89. package/test/media-seek-snapshot.test.js +11 -0
  90. package/test/media-source-config.test.js +98 -0
  91. package/test/media-source-webm-accounting.test.js +98 -0
  92. package/test/media-source-webm.test.js +108 -0
  93. package/test/navigation.test.js +1 -0
  94. package/test/network-service.test.js +388 -0
  95. package/test/page-scripts.test.js +160 -2
  96. package/test/paint-frame.test.js +103 -0
  97. package/test/paint-paragraph-cache.test.js +87 -0
  98. package/test/piano-input.test.js +44 -0
  99. package/test/playback-buffer.test.js +31 -0
  100. package/test/position-audio.test.js +24 -0
  101. package/test/positioning.test.js +22 -0
  102. package/test/prepared-script-cache.test.js +45 -0
  103. package/test/pulse-client.test.js +27 -5
  104. package/test/raster-readback.test.js +40 -0
  105. package/test/render-tree.test.js +28 -0
  106. package/test/renderer-memory.test.js +11 -0
  107. package/test/selector-traits.test.js +24 -0
  108. package/test/shared-frame-pool.test.js +63 -0
  109. package/test/style-sheets.test.js +51 -0
  110. package/test/svg.test.js +29 -0
  111. package/test/text-layout.test.js +168 -0
  112. package/test/video-keyframe.test.js +61 -0
  113. package/test/video-presentation.test.js +136 -0
  114. package/tools/browser-lab.js +78 -0
  115. package/tools/browser-soak.js +75 -0
  116. package/tools/network-reference.js +49 -0
  117. package/tools/process-memory.js +38 -0
  118. package/tools/script-benchmark.js +25 -0
@@ -7,7 +7,7 @@
7
7
  * on finish/fail, which is how a decoder knows its snapshot is stale.
8
8
  * `waitFor` resolves when `size` reaches the mark or the pipe is finished.
9
9
  */
10
- export function createBytePipe() {
10
+ export function createBytePipe({ retainBytes = true } = {}) {
11
11
  const chunks = [];
12
12
  let size = 0;
13
13
  let generation = 0;
@@ -25,7 +25,7 @@ export function createBytePipe() {
25
25
  if (done || bytes == null) return;
26
26
  const chunk = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
27
27
  if (!chunk.byteLength) return;
28
- chunks.push(chunk);
28
+ if (retainBytes) chunks.push(chunk);
29
29
  size += chunk.byteLength;
30
30
  generation += 1;
31
31
  wake();
@@ -1,5 +1,5 @@
1
1
  /** Advance a PCM decoder to an exact sample cursor, never playing discarded PCM.
2
- * Reopened snapshots start at sample zero, including their pending first chunk.
2
+ * Reopened snapshots start at their epoch's first sample, including its pending chunk.
3
3
  * The caller holds output requests until this transaction has completed.
4
4
  */
5
5
  export async function advanceAudio(getSound, seconds, { reopen, needsMore, active = () => true }) {
@@ -10,6 +10,9 @@ export async function advanceAudio(getSound, seconds, { reopen, needsMore, activ
10
10
  if (sound.position === target) return true;
11
11
  if (sound.position > target) {
12
12
  if (!await reopen()) return false;
13
+ // A seek before the first retained epoch cannot be satisfied by
14
+ // reopening that same later epoch forever.
15
+ if (getSound().position > target) return false;
13
16
  continue;
14
17
  }
15
18
  // This is also the first chunk of EVERY replacement decoder. Skipping it
@@ -29,6 +32,12 @@ export async function advanceAudio(getSound, seconds, { reopen, needsMore, activ
29
32
  continue;
30
33
  }
31
34
  if (!needsMore(sound)) return false;
35
+ if (Number.isFinite(sound.endTime) && seconds >= sound.endTime) {
36
+ if (!await reopen()) return false;
37
+ const next = getSound();
38
+ if (next.snapshotId === sound.snapshotId && next.openedGeneration === sound.openedGeneration) return false;
39
+ continue;
40
+ }
32
41
  const pipe = sound.pipe;
33
42
  if (pipe.error) throw pipe.error;
34
43
  // Data may already have arrived while this snapshot was being decoded.
@@ -0,0 +1,126 @@
1
+ import { createBytePipe } from './byte-pipe.js';
2
+
3
+ // MSE may replace an MP4 initialization segment while old frames are still
4
+ // buffered. A file demuxer cannot consume that sequence as one MP4: keep each
5
+ // initialization and its fragments together, sharing the original byte views.
6
+ export function createSourceSnapshots(inspect) {
7
+ const epochs = [];
8
+ const pending = [];
9
+ let pendingSize = 0, headers = [], candidate = null, current = null;
10
+ let skipMedia = false, serial = 0, fragment = null, openEnded = false;
11
+ const join = parts => parts.length === 1 ? parts[0] : Bun.concatArrayBuffers(parts);
12
+ const bytesOf = parts => {
13
+ const bytes = join(parts);
14
+ return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
15
+ };
16
+ const same = (a, b) => a.length === b.length && a.every((byte, index) => byte === b[index]);
17
+ const take = count => {
18
+ const parts = [];
19
+ pendingSize -= count;
20
+ while (count > 0) {
21
+ const first = pending.shift(), length = Math.min(count, first.length);
22
+ parts.push(first.subarray(0, length));
23
+ if (length < first.length) pending.unshift(first.subarray(length));
24
+ count -= length;
25
+ }
26
+ return bytesOf(parts);
27
+ };
28
+ const peekHeader = () => {
29
+ if (pending[0].length >= Math.min(16, pendingSize)) return pending[0];
30
+ const header = new Uint8Array(Math.min(16, pendingSize));
31
+ let offset = 0;
32
+ for (const part of pending) {
33
+ const length = Math.min(part.length, header.length - offset);
34
+ header.set(part.subarray(0, length), offset); offset += length;
35
+ if (offset === header.length) break;
36
+ }
37
+ return header;
38
+ };
39
+ const activate = start => {
40
+ if (!candidate) return current;
41
+ candidate.startTime = Number.isFinite(start) ? start : 0;
42
+ epochs.push(candidate);
43
+ current = candidate;
44
+ candidate = null;
45
+ return current;
46
+ };
47
+ const accept = (type, bytes) => {
48
+ if (type === 'ftyp') { headers = [bytes]; return true; }
49
+ if (type === 'moov') {
50
+ if (current && same(current.header, bytes)) { headers = []; candidate = null; return false; }
51
+ if (candidate && same(candidate.header, bytes)) { headers = []; return false; }
52
+ const pipe = createBytePipe(), state = {};
53
+ for (const header of headers) pipe.append(header);
54
+ pipe.append(bytes);
55
+ inspect(bytes, state);
56
+ candidate = { id: ++serial, pipe, state, header: bytes.slice(), startTime: 0, covered: [], fragments: [], cached: null };
57
+ headers = [];
58
+ fragment = null;
59
+ skipMedia = false;
60
+ return true;
61
+ }
62
+ if (type === 'moof') {
63
+ const epoch = candidate ?? current;
64
+ if (!epoch) { headers.push(bytes); return true; }
65
+ const end = inspect(bytes, epoch.state), start = epoch.state.appendStart;
66
+ activate(start);
67
+ skipMedia = Number.isFinite(start) && Number.isFinite(end)
68
+ && current.covered.some(([from, to]) => from <= start + 0.002 && to + 0.002 >= end);
69
+ if (!skipMedia) {
70
+ if (Number.isFinite(start)) current.startTime = Math.min(current.startTime, start);
71
+ fragment = { start: Number.isFinite(start) ? start : current.startTime, pipe: createBytePipe() };
72
+ fragment.pipe.append(bytes);
73
+ current.fragments.push(fragment);
74
+ current.cached = null;
75
+ if (Number.isFinite(start) && Number.isFinite(end)) current.covered.push([start, end]);
76
+ }
77
+ return !skipMedia;
78
+ }
79
+ if (type === 'mdat') {
80
+ activate(0); // A non-fragmented MP4 has no tfdt/moof timeline.
81
+ if (!current) { headers.push(bytes); return true; }
82
+ if (!skipMedia) { (fragment?.pipe ?? current.pipe).append(bytes); current.cached = null; }
83
+ return !skipMedia;
84
+ }
85
+ if (candidate) candidate.pipe.append(bytes);
86
+ else if (current && !skipMedia) { (fragment?.pipe ?? current.pipe).append(bytes); current.cached = null; }
87
+ else headers.push(bytes);
88
+ return true;
89
+ };
90
+ return {
91
+ append(bytes) {
92
+ if (openEnded && current) { (fragment?.pipe ?? current.pipe).append(bytes); current.cached = null; return true; }
93
+ if (bytes.length) { pending.push(bytes); pendingSize += bytes.length; }
94
+ let accepted = false;
95
+ while (pendingSize >= 8) {
96
+ const header = peekHeader(), view = new DataView(header.buffer, header.byteOffset, header.byteLength);
97
+ let size = view.getUint32(0), headerSize = 8;
98
+ const type = String.fromCharCode(...header.subarray(4, 8));
99
+ // A zero-sized final mdat extends to EOF, including later appends.
100
+ if (size === 0 && type === 'mdat') { size = pendingSize; openEnded = true; }
101
+ if (size === 1) {
102
+ if (pendingSize < 16) break;
103
+ const large = view.getBigUint64(8);
104
+ if (large > BigInt(Number.MAX_SAFE_INTEGER)) break;
105
+ size = Number(large); headerSize = 16;
106
+ }
107
+ if (size < headerSize || size > pendingSize) break;
108
+ accepted = accept(type, take(size)) || accepted;
109
+ }
110
+ return accepted || pendingSize > 0;
111
+ },
112
+ at(seconds = 0) {
113
+ let selected = null;
114
+ for (const epoch of epochs) {
115
+ if (epoch.startTime <= seconds + 1e-6 && (!selected || epoch.startTime >= selected.startTime)) selected = epoch;
116
+ }
117
+ selected ??= epochs[0] ?? candidate;
118
+ if (!selected) return { id: 0, startTime: 0, endTime: Infinity, bytes: bytesOf([...headers, ...pending]) };
119
+ let endTime = Infinity;
120
+ for (const epoch of epochs) if (epoch.startTime > selected.startTime && epoch.startTime < endTime) endTime = epoch.startTime;
121
+ selected.cached ??= bytesOf([selected.pipe.bytes(), ...selected.fragments.slice()
122
+ .sort((a, b) => a.start - b.start).map(fragment => fragment.pipe.bytes())]);
123
+ return { id: selected.id, startTime: selected.startTime, endTime, bytes: selected.cached };
124
+ },
125
+ };
126
+ }
@@ -0,0 +1,42 @@
1
+ // wasmpeg seeks to a keyframe without returning its timestamp. Locate it by
2
+ // comparing tiny decoded probes, then rewind before delivering full frames.
3
+ export function seekVideoKeyframe(decoder, seconds, startTime = 0) {
4
+ const start = Math.max(0, startTime);
5
+ seconds = Math.max(start, seconds);
6
+ const seek = time => decoder.seek(Math.max(1, time * 1000));
7
+ const frame = 1 / (decoder.fps || 30);
8
+ if (!(seconds >= start + frame)) { seek(start); return start; }
9
+ const probe = time => {
10
+ seek(time);
11
+ const pixels = decoder.nextFrame(32, 18);
12
+ if (!pixels) return null;
13
+ let hash = 0x811c9dc5;
14
+ for (let i = 0; i < pixels.length; i += 3) hash = Math.imul(hash ^ pixels[i], 0x01000193);
15
+ return hash >>> 0;
16
+ };
17
+ const target = probe(seconds);
18
+ let keyframe = seconds;
19
+ if (target !== null) {
20
+ let distance = 10;
21
+ let earlier = Math.max(start, seconds - distance);
22
+ let previous = probe(earlier);
23
+ // A GOP can span more than ten seconds. Keep widening the search until
24
+ // it crosses the keyframe or reaches the start; otherwise the reported
25
+ // time moves forward while the decoder still returns that older frame.
26
+ while (earlier > start && previous === target) {
27
+ keyframe = earlier;
28
+ distance *= 2;
29
+ earlier = Math.max(start, seconds - distance);
30
+ previous = probe(earlier);
31
+ }
32
+ if (previous !== target) {
33
+ while (keyframe - earlier > frame) {
34
+ const middle = (earlier + keyframe) / 2;
35
+ if (probe(middle) === target) keyframe = middle;
36
+ else earlier = middle;
37
+ }
38
+ } else keyframe = earlier;
39
+ }
40
+ seek(keyframe);
41
+ return keyframe;
42
+ }
@@ -0,0 +1,106 @@
1
+ /** EBML element header, without reading or scanning the element's payload. */
2
+ export function ebmlHeader(bytes, offset = 0) {
3
+ const vint = (at, id = false) => {
4
+ if (at >= bytes.length || bytes[at] === 0) return null;
5
+ let length = 1, mask = 0x80;
6
+ while (!(bytes[at] & mask)) { length++; mask >>= 1; }
7
+ if (at + length > bytes.length) return null;
8
+ let value = id ? bytes[at] : bytes[at] & (mask - 1);
9
+ let unknown = !id && value === mask - 1;
10
+ for (let i = 1; i < length; i++) {
11
+ value = value * 256 + bytes[at + i];
12
+ unknown &&= bytes[at + i] === 0xff;
13
+ }
14
+ return { length, value, unknown };
15
+ };
16
+ const id = vint(offset, true);
17
+ const size = id && vint(offset + id.length);
18
+ if (!size || (!size.unknown && !Number.isSafeInteger(size.value))) return null;
19
+ return { id: id.value, length: id.length + size.length, size: size.unknown ? Infinity : size.value };
20
+ }
21
+
22
+ const SEGMENT = 0x18538067, CLUSTER = 0x1f43b675;
23
+ // An unknown-size Cluster ends at another Segment child, never at an ID
24
+ // which merely happens to occur inside an encoded audio/video packet.
25
+ const LEVEL_ONE = new Set([0x1a45dfa3, SEGMENT, CLUSTER, 0x114d9b74,
26
+ 0x1549a966, 0x1654ae6b, 0x1c53bb6b, 0x1941a469, 0x1043a770, 0x1254c367]);
27
+
28
+ /** Reassembles complete WebM elements across arbitrary SourceBuffer appends.
29
+ * Segment headers are emitted separately; their children (especially each
30
+ * Cluster) remain separate so retries can be discarded without new media.
31
+ * Pending input is kept as views and copied only when an element completes.
32
+ */
33
+ export function createWebMStream() {
34
+ const chunks = [];
35
+ let available = 0, unknown = null;
36
+ const peek = count => {
37
+ count = Math.min(count, available);
38
+ if (chunks[0]?.length >= count) return chunks[0].subarray(0, count);
39
+ const bytes = new Uint8Array(count);
40
+ let offset = 0;
41
+ for (const chunk of chunks) {
42
+ const n = Math.min(chunk.length, count - offset);
43
+ bytes.set(chunk.subarray(0, n), offset); offset += n;
44
+ if (offset === count) break;
45
+ }
46
+ return bytes;
47
+ };
48
+ const take = count => {
49
+ const bytes = peek(count);
50
+ available -= count;
51
+ for (let left = count; left > 0;) {
52
+ const chunk = chunks[0];
53
+ if (left < chunk.length) { chunks[0] = chunk.subarray(left); break; }
54
+ chunks.shift(); left -= chunk.length;
55
+ }
56
+ return bytes;
57
+ };
58
+ const parse = (final = false) => {
59
+ const result = [];
60
+ while (available) {
61
+ const header = ebmlHeader(peek(16));
62
+ if (!header) break;
63
+ if (unknown && LEVEL_ONE.has(header.id)) {
64
+ result.push({ closedCluster: unknown }); unknown = null; continue;
65
+ }
66
+ if (unknown) {
67
+ const size = header.length + header.size;
68
+ if (!Number.isFinite(size) || size > available) break;
69
+ const bytes = take(size);
70
+ if (header.id === 0xe7) {
71
+ let time = 0;
72
+ for (let i = header.length; i < bytes.length; i++) time = time * 256 + bytes[i];
73
+ unknown.time = time;
74
+ unknown.prefix.push(bytes);
75
+ } else if (header.id === 0xa3 || header.id === 0xa0) {
76
+ // Live streams may use one Cluster indefinitely. A complete Block
77
+ // is independently delimited, so expose it without waiting for the
78
+ // next Cluster. The writer inserts its original prefix only once.
79
+ result.push({ id: CLUSTER, bytes, cluster: { token: unknown, time: unknown.time, prefix: unknown.prefix } });
80
+ unknown.delivered = true;
81
+ } else if (!unknown.delivered) unknown.prefix.push(bytes);
82
+ } else if (header.id === SEGMENT) {
83
+ result.push({ id: SEGMENT, bytes: take(header.length) });
84
+ } else if (header.id === CLUSTER && !Number.isFinite(header.size)) {
85
+ unknown = { prefix: [take(header.length)], time: 0, delivered: false, accepted: false };
86
+ } else {
87
+ const size = header.length + header.size;
88
+ if (!Number.isFinite(size) || size > available) break;
89
+ result.push({ id: header.id, bytes: take(size) });
90
+ }
91
+ }
92
+ if (final && unknown && !available) { result.push({ closedCluster: unknown }); unknown = null; }
93
+ return result;
94
+ };
95
+ return {
96
+ append(bytes) {
97
+ if (bytes.length) { chunks.push(bytes); available += bytes.length; }
98
+ return parse();
99
+ },
100
+ finish() { return parse(true); },
101
+ get pendingSize() {
102
+ return available + (unknown && !unknown.accepted ? unknown.prefix.reduce((sum, part) => sum + part.length, 0) : 0);
103
+ },
104
+ reset() { chunks.length = 0; available = 0; unknown = null; },
105
+ };
106
+ }