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 diff --git a/packages/mobile/src/components/audio/AudioPlayer.tsx b/packages/mobile/src/components/audio/AudioPlayer.tsx index 3cac27ba79c..30b14be4011 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 = @@ -760,17 +777,37 @@ 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. + // + // 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 () => { + try { + await TrackPlayer.reset() + + await TrackPlayer.play() + + const firstTrack = newQueueTracks[queueIndex] + if (!firstTrack) return + + await TrackPlayer.add(await makeTrackData(firstTrack)) + + await enqueueTracks(newQueueTracks, queueIndex) + } catch (e) { + console.warn('handleQueueChange setup error:', e) + } + })() + enqueueTracksJobRef.current = setupPromise + await setupPromise enqueueTracksJobRef.current = undefined } }, [ @@ -793,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]) 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)