capacitor-plugin-playlist 0.9.5 → 0.10.8

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 (76) hide show
  1. package/CapacitorPluginPlaylist.podspec +1 -1
  2. package/Package.swift +31 -0
  3. package/README.md +1133 -137
  4. package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +24 -16
  5. package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +66 -11
  6. package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistManager.kt +4 -0
  7. package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandler.java +116 -0
  8. package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaService.kt +19 -0
  9. package/dist/docs.json +72 -32
  10. package/dist/esm/definitions.d.ts +120 -6
  11. package/dist/esm/web.d.ts +3 -1
  12. package/dist/esm/web.js +1 -1
  13. package/dist/esm/web.js.map +1 -1
  14. package/dist/plugin.cjs.js +1 -1
  15. package/dist/plugin.cjs.js.map +1 -1
  16. package/dist/plugin.js +1 -1
  17. package/dist/plugin.js.map +1 -1
  18. package/ios/{Plugin → Sources/PlaylistPlugin}/DispatchQueue.swift +3 -3
  19. package/ios/{Plugin → Sources/PlaylistPlugin}/Plugin.swift +37 -3
  20. package/ios/{Plugin → Sources/PlaylistPlugin}/RmxAudioPlayer.swift +58 -17
  21. package/ios/{PluginTests → Tests/PlaylistPluginTests}/PluginTests.swift +1 -1
  22. package/package.json +3 -2
  23. package/ios/Plugin/Info.plist +0 -24
  24. package/ios/Plugin/Plugin.h +0 -10
  25. package/ios/Plugin/Plugin.m +0 -29
  26. package/ios/Plugin.xcodeproj/project.pbxproj +0 -574
  27. package/ios/Plugin.xcworkspace/contents.xcworkspacedata +0 -10
  28. package/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  29. package/ios/PluginTests/Info.plist +0 -22
  30. package/ios/Podfile +0 -26
  31. package/ios/Podfile.lock +0 -22
  32. package/ios/Pods/Local Podspecs/Capacitor.podspec.json +0 -32
  33. package/ios/Pods/Local Podspecs/CapacitorCordova.podspec.json +0 -31
  34. package/ios/Pods/Manifest.lock +0 -22
  35. package/ios/Pods/Pods.xcodeproj/project.pbxproj +0 -1641
  36. package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/Capacitor-Capacitor.xcscheme +0 -58
  37. package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/Capacitor.xcscheme +0 -58
  38. package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/CapacitorCordova-CapacitorCordova.xcscheme +0 -58
  39. package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/CapacitorCordova.xcscheme +0 -58
  40. package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/Pods-Plugin.xcscheme +0 -58
  41. package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/Pods-PluginTests.xcscheme +0 -58
  42. package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/xcschememanagement.plist +0 -41
  43. package/ios/Pods/Target Support Files/Capacitor/Capacitor-Info.plist +0 -26
  44. package/ios/Pods/Target Support Files/Capacitor/Capacitor-dummy.m +0 -5
  45. package/ios/Pods/Target Support Files/Capacitor/Capacitor-prefix.pch +0 -12
  46. package/ios/Pods/Target Support Files/Capacitor/Capacitor.debug.xcconfig +0 -16
  47. package/ios/Pods/Target Support Files/Capacitor/Capacitor.modulemap +0 -8
  48. package/ios/Pods/Target Support Files/Capacitor/Capacitor.release.xcconfig +0 -16
  49. package/ios/Pods/Target Support Files/Capacitor/ResourceBundle-Capacitor-Capacitor-Info.plist +0 -24
  50. package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova-Info.plist +0 -26
  51. package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova-dummy.m +0 -5
  52. package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova-prefix.pch +0 -12
  53. package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova.debug.xcconfig +0 -13
  54. package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova.modulemap +0 -6
  55. package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova.release.xcconfig +0 -13
  56. package/ios/Pods/Target Support Files/CapacitorCordova/ResourceBundle-CapacitorCordova-CapacitorCordova-Info.plist +0 -24
  57. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-Info.plist +0 -26
  58. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-acknowledgements.markdown +0 -53
  59. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-acknowledgements.plist +0 -91
  60. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-dummy.m +0 -5
  61. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-umbrella.h +0 -16
  62. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin.debug.xcconfig +0 -15
  63. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin.modulemap +0 -6
  64. package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin.release.xcconfig +0 -15
  65. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-Info.plist +0 -26
  66. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-acknowledgements.markdown +0 -53
  67. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-acknowledgements.plist +0 -91
  68. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-dummy.m +0 -5
  69. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh +0 -188
  70. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-umbrella.h +0 -16
  71. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.debug.xcconfig +0 -16
  72. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.modulemap +0 -6
  73. package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.release.xcconfig +0 -16
  74. /package/ios/{Plugin → Sources/PlaylistPlugin}/AVBidirectionalQueuePlayer.swift +0 -0
  75. /package/ios/{Plugin → Sources/PlaylistPlugin}/AudioTrack.swift +0 -0
  76. /package/ios/{Plugin → Sources/PlaylistPlugin}/Constants.swift +0 -0
