ytdlp-react-native 1.0.0 → 1.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +54 -1
  2. package/README.md +422 -267
  3. package/android/src/main/AndroidManifest.xml +20 -1
  4. package/android/src/main/java/expo/modules/ytdlp/ExpoYtDlpModule.kt +8 -0
  5. package/android/src/main/java/expo/modules/ytdlp/YtDlpDownloadManager.kt +177 -16
  6. package/android/src/main/java/expo/modules/ytdlp/YtDlpEngine.kt +35 -4
  7. package/android/src/main/java/expo/modules/ytdlp/YtDlpForegroundService.kt +241 -0
  8. package/android/src/main/java/expo/modules/ytdlp/YtDlpTask.kt +44 -2
  9. package/build/ExpoYtDlpModule.d.ts +2 -0
  10. package/build/ExpoYtDlpModule.d.ts.map +1 -1
  11. package/build/ExpoYtDlpModule.js.map +1 -1
  12. package/build/YtDlp.d.ts +10 -0
  13. package/build/YtDlp.d.ts.map +1 -1
  14. package/build/YtDlp.js +22 -77
  15. package/build/YtDlp.js.map +1 -1
  16. package/build/constants.d.ts +1 -4
  17. package/build/constants.d.ts.map +1 -1
  18. package/build/constants.js +2 -1
  19. package/build/constants.js.map +1 -1
  20. package/build/downloadTask.d.ts +2 -0
  21. package/build/downloadTask.d.ts.map +1 -1
  22. package/build/downloadTask.js +7 -0
  23. package/build/downloadTask.js.map +1 -1
  24. package/build/errors.js +3 -3
  25. package/build/errors.js.map +1 -1
  26. package/build/index.d.ts +3 -1
  27. package/build/index.d.ts.map +1 -1
  28. package/build/index.js +3 -1
  29. package/build/index.js.map +1 -1
  30. package/build/mappers.js +3 -2
  31. package/build/mappers.js.map +1 -1
  32. package/build/serializers.d.ts +5 -0
  33. package/build/serializers.d.ts.map +1 -0
  34. package/build/serializers.js +96 -0
  35. package/build/serializers.js.map +1 -0
  36. package/build/types.d.ts +39 -1
  37. package/build/types.d.ts.map +1 -1
  38. package/build/types.js.map +1 -1
  39. package/package.json +7 -5
  40. package/src/ExpoYtDlpModule.ts +2 -0
  41. package/src/YtDlp.ts +21 -57
  42. package/src/__tests__/errors.test.ts +117 -0
  43. package/src/__tests__/mappers.test.ts +167 -0
  44. package/src/__tests__/serializers.test.ts +72 -0
  45. package/src/constants.ts +3 -1
  46. package/src/downloadTask.ts +9 -0
  47. package/src/errors.ts +3 -3
  48. package/src/index.ts +3 -1
  49. package/src/mappers.ts +3 -2
  50. package/src/serializers.ts +78 -0
  51. package/src/types.ts +47 -1
