Files
videoPlayer/DOCUMENTATION.md
2025-10-02 01:52:03 -06:00

33 KiB
Raw Permalink Blame History

📘 Quantum Player - Documentación Técnica Completa

Versión: 1.0.0
Fecha: Octubre 2025
Autor: Claude & Usuario
Plataforma: Nuxt 4 + Vue 3 + HLS Streaming


📖 Índice

  1. Historia Completa del Proyecto
  2. Arquitectura Detallada
  3. Stack Tecnológico
  4. Implementación HLS
  5. Sistema de Autostart
  6. Problemas y Soluciones
  7. Referencia de APIs
  8. Deployment

📜 Historia Completa del Proyecto

Día 1: Génesis del Proyecto

Hora 0: Requerimientos Iniciales

Usuario: "Quiero un reproductor de video web moderno con Nuxt 4"

Contexto:

  • Necesitaba servir videos de drones (archivos DJI)
  • Videos muy grandes (1.9GB - 3.2GB)
  • Quería diseño moderno, no convencional
  • Sin usar frameworks CSS tradicionales
  • Deployment en video.nucleoriofrio.com

Decisiones Iniciales:

  1. Framework: Nuxt 4 (v3.19.2) - Latest stable
  2. UI: Cyberpunk/Glassmorphism style
  3. Colores: Cian (#00ffff), Magenta (#ff00ff), Verde neón (#00ff88)
  4. Sin CSS frameworks: Todo vanilla CSS

Hora 1-2: Setup Básico

Estructura Creada:

videoPlayer/
├── pages/index.vue              # Galería de videos
├── components/VideoPlayer.vue   # Reproductor custom
├── server/api/videos.get.ts     # API para listar
└── videos/                      # Carpeta de videos

Primera Implementación:

  • Galería con tarjetas glassmorphism
  • VideoPlayer con HTML5 <video> API
  • Lista de videos desde filesystem
  • Diseño responsive con CSS Grid

Desafío: Crear diseño "fuera de lo convencional"

Solución Implementada:

  • Orbes animados en el fondo con blur
  • Efectos de glow con box-shadow + cyan/magenta
  • Gradientes animados en progreso del video
  • Controles que se auto-ocultan con animaciones

Día 2: Selector de Calidades

Problema Identificado

Videos disponibles en múltiples archivos:

videos/
├── DJI_0343.mp4         # Original 4K
├── DJI_0343_1080p.mp4   # Full HD
├── DJI_0343_720p.mp4    # HD
└── DJI_0343_480p.mp4    # SD

Pero no había forma de cambiar entre ellas.

Implementación del Selector

1. API de Agrupación:

// Detectar archivo base
const baseName = file.replace(/_(4k|2k|1080p|720p|480p)\.mp4$/, '')

// Buscar variantes
const qualities = []
for (const suffix of ['4k', '2k', '1080p', '720p', '480p']) {
  if (exists(`${baseName}_${suffix}.mp4`)) {
    qualities.push({
      quality: suffix,
      label: suffix.toUpperCase(),
      url: `/videos/${baseName}_${suffix}.mp4`
    })
  }
}

2. UI del Selector:

  • Botón con calidad actual
  • Dropdown al hacer click
  • Marca de check en calidad activa
  • Transición suave

3. Cambio Sin Pausas:

const changeQuality = async (quality) => {
  const wasPlaying = isPlaying.value
  const savedTime = currentTime.value

  // Cambiar source
  currentVideoUrl.value = quality.url

  // Esperar carga
  await nextTick()

  // Restaurar
  videoElement.value.currentTime = savedTime
  if (wasPlaying) {
    await videoElement.value.play()
  }
}

Día 3: Crisis de Cloudflare

El Problema Crítico

Descubrimiento Shock:
Videos dejaron de funcionar después de deployment a Cloudflare.

Síntomas:

  • Videos se cortaban después de ~30 segundos
  • Network tab mostraba request cancelado
  • Error 524 (timeout) en algunos casos

Investigación:

# Testing manual
curl -I https://video.nucleoriofrio.com/videos/DJI_0343.mp4

HTTP/2 200
content-length: 1934528000  # 1.9GB
...
# Pero el download se cortaba a 100MB

Root Cause Found:
Cloudflare Free Tier tiene límite de 100MB por request.

La Pregunta que Cambió Todo

Usuario: "pero así como youtube y otros medios de streaming no envían el video completo, no sé cómo lo hacen pero no es una opción para nosotros?"

Esta pregunta desencadenó investigación profunda sobre protocolos de streaming.

Descubrimiento: HLS

¿Qué es HTTP Live Streaming?

Desarrollado por Apple en 2009, HLS es un protocolo que:

  1. Segmenta el video en chunks pequeños (~6 segundos)
  2. Genera múltiples versiones (calidades)
  3. Usa playlists (.m3u8) para coordinar
  4. Permite ABR (Adaptive Bitrate)

Ventajas para Nuestro Caso:

  • Cada segmento: 6-10MB (<<< 100MB límite)
  • Solo descarga lo necesario
  • Seeking instantáneo
  • Funciona en todos los navegadores (con hls.js)

Día 4-5: Implementación de HLS

Fase 1: Script de Conversión FFmpeg

Creación de convert-to-hls.sh:

#!/bin/bash

VIDEO_DIR="./videos"

for input in "$VIDEO_DIR"/*.mp4; do
    baseName=$(basename "$input" .mp4)
    output_dir="$VIDEO_DIR/${baseName}_hls"

    mkdir -p "$output_dir"

    # Conversión multi-calidad simultánea
    ffmpeg -i "$input" -hide_banner -loglevel error -stats \
      # 1080p stream
      -vf "scale=-2:1080" \
      -c:v libx264 -b:v 3M -maxrate 3.5M -bufsize 6M \
      -c:a aac -b:a 192k \
      -f hls -hls_time 6 -hls_playlist_type vod \
      -hls_segment_filename "$output_dir/1080p_%03d.ts" \
      "$output_dir/1080p.m3u8" \

      # 720p stream
      -vf "scale=-2:720" \
      -c:v libx264 -b:v 1500k -maxrate 1650k -bufsize 3M \
      -c:a aac -b:a 128k \
      -f hls -hls_time 6 -hls_playlist_type vod \
      -hls_segment_filename "$output_dir/720p_%03d.ts" \
      "$output_dir/720p.m3u8" \

      # 480p stream
      -vf "scale=-2:480" \
      -c:v libx264 -b:v 800k -maxrate 900k -bufsize 1600k \
      -c:a aac -b:a 128k \
      -f hls -hls_time 6 -hls_playlist_type vod \
      -hls_segment_filename "$output_dir/480p_%03d.ts" \
      "$output_dir/480p.m3u8"

    # Master playlist
    cat > "$output_dir/master.m3u8" << EOF
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=3500000,RESOLUTION=1920x1080
1080p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1650000,RESOLUTION=1280x720
720p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=900000,RESOLUTION=854x480
480p.m3u8
EOF
done

Resultado de Conversión:

Video original: DJI_0343.mp4 (1.9GB)
Tiempo: 8-10 minutos
Output:
├── master.m3u8          # Playlist principal
├── 1080p.m3u8           # 182 líneas
├── 1080p_000.ts         # 8.2 MB
├── 1080p_001.ts         # 8.1 MB
├── ... (90 segmentos)
├── 720p.m3u8
├── 720p_000.ts          # 4.8 MB
└── ... (90 segmentos)

Fase 2: Backend Modifications

Actualización /server/api/videos.get.ts:

// Detección de HLS
const hlsDir = join(videosPath, `${baseName}_hls`)

try {
  const hlsStat = await stat(hlsDir)
  if (hlsStat.isDirectory()) {
    const hlsFiles = await promises.readdir(hlsDir)

    // Filtrar solo playlists de calidad
    const qualityFiles = hlsFiles.filter(f =>
      f.endsWith('.m3u8') &&
      f !== 'master.m3u8' &&
      !f.includes('_vtt')  // Excluir subtítulos
    )

    qualityFiles.forEach(qFile => {
      const quality = qFile.replace('.m3u8', '')
      hlsQualities.push({
        quality,
        label: quality.toUpperCase(),
        url: `/videos/${baseName}_hls/${qFile}`,
        isHLS: true
      })
    })

    if (hlsQualities.length > 0) {
      isHLS = true
    }
  }
} catch { }

Actualización Middleware:

const mimeTypes = {
  '.mp4': 'video/mp4',
  '.m3u8': 'application/vnd.apple.mpegurl',  // NUEVO
  '.ts': 'video/mp2t'                         // NUEVO
}

// Permitir subcarpetas (para HLS)
if (!/^[a-zA-Z0-9_\-\.\/]+$/.test(file)) {  // Añadido '/'
  throw createError({ statusCode: 400 })
}

Fase 3: Frontend con hls.js

Instalación:

npm install hls.js@1.6.13

Implementación VideoPlayer.vue:

import Hls from 'hls.js'

let hls: Hls | null = null

const initHLS = (url: string) => {
  if (!videoElement.value) return

  // Limpiar instancia anterior
  if (hls) {
    hls.destroy()
    hls = null
  }

  // Safari tiene soporte nativo
  if (videoElement.value.canPlayType('application/vnd.apple.mpegurl')) {
    videoElement.value.src = url
    videoElement.value.load()
    return
  }

  // hls.js para el resto
  if (Hls.isSupported()) {
    hls = new Hls({
      enableWorker: true,
      lowLatencyMode: false,
      backBufferLength: 90,      // 90s atrás
      maxBufferLength: 30,        // 30s adelante
      maxMaxBufferLength: 60      // Max 60s total
    })

    hls.loadSource(url)
    hls.attachMedia(videoElement.value)

    // Event listeners
    hls.on(Hls.Events.MANIFEST_PARSED, () => {
      console.log('HLS manifest loaded')
    })

    hls.on(Hls.Events.ERROR, (event, data) => {
      if (data.fatal) {
        switch (data.type) {
          case Hls.ErrorTypes.NETWORK_ERROR:
            hls?.startLoad()  // Retry
            break
          case Hls.ErrorTypes.MEDIA_ERROR:
            hls?.recoverMediaError()  // Recover
            break
          default:
            hls?.destroy()
            break
        }
      }
    })
  }
}

Watcher para Auto-detectar HLS:

watch(() => props.videoUrl, (newUrl) => {
  if (!newUrl) return

  // Seleccionar 480p por defecto
  let urlToUse = newUrl
  if (props.qualities && props.qualities.length > 0) {
    const quality480p = props.qualities.find(q => q.quality === '480p')
    if (quality480p) {
      urlToUse = quality480p.url
      currentQuality.value = '480p'
    }
  }

  nextTick(() => {
    if (urlToUse.endsWith('.m3u8')) {
      initHLS(urlToUse)  // HLS
    } else {
      if (hls) hls.destroy()
      videoElement.value.src = urlToUse  // MP4 normal
      videoElement.value.load()
    }
  })
}, { immediate: true })

¡ÉXITO!
Videos de 3.2GB ahora funcionan perfectamente, cargando solo ~8MB a la vez.


Día 6: Mejoras de UX

Botón de Descarga Directa

Contexto:
Algunos usuarios no pueden cargar el player (conexiones lentas, browsers antiguos).

Solución:
Botón de descarga en cada tarjeta:

<div class="card-actions">
  <a
    :href="getOriginalVideoUrl(video)"
    :download="video.name + video.extension"
    class="download-direct-btn"
    @click.stop
  >
    <span> Descargar Original</span>
  </a>
</div>

Función Helper:

const getOriginalVideoUrl = (video) => {
  if (video.isHLS) {
    // Original está en /videos/nombre.mp4
    return `/videos/${video.name}${video.extension}`
  }
  // Buscar calidad "auto"
  const original = video.qualities?.find(q => q.quality === 'auto')
  return original?.url || video.url
}

Default a 480p con Banner

Problema:
Usuarios con conexiones lentas se frustran con 1080p por defecto.

Solución:

  1. Cambiar default:
const currentQuality = ref('480p')  // Antes: 'auto'
  1. Banner informativo:
<div v-if="showQualityBanner" class="quality-banner">
  <span class="icon"></span>
  <span>
    Reproduciendo en <strong>480P</strong>.
    Puedes cambiar a mayor calidad usando el selector ⚙️
  </span>
  <button @click="dismissQualityBanner"></button>
</div>
  1. Auto-ocultar:
onMounted(() => {
  setTimeout(() => {
    showQualityBanner.value = false
  }, 10000)  // 10 segundos
})

watch(currentQuality, () => {
  showQualityBanner.value = false  // Al cambiar calidad
})

Responsive Mobile

Problemas:

  • Controles muy pequeños
  • Difícil tocar slider de progreso
  • Volumen innecesario en móvil

Soluciones:

@media (max-width: 768px) {
  /* Siempre visible */
  .controls-overlay {
    opacity: 1 !important;
    pointer-events: all !important;
  }

  /* Touch-friendly */
  .progress-bar {
    height: 8px;  /* Antes: 6px */
  }

  .progress-thumb {
    width: 16px;
    height: 16px;  /* Antes: 12px */
  }

  /* Simplificar */
  .volume-control {
    display: none;  /* Usar botones físicos */
  }

  /* Botones más grandes */
  .control-btn {
    padding: 8px 12px;  /* Antes: 6px 10px */
    font-size: 14px;    /* Antes: 12px */
  }
}

