react-native-nitro-player 1.4.3 → 1.4.4-alpha.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.
@@ -57,6 +57,10 @@ class TrackPlayerCore: NSObject {
57
57
  internal let preloadQueue = DispatchQueue(label: "com.nitroplayer.preload", qos: .utility)
58
58
  internal var didRequestUrlsForCurrentItem = false
59
59
 
60
+ // Follows http→https redirects that AVURLAsset otherwise silently drops (issue #111).
61
+ // Attached as the resource-loader delegate for cleartext-http assets only.
62
+ internal let redirectResolver = TrackPlayerRedirectResolver()
63
+
60
64
  // MARK: - Stall & network recovery
61
65
  // Whether the user/app wants playback ongoing (true after play(), false after pause()).
62
66
  internal var intendedToPlay = false
@@ -216,6 +220,7 @@ class TrackPlayerCore: NSObject {
216
220
  self.pathMonitor = nil
217
221
  self.preloadedAssets.removeAll()
218
222
  self.failedItemRetryCounts.removeAll()
223
+ self.redirectResolver.clear()
219
224
  }
220
225
  }
221
226
 
@@ -389,6 +389,20 @@ extension TrackPlayerCore {
389
389
  return options
390
390
  }
391
391
 
392
+ /// Builds the AVURLAsset for a track URL. Cleartext-`http` remote URLs are routed through
393
+ /// `redirectResolver` so AVFoundation follows http→https redirects it would otherwise drop
394
+ /// (issue #111). HTTPS and local URLs load natively, unchanged.
395
+ func makeAsset(for track: TrackItem, url: URL, isLocal: Bool) -> AVURLAsset {
396
+ let options = assetOptions(for: track, isLocal: isLocal)
397
+ guard !isLocal, let wrappedURL = TrackPlayerRedirectResolver.wrap(url) else {
398
+ return AVURLAsset(url: url, options: options)
399
+ }
400
+ redirectResolver.registerHeaders(httpHeaders(for: track), for: wrappedURL)
401
+ let asset = AVURLAsset(url: wrappedURL, options: options)
402
+ asset.resourceLoader.setDelegate(redirectResolver, queue: redirectResolver.queue)
403
+ return asset
404
+ }
405
+
392
406
  /// Creates a gapless-optimized AVPlayerItem with proper buffering configuration
