buninu-browser 0.2.4 → 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.
@@ -0,0 +1,76 @@
1
+ {
2
+ "sandbox": {
3
+ "seccomp": {
4
+ "installed": true,
5
+ "reason": null
6
+ },
7
+ "ses": {
8
+ "installed": true,
9
+ "reason": null
10
+ }
11
+ },
12
+ "capture": [
13
+ 1920,
14
+ 1035
15
+ ],
16
+ "reports": [
17
+ {
18
+ "format": "png",
19
+ "count": 20,
20
+ "meanMs": 168.16266580000007,
21
+ "medianMs": 167.81952999999976,
22
+ "bytes": 123889
23
+ },
24
+ {
25
+ "format": "jpeg",
26
+ "count": 20,
27
+ "meanMs": 42.5512477499999,
28
+ "medianMs": 41.08310699999947,
29
+ "bytes": 150292
30
+ },
31
+ {
32
+ "format": "rgba",
33
+ "count": 20,
34
+ "meanMs": 22.61524315000006,
35
+ "medianMs": 23.11667800000032,
36
+ "bytes": 7948800
37
+ },
38
+ {
39
+ "format": "bgra",
40
+ "count": 20,
41
+ "meanMs": 25.103456849999976,
42
+ "medianMs": 25.162263999999595,
43
+ "bytes": 7948800
44
+ },
45
+ {
46
+ "format": "bgra",
47
+ "count": 20,
48
+ "meanMs": 24.67374660000005,
49
+ "medianMs": 25.82330600000023,
50
+ "bytes": 7948800
51
+ },
52
+ {
53
+ "format": "rgba",
54
+ "count": 20,
55
+ "meanMs": 21.321299350000118,
56
+ "medianMs": 21.798502999999982,
57
+ "bytes": 7948800
58
+ },
59
+ {
60
+ "format": "jpeg",
61
+ "count": 20,
62
+ "meanMs": 43.68695314999977,
63
+ "medianMs": 46.23293099999864,
64
+ "bytes": 150292
65
+ },
66
+ {
67
+ "format": "png",
68
+ "count": 20,
69
+ "meanMs": 169.70291704999983,
70
+ "medianMs": 169.88830599999892,
71
+ "bytes": 123889
72
+ }
73
+ ],
74
+ "method": "Native Intel i5-5200U, alternating viewport crops, three warmups then 20 captures per pass; round trip includes controller file write for raw. Not terminal FPS.",
75
+ "date": "2026-10-22"
76
+ }
@@ -0,0 +1,152 @@
1
+ import {test,expect} from 'bun:test';
2
+ import {readFileSync,existsSync,unlinkSync} from 'node:fs';
3
+ import {dirname} from 'node:path';
4
+ import {RendererHost} from '../lib/renderer/host.js';
5
+ import {PacketReader,encodePacket,listenCompositor,connectCompositor} from '../lib/renderer/compositor-channel.js';
6
+ import {RawFrames} from '../lib/paint/raw-frames.js';
7
+ import CanvasKitInit from '../usr/lib/canvaskit/canvaskit.js';
8
+ const kit=CanvasKitInit({wasmBinary:readFileSync(new URL('../usr/lib/canvaskit/canvaskit.wasm',import.meta.url))});
9
+
10
+ test('binary channel handles fragmented packets, rejects oversized input and authenticates peers',async()=>{
11
+ const results=[];const reader=new PacketReader((message,pixels)=>results.push([message,[...pixels]]));
12
+ const bytes=Buffer.concat([encodePacket({id:1},Uint8Array.of(1,2,3)),encodePacket({id:2})]);
13
+ for(let i=0;i<bytes.length;i++)reader.push(bytes.subarray(i,i+1));
14
+ expect(results).toEqual([[{id:1},[1,2,3]],[{id:2},[]]]);
15
+ const header=Buffer.alloc(8);header.writeUInt32LE(0xffffffff,0);
16
+ expect(()=>new PacketReader(()=>{}).push(header)).toThrow('size');
17
+ let reply;const got=new Promise(resolve=>reply=resolve);
18
+ const server=await listenCompositor((message,pixels)=>{server.send({id:message.id,length:pixels.length});},()=>{});
19
+ let peer;
20
+ try{
21
+ await expect(connectCompositor({...server.endpoint,token:'wrong'},()=>{},()=>{})).rejects.toThrow();
22
+ peer=await connectCompositor(server.endpoint,message=>reply(message),()=>{});
23
+ peer.send({id:8},new Uint8Array(1024));expect(await got).toEqual({id:8,length:1024});
24
+ }finally{peer?.close();server.close();}
25
+ });
26
+
27
+ test('raw file owner rejects malformed input and bounds unconsumed frames',()=>{
28
+ const store=new RawFrames();let path;
29
+ try{
30
+ expect(()=>store.write(new Uint8Array(3),1,1,'rgba')).toThrow('Invalid');
31
+ for(let i=0;i<16;i++)path=JSON.parse(store.write(new Uint8Array(4),1,1,'rgba')).path;
32
+ expect(()=>store.write(new Uint8Array(4),1,1,'rgba')).toThrow('stalled');
33
+ }finally{store.close();}
34
+ expect(existsSync(dirname(path))).toBe(false);
35
+ });
36
+
37
+ for(const compositor of [true,false])test(`raw screenshots match PNG and stay fresh after consumer deletion (compositor=${compositor})`,async()=>{
38
+ const host=new RendererHost({compositor});let directory;
39
+ try{
40
+ await host.start();await host.call('resize',32,24,1);
41
+ await host.call('loadDocument',{url:'https://page.test/',source:'<body style="margin:0;background:rgb(211,73,19)">',contentType:'text/html'});
42
+ await host.call('whenLoaded');
43
+ const png=await host.composite('screenshot',{format:'png'});
44
+ const CK=await kit;const image=CK.MakeImageFromEncoded(Buffer.from(png,'base64'));
45
+ const reference=image.readPixels(0,0,{width:32,height:24,colorType:CK.ColorType.RGBA_8888,alphaType:CK.AlphaType.Unpremul,colorSpace:CK.ColorSpace.SRGB});image.delete();
46
+ for(const format of ['rgba','bgra']){
47
+ const frame=JSON.parse(await host.composite('screenshot',{format}));directory=dirname(frame.path);
48
+ const pixels=readFileSync(frame.path);expect(pixels.length).toBe(32*24*4);
49
+ if(format==='bgra')for(let i=0;i<pixels.length;i+=4){const r=pixels[i];pixels[i]=pixels[i+2];pixels[i+2]=r;}
50
+ expect([...pixels]).toEqual([...reference]);unlinkSync(frame.path);
51
+ const next=JSON.parse(await host.composite('screenshot',{format}));expect(next.path).not.toBe(frame.path);expect(existsSync(next.path)).toBe(true);unlinkSync(next.path);
52
+ }
53
+ if(compositor){
54
+ const busy=host.call('evaluate',"(()=>{const end=Date.now()+1800;while(Date.now()<end);return 1})()");await Bun.sleep(150);
55
+ const start=performance.now();const raw=JSON.parse(await host.composite('screenshot',{format:'bgra'}));
56
+ expect(performance.now()-start).toBeLessThan(1400);unlinkSync(raw.path);await busy;
57
+ }
58
+ }finally{host.close();}
59
+ expect(existsSync(directory)).toBe(false);
60
+ },20000);
61
+
62
+ test('normal CDP routes raw requests to the controller without changing standard screenshots',async()=>{
63
+ const {createBrowser}=await import('../lib/headless-shell.js');
64
+ const {CdpServer}=await import('../lib/cdp-server.js');
65
+ const {CDP}=await import('../tools/browser-lab.js');
66
+ const browser=await createBrowser({spareRenderer:false});
67
+ const server=CdpServer.create(browser.context).listen(0,'127.0.0.1');
68
+ const cdp=new CDP(`ws://127.0.0.1:${server.port}/devtools/browser/cdp-server`);
69
+ let directory;
70
+ try{
71
+ await cdp.ready;const {targetId}=await cdp.send('Target.createTarget',{url:'about:blank'});
72
+ const {sessionId}=await cdp.send('Target.attachToTarget',{targetId,flatten:true});cdp.sessionId=sessionId;
73
+ await cdp.send('Emulation.setDeviceMetricsOverride',{width:16,height:9,deviceScaleFactor:1.75,mobile:false});
74
+ const metrics=await cdp.send('Page.getLayoutMetrics');
75
+ expect(metrics.cssVisualViewport).toMatchObject({pageX:0,pageY:0});
76
+ const {data}=await cdp.send('Page.captureScreenshot',{format:'bgra',clip:{x:0,y:0,width:16,height:15/1.75,scale:1}});
77
+ const frame=JSON.parse(data);directory=dirname(frame.path);
78
+ expect(frame).toMatchObject({version:1,transport:'file',format:'bgra',width:28,height:15});expect(existsSync(frame.path)).toBe(true);
79
+ cdp.events.length=0;
80
+ await cdp.send('Page.startScreencast',{format:'png',maxWidth:4,maxHeight:2,clip:{x:0,y:0,width:4,height:2,scale:1},everyNthFrame:1});
81
+ let event;for(let i=0;i<100;i++){event=cdp.events.find(e=>e.method==='Page.screencastFrame');if(event)break;await Bun.sleep(20);}
82
+ expect(event).toBeDefined();
83
+ const crop=Buffer.from(event.params.data,'base64');
84
+ // Native 1.75 density is preserved, rather than rescaling to maxWidth/maxHeight.
85
+ expect([crop.readUInt32BE(16),crop.readUInt32BE(20)]).toEqual([7,4]);
86
+ await cdp.send('Page.screencastFrameAck',{sessionId:event.params.sessionId});
87
+ await cdp.send('Page.stopScreencast');
88
+ const png=await cdp.send('Page.captureScreenshot',{format:'png'});
89
+ expect([...Buffer.from(png.data,'base64').subarray(0,4)]).toEqual([137,80,78,71]);
90
+ }finally{cdp.close();server.stop(true);browser.close();}
91
+ expect(existsSync(directory)).toBe(false);
92
+ },20000);
93
+
94
+ test('closing during listener startup rejects promptly and a new start remains usable',async()=>{
95
+ const host=new RendererHost();
96
+ const pending=host.start();host.close();
97
+ await expect(pending).rejects.toThrow('closed');
98
+ try{await host.start();expect(host.pid).toBeGreaterThan(0);}finally{host.close();}
99
+ },15000);
100
+
101
+ for(const compositor of [true,false])test(`fractional CDP clips preserve exact physical dimensions and raw pixels (compositor=${compositor})`,async()=>{
102
+ const host=new RendererHost({compositor});
103
+ try {
104
+ await host.start();await host.call('resize',16,9,1.75);
105
+ await host.call('loadDocument',{url:'https://clip.test/',contentType:'text/html',source:'<body style="margin:0;background:red"><div style="height:3px;background:blue"></div><div style="height:100px"></div>'});
106
+ await host.call('whenLoaded');
107
+ const full=JSON.parse(await host.composite('screenshot',{format:'rgba'}));
108
+ expect([full.width,full.height]).toEqual([28,16]);
109
+ const clip={x:0,y:0,width:16,height:15/1.75,scale:1};
110
+ const cropped=JSON.parse(await host.composite('screenshot',{format:'rgba',clip}));
111
+ expect([cropped.width,cropped.height]).toEqual([28,15]);
112
+ expect(readFileSync(cropped.path)).toEqual(readFileSync(full.path).subarray(0,28*15*4));
113
+ const CK=await kit;const png=CK.MakeImageFromEncoded(Buffer.from(await host.composite('screenshot',{format:'png',clip}),'base64'));
114
+ expect([png.width(),png.height()]).toEqual([28,15]);png.delete();
115
+ await host.composite('scroll',0,4);
116
+ const offset=await host.composite('scrollOffset');
117
+ const scrolled=JSON.parse(await host.composite('screenshot',{format:'bgra',clip:{...clip,y:offset.y}}));
118
+ expect([...readFileSync(scrolled.path).subarray(0,4)]).toEqual([0,0,255,255]);
119
+ await expect(host.composite('screenshot',{format:'rgba',clip:{...clip,width:Infinity}})).rejects.toThrow();
120
+ }finally{host.close();}
121
+ },20000);
122
+
123
+ test('scale-one full-width fractional clip borrows contiguous raster bytes',async()=>{
124
+ const {captureViewport}=await import('../lib/paint/frame.js');
125
+ const tile={x:0,y:0,scale:1.75,pixelWidth:28,pixelHeight:16,pixels:new Uint8Array(28*16*4)};
126
+ const frame=captureViewport(tile,0,0,16,9,{x:0,y:0,width:16,height:15/1.75,scale:1});
127
+ expect(frame.pixels.buffer).toBe(tile.pixels.buffer);
128
+ expect(frame.pixels.byteLength).toBe(28*15*4);
129
+ });
130
+
131
+ test('in-flight raw capture follows an origin renderer swap without exposing compositor closed',async()=>{
132
+ const {createBrowser}=await import('../lib/headless-shell.js');
133
+ const server=Bun.serve({port:0,fetch:()=>new Response('<body style="margin:0;background:rgb(211,73,19)">',{headers:{'content-type':'text/html'}})});
134
+ const browser=await createBrowser({spareRenderer:false});
135
+ const original=RendererHost.prototype.composite;let rejectCapture,armed=true;
136
+ const reached=Promise.withResolvers();
137
+ RendererHost.prototype.composite=function(method,...params){
138
+ if(armed&&method==='screenshot'){
139
+ armed=false;reached.resolve();return new Promise((_,reject)=>rejectCapture=reject);
140
+ }
141
+ return original.call(this,method,...params);
142
+ };
143
+ try{
144
+ const pending=browser.context.screenshot({format:'bgra'});
145
+ await reached.promise;
146
+ await browser.context.navigate(`http://127.0.0.1:${server.port}/`);
147
+ rejectCapture(Error('compositor closed'));
148
+ const frame=JSON.parse(await pending);const bytes=readFileSync(frame.path);
149
+ expect(frame.format).toBe('bgra');expect([...bytes.subarray(0,4)]).toEqual([19,73,211,255]);
150
+ unlinkSync(frame.path);
151
+ }finally{RendererHost.prototype.composite=original;browser.close();server.stop(true);}
152
+ },20000);
@@ -0,0 +1,36 @@
1
+ import {test, expect} from 'bun:test';
2
+ import {RendererHost} from '../lib/renderer/host.js';
3
+
4
+ test('viewport resize notifies window listeners with fresh geometry and repaints their changes',async()=>{
5
+ const host=new RendererHost({timeout:20000});
6
+ try {
7
+ await host.start();
8
+ await host.call('resize',320,240,1);
9
+ await host.call('loadDocument',{url:'https://resize.test/',contentType:'text/html',
10
+ source:'<body style="margin:0"><div id="box" style="width:100%;height:10px"></div><canvas id="canvas"></canvas></body>'});
11
+ await host.call('whenLoaded');
12
+ await host.call('evaluate',`window.events=[];window.propertyEvents=0;
13
+ window.onresize=()=>window.propertyEvents++;
14
+ window.addEventListener('resize',e=>{
15
+ const c=document.getElementById('canvas');
16
+ c.width=innerWidth;c.height=innerHeight;
17
+ events.push([innerWidth,innerHeight,devicePixelRatio,
18
+ document.getElementById('box').getBoundingClientRect().width,
19
+ e.target===window,e.bubbles,e.cancelable]);
20
+ document.body.style.backgroundColor='rgb(17,34,51)';
21
+ });`);
22
+ await host.call('resize',400,300,1.5);
23
+ expect(await host.call('evaluate','JSON.stringify(events)')).toBe('[[400,300,1.5,400,true,false,false]]');
24
+ expect(await host.call('evaluate','propertyEvents')).toBe(1);
25
+ expect(await host.call('evaluate',`[document.getElementById('canvas').width,document.getElementById('canvas').height].join(',')`)).toBe('400,300');
26
+ const png=Buffer.from(await host.composite('screenshot',{format:'png'}),'base64');
27
+ expect([png.readUInt32BE(16),png.readUInt32BE(20)]).toEqual([600,450]);
28
+ expect(await host.call('evaluate',"getComputedStyle(document.body).backgroundColor")).toBe('rgba(17, 34, 51, 1)');
29
+ await host.call('resize',400,300,1.5);
30
+ await host.call('resize',400,300,2);
31
+ expect(await host.call('evaluate','events.length')).toBe(1);
32
+ await host.call('resize',400,200,2);
33
+ expect(await host.call('evaluate','JSON.stringify(events[1])')).toBe('[400,200,2,400,true,false,false]');
34
+ expect(await host.call('evaluate','propertyEvents')).toBe(2);
35
+ }finally{host.close();}
36
+ },30000);
@@ -0,0 +1,32 @@
1
+ // Run with Bun on the host to measure capture delivery, not terminal FPS.
2
+ // Uses a local synthetic page, the normal renderer sandbox and changing crops.
3
+ import {RendererHost} from '../lib/renderer/host.js';
4
+ import {statSync,unlinkSync} from 'node:fs';
5
+ const count=Number(process.argv.find(a=>a.startsWith('--samples='))?.split('=')[1] ?? 30);
6
+ if(!Number.isInteger(count)||count<1||count>1000)throw Error('Invalid sample count');
7
+ const host=new RendererHost();
8
+ const reports=[];
9
+ try{
10
+ await host.start();await host.call('resize',1280,690,1.5);
11
+ await host.call('loadDocument',{url:'https://benchmark.test/',contentType:'text/html',source:'<body style="margin:0">'+Array.from({length:80},(_,i)=>`<div style="height:44px;background:rgb(${i*17%256},${i*37%256},${i*71%256})">Frame ${i} 中文 ABCDEFG 0123456789</div>`).join('')});
12
+ await host.call('whenLoaded');await Bun.sleep(500);
13
+ const formats=process.argv.find(a=>a.startsWith('--formats='))?.slice('--formats='.length).split(',') ?? ['png','jpeg','rgba','bgra','bgra','rgba','jpeg','png'];
14
+ for(const format of formats){
15
+ if(!['png','jpeg','rgba','bgra'].includes(format))throw Error('Invalid format');
16
+ const samples=[];let bytes=0;
17
+ for(let i=0;i<count+3;i++){
18
+ await host.composite('scroll',0,i%2?17:-17);
19
+ const start=performance.now();
20
+ const data=await host.composite('screenshot',{format,quality:85,optimizeForSpeed:true});
21
+ const ms=performance.now()-start;
22
+ if(format==='rgba'||format==='bgra'){
23
+ const f=JSON.parse(data);bytes=statSync(f.path).size;unlinkSync(f.path);
24
+ }else bytes=Buffer.byteLength(data,'base64');
25
+ if(i>=3)samples.push(ms);
26
+ }
27
+ samples.sort((a,b)=>a-b);
28
+ const report={format,count,meanMs:samples.reduce((a,b)=>a+b,0)/count,medianMs:samples[Math.floor(count/2)],bytes};
29
+ reports.push(report);console.log(JSON.stringify(report));
30
+ }
31
+ console.log(JSON.stringify({sandbox:host.sandbox,capture:[1920,1035],reports}));
32
+ }finally{host.close();}