Skip to Content

Subida con Uppy

Uppy  es un cargador de archivos de código abierto muy usado que gestiona el troceado, los reintentos y el seguimiento del progreso automáticamente. Esta guía muestra cómo integrar Uppy con la API de Ignite Video para subidas de un solo archivo y multiparte.

Uppy gestiona automáticamente las subidas multiparte para archivos grandes, por eso es el enfoque recomendado para aplicaciones en producción.

Para una implementación manual sin dependencias externas, consulta la guía Subida multiparte.

Instalación

Instala los paquetes de Uppy necesarios:

npm install @uppy/core @uppy/aws-s3

Configuración básica

Configurar subidas de un solo archivo

Para archivos de menos de 100 MB, usa subida directa con una URL pre-firmada.

import Uppy from '@uppy/core' import AwsS3 from '@uppy/aws-s3' async function uploadVideo(file, title, token) { const uppy = new Uppy() // Step 1: Create video and get signed URL const response = await fetch('https://app.ignitevideo.cloud/api/videos/upload', { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ title, mimeType: file.type }) }) const { videoId, signedUrl } = await response.json() // Step 2: Configure Uppy for direct upload uppy.use(AwsS3, { shouldUseMultipart: false, getUploadParameters: () => ({ method: 'PUT', url: signedUrl, headers: { 'Content-Type': file.type } }) }) // Step 3: Add file and upload uppy.addFile({ name: file.name, type: file.type, data: file }) await uppy.upload() // Clean up uppy.destroy() return videoId }

Configurar subidas multiparte

Para archivos de más de 100 MB, usa subida multiparte para mayor fiabilidad.

import Uppy from '@uppy/core' import AwsS3 from '@uppy/aws-s3' async function uploadLargeVideo(file, title, token) { const uppy = new Uppy() // Step 1: Create video with multipart flag const response = await fetch('https://app.ignitevideo.cloud/api/videos/upload', { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ title, mimeType: file.type, useMultipart: true }) }) const { videoId, multipartUpload } = await response.json() const { uploadId, key } = multipartUpload // Step 2: Configure Uppy for multipart upload uppy.use(AwsS3, { shouldUseMultipart: true, limit: 5, // Max concurrent part uploads retryDelays: [0, 1000, 3000, 5000], // Retry delays in ms // Return the existing multipart upload info createMultipartUpload: () => Promise.resolve({ uploadId, key }), // Get signed URL for each part signPart: async (file, { partNumber }) => { const response = await fetch( `https://app.ignitevideo.cloud/api/videos/upload/s3/multipart/${uploadId}/${partNumber}?key=${encodeURIComponent(key)}`, { headers: { 'Authorization': `Bearer ${token}` } } ) const data = await response.json() return { url: data.url } }, // Complete the multipart upload completeMultipartUpload: async (file, { uploadId, key, parts }) => { const response = await fetch( `https://app.ignitevideo.cloud/api/videos/upload/s3/multipart/${uploadId}/complete?key=${encodeURIComponent(key)}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ parts }) } ) return response.json() } }) // Step 3: Add file and upload uppy.addFile({ name: file.name, type: file.type, data: file }) await uppy.upload() // Clean up uppy.destroy() return videoId }

Ejemplo completo con seguimiento del progreso

Aquí tienes una implementación completa que elige automáticamente entre subida de un solo archivo y multiparte según el tamaño del archivo:

import Uppy from '@uppy/core' import AwsS3 from '@uppy/aws-s3' const API_BASE = 'https://app.ignitevideo.cloud/api' const MULTIPART_THRESHOLD = 100 * 1024 * 1024 // 100MB export async function uploadVideo(file, title, token, onProgress) { const useMultipart = file.size > MULTIPART_THRESHOLD // Step 1: Initialize video on server const initResponse = await fetch(`${API_BASE}/videos/upload`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ title, mimeType: file.type, useMultipart }) }) if (!initResponse.ok) { throw new Error('Failed to initialize upload') } const initData = await initResponse.json() const { videoId } = initData // Step 2: Create and configure Uppy const uppy = new Uppy({ restrictions: { maxFileSize: 60 * 1024 * 1024 * 1024, allowedFileTypes: ['video/*'] } }) if (!useMultipart) { // Single file upload uppy.use(AwsS3, { shouldUseMultipart: false, getUploadParameters: () => ({ method: 'PUT', url: initData.signedUrl, headers: { 'Content-Type': file.type } }) }) } else { // Multipart upload const { uploadId, key } = initData.multipartUpload uppy.use(AwsS3, { shouldUseMultipart: true, limit: 5, retryDelays: [0, 1000, 3000, 5000], createMultipartUpload: () => Promise.resolve({ uploadId, key }), signPart: async (file, { partNumber }) => { const response = await fetch( `${API_BASE}/videos/upload/s3/multipart/${uploadId}/${partNumber}?key=${encodeURIComponent(key)}`, { headers: { 'Authorization': `Bearer ${token}` } } ) if (!response.ok) throw new Error(`Failed to sign part ${partNumber}`) const data = await response.json() return { url: data.url } }, completeMultipartUpload: async (file, { uploadId, key, parts }) => { const response = await fetch( `${API_BASE}/videos/upload/s3/multipart/${uploadId}/complete?key=${encodeURIComponent(key)}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ parts }) } ) if (!response.ok) throw new Error('Failed to complete multipart upload') return response.json() } }) } // Step 3: Set up event handlers uppy.on('upload-progress', (file, progress) => { const percent = (progress.bytesUploaded / progress.bytesTotal) * 100 onProgress?.(percent) }) uppy.on('upload-error', (file, error) => { console.error('Upload error:', error) }) // Step 4: Add file and upload uppy.addFile({ name: file.name, type: file.type, data: file }) await uppy.upload() // Clean up uppy.destroy() return videoId }

Integración con React

Ejemplo de componente React usando Uppy:

import React, { useState, useCallback } from 'react' import { uploadVideo } from './uploadVideo' // The function from above function VideoUploader({ token }) { const [progress, setProgress] = useState(0) const [status, setStatus] = useState('idle') // idle, uploading, complete, error const [videoId, setVideoId] = useState(null) const handleFileChange = useCallback(async (event) => { const file = event.target.files?.[0] if (!file) return const title = file.name.replace(/\.[^/.]+$/, '') // Remove extension setStatus('uploading') setProgress(0) try { const id = await uploadVideo(file, title, token, (percent) => { setProgress(Math.round(percent)) }) setVideoId(id) setStatus('complete') } catch (error) { console.error('Upload failed:', error) setStatus('error') } }, [token]) return ( <div> <input type="file" accept="video/*" onChange={handleFileChange} disabled={status === 'uploading'} /> {status === 'uploading' && ( <div> <progress value={progress} max="100" /> <span>{progress}%</span> </div> )} {status === 'complete' && ( <p>Upload complete! Video ID: {videoId}</p> )} {status === 'error' && ( <p>Upload failed. Please try again.</p> )} </div> ) } export default VideoUploader

Avanzado: obtención de URLs por lotes

Para mejor rendimiento con archivos grandes, obtén las URLs firmadas por lotes en lugar de una a una. Así reduces mucho el número de llamadas a la API en subidas grandes.

import Uppy from '@uppy/core' import AwsS3 from '@uppy/aws-s3' const API_BASE = 'https://app.ignitevideo.cloud/api' const BATCH_SIZE = 100 async function uploadWithBatchFetching(file, title, token) { // Initialize upload const initResponse = await fetch(`${API_BASE}/videos/upload`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ title, mimeType: file.type, useMultipart: true }) }) const { videoId, multipartUpload } = await initResponse.json() const { uploadId, key } = multipartUpload // Batch fetching state const urlCache = new Map() const batchPromises = new Map() const getBatchNumber = (partNumber) => Math.floor((partNumber - 1) / BATCH_SIZE) const fetchBatch = async (batchNumber) => { const startPart = batchNumber * BATCH_SIZE + 1 const endPart = startPart + BATCH_SIZE - 1 const response = await fetch( `${API_BASE}/videos/upload/s3/multipart/${uploadId}/prepare-parts?key=${encodeURIComponent(key)}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ startPart, endPart }) } ) const data = await response.json() for (const part of data.parts) { urlCache.set(part.partNumber, part.url) } } const ensureBatchLoaded = async (partNumber) => { const batchNumber = getBatchNumber(partNumber) // Start fetching this batch if not already in progress if (!batchPromises.has(batchNumber)) { batchPromises.set(batchNumber, fetchBatch(batchNumber)) } await batchPromises.get(batchNumber) // Look-ahead: pre-fetch next batch when past 50% of current batch const positionInBatch = (partNumber - 1) % BATCH_SIZE if (positionInBatch > BATCH_SIZE / 2) { const nextBatch = batchNumber + 1 if (!batchPromises.has(nextBatch)) { batchPromises.set(nextBatch, fetchBatch(nextBatch).catch(() => { batchPromises.delete(nextBatch) })) } } } // Configure Uppy const uppy = new Uppy() uppy.use(AwsS3, { shouldUseMultipart: true, limit: 5, retryDelays: [0, 1000, 3000, 5000], createMultipartUpload: () => { // Pre-fetch first batch of URLs immediately ensureBatchLoaded(1).catch(console.error) return Promise.resolve({ uploadId, key }) }, signPart: async (file, { partNumber }) => { // Check cache first let url = urlCache.get(partNumber) if (url) return { url } // Ensure batch is loaded await ensureBatchLoaded(partNumber) url = urlCache.get(partNumber) if (url) return { url } // Fallback: fetch single part const response = await fetch( `${API_BASE}/videos/upload/s3/multipart/${uploadId}/${partNumber}?key=${encodeURIComponent(key)}`, { headers: { 'Authorization': `Bearer ${token}` } } ) const data = await response.json() return { url: data.url } }, completeMultipartUpload: async (file, { uploadId, key, parts }) => { // For large uploads, let server fetch parts info const useFetchFromStorage = parts.length > 100 const response = await fetch( `${API_BASE}/videos/upload/s3/multipart/${uploadId}/complete?key=${encodeURIComponent(key)}`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify( useFetchFromStorage ? { fetchPartsFromStorage: true } : { parts } ) } ) return response.json() } }) uppy.addFile({ name: file.name, type: file.type, data: file }) await uppy.upload() uppy.destroy() return videoId }

Manejo de errores y reintentos

Uppy incluye lógica de reintegro integrada. Configura el comportamiento así:

uppy.use(AwsS3, { shouldUseMultipart: true, retryDelays: [0, 1000, 3000, 5000], // Retry delays in ms limit: 5, // Max concurrent uploads // ... other options })

Para un manejo de errores personalizado:

uppy.on('upload-error', (file, error, response) => { console.error('Upload failed for file:', file.name) console.error('Error:', error) // You can retry the upload // uppy.retryUpload(file.id) }) uppy.on('error', (error) => { console.error('Uppy error:', error) })