@mustafaj/capacitor-plugin-playlist 0.9.0 → 0.9.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/CHANGELOG.md +9 -0
- package/{CapacitorPluginPlaylist.podspec → MustafajCapacitorPluginPlaylist.podspec} +2 -2
- package/Package.swift +27 -0
- package/README.md +7 -26
- package/android/build.gradle +1 -1
- package/android/src/main/AndroidManifest.xml +12 -2
- package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +1 -1
- package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistRuntime.kt +27 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +7 -7
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/Options.kt +2 -2
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistManager.kt +16 -1
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaImageProvider.kt +4 -2
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaService.kt +3 -3
- package/ios/Plugin/AVBidirectionalQueuePlayer.swift +2 -1
- package/ios/Plugin/Plugin.m +0 -1
- package/ios/Plugin/RmxAudioPlayer.swift +126 -60
- package/package.json +7 -6
- package/android/src/main/java/org/dwbn/plugins/playlist/App.kt +0 -19
- package/dist/plugin.cjs.js +0 -993
- package/dist/plugin.cjs.js.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
|
|
7
|
+
- Low-level `Playlist.removeItem()` and `Playlist.removeItems()` now accept only `id` / `index`.
|
|
8
|
+
- If you were calling the raw Capacitor plugin API with `trackId` / `trackIndex`, update those calls to `id` / `index`.
|
|
9
|
+
- The higher-level `RmxAudioPlayer` wrapper still accepts `trackId` / `trackIndex` and maps them to the canonical plugin contract.
|
|
@@ -3,7 +3,7 @@ require 'json'
|
|
|
3
3
|
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
4
|
|
|
5
5
|
Pod::Spec.new do |s|
|
|
6
|
-
s.name = '
|
|
6
|
+
s.name = 'MustafajCapacitorPluginPlaylist'
|
|
7
7
|
s.version = package['version']
|
|
8
8
|
s.summary = package['description']
|
|
9
9
|
s.license = package['license']
|
|
@@ -13,5 +13,5 @@ Pod::Spec.new do |s|
|
|
|
13
13
|
s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
|
|
14
14
|
s.ios.deployment_target = '15.0'
|
|
15
15
|
s.dependency 'Capacitor'
|
|
16
|
-
s.swift_version = '5.
|
|
16
|
+
s.swift_version = '5.9'
|
|
17
17
|
end
|
package/Package.swift
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "CapacitorPluginPlaylist",
|
|
6
|
+
platforms: [.iOS(.v15)],
|
|
7
|
+
products: [
|
|
8
|
+
.library(
|
|
9
|
+
name: "CapacitorPluginPlaylist",
|
|
10
|
+
targets: ["Plugin"]
|
|
11
|
+
)
|
|
12
|
+
],
|
|
13
|
+
dependencies: [
|
|
14
|
+
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
|
|
15
|
+
],
|
|
16
|
+
targets: [
|
|
17
|
+
.target(
|
|
18
|
+
name: "Plugin",
|
|
19
|
+
dependencies: [
|
|
20
|
+
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
21
|
+
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
22
|
+
],
|
|
23
|
+
path: "ios/Plugin",
|
|
24
|
+
publicHeadersPath: "."
|
|
25
|
+
)
|
|
26
|
+
]
|
|
27
|
+
)
|
package/README.md
CHANGED
|
@@ -60,34 +60,16 @@ architect => build => options:
|
|
|
60
60
|
]
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
#####
|
|
63
|
+
##### Android manifest
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
67
|
-
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
68
|
-
<application
|
|
69
|
-
android:name="org.dwbn.plugins.playlist.App"
|
|
70
|
-
>
|
|
71
|
-
<service android:enabled="true" android:exported="false"
|
|
72
|
-
android:name="org.dwbn.plugins.playlist.service.MediaService">
|
|
73
|
-
</service>
|
|
74
|
-
</application>
|
|
75
|
-
```
|
|
65
|
+
The plugin now ships its own merged Android manifest entries for:
|
|
76
66
|
|
|
77
|
-
|
|
67
|
+
- `WAKE_LOCK`
|
|
68
|
+
- `FOREGROUND_SERVICE`
|
|
69
|
+
- `FOREGROUND_SERVICE_MEDIA_PLAYBACK`
|
|
70
|
+
- `org.dwbn.plugins.playlist.service.MediaService`
|
|
78
71
|
|
|
79
|
-
|
|
80
|
-
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
81
|
-
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
|
82
|
-
<application
|
|
83
|
-
android:name="org.dwbn.plugins.playlist.App"
|
|
84
|
-
>
|
|
85
|
-
<service android:enabled="true" android:exported="false"
|
|
86
|
-
android:foregroundServiceType="mediaPlayback"
|
|
87
|
-
android:name="org.dwbn.plugins.playlist.service.MediaService">
|
|
88
|
-
</service>
|
|
89
|
-
</application>
|
|
90
|
-
```
|
|
72
|
+
You do not need to replace your app's `Application` class to use the plugin.
|
|
91
73
|
|
|
92
74
|
##### Gradle Configuration (Gradle 9+)
|
|
93
75
|
|
|
@@ -141,7 +123,6 @@ also see https://guides.codepath.com/android/Displaying-Images-with-the-Glide-Li
|
|
|
141
123
|
<key>UIBackgroundModes</key>
|
|
142
124
|
<array>
|
|
143
125
|
<string>audio</string>
|
|
144
|
-
<string>fetch</string>
|
|
145
126
|
</array>
|
|
146
127
|
```
|
|
147
128
|
|
package/android/build.gradle
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
|
|
2
|
-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
-
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
4
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
5
|
+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
|
6
|
+
|
|
7
|
+
<application>
|
|
8
|
+
<service
|
|
9
|
+
android:name="org.dwbn.plugins.playlist.service.MediaService"
|
|
10
|
+
android:enabled="true"
|
|
11
|
+
android:exported="false"
|
|
12
|
+
android:foregroundServiceType="mediaPlayback" />
|
|
13
|
+
</application>
|
|
4
14
|
</manifest>
|
|
@@ -19,7 +19,7 @@ public class PlaylistPlugin : Plugin(), OnStatusReportListener {
|
|
|
19
19
|
private var resetStreamOnPause = true
|
|
20
20
|
|
|
21
21
|
override fun load() {
|
|
22
|
-
audioPlayerImpl = RmxAudioPlayer(this,
|
|
22
|
+
audioPlayerImpl = RmxAudioPlayer(this, this.context.applicationContext)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
@PluginMethod
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist
|
|
2
|
+
|
|
3
|
+
import android.app.Application
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import org.dwbn.plugins.playlist.manager.PlaylistManager
|
|
6
|
+
|
|
7
|
+
object PlaylistRuntime {
|
|
8
|
+
private var playlistManager: PlaylistManager? = null
|
|
9
|
+
|
|
10
|
+
@JvmStatic
|
|
11
|
+
fun getPlaylistManager(context: Context): PlaylistManager {
|
|
12
|
+
synchronized(this) {
|
|
13
|
+
if (playlistManager == null) {
|
|
14
|
+
playlistManager = PlaylistManager(context.applicationContext as Application)
|
|
15
|
+
}
|
|
16
|
+
return playlistManager!!
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@JvmStatic
|
|
21
|
+
fun resetPlaylistManager(context: Context): PlaylistManager {
|
|
22
|
+
synchronized(this) {
|
|
23
|
+
playlistManager = PlaylistManager(context.applicationContext as Application)
|
|
24
|
+
return playlistManager!!
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
package org.dwbn.plugins.playlist;
|
|
2
2
|
|
|
3
|
+
import android.content.Context;
|
|
3
4
|
import android.util.Log;
|
|
4
5
|
|
|
5
6
|
import androidx.annotation.NonNull;
|
|
@@ -46,9 +47,9 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
|
|
|
46
47
|
private boolean resetStreamOnPause = true;
|
|
47
48
|
private String pendingSelectionTrackId = null;
|
|
48
49
|
private boolean suppressSelectionPlaybackEvents = false;
|
|
49
|
-
private final
|
|
50
|
+
private final Context context;
|
|
50
51
|
|
|
51
|
-
public RmxAudioPlayer(@NonNull OnStatusReportListener statusListener,
|
|
52
|
+
public RmxAudioPlayer(@NonNull OnStatusReportListener statusListener, @NonNull Context context) {
|
|
52
53
|
// AudioPlayerPlugin and RmxAudioPlayer are separate classes in order to increase
|
|
53
54
|
// the portability of this code.
|
|
54
55
|
// Because AudioPlayerPlugin itself holds a strong reference to this class,
|
|
@@ -56,10 +57,9 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
|
|
|
56
57
|
// but these two objects will always live together (And the plugin couldn't function
|
|
57
58
|
// at all if this one gets garbage collected).
|
|
58
59
|
this.statusListener = statusListener;
|
|
59
|
-
this.
|
|
60
|
+
this.context = context.getApplicationContext();
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
getPlaylistManager();
|
|
62
|
+
playlistManager = PlaylistRuntime.resetPlaylistManager(this.context);
|
|
63
63
|
playlistManager.setId(PLAYLIST_ID);
|
|
64
64
|
playlistManager.setPlaybackStatusListener(this);
|
|
65
65
|
playlistManager.setOnErrorListener(this);
|
|
@@ -67,7 +67,7 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
public PlaylistManager getPlaylistManager() {
|
|
70
|
-
playlistManager =
|
|
70
|
+
playlistManager = PlaylistRuntime.getPlaylistManager(context);
|
|
71
71
|
return playlistManager;
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -81,7 +81,7 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
public void setOptions(JSONObject val) {
|
|
84
|
-
Options options = new Options(
|
|
84
|
+
Options options = new Options(context, val);
|
|
85
85
|
getPlaylistManager().setOptions(options);
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -205,9 +205,24 @@ class PlaylistManager(application: Application) :
|
|
|
205
205
|
val progress = currentProgress
|
|
206
206
|
val seekPosition: Long = if (progress != null) progress.position else 0
|
|
207
207
|
|
|
208
|
+
val snapshot = audioTracks.toList()
|
|
209
|
+
val resolvedIndices = LinkedHashSet<Int>()
|
|
208
210
|
for (item in its) {
|
|
209
|
-
val resolvedIndex =
|
|
211
|
+
val resolvedIndex =
|
|
212
|
+
if (item.trackIndex >= 0 && item.trackIndex < snapshot.size) {
|
|
213
|
+
item.trackIndex
|
|
214
|
+
} else if (item.trackId.isNotEmpty()) {
|
|
215
|
+
snapshot.indexOfFirst { it.trackId == item.trackId }
|
|
216
|
+
} else {
|
|
217
|
+
-1
|
|
218
|
+
}
|
|
210
219
|
if (resolvedIndex >= 0) {
|
|
220
|
+
resolvedIndices.add(resolvedIndex)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (resolvedIndex in resolvedIndices.sortedDescending()) {
|
|
225
|
+
if (resolvedIndex in audioTracks.indices) {
|
|
211
226
|
val foundItem = audioTracks[resolvedIndex]
|
|
212
227
|
if (foundItem == currentItem) {
|
|
213
228
|
removingCurrent = true
|
|
@@ -66,10 +66,12 @@ class MediaImageProvider(
|
|
|
66
66
|
private inner class RemoteViewImageTarget : CustomTarget<Bitmap>() {
|
|
67
67
|
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
|
68
68
|
artworkImage = resource
|
|
69
|
+
onImageUpdatedListener.onImageUpdated()
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
override fun onLoadCleared(placeholder: android.graphics.drawable.Drawable?) {
|
|
72
|
-
|
|
73
|
+
artworkImage = null
|
|
74
|
+
onImageUpdatedListener.onImageUpdated()
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -80,4 +82,4 @@ class MediaImageProvider(
|
|
|
80
82
|
fakeR.getId("drawable", options.icon)
|
|
81
83
|
)
|
|
82
84
|
}
|
|
83
|
-
}
|
|
85
|
+
}
|
|
@@ -8,7 +8,7 @@ import android.os.Build
|
|
|
8
8
|
import android.util.Log
|
|
9
9
|
import com.devbrackets.android.playlistcore.components.playlisthandler.PlaylistHandler
|
|
10
10
|
import com.devbrackets.android.playlistcore.service.BasePlaylistService
|
|
11
|
-
import org.dwbn.plugins.playlist.
|
|
11
|
+
import org.dwbn.plugins.playlist.PlaylistRuntime
|
|
12
12
|
import org.dwbn.plugins.playlist.data.AudioTrack
|
|
13
13
|
import org.dwbn.plugins.playlist.manager.PlaylistManager
|
|
14
14
|
import org.dwbn.plugins.playlist.playlist.AudioApi
|
|
@@ -78,7 +78,7 @@ class MediaService : BasePlaylistService<AudioTrack, PlaylistManager>() {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
override val playlistManager: PlaylistManager
|
|
81
|
-
get() = (applicationContext
|
|
81
|
+
get() = PlaylistRuntime.getPlaylistManager(applicationContext)
|
|
82
82
|
|
|
83
83
|
override fun newPlaylistHandler(): PlaylistHandler<AudioTrack> {
|
|
84
84
|
val imageProvider = MediaImageProvider(applicationContext, object : OnImageUpdatedListener {
|
|
@@ -95,4 +95,4 @@ class MediaService : BasePlaylistService<AudioTrack, PlaylistManager>() {
|
|
|
95
95
|
null
|
|
96
96
|
).build()
|
|
97
97
|
}
|
|
98
|
-
}
|
|
98
|
+
}
|
|
@@ -35,6 +35,7 @@ let AVBidirectionalQueueCleared = "AVBidirectionalQueuePlayer.Cleared"
|
|
|
35
35
|
|
|
36
36
|
class AVBidirectionalQueuePlayer: AVQueuePlayer {
|
|
37
37
|
var queuedAudioTracks: [AudioTrack] = []
|
|
38
|
+
var wrapsWhenAtEnd = false
|
|
38
39
|
|
|
39
40
|
var isPlaying: Bool {
|
|
40
41
|
timeControlStatus == .playing
|
|
@@ -126,7 +127,7 @@ class AVBidirectionalQueuePlayer: AVQueuePlayer {
|
|
|
126
127
|
open override func advanceToNextItem() {
|
|
127
128
|
if currentIndex() == nil || currentIndex()! < queuedAudioTracks.count - 1{
|
|
128
129
|
super.advanceToNextItem();
|
|
129
|
-
} else {
|
|
130
|
+
} else if wrapsWhenAtEnd {
|
|
130
131
|
setCurrentIndex(0)
|
|
131
132
|
}
|
|
132
133
|
}
|
package/ios/Plugin/Plugin.m
CHANGED
|
@@ -21,7 +21,6 @@ CAP_PLUGIN(PlaylistPlugin, "Playlist",
|
|
|
21
21
|
CAP_PLUGIN_METHOD(seekTo, CAPPluginReturnPromise);
|
|
22
22
|
CAP_PLUGIN_METHOD(playTrackByIndex, CAPPluginReturnPromise);
|
|
23
23
|
CAP_PLUGIN_METHOD(playTrackById, CAPPluginReturnPromise);
|
|
24
|
-
CAP_PLUGIN_METHOD(playTrackByIndex, CAPPluginReturnPromise);
|
|
25
24
|
CAP_PLUGIN_METHOD(selectTrackByIndex, CAPPluginReturnPromise);
|
|
26
25
|
CAP_PLUGIN_METHOD(selectTrackById, CAPPluginReturnPromise);
|
|
27
26
|
CAP_PLUGIN_METHOD(setPlaybackVolume, CAPPluginReturnPromise);
|
|
@@ -27,6 +27,8 @@ final class RmxAudioPlayer: NSObject {
|
|
|
27
27
|
private var isReplacingItems = false
|
|
28
28
|
private var isWaitingToStartPlayback = false
|
|
29
29
|
private var loop = false
|
|
30
|
+
private var queueObserversRegistered = false
|
|
31
|
+
private var observedTrackItems: Set<ObjectIdentifier> = []
|
|
30
32
|
|
|
31
33
|
let avQueuePlayer = AVBidirectionalQueuePlayer(items: [])
|
|
32
34
|
|
|
@@ -52,9 +54,7 @@ final class RmxAudioPlayer: NSObject {
|
|
|
52
54
|
print("RmxAudioPlayer.execute=initialize")
|
|
53
55
|
|
|
54
56
|
avQueuePlayer.actionAtItemEnd = .advance
|
|
55
|
-
|
|
56
|
-
avQueuePlayer.addObserver(self, forKeyPath: "rate", options: .new, context: nil)
|
|
57
|
-
avQueuePlayer.addObserver(self, forKeyPath: "timeControlStatus", options: .new, context: nil)
|
|
57
|
+
registerQueueObservers()
|
|
58
58
|
|
|
59
59
|
let interval = CMTimeMakeWithSeconds(Float64(1.0), preferredTimescale: Int32(Double(NSEC_PER_SEC)))
|
|
60
60
|
playbackTimeObserver = avQueuePlayer.addPeriodicTimeObserver(forInterval: interval, queue: .main, using: { [weak self] time in
|
|
@@ -114,24 +114,26 @@ final class RmxAudioPlayer: NSObject {
|
|
|
114
114
|
|
|
115
115
|
var removed = 0
|
|
116
116
|
if items.count > 0 {
|
|
117
|
+
let snapshot = avQueuePlayer.queuedAudioTracks
|
|
118
|
+
var indices = Set<Int>()
|
|
117
119
|
for item in items {
|
|
118
120
|
guard let item = item as? [String: Any] else {
|
|
119
121
|
continue
|
|
120
122
|
}
|
|
121
123
|
|
|
122
|
-
if let
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
else if let index = (item["index"] as? NSNumber)?.intValue {
|
|
129
|
-
do {
|
|
130
|
-
try removeItem(index)
|
|
131
|
-
removed += 1
|
|
132
|
-
} catch {}
|
|
124
|
+
if let index = (item["index"] as? NSNumber)?.intValue, snapshot.indices.contains(index) {
|
|
125
|
+
indices.insert(index)
|
|
126
|
+
} else if let id = item["id"] as? String,
|
|
127
|
+
let index = snapshot.firstIndex(where: { $0.trackId == id }) {
|
|
128
|
+
indices.insert(index)
|
|
133
129
|
}
|
|
130
|
+
}
|
|
134
131
|
|
|
132
|
+
for index in indices.sorted(by: >) {
|
|
133
|
+
do {
|
|
134
|
+
try removeItem(index)
|
|
135
|
+
removed += 1
|
|
136
|
+
} catch {}
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
139
|
|
|
@@ -189,6 +191,7 @@ final class RmxAudioPlayer: NSObject {
|
|
|
189
191
|
|
|
190
192
|
func setLoopAll(_ loop: Bool) {
|
|
191
193
|
self.loop = loop
|
|
194
|
+
avQueuePlayer.wrapsWhenAtEnd = loop
|
|
192
195
|
|
|
193
196
|
print("RmxAudioPlayer.execute=setLoopAll, \(loop)")
|
|
194
197
|
}
|
|
@@ -213,12 +216,14 @@ final class RmxAudioPlayer: NSObject {
|
|
|
213
216
|
guard !avQueuePlayer.queuedAudioTracks.isEmpty else {
|
|
214
217
|
throw "Queue is Empty"
|
|
215
218
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
219
|
+
guard
|
|
220
|
+
let result = findTrack(byId: id),
|
|
221
|
+
let idx = (result["index"] as? NSNumber)?.intValue,
|
|
222
|
+
idx >= 0
|
|
223
|
+
else {
|
|
224
|
+
throw "Track ID not found"
|
|
221
225
|
}
|
|
226
|
+
avQueuePlayer.setCurrentIndex(idx)
|
|
222
227
|
}
|
|
223
228
|
|
|
224
229
|
func removeItem(_ index: Int) throws {
|
|
@@ -232,20 +237,18 @@ final class RmxAudioPlayer: NSObject {
|
|
|
232
237
|
}
|
|
233
238
|
|
|
234
239
|
func removeItem(_ id: String) throws {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
240
|
+
guard
|
|
241
|
+
let result = findTrack(byId: id),
|
|
242
|
+
let idx = (result["index"] as? NSNumber)?.intValue,
|
|
243
|
+
let track = result["track"] as? AudioTrack,
|
|
244
|
+
idx >= 0
|
|
245
|
+
else {
|
|
246
|
+
throw "Could not find track by id " + id
|
|
241
247
|
}
|
|
242
|
-
// AudioTrack* item = [self avQueuePlayer].itemsForPlayer[idx];
|
|
243
|
-
removeTrackObservers(track)
|
|
244
248
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
onStatus(.rmxstatus_ITEM_REMOVED, trackId: track?.trackId, param: track?.toDict())
|
|
249
|
+
removeTrackObservers(track)
|
|
250
|
+
avQueuePlayer.remove(track)
|
|
251
|
+
onStatus(.rmxstatus_ITEM_REMOVED, trackId: track.trackId, param: track.toDict())
|
|
249
252
|
}
|
|
250
253
|
|
|
251
254
|
// MARK: - player actions
|
|
@@ -390,7 +393,9 @@ final class RmxAudioPlayer: NSObject {
|
|
|
390
393
|
}
|
|
391
394
|
|
|
392
395
|
func setTracks(_ tracks: [AudioTrack], startIndex: Int, startPosition: Float) {
|
|
393
|
-
avQueuePlayer.
|
|
396
|
+
for item in avQueuePlayer.queuedAudioTracks {
|
|
397
|
+
removeTrackObservers(item)
|
|
398
|
+
}
|
|
394
399
|
|
|
395
400
|
isReplacingItems = true
|
|
396
401
|
print("RmxAudioPlayer[setTracks] replacing tracks ")
|
|
@@ -662,13 +667,11 @@ final class RmxAudioPlayer: NSObject {
|
|
|
662
667
|
updatedNowPlayingInfo![MPMediaItemPropertyTitle] = currentItem?.title
|
|
663
668
|
updatedNowPlayingInfo![MPMediaItemPropertyAlbumTitle] = currentItem?.album
|
|
664
669
|
|
|
665
|
-
|
|
666
|
-
updatedNowPlayingInfo![MPMediaItemPropertyArtwork] = mediaItemArtwork
|
|
667
|
-
}
|
|
670
|
+
updateNowPlayingArtwork(currentItem?.albumArt?.absoluteString)
|
|
668
671
|
}
|
|
669
672
|
updatedNowPlayingInfo![MPMediaItemPropertyPlaybackDuration] = duration ?? 0.0
|
|
670
673
|
updatedNowPlayingInfo![MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime ?? 0.0
|
|
671
|
-
updatedNowPlayingInfo![MPNowPlayingInfoPropertyPlaybackRate] =
|
|
674
|
+
updatedNowPlayingInfo![MPNowPlayingInfoPropertyPlaybackRate] = avQueuePlayer.rate != 0.0 ? avQueuePlayer.rate : 0.0
|
|
672
675
|
|
|
673
676
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = updatedNowPlayingInfo
|
|
674
677
|
}
|
|
@@ -678,24 +681,47 @@ final class RmxAudioPlayer: NSObject {
|
|
|
678
681
|
commandCenter.previousTrackCommand.isEnabled = !avQueuePlayer.isAtBeginning
|
|
679
682
|
}
|
|
680
683
|
|
|
681
|
-
func
|
|
684
|
+
func updateNowPlayingArtwork(_ coverUriOrNil: String?) {
|
|
682
685
|
guard let coverUri = coverUriOrNil else {
|
|
683
|
-
|
|
686
|
+
updatedNowPlayingInfo?.removeValue(forKey: MPMediaItemPropertyArtwork)
|
|
687
|
+
return
|
|
684
688
|
}
|
|
685
|
-
var coverImage: UIImage? = nil
|
|
686
|
-
if coverUri.hasPrefix("http://") || coverUri.hasPrefix("https://") {
|
|
687
|
-
let coverImageUrl = URL(string: coverUri)!
|
|
688
689
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
print("Error creating the coverImageData");
|
|
690
|
+
if coverUri.hasPrefix("http://") || coverUri.hasPrefix("https://") {
|
|
691
|
+
guard let coverImageUrl = URL(string: coverUri) else {
|
|
692
|
+
updatedNowPlayingInfo?.removeValue(forKey: MPMediaItemPropertyArtwork)
|
|
693
|
+
return
|
|
694
694
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
695
|
+
downloadImage(url: coverImageUrl) { [weak self] image in
|
|
696
|
+
guard
|
|
697
|
+
let self = self,
|
|
698
|
+
self.isCoverImageValid(image)
|
|
699
|
+
else {
|
|
700
|
+
return
|
|
701
|
+
}
|
|
702
|
+
let artwork = MPMediaItemArtwork(boundsSize: image!.size) { _ in image! }
|
|
703
|
+
self.nowPlayingInfoQueue.sync {
|
|
704
|
+
self.updatedNowPlayingInfo?[MPMediaItemPropertyArtwork] = artwork
|
|
705
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = self.updatedNowPlayingInfo
|
|
706
|
+
}
|
|
698
707
|
}
|
|
708
|
+
return
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if let mediaItemArtwork = createCoverArtwork(coverUri) {
|
|
712
|
+
updatedNowPlayingInfo?[MPMediaItemPropertyArtwork] = mediaItemArtwork
|
|
713
|
+
} else {
|
|
714
|
+
updatedNowPlayingInfo?.removeValue(forKey: MPMediaItemPropertyArtwork)
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
func createCoverArtwork(_ coverUriOrNil: String?) -> MPMediaItemArtwork? {
|
|
719
|
+
guard let coverUri = coverUriOrNil else {
|
|
720
|
+
return nil
|
|
721
|
+
}
|
|
722
|
+
var coverImage: UIImage? = nil
|
|
723
|
+
if FileManager.default.fileExists(atPath: coverUri) {
|
|
724
|
+
coverImage = UIImage(contentsOfFile: coverUri)
|
|
699
725
|
}
|
|
700
726
|
|
|
701
727
|
if isCoverImageValid(coverImage) {
|
|
@@ -1033,10 +1059,19 @@ final class RmxAudioPlayer: NSObject {
|
|
|
1033
1059
|
}
|
|
1034
1060
|
|
|
1035
1061
|
func addTrackObservers(_ playerItem: AudioTrack?) {
|
|
1062
|
+
guard let playerItem = playerItem else {
|
|
1063
|
+
return
|
|
1064
|
+
}
|
|
1065
|
+
let trackId = ObjectIdentifier(playerItem)
|
|
1066
|
+
guard !observedTrackItems.contains(trackId) else {
|
|
1067
|
+
return
|
|
1068
|
+
}
|
|
1069
|
+
observedTrackItems.insert(trackId)
|
|
1070
|
+
|
|
1036
1071
|
let options: NSKeyValueObservingOptions = [.old, .new]
|
|
1037
|
-
playerItem
|
|
1038
|
-
playerItem
|
|
1039
|
-
playerItem
|
|
1072
|
+
playerItem.addObserver(self, forKeyPath: "status", options: options, context: nil)
|
|
1073
|
+
playerItem.addObserver(self, forKeyPath: "duration", options: options, context: nil)
|
|
1074
|
+
playerItem.addObserver(self, forKeyPath: "loadedTimeRanges", options: options, context: nil)
|
|
1040
1075
|
|
|
1041
1076
|
// We don't need this one because we get the currentItem notification from the queue.
|
|
1042
1077
|
// But we will wire it up anyway...
|
|
@@ -1045,7 +1080,7 @@ final class RmxAudioPlayer: NSObject {
|
|
|
1045
1080
|
// Subscribe to the AVPlayerItem's PlaybackStalledNotification notification.
|
|
1046
1081
|
listener.addObserver(self, selector: #selector(itemStalledPlaying(_:)), name: .AVPlayerItemPlaybackStalled, object: playerItem)
|
|
1047
1082
|
|
|
1048
|
-
onStatus(.rmxstatus_ITEM_ADDED, trackId: playerItem
|
|
1083
|
+
onStatus(.rmxstatus_ITEM_ADDED, trackId: playerItem.trackId, param: playerItem.toDict())
|
|
1049
1084
|
}
|
|
1050
1085
|
|
|
1051
1086
|
@objc func queueCleared(_ notification: Notification?) {
|
|
@@ -1055,22 +1090,52 @@ final class RmxAudioPlayer: NSObject {
|
|
|
1055
1090
|
}
|
|
1056
1091
|
|
|
1057
1092
|
func removeTrackObservers(_ playerItem: AudioTrack?) {
|
|
1058
|
-
|
|
1093
|
+
guard let playerItem = playerItem else {
|
|
1094
|
+
return
|
|
1095
|
+
}
|
|
1096
|
+
let trackId = ObjectIdentifier(playerItem)
|
|
1097
|
+
guard observedTrackItems.remove(trackId) != nil else {
|
|
1098
|
+
return
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
playerItem.removeObserver(self, forKeyPath: "status")
|
|
1102
|
+
playerItem.removeObserver(self, forKeyPath: "duration")
|
|
1103
|
+
playerItem.removeObserver(self, forKeyPath: "loadedTimeRanges")
|
|
1104
|
+
|
|
1105
|
+
let listener = NotificationCenter.default
|
|
1106
|
+
listener.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: playerItem)
|
|
1107
|
+
listener.removeObserver(self, name: .AVPlayerItemPlaybackStalled, object: playerItem)
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
func registerQueueObservers() {
|
|
1111
|
+
guard !queueObserversRegistered else {
|
|
1112
|
+
return
|
|
1113
|
+
}
|
|
1114
|
+
avQueuePlayer.addObserver(self, forKeyPath: "currentItem", options: .new, context: nil)
|
|
1115
|
+
avQueuePlayer.addObserver(self, forKeyPath: "rate", options: .new, context: nil)
|
|
1116
|
+
avQueuePlayer.addObserver(self, forKeyPath: "timeControlStatus", options: .new, context: nil)
|
|
1117
|
+
queueObserversRegistered = true
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
func unregisterQueueObservers() {
|
|
1121
|
+
guard queueObserversRegistered else {
|
|
1122
|
+
return
|
|
1123
|
+
}
|
|
1124
|
+
avQueuePlayer.removeObserver(self, forKeyPath: "currentItem")
|
|
1125
|
+
avQueuePlayer.removeObserver(self, forKeyPath: "rate")
|
|
1126
|
+
avQueuePlayer.removeObserver(self, forKeyPath: "timeControlStatus")
|
|
1127
|
+
queueObserversRegistered = false
|
|
1059
1128
|
}
|
|
1060
1129
|
|
|
1061
1130
|
func activateAudioSession() {
|
|
1062
1131
|
let avSession = AVAudioSession.sharedInstance()
|
|
1063
1132
|
|
|
1064
|
-
|
|
1065
|
-
var options: AVAudioSession.CategoryOptions = .defaultToSpeaker
|
|
1066
|
-
|
|
1067
|
-
// If both Bluetooth streaming options are enabled, the low quality stream is preferred; enable A2DP only.
|
|
1068
|
-
options.insert(.allowBluetoothA2DP)
|
|
1133
|
+
let options: AVAudioSession.CategoryOptions = [.allowBluetoothA2DP]
|
|
1069
1134
|
|
|
1070
1135
|
do {
|
|
1071
1136
|
// Always set category first, even if session is already active
|
|
1072
1137
|
// This ensures we have the correct category after video player exits
|
|
1073
|
-
try avSession.setCategory(.
|
|
1138
|
+
try avSession.setCategory(.playback, options: options)
|
|
1074
1139
|
} catch {
|
|
1075
1140
|
print("Error setting category! \(error.localizedDescription)")
|
|
1076
1141
|
}
|
|
@@ -1140,6 +1205,7 @@ final class RmxAudioPlayer: NSObject {
|
|
|
1140
1205
|
if let playbackTimeObserver = playbackTimeObserver {
|
|
1141
1206
|
avQueuePlayer.removeTimeObserver(playbackTimeObserver)
|
|
1142
1207
|
}
|
|
1208
|
+
unregisterQueueObservers()
|
|
1143
1209
|
deregisterMusicControlsEventListener()
|
|
1144
1210
|
|
|
1145
1211
|
removeAllTracks()
|