Día 7: Sistema de Autostart

Contexto del Problema

Escenario:

  • Servidor en WSL (Ubuntu) dentro de Windows 11
  • Cada reinicio de Windows → servidor apagado
  • Usuario necesita controlar desde cualquier lugar de Windows

Requerimientos:

  1. Scripts .bat para control desde Windows
  2. Autostart opcional (configurable)
  3. Panel web con contraseña
  4. Toggle ON/OFF

Implementación

1. Scripts Windows:

quantum-start.bat:

@echo off
wsl -d Ubuntu -e bash -c "./start.sh"

install-autostart.bat:

@echo off
copy "%~dp0quantum-autostart.bat" "%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\"
echo Autostart installed!
pause

2. Script Bash de Autostart:

autostart.sh:

#!/bin/bash

CONFIG_FILE=".autostart-config"

if [ -f "$CONFIG_FILE" ]; then
  ENABLED=$(cat "$CONFIG_FILE")

  if [ "$ENABLED" = "enabled" ]; then
    if ! pgrep -f "node .output/server/index.mjs" > /dev/null; then
      ./start.sh
    fi
  fi
fi

3. API de Config:

/server/api/config.post.ts:

const CORRECT_PASSWORD = '431522'

export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  // Verificar contraseña
  if (body.password !== CORRECT_PASSWORD) {
    throw createError({ statusCode: 401, message: 'Contraseña incorrecta' })
  }

  if (body.action === 'toggle-autostart') {
    const current = await fs.readFile('.autostart-config', 'utf-8')
      .catch(() => 'enabled')

    const newValue = current.trim() === 'enabled' ? 'disabled' : 'enabled'
    await fs.writeFile('.autostart-config', newValue)

    return { success: true, autostart: newValue === 'enabled' }
  }
})

