import { chromium } from 'playwright'; import { attachRecorder } from 'playwright-recorder-plus'; import fs from 'fs'; import path from 'path'; export class HtmlRender { constructor() { this.browser = null; this.page = null; } async init() { if (!this.browser) { this.browser = await chromium.launch(); this.page = await this.browser.newPage(); } } async renderFrame(html, htmlFile, outFilePath, options = {}) { console.log('Rendering frame from', html?.substring(0, 100) || htmlFile, '->', outFilePath) await this.init(); const vw = (options.viewport && options.viewport.width) || 1080; const vh = (options.viewport && options.viewport.height) || 1920; const context = await this.browser.newContext({ viewport: { width: vw, height: vh } }); const page = await context.newPage(); if (html) { await page.setContent(html, { waitUntil: 'networkidle' }); } else if (htmlFile) { await page.goto(`file://${htmlFile}`, { waitUntil: 'networkidle' }); } else { throw new Error('Either html or htmlFile must be provided.'); } await page.screenshot({ path: outFilePath, ...options }); } async renderVideo(html, htmlFile, outFilePath, durationSec, options = {}) { console.log('Rendering video from', html?.substring(0, 100) || htmlFile, '->', outFilePath, "duration", durationSec) if (!this.browser) { this.browser = await chromium.launch(); } const vw = (options.viewport && options.viewport.width) || 1080; const vh = (options.viewport && options.viewport.height) || 1920; const context = await this.browser.newContext({ viewport: { width: vw, height: vh } }); const page = await context.newPage(); if (html) { await page.setContent(html, { waitUntil: 'networkidle' }); } else if (htmlFile) { const absPath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile); await page.goto(`file://${absPath}`, { waitUntil: 'networkidle' }); } // Force a layout/paint await page.evaluate(() => document.body.offsetHeight); // Attach high-quality recorder const recorder = await attachRecorder(page, { path: outFilePath, fps: options.fps || 30, ffmpegOptions: { // Ensuring high quality for the render farm crf: 18, preset: 'veryfast' } }); // Wait for the requested duration await page.waitForTimeout(durationSec * 1000); // Stop and finalize await recorder.stop(); await recorder.finalized; await page.close(); await context.close(); } async close() { if (this.browser) { await this.browser.close(); this.browser = null; this.page = null; } } }