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
npm install @uppy/core @uppy/aws-s3Configuració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 VideoUploaderAvanzado: 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)
})