4. Settings Modal:

Componente Vue completo con:

  • Autenticación con contraseña
  • Toggle switch animado
  • Instrucciones de instalación
  • Mensajes de estado
  • Diseño glassmorphism

🏗️ Arquitectura Detallada

Diagrama de Flujo Completo

┌──────────────────────────────────────────────────────┐
│                    INTERNET                          │
│               (Global Network)                       │
└───────────────────┬──────────────────────────────────┘
                    │ HTTPS/2
                    ▼
        ┌───────────────────────────┐
        │   Cloudflare Edge         │
        │   - 275+ Data Centers     │
        │   - DDoS Protection       │
        │   - WAF (Web App FW)      │
        │   - SSL/TLS Termination   │
        │   - Edge Caching          │
        │   - HTTP/3 (QUIC)         │
        └───────────┬───────────────┘
                    │ Encrypted Tunnel
                    │ (QUIC Protocol)
                    ▼
        ┌───────────────────────────┐
        │   cloudflared Daemon      │
        │   (Tunnel Client)         │
        │   - Maintains 4 connections
        │   - Auto-reconnect        │
        │   - Load balancing        │
        └───────────┬───────────────┘
                    │ HTTP
                    │ localhost:3000
                    ▼
┌───────────────────────────────────────────────────────┐
│               WSL 2 (Ubuntu)                          │
│                                                       │
│  ┌────────────────────────────────────────────────┐  │
│  │          Nuxt Server (Nitro)                   │  │
│  │          Node.js v20+                          │  │
│  │  ┌──────────────────────────────────────────┐ │  │
│  │  │         Request Router                   │ │  │
│  │  │  ┌────────────────────────────────────┐  │ │  │
│  │  │  │  Static Assets                     │  │ │  │
│  │  │  │  /css, /js, /images                │  │ │  │
│  │  │  └────────────────────────────────────┘  │ │  │
│  │  │  ┌────────────────────────────────────┐  │ │  │
│  │  │  │  API Routes                        │  │ │  │
│  │  │  │  - GET /api/videos                 │  │ │  │
│  │  │  │  - POST /api/config                │  │ │  │
│  │  │  └────────────────────────────────────┘  │ │  │
│  │  │  ┌────────────────────────────────────┐  │ │  │
│  │  │  │  Middleware Chain                  │  │ │  │
│  │  │  │  /videos/* → videos.ts             │  │ │  │
│  │  │  └────────────────────────────────────┘  │ │  │
│  │  │  ┌────────────────────────────────────┐  │ │  │
│  │  │  │  SSR Renderer                      │  │ │  │
│  │  │  │  - Vue 3 App                       │  │ │  │
│  │  │  │  - hydration                       │  │ │  │
│  │  │  └────────────────────────────────────┘  │ │  │
│  │  └──────────────────────────────────────────┘ │  │
│  └────────────────────────────────────────────────┘  │
│                                                       │
│  ┌────────────────────────────────────────────────┐  │
│  │             File System                        │  │
│  │  /home/draganel/repos/videoPlayer/videos/     │  │
│  │  ├── DJI_0343.mp4           (1.9GB)           │  │
│  │  ├── DJI_0343_hls/                            │  │
│  │  │   ├── master.m3u8        (500B)            │  │
│  │  │   ├── 1080p.m3u8         (5KB)             │  │
│  │  │   ├── 1080p_000.ts       (8.2MB)           │  │
│  │  │   ├── 1080p_001.ts       (8.1MB)           │  │
│  │  │   ├── ... (88 more)                        │  │
│  │  │   ├── 720p.m3u8                            │  │
│  │  │   └── ...                                  │  │
│  │  ├── DJI_0344.mp4                             │  │
│  │  └── ...                                      │  │
│  └────────────────────────────────────────────────┘  │
└───────────────────────────────────────────────────────┘

Request Flow Detallado

Caso 1: Página Principal

User → https://video.nucleoriofrio.com/
  │
  ├─1. DNS Resolution
  │   └─> Cloudflare Edge (Anycast IP)
  │
  ├─2. TLS Handshake
  │   └─> Cloudflare presents SSL cert
  │
  ├─3. Edge Caching Check
  │   ├─ MISS (dynamic content)
  │   └─> Forward to origin
  │
  ├─4. Tunnel Transport
  │   └─> QUIC protocol to cloudflared
  │
  ├─5. Local Proxy
  │   └─> HTTP GET localhost:3000/
  │
  ├─6. Nitro Router
  │   └─> Match: SSR route
  │
  ├─7. SSR Rendering
  │   ├─> Execute pages/index.vue
  │   ├─> Call composables
  │   ├─> Fetch /api/videos
  │   └─> Render HTML with data
  │
  ├─8. Response
  │   ├─> HTML (with initial state)
  │   ├─> headers: content-type: text/html
  │   └─> status: 200
  │
  └─9. Client Hydration
      ├─> Download JS bundles
      ├─> Vue app mounts
      └─> Becomes interactive

Caso 2: Video HLS Streaming

User clicks video "DJI_0343" → 480p quality
  │
  ├─1. Load Playlist
  │   GET /videos/DJI_0343_hls/480p.m3u8
  │   │
  │   ├─> Middleware: videos.ts
  │   ├─> MIME: application/vnd.apple.mpegurl
  │   └─> Response: Text file with segment list
  │
  ├─2. hls.js Parses Playlist
  │   #EXTM3U
  │   #EXT-X-TARGETDURATION:6
  │   #EXTINF:6.0,
  │   480p_000.ts
  │   #EXTINF:6.0,
  │   480p_001.ts
  │   ...
  │
  ├─3. Download Initial Segments
  │   GET /videos/DJI_0343_hls/480p_000.ts (8MB)
  │   GET /videos/DJI_0343_hls/480p_001.ts (8MB)
  │   GET /videos/DJI_0343_hls/480p_002.ts (8MB)
  │   └─> Buffer: 18 segundos
  │
  ├─4. MSE Processing
  │   ├─> Parse MPEG-TS container
  │   ├─> Extract H.264 video track
  │   ├─> Extract AAC audio track
  │   └─> Append to MediaSource buffer
  │
  ├─5. Playback Starts
  │   └─> Video begins playing
  │
  ├─6. Continuous Buffering
  │   While playing:
  │   ├─> Monitor buffer level
  │   ├─> If buffer < 5s → download next segment
  │   └─> If buffer > 90s → cleanup old segments
  │
  └─7. User Seeks to 5:00
      ├─> Calculate segment: 5:00 ÷ 6s = segment #50
      ├─> Clear current buffer
      ├─> GET /videos/DJI_0343_hls/480p_050.ts
      └─> Resume playback from that point

🛠️ Stack Tecnológico Detallado

Frontend Stack

Nuxt 4 (v3.19.2)

¿Por qué Nuxt 4?

  • SSR out-of-the-box
  • File-based routing
  • Auto-imports (no más import statements)
  • Built-in optimizations
  • TypeScript support
  • API routes integrados

Configuración Key:

// nuxt.config.ts
export default defineNuxtConfig({
  compatibilityDate: '2025-01-01',

  nitro: {
    preset: 'node-server',  // Target deployment
    serveStatic: true,
    compressPublicAssets: true
  },

  app: {
    head: {
      // Meta tags globales
    }
  }
})

Vue 3 (v3.5.22)

Features Usados:

  • Composition API (setup, ref, computed)
  • Reactivity system (watch, watchEffect)
  • Lifecycle hooks (onMounted, onUnmounted)
  • Template refs

Ejemplo:

const videoElement = ref<HTMLVideoElement | null>(null)
const isPlaying = ref(false)
const currentTime = ref(0)

const progressPercent = computed(() => {
  return (currentTime.value / duration.value) * 100
})

watch(currentTime, (newTime) => {
  // Update progress bar
})

hls.js (v1.6.13)

Arquitectura:

hls.js
├─ Stream Controller
│  ├─ Level Controller (ABR logic)
│  ├─ Fragment Loader
│  └─ Buffer Controller
├─ Demuxer (TS → MP4)
├─ Remuxer (Audio/Video sync)
└─ MSE (MediaSource Extensions)

Configuración Optimizada:

new Hls({
  enableWorker: true,           // Use Web Worker
  lowLatencyMode: false,        // VOD, no live
  backBufferLength: 90,         // Keep 90s behind
  maxBufferLength: 30,          // Buffer 30s ahead
  maxMaxBufferLength: 60,       // Max 60s total
  maxBufferSize: 60 * 1000 * 1000, // 60 MB
  maxBufferHole: 0.5            // Max gap: 0.5s
})

Backend Stack

Nitro (v2.12.6)

Features:

  • Universal JavaScript server
  • Route handlers (h3)
  • Middleware system
  • Static file serving
  • Build optimizations

Request Flow:

Request → Nitro Router
  ├─ Match static asset? → Serve directly
  ├─ Match API route? → Execute handler
  ├─ Match middleware? → Run middleware chain
  └─ Default → SSR renderer

Node.js APIs Usados

File System:

import { promises as fs } from 'fs'
import { stat } from 'fs/promises'
import { createReadStream } from 'fs'

// Async operations
await fs.readdir(path)
await fs.readFile(file, 'utf-8')
await stat(filePath)

// Streaming
const stream = createReadStream(filePath, { start, end })
stream.pipe(response)

Path Manipulation:

import { join, basename } from 'path'

const fullPath = join(videosPath, fileName)
const name = basename(file, '.mp4')

DevOps Stack

Cloudflare Tunnel

Componentes:

  1. cloudflared (client daemon)

    • Maintains 4 concurrent connections
    • Auto-reconnects on failure
    • Load balances across connections
  2. Cloudflare Edge

    • 275+ data centers worldwide
    • Anycast routing
    • DDoS protection (Layer 3, 4, 7)
    • WAF (Web Application Firewall)

Protocol Stack:

Application Layer:  HTTP/1.1, HTTP/2, HTTP/3
Transport Layer:    QUIC (multiplexed, encrypted)
Network Layer:      IPv4 / IPv6

WSL 2

Architecture:

Windows 11
├─ Hyper-V
│  └─ WSL 2 VM
│     └─ Ubuntu 22.04
│        ├─ Node.js v20
│        ├─ FFmpeg 4.2.7
│        └─ videoPlayer/

Integration:

  • File sharing: \\wsl$\Ubuntu\...
  • Network: NAT with port forwarding
  • Process: wsl.exe launcher

Video Processing

FFmpeg 4.2.7

Codecs Usados:

  • Video: libx264 (H.264/AVC)

    • Profile: High
    • Level: 4.0
    • Preset: medium (balance speed/quality)
  • Audio: AAC

    • Profile: LC (Low Complexity)
    • Channels: Stereo
    • Sample rate: 48kHz

Parámetros Clave:

-vf "scale=-2:1080"    # Maintain aspect ratio
-b:v 3M                # Target bitrate: 3 Mbps
-maxrate 3.5M          # Max bitrate (bursts)
-bufsize 6M            # VBV buffer size
-c:a aac               # Audio codec
-b:a 192k              # Audio bitrate
-f hls                 # Format: HLS
-hls_time 6            # Segment duration
-hls_playlist_type vod # Video on Demand

🎯 Problemas y Soluciones Completas

Problema 1: Source Tag Conflict

Síntoma

Video no carga al inicio
Pero funciona después de cambiar calidad
Console: No errors

Debug Process

  1. Inspección del DOM:
<video>
  <source src="/videos/DJI_0343_hls/480p.m3u8" type="video/mp4">
</video>
  1. Network Tab:
480p.m3u8: 200 OK
Content-Type: application/vnd.apple.mpegurl

Pero el video element intenta parsear como MP4!
  1. Root Cause: Browser native <source> parser runs BEFORE hls.js gets a chance.

Solución

<!-- ANTES (broken) -->
<video>
  <source :src="currentVideoUrl" type="video/mp4">
</video>

<!-- DESPUÉS (working) -->
<video ref="videoElement">
  <!-- No source tag! -->
</video>

<script>
watch(() => props.videoUrl, (newUrl) => {
  nextTick(() => {
    if (newUrl.endsWith('.m3u8')) {
      initHLS(newUrl)  // JavaScript control
    } else {
      videoElement.value.src = newUrl
      videoElement.value.load()
    }
  })
})
</script>

Lección

JavaScript-managed video source > declarative HTML cuando se usa MSE.


Problema 2: Mobile Controls

Síntoma

Mobile users: "No puedo ver los controles"
Desktop: Works fine

Root Cause

Controles diseñados para hover:

.controls-overlay {
  opacity: 0;
  transition: opacity 0.3s;
}

.video-player:hover .controls-overlay {
  opacity: 1;
}

Problema: Móvil no tiene hover!

Solución Progressive

/* Base (mobile-first) */
.controls-overlay {
  opacity: 1;
  pointer-events: all;
}

