react-native-hls-cache 0.1.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 (52) hide show
  1. package/HlsCache.podspec +29 -0
  2. package/LICENSE +20 -0
  3. package/README.md +233 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +118 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  8. package/android/src/main/java/com/margelo/nitro/hlscache/HlsCache.kt +32 -0
  9. package/android/src/main/java/com/margelo/nitro/hlscache/HlsCachePackage.kt +22 -0
  10. package/ios/ClientConnectionHandler.swift +234 -0
  11. package/ios/DataSource.swift +394 -0
  12. package/ios/HlsCache.swift +121 -0
  13. package/ios/NetworkDownloader.swift +220 -0
  14. package/ios/PrefetchManager.swift +235 -0
  15. package/ios/VideoCacheStorage.swift +235 -0
  16. package/ios/VideoProxyServer.swift +185 -0
  17. package/lib/module/HlsCache.nitro.js +4 -0
  18. package/lib/module/HlsCache.nitro.js.map +1 -0
  19. package/lib/module/index.js +71 -0
  20. package/lib/module/index.js.map +1 -0
  21. package/lib/module/package.json +1 -0
  22. package/lib/typescript/package.json +1 -0
  23. package/lib/typescript/src/HlsCache.nitro.d.ts +13 -0
  24. package/lib/typescript/src/HlsCache.nitro.d.ts.map +1 -0
  25. package/lib/typescript/src/index.d.ts +36 -0
  26. package/lib/typescript/src/index.d.ts.map +1 -0
  27. package/nitro.json +23 -0
  28. package/nitrogen/generated/android/c++/JHybridHlsCacheSpec.cpp +89 -0
  29. package/nitrogen/generated/android/c++/JHybridHlsCacheSpec.hpp +68 -0
  30. package/nitrogen/generated/android/hlscache+autolinking.cmake +81 -0
  31. package/nitrogen/generated/android/hlscache+autolinking.gradle +27 -0
  32. package/nitrogen/generated/android/hlscacheOnLoad.cpp +54 -0
  33. package/nitrogen/generated/android/hlscacheOnLoad.hpp +34 -0
  34. package/nitrogen/generated/android/kotlin/com/margelo/nitro/hlscache/HybridHlsCacheSpec.kt +75 -0
  35. package/nitrogen/generated/android/kotlin/com/margelo/nitro/hlscache/hlscacheOnLoad.kt +35 -0
  36. package/nitrogen/generated/ios/HlsCache+autolinking.rb +60 -0
  37. package/nitrogen/generated/ios/HlsCache-Swift-Cxx-Bridge.cpp +49 -0
  38. package/nitrogen/generated/ios/HlsCache-Swift-Cxx-Bridge.hpp +160 -0
  39. package/nitrogen/generated/ios/HlsCache-Swift-Cxx-Umbrella.hpp +46 -0
  40. package/nitrogen/generated/ios/HlsCacheAutolinking.mm +33 -0
  41. package/nitrogen/generated/ios/HlsCacheAutolinking.swift +26 -0
  42. package/nitrogen/generated/ios/c++/HybridHlsCacheSpecSwift.cpp +11 -0
  43. package/nitrogen/generated/ios/c++/HybridHlsCacheSpecSwift.hpp +118 -0
  44. package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
  45. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
  46. package/nitrogen/generated/ios/swift/HybridHlsCacheSpec.swift +60 -0
  47. package/nitrogen/generated/ios/swift/HybridHlsCacheSpec_cxx.swift +237 -0
  48. package/nitrogen/generated/shared/c++/HybridHlsCacheSpec.cpp +26 -0
  49. package/nitrogen/generated/shared/c++/HybridHlsCacheSpec.hpp +69 -0
  50. package/package.json +174 -0
  51. package/src/HlsCache.nitro.ts +15 -0
  52. package/src/index.tsx +75 -0
