Cloudflare Stream a fondo: sube, codifica y reproduce vídeo sin montar nada
Qué es Cloudflare Stream y cómo funciona: subida directa desde el navegador, codificación automática, reproductor y HLS/DASH, vídeos privados con tokens firmados y directos por RTMPS o SRT. Precios por minutos y un Worker con el binding de Stream probado en local.
Contenido
Servir vídeo bien es más difícil de lo que parece. Un MP4 subido tal cual pesa demasiado para un móvil con mala cobertura y demasiado poco para una tele 4K; lo correcto es codificarlo en varias calidades, trocearlo en segmentos y servirlo con streaming adaptativo (HLS o DASH) para que el reproductor cambie de calidad según la conexión. Montarlo tú supone FFmpeg, colas de trabajo, almacenamiento, una CDN y un reproductor.
Cloudflare Stream lo hace todo con una API: subes el vídeo (o dejas que tus usuarios lo suban directamente), Stream lo codifica, lo guarda y lo sirve con su reproductor o con tus propias URL HLS/DASH. También sirve para directos. Y se paga solo por dos cosas: minutos guardados y minutos vistos.
En este artículo vemos cómo funciona, cómo se factura, cómo hacer privados los vídeos y un Worker que importa vídeos y genera enlaces firmados con el binding de Stream, probado en el simulador local de Wrangler.
Cómo funciona

