Rename and fix screenshots

This commit is contained in:
Andras Schmelczer 2026-02-07 22:19:06 +00:00
parent 9e71ed77df
commit e5d5819098
8 changed files with 81 additions and 18 deletions

View file

@ -0,0 +1,205 @@
import { chromium, type Browser, type Page } from 'playwright';
import { existsSync, readdirSync } from 'fs';
const VIEWPORT = { width: 1200, height: 630 };
const NAVIGATION_TIMEOUT = 15_000;
const TILE_BUFFER_MS = 500;
const POOL_SIZE = 3;
let browser: Browser | null = null;
const pagePool: Page[] = [];
let warmingUp = false;
/**
* Detect if a GPU is available for hardware-accelerated rendering.
* Checks for DRI devices on Linux which indicate GPU + driver availability.
*/
function detectGpu(): boolean {
try {
// Check for DRI render nodes (Linux)
if (existsSync('/dev/dri')) {
const devices = readdirSync('/dev/dri');
// Look for renderD* (render nodes) or card* (display cards)
const hasGpu = devices.some((d) => d.startsWith('renderD') || d.startsWith('card'));
if (hasGpu) {
console.log(`GPU detected: /dev/dri contains ${devices.join(', ')}`);
return true;
}
}
} catch {
// Ignore errors - fall back to software rendering
}
console.log('No GPU detected, using SwiftShader software rendering');
return false;
}
const hasGpu = detectGpu();
function getBrowserArgs(): string[] {
const baseArgs = [
'--no-sandbox',
'--disable-dev-shm-usage',
'--enable-webgl',
'--ignore-gpu-blocklist',
];
if (hasGpu) {
// Use hardware GPU acceleration
return [...baseArgs, '--enable-gpu', '--use-gl=egl'];
} else {
// Fall back to SwiftShader software rendering
return [...baseArgs, '--disable-gpu', '--use-gl=swiftshader'];
}
}
async function ensureBrowser(): Promise<Browser> {
if (!browser || !browser.isConnected()) {
const args = getBrowserArgs();
console.log(`Launching browser with args: ${args.join(' ')}`);
browser = await chromium.launch({ args });
}
return browser;
}
async function createPage(): Promise<Page> {
const instance = await ensureBrowser();
const context = await instance.newContext({
viewport: VIEWPORT,
deviceScaleFactor: 1,
});
return context.newPage();
}
async function warmPool(): Promise<void> {
if (warmingUp) return;
warmingUp = true;
try {
const instance = await ensureBrowser();
while (pagePool.length < POOL_SIZE && instance.isConnected()) {
const page = await createPage();
pagePool.push(page);
}
} finally {
warmingUp = false;
}
}
async function acquirePage(): Promise<Page> {
// Try to get a page from the pool
const page = pagePool.shift();
if (page && !page.isClosed()) {
// Refill pool in background
warmPool().catch(() => {});
return page;
}
// No pooled page available, create one
const newPage = await createPage();
// Start warming pool in background
warmPool().catch(() => {});
return newPage;
}
async function releasePage(page: Page): Promise<void> {
try {
if (page.isClosed()) return;
// Reset page state for reuse
await page.goto('about:blank', { timeout: 5000 }).catch(() => {});
if (!page.isClosed() && pagePool.length < POOL_SIZE) {
pagePool.push(page);
} else {
await page.context().close().catch(() => {});
}
} catch {
await page.context().close().catch(() => {});
}
}
export async function takeScreenshot(url: string): Promise<Buffer> {
const page = await acquirePage();
// Log browser console messages for diagnostics
page.on('console', (msg) => {
if (msg.type() === 'error' || msg.type() === 'warning') {
console.log(`[browser ${msg.type()}] ${msg.text()}`);
}
});
page.on('pageerror', (err) => {
console.log(`[browser exception] ${err.message}`);
});
try {
// Use domcontentloaded instead of networkidle - let __og_ready handle readiness
const response = await page.goto(url, {
waitUntil: 'domcontentloaded',
timeout: NAVIGATION_TIMEOUT,
});
if (response) {
console.log(`Page loaded: ${response.status()} ${response.statusText()}`);
}
// Wait for the frontend to signal readiness
try {
await page.waitForFunction('window.__og_ready === true', {
timeout: NAVIGATION_TIMEOUT,
});
console.log('Frontend signalled ready');
} catch {
console.warn('Timed out waiting for __og_ready, proceeding with partial screenshot');
}
// Extra buffer for map tiles to finish rendering
await page.waitForTimeout(TILE_BUFFER_MS);
const screenshot = await page.screenshot({ type: 'png' });
return Buffer.from(screenshot);
} finally {
// Remove listeners before releasing page back to pool
page.removeAllListeners('console');
page.removeAllListeners('pageerror');
await releasePage(page);
}
}
export async function checkWebGL(): Promise<Record<string, unknown>> {
const page = await acquirePage();
try {
await page.setContent('<canvas id="c" width="1" height="1"></canvas>');
const info = await page.evaluate(() => {
const canvas = document.getElementById('c') as HTMLCanvasElement;
const gl =
canvas.getContext('webgl2') || canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) return { webgl: false, error: 'No WebGL context available' };
const g = gl as WebGLRenderingContext;
const debugExt = g.getExtension('WEBGL_debug_renderer_info');
return {
webgl: true,
version: g.getParameter(g.VERSION),
renderer: debugExt ? g.getParameter(debugExt.UNMASKED_RENDERER_WEBGL) : 'unknown',
vendor: debugExt ? g.getParameter(debugExt.UNMASKED_VENDOR_WEBGL) : 'unknown',
};
});
return info;
} finally {
await releasePage(page);
}
}
export async function closeBrowser(): Promise<void> {
// Close all pooled pages
for (const page of pagePool) {
await page.context().close().catch(() => {});
}
pagePool.length = 0;
if (browser) {
await browser.close().catch(() => {});
browser = null;
}
}
// Pre-warm the pool on module load
warmPool().catch(() => {});