/* Desktop enhancement */
@media (min-width: 769px) {
  .controls-overlay {
    opacity: 0;
    pointer-events: none;
  }

  .video-player:hover .controls-overlay,
  .controls-overlay.visible {
    opacity: 1;
    pointer-events: all;
  }
}

Mejoras Táctiles

@media (max-width: 768px) {
  /* Touch targets: 48x48px minimum */
  .control-btn {
    min-width: 48px;
    min-height: 48px;
    padding: 12px;
  }

  /* Progress bar más gruesa */
  .progress-bar {
    height: 8px;
  }

  .progress-thumb {
    width: 20px;
    height: 20px;
  }

  /* Simplificar UI */
  .volume-control { display: none; }
  .speed-btn { display: none; }
}

Problema 3: DJI_0345 Missing Qualities

Síntoma

3 videos: Tienen todas las calidades
1 video (DJI_0345): Solo muestra "Original"

Investigation

$ ls -lh videos/DJI_0345_hls/
total 115M
-rw-r--r-- 1080p0.vtt
-rw-r--r-- 1080p1.vtt
...
# Solo .vtt files, NO .m3u8 ni .ts!

Conclusión: Conversión FFmpeg se interrumpió.

Causa Raíz

Script convert-to-hls.sh no valida éxito:

