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.
- package/CapacitorPluginPlaylist.podspec +1 -1
- package/Package.swift +31 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +19 -15
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +50 -10
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistManager.kt +4 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandler.java +117 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaService.kt +19 -0
- package/dist/docs.json +7 -0
- package/dist/esm/RmxAudioPlayer.d.ts +1 -1
- package/dist/esm/definitions.d.ts +2 -0
- package/ios/{Plugin → Sources/PlaylistPlugin}/DispatchQueue.swift +3 -3
- package/ios/{Plugin → Sources/PlaylistPlugin}/Plugin.swift +32 -2
- package/ios/{Plugin → Sources/PlaylistPlugin}/RmxAudioPlayer.swift +4 -1
- package/ios/{PluginTests → Tests/PlaylistPluginTests}/PluginTests.swift +1 -1
- package/package.json +4 -4
- package/ios/Plugin/Info.plist +0 -24
- package/ios/Plugin/Plugin.h +0 -10
- package/ios/Plugin/Plugin.m +0 -29
- package/ios/Plugin.xcodeproj/project.pbxproj +0 -574
- package/ios/Plugin.xcworkspace/contents.xcworkspacedata +0 -10
- package/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- package/ios/PluginTests/Info.plist +0 -22
- package/ios/Podfile +0 -26
- package/ios/Podfile.lock +0 -22
- package/ios/Pods/Local Podspecs/Capacitor.podspec.json +0 -32
- package/ios/Pods/Local Podspecs/CapacitorCordova.podspec.json +0 -31
- package/ios/Pods/Manifest.lock +0 -22
- package/ios/Pods/Pods.xcodeproj/project.pbxproj +0 -1641
- package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/Capacitor-Capacitor.xcscheme +0 -58
- package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/Capacitor.xcscheme +0 -58
- package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/CapacitorCordova-CapacitorCordova.xcscheme +0 -58
- package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/CapacitorCordova.xcscheme +0 -58
- package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/Pods-Plugin.xcscheme +0 -58
- package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/Pods-PluginTests.xcscheme +0 -58
- package/ios/Pods/Pods.xcodeproj/xcuserdata/philippmohrenweiser.xcuserdatad/xcschemes/xcschememanagement.plist +0 -41
- package/ios/Pods/Target Support Files/Capacitor/Capacitor-Info.plist +0 -26
- package/ios/Pods/Target Support Files/Capacitor/Capacitor-dummy.m +0 -5
- package/ios/Pods/Target Support Files/Capacitor/Capacitor-prefix.pch +0 -12
- package/ios/Pods/Target Support Files/Capacitor/Capacitor.debug.xcconfig +0 -16
- package/ios/Pods/Target Support Files/Capacitor/Capacitor.modulemap +0 -8
- package/ios/Pods/Target Support Files/Capacitor/Capacitor.release.xcconfig +0 -16
- package/ios/Pods/Target Support Files/Capacitor/ResourceBundle-Capacitor-Capacitor-Info.plist +0 -24
- package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova-Info.plist +0 -26
- package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova-dummy.m +0 -5
- package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova-prefix.pch +0 -12
- package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova.debug.xcconfig +0 -13
- package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova.modulemap +0 -6
- package/ios/Pods/Target Support Files/CapacitorCordova/CapacitorCordova.release.xcconfig +0 -13
- package/ios/Pods/Target Support Files/CapacitorCordova/ResourceBundle-CapacitorCordova-CapacitorCordova-Info.plist +0 -24
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-Info.plist +0 -26
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-acknowledgements.markdown +0 -53
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-acknowledgements.plist +0 -91
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-dummy.m +0 -5
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin-umbrella.h +0 -16
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin.debug.xcconfig +0 -15
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin.modulemap +0 -6
- package/ios/Pods/Target Support Files/Pods-Plugin/Pods-Plugin.release.xcconfig +0 -15
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-Info.plist +0 -26
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-acknowledgements.markdown +0 -53
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-acknowledgements.plist +0 -91
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-dummy.m +0 -5
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-frameworks.sh +0 -188
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests-umbrella.h +0 -16
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.debug.xcconfig +0 -16
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.modulemap +0 -6
- package/ios/Pods/Target Support Files/Pods-PluginTests/Pods-PluginTests.release.xcconfig +0 -16
- /package/ios/{Plugin → Sources/PlaylistPlugin}/AVBidirectionalQueuePlayer.swift +0 -0
- /package/ios/{Plugin → Sources/PlaylistPlugin}/AudioTrack.swift +0 -0
- /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/
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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(): "
|
|
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
|
-
|
|
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`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capacitor-plugin-playlist",
|
|
3
|
-
"version": "0.
|
|
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": "
|
|
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",
|
package/ios/Plugin/Info.plist
DELETED
|
@@ -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>
|
package/ios/Plugin/Plugin.h
DELETED
|
@@ -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
|
-
|
package/ios/Plugin/Plugin.m
DELETED
|
@@ -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
|
-
)
|