// scripts/remote_puppeteer.cjs // Conecta a un Chrome en --remote-debugging-port=9222, navega a la URL indicada y saca un screenshot. // Uso: node scripts/remote_puppeteer.cjs [url] [--keep-open] const fs = require('fs') const path = require('path') async function tryFetchJson(url) { try { const r = await fetch(url, { method: 'GET' }) if (!r.ok) return { ok: false, status: r.status, body: await r.text().catch(() => '') } const json = await r.json().catch(() => null) return { ok: true, json } } catch (err) { return { ok: false, error: String(err) } } } async function findDebuggerInfo() { const envUrl = process.env.CHROME_DEBUG_URL const envWs = process.env.CHROME_DEBUG_WS if (envWs) return { webSocketDebuggerUrl: envWs, source: 'CHROME_DEBUG_WS' } const candidates = [] if (envUrl) candidates.push(envUrl) candidates.push('http://127.0.0.1:9222/json/version') candidates.push('http://localhost:9222/json/version') candidates.push('http://0.0.0.0:9222/json/version') candidates.push('http://[::1]:9222/json/version') for (const c of candidates) { try { console.log('[remote_puppeteer] probing', c) const res = await tryFetchJson(c) if (res.ok && res.json && res.json.webSocketDebuggerUrl) { return { webSocketDebuggerUrl: res.json.webSocketDebuggerUrl, source: c, raw: res.json } } if (res.ok) console.log('[remote_puppeteer] probe ok but no webSocketDebuggerUrl in', c, 'response keys:', Object.keys(res.json || {})) else console.log('[remote_puppeteer] probe failed for', c, res.status || res.error || res.body) } catch (err) { console.warn('[remote_puppeteer] probe error', c, err && (err.stack || err.message || String(err))) } } return null } async function runFlow(page) { // This function automates the UI flow in broadcast-panel try { console.log('[remote_puppeteer] waiting for create card (Transmisión en vivo)') const createXPath = "//button[.//span/text() = 'Transmisión en vivo' or .//span[contains(text(),'Transmisión en vivo')]]" await page.waitForXPath(createXPath, { timeout: 15000 }) const [createBtn] = await page.$x(createXPath) if (!createBtn) { console.warn('[remote_puppeteer] create button not found') } else { await createBtn.click() console.log('[remote_puppeteer] clicked create') } // wait a bit for modal await page.waitForTimeout(500) // click 'Omitir por ahora' if present const skipTexts = ["Omitir por ahora", 'Omitar por ahora', 'Skip for now'] let skipped = false for (const txt of skipTexts) { try { const xp = `//*[normalize-space(text())='${txt}']` await page.waitForXPath(xp, { timeout: 2000 }) const [btn] = await page.$x(xp) if (btn) { await btn.click(); skipped = true; console.log('[remote_puppeteer] clicked skip ->', txt); break } } catch (e) { /* continue */ } } // click Empezar ahora / Crear transmisión en vivo const startBtnXPath = "//button[normalize-space(text())='Empezar ahora' or normalize-space(text())='Crear transmisión en vivo' or normalize-space(text())='Guardar cambios' or normalize-space(text())='Start now']" try { await page.waitForXPath(startBtnXPath, { timeout: 8000 }) const [startBtn] = await page.$x(startBtnXPath) if (startBtn) { await startBtn.click(); console.log('[remote_puppeteer] clicked start/create') } } catch (e) { console.warn('[remote_puppeteer] start/create button not found', e && e.message) } // Wait for 'Entrar al estudio' button in the table and click the first const enterXPath = "(//button[contains(normalize-space(.),'Entrar al estudio') or contains(@aria-label,'Entrar al estudio') or normalize-space(text())='Entrar al estudio'])[1]" await page.waitForXPath(enterXPath, { timeout: 10000 }) const [enterBtn] = await page.$x(enterXPath) if (!enterBtn) throw new Error('Entrar al estudio button not found') await enterBtn.click() console.log('[remote_puppeteer] clicked Entrar al estudio') // Wait for StudioPortal overlay to appear await page.waitForFunction(() => { return !!document.querySelector('.studio-portal') || Array.from(document.querySelectorAll('*')).some(el => (el.textContent||'').includes('Esperando token')) }, { timeout: 10000 }) console.log('[remote_puppeteer] Studio overlay present') // Send a postMessage with LIVEKIT_TOKEN const fakeToken = 'puppet-token-' + Math.random().toString(36).slice(2,8) const fakeServer = 'wss://livekit.example' await page.evaluate((tkn, srv) => { window.postMessage({ type: 'LIVEKIT_TOKEN', token: tkn, url: srv, room: 'puppet-room' }, '*') }, fakeToken, fakeServer) console.log('[remote_puppeteer] posted LIVEKIT_TOKEN message') // Wait for StudioPortal to show confirmation await page.waitForFunction(() => Array.from(document.querySelectorAll('*')).some(el => (el.textContent||'').includes('Token recibido desde Broadcast Panel')), { timeout: 8000 }) console.log('[remote_puppeteer] StudioPortal acknowledged token in DOM') return true } catch (err) { console.error('[remote_puppeteer] runFlow error', err && (err.stack || err.message || err)) return false } } async function main() { const target = process.argv.find(a => !a.startsWith('--') && !a.includes('remote_puppeteer.cjs')) || process.argv[2] || 'http://localhost:5175' const KEEP_OPEN = process.env.KEEP_OPEN === '1' || process.argv.includes('--keep-open') try { // ensure fetch exists (Node 18+ has it). If not, lazy import node-fetch if (typeof fetch === 'undefined') { global.fetch = (...args) => import('node-fetch').then(m => m.default(...args)) } console.log('[remote_puppeteer] looking for Chrome remote debugger...') const info = await findDebuggerInfo() if (!info) { console.error('[remote_puppeteer] No remote debugger endpoint found.\n\nCommon fixes:\n - Start chrome with: --remote-debugging-port=9222 --remote-debugging-address=0.0.0.0\n - Ensure no firewall blocks localhost:9222\n - If running chrome inside sandbox/flatpak/snap it may not expose the port to the host.\n - You can set CHROME_DEBUG_URL=http://HOST:9222/json/version or CHROME_DEBUG_WS=ws://HOST:9222/devtools/browser/') process.exit(3) } console.log('[remote_puppeteer] debugger info found from', info.source) console.log('[remote_puppeteer] webSocketDebuggerUrl=', info.webSocketDebuggerUrl) const puppeteer = require('puppeteer-core') console.log('[remote_puppeteer] connecting to browser...') const browser = await puppeteer.connect({ browserWSEndpoint: info.webSocketDebuggerUrl, defaultViewport: { width: 1280, height: 800 } }) // Open a new page (visible in the connected Chrome) const page = await browser.newPage() try { // Bring page to front so user sees the navigation in the visible Chrome instance if (typeof page.bringToFront === 'function') { await page.bringToFront() } // Make sure the window has focus try { await page.evaluate(() => { if (typeof window.focus === 'function') window.focus() }) } catch (e) { /* ignore */ } } catch (e) { /* ignore bringToFront errors */ } console.log('[remote_puppeteer] navigating to', target) await page.goto(target, { waitUntil: 'networkidle2', timeout: 30000 }) // Run the UI flow to open the studio and send token const flowOk = await runFlow(page) const out = path.resolve(process.cwd(), flowOk ? 'remote_screenshot_flow.png' : 'remote_screenshot.png') await page.screenshot({ path: out, fullPage: true }) console.log('[remote_puppeteer] screenshot saved to', out) if (KEEP_OPEN) { console.log('[remote_puppeteer] KEEP_OPEN is set — keeping the page open so you can watch the navigation in the real browser.') console.log('[remote_puppeteer] The page was brought to front. When you want to finish, press Ctrl+C in this terminal to exit and the browser tab will remain open.') // Do not close page or disconnect; keep the Node process alive // Optionally poll to keep alive await new Promise(() => {}) } else { await page.close() await browser.disconnect() process.exit(flowOk ? 0 : 2) } } catch (err) { console.error('[remote_puppeteer] error:', err && (err.stack || err.message || String(err))) process.exit(2) } } main()