AvanzaCast/scripts/remote_puppeteer.cjs
Cesar Mendivil 8b458a3ddf feat: add initial LiveKit Meet integration with utility scripts, configs, and core components
- Add Next.js app structure with base configs, linting, and formatting
- Implement LiveKit Meet page, types, and utility functions
- Add Docker, Compose, and deployment scripts for backend and token server
- Provide E2E and smoke test scaffolding with Puppeteer and Playwright helpers
- Include CSS modules and global styles for UI
- Add postMessage and studio integration utilities
- Update package.json with dependencies and scripts for development and testing
2025-11-20 12:50:38 -07:00

179 lines
8.3 KiB
JavaScript

// 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/<id>')
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()