# ST4.3: Video Upload Backend - Progress Report

**Blocker:** BLOCKER-003
**Prioridad:** P0 - CRÍTICO
**Esfuerzo Estimado:** 60h
**Esfuerzo Real:** 2h (completado 33%)
**Fecha:** 2026-01-26

---

## Estado: 🔄 EN PROGRESO (33% completado)

**Hallazgo:** Frontend ya existe (simulado), backend necesita implementación completa.

---

## Análisis Inicial (C: Contexto)

### Frontend: ✅ YA EXISTE (simulado)

**Archivo:** `apps/frontend/src/modules/education/components/VideoUploadForm.tsx`

**Features existentes:**
- ✅ Multi-step form (3 pasos: Select → Metadata → Upload)
- ✅ File validation (formato, tamaño max 500MB)
- ✅ Drag & drop support
- ✅ Video preview
- ✅ Thumbnail upload
- ✅ Metadata form (title, description, difficulty, tags, language)
- ✅ Progress bar UI
- ❌ Upload **SIMULADO** (línea 214-248)

**Código simulado (líneas 214-248):**
```typescript
// Simulate upload (replace with actual API call)
const handleUpload = useCallback(async () => {
  if (!selectedFile || !validateMetadata()) return;

  setUploadProgress({ status: 'uploading', progress: 0 });

  try {
    // Simulate upload progress
    for (let i = 0; i <= 100; i += 10) {
      await new Promise((r) => setTimeout(r, 200));
      setUploadProgress({ status: 'uploading', progress: i, message: `Uploading... ${i}%` });
    }

    // Simulate processing
    setUploadProgress({ status: 'processing', progress: 100, message: 'Processing video...' });
    await new Promise((r) => setTimeout(r, 1500));

    // Complete
    const videoId = `vid_${Date.now()}`;
    setUploadProgress({
      status: 'completed',
      progress: 100,
      message: 'Upload complete!',
      videoId,
    });

    onUploadComplete?.(videoId, metadata);
  } catch (error) {
    setUploadProgress({
      status: 'error',
      progress: 0,
      message: error instanceof Error ? error.message : 'Upload failed',
    });
  }
}, [selectedFile, metadata, validateMetadata, onUploadComplete]);
```

**Conclusión:** Frontend está listo pero necesita integración con backend real.

---

### Backend: ❌ NO EXISTE

**Verificado:**
- ❌ No hay `video.controller.ts`
- ❌ No hay `video.service.ts`
- ❌ No hay endpoints `/education/videos/*`
- ❌ No hay storage service

**Resultado:** Backend debe implementarse desde cero.

---

### Database: ❌ NO EXISTÍA

**Verificado:**
- ❌ No había tabla `videos`

**Resultado:** Tabla creada en ST4.3.1

---

## Tareas Completadas

### ✅ ST4.3.1: Database Tabla Videos (0.5h)

**Archivo:** `apps/database/ddl/schemas/education/tables/15-videos.sql`

**Commit:** `3f7816d` (database submodule)

**Features:**
- ✅ Tabla `education.videos` completa (150 líneas)
- ✅ Relaciones: course_id, lesson_id, uploaded_by
- ✅ Storage info: provider, bucket, key, region
- ✅ File info: size, mime_type, duration, original_filename
- ✅ Status tracking: uploading → uploaded → processing → ready → error/deleted
- ✅ Processing timestamps: started_at, completed_at, error
- ✅ CDN URLs: cdn_url, thumbnail_url
- ✅ Transcoded versions JSONB (múltiples resoluciones)
- ✅ Metadata JSONB (tags, language, difficulty, captions, transcript, codecs)
- ✅ Multipart upload tracking: upload_id, parts_completed, progress_percent
- ✅ Soft delete (deleted_at)
- ✅ Constraints: valid status, valid storage_provider, positive values
- ✅ Índices: course, lesson, uploader, status, storage_key, metadata (GIN), active (WHERE deleted_at IS NULL)
- ✅ Trigger: auto-update updated_at
- ✅ Function: soft_delete_video()
- ✅ View: active_videos

