capacitor-plugin-playlist 0.9.4 → 0.10.3

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.
Files changed (69) hide show
  1. package/CapacitorPluginPlaylist.podspec +1 -1
  2. package/Package.swift +31 -0
  3. package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +19 -15
  4. package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +50 -10
  5. package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistManager.kt +4 -0
  6. package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandler.java +117 -0
  7. package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaService.kt +19 -0
  8. package/dist/docs.json +7 -0
  9. package/dist/esm/RmxAudioPlayer.d.ts +1 -1
  10. package/dist/esm/definitions.d.ts +2 -0
  11. package/ios/{Plugin → Sources/PlaylistPlugin}/DispatchQueue.swift +3 -3
  12. package/ios/{Plugin → Sources/PlaylistPlugin}/Plugin.swift +32 -2
  13. package/ios/{Plugin → Sources/PlaylistPlugin}/RmxAudioPlayer.swift +4 -1
  14. package/ios/{PluginTests → Tests/PlaylistPluginTests}/PluginTests.swift +1 -1
  15. package/package.json +4 -4
  16. package/ios/Plugin/Info.plist +0 -24
  17. package/ios/Plugin/Plugin.h +0 -10
  18. package/ios/Plugin/Plugin.m +0 -29
  19. package/ios/Plugin.xcodeproj/project.pbxproj +0 -574
  20. package/ios/Plugin.xcworkspace/contents.xcworkspacedata +0 -10
  21. package/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  22. package/ios/PluginTests/Info.plist +0 -22
  23. package/ios/Podfile +0 -26
  24. package/ios/Podfile.lock +0 -22
  25. package/ios/Pods/Local Podspecs/Capacitor.podspec.json +0 -32
  26. package/ios/Pods/Local Podspecs/CapacitorCordova.podspec.json +0 -31
  27. package/ios/Pods/Manifest.lock +0 -22
  28. package/ios/Pods/Pods.xcodeproj/project.pbxproj +0 -1641
  29. package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/Capacitor-Capacitor.xcscheme +0 -58
  30. package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/Capacitor.xcscheme +0 -58
  31. package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/CapacitorCordova-CapacitorCordova.xcscheme +0 -58
  32. package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/CapacitorCordova.xcscheme +0 -58
  33. package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/Pods-Plugin.xcscheme +0 -58
  34. package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/Pods-PluginTests.xcscheme +0 -58
  35. package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/xcschememanagement.plist +0 -41
  36. package/ios/Pods/Target Support Files/Capacitor/Capacitor-Info.plist +0 -26
  37. package/ios/Pods/Target Support Files/Capacitor/Capacitor-dummy.m +0 -5
  38. package/ios/Pods/Target Support Files/Capacitor/Capacitor-prefix.pch +0 -12
  39. package/ios/Pods/Target Support Files/Capacitor/Capacitor.debug.xcconfig +0 -16
  40. package/ios/Pods/Target Support Files/Capacitor/Capacitor.modulemap +0 -8
  41. package/ios/Pods/Target Support Files/Capacitor/Capacitor.release.xcconfig +0 -16
  42. package/ios/Pods/Target Support Files/Capacitor/ResourceBundle-Capacitor-Capacitor-Info.plist +0 -24
  43. package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova-Info.plist +0 -26
  44. package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova-dummy.m +0 -5
  45. package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova-prefix.pch +0 -12
  46. package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova.debug.xcconfig +0 -13
  47. package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova.modulemap +0 -6
  48. package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova.release.xcconfig +0 -13
  49. package/ios/Pods/Target Support Files/CapacitorCordova/ResourceBundle-CapacitorCordova-CapacitorCordova-Info.plist +0 -24
  50. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-Info.plist +0 -26
  51. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-acknowledgements.markdown +0 -53
  52. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-acknowledgements.plist +0 -91
  53. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-dummy.m +0 -5
  54. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-umbrella.h +0 -16
  55. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin.debug.xcconfig +0 -15
  56. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin.modulemap +0 -6
  57. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin.release.xcconfig +0 -15
  58. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-Info.plist +0 -26
  59. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-acknowledgements.markdown +0 -53
  60. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-acknowledgements.plist +0 -91
  61. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-dummy.m +0 -5
  62. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh +0 -188
  63. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-umbrella.h +0 -16
  64. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.debug.xcconfig +0 -16
  65. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.modulemap +0 -6
  66. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.release.xcconfig +0 -16
  67. /package/ios/{Plugin → Sources/PlaylistPlugin}/AVBidirectionalQueuePlayer.swift +0 -0
  68. /package/ios/{Plugin → Sources/PlaylistPlugin}/AudioTrack.swift +0 -0
  69. /package/ios/{Plugin → Sources/PlaylistPlugin}/Constants.swift +0 -0
