So, here is the problem. I'm using audio html5 element in my react project.
The typical flow for problem is:
the song is playing.
User presses pause on some time(let's say that time is 1.20).
User locks the phone.
After several minutes user unlocks his phone, presses "Play" button and here what happens:
The mediaPositionState count current time as previous(1.20) PLUS current time of the audio, instead of counting just current.
This extra 1.20 is remaining even when changing songs.
I've tried to control it in useEffect below
useEffect(() => {
const audioEl = audioRef.current;
if (audioEl) {
audioEl.addEventListener('timeupdate', updateTime);
audioEl.addEventListener('loadeddata', updatePositionState);
}
return () => {
if (audioEl) {
audioEl.removeEventListener('timeupdate', updateTime);
audioEl.removeEventListener('loadeddata', updateTime);
updatePositionState();
}
};
}, []);
but it works normally only when user is in focus with audio.
Also I have following code:
function updatePositionState() {
if (navigator.mediaSession?.setPositionState) {
navigator.mediaSession.setPositionState({
duration: audioRef.current?.duration ?? 0.0,
position: audioRef.current?.currentTime ?? 0.0,
});
}
}
const createMediaSession = (state: AudioStateType) => {
if (navigator.mediaSession) {
navigator.mediaSession.metadata = new MediaMetadata({
title: state.currentSongName,
artist: state.currentArtistName,
album: state.currentAlbumName,
artwork: [
{
sizes: '300x300',
src: `http://storage.musicstream.app/cover/${state.currentAlbumCoverId}`,
},
],
});
navigator.mediaSession.setActionHandler('play', function () {
dispatch({ type: 'resume' });
updatePositionState();
});
navigator.mediaSession.setActionHandler('pause', function () {
dispatch({ type: 'pause' });
updatePositionState();
});
navigator.mediaSession.setActionHandler('seekto', function (details) {
dispatch({ type: 'manual_update_time', time: details.seekTime });
updatePositionState();
});
navigator.mediaSession.setActionHandler('previoustrack', () => {
return 0;
});
navigator.mediaSession.setActionHandler('nexttrack', () => {
return 0;
});
}
};
I don't know how to normally describe the problem, let's assume that mediaposition messes up when user swipes out the MediaSession notification.
I will provide more code if you ask.
Also I provide the screenshots(despite I tried to force the problem similar one occured: it shows time as the end of track).
current song time
current song time is okay when paused
current song time is messed when playing
Adding updateTime function by request
It is just for updating state in react.Context
const updateTime = () => {
if (audioRef.current) {
dispatch({ type: 'update_time', time: audioRef.current.currentTime });
}
};
Also, the full reducer looks like this(I don't think it would be helpful):
function audioReducer(state: AudioStateType, action: Action): AudioStateType {
switch (action.type) {
case 'fetch_and_play': {
play(action.songData?.currentSongId).then(() => {
dispatch({
type: 'play',
songData: {
...action.songData,
length: audioRef.current?.duration,
},
});
});
return state;
}
case 'play': {
createMediaSession({ ...state, ...action.songData });
return { ...state, ...action.songData };
}
case 'pause': {
pause();
return { ...state, songIsPaused: true };
}
case 'resume': {
resume();
return { ...state, songIsPaused: false };
}
case 'update_time': {
return { ...state, currentTime: action.time };
}
case 'manual_update_time': {
if (audioRef.current) {
audioRef.current.currentTime = action.time;
return { ...state, currentTime: action.time };
} else {
return state;
}
}
default: {
return state;
}
}
}
I made a codesandbox, where you can see my problem. https://codesandbox.io/s/wizardly-wiles-87qi1?file=/src/App.js
In order to truly understand please use an android phone
Reducer should be Pure function, use action before reducer for dispatch and other staff:
Action.ts
const fetchAndPlay = (currentSongId) => {
play(currentSongId).then(() => {
createMediaSession({ ...state, ...action.songData });
dispatch({
type: 'play',
//just pass data u need to show or you want rerender them
length: audioRef.current?.duration,
});
});
}
Reducer.ts
function audioReducer(state: AudioStateType, action: Action): AudioStateType {
switch (action.type) {
case 'play':
return { ...state, length:action.length, playing: true};
...
}
In your component just call this action :
fetchAndPlay(songId)
Related
i want to implement deffered link to the app, process link params after app first installing. onInstallConversionData method invokes when app is already installed, but not on first launch. i implemented this code in App.tsx.
const onInstallConversionDataCanceller = appsFlyer.onInstallConversionData(
(res: any) => {
if (JSON.parse(res.data.is_first_launch) == true) {
if (res.data.af_status === 'Non-organic') {
const media_source = res.data.media_source;
const campaign = res.data.campaign;
console.log(
'This is first launch and a Non-Organic install. Media source: ' +
media_source +
' Campaign: ' +
campaign,
);
} else if (res.data.af_status === 'Organic') {
console.log('This is first launch and a Organic Install');
}
} else {
console.log('This is not first launch');
}
},
);
appsFlyer.initSdk(
{
devKey: 'key',
isDebug: false, // set to true if you want to see data in the logs
appId: '1234567', // iOS app id
},
(result: any) => {
console.log(result);
},
(error: any) => {
console.error(error);
},
);
return () => {
onInstallConversionDataCanceller();
console.log('unregister onInstallConversionDataCanceller');
onAppOpenAttributionCanceller();
console.log('unregister onAppOpenAttributionCanceller');
};
also i have onNewIntent implementation inside MainActivity class:
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
}
LoginScreen.js
this.props.navigator.push({
screen: "auxxa.LandingScreen",
passProps: { login: true },
overrideBackPress: true,
navigatorStyle: {
navBarHidden: true
}
});
LandingScreen.js
constructor(props) {
super(props);
this.handleBackButtonClick = this.handleBackButtonClick.bind(this);
// this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
this.state = {
size: { width, height },
tileData: null,
isLoading: true,
user_id: null,
refetching: false,
access_token: null
};
}
componentWillMount() {
BackHandler.addEventListener(
"hardwareBackPress",
this.handleBackButtonClick
);
}
handleBackButtonClick() {
console.log("check login " + this.props.login);
if (this.backPressed && this.backPressed > 0) {
if (this.props.login) {
console.log("login");
RNExitApp.exitApp();
} else {
console.log("root");
this.props.navigator.popToRoot({ animated: false });
return false;
}
}
this.backPressed = 1;
this.props.navigator.showSnackbar({
text: "Press one more time to exit",
duration: "long"
});
return true;
}
componentDidMount() {
BackHandler.removeEventListener(
"hardwareBackPress",
this.handleBackButtonClick
);
}
I used react-native-navigation from Wix for my app nevigation purpose.Here I have attached login screen and landing screen.after successful login app navigate to landing screen.after that I click back button It will return to login screen.I need to avoid that.How can I do that thing? I tried to exit from the app.But it also not working properly.
Please help me if some one know this.Thanks in advanced.
Use this call in handleBackButtonClick function and why are you removing the listener in componentDidMount ?
this.props.navigator.resetTo({ screen: 'example.ScreenThree'})
.
Uncomment this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this)); on your constructor to listen to navigation events
and add the navigationEvent listener method
onNavigatorEvent(event: NavigatorEvent) {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'skill_information') {
// Add here whatever you would like to do (this.handleBackButtonClick() for example)
}
}
I am new in react-native an I am working on a react-native project, I use react-native-navigation from wix and didn't find any solution for how to clear the SplashScreen or any Screen from stack which I don't need to go back again.
I use this to navigate after 2 second.
componentWillMount(){
setTimeout(
() => {
this.props.navigator.push({
screen: 'SampleApp.LoginScreen',
})
}, 2000
);
}
and this in my index.js
export function registerScreens() {
Navigation.registerComponent('SampleApp.SplashScreen', () => SplashScreen);
Navigation.registerComponent('SampleApp.LoginScreen', () => LoginScreen);
}
Please help me to find the solution where I need to call finish() or is there something else. Thanks in advance
You can try this,
import {BackHandler} from 'react-native';
constructor(props) {
super(props)
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
onNavigatorEvent(event) {
switch (event.id) {
case 'willAppear':
this.backHandler = BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);
this.backHandler.remove();
break;
case 'willDisappear':
this.backPressed = 0;
break;
default:
break;
}
}
handleBackPress = () => {
if (this.backPressed && this.backPressed > 0) {
this.props.navigator.popToRoot({ animated: false });
return false;
}
this.backPressed = 1;
this.props.navigator.showSnackbar({
text: 'Press one more time to exit',
duration: 'long',
});
return true;
}
I've read somewhere that when you use any navigator from the react-navigation package and if you implement redux; every component in the navigators' routes will be reloaded (i.e componentWillRecieveProps)
However, I've got two pages where the user can login
export const MainScreenTabNavigator = TabNavigator({
Start: {
screen: Start,
navigationOptions: {
title: 'Start',
},
},
},{
tabBarPosition: "bottom",
tabBarOptions: {
activeTintColor: '#222',
labelStyle: {
color: '#ddd',
},
style: { backgroundColor: '#333' },
}
});
export const AppNavigator = StackNavigator({
Main: {
screen: MainScreenTabNavigator, // Nested tab navigator
},
Login: {
screen: Login,
navigationOptions: {
title: 'Aanmelden',
}
},
Camera: {
screen: AppCamera,
navigationOptions: {
title: 'Camera',
}
}
}, {
mode: 'modal',
headerMode: 'none',
});
The login screen is initially shown to the user. It has a form where the user can enter it's credentials manually and a button that navigates to the camera where the user can scan a QR code with login credentials.
In both cases the user dispatches a login action.
Both the login page and the camera page listen to the same prop changes:
componentWillReceiveProps(nextProps) {
if (nextProps.account.loginSuccess) {
this.props.navigation.dispatch(NavigationActions.navigate({ routeName: 'Start' }));
} else {
if (nextProps.account.loginError) {
Toast.showLongBottom(nextProps.loginError);
}
}
this.setState({spinnerVisible: false});
}
The app successfully navigates to 'Start' but when it does both the Login and Camera page are reloaded, causing an infinite loop into componentWillReceiveProps and infinitely navigates to 'Start' over and over again.
This is my navigation reducer:
function nav(state = initialNavState, action) {
const nextState = AppNavigator.router.getStateForAction(action, state);
// Simply return the original `state` if `nextState` is null or undefined.
return nextState || state;
}
What can I do to prevent this behavior?
Well,
As a temporary solution I introduced another boolean into the nav state:
function nav(state = initialNavState, action) {
let nextState = null;
if (action.type === 'Navigation/NAVIGATE' && action.routeName === 'Start') {
nextState = {...AppNavigator.router.getStateForAction(action, state), authenticated: true};
} else {
nextState = AppNavigator.router.getStateForAction(action, state);
}
// Simply return the original `state` if `nextState` is null or undefined.
return nextState || state;
}
I use authenticated to check if the login or camera component should navigate to start after logging in.
It works but it still feels like I'm missing something.
I am quite new to React / React Native / Redux so I feel I am doing something wrong.
The problem
I want to show a spinner while an API is called, and an error message once this API call fails. Props are not updating, and so the components don't show the desired message or spinner
The code (only the relevant chunks)
The component
class Home extends Component {
componentWillMount() {
this.props.tokenGet();
}
renderSpinner() {
if (this.props.loading) {
return (
<Spinner size="large" />
);
}
return null;
}
renderMessage() {
if (this.props.message) {
return (
<Text style={{flex: 1, background: red, color: black}}>
{ this.props.message }
</Text>
)
}
return null;
}
render() {
return (
{ this.renderSpinner() }
{ this.renderMessage() }
)
}
}
const mapStateToProps = (state) => {
const { auth } = state;
const {
loading,
token,
message
} = auth || {
loading: false,
token: null,
message: null
};
return {
loading,
token,
message
}
};
export default connect(mapStateToProps, { tokenGet } )(Home);
The action creator
export const tokenGet = () => {
return (dispatch) => {
dispatch({ type: 'TOKEN_GET_START'});
// Perform the actual API call
let requestToken = fetch(apiBaseUrl + "/tokens", {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(.....)
});
Promise
.race([timeout, requestToken])
.then((response) => response.json())
.then((responseJson) => {
... not relevant ...
})
.catch((error) => {
dispatch({ type: 'TOKEN_GET_FAIL', payload: error});
});
The timeout function, which gets called when the server fails to respond
let timeout = new Promise((resolve, reject) => {
setTimeout(reject, 2000, 'Request timed out. Please check your internet connection.');
});
The reducer
import {
TOKEN_GET_START,
TOKEN_GET_SUCCESS,
TOKEN_GET_FAIL
} from '../actions/types';
const INITIAL_STATE = {
loading: false,
token: null,
message: null
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case TOKEN_GET_START:
return { ...state, loading: true };
case TOKEN_GET_SUCCESS:
return { ...state, loading: false, token: action.payload };
case TOKEN_GET_FAIL:
return { ...state, loading: false, message: action.payload };
default:
return state;
}
};
The combined reducers
import { combineReducers } from 'redux';
import AuthReducer from './AuthReducer';
export default combineReducers({
auth: AuthReducer
});
The actual behavior is that the props don't change and no message or spinner is visible. With some console logs I know that the API call ends because of the timeout. I am not sure if the state gets updated properly though. I don't know in at which point I can console log this.
It turned out to be because of the quotes in 'TOKEN_GET_FAIL'
That is a string and not the const I need. So I changed to TOKEN_GET_FAIL and it works.