From fca832a89bb08bc75a947147990731fdfd6251b9 Mon Sep 17 00:00:00 2001 From: skalthoff <32023561+skalthoff@users.noreply.github.com> Date: Mon, 12 Jan 2026 13:29:08 -0800 Subject: [PATCH] feat: Enhance PlaybackService with proactive buffering and queue management for improved background playback --- ios/Gemfile.lock | 16 +++++++------- ios/Podfile.lock | 2 +- src/player/index.ts | 51 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 03c39de9..ac42bf76 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -111,17 +111,16 @@ GEM ffi (>= 1.15.0) event_emitter (0.2.6) excon (0.112.0) - faraday (1.10.4) + faraday (1.8.0) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) + faraday-httpclient (~> 1.0.1) faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) + faraday-net_http_persistent (~> 1.1) faraday-patron (~> 1.0) faraday-rack (~> 1.0) - faraday-retry (~> 1.0) + multipart-post (>= 1.2, < 3) ruby2_keywords (>= 0.0.4) faraday-cookie_jar (0.0.7) faraday (>= 0.8.0) @@ -130,13 +129,10 @@ GEM faraday-em_synchrony (1.0.0) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.1.0) - multipart-post (~> 2.0) faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) - faraday-retry (1.0.3) faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.0) @@ -255,7 +251,8 @@ GEM mime-types-data (3.2025.0610) mini_magick (4.13.2) mini_mime (1.1.5) - minitest (5.25.5) + minitest (6.0.1) + prism (~> 1.5) molinillo (0.8.0) multi_json (1.15.0) multipart-post (2.4.1) @@ -270,6 +267,7 @@ GEM ffi os (1.1.4) plist (3.7.2) + prism (1.7.0) public_suffix (4.0.7) rake (13.3.0) rbnacl (3.4.0) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 1664c8aa..1e9b8c4c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3598,7 +3598,7 @@ SPEC CHECKSUMS: Gifu: 9f7e52357d41c0739709019eb80a71ad9aab1b6d glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 google-cast-sdk: 32f65af50d164e3c475e79ad123db3cc26fbcd37 - hermes-engine: 83ac7cadb2a3a158ae6d9e4192417c5232065e99 + hermes-engine: 6c2b048484247d03ebf5a9bbe01dd70c28654a1e MMKVCore: f2dd4c9befea04277a55e84e7812f930537993df NitroFetch: 660adfb47f84b28db664f97b50e5dc28506ab6c1 NitroMmkv: 8ed7ef6f41b91785fc580c975f68d6d675214767 diff --git a/src/player/index.ts b/src/player/index.ts index 0405aaff..b3910f56 100644 --- a/src/player/index.ts +++ b/src/player/index.ts @@ -1,12 +1,18 @@ import TrackPlayer, { Event } from 'react-native-track-player' import { CarPlay } from 'react-native-carplay' import { previous, skip } from '../hooks/player/functions/controls' +import { usePlayerQueueStore } from '../stores/player/queue' +import { optimizePlayerQueue } from './helpers/gapless' +import { PREFETCH_THRESHOLD_SECONDS } from '../configs/gapless.config' /** * Jellify Playback Service. * * Sets up event listeners for remote control events and - * runs for the duration of the app lifecycle + * runs for the duration of the app lifecycle. + * + * Also handles background queue optimization to ensure tracks + * are buffered before they're needed (fixes iOS background playback). */ export async function PlaybackService() { TrackPlayer.addEventListener(Event.RemotePlay, async () => { @@ -23,6 +29,49 @@ export async function PlaybackService() { TrackPlayer.addEventListener(Event.RemoteSeek, async (event) => { await TrackPlayer.seekTo(event.position) }) + + /** + * Monitor playback progress to proactively buffer upcoming tracks. + * This runs even when the app is backgrounded, ensuring the next track + * is ready before the current one ends. + */ + TrackPlayer.addEventListener(Event.PlaybackProgressUpdated, async (event) => { + const { position, duration } = event + + if (!duration || duration <= 0) return + + const remainingSeconds = duration - position + + // When approaching the end of a track, ensure upcoming tracks are in the queue + if (remainingSeconds <= PREFETCH_THRESHOLD_SECONDS && remainingSeconds > 0) { + const queue = usePlayerQueueStore.getState().queue + const currentIndex = usePlayerQueueStore.getState().currentIndex + + if (queue.length > 0 && currentIndex !== undefined) { + await optimizePlayerQueue(queue, currentIndex) + } + } + }) + + /** + * Handle the case where the player queue ends unexpectedly. + * This can happen if iOS throttles network requests for backgrounded apps. + * When the app queue has more tracks, add them and resume playback. + */ + TrackPlayer.addEventListener(Event.PlaybackQueueEnded, async () => { + const queue = usePlayerQueueStore.getState().queue + const currentIndex = usePlayerQueueStore.getState().currentIndex + + // If there are more tracks in the app queue that aren't in the player queue + if (currentIndex !== undefined && currentIndex < queue.length - 1) { + const remainingTracks = queue.slice(currentIndex + 1) + + if (remainingTracks.length > 0) { + await TrackPlayer.add(remainingTracks) + await TrackPlayer.play() + } + } + }) } export function registerAutoService(onConnect: () => void, onDisconnect: () => void) {