@@ -10,7 +10,7 @@ Pod::Spec.new do |s|
10
10
  s.homepage = package['repository']['url']
11
11
  s.author = package['author']
12
12
  s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
13
- s.source_files = 'ios/Plugin/**/*.{swift,h,m,c,cc,mm,cpp}'
13
+ s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
14
14
  s.ios.deployment_target = '18.0'
15
15
  s.dependency 'Capacitor'
16
16
  s.swift_version = '5.3'
package/Package.swift ADDED
@@ -0,0 +1,31 @@
1
+ // swift-tools-version: 6.0
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "CapacitorPluginPlaylist",
6
+ platforms: [.iOS(.v18)],
7
+ products: [
8
+ .library(
9
+ name: "CapacitorPluginPlaylist",
10
+ targets: ["PlaylistPlugin"])
11
+ ],
12
+ dependencies: [
13
+ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
14
+ ],
15
+ targets: [
16
+ .target(
17
+ name: "PlaylistPlugin",
18
+ dependencies: [
19
+ .product(name: "Capacitor", package: "capacitor-swift-pm"),
20
+ .product(name: "Cordova", package: "capacitor-swift-pm")
21
+ ],
22
+ path: "ios/Sources/PlaylistPlugin",
23
+ swiftSettings: [
24
+ .swiftLanguageMode(.v5),
25
+ ]),
26
+ .testTarget(
27
+ name: "PlaylistPluginTests",
28
+ dependencies: ["PlaylistPlugin"],
29
+ path: "ios/Tests/PlaylistPluginTests")
30
+ ]
31
+ )
@@ -7,6 +7,8 @@ import com.devbrackets.android.playlistcore.data.MediaProgress
7
7
  import com.getcapacitor.*
8
8
  import com.getcapacitor.annotation.CapacitorPlugin
9
9
  import org.dwbn.plugins.playlist.data.AudioTrack
10
+ import org.dwbn.plugins.playlist.playlist.AudioPlaylistHandler
11
+ import org.dwbn.plugins.playlist.service.MediaService
10
12
  import org.json.JSONArray
11
13
  import org.json.JSONObject
12
14
  import java.util.*
@@ -214,20 +216,18 @@ public class PlaylistPlugin : Plugin(), OnStatusReportListener {
214
216
  fun play(call: PluginCall) {
215
217
  Handler(Looper.getMainLooper()).post {
216
218
  val handler = audioPlayerImpl!!.playlistManager.playlistHandler
219
+ val serviceForeground = MediaService.instance?.isRunningInForeground() == true
217
220
  if (handler == null || handler.currentMediaPlayer == null) {
218
- // MediaPlayer was released or MediaService was killed (e.g. after a long video session
219
- // where audio focus was permanently abandoned). Fall back to beginPlayback which
220
- // re-starts the service, re-prepares the MediaPlayer, and re-acquires audio focus.
221
221
  val posMs = (audioPlayerImpl!!.getLastKnownPositionSec() * 1000f).toLong()
222
- audioPlayerImpl!!.playlistManager.beginPlayback(posMs, false)
223
- Log.i(TAG, "play: handler/mediaPlayer was null — re-armed via beginPlayback at ${posMs}ms")
224
- } else {
225
- // Handler and MediaPlayer are alive — use the lightweight resume path.
226
- // Guard against stacking up repeat cycles (playlistcore bug): skip if already playing.
227
- val isPlaying = handler.currentMediaPlayer?.isPlaying ?: false
228
- if (!isPlaying) {
229
- handler.play()
222
+ if (serviceForeground && handler is AudioPlaylistHandler<*, *>) {
223
+ handler.startItemPlayback(posMs, false)
224
+ Log.i(TAG, "play: re-armed via startItemPlayback at ${posMs}ms (FGS already foreground)")
225
+ } else {
226
+ audioPlayerImpl!!.playlistManager.beginPlayback(posMs, false)
227
+ Log.i(TAG, "play: handler/mediaPlayer was null — re-armed via beginPlayback at ${posMs}ms")
230
228
  }
229
+ } else {
230
+ handler.play()
231
231
  }
232
232
 
233
233
  call.resolve()
@@ -257,11 +257,14 @@ public class PlaylistPlugin : Plugin(), OnStatusReportListener {
257
257
  Handler(Looper.getMainLooper()).post {
258
258
  val id: String = call.getString("id")!!
259
259
  if ("" != id) {
260
- // alternatively we could search for the item and set the current index to that item.
261
260
  val code = id.hashCode()
262
261
  val seekPosition = (call.getFloat("position", 0f)!! * 1000.0f).toLong()
263
262
  audioPlayerImpl!!.playlistManager.setCurrentItem(code.toLong())
264
- audioPlayerImpl!!.playlistManager.beginPlayback(seekPosition, false)
263
+ val handler = audioPlayerImpl!!.playlistManager.playlistHandler
264
+ val alreadyPlaying = handler?.currentMediaPlayer?.isPlaying == true
265
+ if (!audioPlayerImpl!!.tryResumeVideoHandoffInPlace(seekPosition) && !alreadyPlaying) {
266
+ audioPlayerImpl!!.playlistManager.beginPlayback(seekPosition, false)
267
+ }
265
268
  }
266
269
 
267
270
  call.resolve()
@@ -406,9 +409,10 @@ public class PlaylistPlugin : Plugin(), OnStatusReportListener {
406
409
  fun resumeAfterVideoHandoff(call: PluginCall) {
407
410
  Handler(Looper.getMainLooper()).post {
408
411
  val position = call.getFloat("position", 0f)!!
409
- audioPlayerImpl!!.resumeAfterVideoHandoff(position)
412
+ val prewarm = call.getBoolean("prewarm", false) ?: false
413
+ audioPlayerImpl!!.resumeAfterVideoHandoff(position, prewarm)
410
414
  call.resolve()
411
- Log.i(TAG, "resumeAfterVideoHandoff")
415
+ Log.i(TAG, "resumeAfterVideoHandoff prewarm=$prewarm")
412
416
  }
413
417
  }
414
418
 
@@ -20,6 +20,8 @@ import org.dwbn.plugins.playlist.data.AudioTrack;
20
20
  import org.dwbn.plugins.playlist.manager.MediaControlsListener;
21
21
  import org.dwbn.plugins.playlist.manager.Options;
22
22
  import org.dwbn.plugins.playlist.manager.PlaylistManager;
23
+ import org.dwbn.plugins.playlist.playlist.AudioPlaylistHandler;
24
+ import org.dwbn.plugins.playlist.service.MediaService;
23
25
  import org.json.JSONException;
24
26
  import org.json.JSONObject;
25
27
 
@@ -460,25 +462,63 @@ public class RmxAudioPlayer implements PlaybackStatusListener<AudioTrack>,
460
462
  } else {
461
463
  lastKnownHandoffPositionSec = 0f;
462
464
  }
463
- // Pause unconditionally (not only when isPlaying) so that DefaultAudioFocusProvider
464
- // always abandons audio focus before CapacitorVideoPlayer.initPlayer acquires it.
465
- // Calling pause() on an already-paused handler is safe; it triggers focus release.
466
- if (playlistManager.getPlaylistHandler() != null) {
467
- playlistManager.getPlaylistHandler().pause(false);
465
+ com.devbrackets.android.playlistcore.components.playlisthandler.PlaylistHandler<?> handler =
466
+ playlistManager.getPlaylistHandler();
467
+ if (handler instanceof AudioPlaylistHandler) {
468
+ ((AudioPlaylistHandler<?, ?>) handler).pauseForVideoHandoff();
469
+ } else if (handler != null) {
470
+ handler.pause(false);
468
471
  }
469
472
  }
470
473
 
471
474
  public void resumeAfterVideoHandoff(float positionSec) {
475
+ resumeAfterVideoHandoff(positionSec, false);
476
+ }
477
+
478
+ public void resumeAfterVideoHandoff(float positionSec, boolean prewarm) {
472
479
  lastKnownHandoffPositionSec = positionSec;
473
- // Re-arm the audio session/service so the handler is primed before JS calls play().
474
- // On Android, prepareForVideoHandoff() abandoned audio focus which may have caused
475
- // the MediaService to stop itself (foreground service removal). beginPlayback with
476
- // startPaused=true restarts the service, re-acquires audio focus, and prepares the
477
- // MediaPlayer at the stored position — without auto-starting playback (JS owns FR111).
478
480
  long positionMs = (long) (positionSec * 1000f);
481
+ if (prewarm) {
482
+ playlistManager.setVideoHandoffForegroundRetain(true);
483
+ playlistManager.beginPlayback(positionMs, true);
484
+ return;
485
+ }
486
+ if (tryResumeVideoHandoffInPlace(positionMs)) {
487
+ return;
488
+ }
489
+ // Service not foreground — last resort (may be muted on Android 17 when backgrounded).
490
+ playlistManager.setVideoHandoffForegroundRetain(false);
479
491
  playlistManager.beginPlayback(positionMs, true);
480
492
  }
481
493
 
494
+ /**
495
+ * Resume audio at {@code positionMs} without restarting MediaService (Android 17 AudioHardening).
496
+ */
497
+ public boolean tryResumeVideoHandoffInPlace(long positionMs) {
498
+ if (!playlistManager.getVideoHandoffForegroundRetain()) {
499
+ return false;
500
+ }
501
+ MediaService service = MediaService.getInstance();
502
+ if (service == null || !service.isRunningInForeground()) {
503
+ return false;
504
+ }
505
+ com.devbrackets.android.playlistcore.components.playlisthandler.PlaylistHandler<?> handler =
506
+ playlistManager.getPlaylistHandler();
507
+ if (!(handler instanceof AudioPlaylistHandler)) {
508
+ return false;
509
+ }
510
+ AudioPlaylistHandler<?, ?> audioHandler = (AudioPlaylistHandler<?, ?>) handler;
511
+ com.devbrackets.android.playlistcore.api.MediaPlayerApi<?> mediaPlayer = audioHandler.getCurrentMediaPlayer();
512
+ if (mediaPlayer != null) {
513
+ audioHandler.resumePlaybackAfterVideoHandoff(positionMs);
514
+ } else {
515
+ // Re-prepare within the existing foreground service — do not call beginPlayback/startService.
516
+ audioHandler.startItemPlayback(positionMs, false);
517
+ }
518
+ playlistManager.setVideoHandoffForegroundRetain(false);
519
+ return true;
520
+ }
521
+
482
522
  public float getLastKnownPositionSec() {
483
523
  return lastKnownHandoffPositionSec;
484
524
  }
@@ -29,6 +29,10 @@ class PlaylistManager(application: Application) :
29
29
  var isShouldStopPlaylist = false
30
30
  var currentErrorTrack: AudioTrack? = null
31
31
 
32
+ /** When true, MediaService stays in foreground during native video (Android 17 AudioHardening). */
33
+ var videoHandoffForegroundRetain = false
34
+ var mediaServiceInForeground = false
35
+
32
36
  // Really need a way to propagate the settings through the app
33
37
  var resetStreamOnPause = true
34
38
  var options: Options
@@ -16,12 +16,14 @@ import com.devbrackets.android.playlistcore.components.mediacontrols.MediaContro
16
16
  import com.devbrackets.android.playlistcore.components.mediasession.DefaultMediaSessionProvider;
17
17
  import com.devbrackets.android.playlistcore.components.mediasession.MediaSessionProvider;
18
18
  import com.devbrackets.android.playlistcore.components.playlisthandler.DefaultPlaylistHandler;
19
+ import com.devbrackets.android.playlistcore.data.PlaybackState;
19
20
  import com.devbrackets.android.playlistcore.manager.BasePlaylistManager;
20
21
 
21
22
  import org.dwbn.plugins.playlist.RmxAudioPlayer;
22
23
  import org.dwbn.plugins.playlist.data.AudioTrack;
23
24
  import org.dwbn.plugins.playlist.manager.PlaylistManager;
24
25
  import org.dwbn.plugins.playlist.notification.PlaylistNotificationProvider;
26
+ import org.dwbn.plugins.playlist.service.MediaService;
25
27
  import org.jetbrains.annotations.NotNull;
26
28
 
27
29
 
@@ -65,11 +67,126 @@ public class AudioPlaylistHandler<I extends PlaylistItem, M extends BasePlaylist
65
67
  startItemPlayback(0, !this.isPlaying());
66
68
  }
67
69
 
70
+ /**
71
+ * Request audio focus before starting ExoMedia — DefaultPlaylistHandler.play() starts the
72
+ * MediaPlayer first, which fails silently after native video handoff (focus abandoned in
73
+ * pauseForVideoHandoff while the video ExoPlayer still held the stream).
74
+ * During Epic 45 prewarm (native video active), never request focus or start audible playback.
75
+ */
76
+ @Override
77
+ public void play() {
78
+ if (isVideoHandoffPrewarmActive()) {
79
+ setupForeground();
80
+ return;
81
+ }
82
+ getAudioFocusProvider().requestFocus();
83
+ com.devbrackets.android.playlistcore.api.MediaPlayerApi<I> mediaPlayer = getCurrentMediaPlayer();
84
+ if (mediaPlayer != null) {
85
+ if (mediaPlayer.isPlaying()) {
86
+ mediaPlayer.pause();
87
+ }
88
+ mediaPlayer.play();
89
+ }
90
+ getMediaProgressPoll().start();
91
+ setPlaybackState(PlaybackState.PLAYING);
92
+ setupForeground();
93
+ }
94
+
95
+ /**
96
+ * Resume at {@code positionMs} after native video ends. Re-requests focus and clears a stale
97
+ * isPlaying() state before {@link #play()}.
98
+ */
99
+ public void resumePlaybackAfterVideoHandoff(long positionMs) {
100
+ ((PlaylistManager) getPlaylistManager()).setVideoHandoffForegroundRetain(false);
101
+ getAudioFocusProvider().requestFocus();
102
+ com.devbrackets.android.playlistcore.api.MediaPlayerApi<I> mediaPlayer = getCurrentMediaPlayer();
103
+ if (mediaPlayer != null) {
104
+ if (positionMs > 0) {
105
+ seek(positionMs);
106
+ }
107
+ if (mediaPlayer.isPlaying()) {
108
+ mediaPlayer.pause();
109
+ }
110
+ }
111
+ play();
112
+ }
113
+
114
+ /**
115
+ * Release audio focus for video without tearing down the foreground service (Epic 45 / Android 17).
116
+ */
117
+ public void pauseForVideoHandoff() {
118
+ if (isPlaying()) {
119
+ com.devbrackets.android.playlistcore.api.MediaPlayerApi<I> mediaPlayer = getCurrentMediaPlayer();
120
+ if (mediaPlayer != null) {
121
+ mediaPlayer.pause();
122
+ }
123
+ }
124
+ getMediaProgressPoll().stop();
125
+ setPlaybackState(com.devbrackets.android.playlistcore.data.PlaybackState.PAUSED);
126
+ getAudioFocusProvider().abandonFocus();
127
+ }
128
+
129
+ @Override
130
+ public void pause(boolean isTemporary) {
131
+ if (((PlaylistManager) getPlaylistManager()).getVideoHandoffForegroundRetain()) {
132
+ if (isPlaying()) {
133
+ com.devbrackets.android.playlistcore.api.MediaPlayerApi<I> mediaPlayer = getCurrentMediaPlayer();
134
+ if (mediaPlayer != null) {
135
+ mediaPlayer.pause();
136
+ }
137
+ }
138
+ getMediaProgressPoll().stop();
139
+ setPlaybackState(com.devbrackets.android.playlistcore.data.PlaybackState.PAUSED);
140
+ if (!isTemporary) {
141
+ getAudioFocusProvider().abandonFocus();
142
+ }
143
+ return;
144
+ }
145
+ super.pause(isTemporary);
146
+ }
147
+
148
+ @Override
149
+ protected void relaxResources() {
150
+ if (((PlaylistManager) getPlaylistManager()).getVideoHandoffForegroundRetain()) {
151
+ return;
152
+ }
153
+ super.relaxResources();
154
+ }
155
+
156
+ /**
157
+ * Android 17 AudioHardening: re-calling startForeground from background revokes WIU and mutes playback.
158
+ * When prewarm already promoted MediaService, only update the notification via super when not foreground.
159
+ */
160
+ @Override
161
+ protected void setupForeground() {
162
+ MediaService service = MediaService.getInstance();
163
+ if (service != null && service.isRunningInForeground()) {
164
+ return;
165
+ }
166
+ super.setupForeground();
167
+ }
168
+
68
169
  @Override
69
170
  public void onPrepared(@NotNull MediaPlayerApi<I> mediaPlayer) {
171
+ if (isVideoHandoffPrewarmActive()) {
172
+ // Prewarm: prepare at seek position but stay silent — native video owns audio focus.
173
+ super.onPrepared(mediaPlayer);
174
+ com.devbrackets.android.playlistcore.api.MediaPlayerApi<I> currentPlayer = getCurrentMediaPlayer();
175
+ if (currentPlayer != null && currentPlayer.isPlaying()) {
176
+ currentPlayer.pause();
177
+ }
178
+ getMediaProgressPoll().stop();
179
+ setPlaybackState(com.devbrackets.android.playlistcore.data.PlaybackState.PAUSED);
180
+ getAudioFocusProvider().abandonFocus();
181
+ return;
182
+ }
70
183
  super.onPrepared(mediaPlayer);
71
184
  }
72
185
 
186
+ private boolean isVideoHandoffPrewarmActive() {
187
+ return ((PlaylistManager) getPlaylistManager()).getVideoHandoffForegroundRetain();
188
+ }
189
+
73
190
  @Override
74
191
  public boolean onError(@NotNull MediaPlayerApi<I> mediaPlayer) {
75
192
  ((PlaylistManager)getPlaylistManager()).setCurrentErrorTrack((AudioTrack) getCurrentPlaylistItem());
@@ -22,9 +22,14 @@ import org.dwbn.plugins.playlist.service.MediaImageProvider.OnImageUpdatedListen
22
22
  class MediaService : BasePlaylistService<AudioTrack, PlaylistManager>() {
23
23
  companion object {
24
24
  private const val TAG = "MediaService"
25
+
26
+ @JvmStatic
27
+ @Volatile
28
+ var instance: MediaService? = null
25
29
  }
26
30
 
27
31
  override fun onCreate() {
32
+ instance = this
28
33
  super.onCreate()
29
34
  // Adds the audio player implementation, otherwise there's nothing to play media with
30
35
  val newAudio = AudioApi(applicationContext)
@@ -33,6 +38,7 @@ class MediaService : BasePlaylistService<AudioTrack, PlaylistManager>() {
33
38
  }
34
39
 
35
40
  override fun onDestroy() {
41
+ instance = null
36
42
  super.onDestroy()
37
43
 
38
44
  // Releases and clears all the MediaPlayersMediaImageProvider
@@ -57,6 +63,7 @@ class MediaService : BasePlaylistService<AudioTrack, PlaylistManager>() {
57
63
 
58
64
  override fun runAsForeground(notificationId: Int, notification: Notification) {
59
65
  if (inForeground) {
66
+ playlistManager.mediaServiceInForeground = true
60
67
  return
61
68
  }
62
69
 
@@ -68,15 +75,27 @@ class MediaService : BasePlaylistService<AudioTrack, PlaylistManager>() {
68
75
  startForeground(notificationId, notification,
69
76
  ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
70
77
  }
78
+ playlistManager.mediaServiceInForeground = true
71
79
  } catch (e: android.app.ForegroundServiceStartNotAllowedException) {
72
80
  // Android 12+ blocks foreground service start from background
73
81
  // Log the error but don't crash - service can still function without foreground status
74
82
  Log.w(TAG, "Cannot start foreground service: app is in background", e)
75
83
  inForeground = false
84
+ playlistManager.mediaServiceInForeground = false
76
85
  // Service will continue as a regular service (may be killed by system)
77
86
  }
78
87
  }
79
88
 
89
+ override fun endForeground(removeNotification: Boolean) {
90
+ if (playlistManager.videoHandoffForegroundRetain) {
91
+ return
92
+ }
93
+ super.endForeground(removeNotification)
94
+ playlistManager.mediaServiceInForeground = false
95
+ }
96
+
97
+ fun isRunningInForeground(): Boolean = inForeground
98
+
80
99
  override val playlistManager: PlaylistManager
81
100
  get() = (applicationContext as App).playlistManager
82
101
 
package/dist/docs.json CHANGED
@@ -1916,6 +1916,13 @@
1916
1916
  "docs": "",
1917
1917
  "complexTypes": [],
1918
1918
  "type": "number"
1919
+ },
1920
+ {
1921
+ "name": "prewarm",
1922
+ "tags": [],
1923
+ "docs": "Android: keep MediaService FGS alive (with WIU) while native video plays.",
1924
+ "complexTypes": [],
1925
+ "type": "boolean | undefined"
1919
1926
  }
1920
1927
  ]
1921
1928
  },
@@ -15,7 +15,7 @@ export declare class RmxAudioPlayer {
15
15
  * because they properly interpret the range of these values, but this field is exposed if you wish to observe
16
16
  * or interrogate it.
17
17
  */
18
- get currentState(): "error" | "stopped" | "unknown" | "paused" | "loading" | "playing" | "ready";
18
+ get currentState(): "unknown" | "ready" | "error" | "playing" | "loading" | "paused" | "stopped";
19
19
  get currentTrack(): AudioTrack | null;
20
20
  /**
21
21
  * If the playlist is currently playling a track.
@@ -43,6 +43,8 @@ export interface PlaylistPlugin {
43
43
  }
44
44
  export interface ResumeAfterVideoHandoffOptions {
45
45
  position: number;
46
+ /** Android: keep MediaService FGS alive (with WIU) while native video plays. */
47
+ prewarm?: boolean;
46
48
  }
47
49
  export interface GetLastKnownPositionResult {
48
50
  position: number;
@@ -1,8 +1,8 @@
1
1
  import Dispatch
2
2
 
3
- private var throttleWorkItems = [AnyHashable: DispatchWorkItem]()
4
- private var lastDebounceCallTimes = [AnyHashable: DispatchTime]()
5
- private let nilContext: AnyHashable = arc4random()
3
+ nonisolated(unsafe) private var throttleWorkItems = [AnyHashable: DispatchWorkItem]()
4
+ nonisolated(unsafe) private var lastDebounceCallTimes = [AnyHashable: DispatchTime]()
5
+ nonisolated(unsafe) private let nilContext: AnyHashable = arc4random()
6
6
 
7
7
  public extension DispatchQueue {
8
8
  /**
@@ -9,7 +9,36 @@ protocol StatusUpdater {
9
9
  * here: https://capacitorjs.com/docs/plugins/ios
10
10
  */
11
11
  @objc(PlaylistPlugin)
12
- public class PlaylistPlugin: CAPPlugin, StatusUpdater {
12
+ public class PlaylistPlugin: CAPPlugin, StatusUpdater, CAPBridgedPlugin {
13
+ public let identifier = "PlaylistPlugin"
14
+ public let jsName = "Playlist"
15
+ public let pluginMethods: [CAPPluginMethod] = [
16
+ CAPPluginMethod(name: "setOptions", returnType: CAPPluginReturnPromise),
17
+ CAPPluginMethod(name: "initialize", returnType: CAPPluginReturnPromise),
18
+ CAPPluginMethod(name: "release", returnType: CAPPluginReturnPromise),
19
+ CAPPluginMethod(name: "setPlaylistItems", returnType: CAPPluginReturnPromise),
20
+ CAPPluginMethod(name: "addItem", returnType: CAPPluginReturnPromise),
21
+ CAPPluginMethod(name: "addAllItems", returnType: CAPPluginReturnPromise),
22
+ CAPPluginMethod(name: "removeItem", returnType: CAPPluginReturnPromise),
23
+ CAPPluginMethod(name: "removeItems", returnType: CAPPluginReturnPromise),
24
+ CAPPluginMethod(name: "clearAllItems", returnType: CAPPluginReturnPromise),
25
+ CAPPluginMethod(name: "getPlaylist", returnType: CAPPluginReturnPromise),
26
+ CAPPluginMethod(name: "play", returnType: CAPPluginReturnPromise),
27
+ CAPPluginMethod(name: "pause", returnType: CAPPluginReturnPromise),
28
+ CAPPluginMethod(name: "skipForward", returnType: CAPPluginReturnPromise),
29
+ CAPPluginMethod(name: "skipBack", returnType: CAPPluginReturnPromise),
30
+ CAPPluginMethod(name: "seekTo", returnType: CAPPluginReturnPromise),
31
+ CAPPluginMethod(name: "playTrackByIndex", returnType: CAPPluginReturnPromise),
32
+ CAPPluginMethod(name: "playTrackById", returnType: CAPPluginReturnPromise),
33
+ CAPPluginMethod(name: "selectTrackByIndex", returnType: CAPPluginReturnPromise),
34
+ CAPPluginMethod(name: "selectTrackById", returnType: CAPPluginReturnPromise),
35
+ CAPPluginMethod(name: "setPlaybackVolume", returnType: CAPPluginReturnPromise),
36
+ CAPPluginMethod(name: "setLoop", returnType: CAPPluginReturnPromise),
37
+ CAPPluginMethod(name: "setPlaybackRate", returnType: CAPPluginReturnPromise),
38
+ CAPPluginMethod(name: "prepareForVideoHandoff", returnType: CAPPluginReturnPromise),
39
+ CAPPluginMethod(name: "resumeAfterVideoHandoff", returnType: CAPPluginReturnPromise),
40
+ CAPPluginMethod(name: "getLastKnownPosition", returnType: CAPPluginReturnPromise),
41
+ ]
13
42
  let audioPlayerImpl = RmxAudioPlayer()
14
43
 
15
44
  // MARK: - Capacitor API
@@ -185,7 +214,8 @@ public class PlaylistPlugin: CAPPlugin, StatusUpdater {
185
214
 
186
215
  @objc func resumeAfterVideoHandoff(_ call: CAPPluginCall) {
187
216
  let position = call.getFloat("position", 0)
188
- audioPlayerImpl.resumeAfterVideoHandoff(position: position)
217
+ let prewarm = call.getBool("prewarm", false)
218
+ audioPlayerImpl.resumeAfterVideoHandoff(position: position, prewarm: prewarm)
189
219
  call.resolve()
190
220
  }
191
221
 
@@ -1235,8 +1235,11 @@ final class RmxAudioPlayer: NSObject {
1235
1235
  }
1236
1236
  }
1237
1237
 
1238
- func resumeAfterVideoHandoff(position: Float) {
1238
+ func resumeAfterVideoHandoff(position: Float, prewarm: Bool = false) {
1239
1239
  lastKnownHandoffPosition = position
1240
+ if prewarm {
1241
+ return
1242
+ }
1240
1243
  activateAudioSession()
1241
1244
  // Reset lastTrackId so the timeControlStatus KVO guard does not suppress the PLAYING
1242
1245
  // event on same-track non-index-0 resume. The guard `lastTrackId != trackId || isAtBeginning`
@@ -1,5 +1,5 @@
1
1
  import XCTest
2
- @testable import Plugin
2
+ @testable import PlaylistPlugin
3
3
 
4
4
  class PluginTests: XCTestCase {
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-plugin-playlist",
3
- "version": "0.9.4",
3
+ "version": "0.10.3",
4
4
  "description": "Playlist ",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",
@@ -9,12 +9,13 @@
9
9
  "files": [
10
10
  "dist/",
11
11
  "ios/",
12
+ "Package.swift",
12
13
  "android/",
13
14
  "CapacitorPluginPlaylist.podspec"
14
15
  ],
15
16
  "scripts": {
16
17
  "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
17
- "verify:ios": "cd ios && pod install && xcodebuild -workspace Plugin.xcworkspace -scheme Plugin && cd ..",
18
+ "verify:ios": "xcodebuild -scheme CapacitorPluginPlaylist -destination generic/platform=iOS",
18
19
  "verify:android": "cd android && ./gradlew clean build test && cd ..",
19
20
  "verify:web": "npm run build",
20
21
  "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
@@ -26,8 +27,7 @@
26
27
  "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
27
28
  "clean": "rimraf ./dist",
28
29
  "watch": "tsc --watch",
29
- "prepublishOnly": "npm run build",
30
- "prepare": "npm run build"
30
+ "prepublishOnly": "npm run build"
31
31
  },
32
32
  "author": "phiamo",
33
33
  "license": "MIT",
@@ -1,24 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
- <plist version="1.0">
4
- <dict>
5
- <key>CFBundleDevelopmentRegion</key>
6
- <string>$(DEVELOPMENT_LANGUAGE)</string>
7
- <key>CFBundleExecutable</key>
8
- <string>$(EXECUTABLE_NAME)</string>
9
- <key>CFBundleIdentifier</key>
10
- <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11
- <key>CFBundleInfoDictionaryVersion</key>
12
- <string>6.0</string>
13
- <key>CFBundleName</key>
14
- <string>$(PRODUCT_NAME)</string>
15
- <key>CFBundlePackageType</key>
16
- <string>FMWK</string>
17
- <key>CFBundleShortVersionString</key>
18
- <string>1.0</string>
19
- <key>CFBundleVersion</key>
20
- <string>$(CURRENT_PROJECT_VERSION)</string>
21
- <key>NSPrincipalClass</key>
22
- <string></string>
23
- </dict>
24
- </plist>
@@ -1,10 +0,0 @@
1
- #import <UIKit/UIKit.h>
2
-
3
- //! Project version number for Plugin.
4
- FOUNDATION_EXPORT double PluginVersionNumber;
5
-
6
- //! Project version string for Plugin.
7
- FOUNDATION_EXPORT const unsigned char PluginVersionString[];
8
-
9
- // In this header, you should import all the public headers of your framework using statements like #import <Plugin/PublicHeader.h>
10
-
@@ -1,29 +0,0 @@
1
- #import <Foundation/Foundation.h>
2
- #import <Capacitor/Capacitor.h>
3
-
4
- // Define the plugin using the CAP_PLUGIN Macro, and
5
- // each method the plugin supports using the CAP_PLUGIN_METHOD macro.
6
- CAP_PLUGIN(PlaylistPlugin, "Playlist",
7
- CAP_PLUGIN_METHOD(setOptions, CAPPluginReturnPromise);
8
- CAP_PLUGIN_METHOD(initialize, CAPPluginReturnPromise);
9
- CAP_PLUGIN_METHOD(release, CAPPluginReturnPromise);
10
- CAP_PLUGIN_METHOD(setPlaylistItems, CAPPluginReturnPromise);
11
- CAP_PLUGIN_METHOD(addItem, CAPPluginReturnPromise);
12
- CAP_PLUGIN_METHOD(addAllItems, CAPPluginReturnPromise);
13
- CAP_PLUGIN_METHOD(removeItem, CAPPluginReturnPromise);
14
- CAP_PLUGIN_METHOD(removeItems, CAPPluginReturnPromise);
15
- CAP_PLUGIN_METHOD(clearAllItems, CAPPluginReturnPromise);
16
- CAP_PLUGIN_METHOD(getPlaylist, CAPPluginReturnPromise);
17
- CAP_PLUGIN_METHOD(play, CAPPluginReturnPromise);
18
- CAP_PLUGIN_METHOD(pause, CAPPluginReturnPromise);
19
- CAP_PLUGIN_METHOD(skipForward, CAPPluginReturnPromise);
20
- CAP_PLUGIN_METHOD(skipBack, CAPPluginReturnPromise);
21
- CAP_PLUGIN_METHOD(seekTo, CAPPluginReturnPromise);
22
- CAP_PLUGIN_METHOD(playTrackByIndex, CAPPluginReturnPromise);
23
- CAP_PLUGIN_METHOD(playTrackById, CAPPluginReturnPromise);
24
- CAP_PLUGIN_METHOD(selectTrackByIndex, CAPPluginReturnPromise);
25
- CAP_PLUGIN_METHOD(selectTrackById, CAPPluginReturnPromise);
26
- CAP_PLUGIN_METHOD(setPlaybackVolume, CAPPluginReturnPromise);
27
- CAP_PLUGIN_METHOD(setLoop, CAPPluginReturnPromise);
28
- CAP_PLUGIN_METHOD(setPlaybackRate, CAPPluginReturnPromise);
29
- )