package/README.md CHANGED
@@ -1,267 +1,422 @@
1
- # ytdlp-react-native
2
-
3
- A native **Android** Expo module providing a modern TypeScript API around
4
- [yt-dlp](https://github.com/yt-dlp/yt-dlp). It embeds the Python runtime,
5
- yt-dlp and all site extractors through the
6
- [yt-dlp-android](https://github.com/ffmpegkit-maintained/yt-dlp-android)
7
- library — no Python, yt-dlp, Chaquopy, FFmpeg or Termux setup is required in
8
- your app.
9
-
10
- ```ts
11
- import YtDlp from 'ytdlp-react-native';
12
-
13
- const info = await YtDlp.extractInfo('https://www.youtube.com/watch?v=...');
14
- console.log(info.title);
15
- console.log(info.formats);
16
-
17
- const task = await YtDlp.download({
18
- url: 'https://www.youtube.com/watch?v=...',
19
- format: 'bestvideo+bestaudio',
20
- output: { directory: 'Movies' },
21
- });
22
-
23
- task.addListener('progress', (progress) => {
24
- console.log(progress.percent);
25
- });
26
-
27
- await task.cancel();
28
- ```
29
-
30
- ## Requirements
31
-
32
- - **Platform:** Android only. Importing on iOS/web throws
33
- `YtDlpError` with code `UNSUPPORTED_PLATFORM`.
34
- - **Expo SDK:** 57 (tested against Expo 57 / React Native 0.86).
35
- - **Minimum Android:** API 24.
36
- - **Native build required.** This is a custom native module. It does **not**
37
- work inside standard Expo Go — Expo Go cannot load arbitrary custom native
38
- modules. Use a development build:
39
-
40
- ```bash
41
- npx expo prebuild
42
- npx expo run:android
43
- ```
44
-
45
- ## Installation
46
-
47
- ```bash
48
- npx expo install ytdlp-react-native
49
- ```
50
-
51
- If `expo install` does not resolve the package (e.g. before it is indexed),
52
- fall back to:
53
-
54
- ```bash
55
- npm install ytdlp-react-native
56
- ```
57
-
58
- ## Usage
59
-
60
- ### Extract media information
61
-
62
- ```ts
63
- import YtDlp from 'ytdlp-react-native';
64
-
65
- const info = await YtDlp.extractInfo(url);
66
- console.log(info.title); // string | undefined
67
- console.log(info.duration);
68
- console.log(info.thumbnail);
69
- console.log(info.formats);
70
- ```
71
-
72
- `extractInfo` never downloads media. Fields that a site does not provide are
73
- `undefined` — no field is guaranteed for every site.
74
-
75
- ### List formats
76
-
77
- ```ts
78
- const formats = await YtDlp.getFormats(url);
79
- ```
80
-
81
- `getFormats` reuses the extraction result, so it does not extract twice.
82
-
83
- ### Download
84
-
85
- ```ts
86
- const task = await YtDlp.download({
87
- url,
88
- format: 'bestvideo+bestaudio', // raw yt-dlp format expression
89
- output: {
90
- directory: 'Movies',
91
- filename: '%(title)s.%(ext)s',
92
- },
93
- });
94
-
95
- task.addListener('progress', (progress) => {
96
- console.log(progress.percent, progress.speedBytesPerSecond, progress.etaSeconds);
97
- });
98
-
99
- task.addListener('completed', (result) => {
100
- console.log(result.path);
101
- });
102
-
103
- task.addListener('error', (error) => {
104
- console.log(error.code, error.message);
105
- });
106
- ```
107
-
108
- `download` resolves as soon as the task is registered; progress and the final
109
- result arrive through the task's listeners. Multiple downloads can run at the
110
- same time — every event carries a `taskId`.
111
-
112
- ### Cancel
113
-
114
- ```ts
115
- await task.cancel();
116
- ```
117
-
118
- Cancellation calls the native cancellation path and aborts the underlying
119
- yt-dlp download. To cancel a task after your JS task object is gone:
120
-
121
- ```ts
122
- await YtDlp.cancel(taskId);
123
- ```
124
-
125
- ### Version
126
-
127
- ```ts
128
- const version = await YtDlp.getVersion();
129
- // { ytDlp: '2026.xx.xx', library: '0.1.0' }
130
- ```
131
-
132
- The embedded yt-dlp version and the npm package version are independent.
133
-
134
- ## Supported download options
135
-
136
- | Option | Description |
137
- | --- | --- |
138
- | `url` | Required. Any URL supported by yt-dlp. |
139
- | `format` | Raw yt-dlp format expression, e.g. `best`, `bestaudio`, `best[height<=720]`. |
140
- | `output.directory` | Subdirectory under the app's `yt-dlp` folder. Sanitized. |
141
- | `output.filename` | yt-dlp output template, e.g. `%(title)s.%(ext)s`. Sanitized. |
142
- | `headers` | Extra HTTP headers, e.g. `{ Referer: '...' }`. |
143
- | `userAgent` | Custom `User-Agent`. |
144
- | `referer` | Custom `Referer`. |
145
- | `proxy` | Proxy URL. |
146
- | `cookies.path` | Path to a Netscape-format cookies file. |
147
- | `playlist.enabled` | Default `false` — a playlist URL downloads only the first item unless enabled. |
148
- | `playlist.start` / `playlist.end` | Playlist item range (1-based). |
149
- | `subtitles.enabled` | Write subtitles. |
150
- | `subtitles.languages` | e.g. `['en', 'bn']`. |
151
- | `subtitles.autoGenerated` | Also write auto-generated subtitles. |
152
- | `network.timeout` | Socket timeout in seconds. |
153
- | `network.retries` | Number of retries. |
154
-
155
- ### Not supported (yet)
156
-
157
- The bundled yt-dlp-android build ships no FFmpeg, so the following are
158
- rejected up front with `PROCESSING_FAILED` rather than silently ignored:
159
-
160
- - `merge: true` (e.g. `bestvideo+bestaudio` will fail because merging requires FFmpeg)
161
- - `audio.*` (audio extraction / re-encoding)
162
- - `metadata` (embedding)
163
- - `thumbnail` (embedding)
164
-
165
- Use `format: 'best'` or `format: 'bestaudio'` and download a single stream
166
- that requires no post-processing.
167
-
168
- ## Download task
169
-
170
- A `DownloadTask` exposes:
171
-
172
- - `id: string`
173
- - `cancel(): Promise<void>`
174
- - `getStatus(): Promise<DownloadStatus>`
175
- - `getProgress(): Promise<DownloadProgress | null>`
176
- - `addListener(event, listener): Subscription`
177
-
178
- Statuses: `queued | extracting | downloading | processing | completed |
179
- cancelled | failed`.
180
-
181
- Events:
182
-
183
- - `progress` → `DownloadProgress` (`percent`, `downloadedBytes`, `totalBytes`,
184
- `speedBytesPerSecond`, `etaSeconds`, `filename`, `phase`)
185
- - `state` `{ taskId, status }`
186
- - `completed` `DownloadResult` (`taskId`, `path`, `filename`, `size`)
187
- - `error` `YtDlpError`
188
-
189
- Progress events are throttled to ~200 ms and numeric fields are `undefined`
190
- when the value is unknown (never `NaN`).
191
-
192
- ## Errors
193
-
194
- All failures normalize to `YtDlpError` with a `code`:
195
-
196
- `INVALID_URL`, `EXTRACTION_FAILED`, `DOWNLOAD_FAILED`, `CANCELLED`,
197
- `FORMAT_UNAVAILABLE`, `NETWORK_ERROR`, `AUTHENTICATION_REQUIRED`,
198
- `GEO_RESTRICTED`, `PRIVATE_CONTENT`, `AGE_RESTRICTED`, `PROCESSING_FAILED`,
199
- `STORAGE_ERROR`, `INIT_FAILED`, `UNSUPPORTED_PLATFORM`, `UNKNOWN`.
200
-
201
- ```ts
202
- import { YtDlpError } from 'ytdlp-react-native';
203
-
204
- try {
205
- await YtDlp.extractInfo(url);
206
- } catch (error) {
207
- if (error instanceof YtDlpError) {
208
- console.log(error.code, error.message);
209
- }
210
- }
211
- ```
212
-
213
- Raw native stack traces are never surfaced to the user.
214
-
215
- ## Storage
216
-
217
- Files are written to app-specific external storage:
218
-
219
- ```
220
- Android/data/<your-package>/files/yt-dlp/<output.directory>/...
221
- ```
222
-
223
- Filenames and directory segments are sanitized against illegal characters,
224
- path traversal, excessive length and empty names. This avoids dangerous
225
- permissions like `MANAGE_EXTERNAL_STORAGE`. The returned `DownloadResult.path`
226
- is an absolute path inside your app's own storage.
227
-
228
- ## Limitations
229
-
230
- - Android only.
231
- - No FFmpeg-based post-processing (see "Not supported" above).
232
- - No background/foreground service: downloads pause if the JS/native runtime
233
- is destroyed. Persisting task IDs lets you re-issue cancellation later.
234
- - yt-dlp site support changes frequently. Not every website works forever,
235
- and not every site provides every field.
236
- - This package does **not** bundle or provide a way to update the embedded
237
- yt-dlp at runtime.
238
- - Playlists are opt-in via `playlist.enabled` to avoid accidental bulk
239
- downloads.
240
-
241
- ## Legal & responsible use
242
-
243
- `ytdlp-react-native` is a technical wrapper around yt-dlp. It does not circumvent
244
- DRM (Widevine, FairPlay, PlayReady, ...), bypass authentication, or access
245
- private or unauthorized content — content that requires DRM or authentication
246
- will fail with an error.
247
-
248
- You are responsible for complying with:
249
-
250
- - website terms of service
251
- - copyright law and content licenses
252
- - authentication rules
253
- - platform policies
254
-
255
- Do not use this library to download content you do not have the right to
256
- download.
257
-
258
- ## Third-party licenses
259
-
260
- | Component | License |
261
- | --- | --- |
262
- | `ytdlp-react-native` (this package) | MIT |
263
- | `yt-dlp-android` (Maven `dev.ffmpegkit-maintained:yt-dlp-android`) | MIT |
264
- | [yt-dlp](https://github.com/yt-dlp/yt-dlp) | Unlicense |
265
- | [Chaquopy](https://chaquo.com/chaquopy/) | BSD-style (per the embedded distribution) |
266
-
267
- Re-verify third-party licenses at release time.
1
+ # ytdlp-react-native
2
+
3
+ A native **Android** Expo module providing a modern TypeScript API around
4
+ [yt-dlp](https://github.com/yt-dlp/yt-dlp). It embeds the Python runtime,
5
+ yt-dlp and all site extractors through the
6
+ [yt-dlp-android](https://github.com/ffmpegkit-maintained/yt-dlp-android)
7
+ library — no Python, yt-dlp, Chaquopy, FFmpeg or Termux setup is required in
8
+ your app.
9
+
10
+ ```ts
11
+ import YtDlp from 'ytdlp-react-native';
12
+
13
+ const info = await YtDlp.extractInfo('https://www.youtube.com/watch?v=...');
14
+ console.log(info.title);
15
+ console.log(info.formats);
16
+
17
+ const task = await YtDlp.download({
18
+ url: 'https://www.youtube.com/watch?v=...',
19
+ format: 'best', // prefer a single progressive stream (no FFmpeg merge)
20
+ output: { directory: 'Movies' },
21
+ });
22
+
23
+ task.addListener('progress', (progress) => {
24
+ console.log(progress.percent);
25
+ });
26
+
27
+ await task.cancel();
28
+ ```
29
+
30
+ ## Requirements
31
+
32
+ - **Platform:** Android only. Importing on iOS/web throws
33
+ `YtDlpError` with code `UNSUPPORTED_PLATFORM`.
34
+ - **Expo SDK:** 57 (tested against Expo 57 / React Native 0.86).
35
+ - **Minimum Android:** API 24.
36
+ - **Native build required.** This is a custom native module. It does **not**
37
+ work inside standard Expo Go — Expo Go cannot load arbitrary custom native
38
+ modules. Use a development build:
39
+
40
+ ```bash
41
+ npx expo prebuild
42
+ npx expo run:android
43
+ ```
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ npx expo install ytdlp-react-native
49
+ ```
50
+
51
+ If `expo install` does not resolve the package (e.g. before it is indexed),
52
+ fall back to:
53
+
54
+ ```bash
55
+ npm install ytdlp-react-native
56
+ ```
57
+
58
+ ## Usage
59
+
60
+ ### Extract media information
61
+
62
+ ```ts
63
+ import YtDlp from 'ytdlp-react-native';
64
+
65
+ const info = await YtDlp.extractInfo(url);
66
+ console.log(info.title); // string | undefined
67
+ console.log(info.duration);
68
+ console.log(info.thumbnail);
69
+ console.log(info.formats);
70
+ ```
71
+
72
+ `extractInfo` never downloads media. Fields that a site does not provide are
73
+ `undefined` — no field is guaranteed for every site.
74
+
75
+ ### List formats
76
+
77
+ ```ts
78
+ const formats = await YtDlp.getFormats(url);
79
+ ```
80
+
81
+ `getFormats` reuses the extraction result, so it does not extract twice.
82
+
83
+ ### Download
84
+
85
+ ```ts
86
+ const task = await YtDlp.download({
87
+ url,
88
+ format: 'best', // or 'bestaudio', 'best[height<=720]', etc.
89
+ output: {
90
+ directory: 'Movies',
91
+ filename: '%(title)s.%(ext)s',
92
+ },
93
+ });
94
+
95
+ task.addListener('progress', (progress) => {
96
+ console.log(progress.percent, progress.speedBytesPerSecond, progress.etaSeconds);
97
+ });
98
+
99
+ task.addListener('completed', (result) => {
100
+ console.log(result.path);
101
+ });
102
+
103
+ task.addListener('error', (error) => {
104
+ console.log(error.code, error.message);
105
+ });
106
+ ```
107
+
108
+ `download` resolves as soon as the task is registered; progress and the final
109
+ result arrive through the task's listeners. Multiple downloads can run at the
110
+ same time — every event carries a `taskId`.
111
+
112
+ ### Cancel
113
+
114
+ ```ts
115
+ await task.cancel();
116
+ ```
117
+
118
+ Cancellation calls the native cancellation path and aborts the underlying
119
+ yt-dlp download. To cancel a task after your JS task object is gone:
120
+
121
+ ```ts
122
+ await YtDlp.cancel(taskId);
123
+ ```
124
+
125
+ ### Version
126
+
127
+ ```ts
128
+ const version = await YtDlp.getVersion();
129
+ // { ytDlp: '2026.xx.xx', library: '1.0.0' }
130
+ ```
131
+
132
+ The embedded yt-dlp version and the npm package version are independent.
133
+
134
+ ## Supported download options
135
+
136
+ | Option | Description |
137
+ | --- | --- |
138
+ | `url` | Required. Any URL supported by yt-dlp. |
139
+ | `format` | Raw yt-dlp format expression, e.g. `best`, `bestaudio`, `best[height<=720]`. |
140
+ | `output.directory` | Subdirectory under the app's `yt-dlp` folder. Sanitized. |
141
+ | `output.filename` | yt-dlp output template, e.g. `%(title)s.%(ext)s`. Sanitized. |
142
+ | `headers` | Extra HTTP headers, e.g. `{ Referer: '...' }`. |
143
+ | `userAgent` | Custom `User-Agent`. |
144
+ | `referer` | Custom `Referer`. |
145
+ | `proxy` | Proxy URL. |
146
+ | `cookies.path` | Path to a Netscape-format cookies file. |
147
+ | `playlist.enabled` | Default `false` — a playlist URL downloads only the first item unless enabled. |
148
+ | `playlist.start` / `playlist.end` | Playlist item range (1-based). |
149
+ | `subtitles.enabled` | Write subtitles. |
150
+ | `subtitles.languages` | e.g. `['en', 'bn']`. |
151
+ | `subtitles.autoGenerated` | Also write auto-generated subtitles. |
152
+ | `network.timeout` | Socket timeout in seconds. |
153
+ | `network.retries` | Number of retries. |
154
+ | `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. |
155
+
156
+ ## FFmpeg support
157
+
158
+ The bundled `yt-dlp-android` build ships **no FFmpeg**, and this package does
159
+ not bundle one either — yt-dlp merges separate streams by spawning a real
160
+ `ffmpeg` executable, so an in-process JNI wrapper (e.g. FFmpegKit) is not
161
+ enough. If you have an ffmpeg binary on the device (e.g. extracted into your
162
+ app's files directory), pass its path and `bestvideo+bestaudio` works:
163
+
164
+ ```ts
165
+ const task = await YtDlp.download({
166
+ url,
167
+ format: 'bestvideo+bestaudio',
168
+ merge: true,
169
+ ffmpeg: {
170
+ location: '/data/user/0/com.example.app/files/ffmpeg/ffmpeg',
171
+ },
172
+ });
173
+ ```
174
+
175
+ `yt-dlp` accepts either the binary path or the directory that contains the
176
+ `ffmpeg` executable. With `ffmpeg.location` set, the following features are
177
+ handed to yt-dlp instead of being rejected:
178
+
179
+ - `merge: true` (e.g. `bestvideo+bestaudio`)
180
+ - audio extraction / re-encoding
181
+ - metadata embedding
182
+ - thumbnail embedding
183
+
184
+ If the location does not exist, the download fails immediately with
185
+ `PROCESSING_FAILED`. Without `ffmpeg.location`, the features above are still
186
+ rejected up front with a clear error rather than silently ignored, and you
187
+ should request a single stream (`best`, `bestaudio`, `best[height<=720]`,
188
+ etc.) that needs no post-processing.
189
+
190
+ ## Instagram, TikTok and other social sites
191
+
192
+ yt-dlp supports Instagram (posts, Reels, many stories) and TikTok, among
193
+ hundreds of other sites. Without FFmpeg you must request a **single
194
+ progressive stream**:
195
+
196
+ ```ts
197
+ format: 'best' // preferred
198
+ // or
199
+ format: 'best[height<=1080]'
200
+ format: 'bestaudio'
201
+ ```
202
+
203
+ Many Instagram Reels and TikTok videos already provide combined progressive
204
+ MP4 streams, so downloads often work without extra processing. If the only
205
+ high-quality options are separate video + audio streams, provide an ffmpeg
206
+ binary via `ffmpeg.location` (see above) or the download will fail with
207
+ `PROCESSING_FAILED`.
208
+
209
+ **Cookies tip:** Instagram almost always requires a valid logged-in session
210
+ (Netscape cookies file via `cookies.path`). TikTok sometimes needs them too
211
+ for full reliability.
212
+
213
+ ## Download task
214
+
215
+ A `DownloadTask` exposes:
216
+
217
+ - `id: string`
218
+ - `cancel(): Promise<void>`
219
+ - `pause(): Promise<boolean>`
220
+ - `resume(): Promise<boolean>`
221
+ - `getStatus(): Promise<DownloadStatus>`
222
+ - `getProgress(): Promise<DownloadProgress | null>`
223
+ - `addListener(event, listener): Subscription`
224
+
225
+ Statuses: `queued | extracting | downloading | processing | paused | completed |
226
+ cancelled | failed`.
227
+
228
+ Events:
229
+
230
+ - `progress` → `DownloadProgress` (`percent`, `downloadedBytes`, `totalBytes`,
231
+ `speedBytesPerSecond`, `etaSeconds`, `filename`, `phase`)
232
+ - `state` `{ taskId, status }`
233
+ - `completed` `DownloadResult` (`taskId`, `path`, `filename`, `size`)
234
+ - `error` `YtDlpError`
235
+
236
+ Progress events are throttled to ~200 ms and numeric fields are `undefined`
237
+ when the value is unknown (never `NaN`).
238
+
239
+ ### Pause & resume
240
+
241
+ ```ts
242
+ const task = await YtDlp.download({ url, format: 'bestvideo+bestaudio', ffmpeg: { location } });
243
+
244
+ await task.pause(); // true yt-dlp aborts on the next progress tick
245
+ console.log(await task.getStatus()); // 'paused'
246
+
247
+ await task.resume(); // true — same options re-run; continues from the .part file
248
+ ```
249
+
250
+ `YtDlp.pause(taskId)` and `YtDlp.resume(taskId)` work the same way when you
251
+ only have a task id (e.g. after app re-creation).
252
+
253
+ How it works:
254
+
255
+ - `pause()` cooperatively aborts the download from the progress hook. yt-dlp
256
+ leaves its `.part` file on disk, and the task stays registered with status
257
+ `paused` (so a paused task still exists; cancel it if you're done).
258
+ - `resume()` re-runs the exact same download. yt-dlp continues by default
259
+ (`continue`): byte-range where the source server supports it, fragment-based
260
+ resumes (DASH/HLS) via the `.ytdl` sidecar. If a source does not support
261
+ continuing, yt-dlp restarts that file.
262
+ - Both return `false` when the action is not applicable (unknown task, not
263
+ paused, already finished). Cancelling a paused task finalizes it immediately.
264
+ - No network is consumed while paused, but the pause takes effect on the next
265
+ yt-dlp progress tick (~200 ms). Pausing does **not** cover the download's
266
+ extraction phase.
267
+
268
+ `pause()`/`resume()` are process-local and do not survive native restarts
269
+ (see "Limitations").
270
+
271
+ ## Background downloads
272
+
273
+ While at least one download is running, the module promotes the app with a
274
+ native Android **foreground service** (`dataSync` type), so downloads keep
275
+ going when the app is backgrounded, the screen is off, or the app is swiped
276
+ away. No opt-in needed — starting a download shows an ongoing notification
277
+ with live progress and a **Cancel** action (cancels all active downloads);
278
+ tapping the notification reopens the app. When the last download finishes
279
+ (or fails / is cancelled), the service stops and the notification goes away.
280
+
281
+ ```ts
282
+ const task = await YtDlp.download({ url });
283
+ // app backgrounded here — the download continues
284
+ task.addListener('progress', (p) => console.log(p.percent));
285
+ ```
286
+
287
+ What it does and does not cover:
288
+
289
+ - **Survives:** backgrounding, screen off, task swipe-away.
290
+ - **Does not survive:** the process being killed by the system, device
291
+ reboot, or a native restart. Tasks are process-local: after a restart,
292
+ re-issue downloads (and consider `pause()`/`resume()` semantics for
293
+ partial files left on disk).
294
+ - The service holds a partial wake lock while active, so screen-off
295
+ downloads are not stalled by CPU sleep.
296
+ - On Android 13+ the host app should request the `POST_NOTIFICATIONS`
297
+ runtime permission, otherwise the progress notification is suppressed
298
+ (the download still runs).
299
+ - On Android 15+, the system may time-box `dataSync` foreground services
300
+ (around 6 hours); multi-hour downloads can be stopped by the OS.
301
+ - Cancellation from the notification stops every active download; per-task
302
+ control stays in the app UI via `task.cancel()`.
303
+
304
+ `react-native-continued-task` (see below) remains a complementary option if
305
+ you also want WorkManager-backed scheduling or system progress UI beyond
306
+ this built-in service.
307
+
308
+ ## Errors
309
+
310
+ All failures normalize to `YtDlpError` with a `code`:
311
+
312
+ `INVALID_URL`, `EXTRACTION_FAILED`, `DOWNLOAD_FAILED`, `CANCELLED`,
313
+ `FORMAT_UNAVAILABLE`, `NETWORK_ERROR`, `AUTHENTICATION_REQUIRED`,
314
+ `GEO_RESTRICTED`, `PRIVATE_CONTENT`, `AGE_RESTRICTED`, `PROCESSING_FAILED`,
315
+ `STORAGE_ERROR`, `INIT_FAILED`, `UNSUPPORTED_PLATFORM`, `UNKNOWN`.
316
+
317
+ ```ts
318
+ import { YtDlpError } from 'ytdlp-react-native';
319
+
320
+ try {
321
+ await YtDlp.extractInfo(url);
322
+ } catch (error) {
323
+ if (error instanceof YtDlpError) {
324
+ console.log(error.code, error.message);
325
+ }
326
+ }
327
+ ```
328
+
329
+ Raw native stack traces are never surfaced to the user.
330
+
331
+ ## Storage
332
+
333
+ Files are written to app-specific external storage:
334
+
335
+ ```
336
+ Android/data/<your-package>/files/yt-dlp/<output.directory>/...
337
+ ```
338
+
339
+ Filenames and directory segments are sanitized against illegal characters,
340
+ path traversal, excessive length and empty names. This avoids dangerous
341
+ permissions like `MANAGE_EXTERNAL_STORAGE`. The returned `DownloadResult.path`
342
+ is an absolute path inside your app's own storage.
343
+
344
+ ## Limitations
345
+
346
+ - **Android only.**
347
+ - **FFmpeg is not bundled.** Merging separate video + audio streams and other
348
+ post-processing only work when you supply an ffmpeg executable via
349
+ `ffmpeg.location` (see "FFmpeg support"). Without it, those features are
350
+ rejected with `PROCESSING_FAILED`.
351
+ - **Background execution is best-effort within a live process.** Active
352
+ downloads run under a `dataSync` foreground service (see "Background
353
+ downloads"), but downloads do not survive the process being killed or the
354
+ device rebooting. Per-process pause/resume does not survive a native
355
+ restart. Persisting task IDs lets you re-issue cancellation later.
356
+ - yt-dlp site support changes frequently. Not every website works forever,
357
+ and not every site provides every field.
358
+ - This package does **not** bundle or provide a way to update the embedded
359
+ yt-dlp at runtime.
360
+ - Playlists are opt-in via `playlist.enabled` to avoid accidental bulk
361
+ downloads.
362
+
363
+ ## Recommended production stack
364
+
365
+ `ytdlp-react-native` focuses on reliable extraction and downloading of single
366
+ streams. For a complete video-downloader experience you will usually combine
367
+ it with three complementary libraries:
368
+
369
+ | Concern | Package | Link |
370
+ |---------|---------|------|
371
+ | Long-running background downloads with system progress UI | **react-native-continued-task** | [GitHub](https://github.com/mahdidavoodi7/react-native-continued-task) |
372
+ | Merge video+audio, re-encode, burn subtitles, etc. | **munim-ffmpeg** | [GitHub](https://github.com/munimtechnologies/munim-ffmpeg) |
373
+ | Save finished files to the public Media Library / Gallery | **expo-media-library** | [Expo docs](https://docs.expo.dev/versions/latest/sdk/media-library/) |
374
+
375
+ ### Suggested flow
376
+
377
+ 1. Start a continued background task with `react-native-continued-task`
378
+ (shows Live Activity on iOS 26+ / foreground-service notification on Android).
379
+ 2. Download with `ytdlp-react-native` using a single-stream format (`best` /
380
+ `bestaudio`). Report progress back to the continued task.
381
+ 3. (Optional) If you need to merge separate streams or perform other
382
+ post-processing, use `munim-ffmpeg` (in-process FFmpegKit). Note that
383
+ FFmpegKit-style packages run FFmpeg in-process and **cannot** provide the
384
+ executable `ytdlp-react-native`'s `ffmpeg.location` option needs — that
385
+ option must point to a real ffmpeg binary on disk.
386
+ 4. Move the final file into the user’s public gallery with `expo-media-library`.
387
+
388
+ All three packages (plus this one) require a **development build** — they do
389
+ not work inside Expo Go.
390
+
391
+ > Note: basic background continuity (foreground service + progress
392
+ > notification + cancel) is built into `ytdlp-react-native` (see "Background
393
+ > downloads"). Reach for `react-native-continued-task` when you additionally
394
+ > want WorkManager-backed scheduling or richer system UI.
395
+
396
+ ## Legal & responsible use
397
+
398
+ `ytdlp-react-native` is a technical wrapper around yt-dlp. It does not circumvent
399
+ DRM (Widevine, FairPlay, PlayReady, ...), bypass authentication, or access
400
+ private or unauthorized content — content that requires DRM or authentication
401
+ will fail with an error.
402
+
403
+ You are responsible for complying with:
404
+
405
+ - website terms of service
406
+ - copyright law and content licenses
407
+ - authentication rules
408
+ - platform policies
409
+
410
+ Do not use this library to download content you do not have the right to
411
+ download.
412
+
413
+ ## Third-party licenses
414
+
415
+ | Component | License |
416
+ | --- | --- |
417
+ | `ytdlp-react-native` (this package) | MIT |
418
+ | `yt-dlp-android` (Maven `dev.ffmpegkit-maintained:yt-dlp-android`) | MIT |
419
+ | [yt-dlp](https://github.com/yt-dlp/yt-dlp) | Unlicense |
420
+ | [Chaquopy](https://chaquo.com/chaquopy/) | BSD-style (per the embedded distribution) |
421
+
422
+ Re-verify third-party licenses at release time.