**Ejemplo de estructura:**
```sql
CREATE TABLE education.videos (
  id UUID PRIMARY KEY,
  course_id UUID REFERENCES education.courses(id),
  lesson_id UUID REFERENCES education.lessons(id),
  uploaded_by UUID REFERENCES core.users(id),
  title VARCHAR(200) NOT NULL,
  description TEXT,
  storage_provider VARCHAR(50) DEFAULT 's3',
  storage_key VARCHAR(500) NOT NULL,
  status VARCHAR(50) DEFAULT 'uploading',
  transcoded_versions JSONB, -- [{resolution, storage_key, cdn_url}]
  metadata JSONB, -- {tags, language, difficulty, captions}
  upload_id VARCHAR(500), -- AWS multipart upload ID
  cdn_url VARCHAR(1000),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  ...
);
```

---

### ✅ ST4.3.2: Backend Storage Service (1.5h)

**Archivo:** `apps/backend/src/shared/services/storage.service.ts`

**Commit:** `d7abb53` (backend submodule)

**Features implementadas:**

#### 1. Simple Upload (<5GB)
```typescript
async upload(options: UploadOptions): Promise<{ key: string; url: string }> {
  const command = new PutObjectCommand({
    Bucket: this.config.bucket,
    Key: options.key,
    Body: options.body,
    ContentType: options.contentType,
    Metadata: options.metadata,
    ACL: options.acl,
  });

  await this.client.send(command);
  return { key: options.key, url: this.getPublicUrl(options.key) };
}
```

#### 2. Multipart Upload (>100MB recommended)
```typescript
// Initialize
async initMultipartUpload(key, contentType, metadata): Promise<{ uploadId, key }>

// Upload each part
async uploadPart(key, uploadId, partNumber, body): Promise<{ etag, partNumber }>

// Complete
async completeMultipartUpload(key, uploadId, parts[]): Promise<{ key, url }>

// Abort if error
async abortMultipartUpload(key, uploadId): Promise<void>
```

#### 3. Presigned URLs (client-side upload)
```typescript
// Generate presigned URL for client to upload directly
async getPresignedUploadUrl(key, expiresIn, contentType): Promise<string>

// Generate presigned URL for download
async getPresignedDownloadUrl(key, expiresIn): Promise<string>
```

#### 4. Object Operations
```typescript
// Get object as Buffer
async getObject(key): Promise<Buffer>

// Delete object
async deleteObject(key): Promise<void>

// Copy object
async copyObject(sourceKey, destinationKey): Promise<void>

// Get metadata
async getObjectMetadata(key): Promise<ObjectMetadata>

// List objects with prefix
async listObjects(prefix?, maxKeys?): Promise<ObjectMetadata[]>
```

#### 5. URL Generation
```typescript
// Get public URL (S3, R2, or CDN)
getPublicUrl(key): string

// Generate unique key with timestamp + random
generateKey(prefix, filename): string
```

**Supports:**
- ✅ AWS S3
- ✅ Cloudflare R2 (S3-compatible API)
- ✅ CloudFront/Cloudflare CDN URLs

**Uses AWS SDK v3:**
- `@aws-sdk/client-s3`
- `@aws-sdk/s3-request-presigner`

**Environment variables needed:**
```bash
STORAGE_PROVIDER=s3           # or 'r2'
STORAGE_BUCKET=my-bucket
STORAGE_REGION=us-east-1
STORAGE_ACCESS_KEY_ID=xxx
STORAGE_SECRET_ACCESS_KEY=xxx
STORAGE_ENDPOINT=xxx          # for R2
STORAGE_CDN_URL=https://cdn   # optional
```

**Líneas:** 451

---

## Tareas Pendientes

### ⚠️ ST4.3.3: Backend Video Controller & Upload Endpoint (15h)

**Estado:** PENDIENTE

**Descripción:**
Crear controlador y endpoints REST para gestión de videos y multipart upload.

**Endpoints propuestos:**

#### 1. Initialize Upload
```typescript
POST /api/v1/education/videos/upload-init
Body: {
  courseId: uuid,
  lessonId?: uuid,
  filename: string,
  fileSize: number,
  contentType: string,
  metadata: {
    title: string,
    description: string,
    tags: string[],
    language: string,
    difficulty: string
  }
}

Response: {
  videoId: uuid,
  uploadId: string,
  storageKey: string,
  presignedUrls: string[] // for each part
}
```

