Repository navigation
fix(mobile): advance to next track on iOS when starting mid-list - #14649
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
dylanjeffers
added a commit
that referenced
this pull request
Oct 9, 2026
) Ships in native release **1.5.187**. It targets `chore/mobile-native-release-1.5.187` (#14645). ## What A `post_install` step in the Podfile patches one line of the SwiftAudioEx pod source, `Sources/SwiftAudioEx/QueueManager.swift`: ```diff - if (self.items.count > 1 && currentIndex >= index) { + if (self.items.count > 0 && currentIndex >= index) { ``` SwiftAudioEx 1.0.0 (our Podfile.lock) and 1.1.0 (RNTP 4.1.2's podspec) only shift the current index for an insert in front of the current item when the queue already has more than one item. With one item, inserting at 0 leaves the index one slot early, and when the track ends the player replays it instead of advancing. Upstream `main` has the `> 0` fix, but it isn't released yet. The step replaces only that exact line. If the file is already patched it does nothing. If the line is missing (for example after a SwiftAudioEx update), it raises and `pod install` fails, so the patch can't be skipped silently. ## Relation to #14649 #14649 is the JS fix, which ships OTA. It avoids the bad insert by appending the later tracks first. When the tapped track is the last one in the list, it uses a temporary placeholder instead. This PR fixes the root cause natively, which covers the last-item case without the placeholder. It also covers any other single-item insert. ## Testing - `RCT_NEW_ARCH_ENABLED=0 bundle exec pod install` prints the patch message, and `Pods/SwiftAudioEx/Sources/SwiftAudioEx/QueueManager.swift:148` now reads `items.count > 0`. - I checked the patch logic on three versions of the file: unpatched gets patched, already patched is left alone, and a changed line raises. - Confirmed the same line is in SwiftAudioEx tag 1.0.0 and in 1.1.0. - Podfile.lock is not changed. The local install showed the known RNTP 4.0.1→4.1.2 / SwiftAudioEx 1.0.0→1.1.0 drift, which I left out. - I haven't rebuilt the native app with this patch. The simulator check of the last-item case was done with the JS fix (#14649). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 9, 2026
dylanjeffers
added a commit
that referenced
this pull request
Oct 9, 2026
…14653) ## Bug On iOS, after a list plays to its end: - the play button keeps showing pause until it's tapped - pressing Previous and letting the track "play" to the end stalls. The scrubber moves, but the native player is paused the whole time, so it never sends an end event (no `PlaybackQueueEnded`, no `PlaybackActiveTrackChanged`). Both symptoms come from the same cause. They aren't related to #14649 or to the SwiftAudioEx index bug in #14650: native `currentIndex` is correct at every step. ## Root cause When the queue ends, SwiftAudioEx stops with `playWhenReady = false` and RNTP emits `PlaybackQueueEnded`. `AudioPlayer.tsx` subscribes to that event but has never handled it. It was a `// TODO` before the #14220 refactor, and it was silently dropped after. So redux keeps `playing: true`. `TrackPlayer.play()` is only called from `handleTogglePlay`, when redux `playing` changes. After the queue ends it is already `true`, so: - **Previous** (position > 3s) dispatches `reset({ shouldAutoplay: true })`, which seeks to 0 and sets `playing: true` again. That is not a change, so `play()` is never called. - **Previous twice / tapping a track** calls `skip(index)`. RNTP iOS calls `jumpToItem(atIndex:, playWhenReady: playerState == .playing)`, which is `false` here, so the track loads paused. The same `skip` behavior also stalls without a queue end: **Next then Previous quickly** skips while the player is still buffering, so the track loads paused while redux says playing. Simulator logs from an instrumented build (native state read through RNTP after each event): ``` queue end playback-queue-ended state=paused pwr=false idx=1 redux playing=true <- out of sync Previous redux counter++ (reset) state=ready pwr=false pos=0 redux playing=true <- never plays Prev, Prev skip(0) from paused state=ready pwr=false idx=0 <- never plays Next, Prev skip(0) from buffering playWhenReady -> false, state=ready <- never plays ``` ## Fix - On `PlaybackQueueEnded`, dispatch `pause({ onlySetState: true })` so redux matches the stopped player. The next play is then a real `false -> true` change and reaches `TrackPlayer.play()`. - After `TrackPlayer.skip()` in `handleQueueIdxChange`, call `TrackPlayer.play()` if redux is playing. On Android, skip already keeps `playWhenReady`, so this does nothing there. JS only, so it can ship OTA. ## Testing iOS 26.5 simulator, Release build, old arch (JS thread is `com.facebook.react.JavaScript`), production env, with this branch's bundle. I used a 2-track album and an instrumented bundle that logs native state on each RNTP event and auto-seeks near the end of each track. - Before: - queue end leaves `pwr=false` with redux `playing=true`, and the album and mini player show Pause - Previous leaves native paused at 0 while the scrubber advances - Previous ×2 and Next→Previous load the track paused - After: - queue end sets redux `playing=false`, and the album and mini player show Play - Previous restarts and plays to the end, then pauses cleanly - Next→Previous during buffering: skip still drops `playWhenReady`, then `play()` brings it back, and playback continues and auto-advances - tapping a track row from the paused state plays it - `eslint` is clean and the mobile tests pass (8 suites, 25 tests). `tsc --noEmit -p packages/mobile` shows the same 4 errors with and without this change, in unrelated files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
dylanjeffers
added a commit
that referenced
this pull request
Oct 9, 2026
…ver (#14651) Moves the play bar tracker, the now playing scrubber and the track-list eq bars off JS-driven `Animated` and onto the native driver. JS-only, so it can ship as an OTA. ## Why All three used `useNativeDriver: false`, so every frame ran in JS and committed through `setNativeProps`. On old arch that's cheap. On the new architecture each one is a full `ShadowTree::commit`, and Reanimated 3's commit hook clones the tree on every commit. It was half of the CPU regression in #14648 (blocker 6): playing with the drawer open went from ~15% to ~50%. ## What changed - `animateLinear` in `utils/animation.ts` runs a long linear animation as 10 s native-driver segments. The native driver precomputes one frame per 16 ms, which is why the old code avoided it for track-length animations (the old comment about Hermes). Each segment starts from the wall-clock position, so the bar can't drift after the app is backgrounded. - `TrackingBar` and `Slider` use it with the start position passed in explicitly (from `seek` or `TrackPlayer.getProgress()`), instead of continuing from whatever the JS value was. Seeking, dragging and pausing behave the same. The scrubber's 100 ms tap-to-seek timing moved to the native driver too, because a value can't mix drivers. Both stop the chain on unmount. - `AnimatedEqBars` animated `height`, which the native driver can't do. Each bar is now a full-height bar slid down with `translateY` inside a clip view with the same 2 px radius. The visible shape at every height matches the old one (rounded top from the bar, rounded bottom from the clip). ## Base **Base: `chore/mobile-rn-0.81` (#14642), shipping in native release 1.5.188 (#14654).** Rebased 2026-10-08 after #14642 moved onto the release branch. Merge this into #14642, not main. The svg `LinearGradient` it imports is on main since 1.5.187, so it no longer depends on #14642's code, but it ships with 1.5.188 so the new native-driver animations get device-tested on the RC builds. It doesn't overlap the iOS queue fix (#14649, on main): that change is in `AudioPlayer.tsx` and `addTracksAroundCurrent.ts`, which this PR doesn't touch. ## Measurements (iOS 26.5 simulator, Release, signed in, 30 s `top` averages) | Scenario | Old arch before → after | New arch before → after | |---|---|---| | Feed, playing | 17.1% → 12.9% | 50.4% → 15.3% | | Now playing drawer open, playing | 15.3% → 13.7% | 50.6% → 15.6% | | Background, playing | 9.5% → 12.2% | 12.2% → 14.5% | New-arch "after" also includes #14648's svg and Lottie fixes, which don't affect these screens. Old-arch "after" is the #14642 Release app with this branch's JS bundle swapped in. ## Testing - Simulator, both architectures: the bar and the scrubber advance at the right rate on a 56-minute and a 3-minute track. Tap-to-seek to the middle lands at 28:02 and keeps going. The play bar is at the right position after 30 s in the background. - `tsc` and `eslint` clean on the changed files. - Not checked by eye: the eq bars. In album track lists, the `TrackImage` children (eq bars, play icon overlay) don't show on either architecture, before or after this change. That looks like an existing bug and is out of scope here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
dylanjeffers
added a commit
that referenced
this pull request
Oct 9, 2026
## Summary Version bump for native release **1.5.188**. Merging it starts the RC and production native builds. - `packages/mobile/package.json`: 1.5.187 → 1.5.188 (and the matching `package-lock.json` entry) - iOS `CFBundleShortVersionString`: 1.1.200 → 1.1.201 - Android `versionName`: 1.1.536 → 1.1.537 1.5.188 contains (all still on the old architecture): - #14642: React Native 0.79.5 → 0.81.6, React 19.1.4 monorepo-wide - #14647: RNTP 4.1.2 patch for bridgeless events and `Unit` module methods (no change on old arch) - #14651: playback progress and eq bars on the native driver - #14652: react-native-svg 15.15.1 and the Lottie `shouldBeRecycled NO` patch (no change on old arch) 1.5.189 is then only the new-architecture flag flip. ## Stack ``` main └── chore/mobile-native-release-1.5.188 (this PR) └── chore/mobile-rn-0.81 #14642 ├── fix/mobile-rntp-bridgeless #14647 │ └── fix/mobile-new-arch-prereqs #14652 └── fix/mobile-native-driver-progress #14651 ``` ## Verification of the rebased stack (2026-10-08) Built from a local, unpushed integration branch: #14652's tip (which contains #14647) merged with #14651, on top of #14654. | Check | Result | |---|---| | `npx tsc --noEmit -p packages/mobile` | pass | | eslint on the touched files / `turbo run verify` lint | pass (1 existing warning in `HostRemixContestDrawer.tsx`) | | `cd packages/mobile && npm test` | 9 suites, 34 tests pass | | iOS Release, `generic/platform=iOS Simulator`, arm64, `ENVFILE=.env.prod` | builds; Info.plist 1.1.201, `RCTNewArchEnabled=false`; bundle carries 1.5.188 | | Android `assembleProdRelease`, arm64-v8a, debug keystore | builds; versionName 1.1.537 | | iOS 26.5 sim, installed over the signed-in app, CodePush moved aside | launch to feed; JS thread `com.facebook.react.JavaScript` (old arch) | | Playback from the 2nd feed track | plays; play bar tracker moves | | Auto-advance | seek to 2:43/2:45 on track 2, advanced to track 3 | | Now playing drawer | opens, scrubber advances, tap-to-seek works | | Profile tabs | swipe Tracks → Albums, tap Reposts | | notifee/RNFB APNs token | a temporary console log (not committed, rebuilt without it afterwards) printed an 80-byte lowercase APNs token at startup | | Android 16 emulator, `pm clear` | `Loading JS bundle from "assets://index.android.bundle"`, "Legacy Architecture" warning, sign-up and sign-in screens render | | Android hardware Back | **not verified**: the emulator's system_server was killed by its watchdog three times under host load, so Back never got a clean run | | JS errors on iOS | only `Could not cache profile images` (content node timeout) | ## Merge order 1. **Wait until 1.5.187 is live in both stores.** Until then, any native change on main would go into a 1.5.187 rebuild. 2. **Dispatch a production OTA from main before merging anything below**, for every store version that can run main's JS: ``` gh workflow run mobile.yml --repo AudiusProject/apps --ref main -f ota_channel=production ``` With main at 1.5.187 this publishes to `production/1.5.187`. **Do not** dispatch `-f binary_version=1.5.186` or `1.5.185` from main any more: main now carries #14627, which calls `TurboModuleRegistry.getEnforcing('ReactNativeFs')` at import, so that JS crashes 1.5.186 and older binaries. If 1.5.186 users still need a fix, dispatch from a commit before 5c4ee7e. 3. **Land everything on main in one push.** Merge the stack into this branch from the top down: #14652 into #14647, #14647 and #14651 into #14642, #14642 into this branch. Then merge this PR into main. Any push to main that leaves the version unchanged publishes an RC OTA to the current binary's history, and RN 0.81 JS on a 0.79 binary will not start. With one push that also changes the version, the version check skips the OTA and starts the native builds. After that, OTAs route to `1.5.188` histories. 4. **Device-test the RC builds before submitting:** - cold launch on both platforms, old arch at runtime (iOS JS thread `com.facebook.react.JavaScript`; Android "Legacy Architecture" warning) - background audio with the screen locked for more than 2 minutes - lock-screen and notification controls, Bluetooth, Chromecast - auto-advance, including starting from the 2nd track of a list (#14649/#14650) - play bar and scrubber position: seek, drag, pause/resume, background then foreground - first play does not pause itself (Lottie) - push: token registered after the 1.5.187 → 1.5.188 update and taps open the right screen (#14640) - Android hardware Back on a pushed screen and at the root - gradients and offline downloads 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
On iOS, starting a track that isn't the first one in a list (feed, profile, collection) and letting it finish doesn't advance. The player stays on the same track instead of moving to the next one.
resetQueueadds the tapped track on its own, then inserts the earlier tracks at index 0. SwiftAudioEx (RNTP's iOS player, 1.0.0 and 1.1.0) only shifts its current index for an insert in front of the current item when the queue already has more than one item:So the first insert in front of the lone tapped track leaves
currentIndexone slot too early. When the track ends, "next" is the tapped track again.Fix
Queue building moves into
addTracksAroundCurrent:The final RNTP order is still the redux order, so Android (KotlinAudio) ends up with the same queue as before.
This is JS only and OTA-safe. The native fix (
> 0) is in #14650 for the 1.5.187 release.Testing
addTracksAroundCurrent.test.tsuses a fake queue that copies SwiftAudioEx's index logic. The old insert order fails 4 of its 6 cases and the new one passes all 6.npx tsc --noEmit -p packages/mobile, eslint andnpm test(8 suites, 25 tests) pass.🤖 Generated with Claude Code