capacitor-plugin-playlist 0.11.4 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -127
- package/android/build.gradle +20 -15
- package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
- package/android/src/main/java/org/dwbn/plugins/playlist/AudioMediaItemFactory.java +96 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/PlaylistPlugin.kt +3 -3
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxAudioPlayer.java +145 -188
- package/android/src/main/java/org/dwbn/plugins/playlist/RmxPlaybackErrorMapper.kt +26 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/data/AudioTrack.kt +28 -28
- package/android/src/main/java/org/dwbn/plugins/playlist/handoff/VideoPlayerBridge.kt +102 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaybackProgress.kt +8 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistManager.kt +418 -183
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/PlaylistPlaybackPolicy.kt +98 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/manager/RmxPlaybackState.kt +11 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandler.java +107 -211
- package/android/src/main/java/org/dwbn/plugins/playlist/service/GlideBitmapLoader.kt +121 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/service/HandoffForwardingPlayer.kt +185 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaNotificationPolicy.kt +253 -0
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaService.kt +260 -68
- package/android/src/main/java/org/dwbn/plugins/playlist/service/PlaylistMediaSessionCallback.kt +39 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/AudioMediaItemFactoryMimeTest.kt +98 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/AudioMediaItemFactoryTest.java +71 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/data/AudioTrackTest.kt +53 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/handoff/VideoPlayerBridgeTest.kt +84 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/manager/PlaylistPlaybackPolicyTest.kt +128 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/playlist/AudioPlaylistHandlerTest.kt +145 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/service/GlideBitmapLoaderTest.kt +76 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/service/HandoffForwardingPlayerTest.kt +282 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/service/MediaNotificationPolicyTest.kt +285 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/service/VideoHandoffNotificationCommandsTest.kt +54 -0
- package/android/src/test/java/org/dwbn/plugins/playlist/service/VideoHandoffNotificationPolicyTest.kt +47 -0
- package/dist/docs.json +12 -846
- package/dist/esm/RmxAudioPlayer.d.ts +2 -2
- package/dist/esm/RmxAudioPlayer.js.map +1 -1
- package/dist/esm/definitions.d.ts +7 -7
- package/dist/esm/interfaces.d.ts +7 -1
- package/dist/esm/utils.d.ts +1 -1
- package/dist/esm/utils.js +2 -2
- package/dist/esm/utils.js.map +1 -1
- package/dist/esm/web.d.ts +3 -3
- package/dist/esm/web.js +5 -5
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +7 -7
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +7 -7
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/PlaylistPlugin/AVBidirectionalQueuePlayer.swift +45 -6
- package/ios/Sources/PlaylistPlugin/AudioTrack.swift +15 -2
- package/ios/Tests/PlaylistPluginTests/PositionResumeTests.swift +82 -0
- package/package.json +15 -19
- package/android/src/main/java/org/dwbn/plugins/playlist/notification/PlaylistNotificationProvider.kt +0 -26
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/AudioApi.kt +0 -114
- package/android/src/main/java/org/dwbn/plugins/playlist/playlist/BaseMediaApi.kt +0 -36
- package/android/src/main/java/org/dwbn/plugins/playlist/service/MediaImageProvider.kt +0 -83
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist.handoff
|
|
2
|
+
|
|
3
|
+
import androidx.media3.common.Player
|
|
4
|
+
import org.junit.After
|
|
5
|
+
import org.junit.Assert.assertFalse
|
|
6
|
+
import org.junit.Assert.assertSame
|
|
7
|
+
import org.junit.Assert.assertTrue
|
|
8
|
+
import org.junit.Test
|
|
9
|
+
import java.lang.reflect.Proxy
|
|
10
|
+
|
|
11
|
+
class VideoPlayerBridgeTest {
|
|
12
|
+
|
|
13
|
+
@After
|
|
14
|
+
fun tearDown() {
|
|
15
|
+
VideoPlayerBridge.clearForTests()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Test
|
|
19
|
+
fun attachAndDetach_trackActivePlayer() {
|
|
20
|
+
val player = dummyPlayer()
|
|
21
|
+
VideoPlayerBridge.attach(player)
|
|
22
|
+
assertSame(player, VideoPlayerBridge.activePlayer())
|
|
23
|
+
assertTrue(VideoPlayerBridge.hasActivePlayer())
|
|
24
|
+
|
|
25
|
+
VideoPlayerBridge.detach(player)
|
|
26
|
+
assertFalse(VideoPlayerBridge.hasActivePlayer())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@Test
|
|
30
|
+
fun detach_ignoresDifferentPlayerInstance() {
|
|
31
|
+
val attached = dummyPlayer()
|
|
32
|
+
val other = dummyPlayer()
|
|
33
|
+
VideoPlayerBridge.attach(attached)
|
|
34
|
+
VideoPlayerBridge.detach(other)
|
|
35
|
+
assertTrue(VideoPlayerBridge.hasActivePlayer())
|
|
36
|
+
VideoPlayerBridge.detach(attached)
|
|
37
|
+
assertFalse(VideoPlayerBridge.hasActivePlayer())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Test
|
|
41
|
+
fun attach_notifiesPlaybackChangedListeners() {
|
|
42
|
+
var notifications = 0
|
|
43
|
+
VideoPlayerBridge.addPlaybackChangedListener { notifications++ }
|
|
44
|
+
VideoPlayerBridge.attach(dummyPlayer())
|
|
45
|
+
assertTrue(notifications > 0)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test
|
|
49
|
+
fun reAttach_replacesPreviousPlayer() {
|
|
50
|
+
val first = dummyPlayer()
|
|
51
|
+
val second = dummyPlayer()
|
|
52
|
+
VideoPlayerBridge.attach(first)
|
|
53
|
+
VideoPlayerBridge.attach(second)
|
|
54
|
+
assertSame(second, VideoPlayerBridge.activePlayer())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
fun detach_notifiesPlaybackChangedListeners() {
|
|
59
|
+
val player = dummyPlayer()
|
|
60
|
+
var notifications = 0
|
|
61
|
+
VideoPlayerBridge.addPlaybackChangedListener { notifications++ }
|
|
62
|
+
VideoPlayerBridge.attach(player)
|
|
63
|
+
val afterAttach = notifications
|
|
64
|
+
VideoPlayerBridge.detach(player)
|
|
65
|
+
assertTrue(notifications > afterAttach)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Test
|
|
69
|
+
fun detachNull_clearsActivePlayer() {
|
|
70
|
+
VideoPlayerBridge.attach(dummyPlayer())
|
|
71
|
+
VideoPlayerBridge.detach(null)
|
|
72
|
+
assertFalse(VideoPlayerBridge.hasActivePlayer())
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private fun dummyPlayer(): Player =
|
|
76
|
+
Proxy.newProxyInstance(Player::class.java.classLoader, arrayOf(Player::class.java)) { _, method, _ ->
|
|
77
|
+
when (method.returnType) {
|
|
78
|
+
java.lang.Boolean.TYPE -> false
|
|
79
|
+
java.lang.Integer.TYPE -> Player.STATE_IDLE
|
|
80
|
+
java.lang.Long.TYPE -> 0L
|
|
81
|
+
else -> null
|
|
82
|
+
}
|
|
83
|
+
} as Player
|
|
84
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist.manager
|
|
2
|
+
|
|
3
|
+
import androidx.media3.common.Player
|
|
4
|
+
import org.junit.Assert.assertEquals
|
|
5
|
+
import org.junit.Assert.assertFalse
|
|
6
|
+
import org.junit.Assert.assertTrue
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
|
|
9
|
+
/** Parity with the former PlaylistCore stack (loop, completion, error skip, play recovery). */
|
|
10
|
+
class PlaylistPlaybackPolicyTest {
|
|
11
|
+
|
|
12
|
+
@Test
|
|
13
|
+
fun repeatMode_loopWithSeveralItems_repeatsAll() {
|
|
14
|
+
assertEquals(Player.REPEAT_MODE_ALL, PlaylistPlaybackPolicy.repeatMode(loop = true, itemCount = 3))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Test
|
|
18
|
+
fun repeatMode_loopWithSingleItem_stopsAtEndLikePlaylistCore() {
|
|
19
|
+
assertEquals(Player.REPEAT_MODE_OFF, PlaylistPlaybackPolicy.repeatMode(loop = true, itemCount = 1))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Test
|
|
23
|
+
fun repeatMode_noLoop_isOff() {
|
|
24
|
+
assertEquals(Player.REPEAT_MODE_OFF, PlaylistPlaybackPolicy.repeatMode(loop = false, itemCount = 3))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Test
|
|
28
|
+
fun nextAvailable_matchesFormerPlaylistManager() {
|
|
29
|
+
assertFalse(PlaylistPlaybackPolicy.nextAvailable(0, 1, loop = true))
|
|
30
|
+
assertTrue(PlaylistPlaybackPolicy.nextAvailable(0, 3, loop = false))
|
|
31
|
+
assertFalse(PlaylistPlaybackPolicy.nextAvailable(2, 3, loop = false))
|
|
32
|
+
assertTrue(PlaylistPlaybackPolicy.nextAvailable(2, 3, loop = true))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Test
|
|
36
|
+
fun previousAvailable_atStartOnlyWhenLooping() {
|
|
37
|
+
assertFalse(PlaylistPlaybackPolicy.previousAvailable(0, loop = false, itemCount = 3))
|
|
38
|
+
assertTrue(PlaylistPlaybackPolicy.previousAvailable(0, loop = true, itemCount = 3))
|
|
39
|
+
assertTrue(PlaylistPlaybackPolicy.previousAvailable(1, loop = false, itemCount = 3))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test
|
|
43
|
+
fun previousAvailable_emptyPlaylist_isFalse() {
|
|
44
|
+
assertFalse(PlaylistPlaybackPolicy.previousAvailable(0, loop = true, itemCount = 0))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Test
|
|
48
|
+
fun naturalItemEnd_isAutoOrRepeatTransitionOnly() {
|
|
49
|
+
assertTrue(PlaylistPlaybackPolicy.isNaturalItemEnd(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO))
|
|
50
|
+
assertTrue(PlaylistPlaybackPolicy.isNaturalItemEnd(Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT))
|
|
51
|
+
assertFalse(PlaylistPlaybackPolicy.isNaturalItemEnd(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK))
|
|
52
|
+
assertFalse(PlaylistPlaybackPolicy.isNaturalItemEnd(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun errorSkip_upToThreeSequentialErrorsWhenNextExists() {
|
|
57
|
+
assertTrue(PlaylistPlaybackPolicy.shouldSkipAfterError(1, nextAvailable = true))
|
|
58
|
+
assertTrue(PlaylistPlaybackPolicy.shouldSkipAfterError(3, nextAvailable = true))
|
|
59
|
+
assertFalse(PlaylistPlaybackPolicy.shouldSkipAfterError(4, nextAvailable = true))
|
|
60
|
+
assertFalse(PlaylistPlaybackPolicy.shouldSkipAfterError(1, nextAvailable = false))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test
|
|
64
|
+
fun errorSkip_keepsPlayingUnlessFirstAutoBufferedItemFailed() {
|
|
65
|
+
assertTrue(PlaylistPlaybackPolicy.playWhenReadyAfterErrorSkip(playWhenReady = true, failedIndex = 0))
|
|
66
|
+
assertFalse(PlaylistPlaybackPolicy.playWhenReadyAfterErrorSkip(playWhenReady = false, failedIndex = 0))
|
|
67
|
+
assertTrue(PlaylistPlaybackPolicy.playWhenReadyAfterErrorSkip(playWhenReady = false, failedIndex = 2))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Test
|
|
71
|
+
fun playRecovery_preparesIdleAndRewindsEnded() {
|
|
72
|
+
assertTrue(PlaylistPlaybackPolicy.needsPrepareBeforePlay(Player.STATE_IDLE))
|
|
73
|
+
assertFalse(PlaylistPlaybackPolicy.needsPrepareBeforePlay(Player.STATE_READY))
|
|
74
|
+
assertTrue(PlaylistPlaybackPolicy.needsRewindBeforePlay(Player.STATE_ENDED))
|
|
75
|
+
assertFalse(PlaylistPlaybackPolicy.needsRewindBeforePlay(Player.STATE_BUFFERING))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Test
|
|
79
|
+
fun liveEdge_onlyForLiveStreamsWithResetEnabled() {
|
|
80
|
+
assertTrue(PlaylistPlaybackPolicy.shouldJumpToLiveEdge(true, isStream = true, isLive = true))
|
|
81
|
+
assertFalse(PlaylistPlaybackPolicy.shouldJumpToLiveEdge(true, isStream = true, isLive = false))
|
|
82
|
+
assertFalse(PlaylistPlaybackPolicy.shouldJumpToLiveEdge(false, isStream = true, isLive = true))
|
|
83
|
+
assertFalse(PlaylistPlaybackPolicy.shouldJumpToLiveEdge(true, isStream = false, isLive = true))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Test
|
|
87
|
+
fun errorSkipWindowIndex_midListAndWrapToStart() {
|
|
88
|
+
assertEquals(2, PlaylistPlaybackPolicy.errorSkipWindowIndex(failedIndex = 1, itemCount = 5))
|
|
89
|
+
assertEquals(0, PlaylistPlaybackPolicy.errorSkipWindowIndex(failedIndex = 4, itemCount = 5))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Test
|
|
93
|
+
fun errorSkipWindowIndex_emptyListReturnsZero() {
|
|
94
|
+
assertEquals(0, PlaylistPlaybackPolicy.errorSkipWindowIndex(failedIndex = 0, itemCount = 0))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Test
|
|
98
|
+
fun nextAvailable_emptyPlaylist_isFalse() {
|
|
99
|
+
assertFalse(PlaylistPlaybackPolicy.nextAvailable(0, itemCount = 0, loop = true))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Test
|
|
103
|
+
fun repeatMode_emptyPlaylist_isOff() {
|
|
104
|
+
assertEquals(Player.REPEAT_MODE_OFF, PlaylistPlaybackPolicy.repeatMode(loop = true, itemCount = 0))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Test
|
|
108
|
+
fun nextSkipIndex_midListAdvancesByOne() {
|
|
109
|
+
assertEquals(2, PlaylistPlaybackPolicy.nextSkipIndex(fromIndex = 1, itemCount = 5, loop = false))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Test
|
|
113
|
+
fun nextSkipIndex_atEnd_wrapsToStartOnlyWhenLooping() {
|
|
114
|
+
assertEquals(3, PlaylistPlaybackPolicy.nextSkipIndex(fromIndex = 2, itemCount = 3, loop = false))
|
|
115
|
+
assertEquals(0, PlaylistPlaybackPolicy.nextSkipIndex(fromIndex = 2, itemCount = 3, loop = true))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@Test
|
|
119
|
+
fun previousSkipIndex_midListGoesBackByOne() {
|
|
120
|
+
assertEquals(1, PlaylistPlaybackPolicy.previousSkipIndex(fromIndex = 2, itemCount = 5, loop = false))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@Test
|
|
124
|
+
fun previousSkipIndex_atStart_wrapsToLastOnlyWhenLooping() {
|
|
125
|
+
assertEquals(-1, PlaylistPlaybackPolicy.previousSkipIndex(fromIndex = 0, itemCount = 3, loop = false))
|
|
126
|
+
assertEquals(2, PlaylistPlaybackPolicy.previousSkipIndex(fromIndex = 0, itemCount = 3, loop = true))
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist.playlist
|
|
2
|
+
|
|
3
|
+
import androidx.media3.common.Player
|
|
4
|
+
import org.junit.Assert.assertEquals
|
|
5
|
+
import org.junit.Assert.assertFalse
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
import java.lang.reflect.Proxy
|
|
8
|
+
|
|
9
|
+
class AudioPlaylistHandlerTest {
|
|
10
|
+
|
|
11
|
+
@Test
|
|
12
|
+
fun pauseForVideoHandoff_setsPlayWhenReadyFalseWhenNotPlaying() {
|
|
13
|
+
val recording = RecordingPlayer(isPlaying = false)
|
|
14
|
+
assertFalse(recording.proxy.isPlaying)
|
|
15
|
+
|
|
16
|
+
AudioPlaylistHandler.pauseForVideoHandoff(recording.proxy)
|
|
17
|
+
|
|
18
|
+
assertEquals(listOf(false), recording.setPlayWhenReadyCalls)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Test
|
|
22
|
+
fun audibleResume_playsBeforeSeekSoAudioIsNotSilent() {
|
|
23
|
+
val recording = RecordingPlayer(isPlaying = false)
|
|
24
|
+
|
|
25
|
+
AudioPlaylistHandler.applyAudibleResume(recording.proxy, 175_000L)
|
|
26
|
+
|
|
27
|
+
assertEquals(listOf("playWhenReady:true", "seek:175000"), recording.calls)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Test
|
|
31
|
+
fun audibleResume_atZero_playsWithoutSeek() {
|
|
32
|
+
val recording = RecordingPlayer(isPlaying = false)
|
|
33
|
+
|
|
34
|
+
AudioPlaylistHandler.applyAudibleResume(recording.proxy, 0L)
|
|
35
|
+
|
|
36
|
+
assertEquals(listOf("playWhenReady:true"), recording.calls)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@Test
|
|
40
|
+
fun play_onIdlePlayer_preparesBeforePlaying() {
|
|
41
|
+
val recording = RecordingPlayer(isPlaying = false, playbackState = Player.STATE_IDLE)
|
|
42
|
+
|
|
43
|
+
AudioPlaylistHandler.startAudible(recording.proxy)
|
|
44
|
+
|
|
45
|
+
assertEquals(listOf("prepare", "playWhenReady:true"), recording.calls)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Test
|
|
49
|
+
fun play_onEndedPlayer_rewindsCurrentItemBeforePlaying() {
|
|
50
|
+
val recording = RecordingPlayer(isPlaying = false, playbackState = Player.STATE_ENDED, currentIndex = 2)
|
|
51
|
+
|
|
52
|
+
AudioPlaylistHandler.startAudible(recording.proxy)
|
|
53
|
+
|
|
54
|
+
assertEquals(listOf("seekToDefault:2", "playWhenReady:true"), recording.calls)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Test
|
|
58
|
+
fun play_onReadyPlayer_onlyPlays() {
|
|
59
|
+
val recording = RecordingPlayer(isPlaying = false, playbackState = Player.STATE_READY)
|
|
60
|
+
|
|
61
|
+
AudioPlaylistHandler.startAudible(recording.proxy)
|
|
62
|
+
|
|
63
|
+
assertEquals(listOf("playWhenReady:true"), recording.calls)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test
|
|
67
|
+
fun resetStreamOnPause_liveStream_resumesAtLiveEdge() {
|
|
68
|
+
val recording = RecordingPlayer(isPlaying = false, isLive = true)
|
|
69
|
+
|
|
70
|
+
AudioPlaylistHandler.jumpToLiveEdgeIfReset(recording.proxy, true, true)
|
|
71
|
+
|
|
72
|
+
assertEquals(listOf("seekToDefault:current"), recording.calls)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Test
|
|
76
|
+
fun resetStreamOnPause_vodHlsLecture_keepsPosition() {
|
|
77
|
+
val recording = RecordingPlayer(isPlaying = false, isLive = false)
|
|
78
|
+
|
|
79
|
+
AudioPlaylistHandler.jumpToLiveEdgeIfReset(recording.proxy, true, true)
|
|
80
|
+
|
|
81
|
+
assertEquals(emptyList<String>(), recording.calls)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Test
|
|
85
|
+
fun resetStreamOnPauseOff_liveStream_keepsPosition() {
|
|
86
|
+
val recording = RecordingPlayer(isPlaying = false, isLive = true)
|
|
87
|
+
|
|
88
|
+
AudioPlaylistHandler.jumpToLiveEdgeIfReset(recording.proxy, false, true)
|
|
89
|
+
|
|
90
|
+
assertEquals(emptyList<String>(), recording.calls)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private class RecordingPlayer(
|
|
94
|
+
private val isPlaying: Boolean,
|
|
95
|
+
private val playbackState: Int = Player.STATE_READY,
|
|
96
|
+
private val currentIndex: Int = 0,
|
|
97
|
+
private val isLive: Boolean = false
|
|
98
|
+
) {
|
|
99
|
+
val setPlayWhenReadyCalls = mutableListOf<Boolean>()
|
|
100
|
+
val calls = mutableListOf<String>()
|
|
101
|
+
val proxy: Player = Proxy.newProxyInstance(
|
|
102
|
+
Player::class.java.classLoader,
|
|
103
|
+
arrayOf(Player::class.java)
|
|
104
|
+
) { _, method, args ->
|
|
105
|
+
when (method.name) {
|
|
106
|
+
"isPlaying" -> isPlaying
|
|
107
|
+
"getPlaybackState" -> playbackState
|
|
108
|
+
"getCurrentMediaItemIndex" -> currentIndex
|
|
109
|
+
"isCurrentMediaItemLive" -> isLive
|
|
110
|
+
"prepare" -> {
|
|
111
|
+
calls.add("prepare")
|
|
112
|
+
null
|
|
113
|
+
}
|
|
114
|
+
"seekToDefaultPosition" -> {
|
|
115
|
+
calls.add("seekToDefault:" + (args?.getOrNull(0) ?: "current"))
|
|
116
|
+
null
|
|
117
|
+
}
|
|
118
|
+
"setPlayWhenReady" -> {
|
|
119
|
+
val ready = args[0] as Boolean
|
|
120
|
+
setPlayWhenReadyCalls.add(ready)
|
|
121
|
+
calls.add("playWhenReady:$ready")
|
|
122
|
+
null
|
|
123
|
+
}
|
|
124
|
+
"seekTo" -> {
|
|
125
|
+
val pos = when (args.size) {
|
|
126
|
+
1 -> args[0] as Long
|
|
127
|
+
else -> args[1] as Long
|
|
128
|
+
}
|
|
129
|
+
calls.add("seek:$pos")
|
|
130
|
+
null
|
|
131
|
+
}
|
|
132
|
+
else -> defaultValue(method.returnType)
|
|
133
|
+
}
|
|
134
|
+
} as Player
|
|
135
|
+
|
|
136
|
+
private fun defaultValue(returnType: Class<*>): Any? = when (returnType) {
|
|
137
|
+
java.lang.Boolean.TYPE, java.lang.Boolean::class.java -> false
|
|
138
|
+
java.lang.Integer.TYPE, java.lang.Integer::class.java -> 0
|
|
139
|
+
java.lang.Long.TYPE, java.lang.Long::class.java -> 0L
|
|
140
|
+
java.lang.Float.TYPE, java.lang.Float::class.java -> 0f
|
|
141
|
+
java.lang.Double.TYPE, java.lang.Double::class.java -> 0.0
|
|
142
|
+
else -> null
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package org.dwbn.plugins.playlist.service
|
|
2
|
+
|
|
3
|
+
import androidx.media3.common.MediaMetadata
|
|
4
|
+
import java.util.concurrent.ExecutionException
|
|
5
|
+
import java.util.concurrent.TimeUnit
|
|
6
|
+
import org.junit.Assert.assertFalse
|
|
7
|
+
import org.junit.Assert.assertNull
|
|
8
|
+
import org.junit.Assert.assertTrue
|
|
9
|
+
import org.junit.Test
|
|
10
|
+
import org.junit.runner.RunWith
|
|
11
|
+
import org.robolectric.RobolectricTestRunner
|
|
12
|
+
import org.robolectric.RuntimeEnvironment
|
|
13
|
+
import org.robolectric.annotation.Config
|
|
14
|
+
|
|
15
|
+
@RunWith(RobolectricTestRunner::class)
|
|
16
|
+
@Config(sdk = [28])
|
|
17
|
+
class GlideBitmapLoaderTest {
|
|
18
|
+
|
|
19
|
+
private val loader: GlideBitmapLoader
|
|
20
|
+
get() = GlideBitmapLoader(RuntimeEnvironment.getApplication())
|
|
21
|
+
|
|
22
|
+
@Test
|
|
23
|
+
fun imageJpeg_isSupportedMimeType() {
|
|
24
|
+
assertTrue(GlideBitmapLoader.isSupportedImageMimeType("image/jpeg"))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@Test
|
|
28
|
+
fun imagePng_isSupportedMimeType() {
|
|
29
|
+
assertTrue(GlideBitmapLoader.isSupportedImageMimeType("IMAGE/PNG"))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Test
|
|
33
|
+
fun audioMpeg_isNotSupportedMimeType() {
|
|
34
|
+
assertFalse(GlideBitmapLoader.isSupportedImageMimeType("audio/mpeg"))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test
|
|
38
|
+
fun nullMimeType_isNotSupported() {
|
|
39
|
+
assertFalse(GlideBitmapLoader.isSupportedImageMimeType(null))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test
|
|
43
|
+
fun artworkUri_loadsRemoteArtwork() {
|
|
44
|
+
assertTrue(GlideBitmapLoader.shouldLoadRemoteArtwork("https://example.com/art.jpg"))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Test
|
|
48
|
+
fun missingArtwork_doesNotLoadRemote() {
|
|
49
|
+
assertFalse(GlideBitmapLoader.shouldLoadRemoteArtwork(null))
|
|
50
|
+
assertFalse(GlideBitmapLoader.shouldLoadRemoteArtwork(""))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Test
|
|
54
|
+
fun loadBitmapFromMetadata_returnsNullWhenNoArtwork() {
|
|
55
|
+
val metadata = MediaMetadata.Builder().setTitle("Lecture").build()
|
|
56
|
+
assertNull(loader.loadBitmapFromMetadata(metadata))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Test
|
|
60
|
+
fun loadBitmapFromMetadata_returnsNullForEmptyArtworkBytes() {
|
|
61
|
+
val metadata = MediaMetadata.Builder()
|
|
62
|
+
.setArtworkData(ByteArray(0), MediaMetadata.PICTURE_TYPE_FRONT_COVER)
|
|
63
|
+
.build()
|
|
64
|
+
assertNull(loader.loadBitmapFromMetadata(metadata))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Test
|
|
68
|
+
fun decodeBitmap_nonImageBytes_completesWithoutHanging() {
|
|
69
|
+
val future = loader.decodeBitmap(byteArrayOf(1, 2, 3, 4))
|
|
70
|
+
try {
|
|
71
|
+
future.get(2, TimeUnit.SECONDS)
|
|
72
|
+
} catch (e: ExecutionException) {
|
|
73
|
+
assertTrue(e.cause is IllegalStateException)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|