@rntp/player 5.0.0-beta.4 → 5.0.0-beta.6

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 (56) hide show
  1. package/android/build.gradle +7 -0
  2. package/android/src/main/java/com/doublesymmetry/trackplayer/SleepTimerController.kt +128 -0
  3. package/android/src/main/java/com/doublesymmetry/trackplayer/TrackPlayerModule.kt +40 -0
  4. package/android/src/main/java/com/doublesymmetry/trackplayer/TrackPlayerPlaybackService.kt +99 -87
  5. package/android/src/main/java/com/doublesymmetry/trackplayer/models/PlayerConfig.kt +12 -1
  6. package/android/src/test/java/com/doublesymmetry/trackplayer/ExoPlayerIntegrationTest.kt +319 -0
  7. package/android/src/test/java/com/doublesymmetry/trackplayer/SleepTimerIntegrationTest.kt +473 -0
  8. package/android/src/test/java/com/doublesymmetry/trackplayer/SleepTimerStateTest.kt +58 -0
  9. package/android/src/test/java/com/doublesymmetry/trackplayer/models/BrowseNavigationTest.kt +215 -0
  10. package/android/src/test/java/com/doublesymmetry/trackplayer/models/BrowseTreeTest.kt +166 -0
  11. package/android/src/test/java/com/doublesymmetry/trackplayer/models/EmitEventTest.kt +68 -0
  12. package/android/src/test/java/com/doublesymmetry/trackplayer/models/PlayerConfigTest.kt +400 -0
  13. package/android/src/test/java/com/doublesymmetry/trackplayer/models/TrackPlayerMediaItemTest.kt +380 -0
  14. package/android/src/test/resources/robolectric.properties +1 -0
  15. package/ios/TrackPlayer.swift +47 -101
  16. package/ios/TrackPlayerBridge.mm +2 -0
  17. package/ios/player/AVPlayerEngine.swift +47 -35
  18. package/ios/player/AudioCache.swift +34 -0
  19. package/ios/player/AudioPlayer.swift +70 -22
  20. package/ios/player/CacheProxyServer.swift +429 -0
  21. package/ios/player/DownloadCoordinator.swift +242 -0
  22. package/ios/player/Preloader.swift +21 -90
  23. package/ios/player/SleepTimerController.swift +147 -0
  24. package/ios/tests/AVPlayerEngineIntegrationTests.swift +230 -0
  25. package/ios/tests/AudioPlayerTests.swift +6 -0
  26. package/ios/tests/CacheProxyServerTests.swift +403 -0
  27. package/ios/tests/DownloadCoordinatorTests.swift +197 -0
  28. package/ios/tests/LocalAudioServer.swift +171 -0
  29. package/ios/tests/MockPlayerEngine.swift +1 -0
  30. package/ios/tests/QueueManagerTests.swift +6 -0
  31. package/ios/tests/SleepTimerIntegrationTests.swift +408 -0
  32. package/ios/tests/SleepTimerTests.swift +70 -0
  33. package/lib/commonjs/NativeTrackPlayer.js.map +1 -1
  34. package/lib/commonjs/audio.js +19 -0
  35. package/lib/commonjs/audio.js.map +1 -1
  36. package/lib/commonjs/interfaces/PlayerConfig.js +1 -1
  37. package/lib/commonjs/interfaces/PlayerConfig.js.map +1 -1
  38. package/lib/module/NativeTrackPlayer.js.map +1 -1
  39. package/lib/module/audio.js +17 -0
  40. package/lib/module/audio.js.map +1 -1
  41. package/lib/module/interfaces/PlayerConfig.js +1 -1
  42. package/lib/module/interfaces/PlayerConfig.js.map +1 -1
  43. package/lib/typescript/src/NativeTrackPlayer.d.ts +2 -0
  44. package/lib/typescript/src/NativeTrackPlayer.d.ts.map +1 -1
  45. package/lib/typescript/src/audio.d.ts +12 -1
  46. package/lib/typescript/src/audio.d.ts.map +1 -1
  47. package/lib/typescript/src/interfaces/MediaItem.d.ts +4 -1
  48. package/lib/typescript/src/interfaces/MediaItem.d.ts.map +1 -1
  49. package/lib/typescript/src/interfaces/PlayerConfig.d.ts +21 -2
  50. package/lib/typescript/src/interfaces/PlayerConfig.d.ts.map +1 -1
  51. package/package.json +4 -1
  52. package/src/NativeTrackPlayer.ts +4 -0
  53. package/src/audio.ts +18 -0
  54. package/src/interfaces/MediaItem.ts +4 -1
  55. package/src/interfaces/PlayerConfig.ts +24 -3
  56. package/ios/player/CachingResourceLoader.swift +0 -273
