@rntp/player 5.0.0-beta.3 → 5.0.0-beta.5

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 (61) hide show
  1. package/android/build.gradle +7 -0
  2. package/android/src/main/java/com/doublesymmetry/trackplayer/SleepTimerController.kt +128 -0
  3. package/android/src/main/java/com/doublesymmetry/trackplayer/TrackPlayerModule.kt +40 -0
  4. package/android/src/main/java/com/doublesymmetry/trackplayer/TrackPlayerPlaybackService.kt +107 -87
  5. package/android/src/main/java/com/doublesymmetry/trackplayer/models/BrowseTree.kt +51 -20
  6. package/android/src/main/java/com/doublesymmetry/trackplayer/models/PlayerConfig.kt +12 -1
  7. package/android/src/test/java/com/doublesymmetry/trackplayer/ExoPlayerIntegrationTest.kt +319 -0
  8. package/android/src/test/java/com/doublesymmetry/trackplayer/SleepTimerIntegrationTest.kt +473 -0
  9. package/android/src/test/java/com/doublesymmetry/trackplayer/SleepTimerStateTest.kt +58 -0
  10. package/android/src/test/java/com/doublesymmetry/trackplayer/models/BrowseNavigationTest.kt +215 -0
  11. package/android/src/test/java/com/doublesymmetry/trackplayer/models/BrowseTreeTest.kt +166 -0
  12. package/android/src/test/java/com/doublesymmetry/trackplayer/models/EmitEventTest.kt +68 -0
  13. package/android/src/test/java/com/doublesymmetry/trackplayer/models/PlayerConfigTest.kt +400 -0
  14. package/android/src/test/java/com/doublesymmetry/trackplayer/models/TrackPlayerMediaItemTest.kt +380 -0
  15. package/android/src/test/resources/robolectric.properties +1 -0
  16. package/ios/CarPlay/RNTPCarPlaySceneDelegate.swift +43 -14
  17. package/ios/TrackPlayer.swift +46 -101
  18. package/ios/TrackPlayerBridge.mm +2 -0
  19. package/ios/player/AVPlayerEngine.swift +46 -32
  20. package/ios/player/AudioCache.swift +34 -0
  21. package/ios/player/AudioPlayer.swift +36 -21
  22. package/ios/player/CacheProxyServer.swift +429 -0
  23. package/ios/player/DownloadCoordinator.swift +242 -0
  24. package/ios/player/Preloader.swift +21 -90
  25. package/ios/player/SleepTimerController.swift +147 -0
  26. package/ios/tests/AVPlayerEngineIntegrationTests.swift +230 -0
  27. package/ios/tests/AudioPlayerTests.swift +6 -0
  28. package/ios/tests/CacheProxyServerTests.swift +403 -0
  29. package/ios/tests/DownloadCoordinatorTests.swift +197 -0
  30. package/ios/tests/LocalAudioServer.swift +171 -0
  31. package/ios/tests/MockPlayerEngine.swift +1 -0
  32. package/ios/tests/QueueManagerTests.swift +6 -0
  33. package/ios/tests/SleepTimerIntegrationTests.swift +408 -0
  34. package/ios/tests/SleepTimerTests.swift +70 -0
  35. package/lib/commonjs/NativeTrackPlayer.js.map +1 -1
  36. package/lib/commonjs/audio.js +39 -4
  37. package/lib/commonjs/audio.js.map +1 -1
  38. package/lib/commonjs/interfaces/PlayerConfig.js +1 -1
  39. package/lib/commonjs/interfaces/PlayerConfig.js.map +1 -1
  40. package/lib/module/NativeTrackPlayer.js.map +1 -1
  41. package/lib/module/audio.js +37 -4
  42. package/lib/module/audio.js.map +1 -1
  43. package/lib/module/interfaces/PlayerConfig.js +1 -1
  44. package/lib/module/interfaces/PlayerConfig.js.map +1 -1
  45. package/lib/typescript/src/NativeTrackPlayer.d.ts +2 -0
  46. package/lib/typescript/src/NativeTrackPlayer.d.ts.map +1 -1
  47. package/lib/typescript/src/audio.d.ts +16 -4
  48. package/lib/typescript/src/audio.d.ts.map +1 -1
  49. package/lib/typescript/src/interfaces/BrowseTree.d.ts +35 -5
  50. package/lib/typescript/src/interfaces/BrowseTree.d.ts.map +1 -1
  51. package/lib/typescript/src/interfaces/MediaItem.d.ts +4 -1
  52. package/lib/typescript/src/interfaces/MediaItem.d.ts.map +1 -1
  53. package/lib/typescript/src/interfaces/PlayerConfig.d.ts +19 -2
  54. package/lib/typescript/src/interfaces/PlayerConfig.d.ts.map +1 -1
  55. package/package.json +4 -1
  56. package/src/NativeTrackPlayer.ts +4 -0
  57. package/src/audio.ts +37 -4
  58. package/src/interfaces/BrowseTree.ts +40 -5
  59. package/src/interfaces/MediaItem.ts +4 -1
  60. package/src/interfaces/PlayerConfig.ts +22 -3
  61. package/ios/player/CachingResourceLoader.swift +0 -273
