react-native-audio-browser 0.2.0-next.31 → 0.2.0-next.33

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 (44) hide show
  1. package/ios/AVPlayerEngine.swift +4 -8
  2. package/ios/Browser/BrowserConfig.swift +0 -5
  3. package/ios/Browser/BrowserError.swift +16 -0
  4. package/ios/Browser/BrowserManager.swift +36 -141
  5. package/ios/Browser/BrowserPathHelper.swift +25 -12
  6. package/ios/Browser/FavoriteSet.swift +46 -0
  7. package/ios/Browser/JsonModels.swift +172 -381
  8. package/ios/Browser/PageResolver.swift +16 -96
  9. package/ios/Browser/RequestLayer.swift +97 -2
  10. package/ios/Browser/RequestPipeline.swift +78 -227
  11. package/ios/Browser/SectionScope.swift +25 -0
  12. package/ios/Browser/SimpleRouter.swift +73 -144
  13. package/ios/Extension/SearchParams+Query.swift +28 -0
  14. package/ios/Extension/Track+Copying.swift +3 -1
  15. package/ios/Extension/Track+Identity.swift +7 -0
  16. package/ios/Http/HttpClient.swift +12 -30
  17. package/ios/HybridAudioBrowser+Browser.swift +5 -21
  18. package/ios/HybridAudioBrowser+Player.swift +3 -3
  19. package/ios/Model/NitroTypeStubs.swift +78 -0
  20. package/ios/Player/AudioSessionController.swift +2 -0
  21. package/ios/Player/ErrorStatus.swift +25 -2
  22. package/ios/Player/MediaLoader.swift +23 -71
  23. package/ios/Player/NowPlayingLines.swift +36 -0
  24. package/ios/Player/NowPlayingUpdater.swift +27 -85
  25. package/ios/Player/Player.swift +108 -261
  26. package/ios/Player/RetryLoop.swift +26 -0
  27. package/ios/Player/SleepTimerManager.swift +13 -15
  28. package/ios/Player/VolumeFader.swift +5 -0
  29. package/ios/Tests/BrowserPathHelperTests.swift +33 -0
  30. package/ios/Tests/ErrorStatusTests.swift +50 -0
  31. package/ios/Tests/FavoriteSetTests.swift +60 -0
  32. package/ios/Tests/JsonModelsTests.swift +30 -0
  33. package/ios/Tests/MediaLoaderTests.swift +26 -21
  34. package/ios/Tests/NowPlayingLinesTests.swift +43 -0
  35. package/ios/Tests/PlayerPolicyTests.swift +19 -3
  36. package/ios/Tests/PlayerTests.swift +81 -74
  37. package/ios/Tests/RequestLayerTests.swift +116 -0
  38. package/ios/Tests/RetryLoopTests.swift +35 -0
  39. package/ios/Tests/SearchParamsQueryTests.swift +48 -0
  40. package/ios/Tests/SectionScopeTests.swift +53 -0
  41. package/ios/Tests/SimpleRouterTests.swift +38 -3
  42. package/ios/Tests/SleepTimerManagerTests.swift +65 -41
  43. package/ios/Tests/Support/TestTypes.swift +2 -2
  44. package/package.json +1 -1
