Summary: - ST4.3.1: ✅ Created videos table (DDL) - ST4.3.2: ✅ Created storage service (S3/R2 with multipart upload) - ST4.3.3-6: ⚠️ Pending (controller, processing, frontend, docs) Key findings: - Frontend VideoUploadForm already exists (simulated) - Backend needs full implementation - Storage service supports S3, R2, multipart upload, presigned URLs - DDL table has full support for transcoding, metadata, soft delete Pending work: ~35h (video controller 15h + processing 10h + frontend 8h + docs 2h) Options: 1. Complete ST4.3 fully (35h) 2. MVP without transcoding (16h) 3. Use Cloudflare Stream (20h) Recommendation: Option 1 (complete implementation) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
19 KiB
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):
// 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.videoscompleta (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:
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)
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)
// 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)
// 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
// 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
// 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:
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
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)
PUT /api/v1/education/videos/:videoId/parts/:partNumber
Body: <binary data>
Response: {
etag: string,
partNumber: number
}
3. Complete Upload
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
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
DELETE /api/v1/education/videos/:id
Response: {
message: 'Video deleted successfully'
}
6. List Videos for Course
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)
// 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)
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)
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:
- Crear helper service (
apps/frontend/src/services/video-upload.service.ts):
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;
}
- Actualizar handleUpload en VideoUploadForm.tsx:
// 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:
- ✅ Implementar ST4.3.3: Video controller (15h)
- ✅ Implementar ST4.3.4: Video processing (10h)
- ✅ Implementar ST4.3.5: Frontend integration (8h)
- ✅ 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:
- ✅ ST4.3.3: Video controller (solo upload, sin processing) - 8h
- ✅ ST4.3.5: Frontend integration - 8h
- ⚠️ 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)