---
name: extract_hls_from_cdp_tab
kind: function
lang: py
domain: pipelines
version: "1.0.0"
purity: impure
signature: "def extract_hls_from_cdp_tab(debug_port: int = 9222, url_substring: str | None = None, live_capture_s: float = 6.0, timeout_s: float = 15.0) -> dict"
description: "Extrae URLs de manifiestos HLS (master.m3u8, index*.m3u8) de todas las pestañas e iframes de Chrome via CDP. Combina performance.getEntriesByType('resource') con escucha de eventos Network en vivo para capturar manifests ya cargados y los que se piden tras conectar."
tags: [navegator, cdp, hls, m3u8, scraping]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [json, re, sys, os, time, urllib.request, threading, websocket]
params:
  - name: debug_port
    desc: "Puerto de remote debugging de Chrome (default 9222). Chrome debe lanzarse con --remote-debugging-port=9222."
  - name: url_substring
    desc: "Si se especifica, solo inspecciona targets (pestañas/iframes) cuyo URL contiene este substring. Ej: 'luluvdo.com'. Si None, inspecciona todos."
  - name: live_capture_s
    desc: "Segundos de escucha de eventos Network.requestWillBeSent/responseReceived en vivo por target (default 6.0). Caza manifests que se piden despues de conectar."
  - name: timeout_s
    desc: "Timeout en segundos para conexion WebSocket y operaciones recv por target (default 15.0)."
output: "dict {status: 'ok'|'error', targets: [{url, masters, variants}], all_m3u8: [lista plana deduplicada], error: str}. Si no hay m3u8: status ok, listas vacias."
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/pipelines/extract_hls_from_cdp_tab.py"
---

## Ejemplo

```python
import sys
sys.path.insert(0, "python/functions")
from pipelines.extract_hls_from_cdp_tab import extract_hls_from_cdp_tab

# Chrome lanzado con --remote-debugging-port=9222 --remote-allow-origins=*
# reproduciendo un video en luluvdo.com (o cualquier player hls.js)
r = extract_hls_from_cdp_tab(debug_port=9222, url_substring="luluvdo.com")
print(r["all_m3u8"])
# ['https://cdn.luluvdo.com/.../urlset/master.m3u8?e=28800&t=abc123']

# Sin filtro: escanea todas las pestanas e iframes
r = extract_hls_from_cdp_tab(debug_port=9222)
for target in r["targets"]:
    if target["masters"]:
        print(f"Tab: {target['url']}")
        print(f"  Master: {target['masters']}")
        print(f"  Variantes: {target['variants']}")
```

Lanzar Chrome con las flags necesarias:
```bash
# Desde WSL (Chrome Windows)
/mnt/c/Program\ Files/Google/Chrome/Application/chrome.exe \
  --remote-debugging-port=9222 \
  --remote-allow-origins="*" \
  --user-data-dir=/tmp/chrome_cdp_profile
```

Con proxy NordVPN (para sitios bloqueados por ISP):
```bash
# 1. Levantar bridge
source bash/functions/infra/start_nordvpn_socks_bridge.sh
start_nordvpn_socks_bridge --port 8889
# 2. Lanzar Chrome con proxy
chrome.exe --proxy-server=http://127.0.0.1:8889 --remote-debugging-port=9222 --remote-allow-origins="*"
# 3. Extraer HLS
r = extract_hls_from_cdp_tab(9222)
# 4. Descargar con el mismo proxy
# yt-dlp --proxy http://127.0.0.1:8889 --referer https://luluvdo.com/ r["all_m3u8"][0]
```

## Cuando usarla

Cuando un video se esta reproduciendo en Chrome controlado por CDP y quieres la URL del HLS (master.m3u8) sin reversear el player. Funciona con luluvdo.com, streamwish, filemoon y cualquier player hls.js porque lee lo que el navegador YA pidio al CDN, sin tocar codigo ofuscado. El player ya descifro/cargo el manifest — tu solo lo recoges.

Combina con `start_nordvpn_socks_bridge_bash_infra` cuando el sitio esta bloqueado por ISP: Chrome con --proxy-server apunta al bridge local.

## Gotchas

- **`--remote-allow-origins=*` OBLIGATORIO**: Chrome rechaza conexiones CDP con 403 si falta este flag (o el origin concreto). El header `Origin: http://localhost` en `create_connection` tambien es necesario por el mismo motivo.
- **El `<video>` tiene src `blob:`** (hls.js demuxa en memoria) — NO sirve para obtener el manifest. El URL real solo aparece en performance entries o en eventos Network.
- **El player suele vivir en un iframe**, no en la pestana top — la funcion escanea TODOS los targets para no perderlo.
- **El token del master.m3u8 caduca** (ej. `e=28800` = ~8h) y suele estar ligado a la IP de salida. Descargar desde la MISMA IP y con el mismo proxy. Añadir el Referer del host del player: `yt-dlp --proxy <proxy> --referer https://<host>/ <master.m3u8>`.
- **websocket-client** debe estar en el venv: `uv pip install websocket-client`. Si falta, el error indica el comando exacto.
- **live_capture_s=0** desactiva la escucha en vivo (solo lee performance entries). Util si el video ya esta cargado y quieres rapidez.
- Si `url_substring` es demasiado especifico y el iframe tiene otro dominio (CDN), no pasara el filtro. Usar None para ver todos los targets y luego ajustar.
