TechLead
Leccion 18 de 30
7 min de lectura
Diseño de Sistemas

Diseño de Sistemas: Plataforma de Video Streaming

Diseña una plataforma de streaming de video como YouTube cubriendo pipelines de subida, transcodificación, streaming de bitrate adaptativo, entrega por CDN y optimización de costos

Enunciado del Problema

Diseña una plataforma de streaming de video como YouTube o Netflix. El sistema debe manejar subidas de video, procesamiento, almacenamiento y entrega a millones de espectadores concurrentes. Este problema es desafiante porque el video es el tipo de contenido más intensivo en ancho de banda en internet, requiriendo decisiones arquitectónicas cuidadosas sobre codificación, almacenamiento y entrega.

Paso 1: Requisitos

Requisitos Funcionales

  • Los usuarios pueden subir videos (hasta 1 hora, múltiples formatos)
  • Los videos se transcodifican en múltiples resoluciones y formatos
  • Streaming de bitrate adaptativo basado en condiciones de red del espectador
  • Reproducción de video con búsqueda, pausa y reanudación
  • Generación de miniaturas y preview de video
  • Sistema básico de recomendación

Requisitos No Funcionales

  • Soporte para 1 mil millones de visualizaciones diarias de video
  • Tiempo rápido de inicio (<2 segundos al primer frame)
  • Alta disponibilidad (99.99%)
  • Entrega global con baja latencia
  • Almacenamiento y ancho de banda eficientes en costos

Estimaciones de Escala

Métrica Estimación
Videos subidos por día500,000
Tamaño promedio de video (original)500 MB
Volumen diario de subidas~250 TB
Visualizaciones diarias1 mil millones
Duración promedio de visualización5 minutos
Espectadores concurrentes pico~5 millones

Paso 2: Pipeline de Subida y Procesamiento de Video

El pipeline de subida es un proceso de múltiples etapas que transforma un video crudo subido en múltiples formatos listos para streaming. Esta es la parte más intensiva en cómputo del sistema.

// Pipeline de subida y procesamiento

interface VideoUpload {
  id: string;
  userId: string;
  originalFileName: string;
  originalUrl: string;     // Ubicación en S3 del archivo subido
  status: "uploading" | "processing" | "ready" | "failed";
  metadata: VideoMetadata;
  createdAt: Date;
}

interface VideoMetadata {
  title: string;
  description: string;
  tags: string[];
  duration: number;        // segundos
  resolution: string;      // "1920x1080"
  codec: string;
  frameRate: number;
  fileSize: number;        // bytes
}

interface ProcessedVideo {
  videoId: string;
  variants: VideoVariant[];
  thumbnails: string[];
  manifest: string;        // URL del manifiesto HLS/DASH
}

interface VideoVariant {
  resolution: string;      // "1080p", "720p", "480p", "360p"
  bitrate: number;         // kbps
  codec: string;
  segmentUrls: string[];   // URLs de segmentos de video
}

// Flujo de subida
class VideoUploadService {
  async initiateUpload(userId: string, metadata: Partial<VideoMetadata>): Promise<UploadSession> {
    // Generar una URL pre-firmada de S3 para subida directa
    // El cliente sube directamente a S3 (evita nuestros servidores)
    const uploadId = generateId();
    const s3Key = `uploads/${userId}/${uploadId}/original`;

    const presignedUrl = await this.s3.getSignedUrl("putObject", {
      Bucket: "video-uploads",
      Key: s3Key,
      Expires: 3600, // 1 hora
      ContentType: "video/*",
    });

    // Crear registro de subida
    await this.db.videoUploads.insert({
      id: uploadId,
      userId,
      status: "uploading",
      originalUrl: `s3://video-uploads/${s3Key}`,
      metadata,
    });

    return {
      uploadId,
      presignedUrl,
      // Para archivos grandes, usar subida multipart
      multipartUrls: await this.generateMultipartUrls(s3Key),
    };
  }

  // Activado por notificación de evento S3 cuando se completa la subida
  async onUploadComplete(uploadId: string): Promise<void> {
    await this.db.videoUploads.update(uploadId, { status: "processing" });

    // Enviar al pipeline de procesamiento
    await this.messageQueue.publish("video-processing", {
      uploadId,
      stages: ["validate", "transcode", "thumbnail", "manifest"],
    });
  }
}

Paso 3: Transcodificación y Bitrate Adaptativo

La transcodificación convierte el video original en múltiples resoluciones y bitrates. Esto permite al reproductor cambiar entre niveles de calidad basándose en el ancho de banda del espectador -- una técnica llamada Streaming de Bitrate Adaptativo (ABR).

