A native Android Expo module providing a modern TypeScript API around yt-dlp. It embeds the Python runtime, yt-dlp and all site extractors through the yt-dlp-android library — no Python, yt-dlp, Chaquopy, FFmpeg or Termux setup is required in your app.
import YtDlp from 'ytdlp-react-native';
const info = await YtDlp.extractInfo('https://www.youtube.com/watch?v=...');
console.log(info.title);
console.log(info.formats);
const task = await YtDlp.download({
url: 'https://www.youtube.com/watch?v=...',
format: 'best', // prefer a single progressive stream (no FFmpeg merge)
output: { directory: 'Movies' },
});
task.addListener('progress', (progress) => {
console.log(progress.percent);
});
await task.cancel();-
Platform: Android only. Importing on iOS/web throws
YtDlpErrorwith codeUNSUPPORTED_PLATFORM. -
Expo SDK: 57 (tested against Expo 57 / React Native 0.86).
-
Minimum Android: API 24.
-
Native build required. This is a custom native module. It does not work inside standard Expo Go — Expo Go cannot load arbitrary custom native modules. Use a development build:
npx expo prebuild npx expo run:android
npx expo install ytdlp-react-nativeIf expo install does not resolve the package (e.g. before it is indexed),
fall back to:
npm install ytdlp-react-nativeimport YtDlp from 'ytdlp-react-native';
const info = await YtDlp.extractInfo(url);
console.log(info.title); // string | undefined
console.log(info.duration);
console.log(info.thumbnail);
console.log(info.formats);extractInfo never downloads media. Fields that a site does not provide are
undefined — no field is guaranteed for every site.
const formats = await YtDlp.getFormats(url);getFormats reuses the extraction result, so it does not extract twice.
const task = await YtDlp.download({
url,
format: 'best', // or 'bestaudio', 'best[height<=720]', etc.
output: {
directory: 'Movies',
filename: '%(title)s.%(ext)s',
},
});
task.addListener('progress', (progress) => {
console.log(progress.percent, progress.speedBytesPerSecond, progress.etaSeconds);
});
task.addListener('completed', (result) => {
console.log(result.path);
});
task.addListener('error', (error) => {
console.log(error.code, error.message);
});download resolves as soon as the task is registered; progress and the final
result arrive through the task's listeners. Multiple downloads can run at the
same time — every event carries a taskId.
await task.cancel();Cancellation calls the native cancellation path and aborts the underlying yt-dlp download. To cancel a task after your JS task object is gone:
await YtDlp.cancel(taskId);const version = await YtDlp.getVersion();
// { ytDlp: '2026.xx.xx', library: '1.0.0' }The embedded yt-dlp version and the npm package version are independent.
| Option | Description |
|---|---|
url |
Required. Any URL supported by yt-dlp. |
format |
Raw yt-dlp format expression, e.g. best, bestaudio, best[height<=720]. |
output.directory |
Subdirectory under the app's yt-dlp folder. Sanitized. |
output.filename |
yt-dlp output template, e.g. %(title)s.%(ext)s. Sanitized. |
headers |
Extra HTTP headers, e.g. { Referer: '...' }. |
userAgent |
Custom User-Agent. |
referer |
Custom Referer. |
proxy |
Proxy URL. |
cookies.path |
Path to a Netscape-format cookies file. |
playlist.enabled |
Default false — a playlist URL downloads only the first item unless enabled. |
playlist.start / playlist.end |
Playlist item range (1-based). |
subtitles.enabled |
Write subtitles. |
subtitles.languages |
e.g. ['en', 'bn']. |
subtitles.autoGenerated |
Also write auto-generated subtitles. |
network.timeout |
Socket timeout in seconds. |
network.retries |
Number of retries. |
ffmpeg.location |
Absolute path to an ffmpeg executable (or a directory containing one) already on the device. Enables merging and other FFmpeg-based post-processing. Not bundled — you must supply it. |
The bundled yt-dlp-android build ships no FFmpeg, and this package does
not bundle one either — yt-dlp merges separate streams by spawning a real
ffmpeg executable, so an in-process JNI wrapper (e.g. FFmpegKit) is not
enough. If you have an ffmpeg binary on the device (e.g. extracted into your
app's files directory), pass its path and bestvideo+bestaudio works:
const task = await YtDlp.download({
url,
format: 'bestvideo+bestaudio',
merge: true,
ffmpeg: {
location: '/data/user/0/com.example.app/files/ffmpeg/ffmpeg',
},
});yt-dlp accepts either the binary path or the directory that contains the
ffmpeg executable. With ffmpeg.location set, the following features are
handed to yt-dlp instead of being rejected:
merge: true(e.g.bestvideo+bestaudio)- audio extraction / re-encoding
- metadata embedding
- thumbnail embedding
If the location does not exist, the download fails immediately with
PROCESSING_FAILED. Without ffmpeg.location, the features above are still
rejected up front with a clear error rather than silently ignored, and you
should request a single stream (best, bestaudio, best[height<=720],
etc.) that needs no post-processing.
yt-dlp supports Instagram (posts, Reels, many stories) and TikTok, among hundreds of other sites. Without FFmpeg you must request a single progressive stream:
format: 'best' // preferred
// or
format: 'best[height<=1080]'
format: 'bestaudio'Many Instagram Reels and TikTok videos already provide combined progressive
MP4 streams, so downloads often work without extra processing. If the only
high-quality options are separate video + audio streams, provide an ffmpeg
binary via ffmpeg.location (see above) or the download will fail with
PROCESSING_FAILED.
Cookies tip: Instagram almost always requires a valid logged-in session
(Netscape cookies file via cookies.path). TikTok sometimes needs them too
for full reliability.
A DownloadTask exposes:
id: stringcancel(): Promise<void>pause(): Promise<boolean>resume(): Promise<boolean>getStatus(): Promise<DownloadStatus>getProgress(): Promise<DownloadProgress | null>addListener(event, listener): Subscription
Statuses: queued | extracting | downloading | processing | paused | completed | cancelled | failed.
Events:
progress→DownloadProgress(percent,downloadedBytes,totalBytes,speedBytesPerSecond,etaSeconds,filename,phase)state→{ taskId, status }completed→DownloadResult(taskId,path,filename,size)error→YtDlpError
Progress events are throttled to ~200 ms and numeric fields are undefined
when the value is unknown (never NaN).
const task = await YtDlp.download({ url, format: 'bestvideo+bestaudio', ffmpeg: { location } });
await task.pause(); // true — yt-dlp aborts on the next progress tick
console.log(await task.getStatus()); // 'paused'
await task.resume(); // true — same options re-run; continues from the .part fileYtDlp.pause(taskId) and YtDlp.resume(taskId) work the same way when you
only have a task id (e.g. after app re-creation).
How it works:
pause()cooperatively aborts the download from the progress hook. yt-dlp leaves its.partfile on disk, and the task stays registered with statuspaused(so a paused task still exists; cancel it if you're done).resume()re-runs the exact same download. yt-dlp continues by default (continue): byte-range where the source server supports it, fragment-based resumes (DASH/HLS) via the.ytdlsidecar. If a source does not support continuing, yt-dlp restarts that file.- Both return
falsewhen the action is not applicable (unknown task, not paused, already finished). Cancelling a paused task finalizes it immediately. - No network is consumed while paused, but the pause takes effect on the next yt-dlp progress tick (~200 ms). Pausing does not cover the download's extraction phase.
pause()/resume() are process-local and do not survive native restarts
(see "Limitations").
While at least one download is running, the module promotes the app with a
native Android foreground service (dataSync type), so downloads keep
going when the app is backgrounded, the screen is off, or the app is swiped
away. No opt-in needed — starting a download shows an ongoing notification
with live progress and a Cancel action (cancels all active downloads);
tapping the notification reopens the app. When the last download finishes
(or fails / is cancelled), the service stops and the notification goes away.
const task = await YtDlp.download({ url });
// app backgrounded here — the download continues
task.addListener('progress', (p) => console.log(p.percent));What it does and does not cover:
- Survives: backgrounding, screen off, task swipe-away.
- Does not survive: the process being killed by the system, device
reboot, or a native restart. Tasks are process-local: after a restart,
re-issue downloads (and consider
pause()/resume()semantics for partial files left on disk). - The service holds a partial wake lock while active, so screen-off downloads are not stalled by CPU sleep.
- On Android 13+ the host app should request the
POST_NOTIFICATIONSruntime permission, otherwise the progress notification is suppressed (the download still runs). - On Android 15+, the system may time-box
dataSyncforeground services (around 6 hours); multi-hour downloads can be stopped by the OS. - Cancellation from the notification stops every active download; per-task
control stays in the app UI via
task.cancel().
react-native-continued-task (see below) remains a complementary option if
you also want WorkManager-backed scheduling or system progress UI beyond
this built-in service.
All failures normalize to YtDlpError with a code:
INVALID_URL, EXTRACTION_FAILED, DOWNLOAD_FAILED, CANCELLED,
FORMAT_UNAVAILABLE, NETWORK_ERROR, AUTHENTICATION_REQUIRED,
GEO_RESTRICTED, PRIVATE_CONTENT, AGE_RESTRICTED, PROCESSING_FAILED,
STORAGE_ERROR, INIT_FAILED, UNSUPPORTED_PLATFORM, UNKNOWN.
import { YtDlpError } from 'ytdlp-react-native';
try {
await YtDlp.extractInfo(url);
} catch (error) {
if (error instanceof YtDlpError) {
console.log(error.code, error.message);
}
}Raw native stack traces are never surfaced to the user.
Files are written to app-specific external storage:
Android/data/<your-package>/files/yt-dlp/<output.directory>/...
Filenames and directory segments are sanitized against illegal characters,
path traversal, excessive length and empty names. This avoids dangerous
permissions like MANAGE_EXTERNAL_STORAGE. The returned DownloadResult.path
is an absolute path inside your app's own storage.
- Android only.
- FFmpeg is not bundled. Merging separate video + audio streams and other
post-processing only work when you supply an ffmpeg executable via
ffmpeg.location(see "FFmpeg support"). Without it, those features are rejected withPROCESSING_FAILED. - Background execution is best-effort within a live process. Active
downloads run under a
dataSyncforeground service (see "Background downloads"), but downloads do not survive the process being killed or the device rebooting. Per-process pause/resume does not survive a native restart. Persisting task IDs lets you re-issue cancellation later. - yt-dlp site support changes frequently. Not every website works forever, and not every site provides every field.
- This package does not bundle or provide a way to update the embedded yt-dlp at runtime.
- Playlists are opt-in via
playlist.enabledto avoid accidental bulk downloads.
ytdlp-react-native focuses on reliable extraction and downloading of single
streams. For a complete video-downloader experience you will usually combine
it with three complementary libraries:
| Concern | Package | Link |
|---|---|---|
| Long-running background downloads with system progress UI | react-native-continued-task | GitHub |
| Merge video+audio, re-encode, burn subtitles, etc. | munim-ffmpeg | GitHub |
| Save finished files to the public Media Library / Gallery | expo-media-library | Expo docs |
- Start a continued background task with
react-native-continued-task(shows Live Activity on iOS 26+ / foreground-service notification on Android). - Download with
ytdlp-react-nativeusing a single-stream format (best/bestaudio). Report progress back to the continued task. - (Optional) If you need to merge separate streams or perform other
post-processing, use
munim-ffmpeg(in-process FFmpegKit). Note that FFmpegKit-style packages run FFmpeg in-process and cannot provide the executableytdlp-react-native'sffmpeg.locationoption needs — that option must point to a real ffmpeg binary on disk. - Move the final file into the user’s public gallery with
expo-media-library.
All three packages (plus this one) require a development build — they do not work inside Expo Go.
Note: basic background continuity (foreground service + progress notification + cancel) is built into
ytdlp-react-native(see "Background downloads"). Reach forreact-native-continued-taskwhen you additionally want WorkManager-backed scheduling or richer system UI.
ytdlp-react-native is a technical wrapper around yt-dlp. It does not circumvent
DRM (Widevine, FairPlay, PlayReady, ...), bypass authentication, or access
private or unauthorized content — content that requires DRM or authentication
will fail with an error.
You are responsible for complying with:
- website terms of service
- copyright law and content licenses
- authentication rules
- platform policies
Do not use this library to download content you do not have the right to download.
| Component | License |
|---|---|
ytdlp-react-native (this package) |
MIT |
yt-dlp-android (Maven dev.ffmpegkit-maintained:yt-dlp-android) |
MIT |
| yt-dlp | Unlicense |
| Chaquopy | BSD-style (per the embedded distribution) |
Re-verify third-party licenses at release time.