#### 2. Upload Part (optional si usamos presigned URLs)
```typescript
PUT /api/v1/education/videos/:videoId/parts/:partNumber
Body: <binary data>

Response: {
  etag: string,
  partNumber: number
}
```

#### 3. Complete Upload
```typescript
POST /api/v1/education/videos/:videoId/complete
Body: {
  parts: [{ partNumber: number, etag: string }]
}

Response: {
  videoId: uuid,
  status: 'uploaded',
  message: 'Upload completed, processing started'
}
```

#### 4. Get Video
```typescript
GET /api/v1/education/videos/:id

Response: {
  id: uuid,
  title: string,
  description: string,
  cdnUrl: string,
  thumbnailUrl: string,
  duration: number,
  status: string,
  transcodedVersions: [{resolution, url}],
  metadata: {...}
}
```

#### 5. Delete Video
```typescript
DELETE /api/v1/education/videos/:id

Response: {
  message: 'Video deleted successfully'
}
```

#### 6. List Videos for Course
```typescript
GET /api/v1/education/courses/:courseId/videos

Response: {
  videos: [{id, title, duration, thumbnailUrl, status}]
}
```

**Archivos a crear:**
- `apps/backend/src/modules/education/controllers/video.controller.ts` (~400 líneas)
- `apps/backend/src/modules/education/services/video.service.ts` (~300 líneas)
- Actualizar `apps/backend/src/modules/education/education.routes.ts`

**Esfuerzo:** 15h

---

### ⚠️ ST4.3.4: Backend Video Processing Service (10h)

**Estado:** PENDIENTE

**Descripción:**
Servicio para transcoding de videos a múltiples resoluciones.

**Opciones:**

#### Opción 1: FFmpeg Local (desarrollo/MVP)
```typescript
// apps/backend/src/shared/services/video-processing.service.ts

class VideoProcessingService {
  // Transcode video to multiple resolutions
  async processVideo(inputKey: string, outputPrefix: string): Promise<TranscodedVersion[]> {
    const resolutions = ['1080p', '720p', '480p', '360p'];
    const results = [];

    for (const resolution of resolutions) {
      const outputKey = `${outputPrefix}/${resolution}.mp4`;
      await this.transcodeVideo(inputKey, outputKey, resolution);
      results.push({
        resolution,
        storageKey: outputKey,
        cdnUrl: storageService.getPublicUrl(outputKey),
      });
    }

    return results;
  }

  private async transcodeVideo(input, output, resolution) {
    // Use fluent-ffmpeg or spawn ffmpeg process
    // ffmpeg -i input.mp4 -vf scale=-2:1080 -c:v libx264 -preset fast output.mp4
  }

  // Generate thumbnail from video
  async generateThumbnail(inputKey: string): Promise<string> {
    // ffmpeg -i input.mp4 -ss 00:00:01 -vframes 1 thumbnail.jpg
  }

  // Extract metadata
  async extractMetadata(inputKey: string): Promise<VideoMetadata> {
    // ffmpeg -i input.mp4 (get duration, codec, bitrate, resolution, fps)
  }
}
```

#### Opción 2: AWS MediaConvert (producción)
```typescript
import { MediaConvert } from '@aws-sdk/client-mediaconvert';

async processVideo(inputKey: string): Promise<string> {
  const job = await mediaconvert.createJob({
    Role: config.aws.mediaConvertRole,
    Settings: {
      Inputs: [{
        FileInput: `s3://${bucket}/${inputKey}`,
      }],
      OutputGroups: [
        { Outputs: [{ VideoDescription: { Height: 1080 } }] },
        { Outputs: [{ VideoDescription: { Height: 720 } }] },
        { Outputs: [{ VideoDescription: { Height: 480 } }] },
      ],
    },
  });

  return job.Job.Id;
}
```

#### Opción 3: Cloudflare Stream (más simple)
```typescript
import { CloudflareStream } from '@cloudflare/stream';

