react-native-nitro-thumbnail
v0.1.4
Published
Generate thumbnails from local or remote videos. Nitro-powered, pure Swift & Kotlin, New Architecture, iOS + Android + Web.
Downloads
124
Maintainers
Readme
🎬 react-native-nitro-thumbnail
Generate a thumbnail from any video — local or remote — with one async call.
The same API on iOS, Android & Web. Powered by Nitro: pure Swift & Kotlin, New Architecture, no bridge.
📖 Read the Docs · 🚀 Quick Start · 🧰 API · 🔀 Migrate · 🛡️ Issues Solved
import { createThumbnail } from 'react-native-nitro-thumbnail';
const thumb = await createThumbnail({ url: 'https://media.example.com/clip.mp4' });
// → { path: 'file:///…/thumb.jpg', size: 19856, mime: 'image/jpeg', width: 512, height: 288 }
<Image source={{ uri: thumb.path }} />That's the whole API. One function. It works the same whether the video is a
file:// on disk or an https:// URL behind auth headers — and whether you're on an
iPhone, an Android tablet, or in a browser.
🎥 See it in action
The frame createThumbnail() extracts — { timeStamp: 2000, maxWidth: 1280 }
Running on iOS ▶︎ clip
Demo clip: Sintel © Blender Foundation, CC-BY 3.0.
✨ Features
| | |
|---|---|
| 🧩 One API, three engines | iOS (AVFoundation), Android (MediaMetadataRetriever), Web (<video>+<canvas>) behind one typed function. |
| 🌐 Local & remote | file://, absolute paths, content:// (Android), or http(s) URLs — streamed and decoded directly, no manual download. Custom headers supported. |
| ⚡ Built on Nitro | Pure Swift & Kotlin over JSI — no Obj-C/Java bridge, no JSON marshalling. The native contract is generated from one TS spec. |
| 🧵 Off the main thread | Every call runs async on a background thread, so batch generation never janks your UI. |
| 🎯 Typed errors | Failures reject with a ThumbnailError carrying a typed .code (FILE_NOT_FOUND, REMOTE_FETCH_FAILED, …). |
| 💾 Built-in caching | Deterministic filenames skip re-decoding; a size cap evicts old thumbnails (LRU). Writes to the cache dir — never your gallery. |
| 🖼️ Ready-made UI (optional) | A shipped, fully-customizable <VideoThumbnail> — server-first, shimmer loading, play button, onPress to open your player. Zero extra deps. |
| 🔀 Drop-in replacement | Matches react-native-create-thumbnail — usually a one-line import change. |
| 🛡️ Solves the known issues | Built to avoid the reported failure modes of older wrappers. |
🗺️ How it works
One TypeScript function validates your input and applies defaults, then calls a Nitro
HybridObject implemented natively per platform. The box labelled "your app" never
changes — only the engine behind it does.
📖 The complete request lifecycle — cache check, decode, encode, write, evict — with sequence diagrams, lives in the Architecture guide.
📦 Installation
npm install react-native-nitro-thumbnail react-native-nitro-modules
# or: yarn add react-native-nitro-thumbnail react-native-nitro-modulesreact-native-nitro-modules is a required peer dependency (the Nitro runtime).
iOS — install pods:
cd ios && pod installExpo — needs an Expo dev build (not Expo Go). No config plugin required:
npx expo install react-native-nitro-thumbnail react-native-nitro-modules
npx expo prebuild && npx expo run:ios # or run:androidRequires React Native 0.75+ with the New Architecture enabled.
🚀 Quick start
import { createThumbnail, ThumbnailError } from 'react-native-nitro-thumbnail';
async function makeThumb(videoUri: string) {
try {
const thumb = await createThumbnail({
url: videoUri, // 'file:///…', 'content://…' (Android), or 'https://…'
timeStamp: 1000, // grab the frame at 1.0s (milliseconds)
format: 'jpeg', // 'jpeg' | 'png'
maxWidth: 512, // fit within 512×512, aspect preserved, never upscaled
maxHeight: 512,
quality: 0.9, // jpeg quality 0..1 (ignored for png)
});
return thumb.path; // → <Image source={{ uri: thumb.path }} />
} catch (e) {
if (e instanceof ThumbnailError) {
console.warn(`thumbnail failed [${e.code}]: ${e.message}`);
}
throw e;
}
}📱 Platform support
| Platform | Engine | Minimum | Notes |
|---|---|---|---|
| 🍎 iOS | AVAssetImageGenerator | iOS 13 | async image(at:) on iOS 16+, copyCGImage fallback below |
| 🤖 Android | MediaMetadataRetriever | minSdk 24 | getScaledFrameAtTime on API 27+, scale-after fallback below |
| 🌐 Web | <video> → <canvas> → toBlob | modern browsers | resolved automatically via index.web.ts |
🧰 API
createThumbnail(options: CreateThumbnailOptions): Promise<Thumbnail>
| Option | Type | Default | Description |
|---|---|---|---|
| url | string | required | Local file:///absolute path, content:// (Android), or http(s) URL. |
| timeStamp | number | 0 | Frame time in milliseconds. |
| format | 'jpeg' \| 'png' | 'jpeg' | Output format. |
| maxWidth | number | 512 | Max width — aspect preserved, never upscaled. |
| maxHeight | number | 512 | Max height — aspect preserved, never upscaled. |
| quality | number | 0.9 | JPEG quality 0..1 (clamped). Ignored for PNG. |
| cacheName | string | — | Deterministic filename; existing file returned without re-decoding. |
| dirSize | number | 100 | Cache cap in MB (LRU eviction). |
| headers | Record<string,string> | — | HTTP headers for remote fetches. |
| timeToleranceMs | number | 2000 | How far from timeStamp a frame may be picked (0 = exact). |
| onlySyncedFrames | boolean | true | Prefer the nearest keyframe (faster). |
interface Thumbnail {
path: string; // native: file:// URL · web: blob: object URL
size: number; // file size in bytes
mime: string; // 'image/jpeg' | 'image/png'
width: number; // actual output width (≤ maxWidth)
height: number; // actual output height (≤ maxHeight)
}On native, path is a file:// URL in the app cache dir. On web it's an object URL —
call URL.revokeObjectURL(path) when done.
INVALID_URL · FILE_NOT_FOUND · REMOTE_FETCH_FAILED · DECODE_FAILED ·
UNSUPPORTED_FORMAT · WRITE_FAILED · UNKNOWN
Every failure is a typed ThumbnailError extends Error. Full descriptions in the
Error Handling guide.
📖 Full reference with recipes → API docs
🖼️ <VideoThumbnail> — optional ready-made tile
Don't want to wire up the UI yourself? Import the shipped component. It shows a server thumbnail if you have one, otherwise generates one (cached), with a shimmer while loading and a play button — fully themeable, zero extra dependencies.
import { VideoThumbnail } from 'react-native-nitro-thumbnail';
<VideoThumbnail
videoUrl={item.videoUrl}
serverThumbnail={item.thumbnailUrl} // shown directly if present — no generation
isLoading={item.isFetching} // your API loading → shimmer
onPress={() => openPlayer(item.videoUrl)} // you open the player
playButtonColor="#fff"
shimmerColors={['#222', '#333']}
/>;"Play" is your callback, not a bundled player — the library never depends on a video-player package. Full props + customization in the Recipes guide.
🛡️ Built to solve the known issues
This library was written knowing the common failure modes of video-thumbnail wrappers. The pain points reported against older libraries are solved by design:
- ✅ No Android build breakage — Kotlin (no checked-
IOException), modern AGP, no jcenter, no Apache Commons. - ✅ Remote URLs never crash — network failures reject with a typed
REMOTE_FETCH_FAILED. - ✅ No UI jank — decoding runs off the main thread via Nitro
Promise.async. - ✅
content://gallery videos work on Android. - ✅ No gallery pollution — thumbnails go to the cache dir, not MediaStore.
- ✅ Typed errors, never a silent
null.
📖 See the full issue-by-issue breakdown → Issues Solved
💡 Examples
const thumb = await createThumbnail({
url: 'https://media.example.com/clips/abc.mp4',
headers: { Authorization: `Bearer ${token}` },
timeStamp: 2000,
});// First call decodes and writes thumbnails/poster-42.jpg
await createThumbnail({ url, cacheName: 'poster-42' });
// Subsequent calls (even after restart) return it instantly — no decode
const cached = await createThumbnail({ url, cacheName: 'poster-42' });try {
await createThumbnail({ url });
} catch (e) {
if (e instanceof ThumbnailError && e.code === 'REMOTE_FETCH_FAILED') {
// offer a retry
}
}There's a runnable demo (local + remote, on a button tap) in example/.
📚 Documentation
Everything lives on the documentation site →
| | | | |---|---|---| | 🏛️ Architecture | 📖 API Reference | ⚠️ Error Handling | | 💾 Caching | 🧑🍳 Recipes | 🔀 Migration | | 🛡️ Issues Solved | 🍎 iOS | 🤖 Android | | 🌐 Web | | |
🤝 Contributing
PRs of every size are welcome — a great first one is improving a doc you found confusing.
- 🛠️ How the project is built & how to make a change
- 📋 Development workflow · 🤗 Code of conduct · 🐛 Open an issue
yarn # install (Yarn 4 workspaces)
yarn nitrogen # regenerate native specs after editing *.nitro.ts
yarn test # Jest (TypeScript layer)If this saved you time, a ⭐ on the repo helps others find it.
📄 License
MIT © contributors
