Uploading with Uppy
Uppy (opens in a new tab) 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-s3Basic 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 VideoUploaderAdvanced: 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)
})