@@ -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,14 @@ 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)
410
- call.resolve()
411
- Log.i(TAG, "resumeAfterVideoHandoff")
412
+ val prewarm = call.getBoolean("prewarm", false) ?: false
413
+ // Default true preserves legacy Android in-place play when `play` is omitted.
414
+ val play = call.getBoolean("play", true) ?: true
415
+ val resumed = audioPlayerImpl!!.resumeAfterVideoHandoff(position, prewarm, play)
416
+ val result = JSObject()
417
+ result.put("resumed", resumed)
418
+ call.resolve(result)
419
+ Log.i(TAG, "resumeAfterVideoHandoff prewarm=$prewarm play=$play resumed=$resumed")
412
420
  }
413
421
  }
414
422
 
@@ -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,23 +462,76 @@ 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
- public void resumeAfterVideoHandoff(float positionSec) {
474
+ public boolean resumeAfterVideoHandoff(float positionSec) {
475
+ return resumeAfterVideoHandoff(positionSec, false, true);
476
+ }
477
+
478
+ /**
479
+ * @return {@code true} when in-place resume already seeked and started playback
480
+ * (JS should skip redundant seekTo/play); {@code false} for prewarm, paused handoff,
481
+ * or last-resort beginPlayback.
482
+ */
483
+ public boolean resumeAfterVideoHandoff(float positionSec, boolean prewarm) {
484
+ return resumeAfterVideoHandoff(positionSec, prewarm, true);
485
+ }
486
+
487
+ public boolean resumeAfterVideoHandoff(float positionSec, boolean prewarm, boolean play) {
472
488
  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
489
  long positionMs = (long) (positionSec * 1000f);
490
+ if (prewarm) {
491
+ playlistManager.setVideoHandoffForegroundRetain(true);
492
+ playlistManager.beginPlayback(positionMs, true);
493
+ return false;
494
+ }
495
+ if (!play) {
496
+ // Paused video exit — do not start audio; JS will seekTo only.
497
+ playlistManager.setVideoHandoffForegroundRetain(false);
498
+ return false;
499
+ }
500
+ if (tryResumeVideoHandoffInPlace(positionMs)) {
501
+ return true;
502
+ }
503
+ // Service not foreground — last resort (may be muted on Android 17 when backgrounded).
504
+ playlistManager.setVideoHandoffForegroundRetain(false);
479
505
  playlistManager.beginPlayback(positionMs, true);
506
+ return false;
507
+ }
508
+
509
+ /**
510
+ * Resume audio at {@code positionMs} without restarting MediaService (Android 17 AudioHardening).
511
+ */
512
+ public boolean tryResumeVideoHandoffInPlace(long positionMs) {
513
+ if (!playlistManager.getVideoHandoffForegroundRetain()) {
514
+ return false;
515
+ }
516
+ MediaService service = MediaService.getInstance();
517
+ if (service == null || !service.isRunningInForeground()) {
518
+ return false;
519
+ }
520
+ com.devbrackets.android.playlistcore.components.playlisthandler.PlaylistHandler<?> handler =
521
+ playlistManager.getPlaylistHandler();
522
+ if (!(handler instanceof AudioPlaylistHandler)) {
523
+ return false;
524
+ }
525
+ AudioPlaylistHandler<?, ?> audioHandler = (AudioPlaylistHandler<?, ?>) handler;
526
+ com.devbrackets.android.playlistcore.api.MediaPlayerApi<?> mediaPlayer = audioHandler.getCurrentMediaPlayer();
527
+ if (mediaPlayer != null) {
528
+ audioHandler.resumePlaybackAfterVideoHandoff(positionMs);
529
+ } else {
530
+ // Re-prepare within the existing foreground service — do not call beginPlayback/startService.
531
+ audioHandler.startItemPlayback(positionMs, false);
532
+ }
533
+ playlistManager.setVideoHandoffForegroundRetain(false);
534
+ return true;
480
535
  }
481
536
 
482
537
  public float getLastKnownPositionSec() {
@@ -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,125 @@ 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 starts playback.
97
+ * <p>
98
+ * Must {@link #play()} before {@link #seek(long)}: playlistcore {@code performSeek} sets
99
+ * {@code playingBeforeSeek = isPlaying()}. After video handoff the player is paused, so
100
+ * seek-then-play races with {@code onSeekComplete}, which calls {@code pause()} when
101
+ * {@code playingBeforeSeek} is false — UI briefly shows playing while native audio stays silent.
102
+ */
103
+ public void resumePlaybackAfterVideoHandoff(long positionMs) {
104
+ ((PlaylistManager) getPlaylistManager()).setVideoHandoffForegroundRetain(false);
105
+ setStartPaused(false);
106
+ getAudioFocusProvider().requestFocus();
107
+ play();
108
+ if (positionMs > 0) {
109
+ seek(positionMs);
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Release audio focus for video without tearing down the foreground service (Epic 45 / Android 17).
115
+ */
116
+ public void pauseForVideoHandoff() {
117
+ if (isPlaying()) {
118
+ com.devbrackets.android.playlistcore.api.MediaPlayerApi<I> mediaPlayer = getCurrentMediaPlayer();
119
+ if (mediaPlayer != null) {
120
+ mediaPlayer.pause();
121
+ }
122
+ }
123
+ getMediaProgressPoll().stop();
124
+ setPlaybackState(com.devbrackets.android.playlistcore.data.PlaybackState.PAUSED);
125
+ getAudioFocusProvider().abandonFocus();
126
+ }
127
+
128
+ @Override
129
+ public void pause(boolean isTemporary) {
130
+ if (((PlaylistManager) getPlaylistManager()).getVideoHandoffForegroundRetain()) {
131
+ if (isPlaying()) {
132
+ com.devbrackets.android.playlistcore.api.MediaPlayerApi<I> mediaPlayer = getCurrentMediaPlayer();
133
+ if (mediaPlayer != null) {
134
+ mediaPlayer.pause();
135
+ }
136
+ }
137
+ getMediaProgressPoll().stop();
138
+ setPlaybackState(com.devbrackets.android.playlistcore.data.PlaybackState.PAUSED);
139
+ if (!isTemporary) {
140
+ getAudioFocusProvider().abandonFocus();
141
+ }
142
+ return;
143
+ }
144
+ super.pause(isTemporary);
145
+ }
146
+
147
+ @Override
148
+ protected void relaxResources() {
149
+ if (((PlaylistManager) getPlaylistManager()).getVideoHandoffForegroundRetain()) {
150
+ return;
151
+ }
152
+ super.relaxResources();
153
+ }
154
+
155
+ /**
156
+ * Android 17 AudioHardening: re-calling startForeground from background revokes WIU and mutes playback.
157
+ * When prewarm already promoted MediaService, only update the notification via super when not foreground.
158
+ */
159
+ @Override
160
+ protected void setupForeground() {
161
+ MediaService service = MediaService.getInstance();
162
+ if (service != null && service.isRunningInForeground()) {
163
+ return;
164
+ }
165
+ super.setupForeground();
166
+ }
167
+
68
168
  @Override
69
169
  public void onPrepared(@NotNull MediaPlayerApi<I> mediaPlayer) {
170
+ if (isVideoHandoffPrewarmActive()) {
171
+ // Prewarm: prepare at seek position but stay silent — native video owns audio focus.
172
+ super.onPrepared(mediaPlayer);
173
+ com.devbrackets.android.playlistcore.api.MediaPlayerApi<I> currentPlayer = getCurrentMediaPlayer();
174
+ if (currentPlayer != null && currentPlayer.isPlaying()) {
175
+ currentPlayer.pause();
176
+ }
177
+ getMediaProgressPoll().stop();
178
+ setPlaybackState(com.devbrackets.android.playlistcore.data.PlaybackState.PAUSED);
179
+ getAudioFocusProvider().abandonFocus();
180
+ return;
181
+ }
70
182
  super.onPrepared(mediaPlayer);
71
183
  }
72
184
 
185
+ private boolean isVideoHandoffPrewarmActive() {
186
+ return ((PlaylistManager) getPlaylistManager()).getVideoHandoffForegroundRetain();
187
+ }
188
+
73
189
  @Override
74
190
  public boolean onError(@NotNull MediaPlayerApi<I> mediaPlayer) {
75
191
  ((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