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.

Portada: Cloudflare Stream a fondo

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.

🧪
El Worker de este artículo se ha ejecutado el 3 de octubre de 2026 con Wrangler 4.147.0 y Node.js 24.21.0 contra el simulador local de Stream, que admite parte de la API: importar desde URL, listar y generar tokens (de mentira). La subida directa no está soportada en local. La codificación, la reproducción, los tokens reales y los directos no los he probado: necesitan una cuenta con Stream. Precios y límites comprobados en la documentación oficial ese mismo día.

Cómo funciona

Esquema de Cloudflare Stream: el vídeo entra por subida directa desde el navegador, por URL o por la API; Stream lo codifica en varias calidades H.264 y lo guarda; se reproduce con el reproductor de Stream o con manifiestos HLS y DASH; los directos entran por RTMPS o SRT
Subes un original; Stream genera las calidades, las guarda y las sirve con streaming adaptativo. Diagrama: devutilites.
  1. 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.
  2. 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.
  3. 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 tipo https://customer-<CÓDIGO>.cloudflarestream.com/<ID>/iframe.
  4. 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:

ConceptoPrecioCómo se cuenta
Minutos almacenados5 $ por cada 1000 minutos al mes (prepago)Duración de los originales y las grabaciones, no su tamaño
Minutos entregados1 $ por cada 1000 minutos (por uso)Segmentos de vídeo servidos a los espectadores
Ingesta y codificaciónGratis—
Transferencia de datosIncluida en los minutos entregadosSin 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 maxDurationSeconds hasta 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.
⚠️
La facturación de WebRTC empieza el 15 de octubre de 2026. La documentación de precios avisa de que Stream Live por WebRTC pasa a disponibilidad general y su entrega se facturará desde esa fecha.

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 /token de 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 requireSignedURLs en 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 maxDurationSeconds enorme. 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