From b1da5651231ad165a6d5fab032088711f9f042d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Apr 2026 23:57:56 +0000 Subject: [PATCH 1/4] Fix mobile next track not playing when pressing skip button When pressing next/previous in the now playing drawer, the queue index was updated by the reducer before the PlaybackActiveTrackChanged event fired. This meant playerIndex === queueIndex, causing the event handler to skip the player state update. The player state then relied entirely on the saga chain, which goes through a web audio shim (no-op on mobile) and could fail to dispatch playSucceeded. Two changes: - In AudioPlayer, handle the manual skip case (playerIndex === queueIndex) in the PlaybackActiveTrackChanged handler by updating player info directly when the active track UID differs from the player UID - In NowPlayingDrawer, remove redundant setMediaKey calls from onNext and onPrevious since the useEffect watching playCounter/currentUid already handles this, preventing a double-bump https://claude.ai/code/session_01RiJBWs7WdMdhPvDmfNDtnh --- .../mobile/src/components/audio/AudioPlayer.tsx | 17 +++++++++++++++++ .../now-playing-drawer/NowPlayingDrawer.tsx | 6 ++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/mobile/src/components/audio/AudioPlayer.tsx b/packages/mobile/src/components/audio/AudioPlayer.tsx index 3cac27ba79c..f234a42e232 100644 --- a/packages/mobile/src/components/audio/AudioPlayer.tsx +++ b/packages/mobile/src/components/audio/AudioPlayer.tsx @@ -522,6 +522,23 @@ export const AudioPlayer = () => { } } } + } else if (playerIndex === queueIndex) { + // Manual skip (next/previous button): queue index was already updated + // by the reducer, so playerIndex === queueIndex. Update player info + // directly to avoid relying on the saga chain (which uses a web audio + // shim that is a no-op on mobile). + const { track, playerBehavior } = queueTracks[playerIndex] ?? {} + if (track && queueTrackUids[playerIndex] !== uid) { + const { shouldPreview } = calculatePlayerBehavior( + track, + playerBehavior + ) + updatePlayerInfo({ + previewing: shouldPreview, + trackId: track.track_id, + uid: queueTrackUids[playerIndex] + }) + } } const isLongFormContent = diff --git a/packages/mobile/src/components/now-playing-drawer/NowPlayingDrawer.tsx b/packages/mobile/src/components/now-playing-drawer/NowPlayingDrawer.tsx index 0e5e2c83824..5f386296b8e 100644 --- a/packages/mobile/src/components/now-playing-drawer/NowPlayingDrawer.tsx +++ b/packages/mobile/src/components/now-playing-drawer/NowPlayingDrawer.tsx @@ -243,9 +243,8 @@ export const NowPlayingDrawer = memo(function NowPlayingDrawer( dispatch(seek({ seconds: Math.min(track.duration, newPosition) })) } else { dispatch(next({ skip: true })) - setMediaKey((mediaKey) => mediaKey + 1) } - }, [dispatch, setMediaKey, track]) + }, [dispatch, track]) const onPrevious = useCallback(async () => { const { position: currentPosition } = await TrackPlayer.getProgress() @@ -258,12 +257,11 @@ export const NowPlayingDrawer = memo(function NowPlayingDrawer( const shouldGoToPrevious = currentPosition < RESTART_THRESHOLD_SEC if (shouldGoToPrevious) { dispatch(previous()) - setMediaKey((mediaKey) => mediaKey + 1) } else { dispatch(reset({ shouldAutoplay: true })) } } - }, [dispatch, setMediaKey, track]) + }, [dispatch, track]) const onPressScrubberIn = useCallback(() => { setIsGestureEnabled(false) From becb802e7b72bcb57ceb0ea1429c8c254499f194 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 21:18:30 +0000 Subject: [PATCH 2/4] Add changeset for mobile next track fix https://claude.ai/code/session_01RiJBWs7WdMdhPvDmfNDtnh --- .changeset/fix-mobile-next-track.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-mobile-next-track.md diff --git a/.changeset/fix-mobile-next-track.md b/.changeset/fix-mobile-next-track.md new file mode 100644 index 00000000000..84a0ed2cbd1 --- /dev/null +++ b/.changeset/fix-mobile-next-track.md @@ -0,0 +1,5 @@ +--- +"@audius/mobile": patch +--- + +Fix next/previous track buttons in the now playing drawer not playing the new track on mobile From d0da4c230a7312a38fb5301c1e6d97f6fb0ad154 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 14 Apr 2026 22:58:38 +0000 Subject: [PATCH 3/4] Fix race condition in mobile queue setup causing skipped next press handleQueueChange's non-append path awaited reset/play/add BEFORE assigning enqueueTracksJobRef.current, leaving a window where the ref was undefined while the queue was still being built. If the user pressed next in that window, handleQueueIdxChange's await on the ref resolved immediately and the subsequent skip was silently dropped because queueIndex was past the (still empty) RNTP queue length. Wrap the full setup (reset, play, first track load, middle-out enqueue) in a single promise and assign it to enqueueTracksJobRef before any awaits, so handleQueueIdxChange correctly defers the skip until the queue is ready. https://claude.ai/code/session_01RiJBWs7WdMdhPvDmfNDtnh --- .../src/components/audio/AudioPlayer.tsx | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/mobile/src/components/audio/AudioPlayer.tsx b/packages/mobile/src/components/audio/AudioPlayer.tsx index f234a42e232..8554366f56b 100644 --- a/packages/mobile/src/components/audio/AudioPlayer.tsx +++ b/packages/mobile/src/components/audio/AudioPlayer.tsx @@ -777,17 +777,29 @@ export const AudioPlayer = () => { await enqueueTracksJobRef.current enqueueTracksJobRef.current = undefined } else { - await TrackPlayer.reset() - - await TrackPlayer.play() - - const firstTrack = newQueueTracks[queueIndex] - if (!firstTrack) return - - await TrackPlayer.add(await makeTrackData(firstTrack)) - - enqueueTracksJobRef.current = enqueueTracks(newQueueTracks, queueIndex) - await enqueueTracksJobRef.current + // Wrap the full queue setup (reset, play, first track load, middle-out + // enqueue) in a single promise and assign it to enqueueTracksJobRef + // BEFORE any awaits. This ensures that if the user presses next/previous + // before the queue is done building, handleQueueIdxChange awaits this + // promise and defers the skip until the RNTP queue is ready. Without + // this, there was a window where enqueueTracksJobRef was undefined, + // handleQueueIdxChange's await resolved immediately, and the skip was + // silently dropped because queueIndex was beyond the (still empty) + // RNTP queue length. + const setupPromise = (async () => { + await TrackPlayer.reset() + + await TrackPlayer.play() + + const firstTrack = newQueueTracks[queueIndex] + if (!firstTrack) return + + await TrackPlayer.add(await makeTrackData(firstTrack)) + + await enqueueTracks(newQueueTracks, queueIndex) + })() + enqueueTracksJobRef.current = setupPromise + await setupPromise enqueueTracksJobRef.current = undefined } }, [ From c8c5bbe7256fd7847196e3efebc6aa2fdcecdeec Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Apr 2026 01:31:08 +0000 Subject: [PATCH 4/4] Explicitly play after skip in mobile queue index change TrackPlayer.skip() in RNTP v4 doesn't reliably continue playback when called shortly after queue manipulation - it can leave the player in a Ready state instead of Playing, which is exactly what happens when handleQueueIdxChange skips to the new track right after the initial queue has been built. The drawer shows the new track (from the saga's playSucceeded) but the audio keeps playing the old track because the skip did not resume playback. Explicitly call TrackPlayer.play() after skip() to force playback. This is the audio-switch the saga chain cannot do on mobile, since the web audioPlayer is a no-op shim there. Also wrap the setupPromise body in try/catch so a failing RNTP call doesn't leave enqueueTracksJobRef holding a rejected promise, which would make subsequent awaits in handleQueueIdxChange throw silently. https://claude.ai/code/session_01RiJBWs7WdMdhPvDmfNDtnh --- .../src/components/audio/AudioPlayer.tsx | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/mobile/src/components/audio/AudioPlayer.tsx b/packages/mobile/src/components/audio/AudioPlayer.tsx index 8554366f56b..30b14be4011 100644 --- a/packages/mobile/src/components/audio/AudioPlayer.tsx +++ b/packages/mobile/src/components/audio/AudioPlayer.tsx @@ -786,17 +786,25 @@ export const AudioPlayer = () => { // handleQueueIdxChange's await resolved immediately, and the skip was // silently dropped because queueIndex was beyond the (still empty) // RNTP queue length. + // + // The body is wrapped in try/catch so that a failing RNTP call doesn't + // leave enqueueTracksJobRef pointing at a rejected promise, which would + // make subsequent awaits in handleQueueIdxChange throw silently. const setupPromise = (async () => { - await TrackPlayer.reset() + try { + await TrackPlayer.reset() - await TrackPlayer.play() + await TrackPlayer.play() - const firstTrack = newQueueTracks[queueIndex] - if (!firstTrack) return + const firstTrack = newQueueTracks[queueIndex] + if (!firstTrack) return - await TrackPlayer.add(await makeTrackData(firstTrack)) + await TrackPlayer.add(await makeTrackData(firstTrack)) - await enqueueTracks(newQueueTracks, queueIndex) + await enqueueTracks(newQueueTracks, queueIndex) + } catch (e) { + console.warn('handleQueueChange setup error:', e) + } })() enqueueTracksJobRef.current = setupPromise await setupPromise @@ -822,7 +830,17 @@ export const AudioPlayer = () => { queueIndex !== playerIdx && queueIndex < queue.length ) { - await TrackPlayer.skip(queueIndex) + try { + await TrackPlayer.skip(queueIndex) + // RNTP v4's skip() does not reliably continue playback when called + // shortly after queue setup; it can leave the player in a Ready + // state instead of Playing. Explicitly call play() to ensure the + // new track actually plays. This is the audio-switch that the + // saga chain cannot do on mobile (audioPlayer is a no-op shim). + await TrackPlayer.play() + } catch (e) { + console.warn('TrackPlayer.skip failed:', e) + } } }, [queueIndex])