I have a react-navigation screen set as a modal with a react-native-track player in it. Initially, it loads with no issue, but when I close the modal and open it back - the audio plays but the artwork and details are not visible.
Looks like resources are freed up every time I close the modal(detach). I'm trying to persist the state(AsyncStorage) but I totally confused myself.
import { useEffect, useState } from "react";
import { SafeAreaView } from 'react-native-safe-area-context';
import { Text, View, Image, StyleSheet, ScrollView, TouchableOpacity, Share, ActivityIndicator } from "react-native";
import { Colors, MarqueeDirections, Marquee, Button, Carousel } from "react-native-ui-lib";
import FontAwesome5 from '@expo/vector-icons/FontAwesome5';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { Dialog } from "react-native-ui-lib/src/incubator";
import Slider from '@react-native-community/slider';
import { useBook } from "../context/useBook";
import TrackPlayer, { Capability, State, Event, usePlaybackState, useProgress, useTrackPlayerEvents } from 'react-native-track-player';
import AsyncStorage from '@react-native-async-storage/async-storage';
const podcasts = [
{
title: 'Lettera a un nemico',
url: 'https://api.spreaker.com/download/episode/43447400/letteraoreste.mp3',
},
{
title: '143 - Intelligenza Artificiale Generativa con Jacopo Perfetti',
url: 'https://chtbl.com/track/9E947E/api.spreaker.com/download/episode/52096290/def_hc_perfetti_v2_loud.mp3',
},
{
title: 'IA : Microsoft investit 10 milliards dans Open AI pour tout dominer ?',
url: 'https://traffic.megaphone.fm/FODL8281657475.mp3?updated=1673441802',
}
];
const BG_COLOR = '#3d405b';
const PlayerModal = ({ navigation, route }) => {
const { stateParam, currentBookParam } = route.params;
const book = useBook();
const tracksCount = podcasts.length;
const [trackIndex, setTrackIndex] = useState(0);
const [trackTitle, setTrackTitle] = useState();
const [trackArtist, setTrackArtist] = useState();
const [trackArtwork, setTrackArtwork] = useState();
const [currentBook, setCurrentBook] = useState(0);
const playBackState = usePlaybackState();
const progress = useProgress();
const [isTextDialogVisible, setIsTextDialogVisible] = useState(false);
const [isReadingMode, setIsReadingMode] = useState(false);
const [fontSize, setFontSize] = useState(14);
const [fontTheme, setFontTheme] = useState({ modalBackgroundColor: Colors.white, backgroundColor: Colors.white, color: Colors.grey10 });
const storeData = async (value) => {
try {
const jsonValue = JSON.stringify(value);
await AsyncStorage.setItem('@test', jsonValue);
} catch (e) {
// saving error
}
}
const getData = async () => {
try {
const jsonValue = await AsyncStorage.getItem('@test');
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch (e) {
// error reading value
}
}
const setupPlayer = async () => {
try {
await TrackPlayer.setupPlayer();
await TrackPlayer.updateOptions({
capabilities: [
Capability.Play,
Capability.Pause,
Capability.SkipToNext,
Capability.SkipToPrevious
],
});
//TODO: get all tracks from DB
await TrackPlayer.add(
podcasts
//currentBook?.keyIdeas[0]?.audioTrack
);
await gettrackdata();
await TrackPlayer.play();
} catch (error) { console.log(error); }
};
useTrackPlayerEvents([Event.PlaybackTrackChanged], async event => {
if (event.type === Event.PlaybackTrackChanged && event.nextTrack !== null) {
const track = await TrackPlayer.getTrack(event.nextTrack);
const { title } = track;
setTrackIndex(event.nextTrack);
setTrackTitle(title);
}
});
const gettrackdata = async () => {
let trackIndex = await TrackPlayer.getCurrentTrack();
let trackObject = await TrackPlayer.getTrack(trackIndex);
setTrackIndex(trackIndex);
setTrackTitle(trackObject.title);
setTrackArtist(currentBook?.author);
setTrackArtwork(currentBook?.artwork);
};
const togglePlayBack = async playBackState => {
const currentTrack = await TrackPlayer.getCurrentTrack();
if (currentTrack != null) {
if ((playBackState == State.Paused) | (playBackState == State.Ready)) {
await TrackPlayer.play();
} else {
await TrackPlayer.pause();
}
}
};
const nexttrack = async () => {
if (trackIndex < tracksCount - 1) {
await TrackPlayer.skipToNext();
gettrackdata();
};
};
const previoustrack = async () => {
if (trackIndex > 0) {
await TrackPlayer.skipToPrevious();
gettrackdata();
};
};
async function carouselPageChanged(pageIndex, oldPageIndex) {
if (pageIndex > oldPageIndex) {
await nexttrack();
}
else {
await previoustrack();
}
}
useEffect(() => {
setupPlayer();
// if asyncStorage has data - it means player has been setup already and we have a book
getData().then(async (res) => {
if (res === null) {
console.log(currentBookParam?.title);
// save mode that we get as a param to identify what view to display: Reading or Listening
// it can be later accessed by StickyComponent above Tabs
currentBookParam.mode = stateParam; //read || listen
setCurrentBook(currentBookParam);
storeData(currentBookParam);
}
else {
console.log('res: ' + res?.title);
setCurrentBook(res);
}
});
//book?.addToHistory(currentBook);
//return () => TrackPlayer.destroy();
}, []);
useEffect(() => {
// set header buttons and colors
navigation.setOptions({
title: currentBook?.title,
headerTitleStyle: { color: isReadingMode ? fontTheme.modalBackgroundColor : BG_COLOR },
headerStyle: {
backgroundColor: isReadingMode ? fontTheme.modalBackgroundColor : BG_COLOR,
borderBottom: 0
},
headerLeft: () => (
<View style={{ flexDirection: 'row', alignItems: 'center', alignContent: 'center', justifyContent: 'flex-start' }}>
<TouchableOpacity onPress={() => { navigation.goBack() }}><FontAwesome5 name="chevron-down" size={25} color={isReadingMode ? fontTheme.color : Colors.white} /></TouchableOpacity>
</View>
),
headerRight: () => (
<View style={{ width: 250, flexDirection: 'row', alignItems: 'center', alignContent: 'center', justifyContent: 'flex-end' }}>
{playBackState === State.Buffering || playBackState === State.Connecting || playBackState === State.None ?
<ActivityIndicator size="small" color={fontTheme.color} />
:
<>
{isReadingMode &&
<TouchableOpacity onPress={() => setIsTextDialogVisible((current) => !current)} style={{ marginRight: 15 }}><MaterialIcons name="format-size" size={25} color={fontTheme.color} /></TouchableOpacity>
}
{isReadingMode &&
<TouchableOpacity onPress={() => togglePlayBack(playBackState)} style={{ marginRight: 15 }}>
{playBackState === State.Playing ?
<MaterialIcons name="pause-circle-filled" size={25} color={fontTheme.color} />
:
<MaterialIcons name="play-circle-filled" size={25} color={fontTheme.color} />
}
</TouchableOpacity>
}
{isReadingMode ?
<TouchableOpacity onPress={() => setIsReadingMode((current) => !current)} style={{ marginRight: 15 }}><FontAwesome5 name="headphones-alt" size={25} color={fontTheme.color} /></TouchableOpacity>
:
<TouchableOpacity onPress={() => setIsReadingMode((current) => !current)} style={{ marginRight: 15 }}><FontAwesome5 name="readme" size={25} color={Colors.white} /></TouchableOpacity>
}
<TouchableOpacity><MaterialIcons name="more-horiz" size={30} color={isReadingMode ? fontTheme.color : Colors.white} /></TouchableOpacity>
</>
}
</View>
)
});
}, [currentBook, playBackState, isReadingMode, fontTheme, State]);
const onShare = async () => {
try {
const result = await Share.share({
message: 'Syat | Читай и слушай самое важное из книг за мгновение',
});
if (result.action === Share.sharedAction) {
if (result.activityType) {
// shared with activity type of result.activityType
} else {
// shared
}
} else if (result.action === Share.dismissedAction) {
// dismissed
}
} catch (error) {
alert(error.message);
}
};
return (
<SafeAreaView edges={['bottom']} style={{ flex: 1, backgroundColor: isReadingMode ? fontTheme.modalBackgroundColor : BG_COLOR }}>
<Dialog
useSafeArea
visible={isTextDialogVisible}
top={true}
containerStyle={{ width: '95%', backgroundColor: Colors.white, paddingVertical: 15, paddingHorizontal: 5, marginTop: '20%', borderRadius: 12 }}
onDismiss={() => setIsTextDialogVisible(false)}
direction={'up'}
>
<View style={{ paddingHorizontal: 15, flexDirection: 'row', alignItems: 'center', alignContent: 'center', justifyContent: 'space-around' }}>
<TouchableOpacity><MaterialIcons name="format-size" size={20} color={BG_COLOR} /></TouchableOpacity>
<Slider
style={{ flex: 1 }}
thumbTintColor='#00b4d8'
minimumTrackTintColor='#00b4d8'
maximumTrackTintColor={Colors.grey50}
step={1}
value={fontSize}
minimumValue={12}
maximumValue={24}
onValueChange={(val) => setFontSize(val)}
/>
<TouchableOpacity><MaterialIcons name="format-size" size={30} color={BG_COLOR} /></TouchableOpacity>
</View>
<View style={{ marginTop: 15, flexDirection: 'row', alignItems: 'center', alignContent: 'center', justifyContent: 'space-around' }}>
<Button onPress={() => setFontTheme({ modalBackgroundColor: Colors.white, backgroundColor: Colors.white, color: Colors.grey1 })} label={'Aa'} labelStyle={{ color: Colors.grey1 }} size={Button.sizes.large} outline={true} outlineColor={Colors.grey50} outlineWidth={1} backgroundColor={Colors.white} borderRadius={5} />
<Button onPress={() => setFontTheme({ modalBackgroundColor: Colors.yellow70, backgroundColor: Colors.yellow70, color: Colors.grey1 })} label={'Aa'} labelStyle={{ color: Colors.grey1 }} size={Button.sizes.large} backgroundColor={Colors.yellow70} borderRadius={5} />
<Button onPress={() => setFontTheme({ modalBackgroundColor: BG_COLOR, backgroundColor: BG_COLOR, color: Colors.white })} label={'Aa'} size={Button.sizes.large} backgroundColor={BG_COLOR} borderRadius={5} />
</View>
</Dialog>
<ScrollView bounces={false} showsVerticalScrollIndicator style={styles.container}>
{isReadingMode ?
<Carousel
initialPage={trackIndex}
bounces={false}
onChangePage={carouselPageChanged}>
{currentBook?.keyIdeas?.map((keyIdea, y) =>
<View style={{ flexGrow: 1, backgroundColor: fontTheme.backgroundColor, padding: 14 }}>
<Text selectable style={[styles.textHeading, { color: fontTheme.color }]}>{currentBook?.keyIdeas[y]?.title}</Text>
<Text selectable style={[styles.textParagraph, { fontSize, color: fontTheme.color }]}>{currentBook?.keyIdeas[y]?.content}</Text>
</View>
)}
</Carousel>
:
<View style={styles.bookItem}>
<View style={styles.bookImageBg}>
<Image style={styles.bookImage} source={{ uri: trackArtwork }} />
{/* <Image style={styles.bookImage} source={{ uri: currentBook?.image }} /> */}
<Marquee
key={1}
label={trackTitle}
duration={8000}
direction={MarqueeDirections.LEFT}
containerStyle={{ height: 'auto', marginTop: 15, marginBottom: 5 }}
labelStyle={styles.bookTitle}
numberOfReps={-1}
/>
<Text style={styles.bookAuthor}>{trackArtist}</Text>
{/* <Text style={styles.bookAuthor}>{currentBook?.author}</Text> */}
</View>
<View style={styles.innerContainer}>
{/* Track progress bar */}
<Slider
containerStyle={{ marginTop: 60 }}
thumbTintColor='#00b4d8'
minimumTrackTintColor='#00b4d8'
maximumTrackTintColor={Colors.grey20}
value={progress.position}
minimumValue={0}
maximumValue={progress.duration}
onSlidingComplete={async value => await TrackPlayer.seekTo(value)}
/>
<View style={{ flexDirection: 'row', alignItems: 'center', alignContent: 'center', justifyContent: 'space-between' }}>
<Text style={{ color: Colors.white }}>{
new Date(progress.position * 1000)
.toLocaleTimeString()
.substring(3)}
</Text>
<Text style={{ color: Colors.white }}>
{new Date((progress.duration - progress.position) * 1000)
.toLocaleTimeString()
.substring(3)}
</Text>
</View>
{/* Player buttons */}
<View style={styles.actionsContainer}>
{!State.Ready ?
<ActivityIndicator size="small" color={Colors.white} />
:
<>
<TouchableOpacity onPress={previoustrack} style={{ alignItems: 'center' }}><MaterialIcons name="skip-previous" size={40} color={Colors.grey60} /></TouchableOpacity>
{/* <TouchableOpacity disabled={!audioTrack || !isAudioTrackLoaded} onPress={backwards} style={{ alignItems: 'center' }}><MaterialIcons name="replay-10" size={40} color={Colors.grey60} /></TouchableOpacity> */}
<TouchableOpacity onPress={() => togglePlayBack(playBackState)} style={{ alignItems: 'center' }}>
{playBackState === State.Playing ?
<MaterialIcons name="pause-circle-filled" size={90} color={Colors.grey60} />
:
<MaterialIcons name="play-circle-filled" size={90} color={Colors.grey60} />
}
</TouchableOpacity>
{/* <TouchableOpacity disabled={!audioTrack || !isAudioTrackLoaded} onPress={forward} style={{ alignItems: 'center' }}><MaterialIcons name="forward-10" size={40} color={Colors.grey60} /></TouchableOpacity> */}
<TouchableOpacity onPress={nexttrack} style={{ alignItems: 'center' }}><MaterialIcons name="skip-next" size={40} color={Colors.grey60} /></TouchableOpacity>
</>
}
</View>
{/* END Player buttons */}
</View>
</View>
}
</ScrollView>
</SafeAreaView>
)
}
export default PlayerModal;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: BG_COLOR
},
innerContainer: {
backgroundColor: BG_COLOR,
paddingHorizontal: 24
},
actionsContainer: {
backgroundColor: '#3d405b',
flexDirection: 'row',
alignContent: 'center',
alignItems: 'center',
justifyContent: 'space-evenly',
marginTop: 30
},
sectionTitle: {
fontFamily: 'Inter_600SemiBold',
color: Colors.black,
fontSize: 16,
marginVertical: 10
},
textHeading: {
fontFamily: 'RobotoSerif_600SemiBold',
color: Colors.grey1,
fontSize: 22,
marginBottom: 15
},
textParagraph: {
fontFamily: 'RobotoSerif_400Regular',
color: Colors.grey1,
fontSize: 14,
marginBottom: 15
},
bookTitle: {
fontFamily: 'Inter_800ExtraBold',
color: Colors.grey70,
fontSize: 24
},
bookAuthor: {
fontFamily: 'Inter_600SemiBold',
color: Colors.grey40,
fontSize: 16,
marginBottom: 5
},
bookDesc: {
fontFamily: 'Inter_400Regular',
color: Colors.black,
fontSize: 14,
},
bookItem: {},
bookImageBg: {
backgroundColor: BG_COLOR,
marginBottom: 12,
padding: 10,
alignContent: 'center',
justifyContent: 'center',
alignItems: 'center'
},
bookImage: {
width: '100%',
height: 300,
resizeMode: 'contain'
},
keyIdea: {
flexDirection: 'row',
width: '100%',
alignItems: 'center',
borderBottomColor: Colors.grey10,
borderBottomWidth: StyleSheet.hairlineWidth,
paddingVertical: 10
},
keyIdeaOrder: {
fontFamily: 'Inter_600SemiBold',
color: Colors.black,
fontSize: 16,
},
keyIdeaTitle: {
fontFamily: 'Inter_400Regular',
color: Colors.black,
fontSize: 14,
marginBottom: 5,
marginHorizontal: 25
}
});
I moved the player setup to App.js, leaving the catch section empty.
useEffect(() => {
isPlayerInitialized();
}, []);
async function isPlayerInitialized() {
let isPlayerInitialized = false;
try {
await TrackPlayer.setupPlayer();
await TrackPlayer.updateOptions({
capabilities: [Capability.Play, Capability.Pause, Capability.SkipToNext, Capability.SkipToPrevious]
});
isPlayerInitialized = true;
} catch (e) {
// intentionally leaved as blank
}
}
Passing item object via react-navigation params from modal's parent Component and handling it in my Modal, where I add tracks to already initialized TrackPlayer.
const {
state,
currentBook
} = route.params;
useEffect(() => {
setupPlayer();
}, []);
const setupPlayer = async() => {
try {
setTrackArtist(currentBook ? .author);
setTrackArtwork(currentBook ? .artwork);
await TrackPlayer.add(podcasts);
await gettrackdata();
await TrackPlayer.play();
} catch (error) {
console.log(error);
}
};