I am new to react native. I am trying to make a podcast player. While I was able to make it work in my Podcast component, when I change to other component, the audio player current status is not recognized.
I am reusing AudioPlayer component (the one in green) that is working fine in the Podcast component. But how can I have access to the currently playing song from other pages like Home?
This is my AudioPlayer.js that I am trying to reuse in other components. I want to have controls like pause/play, next work from other other components. I am storing the current audio url in redux store but the only thing that I am to do so far is restart the current audio when I click pause from other component.
import React, {useState} from "react";
import { View, Text, Image, TouchableOpacity, StyleSheet } from "react-native";
import { AntDesign } from "#expo/vector-icons";
import { connect } from "react-redux";
import { Audio } from "expo-av";
import { audioPlayerAction } from "../actions";
import { radio } from "../assets/radio/radio";
import { styles } from "../assets/styles";
const audio = new Audio.Sound();
const AudioPlayer = (props) => {
// get currently playing song url from redux store
const { audioSrc, index } = props.storeState;
const [currentAudioIndex, setCurrentAudioIndex] = useState(index);
const [currentAudio, setCurrentAudio] = useState(null);
const [loaded, setLoaded] = useState(false);
const [paused, setPaused] = useState(false);
const [nowPlaying, setNowPlaying] = useState("");
const [playerStatus, setPlayerStatus] = useState(null);
Audio.setAudioModeAsync({
staysActiveInBackground: true,
});
// continue playing next song when done
const onPlaybackStatusUpdate = (playbackStatus) => {
if (playbackStatus.didJustFinish) {
next();
}
};
const play = async (url) => {
if (currentAudio !== url) {
await audio.unloadAsync();
try {
const status = await audio.loadAsync(
{
uri: url,
},
{ shouldPlay: true }
);
audio.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
setCurrentAudio(url);
setPlayerStatus(status);
setLoaded(true);
setPaused(false);
setNowPlaying(radio[currentAudioIndex].station);
await audio.playAsync();
} catch (error) {
console.log(error);
}
} else if (currentAudio === url && loaded === true && paused === false) {
await audio.pauseAsync();
setPaused(true);
} else if (currentAudio === url && loaded === true && paused === true) {
await audio.playAsync();
setPaused(false);
}
};
const next = async () => {
let nextIndex = index + 1 >= radio.length ? 0 : index + 1;
setCurrentAudioIndex(nextIndex);
let nextAudio = radio[nextIndex].url;
props.audioPlayerAction(nextAudio, true, null, "podcast", nextIndex);
play(nextAudio);
};
const prev = () => {
let prevSongIndex = index - 1 < 0 ? radio.length - 1 : index - 1;
let prevAudio = radio[prevSongIndex].url;
props.audioPlayerAction(prevAudio, true, null, "podcast", prevSongIndex);
play(prevAudio);
};
return (
<View style={style.container}>
<View style={style.musicTitle}>
<Text style={style.musicTitleText}>Now Playing: {nowPlaying}</Text>
</View>
<View style={style.controls}>
<TouchableOpacity onPress={() => prev()}>
<AntDesign style={style.icon} name="stepbackward" />
</TouchableOpacity>
<TouchableOpacity onPress={() => play(audioSrc)}>
<AntDesign style={style.icon} name={paused ? "play" : "pause"} />
</TouchableOpacity>
<TouchableOpacity onPress={() => next()}>
<AntDesign style={style.icon} name="stepforward" />
</TouchableOpacity>
</View>
</View>
);
};
const MapStateToProps = (state) => {
return { storeState: state.audioPlayerReducer };
};
export default connect(MapStateToProps, { audioPlayerAction })(AudioPlayer);
Related
I am using a dependency called vision-camera-code-scanner for QR code scanning in my React Native app. I am getting QR code scan data properly. But i need to pass that data to make an API call. But when i try to do that, it crashes the application. Not sure what should i do here.
Here's my component:
import React, { useState, useCallback, useEffect, useMemo } from "react";
import { StyleSheet, Text } from "react-native";
import {
Camera,
useCameraDevices,
useFrameProcessor,
} from "react-native-vision-camera";
import { useDispatch, useSelector } from "react-redux";
import * as appActions from "../../../redux/app/app.actions";
import { BarcodeFormat, scanBarcodes } from "vision-camera-code-scanner";
interface ScanScreenProps {}
const Scan: React.FC<ScanScreenProps> = () => {
const [hasPermission, setHasPermission] = useState(false);
const devices = useCameraDevices();
const device = devices.back;
const dispatch = useDispatch();
const validateQRStatus = useSelector(validationQRSelector);
const frameProcessor = useFrameProcessor((frame) => {
"worklet";
const detectedBarcodes = scanBarcodes(frame, [BarcodeFormat.QR_CODE], {
checkInverted: true,
});
if (detectedBarcodes?.length !== 0) {
const resultObj = JSON.parse(detectedBarcodes[0].rawValue);
const paramData = `token:${Object.values(resultObj)[0]}`;
validate(paramData);
}, []);
const validate = useCallback((param: string) => dispatch(appActions.validateQR(param)));
useEffect(() => {
(async () => {
const status = await Camera.requestCameraPermission();
setHasPermission(status === "authorized");
})();
}, []);
return (
device != null &&
hasPermission && (
<>
<Camera
style={StyleSheet.absoluteFill}
device={device}
isActive={true}
frameProcessor={frameProcessor}
frameProcessorFps={5}
/>
{/* {barcodes.map((barcode, idx) => (
<Text key={idx} style={styles.barcodeTextURL}>
{barcode.barcodeFormat + ": " + barcode.barcodeText}
</Text>
))} */}
<Text style={styles.barcodeTextURL}>camera</Text>
</>
)
);
};
export default Scan;
const styles = StyleSheet.create({
barcodeTextURL: {
fontSize: 20,
color: "white",
fontWeight: "bold",
alignSelf: "center",
},
});
Your problem is that a worklet is run in a separate JS thread. If you need to call any function from your main thread you need to use runOnJS (https://docs.swmansion.com/react-native-reanimated/docs/next/api/miscellaneous/runOnJS/)
import { runOnJS } from 'react-native-reanimated';
const frameProcessor = useFrameProcessor((frame) => {
"worklet";
const detectedBarcodes = scanBarcodes(frame, [BarcodeFormat.QR_CODE], {
checkInverted: true,
});
if (detectedBarcodes?.length !== 0) {
const resultObj = JSON.parse(detectedBarcodes[0].rawValue);
const paramData = `token:${Object.values(resultObj)[0]}`;
runOnJS(validate)(paramData);
}, []);
I'm working on a mobile phone application with Stripe and Expo Bar Code Scanner. When you start the application, if you gave the permissions for using the camera, you will can scan bar codes. Bar Codes only contains the id of the scanned item. If it exists, two buttons (+/-) will appear in order to choose the amount for the item. If it doesn't exists, nothing happens. When the amount changes, I save in SecureStore the id of the item as the key and the amount as the value.
The problem is when I move on others screens (with React Navigation) and I came back to scan and I rescan the same item, the amount resets to 0. If you don't give the permissions for the camera, it displays a list of available items when you can choose the amount (+/-) buttons and similar problem.
Here the concerned two files :
ItemListComponent.tsx
import { Button, FlatList, View, Text } from 'react-native';
import * as SecureStore from 'expo-secure-store';
import { useState } from 'react';
export const ItemComponent = (props: any) => {
const [amount, setAmount] = useState<number>(0);
const getAmount = async () => {
const amount = await SecureStore.getItemAsync(props.item.id.toString());
if (amount) {
setAmount(parseInt(amount));
}
getAmount();
}
const save = async () => {
await SecureStore.setItemAsync(props.item.id.toString(), amount.toString());
}
return (
<View>
<Text>{props.item.name}</Text>
<Button
onPress={() => {
setAmount(amount + 1);
save();
}}
title='+'
/>
{amount > 0 &&
<Button
onPress={() => {
setAmount(amount - 1);
save();
}}
title='-'
/>
}
</View>
);
};
export const ItemListComponent = (props: any) => {
return (
<FlatList
data={props.items}
renderItem={({ item }) =>
<ItemComponent key={item.id} item={item} />
}
/>
);
};
BarCodeScannerComponent.tsx
import { BarCodeScanner } from 'expo-barcode-scanner';
import { useState } from 'react';
import { StyleSheet } from 'react-native';
import { ItemComponent } from './ItemListComponent';
import Items from '../models/ItemsModel';
export const BarCodeScannerComponent = () => {
const [item, setItem] = useState<Items>();
const getItem = async ({ data }: any) => {
const response = await fetch(`http://192.168.1.81:8000/items/${data}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
if (response.ok) {
const json = await response.json();
setItem(json);
}
}
return (
<View style={styles.container}>
<BarCodeScanner
onBarCodeScanned={getItem}
style={StyleSheet.absoluteFillObject}
/>
{(item !== null && item !== undefined) && <ItemComponent key={item.id} item={item} />}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
},
});
Thanks for help !
It looks like you never call getAmount, and if you did call it you'd get infinite recursion.
const getAmount = async () => {
const amount = await SecureStore.getItemAsync(props.item.id.toString());
if (amount) {
setAmount(parseInt(amount));
}
getAmount();
}
should be
const getAmount = async () => {
const amount = await SecureStore.getItemAsync(props.item.id.toString());
if (amount) {
setAmount(parseInt(amount));
}
}
getAmount();
or, probably even better:
const getAmount = async () => {
const storeAmount = await SecureStore.getItemAsync(props.item.id.toString());
if (amount !== parseInt(storeAmount)) {
setAmount(parseInt(storeAmount));
}
}
useEffect(() => {
getAmount();
}, [props.item.id]);
otherwise, every time it renders you'll call setAmount which will trigger a rerender
I am attempting to enable a Thumbs Up option where when pressed it retrieves the current number of thumbs up in my database for an item, increments it, then updates it. As you can see I've put 2 "here" console.logs, both log to the screen. I get no errors or catches, and the database updates (immediately when pressed). So it technically works, but then there is a lag/hang, for probably 7 seconds where the thumb icon shows as pressed the whole time like it's stuck, and then the modal goes away back to the main screen. As if it "crashed". If I take the update out, there is no hang and the modal remains open until I choose to close it. So it has to be that code. Do you see any glaring errors in my logic?
Update: I've traced the issue possibly to the screen that renders this component. I populate a flatlist using the firebase onValue, of which each object has this "thumbs up" feature. When I take out the onValue section and replace it with a single dummy data object instead, the thumbs up works perfectly. So there must be a problem with the listener maybe not unsubscribing right the way I have it. Previous screen code added.
import React, {useState} from "react";
import { Image, StyleSheet, TouchableOpacity, View } from "react-native";
import AppText from "../components/AppText";
import * as Sentry from 'sentry-expo';
import { ref, child, get, query, onValue, update } from "firebase/database";
function HotDidItWork({indexStore, db, auth}) {
var user = auth.currentUser;
if (!user){
return (
<></>
)
}
const [upPressed, setUpPressed] = useState(false);
const [displayText, setDisplayText] = useState('');
async function didPressUp(){
setUpPressed(true);
const dbRef = ref(db, 'deals/' + indexStore);
if (displayText == ''){
get(query(dbRef))
.then((usersSnapshot)=> {
if (usersSnapshot.exists()) {
let thumbsUpCount = usersSnapshot.val().thumbsUp;
thumbsUpCount = parseInt(thumbsUpCount);
thumbsUpCount += 1;
console.log('here 1');
update(dbRef, {thumbsUp: thumbsUpCount.toString()})
.then(()=> {
console.log('here 2');
setDisplayText('Thanks!');
})
.catch((e)=> {
console.log('error 1: ' + e.message);
})
} else {
console.log("No data available");
}
})
.catch((e)=> {
console.log("error 2: " + e.message);
})
}
}
return (
<View>
<View style={styles.messageContainer}>
<AppText style={upPressed == false && downPressed == false ? styles.didItWork : styles.pressed}>Did this deal work for you?</AppText>
<AppText style={downPressed == true || upPressed == true ? styles.didItWork : styles.pressed}>{displayText}</AppText>
</View>
<View style={styles.thumbs}>
<TouchableOpacity onPress={() => didPressUp()}>
<Image
style={styles.thumbsUp}
source={require("../assets/thumbsUp.png")}
/>
</TouchableOpacity>
</View>
</View>
);
}
export default HotDidItWork;
Previous Screen that renders flatlist of obj with thumbs up feature (I now believe this is where the error is):
import React, { useState, useEffect } from "react";
import {
FlatList,
Image,
ImageBackground,
Platform,
SafeAreaView,
StyleSheet,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import HotCardFav from "../components/HotCardFav";
import { ref, child, get, query, onValue, update } from "firebase/database";
import ListItemSeparator from "../components/ListItemSeparator";
import CardItemDeleteAction from "../components/CardItemDeleteAction";
import { MaterialCommunityIcons } from "#expo/vector-icons";
import { ActivityIndicator } from "react-native";
import {db, auth} from '../../src/config.js';
import AppText from "../components/AppText";
import colors from "../config/colors";
import * as Sentry from 'sentry-expo';
import Header from '../components/Header';
import { ThemeProvider, useFocusEffect } from '#react-navigation/native';
let initialMessagesFav = [];
let listViewRef;
function FavoritesHotScreen() {
var user = auth.currentUser;
if (user == null) {
return (
<></>
)
}
const [loading, setLoading] = useState(false);
const [messagesFav, setMessagesFav] = useState(initialMessagesFav);
const [messagesFavHold, setMessagesFavHold] = useState(initialMessagesFav);
const [refreshing, setRefreshing] = useState(false);
useFocusEffect(
React.useCallback( () => {
async function fetchData() {
// You can await here
const response = await loadListings();
// ...
return () => response();
}
fetchData();
}, [])
);
async function lookupUser(){
const dbRef = ref(db, 'users/' + user.uid);
const usersSnapshot = await get(query(dbRef));
return usersSnapshot;
}
const loadListings = async () => {
let favs = [];
let favsArray = [];
updateInput('');
setLoading(true);
console.log('exists 2');
lookupUser()
.then((snapshot) => {
if (snapshot.exists()) {
favs = snapshot.child("favorites").val();
if (favs != null){
favsArray = favs.split(',');
}
const dbRef = ref(db, 'deals');
return onValue(dbRef , (snapshot) => {
let testData = [];
let searchData = [];
snapshot.forEach((childSnapshot)=>{
let found = favsArray.find(function (element) {
return element == childSnapshot.val().indexStore;
});
if (found != undefined){
testData.push({
id: childSnapshot.key,
title: childSnapshot.val().title,
postedDate: childSnapshot.val().postedDate,
desc: childSnapshot.val().desc,
indexStore: childSnapshot.val().indexStore,
})
checkMessages(testData);
setLoading(false);
}
})
})
.catch((error) => Sentry.Native.captureException('Error FavoritesScreen function loadListings 2 ' + error));
}
const renderItem = ({ item }) => (<HotCardFav
title={item.title}
desc={item.desc}
indexStore={item.id}
postedDate={item.postedDate}
/>);
function checkMessages(testData){
const filtered = testData.filter(country => {
return (country.title != 'NA')
})
setMessagesFav(filtered);
setMessagesFavHold(testData);
setLoading(false);
}
let messagesShow = messagesFav.sort((a, b) => {
const messageA = new Date(parseInt(a.postedDate));
const messageB = new Date(parseInt(b.postedDate));
let comparison = 0;
if (messageA > messageB) {
comparison = 1;
} else if (messageA < messageB) {
comparison = -1;
}
return comparison * -1;
});
return (
<SafeAreaView style={styles.wholeThing}>
<Header image={require('../assets/longlogo4.png')} />
<View style={loading ? styles.activity : styles.none}>
<ActivityIndicator animating={loading} size="large" color="#0000ff" />
</View>
<FlatList
data={messagesShow}
keyExtractor={(messagesShow) => messagesShow.id.toString()}
renderItem={renderItem}
ItemSeparatorComponent={ListItemSeparator}
contentContainerStyle={styles.messagesList}
refreshing={refreshing}
ref={(ref) => {
listViewRef = ref;
}}
/>
</SafeAreaView>
);
}
export default FavoritesHotScreen;
Ok I figured it out here's what I found in case it helps someone in the future. The problem was in my main screen loadlistings() function (that I posted as an edit). It uses onValue to retrieve firebase data which attaches a listener which means ANYTIME data on my database changes, it rerenders the flatlist, entirely. Which automatically closes my modal since it's defaulted to be closed when the screen starts. So by me pressing the "thumbs up", it was changing data, the listener responded, rerendered the flatlist with a closed modal. Maybe this should have been obvious but it wasn't for me. I fixed it by adding a simple "onlyOnce" flag to the end of the onValue per firebase documentation:
return onValue(dbRef , (snapshot) => {
let testData = [];
let searchData = [];
snapshot.forEach((childSnapshot)=>{
let found = favsArray.find(function (element) {
return element == childSnapshot.val().indexStore;
});
if (found != undefined){
testData.push({
id: childSnapshot.key,
title: childSnapshot.val().title,
postedDate: childSnapshot.val().postedDate,
desc: childSnapshot.val().desc,
indexStore: childSnapshot.val().indexStore,
})
checkMessages(testData);
setLoading(false);
})
},{
onlyOnce: true
})
I am working on a musical app with React native, aws and Expo. I am using the Expo AV library to play audio
files. I am trouble getting the song to automatically replay after it finishes.
Below are my attempts at this.
Failed approaches:
I see a didjustFinish boolean variable. I try to reset it to true after the audio finishes playing, then I can await sound.playAsync(); but it appears that is not working
I try to match the durationMillis with the playableDurationMillis - if they are equal then call await sound.playAsync();. This also doe not work.
import React, { useContext, useEffect, useState } from 'react';
import { Text, Image, View, TouchableOpacity } from 'react-native';
import { AntDesign, FontAwesome } from "#expo/vector-icons";
import { API, graphqlOperation } from 'aws-amplify';
import styles from './styles';
import { Song } from "../../types";
import { Sound } from "expo-av/build/Audio/Sound";
import { AppContext } from '../../AppContext';
import { getSong } from "../../src/graphql/queries";
const PlayerWidget = () => {
const [song, setSong] = useState(null);
const [sound, setSound] = useState<Sound | null>(null);
const [isPlaying, setIsPlaying] = useState<boolean>(true);
const [duration, setDuration] = useState<number | null>(null);
const [position, setPosition] = useState<number | null>(null);
const [finish, setFinish] = useState<boolean>(true);
const { songId } = useContext(AppContext);
useEffect(() => {
const fetchSong = async () => {
try {
const data = await API.graphql(graphqlOperation(getSong, { id: songId }))
setSong(data.data.getSong);
} catch (e) {
console.log(e);
}
}
fetchSong();
}, [songId])
const onPlaybackStatusUpdate = (status) => {
setIsPlaying(status.isPlaying);
setDuration(status.durationMillis);
setPosition(status.positionMillis);
setFinish(status.didJustFinish);
// console.log(finish);
console.log(status);
}
const playCurrentSong = async () => {
if (song.artist.length > 10) {
song.artist = song.artist.substring(0, 6) + "...";
}
if (song.title.length > 8) {
song.title = song.title.substring(0, 5) + "...";
}
if (sound) {
await sound.unloadAsync();
}
const { sound: newSound } = await Sound.createAsync(
{ uri: song.uri },
{ shouldPlay: isPlaying },
onPlaybackStatusUpdate
)
setSound(newSound)
}
useEffect(() => {
if (song) {
playCurrentSong();
}
}, [song])
const onPlayPausePress = async () => {
if (!sound) {
return;
}
if (isPlaying) {
await sound.pauseAsync();
}
else {
await sound.playAsync();
}
if (finish) {
await sound.playAsync();
}
}
const getProgress = () => {
if (sound === null || duration === null || position === null) {
return 0;
}
return (position / duration) * 100;
}
if (!song) {
return null;
}
return (
<View style={styles.container}>
<View style={[styles.progress, { width: `${getProgress()}%` }]} />
<View style={styles.row}>
<Image source={{ uri: song.imageUri }} style={styles.image} />
<View style={styles.rightContainer}>
<View style={styles.nameContainer}>
<Text style={styles.title}>{song.title}</Text>
<Text style={styles.artist}>{song.artist}</Text>
</View>
<View style={styles.iconsContainer}>
<AntDesign name="hearto" size={20} color={'white'} />
<TouchableOpacity onPress={onPlayPausePress}>
<AntDesign name={isPlaying ? 'pausecircleo' : 'playcircleo'} size={25} color={'white'} />
</TouchableOpacity>
</View>
</View>
</View>
</View>
)
}
export default PlayerWidget;
Have a look at the docs
There are a few points to keep in mind:
After you play the track through once, calling play on it again will not have any effect. However, you can call sound.replayAsync() to re-start the track.
You could get the sound to loop, so that it automatically restarts if it gets to the end by using (quoting the docs):
playbackObject.setIsLoopingAsync(value)
This is equivalent to playbackObject.setStatusAsync({ isLooping: value })
You need refactor your play/pause method to handle the different cases better. For example, if it's finished but is meant to still be playing (may be try calling replayAsync instead of playAsync).
Another idea is to restart the track if it's finished but still meant to be playing. So if you're not going to be using looping, you can remove the condition
if (finish) {
await sound.playAsync();
}
and put it in a useEffect which is watching 'finish'. I guess using the looping flag is easier.
I want to send the parameter to the function submitLanguageSelection, which is userSelectedLanguage, to a custom hook I've written which (hopefully) saves that parameter to AsyncStorage. The user selects a language, either English or Arabic, from one of the two buttons.
This is my first time ever doing this. I've gotten very stuck.
I would like the submitLanguageSelection function to call the saveData function which is made available through the useLocalStorage hook. I would like the user's choice of language to be persisted in AsyncStorage so I can then later render the ChooseYourLanguageScreen according to whether the user has selected a language or not.
Here is the cutom hook, useLocalStorage:
import React from 'react';
import { Alert } from 'react-native';
import AsyncStorage from '#react-native-community/async-storage';
const STORAGE_KEY = '#has_stored_value';
export default () => {
const [storedValue, setStoredValue] = React.useState('');
const [errorMessage, setErrorMessage] = React.useState('');
const saveData = async () => {
try {
const localValue = await AsyncStorage.setItem(STORAGE_KEY, storedValue);
if (localValue !== null) {
setStoredValue(storedValue);
Alert.alert('Data successfully saved');
}
console.log('stored val', storedValue);
} catch (e) {
setErrorMessage('Something went wrong');
}
};
return [saveData, errorMessage];
};
Here is the ChooseYourLanguageScreen:
import React from 'react';
import { View, Text, StyleSheet, Button } from 'react-native';
import useLocalStorage from '../hooks/useLocalStorage';
const ChooseYourLanguageScreen = ({ navigation }) => {
const [saveData, errorMessage] = useLocalStorage();
const submitLanguageSelection = (userSelectedLanguage) => {
//TODO: save the data locally
//TODO: navigate to welcome screen
// at the moment, the language choice isn't making it to useLocalStorage
if (userSelectedLanguage !== null) {
console.log('user selected lang', userSelectedLanguage);
saveData(userSelectedLanguage);
}
};
return (
<View style={styles.container}>
{errorMessage ? <Text>{errorMessage}</Text> : null}
<Text style={styles.text}>This is the Choose Your Language Screen</Text>
<View style={styles.buttons}>
<View>
<Button
title={'English'}
onPress={() => submitLanguageSelection('English')}
/>
</View>
<View>
<Button
title={'Arabic'}
onPress={() => submitLanguageSelection('Arabic')}
/>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
text: {
alignSelf: 'center',
},
buttons: {
backgroundColor: '#DDDDDD',
padding: 10,
},
});
export default ChooseYourLanguageScreen;
saveData() needs a parameter. You can provide a default value that uses storedValue that came from React.useState(), but when you call it with an explicit argument it will override that default.
export default () => {
const [storedValue, setStoredValue] = React.useState('');
const [errorMessage, setErrorMessage] = React.useState('');
const saveData = async (dataToSave = storedValue) => {
try {
const localValue = await AsyncStorage.setItem(STORAGE_KEY, dataToSave);
if (localValue !== null) {
setStoredValue(dataToSave);
Alert.alert('Data successfully saved');
}
console.log('stored val', dataToSave);
} catch (e) {
setErrorMessage('Something went wrong');
}
};
return [saveData, errorMessage];
};