Repository navigation
fix(mobile): patch SwiftAudioEx insert index for one-item queues - #14650
Merged
dylanjeffers merged 1 commit intoOct 9, 2026
Merged
dylanjeffers merged 1 commit into
dylanjeffers merged 1 commit into
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
dylanjeffers
added a commit
that referenced
this pull request
Oct 8, 2026
) ## 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. `resetQueue` adds 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: ```swift if (self.items.count > 1 && currentIndex >= index) { currentIndex += items.count } ``` So the first insert in front of the lone tapped track leaves `currentIndex` one slot too early. When the track ends, "next" is the tapped track again. ## Fix Queue building moves into `addTracksAroundCurrent`: - the later tracks are appended first, so the queue already has more than one item when the earlier tracks go in - the earlier tracks go in with a single insert at index 0 - if there are no later tracks (the tapped track is last), a placeholder copy of the previous track is appended for the insert and removed afterwards. Removing an item after the current one doesn't touch the current index or playback. 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 - New unit test `addTracksAroundCurrent.test.ts` uses 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 and `npm test` (8 suites, 25 tests) pass. - iOS simulator, Release build (old arch) with a production bundle: - before the fix: starting the 2nd or 3rd feed track and letting it finish stalls on that track at the end - after the fix: - 2nd feed track → advances to the 3rd, and the now playing drawer and the highlighted tile match - previous goes back to the earlier track - first item advances - last item of a 2-track album: RNTP reports active index 1 with the queue in order, and playback ends at the end of the queue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
dylanjeffers
merged commit Oct 9, 2026
37ec8b2
into
chore/mobile-native-release-1.5.187
3 checks passed
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
Phase 1 of the React Native upgrade: 0.79.5 → 0.81.6 with the old architecture still on. **Base: `chore/mobile-native-release-1.5.188` (#14654).** Rebased onto it after 1.5.187 (#14645, which squashed #14627, #14640 and #14650) merged to main; the two lib-swap commits are gone from this branch. Merge #14647 and #14651 into this branch first, then this into #14654, then #14654 into main as one push (see #14654 for the full order). ## Rebase onto 1.5.187 (2026-10-08) - `patches/react-native+0.79.5.patch` (the ExceptionsManager Hermes fix from #14643) is renamed to `react-native+0.81.6.patch`. The anchors are unchanged in 0.81.6, and `scripts/patch-package.sh` applies it with `--error-on-fail` during `npm install` (checked: `getErrorStackSafe` is in the installed `ExceptionsManager.js`). - notifee and Firebase from #14640 are kept as they are: `@notifee/react-native` 9.1.8 is the latest release and `@react-native-firebase/app`/`messaging` 24.1.1 build on 0.81.6 on the old arch (both declare `react-native: *`). No bump needed. - The Firebase Android BoM stays pinned to 33.16.0. This branch moves Kotlin to 2.1.20, and BoM 34 needs 2.2, so the pin still applies; the comment now says so. - The UIScene and notifee lines in `AppDelegate.mm` (#14626, #14640) are untouched by this branch. - `package-lock.json` and `Podfile.lock` were regenerated (`npm install`, then `RCT_NEW_ARCH_ENABLED=0 bundle exec pod install`); the release xcconfig has no `RCT_NEW_ARCH_ENABLED=1`. The SwiftAudioEx post_install patch (#14650) still applies. **Needs a native release (1.5.188).** Don't OTA this JS onto older binaries. Old architecture stays on: Android `newArchEnabled=false`, iOS `RCTNewArchEnabled=false`, pods installed with `RCT_NEW_ARCH_ENABLED=0`. Two commits, one per step. Each step builds on iOS and Android: 1. 0.79.5 → 0.80.3 2. 0.80.3 → 0.81.6 (0.81.6 is the latest 0.81 patch on npm) ## Version bumps | Package | From | To | Why | |---|---|---|---| | react-native | 0.79.5 | 0.81.6 | the upgrade | | `@react-native/*` (babel-preset, eslint-config, metro-config, metro-babel-transformer, typescript-config) | 0.79.5 | 0.81.6 | must match RN | | `@react-native-community/cli`, `cli-platform-android`, `cli-platform-ios` | 18.0.0 | 20.1.3 | CLI line for 0.81; the platform packages were left at 18 on the old ladder branch | | react, react-dom, react-test-renderer (whole monorepo, root `overrides` too) | 19.0.0 | **19.1.4** | RN's bundled renderer throws at startup unless `react` is **exactly** its version: 19.1.0 for 0.80.3, 19.1.4 for 0.81.6. The old ladder branch used 19.1.8, which would throw "Incompatible React versions" on launch. | | @types/react / @types/react-dom | 19.0.0 | 19.1.17 / 19.1.11 | match React 19.1 | | react-native-gesture-handler | 2.25.0 | 2.28.0 | 2.28 is the first 2.x with official 0.81 support (2.25's table stops at 0.79). Stays on 2.x; v3 drops the old architecture. | | @amplitude/analytics-react-native | 1.4.11 | 1.4.14 | Kotlin 2.1 (RN 0.80+) turns the deprecated `toUpperCase(Locale)` in 1.4.11 into a compile error | | tiktok-opensdk-react-native | ^0.10.7 | 0.10.7 (pinned) | it now carries a patch. 0.10.9 fixes the Android signature, but drops the `handleOpenURL`/`handleUserActivity` declarations that SceneDelegate imports | Patches added (`patches/`): - `react-native-track-player+4.1.2.patch`: Kotlin 2.1 rejects `Arguments.fromBundle(Bundle?)`. Now resolves `null` when there is no item. 4.1.2 is the last 4.x and has no fixed release. - `tiktok-opensdk-react-native+0.10.7.patch`: `onNewIntent(intent: Intent)` (RN 0.80 made `ActivityEventListener` non-null) and `reactApplicationContext.currentActivity`. Checked against 0.81 on the old architecture and left alone: Reanimated 3.19.5 (the official table lists 3.19.x for 0.78–0.81 on Paper; Reanimated 4 is out of scope), track-player 4.1.2 (patched above), collapsible-tab-view 8.0.1 (JS only), screens 4.18.0 (0.81 support since 4.14; `react-native-screens+4.18.0.patch` still applies; 4.25+ drops the old arch), pager-view 6.7.1 (7+ drops the old arch), video 6.18.0, svg 15.15.0 (0.81 support since 15.12.1), google-cast 4.6.2 (5.x is new-arch only), notifications 5.1.0, code-push 12.3.2 (README lists 0.77–0.86; 13.x needs a new OTA history), bootsplash 6.3.11, safe-area-context 5.6.2, keyboard-controller 1.19.0, flash-list 1.8.3, datetimepicker 8.3.0 (builds against 0.81.6). ## Template changes (rn-diff-purge 0.79.5 → 0.81.6) - Android: Kotlin 2.0.21 → 2.1.20, Gradle 8.13 → 8.14.3, new `gradlew`/`gradlew.bat`/wrapper jar, `MainApplication` uses `loadReactNative(this)` in place of `SoLoader.init` plus `load()`, and `edgeToEdgeEnabled=false` is added as in the template. The app already draws edge-to-edge through react-native-bars and the SDK 36 target, so this flag changes nothing. - AGP comes from RN: 8.8 → 8.11.0, which supports compileSdk 36, so `android.suppressUnsupportedCompileSdk=36` is removed. - `settings.gradle` resolves `@react-native/gradle-plugin` through `require.resolve` from `react-native`, not a hard-coded hoisted path. With this lockfile npm nests it under `node_modules/react-native/node_modules`. - Kept: `newArchEnabled=false`, the `AudiusScrypt` module (`ScryptPackage` is still registered), the debug manifest (it adds `SYSTEM_ALERT_WINDOW`), and `android:enableOnBackInvokedCallback="false"`. - **Back handling:** since 0.80, `ReactActivity` registers an `OnBackPressedCallback` when targetSdk ≥ 36, which calls `onBackPressed()` → JS `BackHandler`. Our `invokeDefaultOnBackPressed` override (`moveTaskToBack`) still runs. The manifest opt-out is kept as asked and can probably go in a later release. - iOS: no template change between 0.79.5 and 0.81.6. `Info.plist` and `RichPushExtension/Info.plist` now set `RCTNewArchEnabled=false`, which RN 0.80+ reads **at runtime**; a missing key means new arch. `pod install` writes the key and re-sorts the plist each time, so the sorted version is committed. ## AppDelegate / UIScene No code change needed. `RCTAppDelegate` is marked deprecated in 0.81 in favour of `RCTReactNativeFactory`, so there is a `-Wdeprecated-declarations` warning, but every API we use is unchanged: `automaticallyLoadReactNativeWindow`, `rootViewFactory`, `createRootViewController`, `setRootView:toRootViewController:`, `customizeRootView:`, `bundleURL`, and `dependencyProvider`. `didFinishLaunching` still creates the factory and skips the window. Moving to `RCTReactNativeFactory` can wait for the 0.82+ phase. Verified on a Release build: cold launch, a deep link while killed, and a deep link while backgrounded, on iOS 26.5 and iOS 27. iOS 27 needs UIScene and showed no launch crash. ## Web / React 19.1 - Typecheck: web, common and harmony are clean, and so is mobile. - `vite build` (prod env): succeeds. - web vitest: 178 passed, 9 skipped. `TrackTile › Public Premium (non-owner)` timed out once while the machine was under heavy load, then passed 3 out of 3 when run alone. ## Verification matrix (before the rebase; see the bottom for the rebased stack) | | iOS 26.5 sim (Release) | iOS 27 sim (Release) | Android API 36 emu (prodRelease, arm64) | |---|---|---|---| | Build | ✅ Xcode 27, `generic/platform=iOS Simulator` | same binary | ✅ debug keystore | | Old arch at runtime | ✅ JS thread `com.facebook.react.JavaScript` | – | ✅ `ReactRootView` + "Legacy Architecture" warning | | Launch past splash | ✅ | ✅ | ✅ `assets://index.android.bundle` | | Sign-up / sign-in screens | (signed in) | ✅ | ✅ including Create Password | | Signed-in feed, scroll | ✅ | – | – | | Playback + now playing drawer (open, drag closed) | ✅ | – | – | | Background → foreground while playing | ✅ audio kept going (0:09 → 0:41) | – | – | | Search | ✅ | – | – | | Profile: collapsible header + tab swipe | ✅ | – | – | | Deep link, warm | ✅ `audius://audius` | ✅ link delivered | – | | Deep link, cold | ✅ `audius://deadmau5` | ✅ (signed out → sign-on) | – | | Hardware Back | – | – | ✅ pops Create Password; at root backgrounds the app without killing it | | Sign-on footer above nav bar | – | – | ✅ | | Red JS errors | none, apart from a recurring `JSON Parse error: Unexpected character: <` (not yet traced) | none | none | The CodePush folder was moved aside or cleared before every cold launch, so all results above are from the bundled JS. ## Still needs a real device - background audio with the screen locked for more than 2 minutes - lock-screen and notification controls - Chromecast - push notifications ## Notes - A Release build that downloads a production OTA built for 1.5.186 or 1.5.187 runs a 0.79 bundle on a 0.81 binary. 1.5.188 ships with its own package.json version (#14654) so it never receives those OTAs. - The Hermes `ExceptionsManager` fix now applies through patch-package (#14643), renamed for 0.81.6 above. ## 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) | 🤖 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.
Ships in native release 1.5.187. It targets
chore/mobile-native-release-1.5.187(#14645).What
A
post_installstep in the Podfile patches one line of the SwiftAudioEx pod source,Sources/SwiftAudioEx/QueueManager.swift: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
mainhas the> 0fix, 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 installfails, 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 installprints the patch message, andPods/SwiftAudioEx/Sources/SwiftAudioEx/QueueManager.swift:148now readsitems.count > 0.🤖 Generated with Claude Code