Perfiles Estándar de Transcodificación

Calidad Resolución Bitrate (video) Caso de Uso
4K UHD3840x216015-25 MbpsSmart TVs, gama alta
1080p HD1920x10804-8 MbpsEscritorio, buen WiFi
720p HD1280x7202-4 MbpsMóvil en WiFi
480p SD854x4801-2 MbpsMóvil en 4G
360p640x3600.5-1 MbpsConexiones lentas
// Pipeline de transcodificación (ejecuta en workers con GPU)
class TranscodingService {
  private profiles = [
    { name: "1080p", width: 1920, height: 1080, bitrate: 5000 },
    { name: "720p",  width: 1280, height: 720,  bitrate: 2500 },
    { name: "480p",  width: 854,  height: 480,  bitrate: 1200 },
    { name: "360p",  width: 640,  height: 360,  bitrate: 700 },
  ];

  async transcodeVideo(uploadId: string, sourceUrl: string): Promise<VideoVariant[]> {
    const variants: VideoVariant[] = [];

    // Obtener info del video fuente
    const sourceInfo = await this.probe(sourceUrl);

    // Solo transcodificar a resoluciones <= resolución fuente
    const applicableProfiles = this.profiles.filter(
      (p) => p.height <= sourceInfo.height
    );

    // Procesar cada perfil (puede paralelizarse entre workers)
    for (const profile of applicableProfiles) {
      const segments = await this.transcodeToProfile(sourceUrl, profile);
      variants.push({
        resolution: profile.name,
        bitrate: profile.bitrate,
        codec: "h264", // o h265/AV1 para mejor compresión
        segmentUrls: segments,
      });
    }

    return variants;
  }

  // Cada video se divide en segmentos pequeños (2-10 segundos)
  // Esto permite cambio de bitrate adaptativo en los límites de segmento
  private async transcodeToProfile(
    sourceUrl: string,
    profile: TranscodeProfile
  ): Promise<string[]> {
    // Usando FFmpeg internamente:
    // ffmpeg -i input.mp4 \
    //   -vf scale=1280:720 \
    //   -b:v 2500k \
    //   -hls_time 6 \
    //   -hls_segment_filename "segment_%03d.ts" \
    //   output.m3u8

    const outputDir = `processed/${profile.name}/`;
    const segmentDuration = 6; // segundos

    return this.ffmpeg.transcode({
      input: sourceUrl,
      output: outputDir,
      width: profile.width,
      height: profile.height,
      bitrate: profile.bitrate,
      segmentDuration,
      format: "hls", // HTTP Live Streaming
    });
  }
}

Paso 4: Protocolos de Streaming (HLS y DASH)

El streaming moderno de video usa protocolos de streaming adaptativo basados en HTTP. Los dos protocolos dominantes son HLS (HTTP Live Streaming, creado por Apple) y DASH (Dynamic Adaptive Streaming over HTTP, estándar abierto).

HLS vs DASH

Característica HLS DASH
Creado PorAppleMPEG (estándar abierto)
Formato de Manifiesto.m3u8 (texto).mpd (XML)
Formato de Segmento.ts o .fmp4.m4s (fMP4)
Soporte de NavegadorNativo en Safari, via JS en otrosVia JS (dash.js, Shaka)
Soporte de CodecH.264, H.265, AV1Cualquier codec
DRMFairPlayWidevine, PlayReady
// Estructura de manifiesto HLS (simplificado)
// Master playlist (video.m3u8) - apunta a playlists variantes
const masterPlaylist = `
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
1080p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=1280x720
720p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1200000,RESOLUTION=854x480
480p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=700000,RESOLUTION=640x360
360p/playlist.m3u8
`;

// Variant playlist (720p/playlist.m3u8) - lista segmentos
const variantPlaylist = `
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:6
#EXTINF:6.0,
segment_001.ts
#EXTINF:6.0,
segment_002.ts
#EXTINF:6.0,
segment_003.ts
#EXTINF:4.5,
segment_004.ts
#EXT-X-ENDLIST
`;

// El reproductor:
// 1. Descarga el master playlist
// 2. Selecciona una variante basada en el ancho de banda actual
// 3. Descarga segmentos de esa variante
// 4. Si el ancho de banda cambia, cambia a una variante diferente en el siguiente límite de segmento

Paso 5: CDN para Entrega de Video

