Skip to main content

Documentation Index

Fetch the complete documentation index at: https://setup.despia.com/llms.txt

Use this file to discover all available pages before exploring further.

This feature is still in the final beta round. To request access as a beta tester, send an email to offlinemode@despia.com
Cache remote files locally for offline access. Background downloads continue when users close the app, using native OS transfer APIs (NSURLSession on iOS, WorkManager on Android) with built-in retry.

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.
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. Use the 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, 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;
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 the 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"
}
The callback fires when a download completes in the foreground, and when the 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. The native OS handles retry on network failure. On iOS, a Live Activity is shown automatically with real-time download progress. On Android, a native download progress notification appears in the system tray. Both require no setup. When the app reopens, the native layer finds any pending items and calls contentServerChange(item) for each completed download automatically.

Playback

Use the 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 host = window.location.host; // e.g. "localhost:7777"

const res = await fetch(`http://${host}/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; };
  }, []);

  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

const isDespia = navigator.userAgent.toLowerCase().includes('despia')

if (isDespia) {
  // Use Local CDN
} else {
  // Fallback for non-Despia environment
}

Test videos

Free sample videos for testing Local CDN (CC licensed):
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
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) => {
  console.log(`Downloaded: ${item.index} (${(item.size / 1024 / 1024).toFixed(1)}MB)`);
};

const { url, id } = testVideos[0];
despia(`localcdn://write?url=${url}&filename=test/${id}.mp4&index=${id}`);

Resources

NPM Package

despia-native