@@ -0,0 +1,235 @@
1
+ import Foundation
2
+ import CryptoKit
3
+
4
+ /// Manages the disk persistence layer for video caching.
5
+ ///
6
+ /// `VideoCacheStorage` is responsible for handling all filesystem-level
7
+ /// operations related to video caching, including:
8
+ /// - Generating deterministic, filesystem-safe filenames from remote URLs
9
+ /// - Reading and writing cached video data
10
+ /// - Supporting incremental (streaming) writes
11
+ /// - Enforcing a disk size limit using a Least Recently Used (LRU) strategy
12
+ ///
13
+ /// This class is designed to be lightweight, deterministic, and resilient to
14
+ /// filesystem errors. All operations are best-effort and never throw, ensuring
15
+ /// that cache failures do not impact video playback.
16
+ internal final class VideoCacheStorage {
17
+
18
+ // MARK: - Properties
19
+
20
+ /// The file manager instance used for all filesystem operations.
21
+ private let fileManager = FileManager.default
22
+
23
+ /// The maximum allowed size of the cache, in bytes.
24
+ ///
25
+ /// When the total size of cached files exceeds this limit, the cache
26
+ /// is pruned using an LRU (Least Recently Used) strategy.
27
+ private let maxCacheSize: Int
28
+
29
+ /// The root directory where all cached video files are stored.
30
+ ///
31
+ /// This directory is created inside the system Caches directory and
32
+ /// is guaranteed to exist after initialization.
33
+ private let cacheDirectory: URL
34
+
35
+ // MARK: - Initialization
36
+
37
+ /// Initializes a new video cache storage manager.
38
+ ///
39
+ /// During initialization, the cache directory is created if it does not
40
+ /// already exist.
41
+ ///
42
+ /// - Parameter maxCacheSize: The maximum allowed size of the cache in bytes.
43
+ init(maxCacheSize: Int) {
44
+ self.maxCacheSize = maxCacheSize
45
+
46
+ let paths = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)
47
+ self.cacheDirectory = paths[0].appendingPathComponent("ExpoVideoCache")
48
+
49
+ if !fileManager.fileExists(atPath: cacheDirectory.path) {
50
+ try? fileManager.createDirectory(
51
+ at: cacheDirectory,
52
+ withIntermediateDirectories: true
53
+ )
54
+ }
55
+ }
56
+
57
+ // MARK: - Core API
58
+
59
+ /// Removes all cached files from disk.
60
+ ///
61
+ /// This method deletes the entire cache directory and recreates it,
62
+ /// effectively resetting the cache to an empty state.
63
+ func clearAll() {
64
+ try? fileManager.removeItem(at: cacheDirectory)
65
+ try? fileManager.createDirectory(
66
+ at: cacheDirectory,
67
+ withIntermediateDirectories: true
68
+ )
69
+ }
70
+
71
+ /// Generates a deterministic, filesystem-safe file URL for a given key.
72
+ ///
73
+ /// The provided string is hashed using SHA256 to ensure:
74
+ /// - Collision resistance
75
+ /// - Consistent file paths across launches
76
+ /// - Compatibility with all filesystems
77
+ ///
78
+ /// The original file extension (if present) is preserved.
79
+ ///
80
+ /// - Parameter urlString: The remote URL or unique cache key.
81
+ /// - Returns: A local file URL corresponding to the hashed key.
82
+ func getFilePath(for urlString: String) -> URL {
83
+ guard let data = urlString.data(using: .utf8) else {
84
+ return cacheDirectory.appendingPathComponent("unknown.bin")
85
+ }
86
+
87
+ let hash = SHA256.hash(data: data)
88
+ let safeFilename = hash.map { String(format: "%02x", $0) }.joined()
89
+
90
+ var extensionName = "bin"
91
+ if let url = URL(string: urlString) {
92
+ let ext = url.pathExtension
93
+ if !ext.isEmpty {
94
+ extensionName = ext
95
+ }
96
+ }
97
+
98
+ return cacheDirectory.appendingPathComponent(
99
+ "\(safeFilename).\(extensionName)"
100
+ )
101
+ }
102
+
103
+ /// Determines whether a valid cached file exists for a given key.
104
+ ///
105
+ /// A file is considered valid only if:
106
+ /// - It exists on disk
107
+ /// - Its size is greater than zero bytes
108
+ ///
109
+ /// - Parameter urlString: The remote URL or unique cache key.
110
+ /// - Returns: `true` if a valid cached file exists, otherwise `false`.
111
+ func exists(for urlString: String) -> Bool {
112
+ let fileUrl = getFilePath(for: urlString)
113
+
114
+ if let attr = try? fileManager.attributesOfItem(
115
+ atPath: fileUrl.path
116
+ ),
117
+ let size = attr[.size] as? Int64,
118
+ size > 0 {
119
+ return true
120
+ }
121
+
122
+ return false
123
+ }
124
+
125
+ // MARK: - Reading & Writing
126
+
127
+ /// Reads the entire cached file into memory.
128
+ ///
129
+ /// This method is intended for small files such as manifests or metadata.
130
+ ///
131
+ /// - Parameter urlString: The remote URL or unique cache key.
132
+ /// - Returns: The cached file data if it exists, otherwise `nil`.
133
+ func getCachedData(for urlString: String) -> Data? {
134
+ let fileUrl = getFilePath(for: urlString)
135
+ return try? Data(contentsOf: fileUrl)
136
+ }
137
+
138
+ /// Saves data to disk atomically.
139
+ ///
140
+ /// The write operation replaces any existing file and guarantees that
141
+ /// partially written files are never left on disk.
142
+ ///
143
+ /// - Parameters:
144
+ /// - data: The data to be written to disk.
145
+ /// - urlString: The remote URL or unique cache key.
146
+ func save(data: Data, for urlString: String) {
147
+ let fileUrl = getFilePath(for: urlString)
148
+ try? data.write(to: fileUrl, options: .atomic)
149
+ }
150
+
151
+ /// Deletes the cached file associated with a given key.
152
+ ///
153
+ /// This is typically used when a download fails or cached data becomes invalid.
154
+ ///
155
+ /// - Parameter urlString: The remote URL or unique cache key.
156
+ func delete(for urlString: String) {
157
+ let url = getFilePath(for: urlString)
158
+ try? fileManager.removeItem(at: url)
159
+ }
160
+
161
+ // MARK: - Streaming API
162
+
163
+ /// Prepares a file for incremental (streaming) writes.
164
+ ///
165
+ /// Any existing file at the target path is deleted to ensure that
166
+ /// stale or partial data is not mixed with new content.
167
+ ///
168
+ /// - Parameter urlString: The remote URL or unique cache key.
169
+ /// - Returns: A `FileHandle` opened for writing, or `nil` if creation fails.
170
+ func initializeStreamFile(for urlString: String) -> FileHandle? {
171
+ let fileUrl = getFilePath(for: urlString)
172
+
173
+ if fileManager.fileExists(atPath: fileUrl.path) {
174
+ try? fileManager.removeItem(at: fileUrl)
175
+ }
176
+
177
+ fileManager.createFile(
178
+ atPath: fileUrl.path,
179
+ contents: nil
180
+ )
181
+
182
+ return try? FileHandle(forWritingTo: fileUrl)
183
+ }
184
+
185
+ // MARK: - Maintenance
186
+
187
+ /// Prunes the cache to enforce the configured size limit.
188
+ ///
189
+ /// This method removes the least recently modified files first until
190
+ /// the total cache size is within the allowed limit.
191
+ ///
192
+ /// All failures are silently ignored to ensure that cache maintenance
193
+ /// never interferes with normal application execution.
194
+ func prune() {
195
+ let keys: [URLResourceKey] = [
196
+ .fileSizeKey,
197
+ .contentModificationDateKey
198
+ ]
199
+
200
+ do {
201
+ let fileUrls = try fileManager.contentsOfDirectory(
202
+ at: cacheDirectory,
203
+ includingPropertiesForKeys: keys,
204
+ options: []
205
+ )
206
+
207
+ var totalSize = 0
208
+ var files: [(url: URL, size: Int, date: Date)] = []
209
+
210
+ for url in fileUrls {
211
+ let values = try url.resourceValues(forKeys: Set(keys))
212
+ if let size = values.fileSize,
213
+ let date = values.contentModificationDate {
214
+ totalSize += size
215
+ files.append((url, size, date))
216
+ }
217
+ }
218
+
219
+ guard totalSize >= maxCacheSize else { return }
220
+
221
+ files.sort { $0.date < $1.date }
222
+
223
+ for file in files {
224
+ try? fileManager.removeItem(at: file.url)
225
+ totalSize -= file.size
226
+ if totalSize < maxCacheSize {
227
+ break
228
+ }
229
+ }
230
+
231
+ } catch {
232
+ // Intentionally ignored
233
+ }
234
+ }
235
+ }
@@ -0,0 +1,185 @@
1
+ import Foundation
2
+ import Network
3
+
4
+ /// A delegate protocol for managing the lifecycle of child connections.
5
+ protocol ProxyConnectionDelegate: AnyObject {
6
+
7
+ /// Notifies the delegate that a connection handler has completed its work and can be released.
8
+ ///
9
+ /// - Parameter id: The unique identifier of the connection handler.
10
+ func connectionDidClose(id: String)
11
+ }
12
+
13
+ /// A TCP-based video proxy server built using Apple’s Network framework.
14
+ ///
15
+ /// `VideoProxyServer` is responsible for:
16
+ /// - Creating and managing a TCP listener
17
+ /// - Accepting incoming client connections
18
+ /// - Managing the lifecycle of active connection handlers
19
+ /// - Coordinating access to shared state in a thread-safe manner
20
+ ///
21
+ /// The server is designed to be idempotent, thread-safe, and resilient to listener failures.
22
+ internal final class VideoProxyServer: ProxyConnectionDelegate {
23
+
24
+ /// The underlying TCP listener.
25
+ private var listener: NWListener?
26
+
27
+ /// Disk-backed storage used for caching video data.
28
+ internal let storage: VideoCacheStorage
29
+
30
+ /// The local port on which the server listens for incoming connections.
31
+ internal let port: Int
32
+
33
+ /// A thread-safe registry of active client connection handlers.
34
+ private var activeHandlers: [String: ClientConnectionHandler] = [:]
35
+
36
+ /// Internal cached running state used to prevent data races.
37
+ private var _isRunning: Bool = false
38
+
39
+ /// A mutual exclusion lock used to protect all shared mutable state.
40
+ private let serverLock = NSLock()
41
+
42
+ /// If true, only the first few segments of each video are cached.
43
+ private let headOnlyCache: Bool
44
+
45
+ // Internal constant: How many segments to cache when headOnlyCache is true.
46
+ private let HEAD_SEGMENT_LIMIT = 3
47
+
48
+ /// Indicates whether the server is currently running.
49
+ ///
50
+ /// This property is thread-safe and reflects the authoritative server state.
51
+ var isRunning: Bool {
52
+ serverLock.lock()
53
+ defer { serverLock.unlock() }
54
+ return _isRunning
55
+ }
56
+
57
+ /// Creates a new video proxy server.
58
+ ///
59
+ /// - Parameters:
60
+ /// - port: The local TCP port to bind the listener to.
61
+ /// - maxCacheSize: The maximum allowed size of the disk cache, in bytes.
62
+ /// - headOnlyCache: If true, only the first few segments of each video are cached.
63
+ init(port: Int, maxCacheSize: Int, headOnlyCache: Bool = false) {
64
+ self.port = port
65
+ self.storage = VideoCacheStorage(maxCacheSize: maxCacheSize)
66
+ self.headOnlyCache = headOnlyCache
67
+ }
68
+
69
+ /// Starts the TCP server and begins accepting incoming connections.
70
+ ///
71
+ /// This method is thread-safe and prevents duplicate startup attempts.
72
+ ///
73
+ /// - Throws: An error if the provided port is invalid or the listener fails to initialize.
74
+ func start() throws {
75
+ serverLock.lock()
76
+ defer { serverLock.unlock() }
77
+
78
+ guard !_isRunning, listener == nil else {
79
+ return
80
+ }
81
+
82
+ let parameters = NWParameters.tcp
83
+ parameters.allowLocalEndpointReuse = true
84
+ parameters.serviceClass = .responsiveData
85
+
86
+ guard let localPort = NWEndpoint.Port(rawValue: UInt16(port)) else {
87
+ throw NSError(
88
+ domain: "VideoProxyServer",
89
+ code: 500,
90
+ userInfo: [NSLocalizedDescriptionKey: "Invalid port: \(port)"]
91
+ )
92
+ }
93
+
94
+ let listener = try NWListener(using: parameters, on: localPort)
95
+
96
+ listener.stateUpdateHandler = { [weak self] state in
97
+ guard let self = self else { return }
98
+ if case .failed = state {
99
+ DispatchQueue.global().async {
100
+ self.stop()
101
+ }
102
+ }
103
+ }
104
+
105
+ listener.newConnectionHandler = { [weak self] connection in
106
+ self?.handleNewConnection(connection)
107
+ }
108
+
109
+ listener.start(queue: .global(qos: .userInitiated))
110
+ self.listener = listener
111
+ self._isRunning = true
112
+
113
+ DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 5.0) { [weak self] in
114
+ self?.storage.prune()
115
+ }
116
+ }
117
+
118
+ /// Stops the server and terminates all active client connections.
119
+ ///
120
+ /// This method is fully idempotent and safe to call multiple times.
121
+ func stop() {
122
+ serverLock.lock()
123
+
124
+ guard _isRunning || listener != nil else {
125
+ serverLock.unlock()
126
+ return
127
+ }
128
+
129
+ _isRunning = false
130
+ listener?.cancel()
131
+ listener = nil
132
+
133
+ let handlersToStop = activeHandlers.values
134
+ activeHandlers.removeAll()
135
+
136
+ serverLock.unlock()
137
+
138
+ handlersToStop.forEach { $0.stop() }
139
+ }
140
+
141
+ /// Handles a newly accepted network connection.
142
+ ///
143
+ /// - Parameter connection: The incoming TCP connection.
144
+ private func handleNewConnection(_ connection: NWConnection) {
145
+ let limit = headOnlyCache ? HEAD_SEGMENT_LIMIT : 0
146
+
147
+ let handler = ClientConnectionHandler(
148
+ connection: connection,
149
+ storage: storage,
150
+ port: port,
151
+ initialSegmentsToCache: limit
152
+ )
153
+
154
+ handler.delegate = self
155
+
156
+ var shouldStart = false
157
+
158
+ serverLock.lock()
159
+ if _isRunning {
160
+ activeHandlers[handler.id] = handler
161
+ shouldStart = true
162
+ }
163
+ serverLock.unlock()
164
+
165
+ if shouldStart {
166
+ handler.start()
167
+ } else {
168
+ connection.cancel()
169
+ }
170
+ }
171
+
172
+ /// Removes a completed connection handler from the active registry.
173
+ ///
174
+ /// - Parameter id: The unique identifier of the closed connection.
175
+ func connectionDidClose(id: String) {
176
+ serverLock.lock()
177
+ defer { serverLock.unlock() }
178
+ activeHandlers.removeValue(forKey: id)
179
+ }
180
+
181
+ /// Clears all cached video data from persistent storage.
182
+ func clearCache() {
183
+ storage.clearAll()
184
+ }
185
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ export {};
4
+ //# sourceMappingURL=HlsCache.nitro.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../src","sources":["HlsCache.nitro.ts"],"mappings":"","ignoreList":[]}
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+
3
+ import { NitroModules } from 'react-native-nitro-modules';
4
+ import { Platform } from 'react-native';
5
+ let _instance = null;
6
+ function getInstance() {
7
+ if (_instance == null) {
8
+ _instance = NitroModules.createHybridObject('HlsCache');
9
+ }
10
+ return _instance;
11
+ }
12
+
13
+ /**
14
+ * Starts the local HLS proxy server.
15
+ *
16
+ * @param port - Local port to bind. Defaults to 9000.
17
+ * @param maxCacheSize - Max disk cache size in bytes. Defaults to 1 GB.
18
+ * @param headOnlyCache - If true, only caches the first ~3 segments per video (optimised for vertical feeds).
19
+ */
20
+ export function startServer(port, maxCacheSize, headOnlyCache) {
21
+ if (Platform.OS !== 'ios') return;
22
+ getInstance().startServer(port, maxCacheSize, headOnlyCache);
23
+ }
24
+
25
+ /**
26
+ * Rewrites a remote URL to route through the local proxy (iOS only).
27
+ * Returns the original URL unchanged on Android/Web or when the server is not running.
28
+ *
29
+ * @param url - Remote HLS URL (e.g. `https://cdn.example.com/video.m3u8`).
30
+ * @param isCacheable - Set to `false` to bypass the proxy. Defaults to `true`.
31
+ */
32
+ export function convertUrl(url, isCacheable) {
33
+ if (Platform.OS !== 'ios') return url;
34
+ return getInstance().convertUrl(url, isCacheable);
35
+ }
36
+
37
+ /**
38
+ * Purges all cached video files from disk.
39
+ */
40
+ export function clearCache() {
41
+ if (Platform.OS !== 'ios') return Promise.resolve();
42
+ return getInstance().clearCache();
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
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":[]}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,13 @@
1
+ import type { HybridObject } from 'react-native-nitro-modules';
2
+ export interface HlsCache extends HybridObject<{
3
+ ios: 'swift';
4
+ android: 'kotlin';
5
+ }> {
6
+ startServer(port?: number, maxCacheSize?: number, headOnlyCache?: boolean): void;
7
+ convertUrl(url: string, isCacheable?: boolean): string;
8
+ clearCache(): Promise<void>;
9
+ prefetch(url: string, segmentCount?: number): string;
10
+ cancelPrefetch(taskId: string): void;
11
+ cancelAllPrefetch(): void;
12
+ }
13
+ //# sourceMappingURL=HlsCache.nitro.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Starts the local HLS proxy server.
3
+ *
4
+ * @param port - Local port to bind. Defaults to 9000.
5
+ * @param maxCacheSize - Max disk cache size in bytes. Defaults to 1 GB.
6
+ * @param headOnlyCache - If true, only caches the first ~3 segments per video (optimised for vertical feeds).
7
+ */
8
+ export declare function startServer(port?: number, maxCacheSize?: number, headOnlyCache?: boolean): void;
9
+ /**
10
+ * Rewrites a remote URL to route through the local proxy (iOS only).
11
+ * Returns the original URL unchanged on Android/Web or when the server is not running.
12
+ *
13
+ * @param url - Remote HLS URL (e.g. `https://cdn.example.com/video.m3u8`).
14
+ * @param isCacheable - Set to `false` to bypass the proxy. Defaults to `true`.
15
+ */
16
+ export declare function convertUrl(url: string, isCacheable?: boolean): string;
17
+ /**
18
+ * Purges all cached video files from disk.
19
+ */
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
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/nitro.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "cxxNamespace": ["hlscache"],
3
+ "ios": {
4
+ "iosModuleName": "HlsCache"
5
+ },
6
+ "android": {
7
+ "androidNamespace": ["hlscache"],
8
+ "androidCxxLibName": "hlscache"
9
+ },
10
+ "autolinking": {
11
+ "HlsCache": {
12
+ "ios": {
13
+ "language": "swift",
14
+ "implementationClassName": "HybridHlsCache"
15
+ },
16
+ "android": {
17
+ "language": "kotlin",
18
+ "implementationClassName": "HlsCache"
19
+ }
20
+ }
21
+ },
22
+ "ignorePaths": ["node_modules"]
23
+ }