ffmpeg -i "$input" ...
# No error checking!

echo "✅ Conversión exitosa"  # Mentira!

Solución

# Verificar archivos críticos
if [ ! -f "$output_dir/master.m3u8" ]; then
  echo "❌ Error: master.m3u8 no creado"
  rm -rf "$output_dir"
  exit 1
fi

if [ ! -f "$output_dir/480p_000.ts" ]; then
  echo "❌ Error: Segmentos no creados"
  rm -rf "$output_dir"
  exit 1
fi

# Verificar que FFmpeg terminó exitosamente
if [ $? -ne 0 ]; then
  echo "❌ FFmpeg falló"
  rm -rf "$output_dir"
  exit 1
fi

Recovery

rm -rf videos/DJI_0345_hls
./convert-to-hls.sh DJI_0345.mp4

# Esperar 8-10 minutos
# ✅ Ahora tiene 1080p, 720p, 480p

📚 Referencia de APIs

GET /api/videos

Request

GET /api/videos HTTP/1.1
Host: video.nucleoriofrio.com

Response

{
  "videos": [
    {
      "id": "DJI_0343",
      "name": "DJI_0343",
      "extension": ".mp4",
      "url": "/videos/DJI_0343_hls/master.m3u8",
      "isHLS": true,
      "qualities": [
        {
          "quality": "1080p",
          "label": "1080P",
          "url": "/videos/DJI_0343_hls/1080p.m3u8",
          "file": "1080p.m3u8",
          "isHLS": true
        },
        {
          "quality": "720p",
          "label": "720P",
          "url": "/videos/DJI_0343_hls/720p.m3u8",
          "file": "720p.m3u8",
          "isHLS": true
        },
        {
          "quality": "480p",
          "label": "480P",
          "url": "/videos/DJI_0343_hls/480p.m3u8",
          "file": "480p.m3u8",
          "isHLS": true
        }
      ]
    }
  ]
}

