Compare commits
7 commits
0f8e0fec53
...
6e8bca2ac4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e8bca2ac4 | ||
|
|
5fa8455b21 | ||
|
|
944d6529b2 | ||
|
|
42fc408947 | ||
|
|
192b474c60 | ||
|
|
c8d7c4bba5 | ||
|
|
3592b9f95a |
4 changed files with 295 additions and 2 deletions
|
|
@ -49,10 +49,21 @@ torrent. La promesa "play instantáneo cache-fast" no ocurre. Falta: source debr
|
||||||
en el path de streaming + cache-availability + **fallback torrent↔debrid mid-stream**.
|
en el path de streaming + cache-availability + **fallback torrent↔debrid mid-stream**.
|
||||||
Diseño por fases (2a direct-play / 2b HLS-desde-URL / 2c fallback) en el estado abajo.
|
Diseño por fases (2a direct-play / 2b HLS-desde-URL / 2c fallback) en el estado abajo.
|
||||||
|
|
||||||
### Hueco #3 — Device-profile + direct-play + ABR ⬜
|
### Hueco #3 — Device-profile + direct-play + ABR 🔵 EN CURSO (ver estado abajo)
|
||||||
El path HLS **siempre re-encoda** (incluso mp4 h264/aac ya compatible). `DecideAction`
|
El path HLS **siempre re-encoda** (incluso mp4 h264/aac ya compatible). `DecideAction`
|
||||||
(passthrough/remux) existe pero muerto en el path browser. Sin negociación por
|
(passthrough/remux) existe pero muerto en el path browser. Sin negociación por
|
||||||
capacidades del dispositivo. Sin ABR multi-bitrate.
|
capacidades del dispositivo. Sin ABR multi-bitrate.
|
||||||
|
Diseño por fases (3a direct-play / 3b remux-HLS / 3c capability-negotiation / 3d ABR)
|
||||||
|
en el estado abajo. **Fase 3a CERRADA** (CLI c8d7c4b + web 636fbe59); 3b/3c/3d pendientes.
|
||||||
|
|
||||||
|
### Hueco #4 — Pre-transcode (transcode-on-download) 🔵 DISEÑADO (ver estado abajo)
|
||||||
|
Al completar una descarga/import, transcodificar/remuxar en background para que el
|
||||||
|
PRIMER play sea instantáneo (direct o cache-HIT), sin transcode en vivo.
|
||||||
|
Optimización, nunca bloqueante: si no terminó a tiempo → fallback a transcode en
|
||||||
|
vivo (HLS actual). Reaprovecha `hls_cache.go` (cache-HIT ya sirve instantáneo) +
|
||||||
|
el pipeline de `prewarm` (ya hace encode de la siguiente ep) — generaliza prewarm a
|
||||||
|
"todo download, configurable" y puebla también el artefacto direct-play. Configurable
|
||||||
|
desde la web. Diseño + set de opciones en el estado abajo.
|
||||||
|
|
||||||
### Huecos medios ⬜
|
### Huecos medios ⬜
|
||||||
- Sin gestión de espacio en disco (`Statfs`) → disco lleno revienta a mitad.
|
- Sin gestión de espacio en disco (`Statfs`) → disco lleno revienta a mitad.
|
||||||
|
|
@ -194,3 +205,254 @@ Empezar por 2a (valor inmediato, riesgo bajo), 2b y 2c como iteraciones.
|
||||||
**Mejora detectada:** `resolve.go:22` ordena `torrent > debrid > usenet`; para el
|
**Mejora detectada:** `resolve.go:22` ordena `torrent > debrid > usenet`; para el
|
||||||
diferenciador cache-fast convendría que, **cuando hay cache debrid confirmada**,
|
diferenciador cache-fast convendría que, **cuando hay cache debrid confirmada**,
|
||||||
el orden de STREAMING (no el de descarga) prefiera debrid.
|
el orden de STREAMING (no el de descarga) prefiera debrid.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hueco #3 — Device-profile + direct-play + ABR
|
||||||
|
**Estado:** 🔵 EN CURSO (2026-05-31). Análisis cerrado; fase 3a en implementación.
|
||||||
|
|
||||||
|
**Problema (confirmado en el análisis):**
|
||||||
|
- El path browser usa **HLS y SIEMPRE re-encoda**: `buildHLSFFmpegArgsAt`
|
||||||
|
(`engine/hls.go`) pone `-c:v libx264|nvenc|…` + cadena de filtros completa
|
||||||
|
(scale/format/setparams) + AAC, sin rama de copia. Un mp4 h264/aac 8-bit SDR
|
||||||
|
que el navegador reproduciría tal cual se transcodifica entero. Coste de CPU
|
||||||
|
puro desperdicio.
|
||||||
|
- `DecideAction` + `diskFileSource`/`transcodeSource` (`engine/probe.go`,
|
||||||
|
`engine/stream_source.go`) **son código muerto**: cero callers en producción,
|
||||||
|
solo tests. Distinguen `passthrough/remux/remux-audio/transcode-video` y detectan
|
||||||
|
10-bit/HDR — la lógica de decisión ya existe, no está cableada.
|
||||||
|
|
||||||
|
**Lo que ya hay y se reaprovecha:**
|
||||||
|
- El agente ya expone **dos paths** en el StreamServer (puerto 11818):
|
||||||
|
- `/stream` → sirve el fichero crudo con `http.ServeContent` (HTTP Range
|
||||||
|
completo, sin ffmpeg, ya tokenizado). **Direct-play ya es posible aquí.**
|
||||||
|
- `/hls/<id>/…` → transcode HLS.
|
||||||
|
- El web **construye las URLs** (HLS hoy) desde la info de red del agente
|
||||||
|
(`streamPort`, `tailscaleIp`, `lanIp`, `funnelUrl`, `streamSecret`) y **puede
|
||||||
|
mintear tokens** (`mintStreamToken`, scope `stream` es constante). O sea: el web
|
||||||
|
puede construir la URL `/stream?t=…` de direct-play él mismo.
|
||||||
|
- `libraryItem` ya guarda del scan: `videoCodec`, `audioCodec`, `bitDepth`, `hdr`,
|
||||||
|
`resolution`. Con el contenedor (extensión de `fileName`), el web tiene todo
|
||||||
|
para decidir direct-play SIN re-probar.
|
||||||
|
|
||||||
|
**Diseño por fases (de menos a más riesgo):**
|
||||||
|
|
||||||
|
- **Fase 3a — direct-play passthrough para items de biblioteca.** *El web decide.*
|
||||||
|
*Slice acotado, ambos sentidos de version-skew seguros vía gate de versión.*
|
||||||
|
1. WEB `decidePlayMethod({videoCodec,audioCodec,bitDepth,hdr,container})` →
|
||||||
|
`"direct" | "hls"` (espeja la rama passthrough de Go `DecideAction`: solo
|
||||||
|
`mp4/m4v` + `h264` + `aac` + 8-bit + SDR → direct; todo lo demás → hls).
|
||||||
|
2. WEB gate: `supportsDirectPlay(agentVersion)` (constante de versión mínima).
|
||||||
|
Direct-play solo si el agente la soporta; si no → hls (sin regresión).
|
||||||
|
3. WEB sesión: en la rama `libraryItemPublicId`, seleccionar los campos codec;
|
||||||
|
calcular `playMethod` (gated); persistirlo en `streamingSession.play_method`
|
||||||
|
(migración aditiva, `db:generate`); devolver `playMethod` + `streamUrls`
|
||||||
|
(`/stream?t=` minteadas por el web, lan/ts/funnel) en la respuesta.
|
||||||
|
4. WEB sync: `getPendingStreamSessions` emite `playMethod` al agente.
|
||||||
|
5. CLI: `StreamSession.PlayMethod string`; en `OnStreamSession`, si
|
||||||
|
`PlayMethod=="direct"` → `streamSrv.SetFile(NewDiskFileProvider(path))` +
|
||||||
|
`MarkSessionReady` (sin ffmpeg). Else → `StartHLSSession` (actual).
|
||||||
|
6. WEB player (`HlsStreamPlayer.tsx`): si `data.playMethod==="direct"` → usar
|
||||||
|
`data.streamUrls` + attach nativo `<video src>` (mp4 = reproducible en todo
|
||||||
|
navegador, sin hls.js). Else → flujo HLS actual.
|
||||||
|
- **Limitación honesta:** solo cubre items de biblioteca (escaneados, con
|
||||||
|
metadata codec). Raw `infoHash`/`taskId` → hls (sin probe). Cubrir esos
|
||||||
|
casos = fase 3a-bis (el agente decide tras probar, reportando playMethod por
|
||||||
|
`MarkSessionReady` — requiere extender el payload + SSE + diferir el attach
|
||||||
|
del player al evento ready). Diferido por mayor superficie.
|
||||||
|
|
||||||
|
- **Fase 3b — remux fMP4 progresivo vía /stream (ENFOQUE ELEGIDO 2026-05-31).**
|
||||||
|
Caso `mkv` (u otro contenedor no-mp4) con h264 + aac + 8-bit + SDR: codecs ya
|
||||||
|
browser-native, solo el contenedor estorba. `-c copy` evita el re-encode de vídeo.
|
||||||
|
Descartado HLS-copy (duraciones de segmento variables vs manifiesto pre-render →
|
||||||
|
rompe seek; arreglarlo = probe de keyframes lento o reescribir el núcleo HLS).
|
||||||
|
**Enfoque:** ffmpeg `-c copy -movflags +frag_keyframe+empty_moov+default_base_moof
|
||||||
|
-f mp4` mkv→fMP4 a fichero temporal **creciente**; servir ese fMP4 por **/stream**
|
||||||
|
(mismo path direct-play 3a, attach nativo, sin hls.js, sin manifiesto).
|
||||||
|
**Núcleo real (la parte no-trivial):** servir un fichero que **crece** mientras
|
||||||
|
ffmpeg escribe. El `/stream` actual usa `http.ServeContent` (asume fichero completo
|
||||||
|
y seekable). Hay que:
|
||||||
|
- Resucitar/adaptar el `transcodeSource` muerto (`engine/stream_source.go`):
|
||||||
|
ffmpeg→tmp creciente, `ReadAt` con bloqueo hasta que los bytes existan
|
||||||
|
(`readBlockTimeout`), `EstimatedSize` = bitrate×duración para que la barra del
|
||||||
|
player tenga timeline.
|
||||||
|
- Un **responder de Range manual** en /stream para fuentes no-finales (en vez de
|
||||||
|
`http.ServeContent`): leer `Range`, `ReadAt` la fuente, escribir 206 +
|
||||||
|
`Content-Range` con el tamaño estimado. El path mp4-completo (3a) sigue usando
|
||||||
|
ServeContent (rápido).
|
||||||
|
- Caveat: seek-adelante a zona no-remuxada bloquea hasta que el copy la alcanza
|
||||||
|
(copy es I/O-bound, rápido). Seek-atrás (bytes ya en disco) inmediato.
|
||||||
|
**Plan de incrementos seguros:**
|
||||||
|
- **3b-i (agente, dormido):** `remuxSource` + responder Range para fuentes
|
||||||
|
crecientes, gateado tras `PlayMethod=="remux"` (que el web aún no envía) →
|
||||||
|
commiteable sin romper nada, con tests.
|
||||||
|
- **3b-ii (web+player):** `decidePlayMethod` devuelve `"remux"` para
|
||||||
|
contenedor-no-mp4 + h264/aac/8-bit/SDR; player trata `playMethod != "hls"` igual
|
||||||
|
que direct (streamUrls + attach nativo). Activa 3b. Mismo gate de versión.
|
||||||
|
**Ficheros:** CLI `engine/stream_source.go` (remuxSource), `engine/stream_server.go`
|
||||||
|
(range responder + provider creciente), `cmd/daemon.go` (branch `remux`),
|
||||||
|
`engine/transcoder.go` (args `-c copy` fMP4). WEB `lib/stream/play-method.ts`
|
||||||
|
(+"remux"), `stream/session/route.ts`, `HlsStreamPlayer.tsx` (`!= "hls"`).
|
||||||
|
|
||||||
|
- **Fase 3c — capability negotiation (device-profile).** El web envía
|
||||||
|
`{maxHeight, codecs:[h264,hevc,av1], containers}` (de UA + `canPlayType`).
|
||||||
|
`decidePlayMethod` se hace device-aware: p.ej. Safari/AppleTV que reproduce HEVC
|
||||||
|
nativo → passthrough HEVC en vez de transcode HEVC→h264. Reemplaza el heurístico
|
||||||
|
UA-burdo de `resolveAutoQuality`. Web+CLI.
|
||||||
|
|
||||||
|
- **Fase 3d — ABR multi-bitrate.** Ladder de renditions en el master playlist +
|
||||||
|
N pipelines ffmpeg / segmentos por rendition. Alto esfuerzo, baja prioridad;
|
||||||
|
el modelo single-viewer reduce su valor. Último.
|
||||||
|
|
||||||
|
**Ficheros a tocar (3a):** CLI `internal/agent/types.go` (+PlayMethod),
|
||||||
|
`internal/cmd/daemon.go` (branch SetFile vs HLS). WEB
|
||||||
|
`src/lib/services/agent-version-compare.ts` (gate), `src/lib/stream/play-method.ts`
|
||||||
|
(nuevo), `src/lib/stream-token.ts` (scope stream), `src/lib/db/schema.ts` +
|
||||||
|
migración (`streamingSession.play_method`), `src/app/api/internal/stream/session/route.ts`
|
||||||
|
(decisión + URLs), `src/lib/services/agent.ts` (`getPendingStreamSessions` emite
|
||||||
|
playMethod), `src/components/stream/HlsStreamPlayer.tsx` (attach nativo).
|
||||||
|
|
||||||
|
**Seguridad de version-skew (3a):**
|
||||||
|
- Web nuevo + agente viejo: gate `supportsDirectPlay` ve versión vieja → hls. ✓
|
||||||
|
- Web viejo + agente nuevo: web nunca manda `direct` → agente hls. ✓
|
||||||
|
- Campo `PlayMethod` desconocido en agente viejo = ignorado por el unmarshal. ✓
|
||||||
|
|
||||||
|
**Empezar por 3a** (valor inmediato — el caso primario de unarr es la biblioteca
|
||||||
|
local escaneada; mp4-h264-aac es común en web-dl/YIFY). 3b/3c/3d como iteraciones.
|
||||||
|
|
||||||
|
**Hecho (Fase 3a CERRADA 2026-05-31):**
|
||||||
|
- CLI (`feat/unarr-agent` c8d7c4b): `StreamSession.PlayMethod`; `OnStreamSession`
|
||||||
|
ramifica `direct` → `SetFile(NewDiskFileProvider)` + `MarkSessionReady` (sin
|
||||||
|
ffmpeg, antes del check de ffmpeg para funcionar con transcode off). `go build`
|
||||||
|
+ `vet` + tests verdes.
|
||||||
|
- WEB (`feat/unarr-brand` 636fbe59): `decidePlayMethod()` (espeja la rama
|
||||||
|
passthrough de Go, conservador) + test unitario; gate `supportsDirectPlay`
|
||||||
|
(`DIRECT_PLAY_MIN_VERSION = 0.10.0`); decisión en la ruta de sesión (solo
|
||||||
|
library item + sin downscale + `audioIndex == -1`); `buildStreamUrls` mintea
|
||||||
|
token scope `stream` (paridad Go); `streaming_session.play_method` (migración
|
||||||
|
0135) emitido al agente vía `getPendingStreamSessions`; player ramifica a
|
||||||
|
`<video src>` nativo. lint + typecheck:all + 2333 unit + build (brand unarr) OK.
|
||||||
|
- Revisión adversarial (correctness + security/parity, 2 agentes): **0 hallazgos
|
||||||
|
bloqueantes**. Token parity y version-skew (ambos sentidos) confirmados.
|
||||||
|
|
||||||
|
**Correcciones de la revisión propia (3a):** direct-play exige `audioIndex == -1`
|
||||||
|
(servir el fichero entero no respeta una pista de audio no-default elegida por el
|
||||||
|
usuario → esos casos van a HLS con `-map 0:a:N`).
|
||||||
|
|
||||||
|
**Smoke e2e (3a) — PASADO 2026-05-31** (agente dev 0.10.0 build local + item de
|
||||||
|
biblioteca mp4-h264-aac `/mnt/nas/peliculas/.../Tangled.Ever.After...mp4` + browser):
|
||||||
|
- POST `/api/internal/stream/session` → `playMethod: direct`, `streamUrls` con
|
||||||
|
`/stream?t=` (token web scope `stream`), `hlsUrls: null`. ✓
|
||||||
|
- Agente: `[stream …] direct-play: Tangled…mp4` (SetFile, sin ffmpeg). ✓
|
||||||
|
- `/stream`: HEAD 200 `video/mp4` `Content-Length 128321419`; GET Range 0-1023 →
|
||||||
|
206 + bytes mp4 reales (`ftyp isom…avc1`). **Token web verificado por Go → paridad
|
||||||
|
cross-lenguaje confirmada en vivo** (sin token → 404). ✓
|
||||||
|
- CORS desde origen browser (`localhost:3030`): ACAO correcto, preflight 204. ✓
|
||||||
|
- Browser: `<video>.currentSrc` = `/stream?t=…` (NO `/hls`), `readyState 4`,
|
||||||
|
reproduciéndose, 1920×1080 nativo, **13 reqs `/stream`, 0 `/hls`**, attach
|
||||||
|
**nativo** (`[hls] (native) loadedmetadata`, sin hls.js). Telemetría
|
||||||
|
metric/progress OK. ✓
|
||||||
|
|
||||||
|
**Bug pre-existente encontrado + arreglado durante el smoke** (web 764f5b01): el
|
||||||
|
allow-list de la marca unarr (`src/lib/branding/routes.ts`) NO incluía
|
||||||
|
`/api/internal/agent` ni `/api/internal/stream` → en unarr el agente daba 404 al
|
||||||
|
registrar y el player 404 al crear sesión. **El streaming + agente de unarr estaban
|
||||||
|
rotos de raíz.** Añadidos al allow-list (superficie del agente/media propio del
|
||||||
|
usuario, cero superficie torrent).
|
||||||
|
|
||||||
|
**Nota de release:** versión bumpeada a **0.10.0** (`version.go`, CLI 944d652) — solo
|
||||||
|
binario local para el smoke, **sin publicar nada**. `DIRECT_PLAY_MIN_VERSION = 0.10.0`
|
||||||
|
(web 52d958f0). Al publicar la release real del CLI, debe ser >= 0.10.0.
|
||||||
|
|
||||||
|
**Backlog detectado en 3a (baja prioridad):**
|
||||||
|
- `streaming_session.transport` queda `"hls"` también para sesiones direct
|
||||||
|
(el enum `TRANSPORT_VALUES` solo tiene `"hls"`); telemetría imprecisa, no bug.
|
||||||
|
Añadir `"direct"` al vocabulario cuando se toque la métrica.
|
||||||
|
- Modelo single-viewer: dos plays direct simultáneos → el último `SetFile` gana;
|
||||||
|
el tab viejo reproduciría contenido nuevo en silencio (HLS al menos 404ea).
|
||||||
|
- Direct-play no aplica `audioIndex` ni extrae subs a WebVTT (usa pistas
|
||||||
|
embebidas vía `<video>` nativo); subs bitmap no se ven. Aceptable en 3a.
|
||||||
|
- Listener `loadedmetadata {once:true}` del attach nativo no se limpia
|
||||||
|
explícitamente en cleanup (idempotente, impacto nulo).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Hueco #4 — Pre-transcode (transcode-on-download)
|
||||||
|
**Estado:** 🔵 DISEÑADO (2026-05-31), pendiente de implementar.
|
||||||
|
|
||||||
|
**Qué es:** al completar una descarga (o import a biblioteca), procesar en
|
||||||
|
background para que la reproducción sea **instantánea** sin transcode en vivo.
|
||||||
|
Es una optimización: si no terminó cuando el usuario da play → fallback al
|
||||||
|
transcode en vivo (HLS actual). **Nunca bloquea.**
|
||||||
|
|
||||||
|
**Sinergia con lo existente (clave — gran parte de la infra ya está):**
|
||||||
|
- `hls_cache.go`: un encode HLS completo se cachea y el cache-HIT lo sirve
|
||||||
|
instantáneo (cero ffmpeg). Pre-transcode = poblar esa cache antes del play.
|
||||||
|
- `stream-prewarm.ts` + `createPrewarmSession`: ya lanza un encode HLS de la
|
||||||
|
siguiente ep en background. Pre-transcode = generalizar prewarm a "cualquier
|
||||||
|
download, configurable", + producir también el artefacto direct-play (3a).
|
||||||
|
- Por tanto el trabajo NUEVO es: (1) disparador on-download-complete, (2)
|
||||||
|
superficie de config en web, (3) gobernanza de recursos + cola, (4) decisión
|
||||||
|
"qué producir" (remux mp4 para 3a vs HLS cache vs nada si ya es native).
|
||||||
|
|
||||||
|
**Opciones a exponer en la web (set propuesto):**
|
||||||
|
|
||||||
|
1. **Activación + disparador**
|
||||||
|
- Toggle global on/off (default OFF — CPU/disco intensivo).
|
||||||
|
- Disparador: al completar descarga / al escanear-importar / manual
|
||||||
|
("optimizar ahora" por item) / programado (ventana horaria).
|
||||||
|
- Default recomendado: on-download-complete, pero solo en ventana idle + sin
|
||||||
|
stream en vivo activo.
|
||||||
|
|
||||||
|
2. **Qué producir (target) — modo Auto recomendado (por probe):**
|
||||||
|
- ya browser-native (mp4 h264/aac 8-bit SDR) → **nada** (3a lo sirve crudo).
|
||||||
|
- solo contenedor incompatible (mkv h264/aac) → **remux** a mp4 (barato, sin
|
||||||
|
re-encode; habilita 3a direct-play). *(necesita 3b para el manifiesto.)*
|
||||||
|
- codec incompatible (HEVC/AV1/10-bit/HDR) → **transcode** a H.264 (caro).
|
||||||
|
- Modos: solo-remux / remux+transcode / forzar H.264 universal.
|
||||||
|
- Formato salida: mp4 direct-play (seek nativo) vs HLS cache (multi-network)
|
||||||
|
vs ambos. Recomendado: mp4 si compatible, HLS si requiere transcode.
|
||||||
|
|
||||||
|
3. **Calidad**
|
||||||
|
- Mantener original (passthrough cuando se pueda) / cap 1080p / ladder ABR
|
||||||
|
(480/720/1080/original — encaja con 3d).
|
||||||
|
- "Solo transcodear si ayuda" (no tocar lo ya compatible).
|
||||||
|
|
||||||
|
4. **Selección / alcance**
|
||||||
|
- Todo / solo biblioteca (pelis+series) / solo lo problemático (p.ej. solo
|
||||||
|
4K HEVC, dejar h264).
|
||||||
|
- Solo watchlist / recién añadido / todo. Reglas por carpeta de biblioteca.
|
||||||
|
|
||||||
|
5. **Gobernanza de recursos (lo más importante — es pesado):**
|
||||||
|
- Concurrencia (N transcodes paralelos, default 1).
|
||||||
|
- HW accel si disponible (nvenc/qsv/vaapi); cap de threads CPU.
|
||||||
|
- Ventana horaria (solo idle, p.ej. 02:00–08:00).
|
||||||
|
- **Pausar cuando hay stream en vivo** (no pelear por CPU con la reproducción).
|
||||||
|
- Prioridad de cola (watchlist primero / más pequeño primero / más nuevo).
|
||||||
|
- (Laptops) solo con AC / no en batería.
|
||||||
|
|
||||||
|
6. **Disco / retención (liga con el hueco medio de espacio en disco):**
|
||||||
|
- Dónde guardar (cache dir) + tamaño máx + evicción LRU (ya parcial en cache).
|
||||||
|
- Mantener SIEMPRE el original; el transcode es artefacto adicional.
|
||||||
|
- TTL: borrar pre-transcode no visto en N días; pin a visto/favorito.
|
||||||
|
- Re-transcodear al cambiar la config de calidad (invalidación).
|
||||||
|
|
||||||
|
7. **UX / estado**
|
||||||
|
- Cola + progreso por item en la web ("Optimizando para reproducción
|
||||||
|
instantánea…"). Badge en library card: "listo para play instantáneo" vs
|
||||||
|
"se transcodificará al reproducir". Notificación al terminar (opcional).
|
||||||
|
|
||||||
|
8. **Fallback / límites**
|
||||||
|
- Si no terminó a tiempo → transcode en vivo (HLS). Nunca bloquea el play.
|
||||||
|
- Solo ficheros locales en disco (no debrid/torrent sin bajar).
|
||||||
|
|
||||||
|
**MVP recomendado (fase 4a):** toggle on/off + disparador on-download-complete +
|
||||||
|
modo Auto (remux-si-compatible / transcode-si-no) + concurrencia 1 +
|
||||||
|
pausar-si-stream-activo + reusar `hls_cache` + badge "listo". El resto (ladder
|
||||||
|
ABR, ventanas horarias, reglas por carpeta, TTL avanzado, formato mp4 vs HLS
|
||||||
|
configurable) en fases 4b/4c.
|
||||||
|
|
||||||
|
**Dependencias:** el camino mp4/remux depende del hueco #3 (3a ya hecho; 3b para
|
||||||
|
el remux-a-mp4 con manifiesto correcto). El camino HLS-cache es implementable ya
|
||||||
|
(reusa cache + prewarm). La gobernanza (pausar-si-stream) necesita señal de
|
||||||
|
"stream activo" en el daemon (la hay: `streamSrv.HasFile()` + registro HLS).
|
||||||
|
|
|
||||||
|
|
@ -410,6 +410,13 @@ type StreamSession struct {
|
||||||
// AudioIndex selects the source audio track (-map 0:a:N). -1 means
|
// AudioIndex selects the source audio track (-map 0:a:N). -1 means
|
||||||
// "use the default/first track".
|
// "use the default/first track".
|
||||||
AudioIndex int `json:"audioIndex,omitempty"`
|
AudioIndex int `json:"audioIndex,omitempty"`
|
||||||
|
// PlayMethod is how the daemon should serve this session:
|
||||||
|
// "" — default (HLS transcode); also what legacy servers send.
|
||||||
|
// "direct" — the source is already browser-native (the web decided this
|
||||||
|
// from library scan metadata + an agent-version gate). Serve
|
||||||
|
// the raw file over /stream (HTTP Range, no ffmpeg) instead of
|
||||||
|
// transcoding to HLS. See hueco #3 phase 3a in the roadmap.
|
||||||
|
PlayMethod string `json:"playMethod,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncResponse is returned by the server with all pending actions for the CLI.
|
// SyncResponse is returned by the server with all pending actions for the CLI.
|
||||||
|
|
|
||||||
|
|
@ -589,6 +589,30 @@ func runDaemonStart() error {
|
||||||
filePath = found
|
filePath = found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Direct-play (hueco #3 / 3a): the web decided this source is already
|
||||||
|
// browser-native (mp4 h264/aac 8-bit SDR) from library scan metadata,
|
||||||
|
// gated on agent version. Serve the raw file over /stream (HTTP Range,
|
||||||
|
// no ffmpeg) instead of transcoding to HLS — zero CPU, instant seek.
|
||||||
|
// Runs BEFORE the ffmpeg-availability check on purpose: direct-play
|
||||||
|
// needs no ffmpeg, so it must work even when transcode is disabled.
|
||||||
|
if sess.PlayMethod == "direct" {
|
||||||
|
streamSrv.SetFile(engine.NewDiskFileProvider(filePath), sess.TaskID)
|
||||||
|
// cancel just clears the served file so daemon shutdown / drain
|
||||||
|
// stops exposing it on /stream. There's no ffmpeg child to kill.
|
||||||
|
playerSessionRegistry.add(sess.SessionID, func() { streamSrv.ClearFile() })
|
||||||
|
log.Printf("[stream %s] direct-play: %s", agent.ShortID(sess.SessionID), filepath.Base(filePath))
|
||||||
|
// File is on disk → ready immediately. Tell the web so the player
|
||||||
|
// attaches <video src> without burning its HEAD-probe retry budget.
|
||||||
|
go func() {
|
||||||
|
rctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := agentClient.MarkSessionReady(rctx, sess.SessionID); err != nil {
|
||||||
|
log.Printf("[stream %s] mark-ready failed: %v", agent.ShortID(sess.SessionID), err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
tcRuntime := buildTranscodeRuntime(ctx, cfg)
|
tcRuntime := buildTranscodeRuntime(ctx, cfg)
|
||||||
if tcRuntime.FFmpegPath == "" || tcRuntime.FFprobePath == "" {
|
if tcRuntime.FFmpegPath == "" || tcRuntime.FFprobePath == "" {
|
||||||
log.Printf("[hls %s] rejected: ffmpeg/ffprobe unavailable", agent.ShortID(sess.SessionID))
|
log.Printf("[hls %s] rejected: ffmpeg/ffprobe unavailable", agent.ShortID(sess.SessionID))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
// Version is the CLI version. Overridden by goreleaser ldflags at release time.
|
// Version is the CLI version. Overridden by goreleaser ldflags at release time.
|
||||||
var Version = "0.9.19"
|
var Version = "0.10.0"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue