capacitor-plugin-playlist 0.10.3 → 0.10.9
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/README.md +1133 -137
- package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +9 -5
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +20 -5
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandler.java +10 -11
- package/dist/docs.json +66 -33
- package/dist/esm/RmxAudioPlayer.js +11 -14
- package/dist/esm/RmxAudioPlayer.js.map +1 -1
- package/dist/esm/definitions.d.ts +119 -7
- package/dist/esm/web.d.ts +3 -1
- package/dist/esm/web.js +27 -39
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +39 -54
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +39 -54
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/PlaylistPlugin/Plugin.swift +7 -3
- package/ios/Sources/PlaylistPlugin/RmxAudioPlayer.swift +117 -55
- package/package.json +1 -1
|
@@ -52,6 +52,7 @@ final class RmxAudioPlayer: NSObject {
|
|
|
52
52
|
private var resetStreamOnPause = false
|
|
53
53
|
private var updatedNowPlayingInfo: [String : Any]?
|
|
54
54
|
private let nowPlayingInfoQueue = DispatchQueue(label: "RMXAudioPlayerNowPlayingQueue")
|
|
55
|
+
private let coverArtworkCache = NSCache<NSURL, MPMediaItemArtwork>()
|
|
55
56
|
private var isReplacingItems = false
|
|
56
57
|
private var isWaitingToStartPlayback = false
|
|
57
58
|
private var loop = false
|
|
@@ -161,16 +162,17 @@ final class RmxAudioPlayer: NSObject {
|
|
|
161
162
|
var removed = 0
|
|
162
163
|
if items.count > 0 {
|
|
163
164
|
for item in items {
|
|
164
|
-
guard let item = item as? [String:
|
|
165
|
+
guard let item = item as? [String: Any] else {
|
|
165
166
|
continue
|
|
166
167
|
}
|
|
167
|
-
|
|
168
|
+
|
|
169
|
+
if let id = item["id"] as? String {
|
|
168
170
|
do {
|
|
169
171
|
try removeItem(id)
|
|
170
172
|
removed += 1
|
|
171
173
|
} catch {}
|
|
172
174
|
}
|
|
173
|
-
else if let index =
|
|
175
|
+
else if let index = (item["index"] as? NSNumber)?.intValue {
|
|
174
176
|
do {
|
|
175
177
|
try removeItem(index)
|
|
176
178
|
removed += 1
|
|
@@ -305,22 +307,9 @@ final class RmxAudioPlayer: NSObject {
|
|
|
305
307
|
// Re-arm the periodic observer if it was removed by a prior releaseResources() call.
|
|
306
308
|
installPlaybackTimeObserverIfNeeded()
|
|
307
309
|
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
if !audioSession.isOtherAudioPlaying {
|
|
312
|
-
// Only reactivate if no other audio is playing
|
|
313
|
-
do {
|
|
314
|
-
try audioSession.setActive(true)
|
|
315
|
-
} catch {
|
|
316
|
-
print("Warning: Could not activate audio session: \(error.localizedDescription)")
|
|
317
|
-
// Try to reactivate with category setup
|
|
318
|
-
activateAudioSession()
|
|
319
|
-
}
|
|
320
|
-
} else {
|
|
321
|
-
// If other audio is playing, ensure our category is set correctly
|
|
322
|
-
activateAudioSession()
|
|
323
|
-
}
|
|
310
|
+
// Always re-activate after native video handoff — `isOtherAudioPlaying` can still be true
|
|
311
|
+
// briefly while AVPlayerViewController tears down, which previously skipped setActive.
|
|
312
|
+
activateAudioSession()
|
|
324
313
|
|
|
325
314
|
if resetStreamOnPause,
|
|
326
315
|
let currentTrack = avQueuePlayer.currentAudioTrack,
|
|
@@ -714,9 +703,7 @@ final class RmxAudioPlayer: NSObject {
|
|
|
714
703
|
updatedNowPlayingInfo![MPMediaItemPropertyTitle] = currentItem?.title
|
|
715
704
|
updatedNowPlayingInfo![MPMediaItemPropertyAlbumTitle] = currentItem?.album
|
|
716
705
|
|
|
717
|
-
|
|
718
|
-
updatedNowPlayingInfo![MPMediaItemPropertyArtwork] = mediaItemArtwork
|
|
719
|
-
}
|
|
706
|
+
updateNowPlayingArtwork(currentItem?.albumArt?.absoluteString)
|
|
720
707
|
}
|
|
721
708
|
updatedNowPlayingInfo![MPMediaItemPropertyPlaybackDuration] = duration ?? 0.0
|
|
722
709
|
updatedNowPlayingInfo![MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime ?? 0.0
|
|
@@ -730,52 +717,76 @@ final class RmxAudioPlayer: NSObject {
|
|
|
730
717
|
commandCenter.previousTrackCommand.isEnabled = !avQueuePlayer.isAtBeginning
|
|
731
718
|
}
|
|
732
719
|
|
|
733
|
-
func
|
|
720
|
+
private func updateNowPlayingArtwork(_ coverUriOrNil: String?) {
|
|
734
721
|
guard let coverUri = coverUriOrNil else {
|
|
735
|
-
|
|
722
|
+
updatedNowPlayingInfo?.removeValue(forKey: MPMediaItemPropertyArtwork)
|
|
723
|
+
return
|
|
736
724
|
}
|
|
737
|
-
var coverImage: UIImage? = nil
|
|
738
|
-
if coverUri.hasPrefix("http://") || coverUri.hasPrefix("https://") {
|
|
739
|
-
let coverImageUrl = URL(string: coverUri)!
|
|
740
725
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
print("Error creating the coverImageData");
|
|
726
|
+
if coverUri.hasPrefix("http://") || coverUri.hasPrefix("https://") {
|
|
727
|
+
guard let coverImageUrl = URL(string: coverUri) else {
|
|
728
|
+
updatedNowPlayingInfo?.removeValue(forKey: MPMediaItemPropertyArtwork)
|
|
729
|
+
return
|
|
746
730
|
}
|
|
747
|
-
|
|
748
|
-
if
|
|
749
|
-
|
|
731
|
+
|
|
732
|
+
if let cachedArtwork = coverArtworkCache.object(forKey: coverImageUrl as NSURL) {
|
|
733
|
+
updatedNowPlayingInfo?[MPMediaItemPropertyArtwork] = cachedArtwork
|
|
734
|
+
return
|
|
750
735
|
}
|
|
736
|
+
|
|
737
|
+
updatedNowPlayingInfo?.removeValue(forKey: MPMediaItemPropertyArtwork)
|
|
738
|
+
|
|
739
|
+
URLSession.shared.dataTask(with: coverImageUrl) { [weak self] data, response, error in
|
|
740
|
+
guard
|
|
741
|
+
error == nil,
|
|
742
|
+
let httpResponse = response as? HTTPURLResponse,
|
|
743
|
+
(200..<300).contains(httpResponse.statusCode),
|
|
744
|
+
let self = self,
|
|
745
|
+
let data = data,
|
|
746
|
+
let coverImage = UIImage(data: data),
|
|
747
|
+
self.isCoverImageValid(coverImage)
|
|
748
|
+
else {
|
|
749
|
+
return
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
DispatchQueue.main.async {
|
|
753
|
+
let artwork = MPMediaItemArtwork(boundsSize: coverImage.size) { _ in coverImage }
|
|
754
|
+
self.coverArtworkCache.setObject(artwork, forKey: coverImageUrl as NSURL)
|
|
755
|
+
|
|
756
|
+
guard self.avQueuePlayer.currentAudioTrack?.albumArt?.absoluteString == coverUri else {
|
|
757
|
+
return
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
self.nowPlayingInfoQueue.sync {
|
|
761
|
+
self.updatedNowPlayingInfo?[MPMediaItemPropertyArtwork] = artwork
|
|
762
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = self.updatedNowPlayingInfo
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}.resume()
|
|
766
|
+
return
|
|
751
767
|
}
|
|
752
768
|
|
|
753
|
-
if
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
769
|
+
if let mediaItemArtwork = createCoverArtwork(coverUri) {
|
|
770
|
+
updatedNowPlayingInfo?[MPMediaItemPropertyArtwork] = mediaItemArtwork
|
|
771
|
+
} else {
|
|
772
|
+
updatedNowPlayingInfo?.removeValue(forKey: MPMediaItemPropertyArtwork)
|
|
757
773
|
}
|
|
758
|
-
return nil;
|
|
759
774
|
}
|
|
760
775
|
|
|
761
|
-
func
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
776
|
+
private func createCoverArtwork(_ coverUri: String) -> MPMediaItemArtwork? {
|
|
777
|
+
guard
|
|
778
|
+
FileManager.default.fileExists(atPath: coverUri),
|
|
779
|
+
let coverImage = UIImage(contentsOfFile: coverUri),
|
|
780
|
+
isCoverImageValid(coverImage)
|
|
781
|
+
else {
|
|
782
|
+
return nil
|
|
768
783
|
}
|
|
769
|
-
}
|
|
770
784
|
|
|
771
|
-
|
|
772
|
-
URLSession.shared.dataTask(with: url) { (data, response, error) in
|
|
773
|
-
completion(data)
|
|
774
|
-
}.resume()
|
|
785
|
+
return MPMediaItemArtwork(boundsSize: coverImage.size) { _ in coverImage }
|
|
775
786
|
}
|
|
776
787
|
|
|
777
|
-
func isCoverImageValid(_ coverImage: UIImage
|
|
778
|
-
return coverImage
|
|
788
|
+
private func isCoverImageValid(_ coverImage: UIImage) -> Bool {
|
|
789
|
+
return coverImage.ciImage != nil || coverImage.cgImage != nil
|
|
779
790
|
}
|
|
780
791
|
|
|
781
792
|
func handleCurrentItemChanged(_ playerItem: AudioTrack?) {
|
|
@@ -1218,6 +1229,8 @@ final class RmxAudioPlayer: NSObject {
|
|
|
1218
1229
|
// MARK: - Epic 45 video handoff
|
|
1219
1230
|
|
|
1220
1231
|
private var lastKnownHandoffPosition: Float = 0
|
|
1232
|
+
/// Track id at video open — AVQueuePlayer can advance while video owns the session.
|
|
1233
|
+
private var handoffPinnedTrackId: String?
|
|
1221
1234
|
|
|
1222
1235
|
func prepareForVideoHandoff() {
|
|
1223
1236
|
pauseCommand(false)
|
|
@@ -1225,9 +1238,14 @@ final class RmxAudioPlayer: NSObject {
|
|
|
1225
1238
|
// true stopped head, not a value that may have ticked during the pause call.
|
|
1226
1239
|
if let track = avQueuePlayer.currentAudioTrack {
|
|
1227
1240
|
lastKnownHandoffPosition = getTrackCurrentTime(track)
|
|
1241
|
+
handoffPinnedTrackId = track.trackId
|
|
1228
1242
|
} else {
|
|
1229
1243
|
lastKnownHandoffPosition = 0
|
|
1244
|
+
handoffPinnedTrackId = nil
|
|
1230
1245
|
}
|
|
1246
|
+
// Freeze queue: HLS item failure while paused still triggers .advance otherwise.
|
|
1247
|
+
avQueuePlayer.actionAtItemEnd = .none
|
|
1248
|
+
print("prepareForVideoHandoff: pinned=\(handoffPinnedTrackId ?? "nil") actionAtItemEnd=none")
|
|
1231
1249
|
do {
|
|
1232
1250
|
try AVAudioSession.sharedInstance().setActive(false, options: [.notifyOthersOnDeactivation])
|
|
1233
1251
|
} catch {
|
|
@@ -1235,17 +1253,61 @@ final class RmxAudioPlayer: NSObject {
|
|
|
1235
1253
|
}
|
|
1236
1254
|
}
|
|
1237
1255
|
|
|
1238
|
-
|
|
1256
|
+
/// Completes with `true` when native handled seek (and play when requested) so JS can skip redundant seek/play.
|
|
1257
|
+
func resumeAfterVideoHandoff(
|
|
1258
|
+
position: Float,
|
|
1259
|
+
prewarm: Bool = false,
|
|
1260
|
+
play: Bool = false,
|
|
1261
|
+
completion: @escaping (Bool) -> Void
|
|
1262
|
+
) {
|
|
1239
1263
|
lastKnownHandoffPosition = position
|
|
1240
1264
|
if prewarm {
|
|
1265
|
+
completion(false)
|
|
1241
1266
|
return
|
|
1242
1267
|
}
|
|
1268
|
+
avQueuePlayer.actionAtItemEnd = .advance
|
|
1269
|
+
let currentId = avQueuePlayer.currentAudioTrack?.trackId
|
|
1270
|
+
if let pinned = handoffPinnedTrackId, !pinned.isEmpty, currentId != pinned {
|
|
1271
|
+
print("resumeAfterVideoHandoff: restoring pinned=\(pinned) (was \(currentId ?? "nil"))")
|
|
1272
|
+
do {
|
|
1273
|
+
try selectTrack(id: pinned)
|
|
1274
|
+
} catch {
|
|
1275
|
+
print("resumeAfterVideoHandoff: selectTrack failed: \(error.localizedDescription)")
|
|
1276
|
+
}
|
|
1277
|
+
} else {
|
|
1278
|
+
print("resumeAfterVideoHandoff: pinned=\(handoffPinnedTrackId ?? "nil") current=\(currentId ?? "nil")")
|
|
1279
|
+
}
|
|
1280
|
+
handoffPinnedTrackId = nil
|
|
1281
|
+
// Always re-arm session after native video (AVPlayer teardown can briefly look like other audio).
|
|
1243
1282
|
activateAudioSession()
|
|
1244
1283
|
// Reset lastTrackId so the timeControlStatus KVO guard does not suppress the PLAYING
|
|
1245
1284
|
// event on same-track non-index-0 resume. The guard `lastTrackId != trackId || isAtBeginning`
|
|
1246
1285
|
// (where isAtBeginning = currentIndex() == 0) would silently drop the PLAYING transition
|
|
1247
1286
|
// for any audio track at playlist index > 0, leaving JS stuck in PAUSED.
|
|
1248
1287
|
lastTrackId = nil
|
|
1288
|
+
|
|
1289
|
+
let finish: () -> Void = { [weak self] in
|
|
1290
|
+
guard let self = self else {
|
|
1291
|
+
completion(true)
|
|
1292
|
+
return
|
|
1293
|
+
}
|
|
1294
|
+
if play {
|
|
1295
|
+
self.playCommand(false)
|
|
1296
|
+
NSLog("[Playlist] resumeAfterVideoHandoff: seek-then-play at %.3f", position)
|
|
1297
|
+
} else {
|
|
1298
|
+
NSLog("[Playlist] resumeAfterVideoHandoff: seek-only at %.3f", position)
|
|
1299
|
+
}
|
|
1300
|
+
completion(true)
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
if position > 0 {
|
|
1304
|
+
let seekToTime = CMTimeMakeWithSeconds(Float64(position), preferredTimescale: 1000)
|
|
1305
|
+
avQueuePlayer.seek(to: seekToTime, toleranceBefore: .zero, toleranceAfter: .zero) { _ in
|
|
1306
|
+
finish()
|
|
1307
|
+
}
|
|
1308
|
+
} else {
|
|
1309
|
+
finish()
|
|
1310
|
+
}
|
|
1249
1311
|
}
|
|
1250
1312
|
|
|
1251
1313
|
func getLastKnownPosition() -> Float {
|