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.
- package/HlsCache.podspec +29 -0
- package/LICENSE +20 -0
- package/README.md +233 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +118 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/hlscache/HlsCache.kt +32 -0
- package/android/src/main/java/com/margelo/nitro/hlscache/HlsCachePackage.kt +22 -0
- package/ios/ClientConnectionHandler.swift +234 -0
- package/ios/DataSource.swift +394 -0
- package/ios/HlsCache.swift +121 -0
- package/ios/NetworkDownloader.swift +220 -0
- package/ios/PrefetchManager.swift +235 -0
- package/ios/VideoCacheStorage.swift +235 -0
- package/ios/VideoProxyServer.swift +185 -0
- package/lib/module/HlsCache.nitro.js +4 -0
- package/lib/module/HlsCache.nitro.js.map +1 -0
- package/lib/module/index.js +71 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/HlsCache.nitro.d.ts +13 -0
- package/lib/typescript/src/HlsCache.nitro.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +36 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +23 -0
- package/nitrogen/generated/android/c++/JHybridHlsCacheSpec.cpp +89 -0
- package/nitrogen/generated/android/c++/JHybridHlsCacheSpec.hpp +68 -0
- package/nitrogen/generated/android/hlscache+autolinking.cmake +81 -0
- package/nitrogen/generated/android/hlscache+autolinking.gradle +27 -0
- package/nitrogen/generated/android/hlscacheOnLoad.cpp +54 -0
- package/nitrogen/generated/android/hlscacheOnLoad.hpp +34 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/hlscache/HybridHlsCacheSpec.kt +75 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/hlscache/hlscacheOnLoad.kt +35 -0
- package/nitrogen/generated/ios/HlsCache+autolinking.rb +60 -0
- package/nitrogen/generated/ios/HlsCache-Swift-Cxx-Bridge.cpp +49 -0
- package/nitrogen/generated/ios/HlsCache-Swift-Cxx-Bridge.hpp +160 -0
- package/nitrogen/generated/ios/HlsCache-Swift-Cxx-Umbrella.hpp +46 -0
- package/nitrogen/generated/ios/HlsCacheAutolinking.mm +33 -0
- package/nitrogen/generated/ios/HlsCacheAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridHlsCacheSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridHlsCacheSpecSwift.hpp +118 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridHlsCacheSpec.swift +60 -0
- package/nitrogen/generated/ios/swift/HybridHlsCacheSpec_cxx.swift +237 -0
- package/nitrogen/generated/shared/c++/HybridHlsCacheSpec.cpp +26 -0
- package/nitrogen/generated/shared/c++/HybridHlsCacheSpec.hpp +69 -0
- package/package.json +174 -0
- package/src/HlsCache.nitro.ts +15 -0
- 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 @@
|
|
|
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
|
+
}
|