From 329ca138ed383f45e6e87433c72ce5880b23cede Mon Sep 17 00:00:00 2001
From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com>
Date: Mon, 8 Sep 2025 23:21:34 -0500
Subject: [PATCH] Player Render Optimizations, Better DeviceProfile Handling
(#520)
removes hooks from player provider in lieu of nonreactive functions and data fetching. In testing this fixed the issue where the player provider was rerendering when the queue or the currently active track changed
Use a UUID to distinguish between DeviceProfiles, not the name. This means that media info from Jellyfin will be properly refetched if the user selects a different audio quality
mini player rendering optimization fixes, since we don't need that rerendering 4 times a second when updates are only distinguishable every whole second. Also brings some style fixes that reduces vertical height usage
update sentry sdk while we're in there doing debugging
---
ios/Podfile.lock | 14 +-
maestro/tests/6-settings.yaml | 2 -
package.json | 4 +-
patches/@sentry+react-native+6.17.0.patch | 26 --
.../mutations/download/offlineModeUtils.ts | 5 +-
src/api/mutations/download/utils/index.ts | 24 ++
src/api/queries/download/constants/index.ts | 8 +
src/api/queries/download/index.ts | 8 +-
src/api/queries/media/keys.ts | 2 +-
src/components/Global/helpers/slider.tsx | 2 +-
src/components/Player/components/scrubber.tsx | 6 +-
src/components/Player/mini-player.tsx | 148 ++++++------
src/player/config.ts | 10 +
src/providers/Player/functions/index.ts | 5 +-
.../Player/functions/initialization.ts | 22 ++
src/providers/Player/hooks/mutations.ts | 27 ---
src/providers/Player/index.tsx | 145 ++++++------
src/utils/device-profiles.ts | 3 +-
yarn.lock | 224 +++++++++---------
19 files changed, 335 insertions(+), 350 deletions(-)
delete mode 100644 patches/@sentry+react-native+6.17.0.patch
create mode 100644 src/api/mutations/download/utils/index.ts
create mode 100644 src/api/queries/download/constants/index.ts
create mode 100644 src/providers/Player/functions/initialization.ts
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 8783cf05..24168f9a 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -1893,7 +1893,7 @@ PODS:
- SocketRocket
- SSZipArchive (~> 2.4.3)
- Yoga
- - react-native-pager-view (7.0.0):
+ - react-native-pager-view (6.9.1):
- boost
- DoubleConversion
- fast_float
@@ -2816,7 +2816,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- - RNSentry (6.17.0):
+ - RNSentry (7.0.1):
- boost
- DoubleConversion
- fast_float
@@ -2843,7 +2843,7 @@ PODS:
- ReactCodegen
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- - Sentry/HybridSDK (= 8.53.1)
+ - Sentry/HybridSDK (= 8.53.2)
- SocketRocket
- Yoga
- RNWorklets (0.4.1):
@@ -2938,7 +2938,7 @@ PODS:
- SDWebImageWebPCoder (0.8.5):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.10)
- - Sentry/HybridSDK (8.53.1)
+ - Sentry/HybridSDK (8.53.2)
- SocketRocket (0.7.1)
- SSZipArchive (2.4.3)
- SwiftAudioEx (1.1.0)
@@ -3311,7 +3311,7 @@ SPEC CHECKSUMS:
react-native-mmkv: 560d39188cf4d817fb34b0df79426a298934ee7d
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
react-native-ota-hot-update: 5c8fe703c7a789f6de651030e4740923c77fc610
- react-native-pager-view: 0b0b445d3cb9f8e9972842edf6ddf892b46bdc55
+ react-native-pager-view: a0516effb17ca5120ac2113bfd21b91130ad5748
react-native-safe-area-context: c6e2edd1c1da07bdce287fa9d9e60c5f7b514616
react-native-track-player: 89d8e641c83a89bea5dee43c381be743282553e9
react-native-vector-icons-material-design-icons: c502df5b988ce85d6c7d2b7ee909818315760b82
@@ -3355,11 +3355,11 @@ SPEC CHECKSUMS:
RNReactNativeHapticFeedback: be4f1b4bf0398c30b59b76ed92ecb0a2ff3a69c6
RNReanimated: ee96d03fe3713993a30cc205522792b4cb08e4f9
RNScreens: 0bbf16c074ae6bb1058a7bf2d1ae017f4306797c
- RNSentry: 95e1ed0ede28a4af58aaafedeac9fcfaba0e89ce
+ RNSentry: 6c63debc7b22a00cbf7d1c9ed8de43e336216545
RNWorklets: e8335dff9d27004709f58316985769040cd1e8f2
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d
- Sentry: 1e4e974d45f09d153af4b30b42acfb1c79e957d3
+ Sentry: 59993bffde4a1ac297ba6d268dc4bbce068d7c1b
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
SSZipArchive: fe6a26b2a54d5a0890f2567b5cc6de5caa600aef
SwiftAudioEx: f6aa653770f3a0d3851edaf8d834a30aee4a7646
diff --git a/maestro/tests/6-settings.yaml b/maestro/tests/6-settings.yaml
index 77f62564..f96561db 100644
--- a/maestro/tests/6-settings.yaml
+++ b/maestro/tests/6-settings.yaml
@@ -65,8 +65,6 @@ appId: com.jellify
# Test About (Info) Tab
- tapOn:
text: "About"
-- assertVisible:
- text: "Made with love"
- assertVisible:
text: "View Source"
- assertVisible:
diff --git a/package.json b/package.json
index 363243df..92550556 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,7 @@
"@react-navigation/material-top-tabs": "^7.3.7",
"@react-navigation/native": "^7.1.17",
"@react-navigation/native-stack": "^7.3.26",
- "@sentry/react-native": "6.17.0",
+ "@sentry/react-native": "7.0.1",
"@shopify/flash-list": "^2.0.3",
"@tamagui/config": "^1.132.23",
"@tanstack/query-async-storage-persister": "^5.87.1",
@@ -79,7 +79,7 @@
"react-native-linear-gradient": "^2.8.3",
"react-native-mmkv": "3.3.0",
"react-native-ota-hot-update": "2.3.1",
- "react-native-pager-view": "^7.0.0",
+ "react-native-pager-view": "^6.9.1",
"react-native-reanimated": "4.0.2",
"react-native-safe-area-context": "^5.6.1",
"react-native-screens": "4.16.0",
diff --git a/patches/@sentry+react-native+6.17.0.patch b/patches/@sentry+react-native+6.17.0.patch
deleted file mode 100644
index 3048e209..00000000
--- a/patches/@sentry+react-native+6.17.0.patch
+++ /dev/null
@@ -1,26 +0,0 @@
-diff --git a/node_modules/@sentry/react-native/ios/RNSentry.mm b/node_modules/@sentry/react-native/ios/RNSentry.mm
-index 267c41c..b731bad 100644
---- a/node_modules/@sentry/react-native/ios/RNSentry.mm
-+++ b/node_modules/@sentry/react-native/ios/RNSentry.mm
-@@ -819,7 +819,7 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys
- {
- #if SENTRY_PROFILING_ENABLED
- try {
-- facebook::hermes::HermesRuntime::enableSamplingProfiler();
-+// facebook::hermes::HermesRuntime::enableSamplingProfiler();
- if (nativeProfileTraceId == nil && nativeProfileStartTime == 0 && platformProfilers) {
- # if SENTRY_TARGET_PROFILING_SUPPORTED
- nativeProfileTraceId = [RNSentryId newId];
-@@ -879,10 +879,10 @@ + (SentryUser *_Nullable)userFrom:(NSDictionary *)userKeys
- nativeProfileTraceId = nil;
- nativeProfileStartTime = 0;
-
-- facebook::hermes::HermesRuntime::disableSamplingProfiler();
-+// facebook::hermes::HermesRuntime::;
- std::stringstream ss;
- // Before RN 0.69 Hermes used llvh::raw_ostream (profiling is supported for 0.69 and newer)
-- facebook::hermes::HermesRuntime::dumpSampledTraceToStream(ss);
-+// facebook::hermes::HermesRuntime::dumpSampledTraceToStream(ss);
-
- std::string s = ss.str();
- NSString *data = [NSString stringWithCString:s.c_str()
diff --git a/src/api/mutations/download/offlineModeUtils.ts b/src/api/mutations/download/offlineModeUtils.ts
index ba7bfa0b..d47d09af 100644
--- a/src/api/mutations/download/offlineModeUtils.ts
+++ b/src/api/mutations/download/offlineModeUtils.ts
@@ -8,9 +8,8 @@ import {
JellifyDownloadProgress,
JellifyDownloadProgressState,
} from '../../../types/JellifyDownload'
-import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { queryClient } from '../../../constants/query-client'
-import { QueryKeys } from '../../../enums/query-keys'
+import { AUDIO_CACHE_QUERY } from '../../queries/download/constants'
export async function downloadJellyfinFile(
url: string,
@@ -158,7 +157,7 @@ export const saveAudio = async (
return false
}
mmkv.set(MMKV_OFFLINE_MODE_KEYS.AUDIO_CACHE, JSON.stringify(existingArray))
- queryClient.invalidateQueries({ queryKey: [QueryKeys.AudioCache] })
+ queryClient.invalidateQueries(AUDIO_CACHE_QUERY)
return true
}
diff --git a/src/api/mutations/download/utils/index.ts b/src/api/mutations/download/utils/index.ts
new file mode 100644
index 00000000..e1872f5f
--- /dev/null
+++ b/src/api/mutations/download/utils/index.ts
@@ -0,0 +1,24 @@
+import { Api } from '@jellyfin/sdk/lib/api'
+import { BaseItemDto, DeviceProfile } from '@jellyfin/sdk/lib/generated-client'
+import { getAudioCache, saveAudio } from '../offlineModeUtils'
+import { mapDtoToTrack } from '../../../../utils/mappings'
+
+export default async function saveAudioItem(
+ api: Api | undefined,
+ item: BaseItemDto,
+ deviceProfile: DeviceProfile,
+ autoCached: boolean = false,
+) {
+ if (!api) return Promise.reject('API Instance not set')
+
+ const downloadedTracks = getAudioCache()
+
+ // If we already have this track downloaded, resolve the promise
+ if (downloadedTracks?.filter((download) => download.item.Id === item.Id).length ?? 0 > 0)
+ return Promise.resolve(false)
+
+ const track = mapDtoToTrack(api, item, downloadedTracks ?? [], deviceProfile)
+
+ // TODO: fix download progresses
+ return saveAudio(track, () => {}, autoCached)
+}
diff --git a/src/api/queries/download/constants/index.ts b/src/api/queries/download/constants/index.ts
new file mode 100644
index 00000000..cc262071
--- /dev/null
+++ b/src/api/queries/download/constants/index.ts
@@ -0,0 +1,8 @@
+import { getAudioCache } from '../../../mutations/download/offlineModeUtils'
+import DownloadQueryKeys from '../keys'
+
+export const AUDIO_CACHE_QUERY = {
+ queryKey: [DownloadQueryKeys.DownloadedTracks],
+ queryFn: getAudioCache,
+ staleTime: Infinity, // Never stale, we will manually refetch when downloads are completed
+}
diff --git a/src/api/queries/download/index.ts b/src/api/queries/download/index.ts
index be825bb6..638b3c1d 100644
--- a/src/api/queries/download/index.ts
+++ b/src/api/queries/download/index.ts
@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query'
import fetchStorageInUse from './utils/storage-in-use'
import { getAudioCache } from '../../mutations/download/offlineModeUtils'
import DownloadQueryKeys from './keys'
+import { AUDIO_CACHE_QUERY } from './constants'
export const useStorageInUse = () =>
useQuery({
@@ -10,12 +11,7 @@ export const useStorageInUse = () =>
queryFn: fetchStorageInUse,
})
-export const useAllDownloadedTracks = () =>
- useQuery({
- queryKey: [DownloadQueryKeys.DownloadedTracks],
- queryFn: getAudioCache,
- staleTime: Infinity, // Never stale, we will manually refetch when downloads are completed
- })
+export const useAllDownloadedTracks = () => useQuery(AUDIO_CACHE_QUERY)
export const useDownloadedTracks = (itemIds: (string | null | undefined)[]) =>
useAllDownloadedTracks().data?.filter((download) => itemIds.includes(download.item.Id))
diff --git a/src/api/queries/media/keys.ts b/src/api/queries/media/keys.ts
index ef842ac5..cbd10913 100644
--- a/src/api/queries/media/keys.ts
+++ b/src/api/queries/media/keys.ts
@@ -9,7 +9,7 @@ interface MediaInfoQueryProps {
const MediaInfoQueryKey = ({ api, deviceProfile, itemId }: MediaInfoQueryProps) => [
'MEDIA_INFO',
api,
- deviceProfile?.Name,
+ deviceProfile?.Id,
itemId,
]
diff --git a/src/components/Global/helpers/slider.tsx b/src/components/Global/helpers/slider.tsx
index 2b43b9d4..d55a6d06 100644
--- a/src/components/Global/helpers/slider.tsx
+++ b/src/components/Global/helpers/slider.tsx
@@ -51,7 +51,7 @@ export function HorizontalSlider({ value, max, width, props }: SliderProps): Rea
{...props}
>
-
+
+
{nowPlaying && (
- <>
-
-
-
-
-
- navigation.navigate('PlayerRoot', { screen: 'PlayerScreen' })
- }
- >
-
- {api && (
-
-
-
- )}
-
-
-
-
-
+
+
+
+ navigation.navigate('PlayerRoot', { screen: 'PlayerScreen' })
+ }
+ >
+
+ {api && (
-
-
-
- {nowPlaying?.title ?? 'Nothing Playing'}
-
-
-
-
-
- {nowPlaying?.artist ?? ''}
-
-
-
+
-
+ )}
+
-
+
+
+
-
-
+
+
+
+ {nowPlaying?.title ?? 'Nothing Playing'}
+
+
+
+
+
+ {nowPlaying?.artist ?? ''}
+
+
+
+
+
+
+
+
-
-
- >
+
+
+
+
+
)}
-
+
)
})
function MiniPlayerRuntime(): React.JSX.Element {
- const { position } = useProgress(UPDATE_INTERVAL)
+ const { position } = useProgress(MINIPLAYER_UPDATE_INTERVAL)
const { data: nowPlaying } = useNowPlaying()
const { duration } = nowPlaying!
@@ -198,11 +187,11 @@ function MiniPlayerRuntime(): React.JSX.Element {
}
function MiniPlayerProgress(): React.JSX.Element {
- const progress = useProgress(UPDATE_INTERVAL)
+ const progress = useProgress(MINIPLAYER_UPDATE_INTERVAL)
return (