393
407
  func createGaplessPlayerItem(for track: TrackItem, isPreload: Bool = false) -> AVPlayerItem? {
394
408
  let effectiveUrlString = DownloadManagerCore.shared.getEffectiveUrl(track: track)
@@ -427,7 +441,7 @@ extension TrackPlayerCore {
427
441
  asset = preloadedAsset
428
442
  NitroPlayerLogger.log("TrackPlayerCore", "🚀 Using preloaded asset for \(track.title)")
429
443
  } else {
430
- asset = AVURLAsset(url: url, options: assetOptions(for: track, isLocal: isLocal))
444
+ asset = makeAsset(for: track, url: url, isLocal: isLocal)
431
445
  }
432
446
 
433
447
  let item = AVPlayerItem(asset: asset)
@@ -492,7 +506,7 @@ extension TrackPlayerCore {
492
506
  url = remoteUrl
493
507
  }
494
508
 
495
- let asset = AVURLAsset(url: url, options: self.assetOptions(for: track, isLocal: isLocal))
509
+ let asset = self.makeAsset(for: track, url: url, isLocal: isLocal)
496
510
 
497
511
  asset.loadValuesAsynchronously(forKeys: Constants.preloadAssetKeys) { [weak self] in
498
512
  var allKeysLoaded = true
@@ -0,0 +1,150 @@
1
+ //
2
+ // TrackPlayerRedirectResolver.swift
3
+ // NitroPlayer
4
+ //
5
+ // Created by Ritesh Shukla.
6
+ //
7
+ // AVURLAsset does not reliably follow HTTP→HTTPS redirects: when a cleartext `http://`
8
+ // track URL answers with a 30x (e.g. a universal `308 → https://...` upgrade), AVFoundation
9
+ // silently drops the redirect and the item fails to load (issue #111). Unlike ExoPlayer's
10
+ // `setAllowCrossProtocolRedirects(true)`, AVFoundation exposes no such flag.
11
+ //
12
+ // We work around it by giving such assets a sentinel URL scheme so AVFoundation has to ask
13
+ // this resource-loader delegate to load them. We then resolve the redirect chain ourselves
14
+ // with URLSession (which follows redirects) and hand AVFoundation a redirect to the final
15
+ // URL — after which it loads the media natively from the resolved location.
16
+ //
17
+ // Only cleartext-`http` URLs are routed here. HTTPS assets are left untouched: AVFoundation
18
+ // follows same-scheme redirects on its own, so only the failing http path pays the extra
19
+ // resolution request. The app must still permit the initial cleartext connection via ATS
20
+ // (`NSAppTransportSecurity`); this delegate only fixes the redirect-following, not ATS.
21
+ //
22
+
23
+ import AVFoundation
24
+ import Foundation
25
+
26
+ final class TrackPlayerRedirectResolver: NSObject, AVAssetResourceLoaderDelegate {
27
+
28
+ /// Sentinel scheme prefix. A wrapped URL looks like `nitroredirect+http://host/path`.
29
+ private static let schemePrefix = "nitroredirect+"
30
+
31
+ /// Serial queue the resource-loader delegate methods (and our task bookkeeping) run on.
32
+ let queue = DispatchQueue(label: "com.nitroplayer.redirect", qos: .userInitiated)
33
+
34
+ private let session: URLSession
35
+
36
+ // In-flight resolution tasks, keyed by loading request, so a cancelled request cancels its
37
+ // task. Touched only on `queue` (delegate callbacks + the task completion is hopped onto it).
38
+ private var tasksByRequest: [ObjectIdentifier: URLSessionDataTask] = [:]
39
+
40
+ // Custom request headers (e.g. `Authorization`) per wrapped-URL, so the resolution request can
41
+ // authenticate when a server requires auth even to return the redirect. Written on the player
42
+ // queue (registerHeaders), read on `queue`; guarded by `headersLock`.
43
+ private var headersByURL: [String: [String: String]] = [:]
44
+ private let headersLock = NSLock()
45
+
46
+ override init() {
47
+ let config = URLSessionConfiguration.ephemeral
48
+ config.requestCachePolicy = .reloadIgnoringLocalCacheData
49
+ session = URLSession(configuration: config)
50
+ super.init()
51
+ }
52
+
53
+ // MARK: - URL wrapping
54
+
55
+ /// Wraps a cleartext-`http` URL in the sentinel scheme so AVFoundation routes it here.
56
+ /// Returns nil for anything else (https, file, custom schemes) — those load natively.
57
+ static func wrap(_ url: URL) -> URL? {
58
+ guard url.scheme?.lowercased() == "http",
59
+ var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
60
+ else { return nil }
61
+ components.scheme = schemePrefix + "http"
62
+ return components.url
63
+ }
64
+
65
+ /// Restores the real URL from a sentinel-wrapped URL.
66
+ private static func unwrap(_ url: URL) -> URL? {
67
+ guard let scheme = url.scheme?.lowercased(), scheme.hasPrefix(schemePrefix),
68
+ var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
69
+ else { return nil }
70
+ components.scheme = String(scheme.dropFirst(schemePrefix.count))
71
+ return components.url
72
+ }
73
+
74
+ // MARK: - Header registry
75
+
76
+ func registerHeaders(_ headers: [String: String]?, for wrappedURL: URL) {
77
+ guard let headers, !headers.isEmpty else { return }
78
+ headersLock.lock()
79
+ defer { headersLock.unlock() }
80
+ headersByURL[wrappedURL.absoluteString] = headers
81
+ }
82
+
83
+ /// Releases all registered headers. Call when the player is torn down.
84
+ func clear() {
85
+ headersLock.lock()
86
+ defer { headersLock.unlock() }
87
+ headersByURL.removeAll()
88
+ }
89
+
90
+ private func headers(for wrappedURL: URL) -> [String: String]? {
91
+ headersLock.lock()
92
+ defer { headersLock.unlock() }
93
+ return headersByURL[wrappedURL.absoluteString]
94
+ }
95
+
96
+ // MARK: - AVAssetResourceLoaderDelegate
97
+
98
+ func resourceLoader(
99
+ _ resourceLoader: AVAssetResourceLoader,
100
+ shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest
101
+ ) -> Bool {
102
+ guard let wrappedURL = loadingRequest.request.url,
103
+ let realURL = Self.unwrap(wrappedURL)
104
+ else { return false }
105
+
106
+ var request = URLRequest(url: realURL)
107
+ // HEAD is enough: we only need `response.url` after URLSession follows the redirect chain.
108
+ // Even if the resolved endpoint rejects HEAD (405), the post-redirect URL is still reported.
109
+ request.httpMethod = "HEAD"
110
+ headers(for: wrappedURL)?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
111
+ loadingRequest.request.allHTTPHeaderFields?.forEach { request.setValue($1, forHTTPHeaderField: $0) }
112
+
113
+ let key = ObjectIdentifier(loadingRequest)
114
+ let task = session.dataTask(with: request) { [weak self] _, response, error in
115
+ self?.queue.async {
116
+ guard let self else { return }
117
+ self.tasksByRequest.removeValue(forKey: key)
118
+ guard !loadingRequest.isCancelled, !loadingRequest.isFinished else { return }
119
+
120
+ if let error {
121
+ NitroPlayerLogger.log("TrackPlayerCore", "❌ Redirect resolution failed for \(realURL): \(error.localizedDescription)")
122
+ loadingRequest.finishLoading(with: error)
123
+ return
124
+ }
125
+
126
+ // `response.url` is the final URL after URLSession followed all redirects.
127
+ let finalURL = response?.url ?? realURL
128
+ if finalURL != realURL {
129
+ NitroPlayerLogger.log("TrackPlayerCore", "↪️ Resolved redirect \(realURL) → \(finalURL)")
130
+ }
131
+ var redirect = URLRequest(url: finalURL)
132
+ self.headers(for: wrappedURL)?.forEach { redirect.setValue($1, forHTTPHeaderField: $0) }
133
+ loadingRequest.redirect = redirect
134
+ loadingRequest.response = HTTPURLResponse(
135
+ url: finalURL, statusCode: 302, httpVersion: "HTTP/1.1", headerFields: nil)
136
+ loadingRequest.finishLoading()
137
+ }
138
+ }
139
+ tasksByRequest[key] = task
140
+ task.resume()
141
+ return true
142
+ }
143
+
144
+ func resourceLoader(
145
+ _ resourceLoader: AVAssetResourceLoader,
146
+ didCancel loadingRequest: AVAssetResourceLoadingRequest
147
+ ) {
148
+ tasksByRequest.removeValue(forKey: ObjectIdentifier(loadingRequest))?.cancel()
149
+ }
150
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-player",
3
- "version": "1.4.3",
3
+ "version": "1.4.4-alpha.0",
4
4
  "description": "A powerful audio player library for React Native with playlist management, playback controls, and support for Android Auto and CarPlay",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",