@mustafaj/capacitor-plugin-playlist 0.9.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.
- package/CapacitorPluginPlaylist.podspec +17 -0
- package/README.md +248 -0
- package/android/.project +34 -0
- package/android/build.gradle +69 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +22 -0
- package/android/gradlew +251 -0
- package/android/gradlew.bat +94 -0
- package/android/proguard-rules.pro +21 -0
- package/android/settings.gradle +2 -0
- package/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java +26 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/App.kt +19 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/FakeR.kt +39 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/OnStatusCallback.kt +34 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/OnStatusReportListener.java +7 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistItemOptions.java +52 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +447 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioErrorType.java +13 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +487 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioStatusMessage.java +35 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxConstants.java +42 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/TrackRemovalItem.java +12 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/data/AudioTrack.kt +94 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/MediaControlsListener.kt +13 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/Options.kt +77 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistManager.kt +308 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/notification/PlaylistNotificationProvider.kt +26 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioApi.kt +114 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandler.java +146 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/BaseMediaApi.kt +36 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaImageProvider.kt +83 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaService.kt +98 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/android/src/main/res/drawable/ic_closed_caption_white_24dp.xml +9 -0
- package/android/src/main/res/drawable/ic_demo_icon_adaptive.xml +15 -0
- package/android/src/main/res/drawable/ic_launcher_background.xml +48 -0
- package/android/src/main/res/drawable/ic_launcher_foreground.xml +22 -0
- package/android/src/main/res/drawable/ic_notification_icon.png +0 -0
- package/android/src/main/res/layout/bridge_layout_main.xml +15 -0
- package/android/src/main/res/values/colors.xml +3 -0
- package/android/src/main/res/values/strings.xml +3 -0
- package/android/src/main/res/values/styles.xml +3 -0
- package/android/src/test/java/com/getcapacitor/ExampleUnitTest.java +18 -0
- package/dist/docs.json +2071 -0
- package/dist/esm/Constants.d.ts +164 -0
- package/dist/esm/Constants.js +175 -0
- package/dist/esm/Constants.js.map +1 -0
- package/dist/esm/RmxAudioPlayer.d.ts +181 -0
- package/dist/esm/RmxAudioPlayer.js +344 -0
- package/dist/esm/RmxAudioPlayer.js.map +1 -0
- package/dist/esm/definitions.d.ts +78 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/interfaces.d.ts +246 -0
- package/dist/esm/interfaces.js +2 -0
- package/dist/esm/interfaces.js.map +1 -0
- package/dist/esm/plugin.d.ts +3 -0
- package/dist/esm/plugin.js +13 -0
- package/dist/esm/plugin.js.map +1 -0
- package/dist/esm/utils.d.ts +15 -0
- package/dist/esm/utils.js +48 -0
- package/dist/esm/utils.js.map +1 -0
- package/dist/esm/web.d.ts +54 -0
- package/dist/esm/web.js +409 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +993 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +996 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Plugin/AVBidirectionalQueuePlayer.swift +269 -0
- package/ios/Plugin/AudioTrack.swift +63 -0
- package/ios/Plugin/Constants.swift +39 -0
- package/ios/Plugin/DispatchQueue.swift +47 -0
- package/ios/Plugin/Info.plist +24 -0
- package/ios/Plugin/Plugin.h +10 -0
- package/ios/Plugin/Plugin.m +30 -0
- package/ios/Plugin/Plugin.swift +208 -0
- package/ios/Plugin/RmxAudioPlayer.swift +1150 -0
- package/ios/Plugin.xcodeproj/project.pbxproj +574 -0
- package/ios/Plugin.xcworkspace/contents.xcworkspacedata +10 -0
- package/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/ios/PluginTests/Info.plist +22 -0
- package/ios/PluginTests/PluginTests.swift +35 -0
- package/ios/Podfile +16 -0
- package/package.json +89 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Apache 2.0 License
|
|
3
|
+
*
|
|
4
|
+
* Copyright (c) Sebastian Katzer 2017
|
|
5
|
+
*
|
|
6
|
+
* This file contains Original Code and/or Modifications of Original Code
|
|
7
|
+
* as defined in and that are subject to the Apache License
|
|
8
|
+
* Version 2.0 (the 'License'). You may not use this file except in
|
|
9
|
+
* compliance with the License. Please obtain a copy of the License at
|
|
10
|
+
* http://opensource.org/licenses/Apache-2.0/ and read it before using this
|
|
11
|
+
* file.
|
|
12
|
+
*
|
|
13
|
+
* The Original Code and all software distributed under the License are
|
|
14
|
+
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
|
|
15
|
+
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
|
|
16
|
+
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
|
|
18
|
+
* Please see the License for the specific language governing rights and
|
|
19
|
+
* limitations under the License.
|
|
20
|
+
*/
|
|
21
|
+
package org.dwbn.plugins.playlist.manager
|
|
22
|
+
|
|
23
|
+
import android.content.Context
|
|
24
|
+
import org.json.JSONObject
|
|
25
|
+
|
|
26
|
+
class Options {
|
|
27
|
+
/**
|
|
28
|
+
* Wrapped JSON object.
|
|
29
|
+
*/
|
|
30
|
+
// The original JSON object
|
|
31
|
+
val dict: JSONObject
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Application context.
|
|
35
|
+
*/
|
|
36
|
+
// The application context
|
|
37
|
+
val context: Context
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Constructor
|
|
41
|
+
*
|
|
42
|
+
* @param context The application context.
|
|
43
|
+
*/
|
|
44
|
+
constructor(context: Context) {
|
|
45
|
+
this.context = context
|
|
46
|
+
dict = JSONObject()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Constructor
|
|
51
|
+
*
|
|
52
|
+
* @param context The application context.
|
|
53
|
+
* @param options The options dict map.
|
|
54
|
+
*/
|
|
55
|
+
constructor(context: Context, options: JSONObject) {
|
|
56
|
+
this.context = context
|
|
57
|
+
dict = options
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* JSON object as string.
|
|
62
|
+
*/
|
|
63
|
+
override fun toString(): String {
|
|
64
|
+
return dict.toString()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* icon resource ID for the local notification.
|
|
69
|
+
*/
|
|
70
|
+
val icon: String
|
|
71
|
+
get() = dict.optString("icon", DEFAULT_ICON)
|
|
72
|
+
|
|
73
|
+
companion object {
|
|
74
|
+
// Default icon path
|
|
75
|
+
private const val DEFAULT_ICON = "icon"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist.manager
|
|
2
|
+
|
|
3
|
+
import android.app.Application
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import androidx.annotation.FloatRange
|
|
6
|
+
import androidx.annotation.IntRange
|
|
7
|
+
import com.devbrackets.android.exomedia.listener.OnErrorListener
|
|
8
|
+
import com.devbrackets.android.playlistcore.api.MediaPlayerApi
|
|
9
|
+
import com.devbrackets.android.playlistcore.manager.ListPlaylistManager
|
|
10
|
+
import org.dwbn.plugins.playlist.PlaylistItemOptions
|
|
11
|
+
import org.dwbn.plugins.playlist.TrackRemovalItem
|
|
12
|
+
import org.dwbn.plugins.playlist.data.AudioTrack
|
|
13
|
+
import org.dwbn.plugins.playlist.playlist.AudioApi
|
|
14
|
+
import org.dwbn.plugins.playlist.service.MediaService
|
|
15
|
+
import java.lang.ref.WeakReference
|
|
16
|
+
import java.util.*
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A PlaylistManager that extends the [ListPlaylistManager] for use with the
|
|
20
|
+
* [MediaService] which extends [com.devbrackets.android.playlistcore.service.BasePlaylistService].
|
|
21
|
+
*/
|
|
22
|
+
class PlaylistManager(application: Application) :
|
|
23
|
+
ListPlaylistManager<AudioTrack>(application, MediaService::class.java), OnErrorListener {
|
|
24
|
+
private val audioTracks: MutableList<AudioTrack> = ArrayList()
|
|
25
|
+
private var volumeLeft = 1.0f
|
|
26
|
+
private var volumeRight = 1.0f
|
|
27
|
+
private var playbackSpeed = 1.0f
|
|
28
|
+
var loop = false
|
|
29
|
+
var isShouldStopPlaylist = false
|
|
30
|
+
var currentErrorTrack: AudioTrack? = null
|
|
31
|
+
|
|
32
|
+
// Really need a way to propagate the settings through the app
|
|
33
|
+
var resetStreamOnPause = true
|
|
34
|
+
var options: Options
|
|
35
|
+
private var mediaControlsListener = WeakReference<MediaControlsListener?>(null)
|
|
36
|
+
private var errorListener = WeakReference<OnErrorListener?>(null)
|
|
37
|
+
private var currentMediaPlayer: WeakReference<MediaPlayerApi<AudioTrack>?>? =
|
|
38
|
+
WeakReference(null)
|
|
39
|
+
|
|
40
|
+
fun setOnErrorListener(listener: OnErrorListener?) {
|
|
41
|
+
errorListener = WeakReference(listener)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fun setMediaControlsListener(listener: MediaControlsListener?) {
|
|
45
|
+
mediaControlsListener = WeakReference(listener)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
val isPlaying: Boolean
|
|
49
|
+
get() = playlistHandler != null && playlistHandler!!.currentMediaPlayer != null && playlistHandler!!.currentMediaPlayer!!.isPlaying
|
|
50
|
+
|
|
51
|
+
override fun onError(e: Exception?): Boolean {
|
|
52
|
+
|
|
53
|
+
if (e != null && errorListener.get() != null) {
|
|
54
|
+
Log.i(TAG, "onError: $e")
|
|
55
|
+
errorListener.get()!!.onError(e)
|
|
56
|
+
}
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* isNextAvailable, getCurrentItem, and next() are overridden because there is
|
|
62
|
+
* a glaring bug in playlist core where when an item completes, isNextAvailable and
|
|
63
|
+
* getCurrentItem return wildly contradictory things, resulting in endless repeat
|
|
64
|
+
* of the last item in the playlist.
|
|
65
|
+
*/
|
|
66
|
+
override val isNextAvailable: Boolean
|
|
67
|
+
get() {
|
|
68
|
+
if (itemCount <= 1) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
val isAtEnd = currentPosition + 1 >= itemCount
|
|
72
|
+
val isConstrained = currentPosition + 1 in 0 until itemCount
|
|
73
|
+
return if (isAtEnd) {
|
|
74
|
+
loop
|
|
75
|
+
} else isConstrained
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override operator fun next(): AudioTrack? {
|
|
79
|
+
if (isNextAvailable) {
|
|
80
|
+
val isAtEnd = currentPosition + 1 >= itemCount
|
|
81
|
+
currentPosition = if (isAtEnd && loop) {
|
|
82
|
+
0
|
|
83
|
+
} else {
|
|
84
|
+
(currentPosition + 1).coerceAtMost(itemCount)
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
if (loop) {
|
|
88
|
+
currentPosition = INVALID_POSITION
|
|
89
|
+
} else {
|
|
90
|
+
isShouldStopPlaylist = true
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return currentItem
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
* List management
|
|
101
|
+
*/
|
|
102
|
+
fun setAllItems(items: List<AudioTrack>?, options: PlaylistItemOptions) {
|
|
103
|
+
clearItems()
|
|
104
|
+
addAllItems(items)
|
|
105
|
+
currentPosition = 0
|
|
106
|
+
// If the options said to start from a specific position, do so.
|
|
107
|
+
var seekStart: Long = 0
|
|
108
|
+
if (options.playFromPosition > 0) {
|
|
109
|
+
seekStart = options.playFromPosition
|
|
110
|
+
} else if (options.retainPosition) {
|
|
111
|
+
val progress = currentProgress
|
|
112
|
+
if (progress != null) {
|
|
113
|
+
seekStart = progress.position
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If the options said to start from a specific id, do so.
|
|
118
|
+
var idStart: String? = null
|
|
119
|
+
if (options.playFromId != null) {
|
|
120
|
+
idStart = options.playFromId
|
|
121
|
+
}
|
|
122
|
+
if (idStart != null && "" != idStart) {
|
|
123
|
+
val code = idStart.hashCode()
|
|
124
|
+
setCurrentItem(code.toLong())
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// We assume that if the playlist is fully loaded in one go,
|
|
128
|
+
// that the next thing to happen will be to play. So let's start
|
|
129
|
+
// paused, which will allow the player to pre-buffer until the
|
|
130
|
+
// user says Go.
|
|
131
|
+
beginPlayback(seekStart, options.startPaused)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fun addItem(item: AudioTrack?) {
|
|
135
|
+
if (item == null) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
val countBefore = audioTracks.size;
|
|
139
|
+
audioTracks.add(item)
|
|
140
|
+
items = audioTracks
|
|
141
|
+
if (countBefore == 0) {
|
|
142
|
+
currentPosition = 0
|
|
143
|
+
beginPlayback(1, true)
|
|
144
|
+
}
|
|
145
|
+
if (this.playlistHandler != null) {
|
|
146
|
+
this.playlistHandler!!.updateMediaControls()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fun addAllItems(its: List<AudioTrack>?) {
|
|
151
|
+
val currentItem = currentItem // may be null
|
|
152
|
+
audioTracks.addAll(its.orEmpty())
|
|
153
|
+
items =
|
|
154
|
+
audioTracks // not *strictly* needed since they share the reference, but for good measure..
|
|
155
|
+
currentPosition = audioTracks.indexOf(currentItem)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fun removeItem(index: Int, itemId: String): AudioTrack? {
|
|
159
|
+
val wasPlaying = isPlaying
|
|
160
|
+
if (playlistHandler != null) {
|
|
161
|
+
playlistHandler!!.pause(true)
|
|
162
|
+
}
|
|
163
|
+
var currentPosition = currentPosition
|
|
164
|
+
var foundItem: AudioTrack? = null
|
|
165
|
+
var removingCurrent = false
|
|
166
|
+
|
|
167
|
+
// Get the current playback position in milliseconds before removing items
|
|
168
|
+
val progress = currentProgress
|
|
169
|
+
val seekPosition: Long = if (progress != null) progress.position else 0
|
|
170
|
+
|
|
171
|
+
// If isPlaying is true, and currentItem is not null,
|
|
172
|
+
// that implies that currentItem is the currently playing item.
|
|
173
|
+
// If removingCurrent gets set to true, we are removing the currently playing item,
|
|
174
|
+
// and we need to restart playback once we do.
|
|
175
|
+
val resolvedIndex = resolveItemPosition(index, itemId)
|
|
176
|
+
if (resolvedIndex >= 0) {
|
|
177
|
+
foundItem = audioTracks[resolvedIndex]
|
|
178
|
+
if (foundItem == currentItem) {
|
|
179
|
+
removingCurrent = true
|
|
180
|
+
}
|
|
181
|
+
audioTracks.removeAt(resolvedIndex)
|
|
182
|
+
}
|
|
183
|
+
items = audioTracks
|
|
184
|
+
currentPosition = if (removingCurrent) currentPosition else audioTracks.indexOf(currentItem)
|
|
185
|
+
// If removing the current item, start from beginning (0), otherwise preserve playback position
|
|
186
|
+
val seekStart = if (removingCurrent) 0 else seekPosition
|
|
187
|
+
beginPlayback(seekStart, !wasPlaying)
|
|
188
|
+
if (this.playlistHandler != null) {
|
|
189
|
+
this.playlistHandler!!.updateMediaControls()
|
|
190
|
+
}
|
|
191
|
+
return foundItem
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fun removeAllItems(its: ArrayList<TrackRemovalItem>): ArrayList<AudioTrack> {
|
|
195
|
+
val removedTracks = ArrayList<AudioTrack>()
|
|
196
|
+
val wasPlaying = isPlaying
|
|
197
|
+
if (playlistHandler != null) {
|
|
198
|
+
playlistHandler!!.pause(true)
|
|
199
|
+
}
|
|
200
|
+
var currentPosition = currentPosition
|
|
201
|
+
val currentItem = currentItem // may be null
|
|
202
|
+
var removingCurrent = false
|
|
203
|
+
|
|
204
|
+
// Get the current playback position in milliseconds before removing items
|
|
205
|
+
val progress = currentProgress
|
|
206
|
+
val seekPosition: Long = if (progress != null) progress.position else 0
|
|
207
|
+
|
|
208
|
+
for (item in its) {
|
|
209
|
+
val resolvedIndex = resolveItemPosition(item.trackIndex, item.trackId)
|
|
210
|
+
if (resolvedIndex >= 0) {
|
|
211
|
+
val foundItem = audioTracks[resolvedIndex]
|
|
212
|
+
if (foundItem == currentItem) {
|
|
213
|
+
removingCurrent = true
|
|
214
|
+
}
|
|
215
|
+
removedTracks.add(foundItem)
|
|
216
|
+
audioTracks.removeAt(resolvedIndex)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
items = audioTracks
|
|
220
|
+
currentPosition = if (removingCurrent) currentPosition else audioTracks.indexOf(currentItem)
|
|
221
|
+
// If removing the current item, start from beginning (0), otherwise preserve playback position
|
|
222
|
+
val seekStart = if (removingCurrent) 0 else seekPosition
|
|
223
|
+
beginPlayback(seekStart, !wasPlaying)
|
|
224
|
+
return removedTracks
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fun clearItems() {
|
|
228
|
+
playlistHandler?.stop()
|
|
229
|
+
audioTracks.clear()
|
|
230
|
+
items = audioTracks
|
|
231
|
+
currentPosition = INVALID_POSITION
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fun getAllItems(): List<AudioTrack> {
|
|
235
|
+
return audioTracks.toList()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private fun resolveItemPosition(trackIndex: Int, trackId: String): Int {
|
|
239
|
+
var resolvedPosition = -1
|
|
240
|
+
if (trackIndex >= 0 && trackIndex < audioTracks.size) {
|
|
241
|
+
resolvedPosition = trackIndex
|
|
242
|
+
} else if ("" != trackId) {
|
|
243
|
+
val itemPos = getPositionForItem(trackId.hashCode().toLong())
|
|
244
|
+
if (itemPos != INVALID_POSITION) {
|
|
245
|
+
resolvedPosition = itemPos
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return resolvedPosition
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fun getVolumeLeft(): Float {
|
|
252
|
+
return volumeLeft
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
fun getVolumeRight(): Float {
|
|
256
|
+
return volumeRight
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
fun setVolume(
|
|
260
|
+
@FloatRange(from = 0.0, to = 1.0) left: Float,
|
|
261
|
+
@FloatRange(from = 0.0, to = 1.0) right: Float
|
|
262
|
+
) {
|
|
263
|
+
volumeLeft = left
|
|
264
|
+
volumeRight = right
|
|
265
|
+
if (currentMediaPlayer != null && currentMediaPlayer!!.get() != null) {
|
|
266
|
+
Log.i("PlaylistManager", "setVolume completing with volume = $left")
|
|
267
|
+
currentMediaPlayer!!.get()!!.setVolume(volumeLeft, volumeRight)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
fun getPlaybackSpeed(): Float {
|
|
272
|
+
return playbackSpeed
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fun setPlaybackSpeed(@FloatRange(from = 0.0625, to = 16.0) speed: Float) {
|
|
276
|
+
val validSpeed = speed.coerceIn(0.0625f, 16.0f)
|
|
277
|
+
playbackSpeed = validSpeed
|
|
278
|
+
playlistHandler?.let { handler ->
|
|
279
|
+
handler.currentMediaPlayer?.let { mediaPlayer ->
|
|
280
|
+
if (mediaPlayer is AudioApi) {
|
|
281
|
+
Log.i(TAG, "setPlaybackSpeed completing with speed = $validSpeed")
|
|
282
|
+
mediaPlayer.setPlaybackSpeed(playbackSpeed)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
fun beginPlayback(@IntRange(from = 0) seekPosition: Long, startPaused: Boolean) {
|
|
289
|
+
currentItem ?: return
|
|
290
|
+
super.play(seekPosition, startPaused)
|
|
291
|
+
try {
|
|
292
|
+
setVolume(volumeLeft, volumeRight)
|
|
293
|
+
setPlaybackSpeed(playbackSpeed)
|
|
294
|
+
} catch (e: Exception) {
|
|
295
|
+
Log.w(TAG, "beginPlayback: Error setting volume or playback speed: " + e.message)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
companion object {
|
|
300
|
+
private const val TAG = "PlaylistManager"
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
init {
|
|
304
|
+
setParameters(audioTracks, 0)
|
|
305
|
+
options = Options(application.baseContext)
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
}
|
package/android/src/main/java/org/dwbn/plugins/playlist/notification/PlaylistNotificationProvider.kt
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist.notification
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.app.PendingIntent
|
|
5
|
+
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
|
6
|
+
import android.app.PendingIntent.FLAG_IMMUTABLE
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import com.devbrackets.android.playlistcore.components.notification.DefaultPlaylistNotificationProvider
|
|
10
|
+
|
|
11
|
+
class PlaylistNotificationProvider(context: Context?) : DefaultPlaylistNotificationProvider(context!!) {
|
|
12
|
+
override val clickPendingIntent: PendingIntent?
|
|
13
|
+
@SuppressLint("UnspecifiedImmutableFlag")
|
|
14
|
+
get() {
|
|
15
|
+
val context = context
|
|
16
|
+
val pkgName = context.packageName
|
|
17
|
+
val intent = context
|
|
18
|
+
.packageManager
|
|
19
|
+
.getLaunchIntentForPackage(pkgName)
|
|
20
|
+
intent!!.addFlags(
|
|
21
|
+
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
|
22
|
+
return PendingIntent.getActivity(this.context,
|
|
23
|
+
0, intent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
|
|
2
|
+
package org.dwbn.plugins.playlist.playlist
|
|
3
|
+
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.media.AudioManager
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
import android.os.PowerManager
|
|
8
|
+
import androidx.annotation.FloatRange
|
|
9
|
+
import androidx.annotation.IntRange
|
|
10
|
+
import androidx.annotation.OptIn
|
|
11
|
+
import androidx.media3.common.AudioAttributes
|
|
12
|
+
import androidx.media3.common.C
|
|
13
|
+
import androidx.media3.common.util.UnstableApi
|
|
14
|
+
import androidx.media3.exoplayer.util.EventLogger
|
|
15
|
+
import com.devbrackets.android.exomedia.AudioPlayer
|
|
16
|
+
import com.devbrackets.android.exomedia.listener.OnErrorListener
|
|
17
|
+
import com.devbrackets.android.playlistcore.manager.BasePlaylistManager
|
|
18
|
+
import org.dwbn.plugins.playlist.data.AudioTrack
|
|
19
|
+
import java.lang.ref.WeakReference
|
|
20
|
+
import java.util.concurrent.locks.ReentrantLock
|
|
21
|
+
|
|
22
|
+
@OptIn(UnstableApi::class)
|
|
23
|
+
class AudioApi(context: Context) : BaseMediaApi() {
|
|
24
|
+
private val audioPlayer: AudioPlayer = AudioPlayer(context.applicationContext)
|
|
25
|
+
|
|
26
|
+
private val errorListenersLock = ReentrantLock(true)
|
|
27
|
+
private val errorListeners = ArrayList<WeakReference<OnErrorListener>>()
|
|
28
|
+
|
|
29
|
+
override val isPlaying: Boolean
|
|
30
|
+
get() = audioPlayer.isPlaying
|
|
31
|
+
|
|
32
|
+
override val handlesOwnAudioFocus: Boolean
|
|
33
|
+
get() = false
|
|
34
|
+
|
|
35
|
+
override val currentPosition: Long
|
|
36
|
+
get() = if (prepared) audioPlayer.currentPosition else 0
|
|
37
|
+
|
|
38
|
+
override val duration: Long
|
|
39
|
+
get() = if (prepared) audioPlayer.duration else 0
|
|
40
|
+
|
|
41
|
+
override val bufferedPercent: Int
|
|
42
|
+
get() = bufferPercent
|
|
43
|
+
|
|
44
|
+
init {
|
|
45
|
+
audioPlayer.setOnErrorListener(this)
|
|
46
|
+
audioPlayer.setOnPreparedListener(this)
|
|
47
|
+
audioPlayer.setOnCompletionListener(this)
|
|
48
|
+
audioPlayer.setOnSeekCompletionListener(this)
|
|
49
|
+
audioPlayer.setOnBufferUpdateListener(this)
|
|
50
|
+
|
|
51
|
+
audioPlayer.setWakeLevel(PowerManager.PARTIAL_WAKE_LOCK)
|
|
52
|
+
audioPlayer.setAudioAttributes(getAudioAttributes(C.USAGE_MEDIA, C.AUDIO_CONTENT_TYPE_MUSIC))
|
|
53
|
+
audioPlayer.setAnalyticsListener(EventLogger())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
override fun play() {
|
|
57
|
+
audioPlayer.start()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override fun pause() {
|
|
61
|
+
audioPlayer.pause()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override fun stop() {
|
|
65
|
+
audioPlayer.stop()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
override fun reset() {
|
|
69
|
+
audioPlayer.reset()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
override fun release() {
|
|
73
|
+
audioPlayer.release()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
override fun setVolume(@FloatRange(from = 0.0, to = 1.0) left: Float, @FloatRange(from = 0.0, to = 1.0) right: Float) {
|
|
77
|
+
audioPlayer.volume = (left + right) / 2
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
override fun seekTo(@IntRange(from = 0L) milliseconds: Long) {
|
|
81
|
+
audioPlayer.seekTo(milliseconds.toInt().toLong())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
override fun handlesItem(item: AudioTrack): Boolean {
|
|
85
|
+
return item.mediaType == BasePlaylistManager.AUDIO
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
override fun playItem(item: AudioTrack) {
|
|
89
|
+
try {
|
|
90
|
+
bufferPercent = 0
|
|
91
|
+
audioPlayer.setMedia(Uri.parse(if (item.downloaded) item.downloadedMediaUri else item.mediaUrl))
|
|
92
|
+
} catch (e: Exception) {
|
|
93
|
+
//Purposefully left blank
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fun setPlaybackSpeed(@FloatRange(from = 0.0, to = 1.0) speed: Float) {
|
|
98
|
+
audioPlayer.setPlaybackSpeed(speed)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fun addErrorListener(listener: OnErrorListener) {
|
|
102
|
+
errorListenersLock.lock()
|
|
103
|
+
errorListeners.add(WeakReference<OnErrorListener>(listener))
|
|
104
|
+
errorListenersLock.unlock()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Suppress("SameParameterValue")
|
|
108
|
+
private fun getAudioAttributes(@C.AudioUsage usage: Int, @C.AudioContentType contentType: Int): AudioAttributes {
|
|
109
|
+
return AudioAttributes.Builder()
|
|
110
|
+
.setUsage(usage)
|
|
111
|
+
.setContentType(contentType)
|
|
112
|
+
.build()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist.playlist;
|
|
2
|
+
|
|
3
|
+
import android.app.Service;
|
|
4
|
+
import android.content.Context;
|
|
5
|
+
import android.util.Log;
|
|
6
|
+
|
|
7
|
+
import androidx.annotation.Nullable;
|
|
8
|
+
|
|
9
|
+
import com.devbrackets.android.playlistcore.api.MediaPlayerApi;
|
|
10
|
+
import com.devbrackets.android.playlistcore.api.PlaylistItem;
|
|
11
|
+
import com.devbrackets.android.playlistcore.components.audiofocus.AudioFocusProvider;
|
|
12
|
+
import com.devbrackets.android.playlistcore.components.audiofocus.DefaultAudioFocusProvider;
|
|
13
|
+
import com.devbrackets.android.playlistcore.components.image.ImageProvider;
|
|
14
|
+
import com.devbrackets.android.playlistcore.components.mediacontrols.DefaultMediaControlsProvider;
|
|
15
|
+
import com.devbrackets.android.playlistcore.components.mediacontrols.MediaControlsProvider;
|
|
16
|
+
import com.devbrackets.android.playlistcore.components.mediasession.DefaultMediaSessionProvider;
|
|
17
|
+
import com.devbrackets.android.playlistcore.components.mediasession.MediaSessionProvider;
|
|
18
|
+
import com.devbrackets.android.playlistcore.components.playlisthandler.DefaultPlaylistHandler;
|
|
19
|
+
import com.devbrackets.android.playlistcore.manager.BasePlaylistManager;
|
|
20
|
+
|
|
21
|
+
import org.dwbn.plugins.playlist.RmxAudioPlayer;
|
|
22
|
+
import org.dwbn.plugins.playlist.data.AudioTrack;
|
|
23
|
+
import org.dwbn.plugins.playlist.manager.PlaylistManager;
|
|
24
|
+
import org.dwbn.plugins.playlist.notification.PlaylistNotificationProvider;
|
|
25
|
+
import org.jetbrains.annotations.NotNull;
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
public class AudioPlaylistHandler<I extends PlaylistItem, M extends BasePlaylistManager<I>>
|
|
29
|
+
extends DefaultPlaylistHandler<I, M> {
|
|
30
|
+
|
|
31
|
+
private static final String TAG = "PlaylistAudioPlaylistHandler";
|
|
32
|
+
|
|
33
|
+
AudioPlaylistHandler(
|
|
34
|
+
Context context,
|
|
35
|
+
Class<? extends Service> serviceClass,
|
|
36
|
+
M playlistManager,
|
|
37
|
+
ImageProvider<I> imageProvider,
|
|
38
|
+
com.devbrackets.android.playlistcore.components.notification.PlaylistNotificationProvider notificationProvider,
|
|
39
|
+
MediaSessionProvider mediaSessionProvider,
|
|
40
|
+
MediaControlsProvider mediaControlsProvider,
|
|
41
|
+
AudioFocusProvider<I> audioFocusProvider,
|
|
42
|
+
@Nullable Listener<I> listener
|
|
43
|
+
) {
|
|
44
|
+
super(context, serviceClass, playlistManager, imageProvider, notificationProvider,
|
|
45
|
+
mediaSessionProvider, mediaControlsProvider, audioFocusProvider, listener);
|
|
46
|
+
// Lmao this entire class exists for the sake of this one line
|
|
47
|
+
// The default value is 30fps (e.g 33ms), which would overwhelm the Cordova webview with messages
|
|
48
|
+
// Ideally we could make this configurable.
|
|
49
|
+
getMediaProgressPoll().setProgressPollDelay(1000);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public void next() {
|
|
53
|
+
if (!getPlaylistManager().isNextAvailable()) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
getPlaylistManager().next();
|
|
57
|
+
startItemPlayback(0, !this.isPlaying());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public void previous() {
|
|
61
|
+
if (!getPlaylistManager().isPreviousAvailable()) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
getPlaylistManager().previous();
|
|
65
|
+
startItemPlayback(0, !this.isPlaying());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Override
|
|
69
|
+
public void onPrepared(@NotNull MediaPlayerApi<I> mediaPlayer) {
|
|
70
|
+
super.onPrepared(mediaPlayer);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Override
|
|
74
|
+
public boolean onError(@NotNull MediaPlayerApi<I> mediaPlayer) {
|
|
75
|
+
((PlaylistManager)getPlaylistManager()).setCurrentErrorTrack((AudioTrack) getCurrentPlaylistItem());
|
|
76
|
+
int currentIndex = getPlaylistManager().getCurrentPosition();
|
|
77
|
+
int currentErrorCount = getSequentialErrors();
|
|
78
|
+
|
|
79
|
+
super.onError(mediaPlayer);
|
|
80
|
+
// Do not set startPaused to false if we are at the first item.
|
|
81
|
+
// For all other items, the user MUST have triggered playback;
|
|
82
|
+
// for item 0, they will never have done so at this point (since the tracks
|
|
83
|
+
// are auto-buffered when a list is loaded).
|
|
84
|
+
// This is a bit of a guess. What happens of tracks 2,3,4 are also broken?
|
|
85
|
+
// User has no network? The only way to be *certain* is to capture all user interaction
|
|
86
|
+
// input points and create a global flag "somewhere" that says the user has tried to play.
|
|
87
|
+
// Maintaining that would be a nightmare.
|
|
88
|
+
if (currentIndex > 0 && currentErrorCount <= 3) {
|
|
89
|
+
Log.e(TAG, "ListHandler error: setting startPaused to false");
|
|
90
|
+
setStartPaused(false);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Override
|
|
97
|
+
public void onSeekComplete(MediaPlayerApi<I> mediaPlayer) {
|
|
98
|
+
Log.i(TAG, "onSeekComplete! " + mediaPlayer.getCurrentPosition());
|
|
99
|
+
getCurrentMediaProgress().update(mediaPlayer.getCurrentPosition(), mediaPlayer.getBufferedPercent(), mediaPlayer.getDuration());
|
|
100
|
+
super.onSeekComplete(mediaPlayer);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@Override
|
|
104
|
+
public void onCompletion(@NotNull MediaPlayerApi<I> mediaPlayer) {
|
|
105
|
+
((RmxAudioPlayer)super.getPlaylistManager().getPlaybackStatusListener()).onCompletion((AudioTrack) getCurrentPlaylistItem());
|
|
106
|
+
Log.i("AudioPlaylistHandler", "onCompletion");
|
|
107
|
+
// This is called when a single item completes playback.
|
|
108
|
+
// For now, the superclass does the right thing, but we may need to override.
|
|
109
|
+
super.onCompletion(mediaPlayer);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
public static class Builder<I extends PlaylistItem, M extends BasePlaylistManager<I>> {
|
|
114
|
+
|
|
115
|
+
Context context;
|
|
116
|
+
Class<? extends Service> serviceClass;
|
|
117
|
+
M playlistManager;
|
|
118
|
+
ImageProvider<I> imageProvider;
|
|
119
|
+
|
|
120
|
+
com.devbrackets.android.playlistcore.components.notification.PlaylistNotificationProvider notificationProvider = null;
|
|
121
|
+
MediaSessionProvider mediaSessionProvider = null;
|
|
122
|
+
MediaControlsProvider mediaControlsProvider = null;
|
|
123
|
+
AudioFocusProvider<I> audioFocusProvider = null;
|
|
124
|
+
Listener<I> listener;
|
|
125
|
+
public Builder(Context context, Class<? extends Service> serviceClass,
|
|
126
|
+
M playlistManager, ImageProvider<I> imageProvider, Listener<I> listener) {
|
|
127
|
+
this.context = context;
|
|
128
|
+
this.serviceClass = serviceClass;
|
|
129
|
+
this.playlistManager = playlistManager;
|
|
130
|
+
this.imageProvider = imageProvider;
|
|
131
|
+
this.listener = listener;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public AudioPlaylistHandler<I, M> build() {
|
|
135
|
+
return new AudioPlaylistHandler<>(context,
|
|
136
|
+
serviceClass,
|
|
137
|
+
playlistManager,
|
|
138
|
+
imageProvider,
|
|
139
|
+
notificationProvider != null ? notificationProvider : new PlaylistNotificationProvider(context),
|
|
140
|
+
mediaSessionProvider != null ? mediaSessionProvider : new DefaultMediaSessionProvider(context, serviceClass),
|
|
141
|
+
mediaControlsProvider != null ? mediaControlsProvider : new DefaultMediaControlsProvider(context),
|
|
142
|
+
audioFocusProvider != null ? audioFocusProvider : new DefaultAudioFocusProvider<I>(context),
|
|
143
|
+
listener);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|