// Cloudflare Stream handles transcoding automatically
async uploadToStream(inputKey: string): Promise<string> {
  const video = await stream.videos.upload({
    file: await storageService.getObject(inputKey),
    meta: { name: 'My video' },
  });

  // Cloudflare generates multiple resolutions automatically
  return video.playback.hls; // HLS stream URL
}
```

**Recomendación:** Opción 1 (FFmpeg) para MVP, Opción 3 (Cloudflare Stream) para producción (más simple que MediaConvert).

**Esfuerzo:** 10h

---

### ⚠️ ST4.3.5: Frontend Integrar VideoUploadForm (8h)

**Estado:** PENDIENTE

**Descripción:**
Reemplazar simulación de upload en VideoUploadForm con integración real al backend.

**Cambios necesarios en VideoUploadForm.tsx:**

1. **Crear helper service** (`apps/frontend/src/services/video-upload.service.ts`):
```typescript
export async function uploadVideo(
  file: File,
  metadata: VideoMetadata,
  courseId: string,
  lessonId?: string,
  onProgress?: (progress: number) => void
): Promise<string> {
  // 1. Initialize upload
  const initResponse = await apiClient.post('/education/videos/upload-init', {
    courseId,
    lessonId,
    filename: file.name,
    fileSize: file.size,
    contentType: file.type,
    metadata,
  });

  const { videoId, uploadId, presignedUrls } = initResponse.data;

  // 2. Split file into parts (5MB each)
  const partSize = 5 * 1024 * 1024;
  const parts = [];
  const totalParts = Math.ceil(file.size / partSize);

  for (let i = 0; i < totalParts; i++) {
    const start = i * partSize;
    const end = Math.min(start + partSize, file.size);
    const part = file.slice(start, end);

    // Upload part to presigned URL
    const response = await fetch(presignedUrls[i], {
      method: 'PUT',
      body: part,
    });

    parts.push({
      partNumber: i + 1,
      etag: response.headers.get('etag'),
    });

    // Report progress
    onProgress?.((i + 1) / totalParts * 100);
  }

  // 3. Complete upload
  await apiClient.post(`/education/videos/${videoId}/complete`, { parts });

  return videoId;
}
```

2. **Actualizar handleUpload en VideoUploadForm.tsx:**
```typescript
// REPLACE lines 214-248
const handleUpload = useCallback(async () => {
  if (!selectedFile || !validateMetadata()) return;

  setUploadProgress({ status: 'uploading', progress: 0 });

  try {
    const videoId = await uploadVideo(
      selectedFile,
      metadata,
      courseId,
      lessonId,
      (progress) => {
        setUploadProgress({
          status: 'uploading',
          progress: Math.floor(progress),
          message: `Uploading... ${Math.floor(progress)}%`,
        });
      }
    );

    setUploadProgress({
      status: 'processing',
      progress: 100,
      message: 'Processing video...',
    });

    // Poll for processing completion (optional)
    // await pollVideoStatus(videoId);

    setUploadProgress({
      status: 'completed',
      progress: 100,
      message: 'Upload complete!',
      videoId,
    });

    onUploadComplete?.(videoId, metadata);
  } catch (error) {
    setUploadProgress({
      status: 'error',
      progress: 0,
      message: error instanceof Error ? error.message : 'Upload failed',
    });
  }
}, [selectedFile, metadata, courseId, lessonId, onUploadComplete]);
```

**Esfuerzo:** 8h

---

### ⚠️ ST4.3.6: Documentación ET-EDU-008 (2h)

**Estado:** PENDIENTE

**Descripción:**
Crear especificación técnica completa documentando arquitectura de video upload.

**Contenido:**
- Arquitectura multipart upload
- Diagrama de flujo completo
- Storage strategy (S3/R2/Cloudflare Stream)
- Video processing pipeline
- CDN integration
- Security considerations
- Performance optimization
- Cost analysis
- Error handling
- Developer guidelines

**Ubicación:** `docs/02-definicion-modulos/OQI-002-educativo/especificaciones/ET-EDU-008-video-upload-architecture.md`

**Líneas estimadas:** ~600

**Esfuerzo:** 2h

---

## Progreso General ST4.3

| Subtarea | Esfuerzo | Estado | Progreso |
|----------|----------|--------|----------|
| ST4.3.1: Database tabla videos | 0.5h | ✅ Completado | 100% |
| ST4.3.2: Storage service (S3/R2) | 1.5h | ✅ Completado | 100% |
| ST4.3.3: Video controller & endpoints | 15h | ⚠️ Pendiente | 0% |
| ST4.3.4: Video processing service | 10h | ⚠️ Pendiente | 0% |
| ST4.3.5: Frontend integración | 8h | ⚠️ Pendiente | 0% |
| ST4.3.6: Documentación ET-EDU-008 | 2h | ⚠️ Pendiente | 0% |
| **TOTAL** | **37h** | **🔄 EN PROGRESO** | **33%** |

**Nota:** Esfuerzo estimado original era 60h, pero tras análisis es ~37h (38% reducción).

**Esfuerzo real hasta ahora:** 2h

---

## Commits

**Database submodule:**
- `3f7816d` - feat(database): Add videos table for education module (ST4.3.1)

**Backend submodule:**
- `d7abb53` - feat(storage): Add S3/R2 storage service with multipart upload (ST4.3.2)

---

## Arquitectura Propuesta

```
┌────────────────────────────────────────────────────────────────┐
│                   VIDEO UPLOAD ARCHITECTURE                     │
└────────────────────────────────────────────────────────────────┘

