Skip to main content
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}`
);
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
);
url
string
required
Remote file URL to download
filename
string
required
Local path: folder/subfolder/filename
index
string
required
Unique ID for this file
push
boolean
Set to true to show push notification on completion
pushmessage
string
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));
cdnItems
array
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));
cdnItems
array
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

index_full
string
Full file path (e.g., videos/samples/bigbunny.mp4)
index
string
Your unique identifier
extension
string
File extension (mp4, mp3, json)
local_path
string
Absolute device path
local_cdn
string
Use this for playback - localhost URL
cdn
string
Original remote URL
size
string
File size in bytes
status
string
Cache status ("cached")
created_at
string
Unix timestamp

Background Downloads

Downloads continue when users close the app. Native OS handles retry on network failure.
1

App reopens

Native finds pending items
2

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" }
MethodStorage PathURL 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):

Available Test Videos

TitleURLSize
Big Buck Bunnyhttp://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4~158MB
Elephant Dreamhttp://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4~115MB
Sintelhttp://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4~129MB
Tears of Steelhttp://commondatastorage.googleapis.com/gtv-videos-bucket/sample/TearsOfSteel.mp4~185MB
For Bigger Blazeshttp://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4~2MB
For Bigger Escapeshttp://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4~2MB
For Bigger Funhttp://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4~2MB
For Bigger Joyrideshttp://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4~2MB
For Bigger Meltdownshttp://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}`);