I want to play multiple audio files in my React Native application. Currently, it is one audio at a time and i also want single audio to be played at a time. What I want is if one audio is playing and suddenly the user tapped on the second audio button the first one will get paused and second should play. And when the user tapped the first again the paused audio will start again from where it is paused. Similar to whatsapp Audio Messages.
I am using react-native-audio-recorder-player for recording and playing audio in my application.
check app design
My FlatList Item Design:
{ { this.toggleMediaPlayer(item.audiourl, index) } }}>
<TouchableOpacity
style={styles.viewBarWrapper}
onPress={this.onStatusPress}
>
<View style={styles.viewBar}>
<View style={styles.viewBarPlay} />
</View>
</TouchableOpacity>
<Text style={styles.txtCounter}>
{/* {this.state.playTime} / {this.state.duration} */}
</Text>
</View>
MediaPlayer Function:
`toggleMediaPlayer(mediaPath, index) {
if (this.state.mediaFlag[index] == false) {
this.onStartPlay(mediaPath, index)
this.state.mediaFlag[index] = true;
var cloneObj = Object.assign({}, this.state.mediaFlag);
this.setState({ mediaFlag: cloneObj });
console.log(this.state.mediaFlag)
}
else {
this.onPausePlay(mediaPath)
this.state.mediaFlag[index] = false;
var cloneObj = Object.assign({}, this.state.mediaFlag);
this.setState({ mediaFlag: cloneObj });
console.log(this.state.mediaFlag)
}
}
`
REST CODE
audioRecorderPlayer = new AudioRecorderPlayer();
async onStartPlay(path, index) {
console.log('onStartPlay');
this.audioRecorderPlayer.stopPlayer();
const msg = await this.audioRecorderPlayer.startPlayer(path);
console.log(msg);
this.audioRecorderPlayer.addPlayBackListener(async (e) => {
if (e.current_position === e.duration) {
console.log('finished');
// await this.setState({ mediaFlag: !this.state.mediaFlag });
this.state.mediaFlag[index] = false;
var cloneObj = Object.assign({}, this.state.mediaFlag);
this.setState({ mediaFlag: cloneObj });
console.log(this.state.mediaFlag)
this.audioRecorderPlayer.stopPlayer();
this.audioRecorderPlayer.removePlayBackListener();
}
else {
this.setState({
currentPositionSec: e.current_position,
currentDurationSec: e.duration,
playTime: this.audioRecorderPlayer.mmssss(Math.floor(e.current_position)),
duration: this.audioRecorderPlayer.mmssss(Math.floor(e.duration)),
})
}
return;
});
};
onPausePlay = async () => {
await this.audioRecorderPlayer.pausePlayer();
};
async onStopPlay(index) {
console.log('onStopPlay');
this.audioRecorderPlayer.stopPlayer();
this.audioRecorderPlayer.removePlayBackListener();
};
As of v2.4.3, it is not a feature of react-native-audio-recorder-player (see https://github.com/dooboolab/react-native-audio-recorder-player/issues/130)
But i had to do it myself so here's how i managed to do it:
The main point is to have a parent component of all players (or a store - like redux or react context) which knows the state of the players (is a player reading its sound? which one?)
Then, when a player plays a sound, it calls the parent component and the parent component will stop any other player currently reading - it has to be done in a certain order to avoid odd behaviour
Here's my code simplified:
const ParentComponent = () => {
const [playerRecording, setPlayerRecording] = useState(false); // Which recording is currently playing?
const onPlay = async (onStartPlay, playerId) => {
// No player are reading or it is the same player
if (!playerRecording || playerRecording === playerId)
return onStartPlay();
// Another player is reading - stop it before starting the new one
audioRecorderPlayer.removePlayBackListener();
await audioRecorderPlayer.resumePlayer();
await audioRecorderPlayer.stopPlayer();
// Start the new player
onStartPlay();
};
}
const PlayerComponent = ({onPlay, setPlayerRecording, playerId}) => {
const onStartPlay = async () => {
setPlayerRecording(playerId);
audioRecorderPlayer.startPlayer(path);
...
}
return (
<PlayButton onPress={() => onPlay(onStartPlay, playerId)} />
)
}
Related
I am working on a react native app, at this moment I am using EXPO , and I am using expo-av to record the audio.
I would like to know if it is posible to set a minium recording time. I mean, when the user press the recording button, to automatically maintain the recording going for at least 5 seconds.
If this is not posible, maybe, alert the user to record for longer when the audios are too short.
here is the code for recording:
async function startRecording() {
try {
console.log('Requesting permissions..');
await Audio.requestPermissionsAsync();
await Audio.setAudioModeAsync({
allowsRecordingIOS: true,
playsInSilentModeIOS: true,
});
console.log('Starting recording..');
const recording = new Audio.Recording();
await recording.prepareToRecordAsync(Audio.RECORDING_OPTIONS_PRESET_HIGH_QUALITY);
await recording.startAsync();
setRecording(recording);
console.log('Recording started');
} catch (err) {
console.error('Failed to start recording', err);
}
}
async function stopRecording() {
console.log('Stopping recording..');
setRecording(undefined);
await recording.stopAndUnloadAsync();
const uri = recording.getURI();
console.log('Recording stopped and stored at', uri);
setPath(uri);
uploadAudio(uri);
}
the screen display:
return (
<View style={styles.container}>
<Button
title={recording ? 'Stop Recording' : 'Start Recording'}
onPress={recording ? stopRecording : startRecording}
/>
We can achieve that by creating a boolean state and use it to either for alert or disabling the stop function for 5 seconds
Method 1
...
const [disable, setDisable] = useState(false);
...
async function startRecording() {
try {
...
setDisable(true)
setTimeout(() => {
setDisable(false)
}, 5000);
...
...
<Button
title={recording ? 'Stop Recording' : 'Start Recording'}
onPress={recording ? stopRecording : startRecording}
disabled={disable} // Button will only enabled 5 seconds after recording started
/>
...
Method 2
...
const [disable, setDisable] = useState(false);
...
async function startRecording() {
try {
...
setDisable(true)
setTimeout(() => {
setDisable(false)
}, 5000);
...
...
async function stopRecording() {
if(disable){
Alert.alert('Min 5 Sec Required')
return;
}//remaining func will only call if disable is false that is after 5 sec
...
...
react-native-audio-recorder-player, library for use in my project to record audio.
When I start recording everything works fine, but close the application when I call the function to stop recording the audio "audioRecorderPlayer.stopRecorder ()".
It works perfectly on IOS (emulator and real device), and on Android emulator, but, it doesn't work on real android device.
What could be the cause of this error and how could it be resolved?
"react-native": "0.63.3",
"react-native-audio-recorder-player": "^2.6.0-rc3",
hooks for control
const [record, setRecord] = useState(false);
start record function
async function handleRecord() {
try {
const permissions = await getPermissions();
if (permissions) {
const path = Platform.select({
ios: `audio-${new Date().getTime()}.m4a`,
android: `sdcard/audio-${new Date().getTime()}.mp3`
});
const audioSet = {
AudioEncoderAndroid: AudioEncoderAndroidType.AAC,
AudioSourceAndroid: AudioSourceAndroidType.MIC,
AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high,
AVNumberOfChannelsKeyIOS: 2,
AVFormatIDKeyIOS: AVEncodingOption.aac
};
const result = await audioRecorderPlayer.startRecorder(path, audioSet);
setRecord(true);
audioRecorderPlayer.addRecordBackListener(e => {
return;
});
}
} catch (err) {
}
}
stop record function
async function onStopRecord() {
try {
const result = await audioRecorderPlayer.stopRecorder();
audioRecorderPlayer.removeRecordBackListener();
setRecord(false);
setFiles([
...files,
{
uri: result,
type: "audio/mpeg",
name: `audio-${new Date().getTime()}.mp3`
}
]);
}catch(error){
Alert.alert(
"Erro",
error
);
}
}
I am attempting to cycle through sound objects in an Array, using an index value that begins at 0 and increments/decrements depending on whether I press next or back.
This is for a music player for react-native using the Expo//expo-av library. I'll include all relevant code.
State I have in my Context file:
const initialState = {
startMusic: () => null,
stopMusic: () => null,
soundObject: null,
isPlaying: false,
currentIndex: 0,
}
useState()
const [soundObject, setSoundObject] = useState(initialState.soundObject)
const [isPlaying, setIsPlaying] = useState(initialState.isPlaying)
const [currentIndex, setCurrentIndex] = useState(initialState.currentIndex)
Start Music function
const startMusic = async () => {
try {
const songToPlay = songs[currentIndex].song
const source = songs[currentIndex].path
await songToPlay.loadAsync(source)
await songToPlay.playAsync()
setSoundObject(songToPlay)
setIsPlaying(true)
return new Promise(resolve => { // I made this promise when I was setting a loop to play through music. May not need this anymore
songToPlay.setOnPlaybackStatusUpdate(playbackStatus => {
if (playbackStatus.didJustFinish) {
console.log("Song finished")
resolve()
}
})
})
} catch (error) {
console.log(`Error: ${error}`)
return
}
}
And finally, the handler functions that are supposed to cycle through songs:
const handlePreviousTrack = async () => {
if (soundObject) {
await soundObject.stopAsync()
await soundObject.unloadAsync()
// setSoundObject(null)
let newIndex = currentIndex
newIndex < songs.length - 1 ? newIndex-- : (newIndex = 0)
setCurrentIndex(newIndex)
startMusic()
console.log(currentIndex)
}
}
const handleNextTrack = async () => {
if (soundObject) {
await soundObject.stopAsync()
await soundObject.unloadAsync()
// setSoundObject(null)
let newIndex = currentIndex
newIndex < songs.length - 1 ? newIndex++ : (newIndex = 0)
setCurrentIndex(newIndex)
startMusic()
console.log(currentIndex)
}
}
Cycling through next/previous does not go in order.
Sometimes it works, sometimes previous goes to the next song, sometimes pressing next just replays the first song.
Am I manipulating the state via currentIndex incorrectly?
I don't see anything alarmingly wrong in the above code by quickly looking through it. One suggestion I would have is to take your startMusic() call out of your two handleTrack methods and instead just have them set index state as they are doing.
Instead add a useEffect that will fire every time the index changes and put your startMusic() in that useEffect.
It would look something like this:
useEffect(() => {
... (other logic)
startMusic()
}, [currentIndex])
This will ensure startMusic will only get called after the currentIndex state has been changed and not be called too soon etc. If not it may make it easier to track down when the state is changing regardless
I have created a Button to display a rewarded Ad whenever it gets clicked.
There are two issues now:
1. It takes too much time to load the Ad (I can click the button once or twice before anything happens).
2. I want to reload the Ad right after it closes. It works but the App needs to restart.
AdMobRewardedComponent.js
async componentDidMount() {
await setTestDeviceIDAsync("EMULATOR");
AdMobRewarded.setAdUnitID("ca-app-pub-3940256099942544/5224354917");
AdMobRewarded.addEventListener("rewardedVideoDidLoad", () => {
console.log("VideoLoaded")
});
AdMobRewarded.addEventListener("rewardedVideoDidFailToLoad", () =>
console.log("FailedToLoad")
);
AdMobRewarded.addEventListener("rewardedVideoDidOpen", () =>
console.log("Opened")
);
AdMobRewarded.addEventListener("rewardedVideoDidClose", () => {
loadAd(request.build());
console.log("Closed")
});
AdMobRewarded.addEventListener("rewardedVideoWillLeaveApplication", () =>
console.log("LeaveApp")
);
AdMobRewarded.addEventListener("rewardedVideoDidStart", () =>
console.log("Started")
);
AdMobRewarded.addEventListener("rewardedVideoDidRewardUser", () =>
console.log("Rewarded"),
);
await AdMobRewarded.requestAdAsync();
}
componentWillUnmount() {
AdMobRewarded.removeAllListeners();
}
_handlePress = async () => {
await AdMobRewarded.showAdAsync();
};
render() {
const { loadedAd } = this.state;
return (
<TouchableButton onPress={this._handlePress} title="Coins erhalten!" image="adButton" status="active" style={styles.adButton}/>
);
}
};
Is there a way to request a new Ad without restarting the whole App?
Thanks for every answer!
In order to prevent the problem which the button can be pressed several times you can use debounce functionality:
React Native: Using lodash debounce
Or, you can manage your open ads at your store, so you can make sure that you don't open an ad twice and you can open a new ad once the previous ad had been closed, for instance:
const { isInterstitialAdOpen, } = useSelector(state => state.home);
if ((!__DEV__ && !isInterstitialAdOpen)) {
dispatch(openInterstitialAd());
AdMobInterstitial.setAdUnitID(AdMobController.getGeneralInterstitialId());
AdMobInterstitial.setTestDevices([AdMobInterstitial.simulatorId]);
AdMobInterstitial.addEventListener('adClosed', () => dispatch(closeInterstitialAd()));
AdMobInterstitial.requestAd().then(() => AdMobInterstitial.showAd()).catch((error) => {
});
}
I want users to be able to take photos from within my app and have the photos save to their gallery (so that I can later view them in a photo-picker).
I have the following code from react-native-camera, it's basically the bare-bones demo code.
takePicture() {
const options = { quality: 0.5, fixOrientation: false, width: 1920 };
if (this.camera) {
this.camera
.takePictureAsync(options)
.then(data => {
this.saveImage(data.uri);
})
.catch(err => {
console.error("capture picture error", err);
});
} else {
console.error("No camera found!");
}
}
}
To move the attachment, I am using react-native-fs, as follows (more basic demo-y code):
const dirHome = Platform.select({
ios: `${RNFS.DocumentDirectoryPath}/Pictures`,
android: `${RNFS.ExternalStorageDirectoryPath}/Pictures`
});
const dirPictures = `${dirHome}/MyAppName`;
saveImage = async filePath => {
try {
// set new image name and filepath
const newImageName = `${moment().format("DDMMYY_HHmmSSS")}.jpg`;
const newFilepath = `${dirPictures}/${newImageName}`;
// move and save image to new filepath
const imageMoved = await this.moveAttachment(filePath, newFilepath);
console.log("image moved: ", imageMoved);
} catch (error) {
console.log(error);
}
};
moveAttachment = async (filePath, newFilepath) => {
return new Promise((resolve, reject) => {
RNFS.mkdir(dirPictures)
.then(() => {
RNFS.moveFile(filePath, newFilepath)
.then(() => {
console.log("FILE MOVED", filePath, newFilepath);
resolve(true);
})
.catch(error => {
console.log("moveFile error", error);
reject(error);
});
})
.catch(err => {
console.log("mkdir error", err);
reject(err);
});
});
};
When taking a photo, this code executes and prints that the image has been moved within a couple of seconds. But, when I look into the built in Gallery App on the device, it often takes several minutes for the image to finally load. I've tried this across many different devices, both emulated and physical... am I doing something wrong? Thanks!
This was caused by Android's Media Scanner not immediately realizing new files existed.
From this Git issue and the subsequent PR: https://github.com/itinance/react-native-fs/issues/79
I modified my code as follows:
saveImage = async filePath => {
try {
// set new image name and filepath
const newImageName = `${moment().format("DDMMYY_HHmmSSS")}.jpg`;
const newFilepath = `${dirPicutures}/${newImageName}`;
// move and save image to new filepath
const imageMoved = await this.moveAttachment(filePath, newFilepath).then(
imageMoved => {
if (imageMoved) {
return RNFS.scanFile(newFilepath);
} else {
return false;
}
}
);
console.log("image moved", imageMoved);
} catch (error) {
console.log(error);
}
};
using RNFS's scanFile method to force the Media Scanner to realize the file exists. This is rough code I'll need to clean up, but it gets the job done.