@@ -111,4 +111,57 @@ struct SectionScopeTests {
111
111
  @Test func childlessPageHasNoSections() {
112
112
  #expect(ResolvedTrack(path: "/p", title: "P").normalizedSections == nil)
113
113
  }
114
+
115
+ // MARK: - Queue
116
+
117
+ private func queue(_ sections: [Section], _ path: String, singleTrack: Bool = false) -> ([String?], Int)? {
118
+ SectionScope.queue(in: sections, path: path, singleTrack: singleTrack).map { ($0.tracks.map(\.src), $0.selectedIndex) }
119
+ }
120
+
121
+ @Test func queuesThePlayableTracksOfTheTappedSection() {
122
+ let page = [
123
+ section("First", [track("a"), Track(id: "off", src: "off", disabled: true), Track(path: "/browse"), track("b")]),
124
+ section("Second", [track("c")]),
125
+ ]
126
+ let result = queue(page, "/p?__trackId=b&__index=3")
127
+ #expect(result?.0 == ["a", "b"])
128
+ #expect(result?.1 == 1)
129
+ }
130
+
131
+ @Test func thePinnedCopyIsTheSelectedOne() {
132
+ let page = [section(nil, [track("a"), track("b"), track("a")])]
133
+ #expect(queue(page, "/p?__trackId=a&__index=2")?.1 == 2)
134
+ #expect(queue(page, "/p?__trackId=a")?.1 == 0)
135
+ }
136
+
137
+ @Test func aStaleIndexSelectsTheFirstIdentityMatch() {
138
+ let page = [section(nil, [track("x"), track("a"), track("a")])]
139
+ #expect(queue(page, "/p?__trackId=a&__index=0")?.1 == 1)
140
+ }
141
+
142
+ @Test func singleTrackQueuesOnlyTheTappedTrack() {
143
+ let page = [section(nil, [track("a"), track("b"), track("c")])]
144
+ let result = queue(page, "/p?__trackId=b&__index=1", singleTrack: true)
145
+ #expect(result?.0 == ["b"])
146
+ #expect(result?.1 == 0)
147
+ }
148
+
149
+ @Test func aDisabledTappedTrackQueuesNothing() {
150
+ let page = [section(nil, [track("a"), Track(id: "off", src: "off", disabled: true)])]
151
+ #expect(queue(page, "/p?__trackId=off&__index=1") == nil)
152
+ #expect(queue(page, "/p?__trackId=off") == nil)
153
+ }
154
+
155
+ @Test func aVanishedIdentityOrAPlainPathQueuesNothing() {
156
+ let page = [section(nil, [track("a")])]
157
+ #expect(queue(page, "/p?__trackId=gone") == nil)
158
+ #expect(queue(page, "/p") == nil)
159
+ }
160
+
161
+ @Test func isPlayableNeedsASrcAndNoDisabled() {
162
+ #expect(Track(src: "s").isPlayable)
163
+ #expect(Track(src: "s", disabled: false).isPlayable)
164
+ #expect(!Track(src: "s", disabled: true).isPlayable)
165
+ #expect(!Track(path: "/p").isPlayable)
166
+ }
114
167
  }
@@ -7,9 +7,7 @@ private struct TestRoute: RouteEntry {
7
7
  }
8
8
 
9
9
  private func match(_ path: String, _ patterns: [String]) -> (route: TestRoute, match: RouteMatch)? {
10
- let router = SimpleRouter()
11
- let routes = patterns.map { TestRoute(path: $0) }
12
- return router.findBestMatch(path: path, routes: routes)
10
+ SimpleRouter.findBestMatch(path: path, routes: patterns.map { TestRoute(path: $0) })
13
11
  }
14
12
 
15
13
  // MARK: - exact matches
@@ -137,3 +135,40 @@ private func match(_ path: String, _ patterns: [String]) -> (route: TestRoute, m
137
135
  let result = match("/api/v1/users", ["/api/{version}/**", "/api/v1/**"])
138
136
  #expect(result!.route.path == "/api/v1/**")
139
137
  }
138
+
139
+ @Test func treatsADoubleStarBeforeTheEndAsAConstant() {
140
+ #expect(match("/a/**/b", ["/a/**/b"]) != nil)
141
+ #expect(match("/a/x/b", ["/a/**/b"]) == nil)
142
+ }
143
+
144
+ @Test func theFirstRouteWinsATie() {
145
+ let result = match("/artists/123", ["/artists/{id}", "/artists/{name}"])
146
+ #expect(result!.route.path == "/artists/{id}")
147
+ }
148
+
149
+ // MARK: - browse route
150
+
151
+ private func browseRoute(_ path: String, _ patterns: [String]) -> (route: TestRoute, match: RouteMatch)? {
152
+ SimpleRouter.browseRoute(for: path, in: patterns.map { TestRoute(path: $0) })
153
+ }
154
+
155
+ @Test func theSpecialRoutesNeverMatchABrowsePath() {
156
+ #expect(browseRoute("/__tabs__", ["__tabs__", "__search__"]) == nil)
157
+ #expect(browseRoute("__search__", ["__search__"]) == nil)
158
+ }
159
+
160
+ @Test func aMatchingRouteWinsOverTheDefault() {
161
+ let result = browseRoute("/artists/1", ["__default__", "/artists/{id}"])
162
+ #expect(result!.route.path == "/artists/{id}")
163
+ #expect(result!.match.params == ["id": "1"])
164
+ }
165
+
166
+ @Test func withoutAMatchTheDefaultRouteBrowsesThePath() {
167
+ let result = browseRoute("/a/b", ["/artists", "__default__"])
168
+ #expect(result!.route.path == "__default__")
169
+ #expect(result!.match.params == ["path": "/a/b"])
170
+ }
171
+
172
+ @Test func withoutAMatchOrADefaultThereIsNoBrowseRoute() {
173
+ #expect(browseRoute("/a/b", ["/artists", "__tabs__"]) == nil)
174
+ }
@@ -5,103 +5,127 @@ import Testing
5
5
  // Timing tests drive a ManualSleepTimerScheduler rather than the main queue:
