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.
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 throwsYtDlpError with code UNSUPPORTED_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-native
If expo install does not resolve the package (e.g. before it is indexed),
fall back to:
npm install ytdlp-react-native
import 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 areundefined — 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 realffmpeg 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 theffmpeg executable. With ffmpeg.location set, the following features are
handed to yt-dlp instead of being rejected:
merge: true (e.g. bestvideo+bestaudio)If the location does not exist, the download fails immediately withPROCESSING_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.
munim-ffmpegytdlp-react-native can be used together with munim-ffmpeg for applications that need native FFmpeg processing.
ytdlp-react-native is responsible for media extraction and downloading, while munim-ffmpeg can be used for merging, muxing, transcoding, subtitle processing, and other FFmpeg operations.
When downloading separate video and audio streams, you can download them independently with yt-dlp and then pass the resulting local file paths to munim-ffmpeg:
URL
│
▼
ytdlp-react-native
│
├── bestvideo → video file
│
└── bestaudio → audio file
│
▼
munim-ffmpeg
│
▼
final media file
│
▼
Media Library / Gallery
For example, first select the desired video and audio format IDs from extractInfo() / getFormats() and download them separately:
const videoTask = await YtDlp.download({
url,
format: videoFormatId,
output: {
directory: "downloads",
filename: "video.%(ext)s",
},
});
const audioTask = await YtDlp.download({
url,
format: audioFormatId,
output: {
directory: "downloads",
filename: "audio.%(ext)s",
},
});
After both downloads complete, use munim-ffmpeg to mux the streams without re-encoding:
video + audio
│
▼
FFmpeg: -c copy
│
▼
final.mp4
Using stream copy (-c copy) avoids unnecessary transcoding, so compatible video and audio streams can be merged quickly without reducing their original quality.
| Package | Responsibility |
|---|---|
ytdlp-react-native |
Extraction, format selection, downloading, cookies, playlists, download progress |
munim-ffmpeg |
Muxing, merging, transcoding, subtitles, FFprobe and media processing |
This approach is especially useful when your application already includes munim-ffmpeg. In that case, ytdlp-react-native does not need to invoke an FFmpeg executable for the application's post-processing pipeline.
Note: The
ffmpeg.locationoption remains available for standalone yt-dlp workflows where yt-dlp itself needs to invoke an FFmpeg executable, for example when usingbestvideo+bestaudiodirectly. When usingmunim-ffmpeg, you can instead download the streams separately and perform the merge through its native FFmpeg API.
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 withPROCESSING_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): SubscriptionStatuses: 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 → YtDlpErrorProgress 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 file
YtDlp.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.part file 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 defaultcontinue): byte-range where the source server supports it, fragment-based.ytdl sidecar. If a source does not supportfalse when the action is not applicable (unknown task, notpause()/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:
pause()/resume() semantics forPOST_NOTIFICATIONSdataSync foreground servicestask.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.
ffmpeg.location (see "FFmpeg support"). Without it, those features arePROCESSING_FAILED.dataSync foreground service (see "Backgroundplaylist.enabled to avoid accidental bulkytdlp-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 |
react-native-continued-taskytdlp-react-native using a single-stream format (best /bestaudio). Report progress back to the continued task.munim-ffmpeg (in-process FFmpegKit). Note thatytdlp-react-native's ffmpeg.location option needs — thatexpo-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 intoytdlp-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:
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.