@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,400 @@
1
+ package com.doublesymmetry.trackplayer.models
2
+
3
+ import com.facebook.react.bridge.ReadableArray
4
+ import com.facebook.react.bridge.ReadableMap
5
+ import com.facebook.react.bridge.ReadableMapKeySetIterator
6
+ import io.mockk.every
7
+ import io.mockk.mockk
8
+ import org.junit.Test
9
+ import org.junit.Assert.*
10
+ import org.junit.runner.RunWith
11
+ import org.robolectric.RobolectricTestRunner
12
+ import org.robolectric.RuntimeEnvironment
13
+
14
+ @RunWith(RobolectricTestRunner::class)
15
+ class PlayerConfigTest {
16
+
17
+ // ---- Default config ----
18
+
19
+ @Test
20
+ fun `default config has sensible defaults`() {
21
+ val config = PlayerConfig()
22
+
23
+ assertEquals("music", config.contentType)
24
+ assertTrue(config.handleAudioBecomingNoisy)
25
+ assertEquals(WakeMode.NONE, config.wakeMode)
26
+ assertFalse(config.skipSilenceEnabled)
27
+ assertEquals(listOf(PlayerCommand.PLAY_PAUSE), config.availableCommands)
28
+ assertEquals(RemoteControlHandling.NATIVE, config.remoteControlHandling)
29
+ assertTrue(config.perCommandHandling.isEmpty())
30
+ assertEquals(15L, config.forwardInterval)
31
+ assertEquals(15L, config.backwardInterval)
32
+ assertNull(config.notificationChannelId)
33
+ assertNull(config.notificationChannelName)
34
+ assertNull(config.notificationSmallIcon)
35
+ assertNull(config.cacheMaxSizeBytes)
36
+ assertNull(config.castReceiverAppId)
37
+ assertEquals(0.0, config.progressSyncIntervalSeconds, 0.001)
38
+ assertNull(config.progressSyncHttpUrl)
39
+ assertNull(config.progressSyncHttpHeaders)
40
+ }
41
+
42
+ // ---- Store and load round-trip ----
43
+
44
+ @Test
45
+ fun `store and load round-trip preserves config`() {
46
+ val context = RuntimeEnvironment.getApplication()
47
+
48
+ val config = PlayerConfig(
49
+ contentType = "speech",
50
+ handleAudioBecomingNoisy = false,
51
+ wakeMode = WakeMode.NETWORK,
52
+ skipSilenceEnabled = true,
53
+ forwardInterval = 30L,
54
+ backwardInterval = 10L,
55
+ notificationChannelId = "my-channel",
56
+ notificationChannelName = "My Player",
57
+ notificationSmallIcon = "ic_notification",
58
+ cacheMaxSizeBytes = 50L * 1024 * 1024,
59
+ castReceiverAppId = "ABCD1234",
60
+ progressSyncIntervalSeconds = 5.0,
61
+ progressSyncHttpUrl = "https://example.com/progress",
62
+ )
63
+
64
+ config.store(context)
65
+ val loaded = config.load(context)
66
+
67
+ assertEquals("speech", loaded.contentType)
68
+ assertFalse(loaded.handleAudioBecomingNoisy)
69
+ assertEquals(WakeMode.NETWORK, loaded.wakeMode)
70
+ assertTrue(loaded.skipSilenceEnabled)
71
+ assertEquals(30L, loaded.forwardInterval)
72
+ assertEquals(10L, loaded.backwardInterval)
73
+ assertEquals("my-channel", loaded.notificationChannelId)
74
+ assertEquals("My Player", loaded.notificationChannelName)
75
+ assertEquals("ic_notification", loaded.notificationSmallIcon)
76
+ assertEquals(50L * 1024 * 1024, loaded.cacheMaxSizeBytes)
77
+ assertEquals("ABCD1234", loaded.castReceiverAppId)
78
+ assertEquals(5.0, loaded.progressSyncIntervalSeconds, 0.001)
79
+ assertEquals("https://example.com/progress", loaded.progressSyncHttpUrl)
80
+ }
81
+
82
+ @Test
83
+ fun `load with no stored data returns default config`() {
84
+ val context = RuntimeEnvironment.getApplication()
85
+ // Ensure a fresh prefs state by using a unique pref name
86
+ val prefs = context.getSharedPreferences("TrackPlayerPrefs", 0)
87
+ prefs.edit().remove("player_config").apply()
88
+
89
+ val loaded = PlayerConfig().load(context)
90
+ assertEquals("music", loaded.contentType)
91
+ assertEquals(WakeMode.NONE, loaded.wakeMode)
92
+ }
93
+
94
+ // ---- fromReadableMap ----
95
+
96
+ @Test
97
+ fun `fromReadableMap parses contentType`() {
98
+ val map = mockk<ReadableMap>(relaxed = true) {
99
+ every { getString("contentType") } returns "speech"
100
+ every { getMap("android") } returns null
101
+ every { getMap("cache") } returns null
102
+ every { getMap("progressSync") } returns null
103
+ every { hasKey(any()) } returns false
104
+ }
105
+
106
+ val config = PlayerConfig.fromReadableMap(map)
107
+ assertEquals("speech", config.contentType)
108
+ }
109
+
110
+ @Test
111
+ fun `fromReadableMap defaults contentType to music when absent`() {
112
+ val map = mockk<ReadableMap>(relaxed = true) {
113
+ every { getString("contentType") } returns null
114
+ every { getMap("android") } returns null
115
+ every { getMap("cache") } returns null
116
+ every { getMap("progressSync") } returns null
117
+ every { hasKey(any()) } returns false
118
+ }
119
+
120
+ val config = PlayerConfig.fromReadableMap(map)
121
+ assertEquals("music", config.contentType)
122
+ }
123
+
124
+ @Test
125
+ fun `fromReadableMap parses android-specific config`() {
126
+ val androidMap = mockk<ReadableMap>(relaxed = true) {
127
+ every { getString("wakeMode") } returns "network"
128
+ every { hasKey("skipSilenceEnabled") } returns true
129
+ every { getBoolean("skipSilenceEnabled") } returns true
130
+ every { hasKey("cast") } returns true
131
+ every { getString("cast") } returns "RECV_APP_ID"
132
+ every { getMap("notification") } returns null
133
+ }
134
+
135
+ val map = mockk<ReadableMap>(relaxed = true) {
136
+ every { getString("contentType") } returns null
137
+ every { getMap("android") } returns androidMap
138
+ every { getMap("cache") } returns null
139
+ every { getMap("progressSync") } returns null
140
+ every { hasKey(any()) } returns false
141
+ }
142
+
143
+ val config = PlayerConfig.fromReadableMap(map)
144
+ assertEquals(WakeMode.NETWORK, config.wakeMode)
145
+ assertTrue(config.skipSilenceEnabled)
146
+ assertEquals("RECV_APP_ID", config.castReceiverAppId)
147
+ }
148
+
149
+ @Test
150
+ fun `fromReadableMap parses notification fields`() {
151
+ val notifMap = mockk<ReadableMap>(relaxed = true) {
152
+ every { getString("channelId") } returns "rntp-channel"
153
+ every { getString("channelName") } returns "Music Player"
154
+ every { getString("smallIcon") } returns "ic_music"
155
+ }
156
+
157
+ val androidMap = mockk<ReadableMap>(relaxed = true) {
158
+ every { getString("wakeMode") } returns null
159
+ every { hasKey("skipSilenceEnabled") } returns false
160
+ every { hasKey("cast") } returns false
161
+ every { getMap("notification") } returns notifMap
162
+ }
163
+
164
+ val map = mockk<ReadableMap>(relaxed = true) {
165
+ every { getString("contentType") } returns null
166
+ every { getMap("android") } returns androidMap
167
+ every { getMap("cache") } returns null
168
+ every { getMap("progressSync") } returns null
169
+ every { hasKey(any()) } returns false
170
+ }
171
+
172
+ val config = PlayerConfig.fromReadableMap(map)
173
+ assertEquals("rntp-channel", config.notificationChannelId)
174
+ assertEquals("Music Player", config.notificationChannelName)
175
+ assertEquals("ic_music", config.notificationSmallIcon)
176
+ }
177
+
178
+ @Test
179
+ fun `fromReadableMap parses cache config`() {
180
+ val cacheMap = mockk<ReadableMap>(relaxed = true) {
181
+ every { hasKey("maxSizeBytes") } returns true
182
+ every { getDouble("maxSizeBytes") } returns (200.0 * 1024 * 1024)
183
+ }
184
+
185
+ val map = mockk<ReadableMap>(relaxed = true) {
186
+ every { getString("contentType") } returns null
187
+ every { getMap("android") } returns null
188
+ every { getMap("cache") } returns cacheMap
189
+ every { getMap("progressSync") } returns null
190
+ every { hasKey(any()) } returns false
191
+ }
192
+
193
+ val config = PlayerConfig.fromReadableMap(map)
194
+ assertEquals(200L * 1024 * 1024, config.cacheMaxSizeBytes)
195
+ }
196
+
197
+ @Test
198
+ fun `fromReadableMap uses default cache size when maxSizeBytes absent`() {
199
+ val cacheMap = mockk<ReadableMap>(relaxed = true) {
200
+ every { hasKey("maxSizeBytes") } returns false
201
+ }
202
+
203
+ val map = mockk<ReadableMap>(relaxed = true) {
204
+ every { getString("contentType") } returns null
205
+ every { getMap("android") } returns null
206
+ every { getMap("cache") } returns cacheMap
207
+ every { getMap("progressSync") } returns null
208
+ every { hasKey(any()) } returns false
209
+ }
210
+
211
+ val config = PlayerConfig.fromReadableMap(map)
212
+ // Default cache size is 500MB
213
+ assertEquals(500L * 1024 * 1024, config.cacheMaxSizeBytes)
214
+ }
215
+
216
+ @Test
217
+ fun `fromReadableMap parses wakeMode none`() {
218
+ val androidMap = mockk<ReadableMap>(relaxed = true) {
219
+ every { getString("wakeMode") } returns "none"
220
+ every { hasKey("skipSilenceEnabled") } returns false
221
+ every { hasKey("cast") } returns false
222
+ every { getMap("notification") } returns null
223
+ }
224
+
225
+ val map = mockk<ReadableMap>(relaxed = true) {
226
+ every { getString("contentType") } returns null
227
+ every { getMap("android") } returns androidMap
228
+ every { getMap("cache") } returns null
229
+ every { getMap("progressSync") } returns null
230
+ every { hasKey(any()) } returns false
231
+ }
232
+
233
+ val config = PlayerConfig.fromReadableMap(map)
234
+ assertEquals(WakeMode.NONE, config.wakeMode)
235
+ }
236
+
237
+ @Test
238
+ fun `fromReadableMap parses wakeMode local`() {
239
+ val androidMap = mockk<ReadableMap>(relaxed = true) {
240
+ every { getString("wakeMode") } returns "local"
241
+ every { hasKey("skipSilenceEnabled") } returns false
242
+ every { hasKey("cast") } returns false
243
+ every { getMap("notification") } returns null
244
+ }
245
+
246
+ val map = mockk<ReadableMap>(relaxed = true) {
247
+ every { getString("contentType") } returns null
248
+ every { getMap("android") } returns androidMap
249
+ every { getMap("cache") } returns null
250
+ every { getMap("progressSync") } returns null
251
+ every { hasKey(any()) } returns false
252
+ }
253
+
254
+ val config = PlayerConfig.fromReadableMap(map)
255
+ assertEquals(WakeMode.LOCAL, config.wakeMode)
256
+ }
257
+
258
+ // ---- withCommands (command handling modes) ----
259
+
260
+ @Test
261
+ fun `withCommands sets native handling`() {
262
+ val commandsMap = mockk<ReadableMap>(relaxed = true) {
263
+ every { getString("handling") } returns "native"
264
+ every { hasKey("capabilities") } returns false
265
+ every { hasKey("perCommandHandling") } returns false
266
+ every { hasKey("forwardInterval") } returns false
267
+ every { hasKey("backwardInterval") } returns false
268
+ }
269
+
270
+ val config = PlayerConfig().withCommands(commandsMap)
271
+ assertEquals(RemoteControlHandling.NATIVE, config.remoteControlHandling)
272
+ }
273
+
274
+ @Test
275
+ fun `withCommands sets js handling`() {
276
+ val commandsMap = mockk<ReadableMap>(relaxed = true) {
277
+ every { getString("handling") } returns "js"
278
+ every { hasKey("capabilities") } returns false
279
+ every { hasKey("perCommandHandling") } returns false
280
+ every { hasKey("forwardInterval") } returns false
281
+ every { hasKey("backwardInterval") } returns false
282
+ }
283
+
284
+ val config = PlayerConfig().withCommands(commandsMap)
285
+ assertEquals(RemoteControlHandling.JS, config.remoteControlHandling)
286
+ }
287
+
288
+ @Test
289
+ fun `withCommands sets hybrid handling`() {
290
+ val commandsMap = mockk<ReadableMap>(relaxed = true) {
291
+ every { getString("handling") } returns "hybrid"
292
+ every { hasKey("capabilities") } returns false
293
+ every { hasKey("perCommandHandling") } returns false
294
+ every { hasKey("forwardInterval") } returns false
295
+ every { hasKey("backwardInterval") } returns false
296
+ }
297
+
298
+ val config = PlayerConfig().withCommands(commandsMap)
299
+ assertEquals(RemoteControlHandling.HYBRID, config.remoteControlHandling)
300
+ }
301
+
302
+ @Test
303
+ fun `withCommands parses perCommandHandling for hybrid mode`() {
304
+ val perCommandIterator = mockk<ReadableMapKeySetIterator> {
305
+ every { hasNextKey() } returnsMany listOf(true, true, false)
306
+ every { nextKey() } returnsMany listOf("next", "previous")
307
+ }
308
+
309
+ val perCommandMap = mockk<ReadableMap>(relaxed = true) {
310
+ every { keySetIterator() } returns perCommandIterator
311
+ every { getString("next") } returns "js"
312
+ every { getString("previous") } returns "native"
313
+ }
314
+
315
+ val commandsMap = mockk<ReadableMap>(relaxed = true) {
316
+ every { getString("handling") } returns "hybrid"
317
+ every { hasKey("capabilities") } returns false
318
+ every { hasKey("perCommandHandling") } returns true
319
+ every { getMap("perCommandHandling") } returns perCommandMap
320
+ every { hasKey("forwardInterval") } returns false
321
+ every { hasKey("backwardInterval") } returns false
322
+ }
323
+
324
+ val config = PlayerConfig().withCommands(commandsMap)
325
+ assertEquals(RemoteControlHandling.HYBRID, config.remoteControlHandling)
326
+ assertEquals(RemoteControlHandling.JS, config.perCommandHandling["next"])
327
+ assertEquals(RemoteControlHandling.NATIVE, config.perCommandHandling["previous"])
328
+ }
329
+
330
+ @Test
331
+ fun `withCommands parses capabilities`() {
332
+ val capabilitiesArray = mockk<ReadableArray>(relaxed = true) {
333
+ every { size() } returns 3
334
+ every { toArrayList() } returns arrayListOf("seek", "next", "previous")
335
+ }
336
+
337
+ val commandsMap = mockk<ReadableMap>(relaxed = true) {
338
+ every { getString("handling") } returns null
339
+ every { hasKey("capabilities") } returns true
340
+ every { getArray("capabilities") } returns capabilitiesArray
341
+ every { hasKey("perCommandHandling") } returns false
342
+ every { hasKey("forwardInterval") } returns false
343
+ every { hasKey("backwardInterval") } returns false
344
+ }
345
+
346
+ val config = PlayerConfig().withCommands(commandsMap)
347
+ assertTrue(config.availableCommands.contains(PlayerCommand.SEEK))
348
+ assertTrue(config.availableCommands.contains(PlayerCommand.NEXT))
349
+ assertTrue(config.availableCommands.contains(PlayerCommand.PREVIOUS))
350
+ assertFalse(config.availableCommands.contains(PlayerCommand.PLAY_PAUSE))
351
+ }
352
+
353
+ @Test
354
+ fun `withCommands parses forwardInterval and backwardInterval`() {
355
+ val commandsMap = mockk<ReadableMap>(relaxed = true) {
356
+ every { getString("handling") } returns null
357
+ every { hasKey("capabilities") } returns false
358
+ every { hasKey("perCommandHandling") } returns false
359
+ every { hasKey("forwardInterval") } returns true
360
+ every { getDouble("forwardInterval") } returns 30.0
361
+ every { hasKey("backwardInterval") } returns true
362
+ every { getDouble("backwardInterval") } returns 5.0
363
+ }
364
+
365
+ val config = PlayerConfig().withCommands(commandsMap)
366
+ assertEquals(30L, config.forwardInterval)
367
+ assertEquals(5L, config.backwardInterval)
368
+ }
369
+
370
+ @Test
371
+ fun `withCommands with unknown handling preserves existing handling`() {
372
+ val commandsMap = mockk<ReadableMap>(relaxed = true) {
373
+ every { getString("handling") } returns "unknown-value"
374
+ every { hasKey("capabilities") } returns false
375
+ every { hasKey("perCommandHandling") } returns false
376
+ every { hasKey("forwardInterval") } returns false
377
+ every { hasKey("backwardInterval") } returns false
378
+ }
379
+
380
+ val original = PlayerConfig(remoteControlHandling = RemoteControlHandling.JS)
381
+ val config = original.withCommands(commandsMap)
382
+ assertEquals(RemoteControlHandling.JS, config.remoteControlHandling)
383
+ }
384
+
385
+ @Test
386
+ fun `fromReadableMap with null android section defaults wakeMode to NONE`() {
387
+ val map = mockk<ReadableMap>(relaxed = true) {
388
+ every { getString("contentType") } returns null
389
+ every { getMap("android") } returns null
390
+ every { getMap("cache") } returns null
391
+ every { getMap("progressSync") } returns null
392
+ every { hasKey(any()) } returns false
393
+ }
394
+
395
+ val config = PlayerConfig.fromReadableMap(map)
396
+ assertEquals(WakeMode.NONE, config.wakeMode)
397
+ assertFalse(config.skipSilenceEnabled)
398
+ assertNull(config.castReceiverAppId)
399
+ }
400
+ }