6
6
  // every deadline below is virtual, so nothing races the real clock.
7
7
 
8
+ @MainActor
9
+ private final class Volume {
10
+ var level: Float = 0.8
11
+ }
12
+
13
+ @MainActor
14
+ private func makeManager() -> (SleepTimerManager, ManualSleepTimerScheduler, Volume, VolumeFader) {
15
+ let clock = ManualSleepTimerScheduler()
16
+ let volume = Volume()
17
+ let fader = VolumeFader(getVolume: { volume.level }, setVolume: { volume.level = $0 })
18
+ return (SleepTimerManager(fader: fader, scheduler: clock), clock, volume, fader)
19
+ }
20
+
8
21
  @Suite("SleepTimerManager - fade scheduling")
9
22
  struct SleepTimerManagerTests {
10
23
  @Test @MainActor
11
- func fadeFiresBeforeDeadline_completionDoesNotEmitFadeCancel() {
12
- let clock = ManualSleepTimerScheduler()
13
- let manager = SleepTimerManager(scheduler: clock)
14
- var fadeStarts: [TimeInterval] = []
24
+ func fadeStartsBeforeDeadline_completionRestoresAfterHalting() {
25
+ let (manager, clock, volume, fader) = makeManager()
15
26
  var completes = 0
16
- var cancels = 0
17
- manager.onFadeStart = { fadeStarts.append($0) }
18
- manager.onComplete = { completes += 1 }
27
+ var volumeAtHalt: Float?
28
+ manager.onComplete = {
29
+ completes += 1
30
+ volumeAtHalt = volume.level
31
+ }
19
32
  manager.set(seconds: 0.3, fadeDuration: 0.15)
20
- // Attach after set() — replacing a timer emits a defensive onFadeCancel.
21
- manager.onFadeCancel = { cancels += 1 }
22
33
 
23
34
  clock.advance(to: 0.05) // before the fade window
24
- #expect(fadeStarts.isEmpty)
35
+ #expect(!manager.isFading)
25
36
  #expect(manager.get() != nil)
26
37
 
27
38
  clock.advance(to: 0.22) // inside the fade window
28
- #expect(fadeStarts == [0.15])
39
+ #expect(manager.isFading)
40
+ #expect(fader.duration == 0.15)
41
+ #expect(fader.originalVolume == 0.8)
29
42
  #expect(completes == 0)
43
+ volume.level = 0.1 // mid-fade
30
44
 
31
45
  clock.advance(to: 0.4) // past the deadline
32
46
  #expect(completes == 1)
33
- #expect(cancels == 0)
47
+ #expect(volumeAtHalt == 0.1) // halted before the restore
48
+ #expect(volume.level == 0.8)
49
+ #expect(!manager.isFading)
34
50
  #expect(manager.get() == nil)
35
51
  }
36
52
 
37
53
  @Test @MainActor
38
54
  func fadeLongerThanTimer_clampsAndStartsImmediately() {
39
- let clock = ManualSleepTimerScheduler()
40
- let manager = SleepTimerManager(scheduler: clock)
41
- var fadeStarts: [TimeInterval] = []
42
- manager.onFadeStart = { fadeStarts.append($0) }
55
+ let (manager, clock, _, fader) = makeManager()
43
56
 
44
57
  manager.set(seconds: 0.2, fadeDuration: 5)
45
58
 
46
59
  clock.advance(to: 0.1) // fade should have begun
47
- #expect(fadeStarts == [0.2])
60
+ #expect(manager.isFading)
61
+ #expect(fader.duration == 0.2)
48
62
  }
49
63
 
50
64
  @Test @MainActor
51
- func clear_emitsFadeCancel_andCancelsScheduledFade() {
52
- let clock = ManualSleepTimerScheduler()
53
- let manager = SleepTimerManager(scheduler: clock)
54
- var fadeStarts = 0
65
+ func clear_restoresVolume_andCancelsScheduledFade() {
66
+ let (manager, clock, volume, _) = makeManager()
55
67
  manager.set(seconds: 0.2, fadeDuration: 0.15)
56
- var cancels = 0
57
- manager.onFadeStart = { _ in fadeStarts += 1 }
58
- manager.onFadeCancel = { cancels += 1 }
68
+ clock.advance(to: 0.1)
69
+ volume.level = 0.1 // mid-fade
59
70
 
60
71
  #expect(manager.clear())
61
- #expect(cancels == 1)
72
+ #expect(volume.level == 0.8)
73
+ #expect(!manager.isFading)
62
74
 
63
75
  clock.advance(to: 0.3) // well past the would-be deadline
64
- #expect(fadeStarts == 0)
76
+ #expect(!manager.isFading)
77
+ }
78
+
79
+ @Test @MainActor
80
+ func clearBeforeFade_cancelsScheduledFade() {
81
+ let (manager, clock, _, _) = makeManager()
82
+ manager.set(seconds: 0.2, fadeDuration: 0.15)
83
+
84
+ #expect(manager.clear())
85
+
86
+ clock.advance(to: 0.3)
87
+ #expect(!manager.isFading)
65
88
  }
66
89
 
67
90
  @Test @MainActor
68
- func replacingTimer_emitsFadeCancel() {
69
- let manager = SleepTimerManager(scheduler: ManualSleepTimerScheduler())
91
+ func replacingTimer_cancelsTheFade() {
92
+ let (manager, clock, volume, _) = makeManager()
70
93
  manager.set(seconds: 60, fadeDuration: 10)
71
- var cancels = 0
72
- manager.onFadeCancel = { cancels += 1 }
94
+ clock.advance(to: 55)
95
+ volume.level = 0.1 // mid-fade
73
96
 
74
97
  manager.set(seconds: 30)
75
98
 
76
- #expect(cancels == 1)
99
+ #expect(!manager.isFading)
100
+ #expect(volume.level == 0.8)
77
101
  }
78
102
 
79
103
  @Test @MainActor
80
- func switchingToEndOfTrack_emitsFadeCancel() {
81
- let manager = SleepTimerManager(scheduler: ManualSleepTimerScheduler())
104
+ func switchingToEndOfTrack_cancelsTheFade() {
105
+ let (manager, clock, volume, _) = makeManager()
82
106
  manager.set(seconds: 60, fadeDuration: 10)
83
- var cancels = 0
84
- manager.onFadeCancel = { cancels += 1 }
107
+ clock.advance(to: 55)
108
+ volume.level = 0.1 // mid-fade
85
109
 
86
110
  manager.setToEndOfTrack()
87
111
 
88
- #expect(cancels == 1)
112
+ #expect(!manager.isFading)
113
+ #expect(volume.level == 0.8)
89
114
  #expect(manager.get() == .third(SleepTimerEndOfTrack(sleepWhenPlayedToEnd: true)))
90
115
  }
91
116
 
92
117
  @Test @MainActor
93
118
  func noFadeDuration_schedulesNoFade() {
94
- let clock = ManualSleepTimerScheduler()
95
- let manager = SleepTimerManager(scheduler: clock)
96
- var fadeStarts = 0
119
+ let (manager, clock, volume, _) = makeManager()
97
120
  var completes = 0
98
- manager.onFadeStart = { _ in fadeStarts += 1 }
99
121
  manager.onComplete = { completes += 1 }
100
122
 
101
123
  manager.set(seconds: 0.1)
102
124
 
125
+ clock.advance(to: 0.05)
126
+ #expect(!manager.isFading)
103
127
  clock.advance(to: 0.2)
104
- #expect(fadeStarts == 0)
105
128
  #expect(completes == 1)
129
+ #expect(volume.level == 0.8)
106
130
  }
107
131
  }
@@ -118,11 +118,11 @@ func makePlayer(
118
118
  engine: MockPlayerEngine = MockPlayerEngine(),
119
119
  retry: RetryLoop = makeRetryLoop(),
120
120
  retries: Bool = true,
121
- sleepTimer: SleepTimerManager = SleepTimerManager(scheduler: ManualSleepTimerScheduler()),
121
+ sleepTimerScheduler: any SleepTimerScheduling = ManualSleepTimerScheduler(),
122
122
  saveInterval: Duration = .seconds(5),
123
123
  ) -> Player {
124
124
  let player = Player(
125
- engine: engine, retry: retry, sleepTimerManager: sleepTimer,
125
+ engine: engine, retry: retry, sleepTimerScheduler: sleepTimerScheduler,
126
126
  persistenceDefaults: UserDefaults(suiteName: "PlayerTests")!, persistenceSaveInterval: saveInterval,
127
127
  )
128
128
  retry.shouldRetry = { retries }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-audio-browser",
3
- "version": "0.2.0-next.31",
3
+ "version": "0.2.0-next.33",
4
4
  "description": "react-native-audio-browser is a react native package built with Nitro",
5
5
  "keywords": [
6
6
  "react-native",