This feature is still in our final beta round. If you’d like to get access to it as a beta tester, please send us an email at offlinemode@despia.com
Cache remote files locally for offline access. Background downloads continue when users close the app.
Local CDN uses native OS background transfer APIs (NSURLSession on iOS, WorkManager on Android) with built-in retry. Start a download, close the app, get notified when ready.
Installation
npm install despia-native
import despia from 'despia-native' ;
API Reference
Write
Download and cache a remote file. Fire-and-forget - do not await.
// Correct: fire-and-forget, no await, no second argument
const remoteUrl = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" ;
const folder = "videos" ;
const subfolder = "movies" ;
const filename = "bigbuckbunny.mp4" ;
const uniqueId = "movie_bigbuckbunny" ;
despia (
`localcdn://write?url= ${ remoteUrl } &filename= ${ folder } / ${ subfolder } / ${ filename } &index= ${ uniqueId } `
);
despia (
`localcdn://write?url= ${ url } &filename= ${ path } &index= ${ id } &push=true&pushmessage=" ${ message } "`
);
Do not await the write call with a key. The JS bridge has a ~30s timeout. Large files will cause it to resolve with null even though the download continues silently. Use contentServerChange callback instead.
// WRONG: will timeout on large files
const data = await despia (
`localcdn://write?url= ${ url } &filename= ${ path } &index= ${ id } ` ,
[ id ] // Bridge times out after ~30s
);
Remote file URL to download
Local path: folder/subfolder/filename
Set to true to show push notification on completion
Notification message (wrap in quotes)
If you need more control, you can poll via localcdn://read to check download status instead of relying solely on the callback.
Read
Get metadata for specific cached files by ID.
const data = await despia (
`localcdn://read?index= ${ encodeURIComponent ( JSON . stringify ([ "video_bigbunny" , "video_sintel" ])) } ` ,
[ "cdnItems" ]
);
const items = data . cdnItems ; // Array of file objects
items . forEach ( item => console . log ( item . index , item . local_cdn ));
Array of cached file objects
[
{
"index_full" : "videos/movies/bigbuckbunny.mp4" ,
"index" : "movie_bigbuckbunny" ,
"extension" : "mp4" ,
"local_path" : "/var/mobile/.../localcdn/videos/movies/bigbuckbunny.mp4" ,
"local_cdn" : "http://localhost:7777/localcdn/videos/movies/bigbuckbunny.mp4" ,
"cdn" : "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" ,
"size" : "158008374" ,
"status" : "cached" ,
"created_at" : "1709856000"
}
]
// Poll to check if download completed (alternative to callback)
async function checkDownloadStatus ( indexId ) {
const data = await despia (
`localcdn://read?index= ${ encodeURIComponent ( JSON . stringify ([ indexId ])) } ` ,
[ "cdnItems" ]
);
return data . cdnItems ?.[ 0 ]?. status === "cached" ;
}
Query
Return all cached files. Use this to get a full inventory without specifying individual IDs.
const data = await despia (
`localcdn://query` ,
[ "cdnItems" ]
);
const items = data . cdnItems ; // Array of all cached file objects
items . forEach ( item => console . log ( item . index , item . local_cdn ));
Array of all cached file objects across all folders
[
{
"index_full" : "videos/movies/bigbuckbunny.mp4" ,
"index" : "movie_bigbuckbunny" ,
"extension" : "mp4" ,
"local_path" : "/var/mobile/.../localcdn/videos/movies/bigbuckbunny.mp4" ,
"local_cdn" : "http://localhost:7777/localcdn/videos/movies/bigbuckbunny.mp4" ,
"cdn" : "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" ,
"size" : "158008374" ,
"status" : "cached" ,
"created_at" : "1709856000"
}
]
localcdn://query returns every item in the cache. To fetch specific files by ID, use localcdn://read instead.
Delete
Remove cached files.
despia ( `localcdn://delete?index= ${ encodeURIComponent ( JSON . stringify ([ "video_bigbunny" ])) } ` );
// Result available in window.deletedCdnItems
contentServerChange Callback
Called by native runtime when a download completes. This is where you get the file data.
window . contentServerChange = ( item ) => {
// item.local_cdn > localhost URL for playback
// item.cdn > original remote URL
// item.index > your uniqueId from the write call
// item.size > file size in bytes
// item.status > "cached" when complete
// item.local_path > absolute device path
console . log ( "Cached:" , item . index , item . local_cdn );
addToDownloadsList ( item );
};
{
"index_full" : "videos/movies/bigbuckbunny.mp4" ,
"index" : "movie_bigbuckbunny" ,
"extension" : "mp4" ,
"local_path" : "/var/mobile/.../localcdn/videos/movies/bigbuckbunny.mp4" ,
"local_cdn" : "http://localhost:7777/localcdn/videos/movies/bigbuckbunny.mp4" ,
"cdn" : "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" ,
"size" : "158008374" ,
"status" : "cached" ,
"created_at" : "1709856000"
}
When it fires:
When download completes (could be seconds or minutes later)
When app reopens after background downloads completed
Response Schema
Full file path (e.g., videos/samples/bigbunny.mp4)
File extension (mp4, mp3, json)
Use this for playback - localhost URL
Background Downloads
Downloads continue when users close the app. Native OS handles retry on network failure.
App reopens
Native finds pending items
Callback replayed
Calls contentServerChange(item) for each completed download
Playback
Use local_cdn URL for offline playback:
const data = await despia (
`localcdn://read?index= ${ encodeURIComponent ( JSON . stringify ([ indexId ])) } ` ,
[ "cdnItems" ]
);
if ( data . cdnItems ?.[ 0 ]?. status === "cached" ) {
videoElement . src = data . cdnItems [ 0 ]. local_cdn ;
}
HTTP Upload API
Only available when your app is served via Despia Local Server (not from origin/remote server).
Upload user files via HTTP POST:
const fd = new FormData ();
fd . append ( "file" , fileInput . files [ 0 ]);
const res = await fetch ( "http://localhost:7777/api/upload" , {
method: "POST" ,
body: fd
});
const result = await res . json ();
// { success: true, fileName: "video.mp4", url: "http://localhost:7777/files/video.mp4" }
Method Storage Path URL Pattern localcdn://write/localcdn/localhost:{PORT}/localcdn/{filepath}/api/upload/files/localhost:{PORT}/files/{filename}
React Hook
import { useState , useEffect , useCallback } from 'react' ;
import despia from 'despia-native' ;
function useLocalCDN () {
const [ items , setItems ] = useState ([]);
useEffect (() => {
window . contentServerChange = ( item ) => {
setItems ( prev => {
const idx = prev . findIndex ( i => i . index_full === item . index_full );
if ( idx >= 0 ) {
const updated = [ ... prev ];
updated [ idx ] = item ;
return updated ;
}
return [ ... prev , item ];
});
};
return () => { window . contentServerChange = null ; };
}, []);
// Fire-and-forget - result comes via contentServerChange
const download = useCallback (( url , filepath , index ) => {
despia ( `localcdn://write?url= ${ url } &filename= ${ filepath } &index= ${ index } ` );
}, []);
const remove = useCallback (( indices ) => {
const ids = Array . isArray ( indices ) ? indices : [ indices ];
despia ( `localcdn://delete?index= ${ encodeURIComponent ( JSON . stringify ( ids )) } ` );
setItems ( prev => prev . filter ( item => ! ids . includes ( item . index )));
}, []);
return { items , download , remove };
}
Environment Check
if ( navigator . userAgent . includes ( 'despia' )) {
// Use Local CDN
} else {
// Fallback for non-Despia environment
}
Test Videos
Free sample videos for testing Local CDN (CC licensed):
Title URL Size Big Buck Bunny http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4~158MB Elephant Dream http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4~115MB Sintel http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4~129MB Tears of Steel http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4~185MB For Bigger Blazes http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4~2MB For Bigger Escapes http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4~2MB For Bigger Fun http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4~2MB For Bigger Joyrides http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4~2MB For Bigger Meltdowns http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4~2MB
// Test with a small video first
const testVideos = [
{ url: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4" , id: "test_blazes" },
{ url: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" , id: "test_bunny" },
{ url: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4" , id: "test_sintel" }
];
window . contentServerChange = ( item ) => {
alert ( `Downloaded: ${ item . index } ( ${ ( item . size / 1024 / 1024 ). toFixed ( 1 ) } MB)` );
};
// Download first test video
const { url , id } = testVideos [ 0 ];
despia ( `localcdn://write?url= ${ url } &filename=test/ ${ id } .mp4&index= ${ id } ` );