- Subida. Hay cuatro vías: el panel, una URL de la que Stream descarga el vídeo, la API (formulario simple para ficheros de menos de 200 MB y protocolo tus, con reanudación, para los grandes) y la subida directa (Direct Creator Upload): tu servidor pide una URL de un solo uso y el navegador del usuario sube el fichero allí, sin pasar por tu servidor ni ver tu token de API. El máximo es 30 GB por fichero.
- Codificación. Stream codifica en H.264 con varias calidades, de 360p a 1080p, para streaming adaptativo. La ingesta y la codificación son gratis.
- Reproducción. Con el reproductor de Stream (un
<iframe>) o con tu propio reproductor usando los manifiestos HLS o DASH. Cada vídeo tiene una URL del tipohttps://customer-<CÓDIGO>.cloudflarestream.com/<ID>/iframe. - Directos. Creas una entrada en directo y emites con OBS o similar por RTMPS o SRT. Las emisiones pueden grabarse y quedan como vídeos normales. Hay también entrada y salida por WebRTC.
Precios
Stream factura solo dos cosas, igual para vídeo bajo demanda y para directos:
| Concepto | Precio | Cómo se cuenta |
|---|---|---|
| Minutos almacenados | 5 $ por cada 1000 minutos al mes (prepago) | Duración de los originales y las grabaciones, no su tamaño |
| Minutos entregados | 1 $ por cada 1000 minutos (por uso) | Segmentos de vídeo servidos a los espectadores |
| Ingesta y codificación | Gratis | — |
| Transferencia de datos | Incluida en los minutos entregados | Sin cargos de salida |
Algunos detalles de la documentación que conviene saber:
- El almacenamiento es prepago: compras bloques de 1000 minutos. Si te quedas sin espacio, no puedes subir más vídeos ni empezar directos hasta comprar más o borrar.
- No cuentan: las calidades que genera Stream, los vídeos con error, los borrados ni las descargas MP4 generadas.
- Una subida directa reserva su
maxDurationSecondshasta que se completa o caduca el enlace. - La entrega se cuenta por segmentos pedidos, de 4 segundos en los vídeos subidos. La precarga del reproductor cuenta; lo que el navegador reproduce de su caché, no.
- Ejemplo oficial: dos personas que ven 30 minutos cada una son 60 minutos entregados, 0,06 $. Un directo sin espectadores no cuesta entrega, pero su grabación sí ocupa almacenamiento.
Vídeos privados
Por defecto, cualquiera que tenga el id de un vídeo puede verlo. Para restringirlo, marca el vídeo con requireSignedURLs: true: dejan de funcionar todos los enlaces públicos y solo se puede reproducir con un token firmado en lugar del id. Hay tres formas de generar tokens:
- El endpoint
/tokende la API: devuelve un token que caduca en una hora. Recomendado solo para pruebas o menos de 1000 tokens al día, porque cada token es una llamada a la API. - Una clave de firma propia: generas los tokens tú, sin llamar a Cloudflare. Es lo recomendado con mucho tráfico.
- El binding de Stream en un Worker, con
video(id).generateToken(), sin gestionar claves.
También puedes limitar en qué dominios se puede incrustar un vídeo con allowedOrigins.
El código
El binding de Stream para Workers permite subir desde URL, crear subidas directas, listar y gestionar vídeos, subtítulos, descargas y marcas de agua sin exponer un token de API.
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "stream-lab",
"main": "src/index.ts",
"compatibility_date": "2026-10-01",
"stream": { "binding": "STREAM" },
// Subdominio de entrega de tu cuenta: customer-<CODE>.cloudflarestream.com
"vars": { "STREAM_HOST": "customer-abc123.cloudflarestream.com" }
}El Worker tiene tres rutas: crear una subida directa para un usuario, importar un vídeo que ya está en otra URL, y devolver una página con el reproductor de un vídeo privado y su token:
export default {
async fetch(request, env): Promise<Response> {
const url = new URL(request.url);
// 1. El usuario (ya autenticado en tu app) pide permiso para subir un vídeo
if (url.pathname === "/subida" && request.method === "POST") {
const subida = await env.STREAM.createDirectUpload({
maxDurationSeconds: 600, // reserva 10 min de almacenamiento hasta que se suba
expiry: new Date(Date.now() + 30 * 60_000).toISOString(), // enlace válido 30 min
requireSignedURLs: true, // el vídeo será privado
meta: { usuario: "luis" },
});
// El navegador sube el fichero (< 200 MB) directamente a uploadURL, sin pasar por el Worker
return Response.json({ id: subida.id, uploadURL: subida.uploadURL });
}
// 2. Importar un vídeo que ya está en otra URL (S3, tu CMS...)
if (url.pathname === "/importar" && request.method === "POST") {
const { origen } = await request.json<{ origen: string }>();
const video = await env.STREAM.upload(origen, { requireSignedURLs: true, meta: { origen } });
return Response.json({ id: video.id, estado: video.status.state, firmado: video.requireSignedURLs });
}
// 3. Reproducir un vídeo privado: token firmado (caduca en 1 hora)
const m = url.pathname.match(/^\/ver\/([\w-]{1,64})$/);
if (m) {
const token = await env.STREAM.video(m[1]).generateToken();
const iframe = `https://${env.STREAM_HOST}/${token}/iframe`;
return new Response(
`<!doctype html><meta charset="utf-8"><title>Vídeo</title>
<iframe src="${iframe}" style="border:none;width:100%;aspect-ratio:16/9"
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture" allowfullscreen></iframe>`,
{ headers: { "content-type": "text/html; charset=utf-8" } },
);
}
return new Response("No encontrado", { status: 404 });
},
} satisfies ExportedHandler<Env>;En el simulador local
Wrangler incluye un simulador de Stream. Para probarlo generé un vídeo de 3 segundos con FFmpeg y lo serví desde mi máquina en http://127.0.0.1:8899/prueba.mp4:
$ curl -s -X POST localhost:8811/importar -d '{"origen":"http://127.0.0.1:8899/prueba.mp4"}'
{"id":"19c93d21-20d3-4d39-aa61-26cb905b3183","estado":"ready","firmado":true}
$ curl -s localhost:8811/ver/19c93d21-20d3-4d39-aa61-26cb905b3183 | grep -o 'src="[^"]*"'
src="https://customer-abc123.cloudflarestream.com/eyJzdWIiOiIxOWM5M2QyMS0yMGQzLTRkMzktYWE2MS0yNmNiOTA1YjMxODMiLCJraWQiOiJsb2NhbC1tb2RlLWtleSIsImV4cCI6MTc5MTAwMzc3MH0=/iframe"
$ curl -s -X POST localhost:8811/subida
Error: createDirectUpload is not supported in local mode (HTTP 500)El simulador descarga el vídeo de la URL, lo marca como listo y le asigna un id. generateToken() devuelve un token, pero es un JSON en base64 con "kid":"local-mode-key" que solo sirve para que el código funcione: el real lo firma Cloudflare. Y la subida directa responde con un error claro: createDirectUpload is not supported in local mode. Para probar esa parte necesitas wrangler dev --remote o desplegar.
La subida directa por API, sin Worker, sería así (sin probar):
curl https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/stream/direct_upload \
-H "Authorization: Bearer $CF_API_TOKEN" \
--data '{"maxDurationSeconds": 600}'La respuesta trae un uploadURL de un solo uso al que el navegador envía el fichero (hasta 200 MB por este método; para más, tus).
Errores comunes (y cómo evitarlos)
- Dar por hecho que los vídeos son privados. No lo son. Usa
requireSignedURLsen todo lo que no sea público. - Generar un token por visita con
/token. Está limitado; con tráfico real, usa una clave de firma o el binding. - Subidas directas con
maxDurationSecondsenorme. Reservan almacenamiento hasta que se completan o caducan. Ajusta la duración y la caducidad. - Usar el formulario para ficheros grandes. Por encima de 200 MB usa tus, que además permite reanudar.
- Olvidar la precarga. El reproductor descarga segmentos por adelantado y cuentan como minutos entregados.
- Quedarse sin almacenamiento prepago. Las subidas fallan. Vigila el uso en el panel.
- Esperar 4K. Stream codifica hasta 1080p.
Cuándo usar Stream (y cuándo no)
Úsalo cuando tu aplicación necesita vídeo de verdad: cursos, contenido de usuarios, vídeo de producto, directos, todo lo que deba verse bien en cualquier conexión sin que montes una cadena de codificación. El precio por minutos es fácil de prever.
No lo necesitas para un vídeo corto de fondo en tu portada (un MP4 optimizado en R2 basta), ni si necesitas resoluciones por encima de 1080p o códecs específicos. Para transformar vídeos cortos al vuelo (recortes, miniaturas) existe además Media Transformations, que comparte facturación con las transformaciones de Images.
Sigue leyendo
- Cloudflare a fondo: la visión general de la plataforma.
- Cloudflare Images a fondo: lo mismo, pero para imágenes.
- Cloudflare R2 a fondo: para ficheros de vídeo que no necesitan streaming.
- Cloudflare Workers a fondo: el runtime del binding.