@javascriptcommon/react-native-track-player 1.2.9 → 1.2.24
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 +61 -4
- package/android/src/main/AndroidManifest.xml +2 -4
- package/android/src/main/ic_home-playstore.png +0 -0
- package/android/src/main/ic_repeat-playstore.png +0 -0
- package/android/src/main/ic_repeat_50-playstore.png +0 -0
- package/android/src/main/ic_shuffle-playstore.png +0 -0
- package/android/src/main/ic_shuffle_50-playstore.png +0 -0
- package/android/src/main/ic_shuffle_sm-playstore.png +0 -0
- package/android/src/main/ic_stop-playstore.png +0 -0
- package/android/src/main/ic_test-playstore.png +0 -0
- package/android/src/main/java/com/guichaguri/trackplayer/{service/HeadlessJsMediaService.java → HeadlessJsMediaService.java} +83 -32
- package/android/src/main/java/com/guichaguri/trackplayer/TrackPlayer.kt +25 -0
- package/android/src/main/java/com/guichaguri/trackplayer/extensions/AudioPlayerStateExt.kt +19 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/event/EventHolder.kt +30 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/event/NotificationEventHolder.kt +20 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/event/PlayerEventHolder.kt +111 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/AAMediaSessionCallback.kt +10 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/AudioContentType.kt +10 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/AudioItem.kt +66 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/AudioItemTransitionReason.kt +33 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/AudioPlayerState.kt +30 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/BufferConfig.kt +8 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/CacheConfig.kt +17 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/Capability.kt +19 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/FocusChangeData.kt +3 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/MediaSessionCallback.kt +17 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/NotificationConfig.kt +43 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/NotificationMetadata.kt +8 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/NotificationState.kt +8 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlayWhenReadyChangeData.kt +5 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlaybackEndedReason.kt +5 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlaybackError.kt +6 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlaybackMetadata.kt +200 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlayerConfig.kt +33 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PlayerOptions.kt +9 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/PositionChangedReason.kt +39 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/models/QueuedPlayerOptions.kt +49 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/notification/NotificationManager.kt +678 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/AudioPlayer.kt +10 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/BaseAudioPlayer.kt +864 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/QueuedAudioPlayer.kt +269 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/components/MediaSourceExt.kt +35 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/components/PlayerCache.kt +26 -0
- package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/utils/Utils.kt +12 -0
- package/android/src/main/java/com/guichaguri/trackplayer/model/MetadataAdapter.kt +224 -0
- package/android/src/main/java/com/guichaguri/trackplayer/model/State.kt +13 -0
- package/android/src/main/java/com/guichaguri/trackplayer/model/Track.kt +120 -0
- package/android/src/main/java/com/guichaguri/trackplayer/model/TrackAudioItem.kt +19 -0
- package/android/src/main/java/com/guichaguri/trackplayer/model/TrackType.kt +11 -0
- package/android/src/main/java/com/guichaguri/trackplayer/module/AutoConnectionDetector.kt +151 -0
- package/android/src/main/java/com/guichaguri/trackplayer/module/MusicEvents.kt +66 -0
- package/android/src/main/java/com/guichaguri/trackplayer/module/MusicModule.kt +1192 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/BundleUtils.kt +117 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicBinder.kt +31 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicManager.kt +347 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicService.kt +1268 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/Utils.kt +228 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/metadata/ButtonEvents.kt +141 -0
- package/android/src/main/java/com/guichaguri/trackplayer/service/metadata/MetadataManager.kt +396 -0
- package/android/src/main/res/drawable-hdpi/ic_home.png +0 -0
- package/android/src/main/res/drawable-mdpi/ic_home.png +0 -0
- package/android/src/main/res/drawable-xhdpi/ic_home.png +0 -0
- package/android/src/main/res/drawable-xxhdpi/ic_home.png +0 -0
- package/android/src/main/res/drawable-xxxhdpi/ic_home.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_arrow_down_circle_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_clock_now_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_close_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_heart_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_heart_outlined_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_repeat_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_repeat_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_shuffle_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-hdpi/ic_shuffle_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_arrow_down_circle_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_clock_now_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_close_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_heart_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_heart_outlined_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_repeat_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_repeat_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_shuffle_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-mdpi/ic_shuffle_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_arrow_down_circle_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_clock_now_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_close_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_heart_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_heart_outlined_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_repeat_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_repeat_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_shuffle_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xhdpi/ic_shuffle_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_arrow_down_circle_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_clock_now_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_close_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_heart_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_heart_outlined_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_repeat_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_repeat_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_shuffle_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxhdpi/ic_shuffle_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_arrow_down_circle_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_clock_now_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_close_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_heart_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_heart_outlined_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_repeat_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_repeat_on_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_shuffle_off_foreground.png +0 -0
- package/android/src/main/res/mipmap-xxxhdpi/ic_shuffle_on_foreground.png +0 -0
- package/android/src/main/res/raw/silent_5_seconds.mp3 +0 -0
- package/android/src/main/res/strings.xml +6 -0
- package/android/src/main/res/values/strings.xml +6 -0
- package/index.d.ts +62 -1
- package/lib/index.js +10 -9
- package/package.json +1 -1
- package/android/src/main/java/com/guichaguri/trackplayer/TrackPlayer.java +0 -28
- package/android/src/main/java/com/guichaguri/trackplayer/module/MusicEvents.java +0 -55
- package/android/src/main/java/com/guichaguri/trackplayer/module/MusicModule.java +0 -298
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicBinder.java +0 -47
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicManager.java +0 -383
- package/android/src/main/java/com/guichaguri/trackplayer/service/MusicService.java +0 -271
- package/android/src/main/java/com/guichaguri/trackplayer/service/Utils.java +0 -243
- package/android/src/main/java/com/guichaguri/trackplayer/service/metadata/ButtonEvents.java +0 -148
- package/android/src/main/java/com/guichaguri/trackplayer/service/metadata/MetadataManager.java +0 -379
- package/android/src/main/java/com/guichaguri/trackplayer/service/models/Track.java +0 -141
- package/android/src/main/java/com/guichaguri/trackplayer/service/models/TrackType.java +0 -35
- package/android/src/main/res/drawable-hdpi/ic_logo.png +0 -0
- package/android/src/main/res/drawable-mdpi/ic_logo.png +0 -0
- package/android/src/main/res/drawable-xhdpi/ic_logo.png +0 -0
- package/android/src/main/res/drawable-xxhdpi/ic_logo.png +0 -0
- package/android/src/main/res/drawable-xxxhdpi/ic_logo.png +0 -0
package/android/src/main/java/com/guichaguri/trackplayer/kotlinaudio/players/BaseAudioPlayer.kt
ADDED
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
package com.doublesymmetry.kotlinaudio.players
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.media.AudioManager
|
|
6
|
+
import android.media.AudioManager.AUDIOFOCUS_LOSS
|
|
7
|
+
import android.net.Uri
|
|
8
|
+
import android.os.Bundle
|
|
9
|
+
import android.os.ResultReceiver
|
|
10
|
+
import android.support.v4.media.RatingCompat
|
|
11
|
+
import android.support.v4.media.session.MediaSessionCompat
|
|
12
|
+
import android.util.Log
|
|
13
|
+
import android.view.KeyEvent
|
|
14
|
+
import androidx.annotation.CallSuper
|
|
15
|
+
import androidx.core.content.ContextCompat
|
|
16
|
+
import androidx.media.AudioAttributesCompat
|
|
17
|
+
import androidx.media.AudioAttributesCompat.CONTENT_TYPE_MUSIC
|
|
18
|
+
import androidx.media.AudioAttributesCompat.USAGE_MEDIA
|
|
19
|
+
import androidx.media.AudioFocusRequestCompat
|
|
20
|
+
import androidx.media.AudioManagerCompat
|
|
21
|
+
import androidx.media.AudioManagerCompat.AUDIOFOCUS_GAIN
|
|
22
|
+
import com.guichaguri.trackplayer.kotlinaudio.event.EventHolder
|
|
23
|
+
import com.guichaguri.trackplayer.kotlinaudio.event.NotificationEventHolder
|
|
24
|
+
import com.guichaguri.trackplayer.kotlinaudio.event.PlayerEventHolder
|
|
25
|
+
// import com.doublesymmetry.kotlinaudio.models.*
|
|
26
|
+
import com.guichaguri.trackplayer.kotlinaudio.notification.NotificationManager
|
|
27
|
+
import com.guichaguri.trackplayer.kotlinaudio.players.components.PlayerCache
|
|
28
|
+
import com.guichaguri.trackplayer.kotlinaudio.utils.isUriLocal
|
|
29
|
+
import com.google.android.exoplayer2.*
|
|
30
|
+
import com.google.android.exoplayer2.DefaultLoadControl.*
|
|
31
|
+
import com.google.android.exoplayer2.Player.Listener
|
|
32
|
+
import com.google.android.exoplayer2.audio.AudioAttributes
|
|
33
|
+
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
|
34
|
+
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
|
|
35
|
+
import com.google.android.exoplayer2.metadata.Metadata
|
|
36
|
+
import com.google.android.exoplayer2.source.MediaSource
|
|
37
|
+
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
|
38
|
+
import com.google.android.exoplayer2.source.dash.DashMediaSource
|
|
39
|
+
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource
|
|
40
|
+
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
|
41
|
+
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource
|
|
42
|
+
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource
|
|
43
|
+
import com.google.android.exoplayer2.upstream.*
|
|
44
|
+
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
|
|
45
|
+
import com.google.android.exoplayer2.upstream.cache.SimpleCache
|
|
46
|
+
import com.google.android.exoplayer2.util.Util
|
|
47
|
+
import com.guichaguri.trackplayer.kotlinaudio.models.*
|
|
48
|
+
import kotlinx.coroutines.MainScope
|
|
49
|
+
import kotlinx.coroutines.launch
|
|
50
|
+
import timber.log.Timber
|
|
51
|
+
import java.util.*
|
|
52
|
+
import java.util.concurrent.TimeUnit
|
|
53
|
+
|
|
54
|
+
abstract class BaseAudioPlayer internal constructor(
|
|
55
|
+
internal val context: Context,
|
|
56
|
+
playerConfig: PlayerConfig,
|
|
57
|
+
private val bufferConfig: BufferConfig?,
|
|
58
|
+
private val cacheConfig: CacheConfig?,
|
|
59
|
+
mediaSessionCallback: AAMediaSessionCallBack
|
|
60
|
+
) : AudioManager.OnAudioFocusChangeListener {
|
|
61
|
+
protected val exoPlayer: ExoPlayer
|
|
62
|
+
|
|
63
|
+
private var cache: SimpleCache? = null
|
|
64
|
+
private val scope = MainScope()
|
|
65
|
+
private var playerConfig: PlayerConfig = playerConfig
|
|
66
|
+
var mediaSessionCallBack: AAMediaSessionCallBack = mediaSessionCallback
|
|
67
|
+
|
|
68
|
+
val notificationManager: NotificationManager
|
|
69
|
+
|
|
70
|
+
open val playerOptions: PlayerOptions = DefaultPlayerOptions()
|
|
71
|
+
|
|
72
|
+
open val currentItem: AudioItem?
|
|
73
|
+
get() = exoPlayer.currentMediaItem?.localConfiguration?.tag as AudioItem?
|
|
74
|
+
|
|
75
|
+
var playbackError: PlaybackError? = null
|
|
76
|
+
var playerState: AudioPlayerState = AudioPlayerState.IDLE
|
|
77
|
+
private set(value) {
|
|
78
|
+
if (value != field) {
|
|
79
|
+
field = value
|
|
80
|
+
playerEventHolder.updateAudioPlayerState(value)
|
|
81
|
+
if (!playerConfig.handleAudioFocus) {
|
|
82
|
+
when (value) {
|
|
83
|
+
AudioPlayerState.IDLE,
|
|
84
|
+
AudioPlayerState.ERROR -> abandonAudioFocusIfHeld()
|
|
85
|
+
AudioPlayerState.READY -> requestAudioFocus()
|
|
86
|
+
else -> {}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var playWhenReady: Boolean
|
|
93
|
+
get() = exoPlayer.playWhenReady
|
|
94
|
+
set(value) {
|
|
95
|
+
exoPlayer.playWhenReady = value
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
val duration: Long
|
|
99
|
+
get() {
|
|
100
|
+
return if (exoPlayer.duration == C.TIME_UNSET) 0
|
|
101
|
+
else exoPlayer.duration
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private var oldPosition = 0L
|
|
105
|
+
|
|
106
|
+
val position: Long
|
|
107
|
+
get() {
|
|
108
|
+
return if (exoPlayer.currentPosition == C.POSITION_UNSET.toLong()) 0
|
|
109
|
+
else exoPlayer.currentPosition
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
val bufferedPosition: Long
|
|
113
|
+
get() {
|
|
114
|
+
return if (exoPlayer.bufferedPosition == C.POSITION_UNSET.toLong()) 0
|
|
115
|
+
else exoPlayer.bufferedPosition
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
var volume: Float
|
|
119
|
+
get() = exoPlayer.volume
|
|
120
|
+
set(value) {
|
|
121
|
+
exoPlayer.volume = value * volumeMultiplier
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
var playbackSpeed: Float
|
|
125
|
+
get() = exoPlayer.playbackParameters.speed
|
|
126
|
+
set(value) {
|
|
127
|
+
exoPlayer.setPlaybackSpeed(value)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
var automaticallyUpdateNotificationMetadata: Boolean = true
|
|
131
|
+
|
|
132
|
+
private var volumeMultiplier = 1f
|
|
133
|
+
private set(value) {
|
|
134
|
+
field = value
|
|
135
|
+
volume = volume
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
val isPlaying
|
|
139
|
+
get() = exoPlayer.isPlaying
|
|
140
|
+
|
|
141
|
+
private val notificationEventHolder = NotificationEventHolder()
|
|
142
|
+
private val playerEventHolder = PlayerEventHolder()
|
|
143
|
+
|
|
144
|
+
var ratingType: Int = RatingCompat.RATING_NONE
|
|
145
|
+
set(value) {
|
|
146
|
+
field = value
|
|
147
|
+
|
|
148
|
+
mediaSession.setRatingType(ratingType)
|
|
149
|
+
mediaSessionConnector.setRatingCallback(object : MediaSessionConnector.RatingCallback {
|
|
150
|
+
override fun onCommand(
|
|
151
|
+
player: Player,
|
|
152
|
+
command: String,
|
|
153
|
+
extras: Bundle?,
|
|
154
|
+
cb: ResultReceiver?
|
|
155
|
+
): Boolean {
|
|
156
|
+
return true
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
override fun onSetRating(player: Player, rating: RatingCompat) {
|
|
160
|
+
playerEventHolder.updateOnPlayerActionTriggeredExternally(
|
|
161
|
+
MediaSessionCallback.RATING(
|
|
162
|
+
rating,
|
|
163
|
+
null
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
override fun onSetRating(player: Player, rating: RatingCompat, extras: Bundle?) {
|
|
169
|
+
playerEventHolder.updateOnPlayerActionTriggeredExternally(
|
|
170
|
+
MediaSessionCallback.RATING(
|
|
171
|
+
rating,
|
|
172
|
+
extras
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
val event = EventHolder(notificationEventHolder, playerEventHolder)
|
|
180
|
+
|
|
181
|
+
private var focus: AudioFocusRequestCompat? = null
|
|
182
|
+
private var hasAudioFocus = false
|
|
183
|
+
private var wasDucking = false
|
|
184
|
+
|
|
185
|
+
protected val mediaSession = MediaSessionCompat(context, "KotlinAudioPlayer")
|
|
186
|
+
protected val mediaSessionConnector = MediaSessionConnector(mediaSession)
|
|
187
|
+
|
|
188
|
+
init {
|
|
189
|
+
if (cacheConfig != null) {
|
|
190
|
+
cache = PlayerCache.getInstance(context, cacheConfig)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
exoPlayer = ExoPlayer.Builder(context)
|
|
194
|
+
.setHandleAudioBecomingNoisy(playerConfig.handleAudioBecomingNoisy)
|
|
195
|
+
.apply {
|
|
196
|
+
if (bufferConfig != null) setLoadControl(setupBuffer(bufferConfig))
|
|
197
|
+
}
|
|
198
|
+
.build()
|
|
199
|
+
|
|
200
|
+
exoPlayer.pauseAtEndOfMediaItems = true
|
|
201
|
+
|
|
202
|
+
mediaSession.isActive = true
|
|
203
|
+
|
|
204
|
+
val playerToUse =
|
|
205
|
+
if (playerConfig.interceptPlayerActionsTriggeredExternally) createForwardingPlayer() else exoPlayer
|
|
206
|
+
|
|
207
|
+
mediaSession.setCallback(object: MediaSessionCompat.Callback() {
|
|
208
|
+
// HACK: special for podverse that intercepts SkipNext and SkipPrevious and handles accordingly
|
|
209
|
+
// as podverse does not enable these playback capabilities
|
|
210
|
+
override fun onMediaButtonEvent(mediaButtonEvent: Intent?): Boolean {
|
|
211
|
+
if (mediaButtonEvent?.action == Intent.ACTION_MEDIA_BUTTON) {
|
|
212
|
+
val event = mediaButtonEvent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
|
|
213
|
+
if (event != null && event.action == KeyEvent.ACTION_DOWN) {
|
|
214
|
+
when (event.keyCode) {
|
|
215
|
+
KeyEvent.KEYCODE_MEDIA_NEXT -> {
|
|
216
|
+
this.onSkipToNext()
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
|
|
220
|
+
this.onSkipToPrevious()
|
|
221
|
+
return true
|
|
222
|
+
}
|
|
223
|
+
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_STEP_FORWARD -> {
|
|
224
|
+
this.onFastForward()
|
|
225
|
+
}
|
|
226
|
+
KeyEvent.KEYCODE_MEDIA_REWIND, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_STEP_BACKWARD -> {
|
|
227
|
+
this.onRewind()
|
|
228
|
+
}
|
|
229
|
+
else -> {
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return super.onMediaButtonEvent(mediaButtonEvent)
|
|
235
|
+
}
|
|
236
|
+
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
|
237
|
+
Timber.tag("GVATest").d("playing from mediaID: %s", mediaId)
|
|
238
|
+
mediaSessionCallback.handlePlayFromMediaId(mediaId, extras)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
|
242
|
+
super.onPlayFromSearch(query, extras)
|
|
243
|
+
Timber.tag("GVATest").d("playing from query: %s", query)
|
|
244
|
+
mediaSessionCallback.handlePlayFromSearch(query, extras)
|
|
245
|
+
}
|
|
246
|
+
// https://stackoverflow.com/questions/53837783/selecting-media-item-in-android-auto-queue-does-nothing
|
|
247
|
+
override fun onSkipToQueueItem(id: Long) {
|
|
248
|
+
mediaSessionCallback.handleSkipToQueueItem(id)
|
|
249
|
+
}
|
|
250
|
+
// TODO: what's missing?
|
|
251
|
+
override fun onPlay() {
|
|
252
|
+
playerToUse.play()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
override fun onPause() {
|
|
256
|
+
playerToUse.pause()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
override fun onSkipToNext() {
|
|
260
|
+
playerToUse.seekToNext()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
override fun onSkipToPrevious() {
|
|
264
|
+
playerToUse.seekToPrevious()
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
override fun onFastForward() {
|
|
268
|
+
playerToUse.seekForward()
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
override fun onRewind() {
|
|
272
|
+
playerToUse.seekBack()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
override fun onStop() {
|
|
276
|
+
playerToUse.stop()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
override fun onSeekTo(pos: Long) {
|
|
280
|
+
playerToUse.seekTo(pos)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
override fun onSetRating(rating: RatingCompat?) {
|
|
284
|
+
if (rating == null) return
|
|
285
|
+
playerEventHolder.updateOnPlayerActionTriggeredExternally(
|
|
286
|
+
MediaSessionCallback.RATING(
|
|
287
|
+
rating, null
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
override fun onSetRating(rating: RatingCompat?, extras: Bundle?) {
|
|
293
|
+
if (rating == null) return
|
|
294
|
+
playerEventHolder.updateOnPlayerActionTriggeredExternally(
|
|
295
|
+
MediaSessionCallback.RATING(
|
|
296
|
+
rating,
|
|
297
|
+
extras
|
|
298
|
+
)
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
// see NotificationManager.kt. onRewind, onFastForward and onStop do not trigger.
|
|
302
|
+
override fun onCustomAction(action: String?, extras: Bundle?) {
|
|
303
|
+
when (action) {
|
|
304
|
+
NotificationManager.REWIND -> playerToUse.seekBack()
|
|
305
|
+
NotificationManager.FORWARD -> playerToUse.seekForward()
|
|
306
|
+
NotificationManager.STOP-> playerToUse.stop()
|
|
307
|
+
else -> playerEventHolder.updateOnPlayerActionTriggeredExternally(
|
|
308
|
+
MediaSessionCallback.CUSTOMACTION(
|
|
309
|
+
action ?: "NO_ACTION_CODE_PROVIDED"
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
notificationManager = NotificationManager(
|
|
317
|
+
context,
|
|
318
|
+
playerToUse,
|
|
319
|
+
mediaSession,
|
|
320
|
+
mediaSessionConnector,
|
|
321
|
+
notificationEventHolder,
|
|
322
|
+
playerEventHolder
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
exoPlayer.addListener(PlayerListener())
|
|
326
|
+
|
|
327
|
+
scope.launch {
|
|
328
|
+
// Whether ExoPlayer should manage audio focus for us automatically
|
|
329
|
+
// see https://medium.com/google-exoplayer/easy-audio-focus-with-exoplayer-a2dcbbe4640e
|
|
330
|
+
val audioAttributes = AudioAttributes.Builder()
|
|
331
|
+
.setUsage(C.USAGE_MEDIA)
|
|
332
|
+
.setContentType(
|
|
333
|
+
when (playerConfig.audioContentType) {
|
|
334
|
+
AudioContentType.MUSIC -> C.AUDIO_CONTENT_TYPE_MUSIC
|
|
335
|
+
AudioContentType.SPEECH -> C.AUDIO_CONTENT_TYPE_SPEECH
|
|
336
|
+
AudioContentType.SONIFICATION -> C.AUDIO_CONTENT_TYPE_SONIFICATION
|
|
337
|
+
AudioContentType.MOVIE -> C.AUDIO_CONTENT_TYPE_MOVIE
|
|
338
|
+
AudioContentType.UNKNOWN -> C.AUDIO_CONTENT_TYPE_UNKNOWN
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
.build();
|
|
342
|
+
exoPlayer.setAudioAttributes(audioAttributes, playerConfig.handleAudioFocus);
|
|
343
|
+
mediaSessionConnector.setPlayer(playerToUse)
|
|
344
|
+
mediaSessionConnector.setMediaMetadataProvider {
|
|
345
|
+
notificationManager.getMediaMetadataCompat()
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
playerEventHolder.updateAudioPlayerState(AudioPlayerState.IDLE)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
public fun getMediaSessionToken(): MediaSessionCompat.Token {
|
|
353
|
+
return mediaSession.sessionToken
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private fun createForwardingPlayer(): ForwardingPlayer {
|
|
357
|
+
return object : ForwardingPlayer(exoPlayer) {
|
|
358
|
+
override fun play() {
|
|
359
|
+
playerEventHolder.updateOnPlayerActionTriggeredExternally(MediaSessionCallback.PLAY)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
override fun pause() {
|
|
363
|
+
playerEventHolder.updateOnPlayerActionTriggeredExternally(MediaSessionCallback.PAUSE)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
override fun seekToNext() {
|
|
367
|
+
playerEventHolder.updateOnPlayerActionTriggeredExternally(MediaSessionCallback.NEXT)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
override fun seekToPrevious() {
|
|
371
|
+
playerEventHolder.updateOnPlayerActionTriggeredExternally(MediaSessionCallback.PREVIOUS)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
override fun seekForward() {
|
|
375
|
+
playerEventHolder.updateOnPlayerActionTriggeredExternally(MediaSessionCallback.FORWARD)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
override fun seekBack() {
|
|
379
|
+
playerEventHolder.updateOnPlayerActionTriggeredExternally(MediaSessionCallback.REWIND)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
override fun stop() {
|
|
383
|
+
playerEventHolder.updateOnPlayerActionTriggeredExternally(MediaSessionCallback.STOP)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
override fun seekTo(mediaItemIndex: Int, positionMs: Long) {
|
|
387
|
+
playerEventHolder.updateOnPlayerActionTriggeredExternally(
|
|
388
|
+
MediaSessionCallback.SEEK(
|
|
389
|
+
positionMs
|
|
390
|
+
)
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
override fun seekTo(positionMs: Long) {
|
|
395
|
+
playerEventHolder.updateOnPlayerActionTriggeredExternally(
|
|
396
|
+
MediaSessionCallback.SEEK(
|
|
397
|
+
positionMs
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
internal fun updateNotificationMetadataIfAutomatic(overrideAudioItem: AudioItem? = null) {
|
|
405
|
+
if (automaticallyUpdateNotificationMetadata) {
|
|
406
|
+
notificationManager.notificationMetadata = NotificationMetadata(
|
|
407
|
+
overrideAudioItem?.title ?: currentItem?.title,
|
|
408
|
+
overrideAudioItem?.artist ?: currentItem?.artist,
|
|
409
|
+
overrideAudioItem?.artwork ?: currentItem?.artwork,
|
|
410
|
+
overrideAudioItem?.duration ?: currentItem?.duration
|
|
411
|
+
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private fun setupBuffer(bufferConfig: BufferConfig): DefaultLoadControl {
|
|
417
|
+
bufferConfig.apply {
|
|
418
|
+
val multiplier =
|
|
419
|
+
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / DEFAULT_BUFFER_FOR_PLAYBACK_MS
|
|
420
|
+
val minBuffer =
|
|
421
|
+
if (minBuffer != null && minBuffer != 0) minBuffer else DEFAULT_MIN_BUFFER_MS
|
|
422
|
+
val maxBuffer =
|
|
423
|
+
if (maxBuffer != null && maxBuffer != 0) maxBuffer else DEFAULT_MAX_BUFFER_MS
|
|
424
|
+
val playBuffer =
|
|
425
|
+
if (playBuffer != null && playBuffer != 0) playBuffer else DEFAULT_BUFFER_FOR_PLAYBACK_MS
|
|
426
|
+
val backBuffer =
|
|
427
|
+
if (backBuffer != null && backBuffer != 0) backBuffer else DEFAULT_BACK_BUFFER_DURATION_MS
|
|
428
|
+
|
|
429
|
+
return Builder()
|
|
430
|
+
.setBufferDurationsMs(minBuffer, maxBuffer, playBuffer, playBuffer * multiplier)
|
|
431
|
+
.setBackBuffer(backBuffer, false)
|
|
432
|
+
.build()
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Will replace the current item with a new one and load it into the player.
|
|
438
|
+
* @param item The [AudioItem] to replace the current one.
|
|
439
|
+
* @param playWhenReady Whether playback starts automatically.
|
|
440
|
+
*/
|
|
441
|
+
open fun load(item: AudioItem, playWhenReady: Boolean = true) {
|
|
442
|
+
exoPlayer.playWhenReady = playWhenReady
|
|
443
|
+
load(item)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Will replace the current item with a new one and load it into the player.
|
|
448
|
+
* @param item The [AudioItem] to replace the current one.
|
|
449
|
+
*/
|
|
450
|
+
open fun load(item: AudioItem) {
|
|
451
|
+
val mediaSource = getMediaSourceFromAudioItem(item)
|
|
452
|
+
exoPlayer.addMediaSource(mediaSource)
|
|
453
|
+
exoPlayer.prepare()
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
fun togglePlaying() {
|
|
457
|
+
if (exoPlayer.isPlaying) {
|
|
458
|
+
pause()
|
|
459
|
+
} else {
|
|
460
|
+
play()
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
var skipSilence: Boolean
|
|
465
|
+
get() = exoPlayer.skipSilenceEnabled
|
|
466
|
+
set(value) {
|
|
467
|
+
exoPlayer.skipSilenceEnabled = value;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
fun play() {
|
|
471
|
+
exoPlayer.play()
|
|
472
|
+
Log.d("playTest", "playTest play")
|
|
473
|
+
if (currentItem != null) {
|
|
474
|
+
exoPlayer.prepare()
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
fun prepare() {
|
|
479
|
+
Log.d("playTest", "playTest prepare")
|
|
480
|
+
if (currentItem != null) {
|
|
481
|
+
exoPlayer.prepare()
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
fun pause() {
|
|
486
|
+
Log.d("playTest", "playTest pause")
|
|
487
|
+
exoPlayer.pause()
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Stops playback, without clearing the active item. Calling this method will cause the playback
|
|
492
|
+
* state to transition to AudioPlayerState.IDLE and the player will release the loaded media and
|
|
493
|
+
* resources required for playback.
|
|
494
|
+
*/
|
|
495
|
+
open fun stop() {
|
|
496
|
+
playerState = AudioPlayerState.STOPPED
|
|
497
|
+
exoPlayer.playWhenReady = false
|
|
498
|
+
exoPlayer.stop()
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
open fun clear() {
|
|
502
|
+
exoPlayer.clearMediaItems()
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Pause playback whenever an item plays to its end.
|
|
507
|
+
*/
|
|
508
|
+
fun setPauseAtEndOfItem(pause: Boolean) {
|
|
509
|
+
exoPlayer.setPauseAtEndOfMediaItems(pause)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Stops and destroys the player. Only call this when you are finished using the player, otherwise use [pause].
|
|
514
|
+
*/
|
|
515
|
+
@CallSuper
|
|
516
|
+
open fun destroy() {
|
|
517
|
+
abandonAudioFocusIfHeld()
|
|
518
|
+
stop()
|
|
519
|
+
notificationManager.destroy()
|
|
520
|
+
exoPlayer.release()
|
|
521
|
+
cache?.release()
|
|
522
|
+
cache = null
|
|
523
|
+
mediaSession.isActive = false
|
|
524
|
+
mediaSession.release()
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
open fun seek(duration: Long, unit: TimeUnit) {
|
|
528
|
+
Log.d("playTest", "playTest seek")
|
|
529
|
+
val positionMs = TimeUnit.MILLISECONDS.convert(duration, unit)
|
|
530
|
+
exoPlayer.seekTo(positionMs)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
open fun seekBy(offset: Long, unit: TimeUnit) {
|
|
534
|
+
val positionMs = exoPlayer.currentPosition + TimeUnit.MILLISECONDS.convert(offset, unit)
|
|
535
|
+
exoPlayer.seekTo(positionMs)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private fun getMediaItemFromAudioItem(audioItem: AudioItem): MediaItem {
|
|
539
|
+
return MediaItem.Builder().setUri(audioItem.audioUrl).setTag(AudioItemHolder(audioItem)).build()
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
protected fun getMediaSourceFromAudioItem(audioItem: AudioItem): MediaSource {
|
|
543
|
+
val factory: DataSource.Factory
|
|
544
|
+
val uri = Uri.parse(audioItem.audioUrl)
|
|
545
|
+
val mediaItem = getMediaItemFromAudioItem(audioItem)
|
|
546
|
+
|
|
547
|
+
Log.d("pcTest", "pcTest getMediaSource 1")
|
|
548
|
+
|
|
549
|
+
val userAgent =
|
|
550
|
+
if (audioItem.options == null || audioItem.options!!.userAgent.isNullOrBlank()) {
|
|
551
|
+
Util.getUserAgent(context, APPLICATION_NAME)
|
|
552
|
+
} else {
|
|
553
|
+
audioItem.options!!.userAgent
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
Log.d("pcTest", "pcTest getMediaSource 2")
|
|
557
|
+
|
|
558
|
+
factory = when {
|
|
559
|
+
audioItem.options?.resourceId != null -> {
|
|
560
|
+
Log.d("pcTest", "pcTest getMediaSource 2 1")
|
|
561
|
+
val raw = RawResourceDataSource(context)
|
|
562
|
+
Log.d("pcTest", "pcTest getMediaSource 2 1 uri: " + uri)
|
|
563
|
+
raw.open(DataSpec(uri))
|
|
564
|
+
Log.d("pcTest", "pcTest getMediaSource 2 2")
|
|
565
|
+
DataSource.Factory { raw }
|
|
566
|
+
}
|
|
567
|
+
isUriLocal(uri) -> {
|
|
568
|
+
Log.d("pcTest", "pcTest getMediaSource 2 3")
|
|
569
|
+
DefaultDataSourceFactory(context, userAgent)
|
|
570
|
+
}
|
|
571
|
+
else -> {
|
|
572
|
+
val tempFactory = DefaultHttpDataSource.Factory().apply {
|
|
573
|
+
Log.d("pcTest", "pcTest getMediaSource 3")
|
|
574
|
+
setUserAgent(userAgent)
|
|
575
|
+
setAllowCrossProtocolRedirects(true)
|
|
576
|
+
Log.d("pcTest", "pcTest getMediaSource 4")
|
|
577
|
+
|
|
578
|
+
audioItem.options?.headers?.let {
|
|
579
|
+
setDefaultRequestProperties(it.toMap())
|
|
580
|
+
}
|
|
581
|
+
Log.d("pcTest", "pcTest getMediaSource 5")
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
enableCaching(tempFactory)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
Log.d("pcTest", "pcTest getMediaSource 6")
|
|
589
|
+
|
|
590
|
+
return when (audioItem.type) {
|
|
591
|
+
MediaType.DASH -> createDashSource(mediaItem, factory)
|
|
592
|
+
MediaType.HLS -> createHlsSource(mediaItem, factory)
|
|
593
|
+
MediaType.SMOOTH_STREAMING -> createSsSource(mediaItem, factory)
|
|
594
|
+
else -> createProgressiveSource(mediaItem, factory)
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private fun createDashSource(mediaItem: MediaItem, factory: DataSource.Factory?): MediaSource {
|
|
599
|
+
return DashMediaSource.Factory(DefaultDashChunkSource.Factory(factory!!), factory)
|
|
600
|
+
.createMediaSource(mediaItem)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private fun createHlsSource(mediaItem: MediaItem, factory: DataSource.Factory?): MediaSource {
|
|
604
|
+
return HlsMediaSource.Factory(factory!!)
|
|
605
|
+
.createMediaSource(mediaItem)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private fun createSsSource(mediaItem: MediaItem, factory: DataSource.Factory?): MediaSource {
|
|
609
|
+
return SsMediaSource.Factory(DefaultSsChunkSource.Factory(factory!!), factory)
|
|
610
|
+
.createMediaSource(mediaItem)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private fun createProgressiveSource(
|
|
614
|
+
mediaItem: MediaItem,
|
|
615
|
+
factory: DataSource.Factory
|
|
616
|
+
): ProgressiveMediaSource {
|
|
617
|
+
return ProgressiveMediaSource.Factory(
|
|
618
|
+
factory, DefaultExtractorsFactory()
|
|
619
|
+
.setConstantBitrateSeekingEnabled(true)
|
|
620
|
+
)
|
|
621
|
+
.createMediaSource(mediaItem)
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private fun enableCaching(factory: DataSource.Factory): DataSource.Factory {
|
|
625
|
+
return if (cache == null || cacheConfig == null || (cacheConfig.maxCacheSize ?: 0) <= 0) {
|
|
626
|
+
factory
|
|
627
|
+
} else {
|
|
628
|
+
CacheDataSource.Factory().apply {
|
|
629
|
+
setCache(this@BaseAudioPlayer.cache!!)
|
|
630
|
+
setUpstreamDataSourceFactory(factory)
|
|
631
|
+
setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private fun requestAudioFocus() {
|
|
637
|
+
if (hasAudioFocus) return
|
|
638
|
+
Timber.d("Requesting audio focus...")
|
|
639
|
+
|
|
640
|
+
val manager = ContextCompat.getSystemService(context, AudioManager::class.java)
|
|
641
|
+
|
|
642
|
+
focus = AudioFocusRequestCompat.Builder(AUDIOFOCUS_GAIN)
|
|
643
|
+
.setOnAudioFocusChangeListener(this)
|
|
644
|
+
.setAudioAttributes(
|
|
645
|
+
AudioAttributesCompat.Builder()
|
|
646
|
+
.setUsage(USAGE_MEDIA)
|
|
647
|
+
.setContentType(CONTENT_TYPE_MUSIC)
|
|
648
|
+
.build()
|
|
649
|
+
)
|
|
650
|
+
.setWillPauseWhenDucked(playerOptions.alwaysPauseOnInterruption)
|
|
651
|
+
.build()
|
|
652
|
+
|
|
653
|
+
val result: Int = if (manager != null && focus != null) {
|
|
654
|
+
AudioManagerCompat.requestAudioFocus(manager, focus!!)
|
|
655
|
+
} else {
|
|
656
|
+
AudioManager.AUDIOFOCUS_REQUEST_FAILED
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
hasAudioFocus = (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
private fun abandonAudioFocusIfHeld() {
|
|
663
|
+
if (!hasAudioFocus) return
|
|
664
|
+
Timber.d("Abandoning audio focus...")
|
|
665
|
+
|
|
666
|
+
val manager = ContextCompat.getSystemService(context, AudioManager::class.java)
|
|
667
|
+
|
|
668
|
+
val result: Int = if (manager != null && focus != null) {
|
|
669
|
+
AudioManagerCompat.abandonAudioFocusRequest(manager, focus!!)
|
|
670
|
+
} else {
|
|
671
|
+
AudioManager.AUDIOFOCUS_REQUEST_FAILED
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
hasAudioFocus = (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
override fun onAudioFocusChange(focusChange: Int) {
|
|
678
|
+
Timber.d("Audio focus changed")
|
|
679
|
+
var isPermanent = focusChange == AUDIOFOCUS_LOSS
|
|
680
|
+
var isPaused = when (focusChange) {
|
|
681
|
+
AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> true
|
|
682
|
+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> playerOptions.alwaysPauseOnInterruption
|
|
683
|
+
else -> false
|
|
684
|
+
}
|
|
685
|
+
if (!playerConfig.handleAudioFocus) {
|
|
686
|
+
if (isPermanent) abandonAudioFocusIfHeld()
|
|
687
|
+
|
|
688
|
+
var isDucking = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
|
|
689
|
+
&& !playerOptions.alwaysPauseOnInterruption
|
|
690
|
+
if (isDucking) {
|
|
691
|
+
volumeMultiplier = 0.5f
|
|
692
|
+
wasDucking = true
|
|
693
|
+
} else if (wasDucking) {
|
|
694
|
+
volumeMultiplier = 1f
|
|
695
|
+
wasDucking = false
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
playerEventHolder.updateOnAudioFocusChanged(isPaused, isPermanent)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
companion object {
|
|
703
|
+
const val APPLICATION_NAME = "react-native-track-player"
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
inner class PlayerListener : Listener {
|
|
707
|
+
/**
|
|
708
|
+
* Called when there is metadata associated with the current playback time.
|
|
709
|
+
*/
|
|
710
|
+
override fun onMetadata(metadata: Metadata) {
|
|
711
|
+
PlaybackMetadata.fromId3Metadata(metadata)
|
|
712
|
+
?.let { playerEventHolder.updateOnPlaybackMetadata(it) }
|
|
713
|
+
PlaybackMetadata.fromIcy(metadata)
|
|
714
|
+
?.let { playerEventHolder.updateOnPlaybackMetadata(it) }
|
|
715
|
+
PlaybackMetadata.fromVorbisComment(metadata)
|
|
716
|
+
?.let { playerEventHolder.updateOnPlaybackMetadata(it) }
|
|
717
|
+
PlaybackMetadata.fromQuickTime(metadata)
|
|
718
|
+
?.let { playerEventHolder.updateOnPlaybackMetadata(it) }
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* A position discontinuity occurs when the playing period changes, the playback position
|
|
724
|
+
* jumps within the period currently being played, or when the playing period has been
|
|
725
|
+
* skipped or removed.
|
|
726
|
+
*/
|
|
727
|
+
override fun onPositionDiscontinuity(
|
|
728
|
+
oldPosition: Player.PositionInfo,
|
|
729
|
+
newPosition: Player.PositionInfo,
|
|
730
|
+
reason: Int
|
|
731
|
+
) {
|
|
732
|
+
this@BaseAudioPlayer.oldPosition = oldPosition.positionMs
|
|
733
|
+
|
|
734
|
+
when (reason) {
|
|
735
|
+
Player.DISCONTINUITY_REASON_AUTO_TRANSITION -> playerEventHolder.updatePositionChangedReason(
|
|
736
|
+
PositionChangedReason.AUTO(oldPosition.positionMs, newPosition.positionMs)
|
|
737
|
+
)
|
|
738
|
+
Player.DISCONTINUITY_REASON_SEEK -> playerEventHolder.updatePositionChangedReason(
|
|
739
|
+
PositionChangedReason.SEEK(oldPosition.positionMs, newPosition.positionMs)
|
|
740
|
+
)
|
|
741
|
+
Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT -> playerEventHolder.updatePositionChangedReason(
|
|
742
|
+
PositionChangedReason.SEEK_FAILED(
|
|
743
|
+
oldPosition.positionMs,
|
|
744
|
+
newPosition.positionMs
|
|
745
|
+
)
|
|
746
|
+
)
|
|
747
|
+
Player.DISCONTINUITY_REASON_REMOVE -> playerEventHolder.updatePositionChangedReason(
|
|
748
|
+
PositionChangedReason.QUEUE_CHANGED(
|
|
749
|
+
oldPosition.positionMs,
|
|
750
|
+
newPosition.positionMs
|
|
751
|
+
)
|
|
752
|
+
)
|
|
753
|
+
Player.DISCONTINUITY_REASON_SKIP -> playerEventHolder.updatePositionChangedReason(
|
|
754
|
+
PositionChangedReason.SKIPPED_PERIOD(
|
|
755
|
+
oldPosition.positionMs,
|
|
756
|
+
newPosition.positionMs
|
|
757
|
+
)
|
|
758
|
+
)
|
|
759
|
+
Player.DISCONTINUITY_REASON_INTERNAL -> playerEventHolder.updatePositionChangedReason(
|
|
760
|
+
PositionChangedReason.UNKNOWN(oldPosition.positionMs, newPosition.positionMs)
|
|
761
|
+
)
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Called when playback transitions to a media item or starts repeating a media item
|
|
767
|
+
* according to the current repeat mode. Note that this callback is also called when the
|
|
768
|
+
* playlist becomes non-empty or empty as a consequence of a playlist change.
|
|
769
|
+
*/
|
|
770
|
+
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
|
771
|
+
when (reason) {
|
|
772
|
+
Player.MEDIA_ITEM_TRANSITION_REASON_AUTO -> playerEventHolder.updateAudioItemTransition(
|
|
773
|
+
AudioItemTransitionReason.AUTO(oldPosition)
|
|
774
|
+
)
|
|
775
|
+
Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED -> playerEventHolder.updateAudioItemTransition(
|
|
776
|
+
AudioItemTransitionReason.QUEUE_CHANGED(oldPosition)
|
|
777
|
+
)
|
|
778
|
+
Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT -> playerEventHolder.updateAudioItemTransition(
|
|
779
|
+
AudioItemTransitionReason.REPEAT(oldPosition)
|
|
780
|
+
)
|
|
781
|
+
Player.MEDIA_ITEM_TRANSITION_REASON_SEEK -> playerEventHolder.updateAudioItemTransition(
|
|
782
|
+
AudioItemTransitionReason.SEEK_TO_ANOTHER_AUDIO_ITEM(oldPosition)
|
|
783
|
+
)
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
updateNotificationMetadataIfAutomatic()
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Called when the value returned from Player.getPlayWhenReady() changes.
|
|
791
|
+
*/
|
|
792
|
+
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
|
|
793
|
+
val pausedBecauseReachedEnd = reason == Player.PLAY_WHEN_READY_CHANGE_REASON_END_OF_MEDIA_ITEM
|
|
794
|
+
playerEventHolder.updatePlayWhenReadyChange(PlayWhenReadyChangeData(playWhenReady, pausedBecauseReachedEnd))
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* The generic onEvents callback provides access to the Player object and specifies the set
|
|
799
|
+
* of events that occurred together. It’s always called after the callbacks that correspond
|
|
800
|
+
* to the individual events.
|
|
801
|
+
*/
|
|
802
|
+
override fun onEvents(player: Player, events: Player.Events) {
|
|
803
|
+
// Note that it is necessary to set `playerState` in order, since each mutation fires an
|
|
804
|
+
// event.
|
|
805
|
+
for (i in 0 until events.size()) {
|
|
806
|
+
when (events[i]) {
|
|
807
|
+
Player.EVENT_PLAYBACK_STATE_CHANGED -> {
|
|
808
|
+
var state = when (player.playbackState) {
|
|
809
|
+
Player.STATE_BUFFERING -> AudioPlayerState.BUFFERING
|
|
810
|
+
Player.STATE_READY -> AudioPlayerState.READY
|
|
811
|
+
Player.STATE_IDLE ->
|
|
812
|
+
// Avoid transitioning to idle from error or stopped
|
|
813
|
+
if (
|
|
814
|
+
playerState == AudioPlayerState.ERROR ||
|
|
815
|
+
playerState == AudioPlayerState.STOPPED
|
|
816
|
+
)
|
|
817
|
+
null
|
|
818
|
+
else
|
|
819
|
+
AudioPlayerState.IDLE
|
|
820
|
+
Player.STATE_ENDED -> AudioPlayerState.ENDED
|
|
821
|
+
else -> null // noop
|
|
822
|
+
}
|
|
823
|
+
if (state != null && state != playerState) {
|
|
824
|
+
playerState = state
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
Player.EVENT_MEDIA_ITEM_TRANSITION -> {
|
|
828
|
+
playbackError = null
|
|
829
|
+
if (currentItem != null) {
|
|
830
|
+
playerState = AudioPlayerState.LOADING
|
|
831
|
+
if (isPlaying) {
|
|
832
|
+
playerState = AudioPlayerState.READY
|
|
833
|
+
playerState = AudioPlayerState.PLAYING
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
Player.EVENT_PLAY_WHEN_READY_CHANGED -> {
|
|
838
|
+
if (!player.playWhenReady && playerState != AudioPlayerState.STOPPED) {
|
|
839
|
+
playerState = AudioPlayerState.PAUSED
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
Player.EVENT_IS_PLAYING_CHANGED -> {
|
|
843
|
+
if (player.isPlaying) {
|
|
844
|
+
playerState = AudioPlayerState.PLAYING
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
override fun onPlayerError(error: PlaybackException) {
|
|
852
|
+
var _playbackError = PlaybackError(
|
|
853
|
+
error.errorCodeName
|
|
854
|
+
.replace("ERROR_CODE_", "")
|
|
855
|
+
.lowercase(Locale.getDefault())
|
|
856
|
+
.replace("_", "-"),
|
|
857
|
+
error.message
|
|
858
|
+
)
|
|
859
|
+
playerEventHolder.updatePlaybackError(_playbackError)
|
|
860
|
+
playbackError = _playbackError
|
|
861
|
+
playerState = AudioPlayerState.ERROR
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|