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
This commit is contained in:
Violet Caulfield
2025-09-08 23:21:34 -05:00
committed by GitHub
parent ef56ca217a
commit 329ca138ed
19 changed files with 335 additions and 350 deletions

View File

@@ -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

View File

@@ -65,8 +65,6 @@ appId: com.jellify
# Test About (Info) Tab
- tapOn:
text: "About"
- assertVisible:
text: "Made with love"
- assertVisible:
text: "View Source"
- assertVisible:

View File

@@ -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",

View File

@@ -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()

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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))

View File

@@ -9,7 +9,7 @@ interface MediaInfoQueryProps {
const MediaInfoQueryKey = ({ api, deviceProfile, itemId }: MediaInfoQueryProps) => [
'MEDIA_INFO',
api,
deviceProfile?.Name,
deviceProfile?.Id,
itemId,
]

View File

@@ -51,7 +51,7 @@ export function HorizontalSlider({ value, max, width, props }: SliderProps): Rea
{...props}
>
<JellifySliderTrack size='$2'>
<JellifyActiveSliderTrack size={'$'} />
<JellifyActiveSliderTrack />
</JellifySliderTrack>
<JellifySliderThumb
circular

View File

@@ -13,10 +13,10 @@ import { useDisplayAudioQualityBadge } from '../../../stores/settings/player'
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
// Create a simple pan gesture
const scrubGesture = Gesture.Pan().runOnJS(true)
const scrubGesture = Gesture.Pan()
export default function Scrubber(): React.JSX.Element {
const { mutate: seekTo, isPending: seekPending, mutateAsync: seekToAsync } = useSeekTo()
const { isPending: seekPending, mutateAsync: seekToAsync } = useSeekTo()
const { data: nowPlaying } = useNowPlaying()
const { width } = useSafeAreaFrame()
@@ -90,7 +90,7 @@ export default function Scrubber(): React.JSX.Element {
}, 100)
})
},
[useSeekTo],
[seekToAsync],
)
// Memoize time calculations to prevent unnecessary re-renders

View File

