- 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
179 lines
8.3 KiB
JavaScript
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()
|