@@ -0,0 +1,215 @@
1
+ package com.doublesymmetry.trackplayer.models
2
+
3
+ import org.junit.Test
4
+ import org.junit.Assert.*
5
+ import org.junit.runner.RunWith
6
+ import org.robolectric.RobolectricTestRunner
7
+
8
+ @RunWith(RobolectricTestRunner::class)
9
+ class BrowseNavigationTest {
10
+
11
+ // Helper to build a tree with two categories, each having different item types
12
+ private fun buildTree(): BrowseTree {
13
+ return BrowseTree(listOf(
14
+ BrowseCategory(
15
+ mediaId = "cat-music",
16
+ title = "Music",
17
+ items = listOf(
18
+ BrowseMediaItem(
19
+ mediaId = "track-1",
20
+ url = "https://example.com/track1.mp3",
21
+ title = "Track 1",
22
+ artist = "Artist A",
23
+ artworkUrl = "https://example.com/art.jpg",
24
+ ),
25
+ BrowseMediaItem(
26
+ mediaId = "album-1",
27
+ title = "Album",
28
+ children = listOf(
29
+ BrowseMediaItem(
30
+ mediaId = "track-2",
31
+ url = "https://example.com/track2.mp3",
32
+ title = "Track 2",
33
+ artist = "Artist B",
34
+ )
35
+ ),
36
+ ),
37
+ ),
38
+ ),
39
+ BrowseCategory(
40
+ mediaId = "cat-podcasts",
41
+ title = "Podcasts",
42
+ items = listOf(
43
+ BrowseMediaItem(
44
+ mediaId = "episode-1",
45
+ url = "https://example.com/ep1.mp3",
46
+ title = "Episode 1",
47
+ )
48
+ ),
49
+ ),
50
+ ))
51
+ }
52
+
53
+ // --- Simulating onGetChildren root logic ---
54
+ // Root: return categories mapped to browsable MediaItems
55
+
56
+ @Test
57
+ fun `root returns all categories as browsable items`() {
58
+ val tree = buildTree()
59
+
60
+ // Simulate what onGetChildren("root") does: map categories to MediaItems
61
+ val items = tree.categories.map { cat ->
62
+ BrowseMediaItem(
63
+ mediaId = cat.mediaId,
64
+ title = cat.title,
65
+ children = cat.items, // presence of children makes it browsable
66
+ ).toMediaItem()
67
+ }
68
+
69
+ assertEquals(2, items.size)
70
+ items.forEach { item ->
71
+ assertEquals(true, item.mediaMetadata.isBrowsable)
72
+ assertEquals(false, item.mediaMetadata.isPlayable)
73
+ }
74
+ assertEquals("cat-music", items[0].mediaId)
75
+ assertEquals("cat-podcasts", items[1].mediaId)
76
+ assertEquals("Music", items[0].mediaMetadata.title.toString())
77
+ }
78
+
79
+ // --- Simulating onGetChildren for a category parentId ---
80
+
81
+ @Test
82
+ fun `category returns its direct children`() {
83
+ val tree = buildTree()
84
+
85
+ val category = tree.findCategory("cat-music")
86
+ assertNotNull(category)
87
+
88
+ val items = category!!.items.map { it.toMediaItem() }
89
+ assertEquals(2, items.size)
90
+
91
+ // First child is a playable track
92
+ val track = items[0]
93
+ assertEquals("track-1", track.mediaId)
94
+ assertEquals(true, track.mediaMetadata.isPlayable)
95
+ assertEquals(false, track.mediaMetadata.isBrowsable)
96
+
97
+ // Second child is a browsable album
98
+ val album = items[1]
99
+ assertEquals("album-1", album.mediaId)
100
+ assertEquals(false, album.mediaMetadata.isPlayable)
101
+ assertEquals(true, album.mediaMetadata.isBrowsable)
102
+ }
103
+
104
+ // --- Simulating onGetChildren for a browsable item parentId ---
105
+
106
+ @Test
107
+ fun `browsable item returns its children`() {
108
+ val tree = buildTree()
109
+
110
+ val item = tree.findItem("album-1")
111
+ assertNotNull(item)
112
+ assertNotNull(item!!.children)
113
+
114
+ val children = item.children!!.map { it.toMediaItem() }
115
+ assertEquals(1, children.size)
116
+ assertEquals("track-2", children[0].mediaId)
117
+ assertEquals(true, children[0].mediaMetadata.isPlayable)
118
+ }
119
+
120
+ @Test
121
+ fun `unknown parentId returns null for both lookups`() {
122
+ val tree = buildTree()
123
+
124
+ val category = tree.findCategory("nonexistent")
125
+ val item = tree.findItem("nonexistent")
126
+
127
+ assertNull(category)
128
+ assertNull(item)
129
+ }
130
+
131
+ @Test
132
+ fun `playable items in category have correct metadata`() {
133
+ val tree = buildTree()
134
+
135
+ val category = tree.findCategory("cat-music")!!
136
+ val playableItem = category.items.first { it.isPlayable }
137
+ val mediaItem = playableItem.toMediaItem()
138
+
139
+ assertEquals("track-1", mediaItem.mediaId)
140
+ assertEquals("Track 1", mediaItem.mediaMetadata.title.toString())
141
+ assertEquals("Artist A", mediaItem.mediaMetadata.artist.toString())
142
+ assertNotNull(mediaItem.localConfiguration?.uri)
143
+ assertEquals("https://example.com/track1.mp3", mediaItem.localConfiguration!!.uri.toString())
144
+ assertEquals(true, mediaItem.mediaMetadata.isPlayable)
145
+ assertEquals(false, mediaItem.mediaMetadata.isBrowsable)
146
+ }
147
+
148
+ @Test
149
+ fun `browsable items have no URI and isBrowsable is true`() {
150
+ val tree = buildTree()
151
+
152
+ val category = tree.findCategory("cat-music")!!
153
+ val browsableItem = category.items.first { it.isBrowsable }
154
+ val mediaItem = browsableItem.toMediaItem()
155
+
156
+ assertEquals("album-1", mediaItem.mediaId)
157
+ assertEquals(true, mediaItem.mediaMetadata.isBrowsable)
158
+ assertEquals(false, mediaItem.mediaMetadata.isPlayable)
159
+ // Browsable items have no local configuration (no URI set)
160
+ assertNull(mediaItem.localConfiguration)
161
+ }
162
+
163
+ @Test
164
+ fun `category lookup works across multiple categories`() {
165
+ val tree = buildTree()
166
+
167
+ val podcasts = tree.findCategory("cat-podcasts")
168
+ assertNotNull(podcasts)
169
+ assertEquals("Podcasts", podcasts!!.title)
170
+ assertEquals(1, podcasts.items.size)
171
+ assertEquals("episode-1", podcasts.items[0].mediaId)
172
+ }
173
+
174
+ @Test
175
+ fun `toMediaItem sets correct artwork URI for playable items`() {
176
+ val item = BrowseMediaItem(
177
+ mediaId = "t1",
178
+ url = "https://example.com/t.mp3",
179
+ artworkUrl = "https://example.com/art.jpg",
180
+ )
181
+ val mediaItem = item.toMediaItem()
182
+
183
+ assertNotNull(mediaItem.mediaMetadata.artworkUri)
184
+ assertEquals("https://example.com/art.jpg", mediaItem.mediaMetadata.artworkUri.toString())
185
+ }
186
+
187
+ @Test
188
+ fun `toMediaItem stores duration and isLive in extras`() {
189
+ val item = BrowseMediaItem(
190
+ mediaId = "live-1",
191
+ url = "https://example.com/live.mp3",
192
+ duration = 3600.0,
193
+ isLive = true,
194
+ )
195
+ val mediaItem = item.toMediaItem()
196
+
197
+ val extras = mediaItem.mediaMetadata.extras
198
+ assertNotNull(extras)
199
+ assertEquals(3600.0, extras!!.getDouble("duration"), 0.001)
200
+ assertTrue(extras.getBoolean("isLive"))
201
+ // Live items get a LiveConfiguration
202
+ assertNotNull(mediaItem.liveConfiguration)
203
+ }
204
+
205
+ @Test
206
+ fun `empty category returns empty children list`() {
207
+ val tree = BrowseTree(listOf(
208
+ BrowseCategory(mediaId = "empty-cat", title = "Empty", items = emptyList())
209
+ ))
210
+
211
+ val category = tree.findCategory("empty-cat")
212
+ assertNotNull(category)
213
+ assertTrue(category!!.items.isEmpty())
214
+ }
215
+ }
@@ -0,0 +1,166 @@
1
+ package com.doublesymmetry.trackplayer.models
2
+
3
+ import org.junit.Test
4
+ import org.junit.Assert.*
5
+ import org.junit.runner.RunWith
6
+ import org.robolectric.RobolectricTestRunner
7
+ import org.robolectric.RuntimeEnvironment
8
+
9
+ @RunWith(RobolectricTestRunner::class)
10
+ class BrowseTreeTest {
11
+
12
+ @Test
13
+ fun `store and load round-trip preserves data`() {
14
+ val tree = BrowseTree(listOf(
15
+ BrowseCategory("cat-1", "Music", listOf(
16
+ BrowseMediaItem(mediaId = "t1", url = "https://example.com/t1.mp3", title = "Track 1")
17
+ ))
18
+ ))
19
+ val context = RuntimeEnvironment.getApplication()
20
+ tree.store(context)
21
+ val loaded = BrowseTree.load(context)
22
+
23
+ assertEquals(1, loaded.categories.size)
24
+ assertEquals("Music", loaded.categories[0].title)
25
+ assertEquals("t1", loaded.categories[0].items[0].mediaId)
26
+ }
27
+
28
+ @Test
29
+ fun `load with corrupt JSON returns empty tree`() {
30
+ val context = RuntimeEnvironment.getApplication()
31
+ val prefs = context.getSharedPreferences("TrackPlayerPrefs", 0)
32
+ prefs.edit().putString("browse_tree", "not valid json{{{").apply()
33
+
34
+ val tree = BrowseTree.load(context)
35
+ assertTrue(tree.categories.isEmpty())
36
+ }
37
+
38
+ @Test
39
+ fun `load with no saved data returns empty tree`() {
40
+ val context = RuntimeEnvironment.getApplication()
41
+ val tree = BrowseTree.load(context)
42
+ assertTrue(tree.categories.isEmpty())
43
+ }
44
+
45
+ @Test
46
+ fun `findCategory returns matching category`() {
47
+ val tree = BrowseTree(listOf(
48
+ BrowseCategory("cat-1", "Music", emptyList()),
49
+ BrowseCategory("cat-2", "Podcasts", emptyList()),
50
+ ))
51
+ assertEquals("Podcasts", tree.findCategory("cat-2")?.title)
52
+ }
53
+
54
+ @Test
55
+ fun `findCategory returns null for unknown id`() {
56
+ val tree = BrowseTree(listOf(BrowseCategory("cat-1", "Music", emptyList())))
57
+ assertNull(tree.findCategory("unknown"))
58
+ }
59
+
60
+ @Test
61
+ fun `findItem finds top-level item`() {
62
+ val tree = BrowseTree(listOf(
63
+ BrowseCategory("cat-1", "Music", listOf(
64
+ BrowseMediaItem(mediaId = "t1", url = "a.mp3", title = "Track 1"),
65
+ BrowseMediaItem(mediaId = "t2", url = "b.mp3", title = "Track 2"),
66
+ ))
67
+ ))
68
+ assertEquals("Track 2", tree.findItem("t2")?.title)
69
+ }
70
+
71
+ @Test
72
+ fun `findItem finds deeply nested item`() {
73
+ val tree = BrowseTree(listOf(
74
+ BrowseCategory("cat-1", "Music", listOf(
75
+ BrowseMediaItem(mediaId = "album-1", title = "Album", children = listOf(
76
+ BrowseMediaItem(mediaId = "disc-1", title = "Disc 1", children = listOf(
77
+ BrowseMediaItem(mediaId = "deep-track", url = "deep.mp3", title = "Deep Track")
78
+ ))
79
+ ))
80
+ ))
81
+ ))
82
+ val found = tree.findItem("deep-track")
83
+ assertEquals("Deep Track", found?.title)
84
+ assertEquals("deep.mp3", found?.url)
85
+ }
86
+
87
+ @Test
88
+ fun `findItem returns null for unknown id`() {
89
+ val tree = BrowseTree(listOf(
90
+ BrowseCategory("cat-1", "Music", listOf(
91
+ BrowseMediaItem(mediaId = "t1", url = "a.mp3")
92
+ ))
93
+ ))
94
+ assertNull(tree.findItem("nonexistent"))
95
+ }
96
+
97
+ @Test
98
+ fun `isPlayable is true when url is present`() {
99
+ val item = BrowseMediaItem(mediaId = "t1", url = "a.mp3")
100
+ assertTrue(item.isPlayable)
101
+ assertFalse(item.isBrowsable)
102
+ }
103
+
104
+ @Test
105
+ fun `isBrowsable is true when children present and no url`() {
106
+ val item = BrowseMediaItem(mediaId = "folder", children = listOf(
107
+ BrowseMediaItem(mediaId = "t1", url = "a.mp3")
108
+ ))
109
+ assertTrue(item.isBrowsable)
110
+ assertFalse(item.isPlayable)
111
+ }
112
+
113
+ @Test
114
+ fun `url takes precedence over children for playability`() {
115
+ val item = BrowseMediaItem(mediaId = "t1", url = "a.mp3", children = listOf(
116
+ BrowseMediaItem(mediaId = "t2", url = "b.mp3")
117
+ ))
118
+ assertTrue(item.isPlayable)
119
+ assertFalse(item.isBrowsable)
120
+ }
121
+
122
+ @Test
123
+ fun `toMediaItem sets playable metadata for track`() {
124
+ val item = BrowseMediaItem(
125
+ mediaId = "t1", url = "https://example.com/t1.mp3",
126
+ title = "Song", artist = "Artist",
127
+ )
128
+ val mediaItem = item.toMediaItem()
129
+ assertEquals("t1", mediaItem.mediaId)
130
+ assertEquals("Song", mediaItem.mediaMetadata.title.toString())
131
+ assertEquals(true, mediaItem.mediaMetadata.isPlayable)
132
+ assertEquals(false, mediaItem.mediaMetadata.isBrowsable)
133
+ }
134
+
135
+ @Test
136
+ fun `toMediaItem sets browsable metadata for folder`() {
137
+ val item = BrowseMediaItem(
138
+ mediaId = "album-1", title = "Album",
139
+ children = listOf(BrowseMediaItem(mediaId = "t1", url = "a.mp3")),
140
+ )
141
+ val mediaItem = item.toMediaItem()
142
+ assertEquals(false, mediaItem.mediaMetadata.isPlayable)
143
+ assertEquals(true, mediaItem.mediaMetadata.isBrowsable)
144
+ }
145
+
146
+ @Test
147
+ fun `store and load preserves nested children`() {
148
+ val tree = BrowseTree(listOf(
149
+ BrowseCategory("cat-1", "Music", listOf(
150
+ BrowseMediaItem(mediaId = "album", title = "Album", children = listOf(
151
+ BrowseMediaItem(mediaId = "t1", url = "a.mp3", title = "Track 1"),
152
+ BrowseMediaItem(mediaId = "t2", url = "b.mp3", title = "Track 2"),
153
+ ))
154
+ ))
155
+ ))
156
+ val context = RuntimeEnvironment.getApplication()
157
+ tree.store(context)
158
+ val loaded = BrowseTree.load(context)
159
+
160
+ val album = loaded.categories[0].items[0]
161
+ assertEquals("album", album.mediaId)
162
+ assertNotNull(album.children)
163
+ assertEquals(2, album.children!!.size)
164
+ assertEquals("t1", album.children!![0].mediaId)
165
+ }
166
+ }
@@ -0,0 +1,68 @@
1
+ package com.doublesymmetry.trackplayer.models
2
+
3
+ import org.junit.Test
4
+ import org.junit.Assert.*
5
+
6
+ class EmitEventTest {
7
+
8
+ @Test
9
+ fun `PlaybackStateChangedEvent produces correct pairs`() {
10
+ val event = PlaybackStateChangedEvent("ready")
11
+ assertEquals(EmitEventType.PLAYBACK_STATE_CHANGED, event.type)
12
+ assertEquals(1, event.pairs().size)
13
+ assertEquals("state" to "ready", event.pairs()[0])
14
+ }
15
+
16
+ @Test
17
+ fun `IsPlayingChangedEvent produces correct pairs`() {
18
+ val event = IsPlayingChangedEvent(true)
19
+ assertEquals("playing" to true, event.pairs()[0])
20
+ }
21
+
22
+ @Test
23
+ fun `MediaItemTransitionEvent with null item omits item`() {
24
+ val event = MediaItemTransitionEvent(null, 3)
25
+ assertEquals(1, event.pairs().size)
26
+ assertEquals("index" to 3, event.pairs()[0])
27
+ }
28
+
29
+ @Test
30
+ fun `MediaItemTransitionEvent with item includes both`() {
31
+ val event = MediaItemTransitionEvent("item-data", 0)
32
+ assertEquals(2, event.pairs().size)
33
+ assertTrue(event.pairs().any { it.first == "index" && it.second == 0 })
34
+ assertTrue(event.pairs().any { it.first == "item" && it.second == "item-data" })
35
+ }
36
+
37
+ @Test
38
+ fun `SleepTimerTriggeredEvent produces correct pairs`() {
39
+ val event = SleepTimerTriggeredEvent("time")
40
+ assertEquals(EmitEventType.SLEEP_TIMER_TRIGGERED, event.type)
41
+ assertEquals("type" to "time", event.pairs()[0])
42
+ }
43
+
44
+ @Test
45
+ fun `PlaybackErrorEvent produces code and message`() {
46
+ val event = PlaybackErrorEvent("network", "Connection failed")
47
+ assertTrue(event.pairs().any { it.first == "code" && it.second == "network" })
48
+ assertTrue(event.pairs().any { it.first == "message" && it.second == "Connection failed" })
49
+ }
50
+
51
+ @Test
52
+ fun `MetadataReceivedEvent with partial fields only includes non-null`() {
53
+ val event = MetadataReceivedEvent(title = "Song", artist = null)
54
+ assertEquals(1, event.pairs().size)
55
+ assertEquals("title" to "Song", event.pairs()[0])
56
+ }
57
+
58
+ @Test
59
+ fun `QueueChangedEvent has empty pairs`() {
60
+ assertEquals(0, QueueChangedEvent().pairs().size)
61
+ }
62
+
63
+ @Test
64
+ fun `all EmitEventType values have unique string values`() {
65
+ val values = EmitEventType.entries.map { it.value }
66
+ assertEquals(values.size, values.toSet().size)
67
+ }
68
+ }