The ApsaraVideo VOD JavaScript SDK (aliyun-vod-upload-sdk) uploads video and image files from a browser to ApsaraVideo VOD storage. The SDK handles credential retrieval, multipart upload, resumable upload, and progress tracking.
Browser requirements
| Browser | Minimum version |
| Chrome | 60+ |
| Microsoft Edge | 79+ (Chromium) |
| Firefox | 55+ |
| Safari | 11+ |
| Android default browser | 60+ |
| iOS default browser | 11+ |
Quick start
You can download the Demo source code for a complete working example.
Installation
Install the SDK by using npm:
npm install aliyun-vod-upload-sdkThe SDK provides ESM, CJS, and UMD builds and supports all modern bundlers (Vite, webpack, Rollup, and esbuild).
CDN import
To use the SDK without a bundler, import it from a CDN:
<script src="https://g.alicdn.com/apsara-media-box/imp-web-vod-upload/2.0.0/vod-upload.umd.js"></script>
<script>
const { createUploader, UploadError } = window.VodUpload;
</script>Backend API requirements
The SDK does not directly call Alibaba Cloud OpenAPI. Instead, it delegates to your backend through the getAuth callback to obtain upload credentials. Your backend must implement the following APIs, which correspond to VOD OpenAPI operations:
| Backend API | Corresponding OpenAPI operation | Response fields |
| Create video upload credential | CreateUploadVideo | { UploadAuth, UploadAddress, VideoId } |
| Refresh video upload credential | RefreshUploadVideo | { UploadAuth, UploadAddress, VideoId } |
| Create image upload credential | CreateUploadImage | { UploadAuth, UploadAddress, ImageId } |
The backend response JSON is passed as-is to the SDK. The SDK automatically handles base64 decoding and field mapping.
Upload a file in five lines of code
The following example shows the minimal code to upload a file:
import { createUploader } from 'aliyun-vod-upload-sdk';
const uploader = createUploader({
getAuth: () => fetch('/api/vod/auth').then(r => r.json()),
});
// file is a File object, for example from an <input type="file"> element
const { videoId } = await uploader.upload(file);Basic features
Upload videos
The following example creates an uploader with a getAuth callback that handles both initial credential creation and credential refresh:
import { createUploader } from 'aliyun-vod-upload-sdk';
const uploader = createUploader({
getAuth: async (ctx) => {
switch (ctx.kind) {
case 'create-video':
return fetch('/api/vod/create-auth', {
method: 'POST',
body: JSON.stringify({ fileName: ctx.file.name, ...ctx.meta }),
}).then(r => r.json());
case 'refresh-video':
return fetch(`/api/vod/refresh-auth?videoId=${ctx.videoId}`)
.then(r => r.json());
}
},
});
const result = await uploader.upload(file, {
meta: { title: 'My Video', cateId: 1000, tags: 'tutorial,example' },
});
console.log('Upload successful, videoId:', result.videoId);The getAuth callback receives a ctx parameter. The SDK automatically determines ctx.kind based on the scenario:
create-video— First upload. The backend must call CreateUploadVideo.refresh-video— Credential refresh during resumable upload. The backend must call RefreshUploadVideo.create-image— Image upload. The backend must call CreateUploadImage.
Upload images
Image upload uses the same upload() method. The SDK automatically detects the file type based on file.type (image/* types use the image upload path):
const uploader = createUploader({
getAuth: async (ctx) => {
if (ctx.kind === 'create-image') {
return fetch('/api/vod/image-auth', {
method: 'POST',
body: JSON.stringify({ imageType: 'default' }),
}).then(r => r.json());
}
},
});
const result = await uploader.upload(imageFile);
console.log('Image upload successful, imageId:', result.imageId);Track upload progress
Use the onProgress callback to track upload progress:
const result = await uploader.upload(file, {
onProgress: (percent, loaded, total) => {
// percent: 0..1 (e.g. 0.5 means 50%)
// loaded: bytes uploaded
// total: total file size in bytes
progressBar.value = percent;
console.log(`${(percent * 100).toFixed(1)}% - ${loaded} / ${total}`);
},
});The SDK guarantees that percent equals 1 when the upload is complete.
Cancel an upload
You can cancel an upload by using either the task.abort() method or an AbortSignal.
Method 1: task.abort() — Simplest approach
const task = uploader.upload(file);
cancelBtn.onclick = () => task.abort();
try {
const result = await task;
} catch (e) {
if (e.name === 'AbortError') {
console.log('Upload canceled by user');
}
}Method 2: AbortSignal — Suitable for React/Vue component lifecycles
const ctrl = new AbortController();
cancelBtn.onclick = () => ctrl.abort();
try {
const result = await uploader.upload(file, { signal: ctrl.signal });
} catch (e) {
if (e.name === 'AbortError') {
console.log('Upload canceled');
}
}After cancellation, the checkpoint is preserved. The next call to upload(file) automatically resumes from the breakpoint.
Resumable upload
Resumable upload is enabled by default and requires no additional configuration. This feature is supported only for video uploads. Image uploads do not use resumable upload. The SDK automatically:
Stores the progress of completed parts in
localStorage.Resumes from where the upload was interrupted when
upload(file)is called again after a page refresh.Matches checkpoints by using the rule: same file.name + file.size + file.lastModified.
// First upload (user closes the page at 40% progress)
await uploader.upload(file);
// User reopens the page and uploads the same file again
// The SDK automatically resumes from 40% without re-uploading
const result = await uploader.upload(file);To disable resumable upload:
const uploader = createUploader({
getAuth: myGetAuth,
checkpoint: false, // Disable resumable upload
});Advanced features
Batch upload
The SDK provides a single-file upload() API. For batch uploads, use standard JavaScript async primitives.
Serial upload (simplest)
for (const file of files) {
const result = await uploader.upload(file, {
onProgress: p => updateProgress(file.name, p),
});
console.log(`${file.name} complete, videoId: ${result.videoId}`);
}Concurrent upload (high bandwidth and small files)
const results = await Promise.all(
files.map(f => uploader.upload(f)),
);(Recommended) Concurrency-limited upload (large files)
import pLimit from 'p-limit';
const limit = pLimit(2); // Upload at most 2 files simultaneously
const results = await Promise.all(
files.map(f => limit(() => uploader.upload(f, {
onProgress: p => updateItemProgress(f, p),
}))),
);Pause and resume
The SDK does not have an explicit pause API. Achieve the pause effect through cancel + resume:
// Pause: cancel the current upload
task.abort();
// Resume: re-upload the same file, automatically resumes from checkpoint
const result = await uploader.upload(file);Timeout and multi-source cancellation
Use AbortSignal.timeout() and AbortSignal.any() to implement timeout-based and combined cancellation:
// Auto-cancel after 60 seconds
await uploader.upload(file, {
signal: AbortSignal.timeout(60_000),
});
// Multi-source cancellation: user manual cancel OR 60-second timeout
const userCtrl = new AbortController();
await uploader.upload(file, {
signal: AbortSignal.any([
userCtrl.signal,
AbortSignal.timeout(60_000),
]),
});React and Vue integration
React
React Hook example
import { useEffect, useState, useRef } from 'react';
import { createUploader, UploadError } from 'aliyun-vod-upload-sdk';
function useUploader(getAuth) {
const uploaderRef = useRef(createUploader({ getAuth }));
useEffect(() => {
return () => uploaderRef.current.dispose(); // Release on unmount
}, []);
return uploaderRef.current;
}
function UploadButton({ file }) {
const uploader = useUploader(myGetAuth);
const [progress, setProgress] = useState(0);
const handleUpload = () => {
const ctrl = new AbortController();
uploader.upload(file, {
signal: ctrl.signal,
onProgress: p => setProgress(p),
}).then(result => {
console.log('Success', result.videoId);
}).catch(e => {
if (e.name !== 'AbortError') {
console.error('Failed', e);
}
});
};
return <button onClick={handleUpload}>Upload ({(progress * 100).toFixed(0)}%)</button>;
}Vue 3
Vue 3 Composable example
import { onUnmounted, ref } from 'vue';
import { createUploader } from 'aliyun-vod-upload-sdk';
export function useUploader(getAuth) {
const uploader = createUploader({ getAuth });
const progress = ref(0);
onUnmounted(() => uploader.dispose());
async function upload(file) {
return uploader.upload(file, {
onProgress: p => { progress.value = p; },
});
}
return { upload, progress };
}API reference
createUploader(config)
Creates an Uploader instance.
import { createUploader } from 'aliyun-vod-upload-sdk';
const uploader = createUploader(config);UploaderConfig
| Parameter | Type | Required | Default | Description |
| getAuth | GetAuth | Yes | — | Credential retrieval callback. For more information, see getAuth callback. |
| retry | RetryPolicy | No | { count: 3 } | Automatic retry policy for OSS multipart upload failures. |
| checkpoint | false | { store?: CheckpointStore } | No | localStorage | Resumable upload configuration. Set to false to disable. Set to { store } to inject a custom store. |
| parallel | number | No | 4 | Number of concurrent OSS parts. |
| partSize | number | No | 1048576 (1 MB) | OSS part size in bytes. |
| timeout | number | No | 60000 | Network request timeout in milliseconds. |
| cname | string | No | — | Custom OSS domain name. |
| refreshSTSTokenInterval | number | No | 300000 (5 min) | STS credential refresh check interval in milliseconds. |
RetryPolicy
| Field | Type | Default | Description |
| count | number | 3 | Maximum number of retries. |
uploader.upload(file, options?)
Uploads a single file. Returns an UploadTask, which is both a Promise<UploadResult> and an object with an .abort() method.
const task = uploader.upload(file, options);UploadOptions
| Parameter | Type | Description |
| meta | VodVideoMeta | VodImageMeta | Media metadata (title, category, tags, and more). |
| signal | AbortSignal | Web standard cancellation signal. |
| onProgress | (percent, loaded, total) => void | Progress callback. The percent value ranges from 0 to 1. |
| partSize | number | Overrides the global partSize. |
| parallel | number | Overrides the global parallel. |
VodVideoMeta (video metadata)
| Field | Type | Description |
| title | string | Video title. |
| description | string | Video description. |
| cateId | number | Category ID. |
| tags | string | Tags, separated by commas. |
| templateGroupId | string | Transcoding template group ID. |
| storageLocation | string | Storage address. |
| coverUrl | string | Cover image URL. |
| workflowId | string | Workflow ID. |
| appId | string | Application ID. |
| userData | Record | Custom data. |
VodImageMeta (image metadata)
| Field | Type | Description |
| title | string | Image title. |
| description | string | Image description. |
| imageType | 'default' | 'cover' | 'watermark' | Image type. |
| imageExt | string | Image file extension. |
| tags | string | Tags. |
| cateId | number | Category ID. |
| storageLocation | string | Storage address. |
UploadResult
| Field | Type | Description |
| videoId | string? | Video ID. Returned for video uploads. |
| imageId | string? | Image ID. Returned for image uploads. |
| etag | string | OSS ETag. |
| requestId | string | OSS request ID. |
| durationMs | number | Upload duration in milliseconds. |
| uploadTaskId | string | Unique upload task ID. Can be used for troubleshooting with technical support. |
UploadTask
The object returned by upload() is both a Promise<UploadResult> and an object with an .abort() method.
type UploadTask = Promise<UploadResult> & {
abort(): void;
};The .abort() method is lost after chaining. task.then(fn) returns a regular Promise. To retain the ability to cancel after chaining, use options.signal.
uploader.dispose()
Releases resources: cancels all in-progress uploads and clears internal state.
uploader.dispose();After this call, the uploader instance cannot be used again. Call this method when a React or Vue component unmounts to prevent memory leaks.
getAuth callback
type GetAuth = (ctx: AuthContext) => Promise<AuthResult>;AuthContext
The SDK passes different ctx values based on the upload scenario:
| ctx.kind | Trigger scenario | Additional ctx fields |
| 'create-video' | First video upload | file, meta? |
| 'refresh-video' | Credential refresh during resumable upload | file, videoId |
| 'create-image' | Image upload | file, meta? |
AuthResult
The raw JSON returned by the backend from the OpenAPI response. The SDK automatically decodes it:
| Field | Type | Required | Description |
| UploadAuth | string | Yes | Base64-encoded STS credential. |
| UploadAddress | string | Yes | Base64-encoded OSS upload address. |
| VideoId | string | Required for create-video | Video ID. |
| ImageId | string | Required for create-image | Image ID. |
| ImageURL | string | No | Image URL (passed through). |
Simplest getAuth implementation:
// Unified backend route
createUploader({
getAuth: ctx => fetch('/api/vod/auth', {
method: 'POST',
body: JSON.stringify(ctx),
}).then(r => r.json()),
});Automatic fallback on refresh-video failure: When credential refresh during resumable upload fails, the SDK automatically falls back to create-video to re-upload the file. This is fully transparent to the caller.
Error handling
UploadError structure
All upload errors are UploadError instances (extends Error):
import { UploadError } from 'aliyun-vod-upload-sdk';
class UploadError extends Error {
readonly code: string; // Structured error code, e.g. 'UPLOAD.OSS.ACCESS_DENIED'
readonly message: string; // Error description
readonly suggestion: string; // Fix suggestion
readonly cause?: unknown; // Original underlying error
readonly uploadTaskId?: string; // Upload task ID (for troubleshooting)
}User-initiated upload cancellation is not an UploadError. It is a standard DOMException (name === 'AbortError').
Error codes
Error code format: UPLOAD.{layer}.{type}
| Error code | Description | Suggested fix |
| UPLOAD.AUTH.GET_AUTH_FAILED | The getAuth callback threw an error or returned an invalid structure. | Check whether the getAuth callback correctly returns JSON that contains the UploadAuth and UploadAddress fields. Make sure the backend service is available and the response format is correct. |
| UPLOAD.OSS.ACCESS_DENIED | OSS permission error (403). | Check whether the upload credential (STS Token) is valid and has not expired, and whether the RAM policy grants OSS write permissions. |
| UPLOAD.OSS.NO_SUCH_BUCKET | The bucket does not exist. | Check whether the bucket name in the upload address is correct, and make sure the bucket has been created in the corresponding region. |
| UPLOAD.OSS.NO_SUCH_UPLOAD | The multipart upload session has expired. | The multipart upload session has expired (possibly after more than 24 hours). The SDK automatically re-uploads the file. No manual action is required. |
| UPLOAD.OSS.UNKNOWN | Unknown OSS error. | Check the browser console for detailed error information, or contact technical support. |
| UPLOAD.NETWORK.TIMEOUT | Request timed out. | Open the Network panel in the browser developer tools to view the details of the failed request. Check whether the network connection is stable, or try increasing the timeout configuration value. |
| UPLOAD.NETWORK.ERROR | Network connection error. | Open the Network panel in the browser developer tools to view the status code and response of the failed request. Make sure the browser can access the OSS service address. Check whether a proxy or firewall is blocking the request. |
| UPLOAD.NETWORK.OFFLINE | The browser is offline. | Check the network status and retry the upload after reconnecting. |
| UPLOAD.FILE.EMPTY | The file size is 0. | The file size is 0. Empty files cannot be uploaded. Check whether the correct file is selected. |
| UPLOAD.INTERNAL.DISPOSED | The Uploader instance has been disposed. | Call createUploader() again to create a new instance. |
Use error code constants
(Recommended) Use ErrorCode constants instead of string literals:
import { ErrorCode, UploadError } from 'aliyun-vod-upload-sdk';
try {
await uploader.upload(file);
} catch (e) {
if (e instanceof UploadError) {
switch (e.code) {
case ErrorCode.AUTH_GET_AUTH_FAILED:
showToast('Failed to obtain credentials. Refresh the page and try again.');
break;
case ErrorCode.NETWORK_TIMEOUT:
case ErrorCode.NETWORK_ERROR:
showToast('Network error. Check your network and try again.');
break;
case ErrorCode.OSS_ACCESS_DENIED:
showToast('Insufficient upload permissions. Contact the administrator.');
break;
default:
showToast(`Upload failed: ${e.suggestion || e.message}`);
}
}
}Troubleshooting
Each UploadError contains an uploadTaskId, which is a global trace ID for the entire upload. You can provide this ID to Alibaba Cloud technical support for quick issue identification:
catch (e) {
if (e instanceof UploadError) {
// Send to your technical support team
const diagnostic = {
code: e.code,
message: e.message,
uploadTaskId: e.uploadTaskId,
suggestion: e.suggestion,
};
reportToSupport(diagnostic);
}
}The UploadResult.uploadTaskId can also be used for log correlation after a successful upload:
const result = await uploader.upload(file);
myLogger.info('Upload successful', { videoId: result.videoId, uploadTaskId: result.uploadTaskId });