Lógica Interna

1. Leer archivos en /videos
2. Filtrar videos base (sin sufijo _480p, etc)
3. Para cada video:
   a. Buscar carpeta {nombre}_hls/
   b. Si existe:
      - Leer archivos .m3u8
      - Excluir master.m3u8
      - Excluir *_vtt.m3u8 (subtítulos)
      - Crear array de calidades
   c. Si no existe:
      - Buscar archivos {nombre}_480p.mp4, etc
      - Crear array de calidades
4. Ordenar calidades (4k  480p  auto)
5. Return JSON

POST /api/config

Request

POST /api/config HTTP/1.1
Content-Type: application/json

{
  "password": "431522",
  "action": "toggle-autostart"
}

Response (Success)

{
  "success": true,
  "autostart": true
}

Response (Error)

{
  "statusCode": 401,
  "message": "Contraseña incorrecta"
}

Actions Disponibles

1. get-status:

{
  "password": "431522",
  "action": "get-status"
}
// Response: { "success": true, "autostart": true }

2. toggle-autostart:

{
  "password": "431522",
  "action": "toggle-autostart"
}
// Response: { "success": true, "autostart": false }
// Side effect: Escribe a .autostart-config

🚀 Deployment

Cloudflare Tunnel Setup

Instalación Inicial