@@ -1,273 +0,0 @@
1
- //
2
- // Copyright (c) Double Symmetry GmbH
3
- // Commercial use requires a license. See https://rntp.dev/pricing
4
- //
5
-
6
- import AVFoundation
7
- import UniformTypeIdentifiers
8
-
9
- final class CachingResourceLoader: NSObject, AVAssetResourceLoaderDelegate, URLSessionDataDelegate {
10
-
11
- private let cache: AudioCache
12
- let originalURL: URL
13
- private let headers: [String: String]?
14
- let cacheKey: String
15
-
16
- private var pendingRequests: [AVAssetResourceLoadingRequest] = []
17
- private var session: URLSession!
18
- private var activeTask: URLSessionDataTask?
19
-
20
- /// Byte offset of the next data chunk the active download will deliver.
21
- private var downloadCursor: Int64 = 0
22
- /// True when the download started at the cache boundary, so incoming data
23
- /// can be appended sequentially.
24
- private var downloadIsCacheable: Bool = false
25
-
26
- // MARK: - Init
27
-
28
- init(url: URL, headers: [String: String]?, cache: AudioCache, queue: DispatchQueue) {
29
- self.cache = cache
30
- self.originalURL = url
31
- self.headers = headers
32
- self.cacheKey = cache.cacheKey(for: url)
33
- super.init()
34
-
35
- let opQueue = OperationQueue()
36
- opQueue.underlyingQueue = queue
37
- opQueue.maxConcurrentOperationCount = 1
38
- self.session = URLSession(configuration: .default, delegate: self, delegateQueue: opQueue)
39
- }
40
-
41
- func invalidate() {
42
- activeTask?.cancel()
43
- activeTask = nil
44
- session.invalidateAndCancel()
45
- for request in pendingRequests where !request.isFinished && !request.isCancelled {
46
- request.finishLoading(with: URLError(.cancelled))
47
- }
48
- pendingRequests.removeAll()
49
- }
50
-
51
- // MARK: - URL Scheme Helpers
52
-
53
- static func cacheURL(from original: URL) -> URL? {
54
- guard var components = URLComponents(url: original, resolvingAgainstBaseURL: false) else { return nil }
55
- guard let scheme = components.scheme else { return nil }
56
- components.scheme = "trackplayer-\(scheme)"
57
- return components.url
58
- }
59
-
60
- static func originalURL(from cacheURL: URL) -> URL? {
61
- guard var components = URLComponents(url: cacheURL, resolvingAgainstBaseURL: false) else { return nil }
62
- guard let scheme = components.scheme, scheme.hasPrefix("trackplayer-") else { return nil }
63
- components.scheme = String(scheme.dropFirst("trackplayer-".count))
64
- return components.url
65
- }
66
-
67
- // MARK: - AVAssetResourceLoaderDelegate
68
-
69
- func resourceLoader(
70
- _ resourceLoader: AVAssetResourceLoader,
71
- shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest
72
- ) -> Bool {
73
- pendingRequests.append(loadingRequest)
74
- processRequests()
75
- return true
76
- }
77
-
78
- func resourceLoader(
79
- _ resourceLoader: AVAssetResourceLoader,
80
- didCancel loadingRequest: AVAssetResourceLoadingRequest
81
- ) {
82
- pendingRequests.removeAll { $0 === loadingRequest }
83
- if pendingRequests.isEmpty {
84
- activeTask?.cancel()
85
- activeTask = nil
86
- }
87
- }
88
-
89
- // MARK: - Request Processing
90
-
91
- private func processRequests() {
92
- var completed: [AVAssetResourceLoadingRequest] = []
93
-
94
- for request in pendingRequests {
95
- guard !request.isFinished && !request.isCancelled else {
96
- completed.append(request)
97
- continue
98
- }
99
-
100
- let infoReady = fillContentInfo(request)
101
- let dataReady = fillData(request)
102
-
103
- if infoReady && dataReady {
104
- request.finishLoading()
105
- completed.append(request)
106
- }
107
- }
108
-
109
- pendingRequests.removeAll { req in completed.contains { $0 === req } }
110
-
111
- if !pendingRequests.isEmpty {
112
- ensureDownload()
113
- }
114
- }
115
-
116
- private func fillContentInfo(_ request: AVAssetResourceLoadingRequest) -> Bool {
117
- guard let infoRequest = request.contentInformationRequest else { return true }
118
- guard let info = cache.contentInfo(for: cacheKey) else { return false }
119
-
120
- if let utType = UTType(mimeType: info.contentType) {
121
- infoRequest.contentType = utType.identifier
122
- }
123
- infoRequest.contentLength = info.contentLength
124
- infoRequest.isByteRangeAccessSupported = info.isByteRangeAccessSupported
125
- return true
126
- }
127
-
128
- private func fillData(_ request: AVAssetResourceLoadingRequest) -> Bool {
129
- guard let dataRequest = request.dataRequest else { return true }
130
-
131
- let currentOffset = dataRequest.currentOffset
132
- let requestedEnd = requestEnd(for: dataRequest)
133
- let cachedBytes = cache.cachedBytes(for: cacheKey)
134
-
135
- if currentOffset < cachedBytes {
136
- let readable = min(requestedEnd, cachedBytes)
137
- let length = Int(readable - currentOffset)
138
- if length > 0, let data = cache.readData(for: cacheKey, offset: currentOffset, length: length) {
139
- dataRequest.respond(with: data)
140
- }
141
- }
142
-
143
- return dataRequest.currentOffset >= requestedEnd
144
- }
145
-
146
- private func requestEnd(for dataRequest: AVAssetResourceLoadingDataRequest) -> Int64 {
147
- if dataRequest.requestsAllDataToEndOfResource {
148
- return cache.contentInfo(for: cacheKey)?.contentLength ?? Int64.max
149
- }
150
- return dataRequest.requestedOffset + Int64(dataRequest.requestedLength)
151
- }
152
-
153
- // MARK: - Download
154
-
155
- private func ensureDownload() {
156
- guard activeTask == nil else { return }
157
-
158
- guard let dataRequest = pendingRequests.first(where: { $0.dataRequest != nil })?.dataRequest else { return }
159
-
160
- let neededOffset = dataRequest.currentOffset
161
- let cachedBytes = cache.cachedBytes(for: cacheKey)
162
-
163
- guard neededOffset >= cachedBytes else { return }
164
-
165
- downloadCursor = neededOffset
166
- downloadIsCacheable = (neededOffset == cachedBytes)
167
-
168
- var request = URLRequest(url: originalURL)
169
- headers?.forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
170
- request.setValue("bytes=\(neededOffset)-", forHTTPHeaderField: "Range")
171
-
172
- activeTask = session.dataTask(with: request)
173
- activeTask?.resume()
174
- }
175
-
176
- // MARK: - URLSessionDataDelegate
177
-
178
- func urlSession(
179
- _ session: URLSession,
180
- dataTask: URLSessionDataTask,
181
- didReceive response: URLResponse,
182
- completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
183
- ) {
184
- if cache.contentInfo(for: cacheKey) == nil, let http = response as? HTTPURLResponse {
185
- let mime = http.mimeType ?? "audio/mpeg"
186
- var totalLength: Int64 = -1
187
-
188
- if http.statusCode == 206, let rangeHeader = http.value(forHTTPHeaderField: "Content-Range") {
189
- if let slashIdx = rangeHeader.lastIndex(of: "/") {
190
- totalLength = Int64(rangeHeader[rangeHeader.index(after: slashIdx)...]) ?? -1
191
- }
192
- } else {
193
- totalLength = response.expectedContentLength
194
- }
195
-
196
- let byteRange = http.statusCode == 206
197
- let info = AudioCache.ContentInfo(
198
- contentType: mime,
199
- contentLength: totalLength,
200
- isByteRangeAccessSupported: byteRange
201
- )
202
- cache.storeContentInfo(info, for: cacheKey, url: originalURL)
203
- }
204
-
205
- completionHandler(.allow)
206
- processRequests()
207
- }
208
-
209
- func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
210
- if downloadIsCacheable {
211
- cache.appendData(data, for: cacheKey)
212
- }
213
- downloadCursor += Int64(data.count)
214
-
215
- // Respond to pending data requests that fall within the just-downloaded range
216
- var completed: [AVAssetResourceLoadingRequest] = []
217
- for request in pendingRequests {
218
- guard !request.isFinished && !request.isCancelled else {
219
- completed.append(request)
220
- continue
221
- }
222
- let infoReady = fillContentInfo(request)
223
- let dataReady: Bool
224
-
225
- if downloadIsCacheable {
226
- dataReady = fillData(request)
227
- } else if let dataReq = request.dataRequest {
228
- let chunkStart = downloadCursor - Int64(data.count)
229
- let reqOffset = dataReq.currentOffset
230
- if reqOffset >= chunkStart && reqOffset < downloadCursor {
231
- let skip = Int(reqOffset - chunkStart)
232
- let subdata = data.subdata(in: skip..<data.count)
233
- dataReq.respond(with: subdata)
234
- }
235
- dataReady = dataReq.currentOffset >= requestEnd(for: dataReq)
236
- } else {
237
- dataReady = true
238
- }
239
-
240
- if infoReady && dataReady {
241
- request.finishLoading()
242
- completed.append(request)
243
- }
244
- }
245
-
246
- pendingRequests.removeAll { req in completed.contains { $0 === req } }
247
- }
248
-
249
- func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
250
- activeTask = nil
251
-
252
- if let error = error as? URLError, error.code == .cancelled { return }
253
-
254
- if let error = error {
255
- for request in pendingRequests where !request.isFinished && !request.isCancelled {
256
- request.finishLoading(with: error)
257
- }
258
- pendingRequests.removeAll()
259
- return
260
- }
261
-
262
- if downloadIsCacheable {
263
- cache.touchAccessDate(for: cacheKey)
264
- cache.evictIfNeeded()
265
- }
266
-
267
- processRequests()
268
-
269
- if !pendingRequests.isEmpty {
270
- ensureDownload()
271
- }
272
- }
273
- }