@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.
- package/android/build.gradle +7 -0
- package/android/src/main/java/com/doublesymmetry/trackplayer/SleepTimerController.kt +128 -0
- package/android/src/main/java/com/doublesymmetry/trackplayer/TrackPlayerModule.kt +40 -0
- package/android/src/main/java/com/doublesymmetry/trackplayer/TrackPlayerPlaybackService.kt +99 -87
- package/android/src/main/java/com/doublesymmetry/trackplayer/models/PlayerConfig.kt +12 -1
- package/android/src/test/java/com/doublesymmetry/trackplayer/ExoPlayerIntegrationTest.kt +319 -0
- package/android/src/test/java/com/doublesymmetry/trackplayer/SleepTimerIntegrationTest.kt +473 -0
- package/android/src/test/java/com/doublesymmetry/trackplayer/SleepTimerStateTest.kt +58 -0
- package/android/src/test/java/com/doublesymmetry/trackplayer/models/BrowseNavigationTest.kt +215 -0
- package/android/src/test/java/com/doublesymmetry/trackplayer/models/BrowseTreeTest.kt +166 -0
- package/android/src/test/java/com/doublesymmetry/trackplayer/models/EmitEventTest.kt +68 -0
- package/android/src/test/java/com/doublesymmetry/trackplayer/models/PlayerConfigTest.kt +400 -0
- package/android/src/test/java/com/doublesymmetry/trackplayer/models/TrackPlayerMediaItemTest.kt +380 -0
- package/android/src/test/resources/robolectric.properties +1 -0
- package/ios/TrackPlayer.swift +47 -101
- package/ios/TrackPlayerBridge.mm +2 -0
- package/ios/player/AVPlayerEngine.swift +47 -35
- package/ios/player/AudioCache.swift +34 -0
- package/ios/player/AudioPlayer.swift +70 -22
- package/ios/player/CacheProxyServer.swift +429 -0
- package/ios/player/DownloadCoordinator.swift +242 -0
- package/ios/player/Preloader.swift +21 -90
- package/ios/player/SleepTimerController.swift +147 -0
- package/ios/tests/AVPlayerEngineIntegrationTests.swift +230 -0
- package/ios/tests/AudioPlayerTests.swift +6 -0
- package/ios/tests/CacheProxyServerTests.swift +403 -0
- package/ios/tests/DownloadCoordinatorTests.swift +197 -0
- package/ios/tests/LocalAudioServer.swift +171 -0
- package/ios/tests/MockPlayerEngine.swift +1 -0
- package/ios/tests/QueueManagerTests.swift +6 -0
- package/ios/tests/SleepTimerIntegrationTests.swift +408 -0
- package/ios/tests/SleepTimerTests.swift +70 -0
- package/lib/commonjs/NativeTrackPlayer.js.map +1 -1
- package/lib/commonjs/audio.js +19 -0
- package/lib/commonjs/audio.js.map +1 -1
- package/lib/commonjs/interfaces/PlayerConfig.js +1 -1
- package/lib/commonjs/interfaces/PlayerConfig.js.map +1 -1
- package/lib/module/NativeTrackPlayer.js.map +1 -1
- package/lib/module/audio.js +17 -0
- package/lib/module/audio.js.map +1 -1
- package/lib/module/interfaces/PlayerConfig.js +1 -1
- package/lib/module/interfaces/PlayerConfig.js.map +1 -1
- package/lib/typescript/src/NativeTrackPlayer.d.ts +2 -0
- package/lib/typescript/src/NativeTrackPlayer.d.ts.map +1 -1
- package/lib/typescript/src/audio.d.ts +12 -1
- package/lib/typescript/src/audio.d.ts.map +1 -1
- package/lib/typescript/src/interfaces/MediaItem.d.ts +4 -1
- package/lib/typescript/src/interfaces/MediaItem.d.ts.map +1 -1
- package/lib/typescript/src/interfaces/PlayerConfig.d.ts +21 -2
- package/lib/typescript/src/interfaces/PlayerConfig.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/NativeTrackPlayer.ts +4 -0
- package/src/audio.ts +18 -0
- package/src/interfaces/MediaItem.ts +4 -1
- package/src/interfaces/PlayerConfig.ts +24 -3
- 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
|
-
}
|