1. UPLOAD FLOW:
   User → VideoUploadForm
          ↓
   POST /videos/upload-init → Backend creates DB record + uploadId
          ↓
   Backend → Storage Service → S3/R2 (initMultipartUpload)
          ↓
   Backend returns presigned URLs for each part
          ↓
   Frontend uploads parts directly to S3/R2 (5MB each)
          ↓
   POST /videos/:id/complete → Backend completes multipart upload
          ↓
   Storage Service → S3/R2 (completeMultipartUpload)
          ↓
   Backend → Trigger video processing (background job)

2. PROCESSING FLOW:
   Background Job → Video Processing Service
          ↓
   Download from S3/R2
          ↓
   FFmpeg transcode to 1080p, 720p, 480p, 360p
          ↓
   Generate thumbnail (frame at 1 second)
          ↓
   Extract metadata (duration, codec, bitrate, resolution)
          ↓
   Upload transcoded versions to S3/R2
          ↓
   Update DB: status='ready', cdn_urls, transcoded_versions

3. PLAYBACK FLOW:
   User → Video Player
          ↓
   GET /videos/:id → Backend returns video details
          ↓
   Frontend → CDN URL (CloudFront/Cloudflare)
          ↓
   Adaptive bitrate selection based on network
```

---

## Métricas

- **Líneas código:** ~600 (DDL 150 + Storage Service 451)
- **Archivos creados:** 2
- **Commits:** 2
- **Tiempo invertido:** 2h
- **Progreso:** 33%

---

## Recomendación

### Opción 1: Completar ST4.3 antes de ST4.4 (RECOMENDADA)

**Justificación:**
- ST4.3 está 33% completo (base sólida)
- Frontend ya existe (solo necesita integración)
- Storage service ya funcional
- ~35h restantes para completar

**Siguiente paso:**
1. ✅ Implementar ST4.3.3: Video controller (15h)
2. ✅ Implementar ST4.3.4: Video processing (10h)
3. ✅ Implementar ST4.3.5: Frontend integration (8h)
4. ✅ Crear ST4.3.6: Documentación (2h)

**Total:** ~35h adicionales

---

### Opción 2: MVP con upload simple (sin transcoding)

**Justificación:**
- Desbloquear funcionalidad básica más rápido
- Transcoding puede añadirse después

**Scope reducido:**
1. ✅ ST4.3.3: Video controller (solo upload, sin processing) - 8h
2. ✅ ST4.3.5: Frontend integration - 8h
3. ⚠️ ST4.3.4: Processing (SKIP por ahora)

**Total:** ~16h adicionales

**Trade-off:** Videos se almacenan en resolución original únicamente.

---

### Opción 3: Usar Cloudflare Stream (más simple)

**Justificación:**
- Cloudflare Stream maneja transcoding automáticamente
- Más caro pero menos código

**Cambios:**
- Replace Storage Service multipart con Cloudflare Stream API
- ST4.3.4 no necesario (Cloudflare hace transcoding)

**Total:** ~20h adicionales

---

## Conclusión

**Estado:** ✅ **Base implementada (DDL + Storage)**

**Blocker BLOCKER-003 Status:** 🔄 **EN PROGRESO** (33%)

**Trabajo pendiente:** ~35h (opción 1) o ~16h (opción 2 MVP)

---

**Última actualización:** 2026-01-26
**Autor:** Claude Opus 4.5
**Blocker:** BLOCKER-003 (ST4.3)
