react-native-hls-cache 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -1,233 +1,73 @@
1
1
  # react-native-hls-cache
2
2
 
3
- HLS video caching for React Native — intercepts segment requests via a local TCP proxy, caches them to disk, and serves subsequent playback from cache.
4
-
5
- - **iOS** — full proxy implementation (Swift, Network framework)
6
- - **Android** — no-op; ExoPlayer/Media3 handle caching natively
7
- - **Bridge** — [Nitro Modules](https://nitro.margelo.com/) (type-safe FFI)
8
-
9
- ---
3
+ HSL
10
4
 
11
5
  ## Installation
12
6
 
7
+
13
8
  ```sh
14
9
  npm install react-native-hls-cache react-native-nitro-modules
15
- # or
16
- yarn add react-native-hls-cache react-native-nitro-modules
17
- ```
18
-
19
- > `react-native-nitro-modules` is a required peer dependency.
20
-
21
- ---
22
-
23
- ## Quick Start
24
-
25
- ```tsx
26
- import { startServer, convertUrl } from 'react-native-hls-cache';
27
- import { useVideoPlayer, VideoView } from 'expo-video';
28
- import { useEffect } from 'react';
29
10
 
30
- export default function App() {
31
- useEffect(() => {
32
- // Start the proxy once on app launch
33
- startServer();
34
- }, []);
35
-
36
- const remoteUrl = 'https://stream.mux.com/abc123.m3u8';
37
-
38
- const player = useVideoPlayer(
39
- convertUrl(remoteUrl), // rewrites to http://127.0.0.1:9000/proxy?url=...
40
- (p) => { p.loop = true; }
41
- );
42
-
43
- return <VideoView player={player} style={{ flex: 1 }} />;
44
- }
11
+ > `react-native-nitro-modules` is required as this library relies on [Nitro Modules](https://nitro.margelo.com/).
45
12
  ```
46
13
 
47
- ---
48
14
 
49
- ## API
15
+ ## Usage
50
16
 
51
- ### `startServer(port?, maxCacheSize?, headOnlyCache?)`
52
17
 
53
- Starts the local HLS proxy server. Call once on app mount.
18
+ ```js
19
+ import { multiply } from 'react-native-hls-cache';
54
20
 
55
- | Parameter | Type | Default | Description |
56
- |---|---|---|---|
57
- | `port` | `number` | `9000` | Local TCP port to bind |
58
- | `maxCacheSize` | `number` | `1073741824` | Max disk cache in bytes (1 GB) |
59
- | `headOnlyCache` | `boolean` | `false` | Only cache the first 3 segments per stream. Recommended for vertical video feeds |
21
+ // ...
60
22
 
61
- ```ts
62
- startServer(); // defaults
63
- startServer(9000, 512 * 1024 * 1024); // 512 MB cache
64
- startServer(9000, undefined, true); // head-only mode for feeds
23
+ const result = multiply(3, 7);
65
24
  ```
66
25
 
67
- - Synchronous and fast (< 1ms) — does not block the JS thread
68
- - Safe to call multiple times; no-op if already running on the same port
69
- - Throws with code `409` if a server is already running on a different port
70
- - No-op on Android / Web
71
-
72
- ---
73
-
74
- ### `convertUrl(url, isCacheable?)`
75
-
76
- Rewrites a remote HLS URL to route through the local proxy.
77
-
78
- ```ts
79
- const localUrl = convertUrl('https://cdn.example.com/video.m3u8');
80
- // → 'http://127.0.0.1:9000/proxy?url=https%3A%2F%2Fcdn.example.com%2Fvideo.m3u8'
81
- ```
82
-
83
- | Parameter | Type | Default | Description |
84
- |---|---|---|---|
85
- | `url` | `string` | — | Remote HLS URL |
86
- | `isCacheable` | `boolean` | `true` | Pass `false` to bypass the proxy and return the original URL |
87
-
88
- - Returns the original URL unchanged on Android / Web, or if the server is not running
89
- - Synchronous — safe to call inline during render
90
-
91
- ---
92
-
93
- ### `clearCache()`
94
-
95
- Deletes all cached files from disk.
96
-
97
- ```ts
98
- await clearCache();
99
- ```
100
-
101
- - Runs on a background thread — does not block the JS thread
102
- - No-op on Android / Web
103
-
104
- ---
105
-
106
- ### `prefetch(url, segmentCount?)`
107
-
108
- Downloads an HLS stream into cache before the user plays it. Returns a `taskId` for cancellation.
109
-
110
- ```ts
111
- const taskId = prefetch('https://cdn.example.com/video.m3u8');
112
- const taskId = prefetch('https://cdn.example.com/video.m3u8', 5); // cache 5 segments
113
- ```
114
-
115
- | Parameter | Type | Default | Description |
116
- |---|---|---|---|
117
- | `url` | `string` | — | Remote HLS URL |
118
- | `segmentCount` | `number` | `3` | Number of segments to prefetch |
119
-
120
- **What it does:**
121
- 1. Downloads the master playlist
122
- 2. Picks the lowest-bandwidth media playlist (lightweight for background work)
123
- 3. Downloads the first `segmentCount` segments into the disk cache
124
- 4. When the player requests those segments via the proxy → instant cache hit
125
26
 
126
- - Returns immediately — all work happens in the background
127
- - Returns `''` on Android / Web (no-op)
128
-
129
- ---
27
+ ## Contributing
130
28
 
131
- ### `cancelPrefetch(taskId)`
29
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
30
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
31
+ - [Code of conduct](CODE_OF_CONDUCT.md)
132
32
 
133
- Cancels a specific prefetch task.
33
+ ## License
134
34
 
135
- ```ts
136
- const taskId = prefetch(url);
137
- // later...
138
- cancelPrefetch(taskId);
139
- ```
35
+ MIT
140
36
 
141
37
  ---
142
38
 
143
- ### `cancelAllPrefetch()`
39
+ Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
144
40
 
145
- Cancels all active prefetch tasks.
146
41
 
147
- ```ts
148
- cancelAllPrefetch();
149
- ```
150
42
 
151
- ---
43
+ Looking at the current API (startServer, convertUrl, clearCache) and the internals, here's what would genuinely add value:
152
44
 
153
- ## Patterns
45
+ High Value
46
+ stopServer()
47
+ There's no way to stop the proxy from JS. Needed for cleanup on app background/logout.
154
48
 
155
- ### Vertical video feed (TikTok-style)
49
+ getCacheStats() { sizeBytes, fileCount, hitRate }
50
+ Developers need this to show cache info in settings screens and debug eviction behavior. The storage layer already has all the data.
156
51
 
157
- ```tsx
158
- import { startServer, convertUrl, prefetch, cancelAllPrefetch } from 'react-native-hls-cache';
52
+ invalidateUrl(url)
53
+ Right now clearCache() is all-or-nothing. Evicting a single video (e.g. when content is updated server-side) is a very common need.
159
54
 
160
- // App.tsx — start with head-only cache to minimise storage on feeds
161
- useEffect(() => {
162
- startServer(9000, 512 * 1024 * 1024, true);
163
- return () => cancelAllPrefetch();
164
- }, []);
55
+ prefetch(url)
56
+ Download a video into cache before the user plays it. The NetworkDownloader + DataSource pipeline is already there — this would just drive it without a proxy connection. Big UX win for predictable playback.
165
57
 
166
- // VideoItem.tsx — prefetch the next video as the user scrolls
167
- useEffect(() => {
168
- if (shouldPreload) {
169
- const taskId = prefetch(videoUrl);
170
- return () => cancelPrefetch(taskId);
171
- }
172
- }, [shouldPreload, videoUrl]);
58
+ Medium Value
59
+ isRunning: boolean property
60
+ JS callers currently have no way to check server state without trying to start it and catching an error.
173
61
 
174
- const player = useVideoPlayer(convertUrl(videoUrl), (p) => {
175
- p.loop = true;
176
- });
177
- ```
62
+ WiFi-only caching option
63
+ A wifiOnly flag on startServer — skip disk writes on cellular to avoid burning mobile data.
178
64
 
179
- ### Selective caching
65
+ Cache event callbacks onCacheHit / onCacheMiss
66
+ Useful for analytics (measuring actual cache efficiency in production).
180
67
 
181
- ```ts
182
- // Skip cache for live streams or DRM content
183
- const url = convertUrl(streamUrl, !isLive && !isDRM);
184
- ```
68
+ Lower Priority
69
+ Configurable cache directory — some apps have specific storage requirements (e.g. store in Documents for user-visible files vs Caches).
185
70
 
186
- ### Clear cache on logout
71
+ Resume interrupted downloads if a segment write was interrupted (app killed mid-download), the current code deletes it and re-fetches. A partial-resume strategy would save bandwidth for large segments.
187
72
 
188
- ```ts
189
- async function onLogout() {
190
- cancelAllPrefetch();
191
- await clearCache();
192
- }
193
- ```
194
-
195
- ---
196
-
197
- ## How It Works
198
-
199
- ```
200
- Media Player
201
- → HTTP request to localhost:9000/proxy?url=<encoded>
202
- → VideoProxyServer (NWListener, TCP)
203
- → ClientConnectionHandler (per-connection HTTP handler)
204
- → DataSource (cache-first router)
205
- ├── Cache hit → memory-mapped read from disk → instant response
206
- └── Cache miss → NetworkDownloader (priority queue, 32 concurrent)
207
- → stream to player + write to disk simultaneously
208
- ```
209
-
210
- **Cache storage** — files are keyed by SHA256 hash of the URL (+ byte range for fMP4 segments), stored in the system Caches directory under `ExpoVideoCache/`. LRU eviction runs 5 seconds after server start.
211
-
212
- **Manifest rewriting** — `.m3u8` responses are intercepted and all segment URLs are rewritten to route through the proxy, so every subsequent segment request is also cacheable.
213
-
214
- **Priority queue** — `.m3u8` manifests and small requests bypass the concurrency semaphore for instant playback startup. Heavy segment downloads are queued with a limit of 32 concurrent connections.
215
-
216
- ---
217
-
218
- ## Requirements
219
-
220
- - React Native 0.73+
221
- - iOS 15.1+
222
- - Android — no native requirements (no-op)
223
-
224
- ---
225
-
226
- ## Contributing
227
-
228
- - [Development workflow](CONTRIBUTING.md#development-workflow)
229
- - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
230
-
231
- ## License
232
-
233
- MIT
73
+ The three I'd ship first: stopServer, getCacheStats, and prefetch — they cover the most common real-world gaps without requiring deep architectural changes.
@@ -21,12 +21,4 @@ class HlsCache : HybridHlsCacheSpec() {
21
21
  override fun clearCache(): Promise<Unit> {
22
22
  return Promise.resolved(Unit)
23
23
  }
24
-
25
- override fun prefetch(url: String, segmentCount: Double?): String {
26
- return ""
27
- }
28
-
29
- override fun cancelPrefetch(taskId: String): Unit {}
30
-
31
- override fun cancelAllPrefetch(): Unit {}
32
24
  }
@@ -100,14 +100,7 @@ internal final class DataSource: NetworkDownloaderDelegate {
100
100
  }
101
101
  return
102
102
  }
103
-
104
- // Range-keyed file not found — fall back to the full file that prefetch may have
105
- // stored without a range suffix. Slice the requested bytes out of it on the fly.
106
- if range != nil && !isManifest && storage.exists(for: url.absoluteString) {
107
- DataSource.diskQueue.async { self.serveRangeFromFullFile() }
108
- return
109
- }
110
-
103
+
111
104
  if isManifest {
112
105
  downloadManifest()
113
106
  } else {
@@ -179,37 +172,6 @@ internal final class DataSource: NetworkDownloaderDelegate {
179
172
  delegate?.didComplete(error: nil)
180
173
  }
181
174
 
182
- /// Serves a byte-range slice from a full file that was cached by prefetch (no range suffix).
183
- private func serveRangeFromFullFile() {
184
- let path = storage.getFilePath(for: url.absoluteString)
185
- guard let fullData = try? Data(contentsOf: path, options: .mappedIfSafe),
186
- let r = range else {
187
- // Full file unreadable — fall through to network.
188
- startStreamDownload()
189
- return
190
- }
191
-
192
- let fileSize = fullData.count
193
- let lower = r.lowerBound
194
- let upper = r.upperBound == Int.max ? fileSize : min(r.upperBound, fileSize)
195
-
196
- guard lower < fileSize else {
197
- delegate?.didComplete(error: NSError(domain: "RangeError", code: 416))
198
- return
199
- }
200
-
201
- let sliced = Data(fullData[lower..<upper])
202
- let headers: [String: String] = [
203
- "Content-Type": getMimeType(url: url),
204
- "Content-Length": "\(sliced.count)",
205
- "Accept-Ranges": "bytes",
206
- "Content-Range": "bytes \(lower)-\(upper - 1)/\(fileSize)"
207
- ]
208
- delegate?.didReceiveHeaders(headers: headers, status: 206)
209
- delegate?.didReceiveData(data: sliced)
210
- delegate?.didComplete(error: nil)
211
- }
212
-
213
175
  // MARK: - Network Path (Cache Miss)
214
176
 
215
177
  private func startStreamDownload() {
@@ -76,6 +76,7 @@ public class HybridHlsCache: HybridHlsCacheSpec {
76
76
 
77
77
  /// Heavy async method — file I/O runs on a background thread via Promise.async.
78
78
  public func clearCache() throws -> Promise<Void> {
79
+ // Capture server ref synchronously (JS thread) before jumping to background.
79
80
  stateLock.lock()
80
81
  let server = proxyServer
81
82
  stateLock.unlock()
@@ -88,34 +89,4 @@ public class HybridHlsCache: HybridHlsCacheSpec {
88
89
  }
89
90
  }
90
91
  }
91
-
92
- /// Prefetches an HLS stream into cache. Returns a taskId for cancellation.
93
- /// Runs entirely in the background — JS thread returns immediately.
94
- public func prefetch(url: String, segmentCount: Double?) throws -> String {
95
- guard let parsedURL = URL(string: url) else {
96
- throw NSError(
97
- domain: "HlsCache",
98
- code: 400,
99
- userInfo: [NSLocalizedDescriptionKey: "Invalid URL: \(url)"]
100
- )
101
- }
102
-
103
- stateLock.lock()
104
- let server = proxyServer
105
- stateLock.unlock()
106
-
107
- // Reuse the server's storage if running, otherwise create a temporary one.
108
- let storage = server?.storage ?? VideoCacheStorage(maxCacheSize: 1_073_741_824)
109
- let count = segmentCount.map { Int($0) } ?? 3
110
-
111
- return PrefetchManager.shared.prefetch(url: parsedURL, storage: storage, segmentCount: count)
112
- }
113
-
114
- public func cancelPrefetch(taskId: String) throws -> Void {
115
- PrefetchManager.shared.cancel(taskId: taskId)
116
- }
117
-
118
- public func cancelAllPrefetch() throws -> Void {
119
- PrefetchManager.shared.cancelAll()
120
- }
121
92
  }
@@ -25,7 +25,7 @@ internal final class VideoProxyServer: ProxyConnectionDelegate {
25
25
  private var listener: NWListener?
26
26
 
27
27
  /// Disk-backed storage used for caching video data.
28
- internal let storage: VideoCacheStorage
28
+ private let storage: VideoCacheStorage
29
29
 
30
30
  /// The local port on which the server listens for incoming connections.
31
31
  internal let port: Int
@@ -41,31 +41,4 @@ export function clearCache() {
41
41
  if (Platform.OS !== 'ios') return Promise.resolve();
42
42
  return getInstance().clearCache();
43
43
  }
44
-
45
- /**
46
- * Prefetches an HLS stream into the local cache in the background.
47
- * Downloads the manifest + first `segmentCount` segments (default: 3).
48
- *
49
- * @returns A taskId that can be passed to `cancelPrefetch` to stop the download.
50
- */
51
- export function prefetch(url, segmentCount) {
52
- if (Platform.OS !== 'ios') return '';
53
- return getInstance().prefetch(url, segmentCount);
54
- }
55
-
56
- /**
57
- * Cancels a specific prefetch task by its taskId.
58
- */
59
- export function cancelPrefetch(taskId) {
60
- if (Platform.OS !== 'ios') return;
61
- getInstance().cancelPrefetch(taskId);
62
- }
63
-
64
- /**
65
- * Cancels all active prefetch tasks.
66
- */
67
- export function cancelAllPrefetch() {
68
- if (Platform.OS !== 'ios') return;
69
- getInstance().cancelAllPrefetch();
70
- }
71
44
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["NitroModules","Platform","_instance","getInstance","createHybridObject","startServer","port","maxCacheSize","headOnlyCache","OS","convertUrl","url","isCacheable","clearCache","Promise","resolve","prefetch","segmentCount","cancelPrefetch","taskId","cancelAllPrefetch"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,4BAA4B;AACzD,SAASC,QAAQ,QAAQ,cAAc;AAGvC,IAAIC,SAA0B,GAAG,IAAI;AAErC,SAASC,WAAWA,CAAA,EAAa;EAC/B,IAAID,SAAS,IAAI,IAAI,EAAE;IACrBA,SAAS,GAAGF,YAAY,CAACI,kBAAkB,CAAW,UAAU,CAAC;EACnE;EACA,OAAOF,SAAS;AAClB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASG,WAAWA,CACzBC,IAAa,EACbC,YAAqB,EACrBC,aAAuB,EACjB;EACN,IAAIP,QAAQ,CAACQ,EAAE,KAAK,KAAK,EAAE;EAC3BN,WAAW,CAAC,CAAC,CAACE,WAAW,CAACC,IAAI,EAAEC,YAAY,EAAEC,aAAa,CAAC;AAC9D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASE,UAAUA,CAACC,GAAW,EAAEC,WAAqB,EAAU;EACrE,IAAIX,QAAQ,CAACQ,EAAE,KAAK,KAAK,EAAE,OAAOE,GAAG;EACrC,OAAOR,WAAW,CAAC,CAAC,CAACO,UAAU,CAACC,GAAG,EAAEC,WAAW,CAAC;AACnD;;AAEA;AACA;AACA;AACA,OAAO,SAASC,UAAUA,CAAA,EAAkB;EAC1C,IAAIZ,QAAQ,CAACQ,EAAE,KAAK,KAAK,EAAE,OAAOK,OAAO,CAACC,OAAO,CAAC,CAAC;EACnD,OAAOZ,WAAW,CAAC,CAAC,CAACU,UAAU,CAAC,CAAC;AACnC;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASG,QAAQA,CAACL,GAAW,EAAEM,YAAqB,EAAU;EACnE,IAAIhB,QAAQ,CAACQ,EAAE,KAAK,KAAK,EAAE,OAAO,EAAE;EACpC,OAAON,WAAW,CAAC,CAAC,CAACa,QAAQ,CAACL,GAAG,EAAEM,YAAY,CAAC;AAClD;;AAEA;AACA;AACA;AACA,OAAO,SAASC,cAAcA,CAACC,MAAc,EAAQ;EACnD,IAAIlB,QAAQ,CAACQ,EAAE,KAAK,KAAK,EAAE;EAC3BN,WAAW,CAAC,CAAC,CAACe,cAAc,CAACC,MAAM,CAAC;AACtC;;AAEA;AACA;AACA;AACA,OAAO,SAASC,iBAAiBA,CAAA,EAAS;EACxC,IAAInB,QAAQ,CAACQ,EAAE,KAAK,KAAK,EAAE;EAC3BN,WAAW,CAAC,CAAC,CAACiB,iBAAiB,CAAC,CAAC;AACnC","ignoreList":[]}
1
+ {"version":3,"names":["NitroModules","Platform","_instance","getInstance","createHybridObject","startServer","port","maxCacheSize","headOnlyCache","OS","convertUrl","url","isCacheable","clearCache","Promise","resolve"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,4BAA4B;AACzD,SAASC,QAAQ,QAAQ,cAAc;AAGvC,IAAIC,SAA0B,GAAG,IAAI;AAErC,SAASC,WAAWA,CAAA,EAAa;EAC/B,IAAID,SAAS,IAAI,IAAI,EAAE;IACrBA,SAAS,GAAGF,YAAY,CAACI,kBAAkB,CAAW,UAAU,CAAC;EACnE;EACA,OAAOF,SAAS;AAClB;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASG,WAAWA,CACzBC,IAAa,EACbC,YAAqB,EACrBC,aAAuB,EACjB;EACN,IAAIP,QAAQ,CAACQ,EAAE,KAAK,KAAK,EAAE;EAC3BN,WAAW,CAAC,CAAC,CAACE,WAAW,CAACC,IAAI,EAAEC,YAAY,EAAEC,aAAa,CAAC;AAC9D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASE,UAAUA,CAACC,GAAW,EAAEC,WAAqB,EAAU;EACrE,IAAIX,QAAQ,CAACQ,EAAE,KAAK,KAAK,EAAE,OAAOE,GAAG;EACrC,OAAOR,WAAW,CAAC,CAAC,CAACO,UAAU,CAACC,GAAG,EAAEC,WAAW,CAAC;AACnD;;AAEA;AACA;AACA;AACA,OAAO,SAASC,UAAUA,CAAA,EAAkB;EAC1C,IAAIZ,QAAQ,CAACQ,EAAE,KAAK,KAAK,EAAE,OAAOK,OAAO,CAACC,OAAO,CAAC,CAAC;EACnD,OAAOZ,WAAW,CAAC,CAAC,CAACU,UAAU,CAAC,CAAC;AACnC","ignoreList":[]}
@@ -6,8 +6,5 @@ export interface HlsCache extends HybridObject<{
6
6
  startServer(port?: number, maxCacheSize?: number, headOnlyCache?: boolean): void;
7
7
  convertUrl(url: string, isCacheable?: boolean): string;
8
8
  clearCache(): Promise<void>;
9
- prefetch(url: string, segmentCount?: number): string;
10
- cancelPrefetch(taskId: string): void;
11
- cancelAllPrefetch(): void;
12
9
  }
13
10
  //# sourceMappingURL=HlsCache.nitro.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"HlsCache.nitro.d.ts","sourceRoot":"","sources":["../../../src/HlsCache.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,MAAM,WAAW,QACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACzD,WAAW,CACT,IAAI,CAAC,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,MAAM,EACrB,aAAa,CAAC,EAAE,OAAO,GACtB,IAAI,CAAC;IACR,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IACvD,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrD,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,iBAAiB,IAAI,IAAI,CAAC;CAC3B"}
1
+ {"version":3,"file":"HlsCache.nitro.d.ts","sourceRoot":"","sources":["../../../src/HlsCache.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D,MAAM,WAAW,QACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACzD,WAAW,CACT,IAAI,CAAC,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,MAAM,EACrB,aAAa,CAAC,EAAE,OAAO,GACtB,IAAI,CAAC;IACR,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IACvD,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7B"}
@@ -18,19 +18,4 @@ export declare function convertUrl(url: string, isCacheable?: boolean): string;
18
18
  * Purges all cached video files from disk.
19
19
  */
20
20
  export declare function clearCache(): Promise<void>;
21
- /**
22
- * Prefetches an HLS stream into the local cache in the background.
23
- * Downloads the manifest + first `segmentCount` segments (default: 3).
24
- *
25
- * @returns A taskId that can be passed to `cancelPrefetch` to stop the download.
26
- */
27
- export declare function prefetch(url: string, segmentCount?: number): string;
28
- /**
29
- * Cancels a specific prefetch task by its taskId.
30
- */
31
- export declare function cancelPrefetch(taskId: string): void;
32
- /**
33
- * Cancels all active prefetch tasks.
34
- */
35
- export declare function cancelAllPrefetch(): void;
36
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAaA;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,IAAI,CAAC,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,MAAM,EACrB,aAAa,CAAC,EAAE,OAAO,GACtB,IAAI,CAGN;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAGrE;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAGnE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAGnD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAGxC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAaA;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,IAAI,CAAC,EAAE,MAAM,EACb,YAAY,CAAC,EAAE,MAAM,EACrB,aAAa,CAAC,EAAE,OAAO,GACtB,IAAI,CAGN;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAGrE;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
@@ -72,18 +72,5 @@ namespace margelo::nitro::hlscache {
72
72
  return __promise;
73
73
  }();
74
74
  }
75
- std::string JHybridHlsCacheSpec::prefetch(const std::string& url, std::optional<double> segmentCount) {
76
- static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<jni::JString>(jni::alias_ref<jni::JString> /* url */, jni::alias_ref<jni::JDouble> /* segmentCount */)>("prefetch");
77
- auto __result = method(_javaPart, jni::make_jstring(url), segmentCount.has_value() ? jni::JDouble::valueOf(segmentCount.value()) : nullptr);
78
- return __result->toStdString();
79
- }
80
- void JHybridHlsCacheSpec::cancelPrefetch(const std::string& taskId) {
81
- static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JString> /* taskId */)>("cancelPrefetch");
82
- method(_javaPart, jni::make_jstring(taskId));
83
- }
84
- void JHybridHlsCacheSpec::cancelAllPrefetch() {
85
- static const auto method = _javaPart->javaClassStatic()->getMethod<void()>("cancelAllPrefetch");
86
- method(_javaPart);
87
- }
88
75
 
89
76
  } // namespace margelo::nitro::hlscache
@@ -57,9 +57,6 @@ namespace margelo::nitro::hlscache {
57
57
  void startServer(std::optional<double> port, std::optional<double> maxCacheSize, std::optional<bool> headOnlyCache) override;
58
58
  std::string convertUrl(const std::string& url, std::optional<bool> isCacheable) override;
59
59
  std::shared_ptr<Promise<void>> clearCache() override;
60
- std::string prefetch(const std::string& url, std::optional<double> segmentCount) override;
61
- void cancelPrefetch(const std::string& taskId) override;
62
- void cancelAllPrefetch() override;
63
60
 
64
61
  private:
65
62
  jni::global_ref<JHybridHlsCacheSpec::JavaPart> _javaPart;
@@ -40,18 +40,6 @@ abstract class HybridHlsCacheSpec: HybridObject() {
40
40
  @DoNotStrip
41
41
  @Keep
42
42
  abstract fun clearCache(): Promise<Unit>
43
-
44
- @DoNotStrip
45
- @Keep
46
- abstract fun prefetch(url: String, segmentCount: Double?): String
47
-
48
- @DoNotStrip
49
- @Keep
50
- abstract fun cancelPrefetch(taskId: String): Unit
51
-
52
- @DoNotStrip
53
- @Keep
54
- abstract fun cancelAllPrefetch(): Unit
55
43
 
56
44
  // Default implementation of `HybridObject.toString()`
57
45
  override fun toString(): String {
@@ -90,26 +90,6 @@ namespace margelo::nitro::hlscache {
90
90
  auto __value = std::move(__result.value());
91
91
  return __value;
92
92
  }
93
- inline std::string prefetch(const std::string& url, std::optional<double> segmentCount) override {
94
- auto __result = _swiftPart.prefetch(url, segmentCount);
95
- if (__result.hasError()) [[unlikely]] {
96
- std::rethrow_exception(__result.error());
97
- }
98
- auto __value = std::move(__result.value());
99
- return __value;
100
- }
101
- inline void cancelPrefetch(const std::string& taskId) override {
102
- auto __result = _swiftPart.cancelPrefetch(taskId);
103
- if (__result.hasError()) [[unlikely]] {
104
- std::rethrow_exception(__result.error());
105
- }
106
- }
107
- inline void cancelAllPrefetch() override {
108
- auto __result = _swiftPart.cancelAllPrefetch();
109
- if (__result.hasError()) [[unlikely]] {
110
- std::rethrow_exception(__result.error());
111
- }
112
- }
113
93
 
114
94
  private:
115
95
  HlsCache::HybridHlsCacheSpec_cxx _swiftPart;
@@ -16,9 +16,6 @@ public protocol HybridHlsCacheSpec_protocol: HybridObject {
16
16
  func startServer(port: Double?, maxCacheSize: Double?, headOnlyCache: Bool?) throws -> Void
17
17
  func convertUrl(url: String, isCacheable: Bool?) throws -> String
18
18
  func clearCache() throws -> Promise<Void>
19
- func prefetch(url: String, segmentCount: Double?) throws -> String
20
- func cancelPrefetch(taskId: String) throws -> Void
21
- func cancelAllPrefetch() throws -> Void
22
19
  }
23
20
 
24
21
  public extension HybridHlsCacheSpec_protocol {
@@ -193,45 +193,4 @@ open class HybridHlsCacheSpec_cxx {
193
193
  return bridge.create_Result_std__shared_ptr_Promise_void___(__exceptionPtr)
194
194
  }
195
195
  }
196
-
197
- @inline(__always)
198
- public final func prefetch(url: std.string, segmentCount: bridge.std__optional_double_) -> bridge.Result_std__string_ {
199
- do {
200
- let __result = try self.__implementation.prefetch(url: String(url), segmentCount: { () -> Double? in
201
- if bridge.has_value_std__optional_double_(segmentCount) {
202
- let __unwrapped = bridge.get_std__optional_double_(segmentCount)
203
- return __unwrapped
204
- } else {
205
- return nil
206
- }
207
- }())
208
- let __resultCpp = std.string(__result)
209
- return bridge.create_Result_std__string_(__resultCpp)
210
- } catch (let __error) {
211
- let __exceptionPtr = __error.toCpp()
212
- return bridge.create_Result_std__string_(__exceptionPtr)
213
- }
214
- }
215
-
216
- @inline(__always)
217
- public final func cancelPrefetch(taskId: std.string) -> bridge.Result_void_ {
218
- do {
219
- try self.__implementation.cancelPrefetch(taskId: String(taskId))
220
- return bridge.create_Result_void_()
221
- } catch (let __error) {
222
- let __exceptionPtr = __error.toCpp()
223
- return bridge.create_Result_void_(__exceptionPtr)
224
- }
225
- }
226
-
227
- @inline(__always)
228
- public final func cancelAllPrefetch() -> bridge.Result_void_ {
229
- do {
230
- try self.__implementation.cancelAllPrefetch()
231
- return bridge.create_Result_void_()
232
- } catch (let __error) {
233
- let __exceptionPtr = __error.toCpp()
234
- return bridge.create_Result_void_(__exceptionPtr)
235
- }
236
- }
237
196
  }
@@ -17,9 +17,6 @@ namespace margelo::nitro::hlscache {
17
17
  prototype.registerHybridMethod("startServer", &HybridHlsCacheSpec::startServer);
18
18
  prototype.registerHybridMethod("convertUrl", &HybridHlsCacheSpec::convertUrl);
19
19
  prototype.registerHybridMethod("clearCache", &HybridHlsCacheSpec::clearCache);
20
- prototype.registerHybridMethod("prefetch", &HybridHlsCacheSpec::prefetch);
21
- prototype.registerHybridMethod("cancelPrefetch", &HybridHlsCacheSpec::cancelPrefetch);
22
- prototype.registerHybridMethod("cancelAllPrefetch", &HybridHlsCacheSpec::cancelAllPrefetch);
23
20
  });
24
21
  }
25
22
 
@@ -53,9 +53,6 @@ namespace margelo::nitro::hlscache {
53
53
  virtual void startServer(std::optional<double> port, std::optional<double> maxCacheSize, std::optional<bool> headOnlyCache) = 0;
54
54
  virtual std::string convertUrl(const std::string& url, std::optional<bool> isCacheable) = 0;
55
55
  virtual std::shared_ptr<Promise<void>> clearCache() = 0;
56
- virtual std::string prefetch(const std::string& url, std::optional<double> segmentCount) = 0;
57
- virtual void cancelPrefetch(const std::string& taskId) = 0;
58
- virtual void cancelAllPrefetch() = 0;
59
56
 
60
57
  protected:
61
58
  // Hybrid Setup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-hls-cache",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "HSL",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -9,7 +9,4 @@ export interface HlsCache
9
9
  ): void;
10
10
  convertUrl(url: string, isCacheable?: boolean): string;
11
11
  clearCache(): Promise<void>;
12
- prefetch(url: string, segmentCount?: number): string;
13
- cancelPrefetch(taskId: string): void;
14
- cancelAllPrefetch(): void;
15
12
  }
package/src/index.tsx CHANGED
@@ -46,30 +46,3 @@ export function clearCache(): Promise<void> {
46
46
  if (Platform.OS !== 'ios') return Promise.resolve();
47
47
  return getInstance().clearCache();
48
48
  }
49
-
50
- /**
51
- * Prefetches an HLS stream into the local cache in the background.
52
- * Downloads the manifest + first `segmentCount` segments (default: 3).
53
- *
54
- * @returns A taskId that can be passed to `cancelPrefetch` to stop the download.
55
- */
56
- export function prefetch(url: string, segmentCount?: number): string {
57
- if (Platform.OS !== 'ios') return '';
58
- return getInstance().prefetch(url, segmentCount);
59
- }
60
-
61
- /**
62
- * Cancels a specific prefetch task by its taskId.
63
- */
64
- export function cancelPrefetch(taskId: string): void {
65
- if (Platform.OS !== 'ios') return;
66
- getInstance().cancelPrefetch(taskId);
67
- }
68
-
69
- /**
70
- * Cancels all active prefetch tasks.
71
- */
72
- export function cancelAllPrefetch(): void {
73
- if (Platform.OS !== 'ios') return;
74
- getInstance().cancelAllPrefetch();
75
- }
@@ -1,235 +0,0 @@
1
- import Foundation
2
-
3
- /// Manages background prefetch tasks for HLS streams.
4
- ///
5
- /// Each prefetch task downloads the manifest + first N segments into the disk cache
6
- /// so they are instantly served from cache when the video player requests them.
7
- internal final class PrefetchManager {
8
-
9
- static let shared = PrefetchManager()
10
-
11
- private let lock = NSLock()
12
- private var activeTasks: [String: PrefetchTask] = [:]
13
-
14
- /// Starts prefetching an HLS stream. Returns a taskId that can be used to cancel.
15
- func prefetch(url: URL, storage: VideoCacheStorage, segmentCount: Int) -> String {
16
- let taskId = UUID().uuidString
17
- let task = PrefetchTask(url: url, storage: storage, segmentCount: segmentCount)
18
-
19
- lock.lock()
20
- activeTasks[taskId] = task
21
- lock.unlock()
22
-
23
- task.start { [weak self] in
24
- self?.lock.lock()
25
- self?.activeTasks.removeValue(forKey: taskId)
26
- self?.lock.unlock()
27
- }
28
-
29
- return taskId
30
- }
31
-
32
- func cancel(taskId: String) {
33
- lock.lock()
34
- let task = activeTasks.removeValue(forKey: taskId)
35
- lock.unlock()
36
- task?.cancel()
37
- }
38
-
39
- func cancelAll() {
40
- lock.lock()
41
- let tasks = Array(activeTasks.values)
42
- activeTasks.removeAll()
43
- lock.unlock()
44
- tasks.forEach { $0.cancel() }
45
- }
46
- }
47
-
48
- // MARK: - PrefetchTask
49
-
50
- private final class PrefetchTask {
51
-
52
- private let url: URL
53
- private let storage: VideoCacheStorage
54
- private let segmentCount: Int
55
-
56
- private let lock = NSLock()
57
- private var networkTasks: [NetworkTask] = []
58
- private var isCancelled = false
59
-
60
- init(url: URL, storage: VideoCacheStorage, segmentCount: Int) {
61
- self.url = url
62
- self.storage = storage
63
- self.segmentCount = segmentCount
64
- }
65
-
66
- func start(onComplete: @escaping () -> Void) {
67
- fetchManifest(url: url, onComplete: onComplete)
68
- }
69
-
70
- func cancel() {
71
- lock.lock()
72
- isCancelled = true
73
- let tasks = networkTasks
74
- networkTasks.removeAll()
75
- lock.unlock()
76
- tasks.forEach { $0.cancel() }
77
- }
78
-
79
- // MARK: - Private
80
-
81
- private func fetchManifest(url: URL, onComplete: @escaping () -> Void) {
82
- let task = NetworkDownloader.shared.downloadManifest(url: url) { [weak self] data, error in
83
- guard let self = self, !self.isCancelled else { onComplete(); return }
84
- guard let data = data, let content = String(data: data, encoding: .utf8) else {
85
- onComplete(); return
86
- }
87
- self.storage.save(data: data, for: url.absoluteString)
88
- self.processManifest(content: content, baseURL: url, onComplete: onComplete)
89
- }
90
- addNetworkTask(task)
91
- }
92
-
93
- private func processManifest(content: String, baseURL: URL, onComplete: @escaping () -> Void) {
94
- if content.contains("#EXT-X-STREAM-INF") {
95
- // Master playlist — cache ALL variant manifests so any quality AVPlayer picks is available.
96
- let variantURLs = parseAllVariantURLs(content: content, baseURL: baseURL)
97
- guard !variantURLs.isEmpty else { onComplete(); return }
98
-
99
- let group = DispatchGroup()
100
- for variantURL in variantURLs {
101
- guard !isCancelled else { break }
102
- group.enter()
103
- fetchManifest(url: variantURL, onComplete: { group.leave() })
104
- }
105
- group.notify(queue: .global(qos: .background)) { onComplete() }
106
- } else {
107
- // Media playlist — download init segment (if any) + first N segments.
108
- var urls: [URL] = []
109
- if let initURL = parseInitSegmentURL(content: content, baseURL: baseURL) {
110
- urls.append(initURL)
111
- }
112
- urls.append(contentsOf: parseSegmentURLs(content: content, baseURL: baseURL))
113
- downloadSegments(urls: urls, onComplete: onComplete)
114
- }
115
- }
116
-
117
- private func downloadSegments(urls: [URL], onComplete: @escaping () -> Void) {
118
- guard !urls.isEmpty else { onComplete(); return }
119
-
120
- let group = DispatchGroup()
121
-
122
- for url in urls {
123
- guard !isCancelled else { break }
124
- if storage.exists(for: url.absoluteString) { continue }
125
-
126
- group.enter()
127
- let delegate = SegmentAccumulator(storage: storage, url: url) {
128
- group.leave()
129
- }
130
- let task = NetworkDownloader.shared.download(url: url, range: nil, delegate: delegate)
131
- addNetworkTask(task)
132
- }
133
-
134
- group.notify(queue: .global(qos: .background)) { [weak self] in
135
- _ = self // retain until done
136
- onComplete()
137
- }
138
- }
139
-
140
- private func addNetworkTask(_ task: NetworkTask) {
141
- lock.lock()
142
- networkTasks.append(task)
143
- lock.unlock()
144
- }
145
-
146
- // MARK: - HLS Parsing
147
-
148
- /// Returns ALL variant/rendition playlist URLs from a master playlist.
149
- /// Caching every variant ensures AVPlayer can pick any quality while offline.
150
- private func parseAllVariantURLs(content: String, baseURL: URL) -> [URL] {
151
- var urls: [URL] = []
152
- let lines = content.components(separatedBy: .newlines)
153
- var i = 0
154
- while i < lines.count {
155
- let line = lines[i].trimmingCharacters(in: .whitespaces)
156
- if line.hasPrefix("#EXT-X-STREAM-INF") {
157
- let nextIndex = i + 1
158
- if nextIndex < lines.count {
159
- let urlLine = lines[nextIndex].trimmingCharacters(in: .whitespaces)
160
- if !urlLine.isEmpty, !urlLine.hasPrefix("#"),
161
- let resolved = URL(string: urlLine, relativeTo: baseURL)?.absoluteURL {
162
- urls.append(resolved)
163
- }
164
- }
165
- }
166
- i += 1
167
- }
168
- return urls
169
- }
170
-
171
- /// Parses the `#EXT-X-MAP` init segment URI from a media playlist.
172
- /// fMP4 streams require the init segment for every quality level.
173
- private func parseInitSegmentURL(content: String, baseURL: URL) -> URL? {
174
- for line in content.components(separatedBy: .newlines) {
175
- let trimmed = line.trimmingCharacters(in: .whitespaces)
176
- guard trimmed.hasPrefix("#EXT-X-MAP:") else { continue }
177
- if let uriStart = trimmed.range(of: "URI=\""),
178
- let uriEnd = trimmed.range(of: "\"", range: uriStart.upperBound..<trimmed.endIndex) {
179
- let uri = String(trimmed[uriStart.upperBound..<uriEnd.lowerBound])
180
- return URL(string: uri, relativeTo: baseURL)?.absoluteURL
181
- }
182
- }
183
- return nil
184
- }
185
-
186
- /// Parses up to `segmentCount` media segment URLs from a media playlist.
187
- private func parseSegmentURLs(content: String, baseURL: URL) -> [URL] {
188
- var urls: [URL] = []
189
- for line in content.components(separatedBy: .newlines) {
190
- guard urls.count < segmentCount else { break }
191
- let trimmed = line.trimmingCharacters(in: .whitespaces)
192
- guard !trimmed.isEmpty, !trimmed.hasPrefix("#") else { continue }
193
- if let resolved = URL(string: trimmed, relativeTo: baseURL)?.absoluteURL {
194
- urls.append(resolved)
195
- }
196
- }
197
- return urls
198
- }
199
- }
200
-
201
- // MARK: - SegmentAccumulator
202
-
203
- /// Accumulates streaming segment data and saves it to cache on completion.
204
- private final class SegmentAccumulator: NetworkDownloaderDelegate {
205
-
206
- private let storage: VideoCacheStorage
207
- private let url: URL
208
- private let onComplete: () -> Void
209
- private var buffer = Data()
210
- private let lock = NSLock()
211
-
212
- init(storage: VideoCacheStorage, url: URL, onComplete: @escaping () -> Void) {
213
- self.storage = storage
214
- self.url = url
215
- self.onComplete = onComplete
216
- }
217
-
218
- func didReceiveResponse(task: NetworkTask, response: URLResponse) {}
219
-
220
- func didReceiveData(task: NetworkTask, data: Data) {
221
- lock.lock()
222
- buffer.append(data)
223
- lock.unlock()
224
- }
225
-
226
- func didComplete(task: NetworkTask, error: Error?) {
227
- if error == nil {
228
- lock.lock()
229
- let data = buffer
230
- lock.unlock()
231
- storage.save(data: data, for: url.absoluteString)
232
- }
233
- onComplete()
234
- }
235
- }