Streaming de video tipo YouTube
Subir, transcodificar, almacenar y reproducir videos en cualquier dispositivo y red, con baja latencia y alto throughput.
Recursos
Video, UploadSession, ManifestHLS, Channel, ViewEvent.
Operaciones
Upload directo a storage, transcoding async, lectura por CDN.
Escala
Petabytes de blobs, miles de horas/min subidas, billones de plays.
Riesgo
Servir desde app, costos de storage, derechos de copyright.
Requisitos
Aclara qué se debe hacer y con qué garantías de calidad.
Funcionales
- Upload de video con resume de chunks.
- Transcoding a múltiples renditions y thumbnails.
- Playback adaptativo (HLS / DASH) por dispositivo.
- Search, comentarios, likes y subscripciones.
- Reportes / takedown por copyright (DMCA).
No funcionales
- TTFB < 500 ms para reproducción global.
- Durabilidad 11×9 en object storage.
- 99.95% disponibilidad de reproducción.
- Costos de egress bajo control vía CDN.
- Eventual consistency en views/likes.
Cómo aclarar requisitos en la entrevista
Casos de uso típicos. VOD largo (películas, documentales, vlogs), Shorts (videos verticales < 60 s), live streaming (gaming, eventos, deportes), premiere programado, contenido restringido por edad o región, monetización con ads (pre-roll, mid-roll, post-roll), creator dashboard con analytics, family-safe vs maduro.
Preguntas que debes hacer:
• ¿Solo VOD o también live? Live cambia todo: low-latency HLS, ingest, chat, etc.
• ¿DRM obligatorio (Widevine / FairPlay) o solo signed URLs?
• ¿Comments y likes están en este alcance?
• ¿Subtítulos automáticos generados (ASR)?
• ¿Geo-blocking por país (licencias)?
• ¿Monetización: ads, paywall, ambos?
• ¿Privacidad: público / unlisted / privado / paywall?
• ¿Edad / política de contenido: review humana o solo automática?
• ¿Recommendations en este alcance o se asume otro sistema?
Frase clave: "Asumo VOD público con monetización via ads, sin live ni DRM. Si me pides live, agrego ingest server con low-latency HLS y un servicio de chat al final."
Estimaciones de capacidad
Resoluciones año/día/min/seg. Lo que justifica object storage + CDN y no servir desde el app server.
~500M DAU · ~6K logins/s avg.
~262M h/año · ~30 PB/mes nuevos.
Bandwidth dominado por CDN.
Picos tarde-noche por timezone.
Activos calientes + archives + thumbs.
+ audio-only para podcasts.
~5% miss = ~50 PB/mes a origen.
~43 s/día permitidos.
Endpoints
Subida con URL firmada (no pasa por tu app), reproducción vía CDN.
| Método | Path | Descripción | Notas |
|---|---|---|---|
| POST | /api/videos/upload-url | Pide URL pre-firmada para subir directo a object storage. | S3/GCS multipart. |
| POST | /api/videos/{videoId}/complete-upload | Marca el upload terminado y dispara el transcoding. | Encola job. |
| GET | /api/videos/{videoId} | Metadata: título, descripción, estado, thumbnails. | Cacheable. |
| GET | /api/videos/{videoId}/manifest.m3u8 | Manifiesto HLS con renditions. | Vía CDN. |
| POST | /api/events/view | Registra play/progreso. | Async, batch. |
Cómo defiendo estas APIs en la entrevista
El upload no toca mi backend. Es la decisión más importante de las APIs: POST /videos/upload-url devuelve URLs pre-firmadas; el cliente hace PUT directamente al object storage. Mi servicio solo gestiona metadata. Sin esto, sería imposible mantener 500 horas de uploads/min con un app server.
Async by default para work pesado. complete-upload devuelve 202 Accepted con un job ID, no 200 con video listo. El transcoding tarda minutos — no puedo dejar al cliente colgado. El playback usa GET /videos/{id}/manifest.m3u8 que va a CDN; otra vez, mi backend no toca bytes.
Versionado y observabilidad. URL path /v1. Status codes específicos: 201 al crear video metadata, 202 al disparar transcoding, 423 Locked si video bajo DMCA review, 410 Gone si fue removido. Cada response trae correlation-id en header para tracing.
Frase clave: "El upload va por pre-signed URL — el cliente sube directo al storage. El playback va por CDN — los bytes nunca tocan mi backend. Mi API solo orquesta metadata y eventos."
Ejemplo: subida
El cliente sube directo al storage; el backend sólo da credenciales.
POST /api/videos/upload-url
{ "filename": "vacation.mov", "sizeBytes": 524288000 }
201 Created
{
"videoId": "v_9981",
"uploadUrls": [
"https://storage/.../part1?sig=...",
"https://storage/.../part2?sig=..."
],
"expiresAt": "2026-05-01T13:00:00Z"
}
Arquitectura
Auth + Rate Limit"}} --> Meta API -->|complete-upload| Stream Stream --> Trans["Transcoder Workers
FFmpeg async"] Trans --> Store Trans --> Meta Trans --> Thumb["Thumbnail Generator"] Client -- playback request --> Edge(["CDN Edge POPs"]) Edge -->|miss| Store Client -. events .-> Analytics subgraph Store["🗄️ Object Storage · S3 / GCS"] direction TB s1["videos/raw"] s2["videos/hls"] s3["thumbnails"] end subgraph Meta["💾 Metadata DB · Postgres / Spanner"] direction TB m1["videos"] m2["video_assets"] m3["channels"] m4["comments"] m5["subscriptions"] end subgraph Stream["📨 Pub/Sub"] direction TB k1["video.uploaded"] k2["video.transcoded"] end subgraph Analytics["🗄️ Analytics · BigQuery"] direction TB a1["view_events"] a2["watch_time_daily"] end classDef gateway fill:#fbbf24,stroke:#f59e0b,stroke-width:3px,color:#0f172a,font-weight:bold classDef edge fill:#1e1b4b,stroke:#a78bfa,stroke-width:2px,color:#e0e7ff classDef service fill:#7f1d1d,stroke:#fb7185,stroke-width:2px,color:#fecdd3 classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class API gateway class Edge edge class Trans,Thumb service class Client client
Cómo lo explico en la entrevista
Upload. El creador presiona "subir". El cliente pide POST /api/videos/upload-url; el API responde con un videoId y una pre-signed URL multipart al object storage. El cliente sube los chunks directo a S3/GCS — los bytes nunca tocan mi backend. Cuando termina, hace POST /api/videos/{id}/complete-upload y eso publica un evento video.uploaded al Pub/Sub.
Transcoding async. Los Transcoder workers (auto-scaled) consumen el evento, descargan el master, generan renditions (240p, 480p, 720p, 1080p, 4K) más segmentos HLS y thumbnails con FFmpeg, y los suben de regreso al storage. Updatean la metadata DB cambiando estado de processing a ready.
Playback. El viewer hace play. Cliente pide GET /api/videos/{id}/manifest.m3u8. El edge CDN responde si lo tiene cacheado; si no, va al origen y popula. El cliente pide segmentos de 2–10 s al CDN POP más cercano. Mi backend nunca toca los bytes; eso es lo que hace que esto escale a billones de horas/día.
Frase clave: el truco es que el upload va por pre-signed URL y la entrega va por CDN. Mi backend solo gestiona metadata y orquestación de jobs — los blobs pesados los maneja la infra de almacenamiento y entrega.
Por qué cada componente
Cuellos de botella
- Encoding queue lag en eventos virales o uploads masivos.
- Cold cache para videos nuevos virales — primer impacto pega al origen.
- Search index lag detrás de uploads recientes.
- Egress cost cuando hay miss en CDN (clip que no estaba caliente).
- Long-tail storage: 99% de videos nunca se ven después del primer mes pero ocupan EB.
Mejoras / cómo escala
- Tiered encoding: 480p disponible en segundos, resto en background.
- Cache prewarm al detectar trending por geo / canal popular.
- Per-segment caching más fino, no todo el video.
- Cold storage tier (Glacier) para videos sin views > 90 días.
- Two-pass encoding en videos con alto watch time (mejor quality/byte).
- P2P assist (CDN edge serving via clients) para eventos en vivo masivos.
Por qué estos servicios cloud (vs alternativas)
Por qué: 11×9 durabilidad nativa, multipart upload con resume, lifecycle policies (hot → cold → archive automático), pre-signed URLs para upload directo desde el cliente. Es el patrón estándar para blobs masivos.
vs HDFS / MinIO self-hosted: tienes que operar el cluster, manejar disk failures, replication. vs servir desde un filesystem en VM: imposible a escala, no escala horizontal. vs Cassandra / DynamoDB: no diseñados para blobs > 1 MB.
Por qué: desacopla el upload del transcoding pesado. Los workers consumen al ritmo que pueden, autoscaling por backlog. Permite re-procesar (re-encode) si cambias de codec.
vs SQS: queue point-to-point — no soporta múltiples consumers consumiendo el mismo evento (transcoder + analytics + content moderation). vs llamada síncrona al transcoder: el upload se queda bloqueado minutos.
Por qué: POPs globales, latencia <100 ms global, cache hit ratio del 95%+ define el costo del negocio. Adaptive bitrate funciona naturalmente (cada segmento es una URL cacheable).
vs servir desde object storage directo: egress costaría 10–100×, latencia inaceptable fuera de la región. vs CDN self-hosted: imposible a escala global. vs P2P only: falla en eventos cold (no hay peers todavía).
Por qué: FFmpeg es estándar de industria para transcoding. Lo corres en Kubernetes / ECS / Cloud Run con autoscaling según cola pendiente. Un worker = un video a la vez, paralelismo natural.
vs MediaConvert (AWS) / Transcoder API (GCP): managed services aceptables pero más caros y menos flexibles para custom encoding ladders. vs hardware encoders dedicados: rentables solo a escala extrema.
Por qué: lecturas masivas con joins (video + channel + comments), índices full-text para search, ACID en operaciones de canal/upload. Postgres/Spanner para joins, Cassandra si write-heavy y multi-region.
vs DynamoDB: sin joins ni full-text — tendrías que duplicar data. vs MongoDB: aceptable pero performance se degrada con joins via $lookup.
Por qué: serverless OLAP, escala a EB sin provisioning. Eventos de view/click se sinkan vía Dataflow / Streaming. Reportes para creators (analytics dashboard) y ML (training pipelines).
vs ClickHouse: aceptable, más barato pero requiere ops. vs Redshift: requiere provisioning capacity. vs Postgres: ni de cerca a esta escala.
Diseño de base de datos
Metadata en DB relacional, blobs en object storage, eventos analíticos en stream.
(channel_id, created_at DESC)idx
(status)(video_id, rendition)(handle UNIQUE)video_idSink: BigQuery / data lake
Cómo defiendo este diseño de base de datos
Separación radical: metadata vs blobs vs events. Tres stores distintos: una DB transaccional para video metadata (joins, indexes), object storage para los blobs, stream para events. Esta separación es la que hace que el sistema escale a EB de storage y miles de millones de horas vistas/día.
Schema mínimo en metadata. En videos: campos de búsqueda y display, no los bytes. video_assets separado guarda referencias a las renditions en object storage — el cliente pide la rendition adaptive, mi DB solo dice "vive en S3://bucket/path". Index (channel_id, created_at DESC) para "videos del canal", (status) para filtrar processing/ready.
Eventos a stream, no a la DB. View events son ~300K/s — cualquier DB tradicional muere intentando escribir eso. Stream particionado por video_id mantiene orden por video; el sink agrega en BigQuery para analytics. La DB solo se entera de un view via batch que actualiza counters cada minuto.
Frase clave: "Mi DB de metadata es small (PB), el object storage es huge (EB), y el stream es high-throughput. Cada uno hace lo suyo; la falla de uno no rompe los otros."
Decisiones técnicas
| Tema | Decisión | Por qué |
|---|---|---|
| Upload | URL pre-firmada con multipart. | Ahorra ancho de banda en tu app y permite reanudar. |
| Transcoding | Job async por cola con varios workers. | El usuario no espera; se generan múltiples renditions. |
| Entrega | HLS/DASH con segmentos pequeños vía CDN. | Adaptive bitrate y caching natural en edge. |
| Metadata | DB relacional o NoSQL para video, comentarios y estados. | Lecturas masivas, escribibles bajo control. |
| Thumbnails | Generados en el pipeline de transcoding. | Una sola pasada, costo amortizado. |
| Eventos de play | Stream a un pipeline analítico. | Para recomendaciones, billing y trending. |
Cómo defiendo estas decisiones técnicas
Decisión #1: pre-signed URL para upload. El constraint es 500 h/min de uploads. Si pasaran por mi backend, necesitaría miles de servidores solo para recibir bytes. Pre-signed URL: el cliente sube directo a S3, mi backend solo entrega credenciales con expiración corta (15 min).
Decisión #2: transcoding async con cola. El constraint es duración variable del transcoding (10 s para Shorts, 30 min para 4K largo). Sync mata UX. Cola con autoscaling: workers procesan según backlog, el cliente recibe 202 y consulta estado o usa webhook al completar.
Decisión #3: HLS/DASH con segmentos cortos. El constraint es adaptive bitrate y caching. Segmentos de 2–10 s son cacheables por CDN, soportan adaptive (cliente cambia bitrate por segmento), y permiten failover de un POP a otro mid-playback.
Métricas que validan. Cache hit ratio del CDN > 95% (define costo de egress). Encoding queue lag < 5 min (a 60 min, los uploads parecen rotos). TTFB < 500 ms global. Buffer ratio (re-buffering events / play events) < 1%.
Cuándo cambio de opinión. Live streaming → cambio HLS por LL-HLS (low latency) y agrego ingest server. Free tier explosivo → más agresivo el archive a cold storage. Si llega 4K mainstream → más renditions, más codec efficiency (AV1).
Frase clave: "Cada decisión es un trade entre latencia, costo y simplicidad. Pre-signed URL costa una llamada extra pero ahorra petabits/s de bandwidth. Transcoding async cuesta UX inicial pero hace que el sistema sea operable."
Pitfalls
Saturarás el server. Siempre object storage + CDN.
Hace que el upload tarde minutos. Cola y notificación cuando termine.
El CDN es caro a escala. Usa cache hit ratio como métrica clave.
Script de respuesta: “El upload va directo al object storage usando una URL pre-firmada que entrega mi API. Cuando el cliente confirma, encolo un job de transcoding que produce HLS con varias renditions. La metadata vive en una DB; los segmentos los entrega un CDN. La reproducción consume el manifest desde edge, así mi backend no toca el blob nunca.”
Tiers de almacenamiento
Aquí los tiers son todo: long-tail de videos puede vivir frozen indefinidamente. Esto define el costo total del negocio.
Cómo defenderlo en la entrevista
Long-tail es el rey aquí. 99% de los videos en YouTube tienen < 100 views/mes. Mantenerlos en S3 Standard es gastar millones extra al mes. Lifecycle a IA tras 30 días sin views los baja al 50% del costo.
Master files siempre frozen. El upload original (4K master) se guarda por si necesito re-encode con un codec mejor (AV1, HEVC). Pero ese master se toca casi nunca → Glacier Deep Archive desde día 1. Si necesito re-encode, pago 12 h de retrieval (es batch process anyway).
Re-promotion automática. Si un video viejo de pronto recibe views (alguien lo enlazó), un trigger lo promueve de IA → Standard. Sin esto, el primer view sufre 100 ms extra de latencia.
Frase clave: "Sin storage tiering el sistema no es viable económicamente. Los tiers reducen el costo total ~10× y los lifecycle policies hacen que sea automático. CDN + Hot tier sirven el 95% del tráfico desde el 1% del storage."
Versión alternativa: con Load Balancer (no API Gateway)
Para video, la latencia importa pero el bytes mismo va por CDN; el LB solo gestiona metadata.
ALB / Cloud LB"}} --> Pod["API Pods
+ JWT auth middleware
+ Rate limit Redis"] Pod --> Meta Pod -->|complete-upload| Stream Stream --> Trans["Transcoder Workers"] Trans --> Store Edge -->|miss| Store Client -->|pre-signed URL| Store Client -. events .-> Analytics subgraph Store["🗄️ Object Storage · S3 / GCS"] direction TB s1["videos/raw"] s2["videos/hls"] s3["thumbnails"] end subgraph Meta["💾 Metadata DB"] direction TB m1["videos"] m2["video_assets"] m3["channels"] m4["comments"] end subgraph Stream["📨 Pub/Sub"] direction TB k1["video.uploaded"] end subgraph Analytics["🗄️ Pipeline analítico · BigQuery"] direction TB a1["view_events"] end classDef lb fill:#34d399,stroke:#10b981,stroke-width:3px,color:#0f172a,font-weight:bold classDef edge fill:#1e1b4b,stroke:#a78bfa,stroke-width:2px,color:#e0e7ff classDef service fill:#7f1d1d,stroke:#fb7185,stroke-width:2px,color:#fecdd3 classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class LB lb class Edge edge class Pod,Trans service class Client client
API Gateway vs Load Balancer: cómo defender cada uno
YouTube tiene público y privado. La API pública (Data API v3) con developer keys, quotas, plans → Gateway encaja perfecto. La API interna entre tu frontend y backend → LB es más simple y rápido. En producción, ambos coexisten (Gateway en path /v3/, LB en path interno).
Frase clave: "Para Data API pública necesito Gateway: developer keys, monetización, quotas, métricas por endpoint. Para el playback y dashboard de creators (frontend propio), LB con auth en pod es más rápido y barato. En la práctica YouTube usa ambos en paralelo."
Cuándo elegir API Gateway
- Data API pública (developer keys, quotas, monetización por tier).
- Multiple SDKs (web, móvil, smart TV, partners) con políticas distintas.
- OAuth flows para "Sign in with YouTube".
- Rate limits granulares por endpoint (uploads vs lookups).
- Centralizar versioning (
/v3/,/v4/). - WAF y bot detection integrados.
Cuándo elegir Load Balancer
- API interna entre tu frontend y backend.
- Latencia crítica en endpoints de hot path (manifest fetch, watch events).
- Throughput masivo (~300K events/s) — gateway sería caro.
- Auth simple: cookie de sesión propia o JWT verificado local.
- Service mesh ya cubre auth/observability con sidecar.
- Eventos analíticos: ingest endpoint masivo, no necesita features de gateway.