La entrega de video es la parte más intensiva en ancho de banda del sistema. Un CDN es esencial para entregar segmentos de video desde ubicaciones edge cercanas a los espectadores, reduciendo latencia y carga del origen.

  • Videos populares: Cacheados en ubicaciones edge a nivel mundial. La tasa de aciertos de caché debe ser 90%+.
  • Contenido de larga cola: Videos menos populares pueden servirse desde cachés regionales u origen.
  • Caché por niveles: Edge -> Caché Regional -> Origen. Cada nivel reduce solicitudes al origen.
  • Pre-calentamiento: Para contenido viral anticipado, empujar segmentos a ubicaciones edge proactivamente.

Paso 6: Generación de Miniaturas

class ThumbnailService {
  async generateThumbnails(
    videoUrl: string,
    videoId: string,
    duration: number
  ): Promise<string[]> {
    const thumbnails: string[] = [];

    // Generar miniaturas a intervalos regulares
    const intervals = this.calculateIntervals(duration);
    // ej., para un video de 60s: [0, 10, 20, 30, 40, 50]

    for (const timestamp of intervals) {
      // Extraer frame usando FFmpeg
      // ffmpeg -i input.mp4 -ss 10 -frames:v 1 -vf scale=320:180 thumb.jpg
      const thumbnailUrl = await this.extractFrame(videoUrl, timestamp, {
        width: 320,
        height: 180,
        format: "webp", // Más pequeño que JPEG
      });
      thumbnails.push(thumbnailUrl);
    }

    // Generar preview de video (miniatura animada / sprite sheet)
    const spriteSheet = await this.generateSpriteSheet(videoUrl, intervals);

    // Almacenar URLs de miniaturas en metadatos del video
    await this.db.videos.update(videoId, {
      thumbnails,
      defaultThumbnail: thumbnails[Math.floor(thumbnails.length / 3)],
      spriteSheet,
    });

    return thumbnails;
  }
}

Paso 7: Optimización de Almacenamiento y Costos

Desglose de Costos de Almacenamiento

  • Subidas originales: Almacenadas temporalmente, eliminadas después del procesamiento (ahorra 500MB por video)
  • Variantes transcodificadas: 5x el tamaño original (múltiples resoluciones). Un video de 500MB se convierte en ~2.5GB
  • Almacenamiento por niveles: Mover videos viejos y raramente accedidos a almacenamiento más barato (S3 Glacier, almacenamiento frío)
  • Eficiencia de codec: AV1 ofrece 30-50% mejor compresión que H.264 pero es más lento para codificar. Usar para videos populares.
// Estrategia de almacenamiento por niveles
class StorageTierManager {
  // Los videos se clasifican en niveles basados en frecuencia de visualización
  async tierVideo(videoId: string): Promise<void> {
    const stats = await this.getViewStats(videoId);
    const daysSinceUpload = this.daysSince(stats.uploadedAt);
    const recentViews = stats.viewsLast30Days;

    if (recentViews > 1000) {
      // Caliente: mantener todas las variantes en almacenamiento rápido + CDN
      await this.moveToTier(videoId, "hot"); // S3 Standard
    } else if (recentViews > 10) {
      // Tibio: mantener variantes populares, mover otras a almacenamiento más barato
      await this.moveToTier(videoId, "warm"); // S3 Infrequent Access
      // Solo mantener 720p y 360p en almacenamiento rápido
      await this.archiveVariants(videoId, ["1080p", "4k"]);
    } else if (daysSinceUpload > 365) {
      // Frío: archivar al almacenamiento más barato, transcodificar bajo demanda
      await this.moveToTier(videoId, "cold"); // S3 Glacier
      // Mantener solo 360p, re-transcodificar mayor resolución bajo demanda
    }
  }
}

// Optimización de costos de ancho de banda:
// 1. Usar codecs eficientes (AV1 > H.265 > H.264) para contenido popular
// 2. Servir miniaturas WebP/AVIF en lugar de JPEG (50% más pequeñas)
// 3. Estimación de ancho de banda del lado del cliente para evitar sobre-servir calidad
// 4. Limitar calidad máxima basada en tamaño de pantalla (no servir 4K a teléfonos)

Resumen de Arquitectura

  • Subida: Directo a S3 con URLs pre-firmadas, evitando servidores de aplicación
  • Procesamiento: Pipeline asíncrono via cola de mensajes, workers GPU para transcodificación
  • Almacenamiento: S3 con almacenamiento por niveles, CDN para entrega, clasificación basada en popularidad
  • Streaming: HLS/DASH con bitrate adaptativo, segmentos de 6 segundos
  • Entrega: CDN multi-nivel (edge -> regional -> origen)
  • Metadatos: PostgreSQL para metadatos de video, Elasticsearch para búsqueda

Continuar Aprendiendo