Docs/Skills and code/skill video-visual-production

.claude/skills/video-visual-production/scripts/lib/cdp.js

Code63 lines2.8 KBSkill: video-visual-production

What the file says about itself

Minimal Chrome DevTools Protocol client over Node's built-in WebSocket (Node 22+). No dependencies.

// Minimal Chrome DevTools Protocol client over Node's built-in WebSocket (Node 22+). No dependencies.
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawn } = require('child_process');

const CHROME_CANDIDATES = [
  process.env.CHROME_PATH,
  'C:/Program Files/Google/Chrome/Application/chrome.exe',
  'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe',
  'C:/Program Files/Microsoft/Edge/Application/msedge.exe',
  'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',
].filter(Boolean);

function findChrome() {
  for (const p of CHROME_CANDIDATES) if (fs.existsSync(p)) return p;
  throw new Error('No Chrome or Edge found; set CHROME_PATH');
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function launch(port = 9333) {
  const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'vvp-chrome-'));
  const proc = spawn(findChrome(), [
    '--headless=new', `--remote-debugging-port=${port}`, `--user-data-dir=${profile}`, '--no-first-run', '--no-default-browser-check',
    '--hide-scrollbars', '--disable-extensions', '--mute-audio', '--window-size=1280,900', 'about:blank',
  ], { stdio: 'ignore', windowsHide: true });
  let target = null;
  for (let i = 0; i < 60 && !target; i++) {
    await sleep(250);
    try {
      const list = await (await fetch(`http://127.0.0.1:${port}/json`)).json();
      target = list.find((t) => t.type === 'page');
    } catch (_) { /* not up yet */ }
  }
  if (!target) { proc.kill(); throw new Error('Chrome did not expose a page target'); }
  const ws = new WebSocket(target.webSocketDebuggerUrl);
  await new Promise((res, rej) => { ws.onopen = res; ws.onerror = () => rej(new Error('websocket error')); });
  let id = 0;
  const pending = new Map();
  const listeners = [];
  ws.onmessage = (ev) => {
    const m = JSON.parse(ev.data);
    if (m.id && pending.has(m.id)) { const { res, rej } = pending.get(m.id); pending.delete(m.id); m.error ? rej(new Error(m.error.message)) : res(m.result); }
    else if (m.method) listeners.forEach((l) => l(m));
  };
  const send = (method, params = {}) => new Promise((res, rej) => { const i = ++id; pending.set(i, { res, rej }); ws.send(JSON.stringify({ id: i, method, params })); });
  const waitEvent = (name, ms = 30000) => new Promise((res, rej) => {
    const t = setTimeout(() => rej(new Error('timeout waiting for ' + name)), ms);
    const l = (m) => { if (m.method === name) { clearTimeout(t); listeners.splice(listeners.indexOf(l), 1); res(m.params); } };
    listeners.push(l);
  });
  const close = async () => {
    try { ws.close(); } catch (_) { /* ignore */ }
    proc.kill();
    await sleep(500);
    try { fs.rmSync(profile, { recursive: true, force: true }); } catch (_) { /* profile may be locked briefly */ }
  };
  return { send, waitEvent, close };
}

module.exports = { launch, findChrome, sleep };

Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.