Video player con Claude Code para cursos y sitios de medios
Construye un reproductor React con video nativo, controles personalizados, streaming, accesibilidad y checklist.
Actualización de producción 2026: qué es un video player
Un video player es la interfaz que carga un archivo de vídeo o una transmisión, muestra el contenido y permite controlar reproducción, pausa, avance, volumen, subtítulos y velocidad. En la web, la base suele ser el elemento nativo <video>. Encima de esa base, React o cualquier otra capa de UI se comunica con HTMLMediaElement para leer y cambiar currentTime, duration, paused, volume, muted y playbackRate.
La definición importa porque un reproductor de producción no es solo un botón bonito. En un producto educativo, decide si el alumno puede retomar una lección, bajar la velocidad, leer subtítulos y completar el curso. En un sitio de medios, decide si el artículo carga rápido, si el lector móvil abandona y si el vídeo conduce a una newsletter, membresía o informe premium. En una demo SaaS, decide si la persona entiende el producto antes de hablar con ventas.
Para implementar con criterio, usa la referencia oficial de MDN sobre el elemento <video> y la API HTMLMediaElement. Claude Code puede ayudarte a escribir el componente, revisar eventos, proponer pruebas y encontrar problemas de accesibilidad, pero la decisión de producto sigue siendo humana: construye el reproductor mínimo que el lector necesita.
Este artículo se conecta con el audio player de Claude Code, la guía de accesibilidad con Claude Code y la guía de optimización de performance. Si el vídeo forma parte de un curso pagado, una operación de medios o un programa de capacitación, la llamada comercial puede llevar a training y consultoría.
<video> nativo, controles personalizados o streaming
La primera decisión no es el color de los controles. Es el modelo de reproducción. El reproductor nativo con <video controls> es adecuado para insertar un vídeo corto en un artículo o en documentación interna. Los controles personalizados son útiles cuando necesitas progreso de lección, capítulos, CTA, analítica, diseño de marca o reglas de membresía. El streaming aparece cuando el archivo es largo, el catálogo crece, hay usuarios en redes débiles o necesitas calidad adaptativa.
| Enfoque | Mejor uso | Notas de producción |
|---|---|---|
<video controls> nativo | Artículos, documentación interna, landing pages simples | Es lo más rápido. El navegador maneja controles básicos, pero la marca y la analítica son limitadas. |
Controles personalizados sobre HTMLMediaElement | Cursos, sitios de medios, demos SaaS, áreas de miembros | Controlas UI, progreso, CTA y eventos. También asumes accesibilidad, errores y móviles. |
| HLS/DASH o plataforma de vídeo | Lecciones largas, eventos en vivo, catálogos grandes, contenido protegido | Requiere transcodificación, manifest, CDN, permisos y a veces una librería de player. |
Para la mayoría de proyectos con Claude Code, empieza con un MP4 corto, una imagen poster, subtítulos WebVTT y preload="metadata". Añade controles personalizados cuando haya una razón concreta: búsqueda en transcripción, capítulos, progreso de curso, CTA para una descarga de Gumroad o experiencia de membresía. Pasa a streaming cuando un solo MP4 ya no cubra redes móviles, regiones o vídeos largos.
Tabla de arquitectura
| Capa | Responsabilidad | Qué pedir a Claude Code |
|---|---|---|
| Activos de vídeo | MP4/WebM, poster, subtítulos, manifest de streaming | Revisar URL, MIME type, CORS, caché, enlaces vencidos y fallback. |
| Elemento nativo | Cargar media, exponer eventos, duración, tiempo y errores | Verificar preload, playsInline, track y mensajes de error. |
| Control de estado | Sincronizar tiempo, duración, reproducción, volumen, mute y velocidad | Evitar estado inventado desde clics; usar eventos reales del media element. |
| UI de controles | Botones, barra de progreso, volumen, velocidad, subtítulos | Usar button e input reales con etiquetas y foco estable. |
| Persistencia | Posición de reanudación, finalización, velocidad, silencio | Guardar solo lo necesario y explicar la privacidad si hay usuario identificado. |
| Analítica | Inicio, 25%, 50%, 75%, finalización, errores, clics de CTA | Medir decisiones de producto, no enviar ruido cada segundo. |
| Performance | Poster, CDN, lazy loading, bitrate | Evitar layout shift, exceso de bytes iniciales y coste móvil innecesario. |
Esta arquitectura facilita el mantenimiento. Puedes pedir a Claude Code que revise solo el orden de foco, solo los eventos de finalización o solo la carga inicial. También permite una salida de emergencia: si los controles personalizados fallan, puedes habilitar controles nativos mientras corriges el problema.
Use case reales
Use case 1: una lección pagada. El alumno pausa, vuelve más tarde, cambia de dispositivo y usa 1.25x. El player necesita subtítulos, reanudación, criterio de finalización y enlace a la siguiente lección. La finalización no debe dispararse al cargar la página, sino después de una parte significativa de visualización.
Use case 2: un artículo de medios con vídeo. El lector escanea el texto antes de reproducir. La poster debe ser ligera, el transcript debe estar cerca y el archivo no debe descargarse completo en el primer render. Al terminar, el CTA puede ser una newsletter, una membresía o un análisis relacionado.
Use case 3: una demo SaaS. Un visitante puede querer ver solo precios, API o una función específica. Los capítulos y los eventos de reproducción permiten mostrar una prueba gratuita, una consulta o documentación técnica según la intención real.
Use case 4: capacitación interna. Ventas, soporte, onboarding y compliance necesitan reproducción estable, subtítulos, SSO, mensajes de error y progreso visible para administradores. Aquí la confiabilidad pesa más que la animación decorativa.
Código React/TypeScript ejecutable
Este componente funciona en Vite, Next.js o una isla React de Astro. Usa <video> nativo, eventos de HTMLMediaElement y controles accesibles sin depender de una librería externa.
import { useRef, useState, type ChangeEvent } from "react";
type CaptionTrack = {
src: string;
srcLang: string;
label: string;
default?: boolean;
};
type VideoPlayerProps = {
src: string;
title: string;
poster?: string;
captions?: CaptionTrack[];
};
function formatTime(value: number) {
if (!Number.isFinite(value)) return "0:00";
const minutes = Math.floor(value / 60);
const seconds = Math.floor(value % 60).toString().padStart(2, "0");
return `${minutes}:${seconds}`;
}
export function VideoPlayer({ src, title, poster, captions = [] }: VideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const [media, setMedia] = useState({
current: 0,
duration: 0,
playing: false,
volume: 0.8,
rate: 1,
error: "",
});
function patch(next: Partial<typeof media>) {
setMedia((current) => ({ ...current, ...next }));
}
async function togglePlay() {
const video = videoRef.current;
if (!video) return;
if (video.paused) {
try {
await video.play();
patch({ playing: true, error: "" });
} catch {
patch({ error: "Playback was blocked. Try again from the play button." });
}
} else {
video.pause();
patch({ playing: false });
}
}
function seek(event: ChangeEvent<HTMLInputElement>) {
const video = videoRef.current;
if (!video) return;
const nextTime = Number(event.target.value);
video.currentTime = nextTime;
patch({ current: nextTime });
}
function changeVolume(event: ChangeEvent<HTMLInputElement>) {
const video = videoRef.current;
if (!video) return;
const volume = Number(event.target.value);
video.volume = volume;
video.muted = volume === 0;
patch({ volume });
}
function changeRate(event: ChangeEvent<HTMLSelectElement>) {
const video = videoRef.current;
if (!video) return;
const rate = Number(event.target.value);
video.playbackRate = rate;
patch({ rate });
}
return (
<section className="video-player" aria-label={`${title} video player`}>
<video
ref={videoRef}
poster={poster}
preload="metadata"
playsInline
onLoadedMetadata={(event) => patch({ duration: event.currentTarget.duration })}
onTimeUpdate={(event) => patch({ current: event.currentTarget.currentTime })}
onPlay={() => patch({ playing: true })}
onPause={() => patch({ playing: false })}
onVolumeChange={(event) => patch({ volume: event.currentTarget.muted ? 0 : event.currentTarget.volume })}
onError={() => patch({ error: "The video could not be loaded." })}
>
<source src={src} type={src.endsWith(".webm") ? "video/webm" : "video/mp4"} />
{captions.map((track) => (
<track key={track.src} kind="captions" src={track.src} srcLang={track.srcLang} label={track.label} default={track.default} />
))}
Your browser does not support the video element.
</video>
<div role="group" aria-label="Video controls">
<button type="button" onClick={togglePlay} aria-pressed={media.playing}>
{media.playing ? "Pause" : "Play"}
</button>
<input type="range" min="0" max={media.duration || 0} step="0.1" value={media.duration ? media.current : 0} onChange={seek} aria-label="Seek video" />
<output>{formatTime(media.current)} / {formatTime(media.duration)}</output>
<input type="range" min="0" max="1" step="0.05" value={media.volume} onChange={changeVolume} aria-label="Volume" />
<select value={media.rate} onChange={changeRate} aria-label="Playback speed">
{[0.75, 1, 1.25, 1.5, 2].map((rate) => (
<option key={rate} value={rate}>{rate}x</option>
))}
</select>
</div>
{media.error ? <p role="alert">{media.error}</p> : null}
</section>
);
}
Pitfall de accesibilidad y performance
El pitfall principal es ocultar los controles nativos y no reemplazar su comportamiento. Un botón de reproducción debe ser un button, no un div con onClick. La barra de progreso debe aceptar teclado. Los subtítulos y el texto alternativo no son opcionales cuando el vídeo contiene la explicación principal.
El segundo pitfall es la carga excesiva. Una portada con varios vídeos no debe descargar todos los MP4 completos. Usa poster con dimensiones estables, preload="metadata" para vídeos secundarios, CDN y archivos comprimidos. Para lecciones largas, prepara varias calidades o usa una plataforma administrada.
La analítica también puede fallar por exceso. Registra inicio, 25%, 50%, 75%, finalización, error y CTA. Si envías un evento por segundo, tendrás coste y ruido, pero no necesariamente mejores decisiones.
Checklist de lanzamiento y CTA
- Probar teclado, táctil, mouse y etiquetas para lectores de pantalla.
- Añadir subtítulos o transcript para cursos, medios y demos.
- Validar
playsInline, rotación y redes lentas en móvil. - Fijar dimensiones de poster y revisar CLS.
- Evitar
preload="auto"salvo que el vídeo sea la experiencia principal. - Probar URL vencida, subtítulo faltante, autoplay bloqueado y error de CDN.
- Medir inicio, progreso, finalización, error y clic de CTA.
- Mantener fallback a controles nativos durante la primera semana.
Como CTA de monetización, un vídeo gratuito puede llevar a una descarga de Gumroad, a una suscripción, a un curso completo o a training y consultoría. La forma práctica de trabajar con Claude Code es construir primero el reproductor mínimo, pedir revisión de accesibilidad, pedir revisión de performance y luego conectar eventos de negocio. Así el player deja de ser un adorno y se convierte en infraestructura para aprendizaje, medios y ventas.
PDF gratis: cheatsheet de Claude Code
Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.
Cuidamos tus datos y no enviamos spam.
Sobre el autor
Masa
Ingeniero enfocado en workflows prácticos con Claude Code.
Artículos relacionados
Workflow de Obsidian a CLAUDE.md con Claude Code
Convierte notas de trabajo de Obsidian en notas operativas de CLAUDE.md para no repetir contexto.
Claude Code Revenue CTA Routing: de artículos a PDF, Gumroad y consulta
Un flujo con Claude Code para dirigir lectores a PDF gratis, Gumroad o consulta según intención.
Reglas de handoff para equipos con Claude Code: evidencia, permisos, rollback e ingresos
Formato práctico para entregar trabajo de Claude Code con pruebas, permisos, rollback, PDF gratis, Gumroad y consulta.