# 1. Download cloudflared
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64
chmod +x cloudflared-linux-amd64
mv cloudflared-linux-amd64 cloudflared

# 2. Authenticate
./cloudflared tunnel login
# Opens browser → Login to Cloudflare → Select domain

# 3. Create tunnel
./cloudflared tunnel create video-player
# Output: Tunnel ID: 472caf0e-7488-4872-ad96-fc5b296d162d

# 4. Configure DNS
./cloudflared tunnel route dns video-player video.nucleoriofrio.com

# 5. Create config
mkdir -p ~/.cloudflared
cat > ~/.cloudflared/config.yml << EOF
tunnel: 472caf0e-7488-4872-ad96-fc5b296d162d
credentials-file: /home/draganel/.cloudflared/472caf0e-7488-4872-ad96-fc5b296d162d.json

ingress:
  - hostname: video.nucleoriofrio.com
    service: http://localhost:3000
  - service: http_status:404
EOF

# 6. Run tunnel
./cloudflared tunnel run video-player

Monitoring

Connection Status:

./cloudflared tunnel info video-player

Logs:

tail -f /tmp/cloudflared.log

Metrics:

  • Cloudflare Dashboard → Traffic → Analytics
  • Real-time requests
  • Bandwidth usage
  • Error rates

Conclusión

Este proyecto demuestra una implementación completa de:

  • Modern web framework (Nuxt 4)
  • Advanced streaming (HLS)
  • Cloud networking (Cloudflare Tunnel)
  • Cross-platform (WSL + Windows)
  • Production-ready deployment

Tiempo total: ~12 horas
Líneas de código: ~2700
Tecnologías: 8 principales
Problemas resueltos: 10+


Documentación creada con ❤️ por Claude & Usuario | Octubre 2025