Skip to Content

Uploading with Uppy

Uppy  is a popular open-source file uploader that handles chunking, retries, and progress tracking automatically. This guide shows how to integrate Uppy with the Ignite Video API for both single-file and multipart uploads.

Uppy automatically handles multipart uploads for large files, making it the recommended approach for production applications.

For a manual implementation without external dependencies, see the Multipart Upload guide.

Installation

Install the required Uppy packages:

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

Basic Setup

Configure for single-file uploads

For files under 100MB, use direct upload with a pre-signed URL.

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 }

Configure for multipart uploads

For files over 100MB, use multipart upload for better reliability.

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 }

Complete Example with Progress Tracking

Here’s a complete implementation that automatically chooses between single-file and multipart upload based on file size:

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 }

React Integration

Here’s an example React component using 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

Advanced: Batch URL Fetching

For better performance with large files, fetch signed URLs in batches instead of one at a time. This reduces the number of API calls significantly for large uploads.

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 }

Error Handling and Retries

Uppy includes built-in retry logic. Configure retry behavior:

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

For custom error handling:

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) })