@@ -4,10 +4,10 @@ import { useNavigation } from '@react-navigation/native'
import { Text } from '../Global/helpers/text'
import TextTicker from 'react-native-text-ticker'
import PlayPauseButton from './components/buttons'
import { ProgressMultiplier, TextTickerConfig } from './component.config'
import { TextTickerConfig } from './component.config'
import { useJellifyContext } from '../../providers'
import { RunTimeSeconds } from '../Global/helpers/time-codes'
import { UPDATE_INTERVAL } from '../../player/config'
import { MINIPLAYER_UPDATE_INTERVAL } from '../../player/config'
import { Progress as TrackPlayerProgress } from 'react-native-track-player'
import { useProgress } from '../../providers/Player/hooks/queries'
@@ -80,93 +80,82 @@ export const Miniplayer = React.memo(function Miniplayer(): React.JSX.Element {
)
return (
<ZStack height={'$7'} testID='miniplayer-test-id'>
<View testID='miniplayer-test-id' borderTopWidth={'$0.75'} borderColor={'$borderColor'}>
{nowPlaying && (
<>
<GestureDetector gesture={gesture}>
<YStack>
<MiniPlayerProgress />
<XStack
alignItems='center'
margin={0}
padding={0}
height={'$6'}
onPress={() =>
navigation.navigate('PlayerRoot', { screen: 'PlayerScreen' })
}
>
<YStack
justify='center'
alignItems='center'
marginVertical={'auto'}
marginLeft={'$2'}
>
{api && (
<Animated.View
entering={FadeIn}
exiting={FadeOut}
key={`${nowPlaying!.item.AlbumId}-album-image`}
>
<ItemImage
item={nowPlaying!.item}
width={'$12'}
height={'$12'}
/>
</Animated.View>
)}
</YStack>
<YStack
alignContent='flex-start'
justifyContent='center'
marginLeft={'$2'}
marginVertical={'auto'}
flex={6}
>
<MiniPlayerRuntime />
<GestureDetector gesture={gesture}>
<YStack>
<XStack
paddingVertical={'$1'}
alignItems='center'
onPress={() =>
navigation.navigate('PlayerRoot', { screen: 'PlayerScreen' })
}
>
<YStack justify='center' alignItems='center' marginLeft={'$2'}>
{api && (
<Animated.View
entering={FadeIn}
exiting={FadeOut}
key={`${nowPlaying!.item.AlbumId}-mini-player-song-info`}
key={`${nowPlaying!.item.AlbumId}-album-image`}
>
<View width={'100%'}>
<TextTicker {...TextTickerConfig}>
<Text bold width={'100%'}>
{nowPlaying?.title ?? 'Nothing Playing'}
</Text>
</TextTicker>
<TextTicker {...TextTickerConfig}>
<Text height={'$0.5'} width={'100%'}>
{nowPlaying?.artist ?? ''}
</Text>
</TextTicker>
</View>
<ItemImage
item={nowPlaying!.item}
width={'$12'}
height={'$12'}
/>
</Animated.View>
</YStack>
)}
</YStack>
<XStack
justifyContent='flex-end'
alignItems='center'
flex={2}
marginRight={'$2'}
height={'$6'}
<YStack
alignContent='flex-start'
justifyContent='center'
marginLeft={'$2'}
flex={6}
>
<MiniPlayerRuntime />
<Animated.View
entering={FadeIn}
exiting={FadeOut}
key={`${nowPlaying!.item.AlbumId}-mini-player-song-info`}
>
<PlayPauseButton size={getToken('$12')} />
</XStack>
<View width={'100%'}>
<TextTicker {...TextTickerConfig}>
<Text bold width={'100%'}>
{nowPlaying?.title ?? 'Nothing Playing'}
</Text>
</TextTicker>
<TextTicker {...TextTickerConfig}>
<Text height={'$0.5'} width={'100%'}>
{nowPlaying?.artist ?? ''}
</Text>
</TextTicker>
</View>
</Animated.View>
</YStack>
<XStack
justifyContent='flex-end'
alignItems='center'
flex={2}
marginRight={'$2'}
>
<PlayPauseButton size={getToken('$12')} />
</XStack>
</YStack>
</GestureDetector>
</>
</XStack>
<MiniPlayerProgress />
</YStack>
</GestureDetector>
)}
</ZStack>
</View>
)
})
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 (
<Progress
size={'$1'}
size={'$0.75'}
value={calculateProgressPercentage(progress)}
backgroundColor={'$borderColor'}
borderRadius={0}
@@ -213,8 +202,5 @@ function MiniPlayerProgress(): React.JSX.Element {
}
function calculateProgressPercentage(progress: TrackPlayerProgress | undefined): number {
return Math.round(
((progress!.position * ProgressMultiplier) / (progress!.duration * ProgressMultiplier)) *
100,
)
return Math.round((progress!.position / progress!.duration) * 100)
}

View File

@@ -5,6 +5,16 @@
*/
export const UPDATE_INTERVAL: number = 250
/**
* Interval in milliseconds for the miniplayer progress updates from the track player
*
* Lower value provides smoother progress movement, but because of the math involved to
* determine playback progress, updates are only visible every full second.
*
* This is therefore set to 1000ms
*/
export const MINIPLAYER_UPDATE_INTERVAL: number = 1000
/**
* Indicates the seconds the progress position must be
* less than in order to do a skip to the previous

View File

@@ -9,7 +9,7 @@ import {
SHUFFLED_QUERY_KEY,
UNSHUFFLED_QUEUE_QUERY_KEY,
} from '../constants/query-keys'
import { CURRENT_INDEX_QUERY, QUEUE_QUERY } from '../constants/queries'
import { CURRENT_INDEX_QUERY, NOW_PLAYING_QUERY, QUEUE_QUERY } from '../constants/queries'
export function getActiveIndex(): number | undefined {
return queryClient.getQueryData(ACTIVE_INDEX_QUERY_KEY) as number | undefined
@@ -52,6 +52,7 @@ export function setShuffled(shuffled: boolean): void {
}
export function handleActiveTrackChanged(): void {
queryClient.invalidateQueries(NOW_PLAYING_QUERY)
queryClient.ensureQueryData(QUEUE_QUERY)
queryClient.ensureQueryData(CURRENT_INDEX_QUERY)
queryClient.invalidateQueries(CURRENT_INDEX_QUERY)
}

View File

@@ -0,0 +1,22 @@
import { isUndefined } from 'lodash'
import { getActiveIndex, getPlayQueue } from '.'
import TrackPlayer from 'react-native-track-player'
export default async function Initialize() {
const storedPlayQueue = getPlayQueue()
const storedIndex = getActiveIndex()
console.debug(
`StoredIndex: ${storedIndex}, storedPlayQueue: ${storedPlayQueue?.map((track, index) => index)}`,
)
if (!isUndefined(storedPlayQueue) && !isUndefined(storedIndex)) {
console.debug('Initializing play queue from storage')
await TrackPlayer.reset()
await TrackPlayer.add(storedPlayQueue)
await TrackPlayer.skip(storedIndex)
console.debug('Initialized play queue from storage')
}
}

View File

@@ -30,33 +30,6 @@ const PLAYER_MUTATION_OPTIONS = {
retry: false,
}
/**
*
* @returns a mutation hook
*/
export const useInitialization = () =>
useMutation({
mutationFn: async () => {
const storedPlayQueue = getPlayQueue()
const storedIndex = getActiveIndex()
console.debug(
`StoredIndex: ${storedIndex}, storedPlayQueue: ${storedPlayQueue?.map((track, index) => index)}`,
)
if (!isUndefined(storedPlayQueue) && !isUndefined(storedIndex)) {
console.debug('Initializing play queue from storage')
await TrackPlayer.reset()
await TrackPlayer.add(storedPlayQueue)
await TrackPlayer.skip(storedIndex)
console.debug('Initialized play queue from storage')
}
},
onSuccess: async () => console.debug('Play Queue initialized from queryables'),
})
/**
* A mutation to handle starting playback
*/

View File

@@ -1,19 +1,23 @@
import { usePerformanceMonitor } from '../../hooks/use-performance-monitor'
import { Event, State, useTrackPlayerEvents } from 'react-native-track-player'
import { refetchNowPlaying } from './functions/queries'
import { createContext, useEffect } from 'react'
import { useAudioNormalization, useInitialization } from './hooks/mutations'
import { useCurrentIndex, useNowPlaying, useQueue } from './hooks/queries'
import TrackPlayer, { Event, State, useTrackPlayerEvents } from 'react-native-track-player'
import { createContext, useCallback, useEffect } from 'react'
import { handleActiveTrackChanged } from './functions'
import JellifyTrack from '../../types/JellifyTrack'
import { useIsRestoring } from '@tanstack/react-query'
import {
useReportPlaybackProgress,
useReportPlaybackStarted,
useReportPlaybackStopped,
} from '../../api/mutations/playback'
import { useDownloadAudioItem } from '../../api/mutations/download'
import { useAutoDownload } from '../../stores/settings/usage'
import { queryClient } from '../../constants/query-client'
import { NOW_PLAYING_QUERY_KEY } from './constants/query-keys'
import reportPlaybackStopped from '../../api/mutations/playback/functions/playback-stopped'
import reportPlaybackCompleted from '../../api/mutations/playback/functions/playback-completed'
import isPlaybackFinished from '../../api/mutations/playback/utils'
import { useJellifyContext } from '..'
import reportPlaybackProgress from '../../api/mutations/playback/functions/playback-progress'
import reportPlaybackStarted from '../../api/mutations/playback/functions/playback-started'
import calculateTrackVolume from './utils/normalization'
import saveAudioItem from '../../api/mutations/download/utils'
import { useDownloadingDeviceProfile } from '../../stores/device-profile'
import { NOW_PLAYING_QUERY } from './constants/queries'
import Initialize from './functions/initialization'
const PLAYER_EVENTS: Event[] = [
Event.PlaybackActiveTrackChanged,
@@ -26,78 +30,75 @@ interface PlayerContext {}
export const PlayerContext = createContext<PlayerContext>({})
export const PlayerProvider: () => React.JSX.Element = () => {
const { api } = useJellifyContext()
const [autoDownload] = useAutoDownload()
const downloadingDeviceProfile = useDownloadingDeviceProfile()
usePerformanceMonitor('PlayerProvider', 3)
const { mutate: initializePlayQueue } = useInitialization()
const { data: currentIndex } = useCurrentIndex()
const { data: playQueue } = useQueue()
const { data: nowPlaying } = useNowPlaying()
const { mutate: normalizeAudioVolume } = useAudioNormalization()
const { mutate: reportPlaybackStarted } = useReportPlaybackStarted()
const { mutate: reportPlaybackProgress } = useReportPlaybackProgress()
const { mutate: reportPlaybackStopped } = useReportPlaybackStopped()
const [downloadProgress, downloadAudioItem] = useDownloadAudioItem()
const isRestoring = useIsRestoring()
useTrackPlayerEvents(PLAYER_EVENTS, (event) => {
switch (event.type) {
case Event.PlaybackActiveTrackChanged:
if (event.track) normalizeAudioVolume(event.track as JellifyTrack)
const eventHandler = useCallback(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async (event: any) => {
let nowPlaying: JellifyTrack | undefined
handleActiveTrackChanged()
refetchNowPlaying()
switch (event.type) {
case Event.PlaybackActiveTrackChanged:
await handleActiveTrackChanged()
if (event.lastTrack)
reportPlaybackStopped({
track: event.lastTrack as JellifyTrack,
lastPosition: event.lastPosition,
duration: (event.lastTrack as JellifyTrack).duration,
})
break
case Event.PlaybackProgressUpdated:
console.debug(`Completion percentage: ${event.position / event.duration}`)
if (nowPlaying)
reportPlaybackProgress({
track: nowPlaying,
position: event.position,
})
if (event.track) {
nowPlaying = event.track as JellifyTrack
if (event.position / event.duration > 0.3 && autoDownload && nowPlaying)
downloadAudioItem({ item: nowPlaying.item, autoCached: true })
break
case Event.PlaybackState:
switch (event.state) {
case State.Playing:
if (nowPlaying)
reportPlaybackStarted({
track: nowPlaying,
})
break
case State.Paused:
case State.Stopped:
case State.Ended:
if (nowPlaying)
reportPlaybackStopped({
track: nowPlaying,
lastPosition: 0,
duration: nowPlaying.duration,
})
}
break
}
})
const volume = calculateTrackVolume(nowPlaying)
await TrackPlayer.setVolume(volume)
}
if (event.lastTrack)
if (isPlaybackFinished(event.lastPosition, event.lastTrack.duration ?? 1))
await reportPlaybackCompleted(api, event.lastTrack as JellifyTrack)
else await reportPlaybackStopped(api, event.lastTrack as JellifyTrack)
break
case Event.PlaybackProgressUpdated:
console.debug(`Completion percentage: ${event.position / event.duration}`)
nowPlaying = queryClient.getQueryData<JellifyTrack>(NOW_PLAYING_QUERY_KEY)
if (nowPlaying) {
reportPlaybackProgress(api, nowPlaying, event.position)
}
if (event.position / event.duration > 0.3 && autoDownload && nowPlaying)
saveAudioItem(api, nowPlaying.item, downloadingDeviceProfile, true)
break
case Event.PlaybackState:
nowPlaying = queryClient.getQueryData<JellifyTrack>(NOW_PLAYING_QUERY_KEY)
switch (event.state) {
case State.Playing:
if (nowPlaying) reportPlaybackStarted(api, nowPlaying)
queryClient.ensureQueryData(NOW_PLAYING_QUERY)
break
case State.Paused:
case State.Stopped:
case State.Ended:
if (nowPlaying) reportPlaybackStopped(api, nowPlaying)
}
break
}
},
[api, autoDownload],
)
useTrackPlayerEvents(PLAYER_EVENTS, eventHandler)
useEffect(() => {
if (!isRestoring) initializePlayQueue()
if (!isRestoring) Initialize()
}, [isRestoring])
return (

View File

@@ -19,11 +19,11 @@ import {
EncodingContext,
MediaStreamProtocol,
} from '@jellyfin/sdk/lib/generated-client'
import { Platform } from 'react-native'
import { getQualityParams } from './mappings'
import { capitalize } from 'lodash'
import { SourceType } from '../types/JellifyTrack'
import StreamingQuality from '../enums/audio-quality'
import uuid from 'react-native-uuid'
/**
* A constant that defines the options for the {@link useDeviceProfile} hook - building the
@@ -39,6 +39,7 @@ export function getDeviceProfile(
type: SourceType,
): DeviceProfile {
return {
Id: uuid.v4(),
Name: `${capitalize(streamingQuality)} Quality Audio ${capitalize(type)}`,
MaxStaticBitrate:
streamingQuality === 'original'

224
yarn.lock
View File

@@ -2203,96 +2203,96 @@
resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8"
integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==
"@sentry-internal/browser-utils@8.54.0":
version "8.54.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-8.54.0.tgz#2d68c7fa843db867ed98059faf1a750be3eca95a"
integrity sha512-DKWCqb4YQosKn6aD45fhKyzhkdG7N6goGFDeyTaJFREJDFVDXiNDsYZu30nJ6BxMM7uQIaARhPAC5BXfoED3pQ==
"@sentry-internal/browser-utils@10.8.0":
version "10.8.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/browser-utils/-/browser-utils-10.8.0.tgz#a028e8067566cb80026c112cd62b5c119415e337"
integrity sha512-FaQX9eefc8sh3h3ZQy16U73KiH0xgDldXnrFiWK6OeWg8X4bJpnYbLqEi96LgHiQhjnnz+UQP1GDzH5oFuu5fA==
dependencies:
"@sentry/core" "8.54.0"
"@sentry/core" "10.8.0"
"@sentry-internal/feedback@8.54.0":
version "8.54.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-8.54.0.tgz#52c3a63aa5b520eca7acfa1376621e8441984126"
integrity sha512-nQqRacOXoElpE0L0ADxUUII0I3A94niqG9Z4Fmsw6057QvyrV/LvTiMQBop6r5qLjwMqK+T33iR4/NQI5RhsXQ==
"@sentry-internal/feedback@10.8.0":
version "10.8.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-10.8.0.tgz#fa562fe64c806d429f4c33acd8e7ca0f929053f3"
integrity sha512-n7SqgFQItq4QSPG7bCjcZcIwK6AatKnnmSDJ/i6e8jXNIyLwkEuY2NyvTXACxVdO/kafGD5VmrwnTo3Ekc1AMg==
dependencies:
"@sentry/core" "8.54.0"
"@sentry/core" "10.8.0"
"@sentry-internal/replay-canvas@8.54.0":
version "8.54.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-8.54.0.tgz#e57a3893db2bb0ea7ad9dc2a804bb035142fe3ba"
integrity sha512-K/On3OAUBeq/TV2n+1EvObKC+WMV9npVXpVyJqCCyn8HYMm8FUGzuxeajzm0mlW4wDTPCQor6mK9/IgOquUzCw==
"@sentry-internal/replay-canvas@10.8.0":
version "10.8.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-10.8.0.tgz#5adc2f3aa7036fed46415fcb6eabd4d8a6d335e3"
integrity sha512-jC4OOwiNgrlIPeXIPMLkaW53BSS1do+toYHoWzzO5AXGpN6jRhanoSj36FpVuH2N3kFnxKVfVxrwh8L+/3vFWg==
dependencies:
"@sentry-internal/replay" "8.54.0"
"@sentry/core" "8.54.0"
"@sentry-internal/replay" "10.8.0"
"@sentry/core" "10.8.0"
"@sentry-internal/replay@8.54.0":
version "8.54.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-8.54.0.tgz#b92990a51ffbe8d92998ff8188db9e3a6f9d1e18"
integrity sha512-8xuBe06IaYIGJec53wUC12tY2q4z2Z0RPS2s1sLtbA00EvK1YDGuXp96IDD+HB9mnDMrQ/jW5f97g9TvPsPQUg==
"@sentry-internal/replay@10.8.0":
version "10.8.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay/-/replay-10.8.0.tgz#3f14329924ff744611296ce4e9588530e5f168e4"
integrity sha512-9+qDEoEjv4VopLuOzK1zM4LcvcUsvB5N0iJ+FRCM3XzzOCbebJOniXTQbt5HflJc3XLnQNKFdKfTfgj8M/0RKQ==
dependencies:
"@sentry-internal/browser-utils" "8.54.0"
"@sentry/core" "8.54.0"
"@sentry-internal/browser-utils" "10.8.0"
"@sentry/core" "10.8.0"
"@sentry/babel-plugin-component-annotate@3.5.0":
version "3.5.0"
resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.5.0.tgz#1b0d01f903b725da876117d551610085c3dd21c7"
integrity sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw==
"@sentry/babel-plugin-component-annotate@4.3.0":
version "4.3.0"
resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.3.0.tgz#c5b6cbb986952596d3ad233540a90a1fd18bad80"
integrity sha512-OuxqBprXRyhe8Pkfyz/4yHQJc5c3lm+TmYWSSx8u48g5yKewSQDOxkiLU5pAk3WnbLPy8XwU/PN+2BG0YFU9Nw==
"@sentry/browser@8.54.0":
version "8.54.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-8.54.0.tgz#5487075908aac564892e689e1b6d233fdb314f5b"
integrity sha512-BgUtvxFHin0fS0CmJVKTLXXZcke0Av729IVfi+2fJ4COX8HO7/HAP02RKaSQGmL2HmvWYTfNZ7529AnUtrM4Rg==
"@sentry/browser@10.8.0":
version "10.8.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-10.8.0.tgz#f33b95f48e3967db661db1666ad3e1dc0a6922fb"
integrity sha512-2J7HST8/ixCaboq17yFn/j/OEokXSXoCBMXRrFx4FKJggKWZ90e2Iau5mP/IPPhrW+W9zCptCgNMY0167wS4qA==
dependencies:
"@sentry-internal/browser-utils" "8.54.0"
"@sentry-internal/feedback" "8.54.0"
"@sentry-internal/replay" "8.54.0"
"@sentry-internal/replay-canvas" "8.54.0"
"@sentry/core" "8.54.0"
"@sentry-internal/browser-utils" "10.8.0"
"@sentry-internal/feedback" "10.8.0"
"@sentry-internal/replay" "10.8.0"
"@sentry-internal/replay-canvas" "10.8.0"
"@sentry/core" "10.8.0"
"@sentry/cli-darwin@2.47.0":
version "2.47.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.47.0.tgz#171d4ed94a035b35e7cba21e133351fa1d8a5664"
integrity sha512-xEFppdMQogV1A85A/s+Al1VH0NHXk7syy+5BL/jYd168FPeVB3iERP0AwP4h9UhR3/wTe1lTb+tfOKpXrECLCw==
"@sentry/cli-darwin@2.53.0":
version "2.53.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.53.0.tgz#0584f5a4a376c9373f91ad5e1d9194278be2aed6"
integrity sha512-NNPfpILMwKgpHiyJubHHuauMKltkrgLQ5tvMdxNpxY60jBNdo5VJtpESp4XmXlnidzV4j1z61V4ozU6ttDgt5Q==
"@sentry/cli-linux-arm64@2.47.0":
version "2.47.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.47.0.tgz#9802fe08164217483926ce8a1becf45f740fea20"
integrity sha512-qjF87W0Vo5vITbm4GXjtX8uQCDRg2gVT0yP1Uz12IuBri80iJj66IANX1wbae2mG2Io1Ibc4AKN5FWd2HpPiKw==
"@sentry/cli-linux-arm64@2.53.0":
version "2.53.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.53.0.tgz#04a73b2592edf10d6e06957905becc98692605b1"
integrity sha512-xY/CZ1dVazsSCvTXzKpAgXaRqfljVfdrFaYZRUaRPf1ZJRGa3dcrivoOhSIeG/p5NdYtMvslMPY9Gm2MT0M83A==
"@sentry/cli-linux-arm@2.47.0":
version "2.47.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.47.0.tgz#32221dcfe46024babf43d846a1e2d05a57b153a8"
integrity sha512-tE8gDcp2qFCTtndz1ViLAo+JNQEEjniBFJAWIFh1utJKwBxBStB0JDporOZHvWUcnSCP5F+W59iuir2YAAQh/w==
"@sentry/cli-linux-arm@2.53.0":
version "2.53.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.53.0.tgz#caa1dceb23ee40e9d0c82a7c6156c3f010eebc0e"
integrity sha512-NdRzQ15Ht83qG0/Lyu11ciy/Hu/oXbbtJUgwzACc7bWvHQA8xEwTsehWexqn1529Kfc5EjuZ0Wmj3MHmp+jOWw==
"@sentry/cli-linux-i686@2.47.0":
version "2.47.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.47.0.tgz#e551e813e8060de161d6058664e252c952f22291"
integrity sha512-5FYe1dth06xThbr41AOvX67oKZr4xqtDwHJvpFdyCdf+Yh5E5/rtPX35K1beMERgVyT+whRetrNBFAcHnp6LaA==
"@sentry/cli-linux-i686@2.53.0":
version "2.53.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.53.0.tgz#989dc766b098e94c6751bad3efcd4ca0fe1a2565"
integrity sha512-0REmBibGAB4jtqt9S6JEsFF4QybzcXHPcHtJjgMi5T0ueh952uG9wLzjSxQErCsxTKF+fL8oG0Oz5yKBuCwCCQ==
"@sentry/cli-linux-x64@2.47.0":
version "2.47.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.47.0.tgz#56c257e4df8466709fb80ec21e1b5350ee713464"
integrity sha512-wq67T2UpbTst//1lZGDTeFa7nKsnOpP8rS34TQ3GxsGU1LOjinl9zYl0mUPsoVXIHbWxTHlU6YDNf0q0eB7ddA==
"@sentry/cli-linux-x64@2.53.0":
version "2.53.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.53.0.tgz#2a94361233ed24e4a32f08919011a591aea4cb6b"
integrity sha512-9UGJL+Vy5N/YL1EWPZ/dyXLkShlNaDNrzxx4G7mTS9ywjg+BIuemo6rnN7w43K1NOjObTVO6zY0FwumJ1pCyLg==
"@sentry/cli-win32-arm64@2.47.0":
version "2.47.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.47.0.tgz#ce7914253f15160373e856e48738932f03adef41"
integrity sha512-a1sv44bMe35V9eW9Zk/kYymXswzJ/RHXNRjkFnW1m1iXx6NauQD3sjEgkryu3UmuvKO9g3pBkMMT1u6xB/08QA==
"@sentry/cli-win32-arm64@2.53.0":
version "2.53.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.53.0.tgz#946609eabd318657521c4b3ef15a420cc00f1c60"
integrity sha512-G1kjOjrjMBY20rQcJV2GA8KQE74ufmROCDb2GXYRfjvb1fKAsm4Oh8N5+Tqi7xEHdjQoLPkE4CNW0aH68JSUDQ==
"@sentry/cli-win32-i686@2.47.0":
version "2.47.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.47.0.tgz#c6414696b7031a590520b01e8a062e0fe71874be"
integrity sha512-QKLSCED00jNHC4cu9GutLWaFAy5vdVGDrIPvVdztSFLS2fRMhRSSPE8tJwlSYh2OfdHhUHQbMOo58cDVfEklBg==
"@sentry/cli-win32-i686@2.53.0":
version "2.53.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.53.0.tgz#f51937d73cefad16b9d2e89acc4c9f178da36cc6"
integrity sha512-qbGTZUzesuUaPtY9rPXdNfwLqOZKXrJRC1zUFn52hdo6B+Dmv0m/AHwRVFHZP53Tg1NCa8bDei2K/uzRN0dUZw==
"@sentry/cli-win32-x64@2.47.0":
version "2.47.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.47.0.tgz#96ceaf75a9ffb39fd255f11fa2b85f4e7e26b591"
integrity sha512-XcM+I7eWpSp8khy44djunVvQKSMsBP698j0swA41Pd1JL0mxLFV/4P9wfWZw1RRB8R71jks74kZvM45AAh2FZw==
"@sentry/cli-win32-x64@2.53.0":
version "2.53.0"
resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.53.0.tgz#d89cde8354b4eb8e89f2c11dc6a6fb5e7392e2ae"
integrity sha512-1TXYxYHtwgUq5KAJt3erRzzUtPqg7BlH9T7MdSPHjJatkrr/kwZqnVe2H6Arr/5NH891vOlIeSPHBdgJUAD69g==
"@sentry/cli@2.47.0":
version "2.47.0"
resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.47.0.tgz#d9a3309d281d03e23651a68e81fbd04c60f52bfe"
integrity sha512-M1zAbc74rGqcWXPi4vowNY7plADjsvKVEZhyUcSq+K3JtZOQ1m1QJgSno31hLbK9V4sx4qyDesNEcBtUjof07w==
"@sentry/cli@2.53.0":
version "2.53.0"
resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.53.0.tgz#fd5b65b9f6f06f0ed16345acf3ecf0720bd7bcf8"
integrity sha512-n2ZNb+5Z6AZKQSI0SusQ7ZzFL637mfw3Xh4C3PEyVSn9LiF683fX0TTq8OeGmNZQS4maYfS95IFD+XpydU0dEA==
dependencies:
https-proxy-agent "^5.0.0"
node-fetch "^2.6.7"
@@ -2300,55 +2300,47 @@
proxy-from-env "^1.1.0"
which "^2.0.2"
optionalDependencies:
"@sentry/cli-darwin" "2.47.0"
"@sentry/cli-linux-arm" "2.47.0"
"@sentry/cli-linux-arm64" "2.47.0"
"@sentry/cli-linux-i686" "2.47.0"
"@sentry/cli-linux-x64" "2.47.0"
"@sentry/cli-win32-arm64" "2.47.0"
"@sentry/cli-win32-i686" "2.47.0"
"@sentry/cli-win32-x64" "2.47.0"
"@sentry/cli-darwin" "2.53.0"
"@sentry/cli-linux-arm" "2.53.0"
"@sentry/cli-linux-arm64" "2.53.0"
"@sentry/cli-linux-i686" "2.53.0"
"@sentry/cli-linux-x64" "2.53.0"
"@sentry/cli-win32-arm64" "2.53.0"
"@sentry/cli-win32-i686" "2.53.0"
"@sentry/cli-win32-x64" "2.53.0"
"@sentry/core@8.54.0":
version "8.54.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-8.54.0.tgz#a2ebec965cadcb6de89e116689feeef79d5862a6"
integrity sha512-03bWf+D1j28unOocY/5FDB6bUHtYlm6m6ollVejhg45ZmK9iPjdtxNWbrLsjT1WRym0Tjzowu+A3p+eebYEv0Q==
"@sentry/core@10.8.0":
version "10.8.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-10.8.0.tgz#71f71f8ecb5a06c41a426f5ccee2d9767b7d7cfd"
integrity sha512-scYzM/UOItu4PjEq6CpHLdArpXjIS0laHYxE4YjkIbYIH6VMcXGQbD/FSBClsnCr1wXRnlXfXBzj0hrQAFyw+Q==
"@sentry/react-native@6.17.0":
version "6.17.0"
resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-6.17.0.tgz#d897af3861251ced6ca0f60d015e1806b4ec575d"
integrity sha512-R8cQHE5wtespva5tYtBc620xI1+w5dI53fotdzX+ONxHs5G31Da9O1OwYLH9hh2WouIf4dDXfBSmnKoVXL79+Q==
"@sentry/react-native@7.0.1":
version "7.0.1"
resolved "https://registry.yarnpkg.com/@sentry/react-native/-/react-native-7.0.1.tgz#688a68bb1ec12a9e18577c72477593989ad1e6c0"
integrity sha512-xz8ON51qSDvcHVFkdLo0b7rlrQVXpRVXqzm7e1+nHEZ07TX0o+utxx04akxD1Z4hmGPTWPmsHeMlm7diV9NtTQ==
dependencies:
"@sentry/babel-plugin-component-annotate" "3.5.0"
"@sentry/browser" "8.54.0"
"@sentry/cli" "2.47.0"
"@sentry/core" "8.54.0"
"@sentry/react" "8.54.0"
"@sentry/types" "8.54.0"
"@sentry/utils" "8.54.0"
"@sentry/babel-plugin-component-annotate" "4.3.0"
"@sentry/browser" "10.8.0"
"@sentry/cli" "2.53.0"
"@sentry/core" "10.8.0"
"@sentry/react" "10.8.0"
"@sentry/types" "10.8.0"
"@sentry/react@8.54.0":
version "8.54.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-8.54.0.tgz#16cec103b5d5697bdfebacf6e2d35f19699b3ab3"
integrity sha512-42T/fp8snYN19Fy/2P0Mwotu4gcdy+1Lx+uYCNcYP1o7wNGigJ7qb27sW7W34GyCCHjoCCfQgeOqDQsyY8LC9w==
"@sentry/react@10.8.0":
version "10.8.0"
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-10.8.0.tgz#51020c04d9d6ce2a9edc3aa1bd961823f1a27561"
integrity sha512-w/dGLMCLJG2lp8gKVKX1jjeg2inXewKfPb73+PS1CDi9/ihvqZU2DAXxnaNsBA7YYtGwlWVJe1bLAqguwTEpqw==
dependencies:
"@sentry/browser" "8.54.0"
"@sentry/core" "8.54.0"
"@sentry/browser" "10.8.0"
"@sentry/core" "10.8.0"
hoist-non-react-statics "^3.3.2"
"@sentry/types@8.54.0":
version "8.54.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-8.54.0.tgz#1d57bb094443081de4e0d8b638e6ebc40f5ddd36"
integrity sha512-wztdtr7dOXQKi0iRvKc8XJhJ7HaAfOv8lGu0yqFOFwBZucO/SHnu87GOPi8mvrTiy1bentQO5l+zXWAaMvG4uw==
"@sentry/types@10.8.0":
version "10.8.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-10.8.0.tgz#3c87e7d48bf2755841e75e078ff0fb17ff36a37f"
integrity sha512-xRe41/KvnNt4o6t5YeB+yBRTWvLUu6FJpft/VBOs4Bfh1/6rz+l78oxSCtpXo3MsfTd5185I0uuggAjEdD4Y6g==
dependencies:
"@sentry/core" "8.54.0"
"@sentry/utils@8.54.0":
version "8.54.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-8.54.0.tgz#5e28e03a249451b4a55200a0787f4e2c59bab2c5"
integrity sha512-JL8UDjrsKxKclTdLXfuHfE7B3KbrAPEYP7tMyN/xiO2vsF6D84fjwYyalO0ZMtuFZE6vpSze8ZOLEh6hLnPYsw==
dependencies:
"@sentry/core" "8.54.0"
"@sentry/core" "10.8.0"
"@shopify/flash-list@^2.0.3":
version "2.0.3"
@@ -8554,10 +8546,10 @@ react-native-ota-hot-update@2.3.1:
buffer "^6.0.3"
isomorphic-git "1.27.3"
react-native-pager-view@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-7.0.0.tgz#09e72960776d2a95d7b0bc5c71aa2b808a7ca4a4"
integrity sha512-RgpGiTqE7UoSvCMG//AkiMrReF3NxtCfeCiRSvqxhYlTwHq3jMFWqAyZsyEepvCHYCYnMSmRGTMxv9koziat7g==
react-native-pager-view@^6.9.1:
version "6.9.1"
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.9.1.tgz#a9e6d9323935cc2ae1d46d7816b66f76dc3eff8e"
integrity sha512-uUT0MMMbNtoSbxe9pRvdJJKEi9snjuJ3fXlZhG8F2vVMOBJVt/AFtqMPUHu9yMflmqOr08PewKzj9EPl/Yj+Gw==
react-native-reanimated@4.0.2:
version "4.0.2"