Objetivos de aprendizaje
Después de leer este artículo podrás:
- Entender cómo Range, 206 y Content-Range permiten adelantar el video
- Probar con curl si tu storage o servidor acepta range requests
- Corregir los errores comunes: moov al final del MP4, 200 en lugar de 206 y caché
En esta página
- ¿Qué es una range request?
- ¿Por qué el video y el audio dependen de esto?
- ¿Cómo pruebo si el servidor acepta rangos?
- ¿Cómo sirvo video desde un bucket privado?
- ¿Cómo sirvo rangos desde mi propio servidor?
- ¿Qué errores debo evitar?
- ¿Cuándo usar HLS o DASH en lugar de un MP4?
- Cómo hacerlo en Square Cloud
- Preguntas frecuentes
Subes un MP4 de 800 MB al storage, pones la URL en una etiqueta <video> y le das play. Funciona. Entonces alguien arrastra la barra al minuto 40 y el video se congela, vuelve a empezar desde cero o descarga todo antes de reproducir. Casi nunca el problema está en el archivo: es que el servidor no está respondiendo a range requests.
Los detalles del protocolo que siguen se basan en la RFC 9110 y en la documentación de MDN.
¿Qué es una range request?#
Es una petición HTTP que pide solo un trozo del archivo, medido en bytes. El cliente envía el encabezado Range y el servidor devuelve solo ese fragmento:
GET /videos/clase.mp4 HTTP/1.1
Host: ejemplo.com
Range: bytes=1048576-2097151HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Type: video/mp4
Content-Length: 1048576
Content-Range: bytes 1048576-2097151/838860800Los cuatro elementos que importan:
Range: bytes=inicio-finpide desde el byteiniciohastafin, ambos incluidos.bytes=0-1023son los primeros 1024 bytes.bytes=1000-llega hasta el final.bytes=-500pide los últimos 500 bytes.206 Partial Contentindica que el cuerpo es solo una parte del archivo.Content-Range: bytes 1048576-2097151/838860800dice qué fragmento llegó y el tamaño total.Accept-Ranges: bytesanuncia que el servidor acepta rangos. Si el encabezado no aparece, el cliente no puede contar con ese soporte.
Si el servidor ignora el Range, responde 200 OK con el archivo entero. El navegador lo acepta, pero pierde la capacidad de adelantar sin descargarlo todo.
¿Por qué el video y el audio dependen de esto?#
Las etiquetas <video> y <audio> no descargan el archivo completo para reproducirlo después. El navegador pide el inicio, averigua la duración y el formato, y reproduce mientras descarga. Cuando el usuario adelanta, calcula el byte correspondiente y lanza una nueva petición con Range.
El atributo preload controla cuánto se descarga antes de reproducir: none no descarga nada, metadata descarga solo lo necesario (duración y dimensiones) y auto permite descargar el archivo entero. Es una pista, y el navegador puede ignorarla. En páginas con muchos videos, preload="metadata" evita descargar decenas de archivos que nadie va a reproducir:
<video controls preload="metadata" width="720" src="https://ejemplo.com/videos/clase.mp4"></video>Sin soporte de rangos, tienes el escenario del principio: una barra de progreso que no responde, un inicio lento y mucho más consumo de ancho de banda del necesario.
¿Cómo pruebo si el servidor acepta rangos?#
Pide los primeros bytes con curl y mira solo los encabezados:
curl -s -o /dev/null -D - -H "Range: bytes=0-99" https://ejemplo.com/videos/clase.mp4Una respuesta correcta tiene este formato:
HTTP/2 206
accept-ranges: bytes
content-length: 100
content-range: bytes 0-99/838860800
content-type: video/mp4Revisa tres cosas: el estado es 206 (no 200), el content-length es 100 y el content-range trae el total. Si llega 200 con el content-length del archivo entero, el servidor ignoró el Range.
La API de S3 también trabaja con rangos: GetObject acepta el parámetro Range y devuelve solo el fragmento. En Python, con boto3:
import os
import boto3
s3 = boto3.client(
"s3",
endpoint_url=os.environ.get("S3_ENDPOINT"), # omítelo en AWS S3
aws_access_key_id=os.environ["S3_ACCESS_KEY"],
aws_secret_access_key=os.environ["S3_SECRET_KEY"],
)
r = s3.get_object(Bucket="mi-bucket", Key="videos/clase.mp4", Range="bytes=0-99")
print(r["ResponseMetadata"]["HTTPStatusCode"]) # 206
print(r["ContentRange"]) # bytes 0-99/838860800
print(len(r["Body"].read())) # 100¿Cómo sirvo video desde un bucket privado?#
Si el archivo no es público, genera una URL prefirmada y úsala en el src. Por defecto, la URL prefirmada de S3 firma solo el encabezado host, y el Range queda fuera de la firma, así que el navegador puede pedir cualquier fragmento con la misma URL:
url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": "mi-bucket", "Key": "videos/clase.mp4"},
ExpiresIn=6 * 3600,
)Elige el ExpiresIn pensando en cuánto puede durar la reproducción, no solo en la carga de la página: si la URL caduca a mitad del video, el siguiente salto falla con 403. Genera la URL cuando el usuario abre el reproductor, no al compilar el sitio.
¿Cómo sirvo rangos desde mi propio servidor?#
Si el archivo está en el disco de tu aplicación (o lo sirves a través de un proxy propio), tienes que implementar el Range tú mismo. Este servidor Node.js no usa dependencias y cubre las formas inicio-fin, inicio- y -sufijo:
import { createReadStream, statSync } from "node:fs";
import { createServer } from "node:http";
const FILE = "video.mp4";
createServer((req, res) => {
const { size } = statSync(FILE);
const base = { "Content-Type": "video/mp4", "Accept-Ranges": "bytes" };
const m = /^bytes=(\d*)-(\d*)$/.exec(req.headers.range ?? "");
if (!m || (m[1] === "" && m[2] === "")) {
res.writeHead(200, { ...base, "Content-Length": size });
return createReadStream(FILE).pipe(res);
}
let start, end;
if (m[1] === "") {
start = Math.max(size - Number(m[2]), 0); // bytes=-500: últimos 500
end = size - 1;
} else {
start = Number(m[1]);
end = m[2] === "" ? size - 1 : Math.min(Number(m[2]), size - 1);
}
if (start >= size || start > end) {
res.writeHead(416, { "Content-Range": `bytes */${size}` });
return res.end();
}
res.writeHead(206, {
...base,
"Content-Range": `bytes ${start}-${end}/${size}`,
"Content-Length": end - start + 1,
});
createReadStream(FILE, { start, end }).pipe(res);
}).listen(3000);Pruébalo con el curl de la sección anterior apuntando a http://localhost:3000. El ejemplo ignora los rangos múltiples (bytes=0-50, 100-150) y responde el archivo entero, algo que la especificación permite. En producción, prefiere servir el archivo desde el storage o desde un servidor web que ya lo implemente, en lugar de mantener este código.
¿Qué errores debo evitar?#
mooval final del MP4. En un MP4, el índice del video vive en el átomomoov. Si está al final, el navegador tiene que buscarlo antes de reproducir. Muévelo al inicio sin recodificar:ffmpeg -i entrada.mp4 -c copy -movflags +faststart salida.mp4. La documentación de ffmpeg advierte que este paso hace una segunda lectura y no funciona con salida fragmentada.- Recibir
200en lugar de206. Alguna capa en el camino (proxy, middleware de compresión, función serverless) está descartando elRange. Prueba concurldirecto contra el storage y luego contra la URL final para encontrar dónde se pierde. Content-Typeincorrecto. Un MP4 servido comoapplication/octet-streampuede descargarse en vez de reproducirse. Definevideo/mp4,video/webmoaudio/mpegen el objeto.- Comprimir el video. No apliques gzip ni brotli a un MP4: el archivo ya está comprimido, la ganancia es mínima y una capa de compresión en el camino puede desactivar o romper los rangos.
- URL prefirmada demasiado corta. Caduca a mitad de la reproducción y adelantar falla.
- Reemplazar un archivo con la misma URL. Si el objeto cambia mientras el navegador usa fragmentos en caché, las piezas no encajan. Usa un nombre nuevo por versión (por ejemplo
clase-v2.mp4) en lugar de sobrescribir. - Ancho de banda sin control. Cada salto genera más peticiones. En storages que cobran la salida, un video popular se convierte en un costo; lee sobre las tarifas de egress antes de elegir dónde alojarlo.
¿Cuándo usar HLS o DASH en lugar de un MP4?#
Un MP4 con rangos resuelve videos cortos y medianos con una sola calidad. Si necesitas cambiar de resolución según la conexión del usuario, o transmitir en vivo, el camino es HLS o DASH: el video se divide en segmentos pequeños y una playlist indica cuáles descargar. Los segmentos son archivos comunes, y el object storage cumple bien ese papel. Para la mayoría de los sitios, empezar con un MP4 y faststart es suficiente.
Cómo hacerlo en Square Cloud#
El Blob Storage de Square Cloud es un object storage con API compatible con S3, y las descargas responden a range requests con 206, así que el reproductor adelanta el video sin descargar el archivo entero. Los archivos públicos se sirven desde una CDN global en el edge, y cada objeto acepta ajustes como Cache-Control y la elección entre descarga y visualización inline (útil cuando alguien abre la URL del video directamente en el navegador).
Para archivos privados, el código boto3 de arriba funciona con el endpoint https://s3-blob.squarecloud.app, la región auto, el bucket private y las claves de la pestaña "Acceso S3" del panel: la URL prefirmada SigV4 puede durar hasta 7 días. Desde el panel también tienes el enlace temporal de hasta 24 horas y el enlace para compartir de hasta 30 días, que se puede revocar. Los archivos pueden pesar hasta 10 GB, la descarga no tiene cobro de ancho de banda y todos los planes incluyen una cuota de 5 GB a 250 GB.
Antes de publicar, ejecuta la prueba curl -H "Range: bytes=0-99" contra la URL de tu objeto y confirma el 206 y el Accept-Ranges. Es la forma más rápida de validar el comportamiento con tu propio archivo.
Blob Storage
Object storage compatible con S3, sin tarifas de salida
Preguntas frecuentes
¿Necesito un servidor de streaming para reproducir video en mi sitio?
Para un archivo MP4 o WebM común, no. La etiqueta video reproduce el archivo por HTTP siempre que el servidor o el storage responda a range requests. El streaming adaptativo (HLS o DASH) solo hace falta si quieres cambiar de calidad según la red.
¿Qué significa el error 416 Range Not Satisfiable?
El servidor recibió un Range que no cabe en el archivo, por ejemplo un byte inicial mayor que el tamaño del objeto. Responde 416 con Content-Range: bytes */tamaño. Suele indicar que el archivo cambió o que el cliente calculó mal el offset.
¿Por qué el video solo empieza después de descargar casi todo?
En un MP4, los metadatos están en el átomo moov. Si está al final del archivo, el navegador tiene que buscarlo antes de reproducir. Reprocesa con ffmpeg -movflags +faststart para moverlo al inicio, sin recodificar.
¿Funciona con una URL prefirmada?
Sí. Range es un encabezado de la petición y no forma parte de la firma de una URL prefirmada de S3. El navegador puede enviar Range con normalidad, y la URL solo necesita durar lo que dure la reproducción.
Más en Storage
Ver los 15 artículos- Qué es una URL prefirmada (presigned URL)Una URL prefirmada (presigned URL) es un enlace temporal a un archivo privado que funciona sin login. Mira cómo funciona, sus límites y cuándo usarla.
- Qué es el almacenamiento de objetosEl almacenamiento de objetos guarda archivos como objetos con clave y metadatos, accesibles por una API HTTP. Mira cómo funciona, para qué sirve y sus límites.
- Qué es S3 y qué significa 'compatible con S3'S3 es el almacenamiento de objetos de Amazon y su API se volvió el estándar del sector. Aprende qué significa ser compatible con S3 y cómo conectarte.
- Subir archivos grandes: carga multiparte y reanudableLa carga multiparte divide un archivo grande en partes que se envían en paralelo y se reintentan por separado. Mira el paso a paso en Python y cómo reanudar.