react-native-nitro-player 1.4.1-alpha.0 → 1.4.2
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/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerCore.kt +1 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/core/TrackPlayerQueueBuild.kt +17 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/AuthAwareHttpDataSourceFactory.kt +48 -0
- package/android/src/main/java/com/margelo/nitro/nitroplayer/media/ExoPlayerBuilder.kt +10 -0
- package/ios/core/TrackPlayerQueueBuild.swift +33 -6
- package/package.json +1 -1
|
@@ -244,6 +244,7 @@ class TrackPlayerCore private constructor(
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
scope.cancel()
|
|
247
|
+
com.margelo.nitro.nitroplayer.media.AuthAwareHttpDataSourceFactory.clear()
|
|
247
248
|
// Do NOT stop the service — it owns the player.
|
|
248
249
|
// Unbind so Android can clean up if needed.
|
|
249
250
|
if (serviceBound) {
|
|
@@ -150,6 +150,23 @@ internal fun TrackPlayerCore.makeMediaItem(
|
|
|
150
150
|
|
|
151
151
|
val effectiveUrl = downloadManager.getEffectiveUrl(track)
|
|
152
152
|
|
|
153
|
+
// Register custom HTTP headers (e.g. Authorization) from extraPayload.headers so they are
|
|
154
|
+
// injected into the request for this URL. Applies to remote streams only — see ExoPlayerBuilder.
|
|
155
|
+
track.extraPayload?.let { payload ->
|
|
156
|
+
try {
|
|
157
|
+
val headersRaw = payload.toHashMap()["headers"]
|
|
158
|
+
if (headersRaw is Map<*, *>) {
|
|
159
|
+
val headers = headersRaw
|
|
160
|
+
.mapNotNull { (k, v) -> if (k is String && v is String) k to v else null }
|
|
161
|
+
.toMap()
|
|
162
|
+
if (headers.isNotEmpty()) {
|
|
163
|
+
com.margelo.nitro.nitroplayer.media.AuthAwareHttpDataSourceFactory
|
|
164
|
+
.setHeadersForUrl(effectiveUrl, headers)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch (_: Exception) {}
|
|
168
|
+
}
|
|
169
|
+
|
|
153
170
|
return MediaItem
|
|
154
171
|
.Builder()
|
|
155
172
|
.setMediaId(customMediaId ?: track.id)
|
package/android/src/main/java/com/margelo/nitro/nitroplayer/media/AuthAwareHttpDataSourceFactory.kt
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package com.margelo.nitro.nitroplayer.media
|
|
2
|
+
|
|
3
|
+
import androidx.media3.common.util.UnstableApi
|
|
4
|
+
import androidx.media3.datasource.DataSource
|
|
5
|
+
import androidx.media3.datasource.DefaultHttpDataSource
|
|
6
|
+
import androidx.media3.datasource.ResolvingDataSource
|
|
7
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* HTTP [DataSource.Factory] that injects per-URL request headers (e.g. `Authorization`)
|
|
11
|
+
* into ExoPlayer's HTTP requests, enabling playback from authenticated endpoints.
|
|
12
|
+
*
|
|
13
|
+
* Headers are registered per effective URL via [setHeadersForUrl] during MediaItem
|
|
14
|
+
* construction and applied through a [ResolvingDataSource] just before each request opens.
|
|
15
|
+
*
|
|
16
|
+
* The registry is a [ConcurrentHashMap] because it is written on the player thread but
|
|
17
|
+
* read on ExoPlayer's loader threads.
|
|
18
|
+
*/
|
|
19
|
+
@UnstableApi
|
|
20
|
+
class AuthAwareHttpDataSourceFactory : DataSource.Factory {
|
|
21
|
+
private val factory =
|
|
22
|
+
ResolvingDataSource.Factory(
|
|
23
|
+
DefaultHttpDataSource
|
|
24
|
+
.Factory()
|
|
25
|
+
.setAllowCrossProtocolRedirects(true)
|
|
26
|
+
.setConnectTimeoutMs(15_000)
|
|
27
|
+
.setReadTimeoutMs(15_000),
|
|
28
|
+
ResolvingDataSource.Resolver { dataSpec ->
|
|
29
|
+
val headers = headersMap[dataSpec.uri.toString()]
|
|
30
|
+
if (headers.isNullOrEmpty()) dataSpec else dataSpec.withAdditionalHeaders(headers)
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
override fun createDataSource(): DataSource = factory.createDataSource()
|
|
35
|
+
|
|
36
|
+
companion object {
|
|
37
|
+
private val headersMap = ConcurrentHashMap<String, Map<String, String>>()
|
|
38
|
+
|
|
39
|
+
fun setHeadersForUrl(url: String, headers: Map<String, String>) {
|
|
40
|
+
headersMap[url] = headers
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Releases all registered headers. Call when the player is torn down. */
|
|
44
|
+
fun clear() {
|
|
45
|
+
headersMap.clear()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -3,14 +3,18 @@ package com.margelo.nitro.nitroplayer.media
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import androidx.media3.common.AudioAttributes
|
|
5
5
|
import androidx.media3.common.C
|
|
6
|
+
import androidx.media3.common.util.UnstableApi
|
|
7
|
+
import androidx.media3.datasource.DefaultDataSource
|
|
6
8
|
import androidx.media3.exoplayer.DefaultLoadControl
|
|
7
9
|
import androidx.media3.exoplayer.ExoPlayer
|
|
10
|
+
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Builds an [ExoPlayer] with the standard NitroPlayer configuration.
|
|
11
14
|
* The player uses the main application looper so that Media3's
|
|
12
15
|
* [MediaSessionService] notification system works without thread conflicts.
|
|
13
16
|
*/
|
|
17
|
+
@UnstableApi
|
|
14
18
|
object ExoPlayerBuilder {
|
|
15
19
|
fun build(context: Context): ExoPlayer {
|
|
16
20
|
val loadControl =
|
|
@@ -37,9 +41,15 @@ object ExoPlayerBuilder {
|
|
|
37
41
|
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
|
|
38
42
|
.build()
|
|
39
43
|
|
|
44
|
+
// DefaultDataSource routes http/https through AuthAwareHttpDataSourceFactory (so custom
|
|
45
|
+
// request headers can be injected) while handling file://, content:// and asset:// natively.
|
|
46
|
+
val dataSourceFactory = DefaultDataSource.Factory(context, AuthAwareHttpDataSourceFactory())
|
|
47
|
+
val mediaSourceFactory = DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory)
|
|
48
|
+
|
|
40
49
|
return ExoPlayer
|
|
41
50
|
.Builder(context)
|
|
42
51
|
.setLoadControl(loadControl)
|
|
52
|
+
.setMediaSourceFactory(mediaSourceFactory)
|
|
43
53
|
.setAudioAttributes(audioAttrs, /* handleAudioFocus */ true)
|
|
44
54
|
.setHandleAudioBecomingNoisy(true)
|
|
45
55
|
.setWakeMode(C.WAKE_MODE_NETWORK)
|
|
@@ -358,6 +358,37 @@ extension TrackPlayerCore {
|
|
|
358
358
|
}
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
+
/// Extracts custom HTTP headers (e.g. `Authorization`) from `extraPayload.headers`.
|
|
362
|
+
/// These are passed to AVURLAsset so authenticated remote streams can be played.
|
|
363
|
+
/// Returns nil when no string headers are present.
|
|
364
|
+
func httpHeaders(for track: TrackItem) -> [String: String]? {
|
|
365
|
+
// AnyMap.toDictionary() returns nested objects as [String: Any?] (optional values),
|
|
366
|
+
// so cast to that and unwrap each value rather than [String: Any].
|
|
367
|
+
guard let payload = track.extraPayload?.toDictionary(),
|
|
368
|
+
let headersValue = payload["headers"],
|
|
369
|
+
let raw = headersValue as? [String: Any?]
|
|
370
|
+
else { return nil }
|
|
371
|
+
|
|
372
|
+
var headers: [String: String] = [:]
|
|
373
|
+
for (key, value) in raw {
|
|
374
|
+
if let stringValue = value as? String {
|
|
375
|
+
headers[key] = stringValue
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return headers.isEmpty ? nil : headers
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/// Builds AVURLAsset options, attaching custom HTTP headers for remote (non-local) tracks.
|
|
382
|
+
private func assetOptions(for track: TrackItem, isLocal: Bool) -> [String: Any] {
|
|
383
|
+
var options: [String: Any] = [AVURLAssetPreferPreciseDurationAndTimingKey: true]
|
|
384
|
+
if !isLocal, let headers = httpHeaders(for: track) {
|
|
385
|
+
// AVURLAssetHTTPHeaderFieldsKey is the de-facto mechanism for per-asset request
|
|
386
|
+
// headers (also used by react-native-track-player). Applies to non-DRM HTTP(S).
|
|
387
|
+
options["AVURLAssetHTTPHeaderFieldsKey"] = headers
|
|
388
|
+
}
|
|
389
|
+
return options
|
|
390
|
+
}
|
|
391
|
+
|
|
361
392
|
/// Creates a gapless-optimized AVPlayerItem with proper buffering configuration
|
|
362
393
|
func createGaplessPlayerItem(for track: TrackItem, isPreload: Bool = false) -> AVPlayerItem? {
|
|
363
394
|
let effectiveUrlString = DownloadManagerCore.shared.getEffectiveUrl(track: track)
|
|
@@ -396,9 +427,7 @@ extension TrackPlayerCore {
|
|
|
396
427
|
asset = preloadedAsset
|
|
397
428
|
NitroPlayerLogger.log("TrackPlayerCore", "🚀 Using preloaded asset for \(track.title)")
|
|
398
429
|
} else {
|
|
399
|
-
asset = AVURLAsset(url: url, options:
|
|
400
|
-
AVURLAssetPreferPreciseDurationAndTimingKey: true
|
|
401
|
-
])
|
|
430
|
+
asset = AVURLAsset(url: url, options: assetOptions(for: track, isLocal: isLocal))
|
|
402
431
|
}
|
|
403
432
|
|
|
404
433
|
let item = AVPlayerItem(asset: asset)
|
|
@@ -463,9 +492,7 @@ extension TrackPlayerCore {
|
|
|
463
492
|
url = remoteUrl
|
|
464
493
|
}
|
|
465
494
|
|
|
466
|
-
let asset = AVURLAsset(url: url, options:
|
|
467
|
-
AVURLAssetPreferPreciseDurationAndTimingKey: true
|
|
468
|
-
])
|
|
495
|
+
let asset = AVURLAsset(url: url, options: self.assetOptions(for: track, isLocal: isLocal))
|
|
469
496
|
|
|
470
497
|
asset.loadValuesAsynchronously(forKeys: Constants.preloadAssetKeys) { [weak self] in
|
|
471
498
|
var allKeysLoaded = true
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-nitro-player",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
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",
|