expo-av not able to record audio in the background when setting staysActiveInBackground: true

Please provide the following:

  1. SDK Version: v36.0.0
  2. Platforms(Android/iOS/web/all): Android

not able to record audio in the background on my android phone with version Android 9. i have included my example code to record audio. when i take the app to the background the audio is flat.

/**

  • @flow
    */

import React from ‘react’;
import {
Dimensions,
Image,
Slider,
StyleSheet,
Text,
TouchableHighlight,
View,
} from ‘react-native’;
import { Asset } from ‘expo-asset’;
import { Audio } from ‘expo-av’;
import * as FileSystem from ‘expo-file-system’;
import * as Font from ‘expo-font’;
import * as Permissions from ‘expo-permissions’;

class Icon {
constructor(module, width, height) {
this.module = module;
this.width = width;
this.height = height;
Asset.fromModule(this.module).downloadAsync();
}
}

const ICON_RECORD_BUTTON = new Icon(require(’./assets/images/record_button.png’), 70, 119);
const ICON_RECORDING = new Icon(require(’./assets/images/record_icon.png’), 20, 14);

const ICON_PLAY_BUTTON = new Icon(require(’./assets/images/play_button.png’), 34, 51);
const ICON_PAUSE_BUTTON = new Icon(require(’./assets/images/pause_button.png’), 34, 51);
const ICON_STOP_BUTTON = new Icon(require(’./assets/images/stop_button.png’), 22, 22);

const ICON_MUTED_BUTTON = new Icon(require(’./assets/images/muted_button.png’), 67, 58);
const ICON_UNMUTED_BUTTON = new Icon(require(’./assets/images/unmuted_button.png’), 67, 58);

const ICON_TRACK_1 = new Icon(require(’./assets/images/track_1.png’), 166, 5);
const ICON_THUMB_1 = new Icon(require(’./assets/images/thumb_1.png’), 18, 19);
const ICON_THUMB_2 = new Icon(require(’./assets/images/thumb_2.png’), 15, 19);

const { width: DEVICE_WIDTH, height: DEVICE_HEIGHT } = Dimensions.get(‘window’);
const BACKGROUND_COLOR = ‘#FFF8ED’;
const LIVE_COLOR = ‘#FF0000’;
const DISABLED_OPACITY = 0.5;
const RATE_SCALE = 3.0;

export default class App extends React.Component {
constructor(props) {
super(props);
this.recording = null;
this.sound = null;
this.isSeeking = false;
this.shouldPlayAtEndOfSeek = false;
this.state = {
haveRecordingPermissions: false,
isLoading: false,
isPlaybackAllowed: false,
muted: false,
soundPosition: null,
soundDuration: null,
recordingDuration: null,
shouldPlay: false,
isPlaying: false,
isRecording: false,
fontLoaded: false,
shouldCorrectPitch: true,
volume: 1.0,
rate: 1.0,
};
this.recordingSettings = JSON.parse(JSON.stringify(Audio.RECORDING_OPTIONS_PRESET_LOW_QUALITY));
// // UNCOMMENT THIS TO TEST maxFileSize:
// this.recordingSettings.android[‘maxFileSize’] = 12000;
}

componentDidMount() {
(async () => {
await Font.loadAsync({
‘cutive-mono-regular’: require(’./assets/fonts/CutiveMono-Regular.ttf’),
});
this.setState({ fontLoaded: true });
})();
this._askForPermissions();
}

_askForPermissions = async () => {
const response = await Permissions.askAsync(Permissions.AUDIO_RECORDING);
this.setState({
haveRecordingPermissions: response.status === ‘granted’,
});
};

_updateScreenForSoundStatus = status => {
if (status.isLoaded) {
this.setState({
soundDuration: status.durationMillis,
soundPosition: status.positionMillis,
shouldPlay: status.shouldPlay,
isPlaying: status.isPlaying,
rate: status.rate,
muted: status.isMuted,
volume: status.volume,
shouldCorrectPitch: status.shouldCorrectPitch,
isPlaybackAllowed: true,
});
} else {
this.setState({
soundDuration: null,
soundPosition: null,
isPlaybackAllowed: false,
});
if (status.error) {
console.log(FATAL PLAYER ERROR: ${status.error});
}
}
};

_updateScreenForRecordingStatus = status => {
if (status.canRecord) {
this.setState({
isRecording: status.isRecording,
recordingDuration: status.durationMillis,
});
} else if (status.isDoneRecording) {
this.setState({
isRecording: false,
recordingDuration: status.durationMillis,
});
if (!this.state.isLoading) {
this._stopRecordingAndEnablePlayback();
}
}
};

async _stopPlaybackAndBeginRecording() {
this.setState({
isLoading: true,
});
if (this.sound !== null) {
await this.sound.unloadAsync();
this.sound.setOnPlaybackStatusUpdate(null);
this.sound = null;
}
await Audio.setAudioModeAsync({
allowsRecordingIOS: true,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
playsInSilentModeIOS: true,
shouldDuckAndroid: true,
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX,
playThroughEarpieceAndroid: false,
staysActiveInBackground: true,
});
if (this.recording !== null) {
this.recording.setOnRecordingStatusUpdate(null);
this.recording = null;
}

const recording = new Audio.Recording();
await recording.prepareToRecordAsync(this.recordingSettings);
recording.setOnRecordingStatusUpdate(this._updateScreenForRecordingStatus);

this.recording = recording;
await this.recording.startAsync(); // Will call this._updateScreenForRecordingStatus to update the screen.
this.setState({
  isLoading: false,
});

}

async _stopRecordingAndEnablePlayback() {
this.setState({
isLoading: true,
});
try {
await this.recording.stopAndUnloadAsync();
} catch (error) {
// Do nothing – we are already unloaded.
}
const info = await FileSystem.getInfoAsync(this.recording.getURI());
console.log(FILE INFO: ${JSON.stringify(info)});
await Audio.setAudioModeAsync({
allowsRecordingIOS: false,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
playsInSilentModeIOS: true,
playsInSilentLockedModeIOS: true,
shouldDuckAndroid: true,
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX,
playThroughEarpieceAndroid: false,
staysActiveInBackground: true,
});
const { sound, status } = await this.recording.createNewLoadedSoundAsync(
{
isLooping: true,
isMuted: this.state.muted,
volume: this.state.volume,
rate: this.state.rate,
shouldCorrectPitch: this.state.shouldCorrectPitch,
},
this._updateScreenForSoundStatus
);
this.sound = sound;
this.setState({
isLoading: false,
});
}

_onRecordPressed = () => {
if (this.state.isRecording) {
this._stopRecordingAndEnablePlayback();
} else {
this._stopPlaybackAndBeginRecording();
}
};

_onPlayPausePressed = () => {
if (this.sound != null) {
if (this.state.isPlaying) {
this.sound.pauseAsync();
} else {
this.sound.playAsync();
}
}
};

_onStopPressed = () => {
if (this.sound != null) {
this.sound.stopAsync();
}
};

_onMutePressed = () => {
if (this.sound != null) {
this.sound.setIsMutedAsync(!this.state.muted);
}
};

_onVolumeSliderValueChange = value => {
if (this.sound != null) {
this.sound.setVolumeAsync(value);
}
};

_trySetRate = async (rate, shouldCorrectPitch) => {
if (this.sound != null) {
try {
await this.sound.setRateAsync(rate, shouldCorrectPitch);
} catch (error) {
// Rate changing could not be performed, possibly because the client’s Android API is too old.
}
}
};

_onRateSliderSlidingComplete = async value => {
this._trySetRate(value * RATE_SCALE, this.state.shouldCorrectPitch);
};

_onPitchCorrectionPressed = async value => {
this._trySetRate(this.state.rate, !this.state.shouldCorrectPitch);
};

_onSeekSliderValueChange = value => {
if (this.sound != null && !this.isSeeking) {
this.isSeeking = true;
this.shouldPlayAtEndOfSeek = this.state.shouldPlay;
this.sound.pauseAsync();
}
};

_onSeekSliderSlidingComplete = async value => {
if (this.sound != null) {
this.isSeeking = false;
const seekPosition = value * this.state.soundDuration;
if (this.shouldPlayAtEndOfSeek) {
this.sound.playFromPositionAsync(seekPosition);
} else {
this.sound.setPositionAsync(seekPosition);
}
}
};

_getSeekSliderPosition() {
if (
this.sound != null &&
this.state.soundPosition != null &&
this.state.soundDuration != null
) {
return this.state.soundPosition / this.state.soundDuration;
}
return 0;
}

_getMMSSFromMillis(millis) {
const totalSeconds = millis / 1000;
const seconds = Math.floor(totalSeconds % 60);
const minutes = Math.floor(totalSeconds / 60);

const padWithZero = number => {
  const string = number.toString();
  if (number < 10) {
    return '0' + string;
  }
  return string;
};
return padWithZero(minutes) + ':' + padWithZero(seconds);

}

_getPlaybackTimestamp() {
if (
this.sound != null &&
this.state.soundPosition != null &&
this.state.soundDuration != null
) {
return ${this._getMMSSFromMillis(this.state.soundPosition)} / ${this._getMMSSFromMillis( this.state.soundDuration )};
}
return ‘’;
}

_getRecordingTimestamp() {
if (this.state.recordingDuration != null) {
return ${this._getMMSSFromMillis(this.state.recordingDuration)};
}
return ${this._getMMSSFromMillis(0)};
}

render() {
if(!this.state.fontLoaded) {
return (

)
}

if (!this.state.haveRecordingPermissions){
    return (
        <View style={styles.container}>
            <View />
            <Text style={[styles.noPermissionsText, { fontFamily: 'cutive-mono-regular' }]}>
              You must enable audio recording permissions in order to use this app.
            </Text>
            <View />
        </View>
    )
}

return (
  <View style={styles.container}>
    <View
      style={[
        styles.halfScreenContainer,
        {
          opacity: this.state.isLoading ? DISABLED_OPACITY : 1.0,
        },
      ]}>
      <View />
      <View style={styles.recordingContainer}>
        <View />
        <TouchableHighlight
          underlayColor={BACKGROUND_COLOR}
          style={styles.wrapper}
          onPress={this._onRecordPressed}
          disabled={this.state.isLoading}>
          <Image style={styles.image} source={ICON_RECORD_BUTTON.module} />
        </TouchableHighlight>
        <View style={styles.recordingDataContainer}>
          <View />
          <Text style={[styles.liveText, { fontFamily: 'cutive-mono-regular' }]}>
            {this.state.isRecording ? 'LIVE' : ''}
          </Text>
          <View style={styles.recordingDataRowContainer}>
            <Image
              style={[styles.image, { opacity: this.state.isRecording ? 1.0 : 0.0 }]}
              source={ICON_RECORDING.module}
            />
            <Text style={[styles.recordingTimestamp, { fontFamily: 'cutive-mono-regular' }]}>
              {this._getRecordingTimestamp()}
            </Text>
          </View>
          <View />
        </View>
        <View />
      </View>
      <View />
    </View>
    <View
      style={[
        styles.halfScreenContainer,
        {
          opacity:
            !this.state.isPlaybackAllowed || this.state.isLoading ? DISABLED_OPACITY : 1.0,
        },
      ]}>
      <View />
      <View style={styles.playbackContainer}>
        <Slider
          style={styles.playbackSlider}
          trackImage={ICON_TRACK_1.module}
          thumbImage={ICON_THUMB_1.module}
          value={this._getSeekSliderPosition()}
          onValueChange={this._onSeekSliderValueChange}
          onSlidingComplete={this._onSeekSliderSlidingComplete}
          disabled={!this.state.isPlaybackAllowed || this.state.isLoading}
        />
        <Text style={[styles.playbackTimestamp, { fontFamily: 'cutive-mono-regular' }]}>
          {this._getPlaybackTimestamp()}
        </Text>
      </View>
      <View style={[styles.buttonsContainerBase, styles.buttonsContainerTopRow]}>
        <View style={styles.volumeContainer}>
          <TouchableHighlight
            underlayColor={BACKGROUND_COLOR}
            style={styles.wrapper}
            onPress={this._onMutePressed}
            disabled={!this.state.isPlaybackAllowed || this.state.isLoading}>
            <Image
              style={styles.image}
              source={this.state.muted ? ICON_MUTED_BUTTON.module : ICON_UNMUTED_BUTTON.module}
            />
          </TouchableHighlight>
          <Slider
            style={styles.volumeSlider}
            trackImage={ICON_TRACK_1.module}
            thumbImage={ICON_THUMB_2.module}
            value={1}
            onValueChange={this._onVolumeSliderValueChange}
            disabled={!this.state.isPlaybackAllowed || this.state.isLoading}
          />
        </View>
        <View style={styles.playStopContainer}>
          <TouchableHighlight
            underlayColor={BACKGROUND_COLOR}
            style={styles.wrapper}
            onPress={this._onPlayPausePressed}
            disabled={!this.state.isPlaybackAllowed || this.state.isLoading}>
            <Image
              style={styles.image}
              source={this.state.isPlaying ? ICON_PAUSE_BUTTON.module : ICON_PLAY_BUTTON.module}
            />
          </TouchableHighlight>
          <TouchableHighlight
            underlayColor={BACKGROUND_COLOR}
            style={styles.wrapper}
            onPress={this._onStopPressed}
            disabled={!this.state.isPlaybackAllowed || this.state.isLoading}>
            <Image style={styles.image} source={ICON_STOP_BUTTON.module} />
          </TouchableHighlight>
        </View>
        <View />
      </View>
      <View style={[styles.buttonsContainerBase, styles.buttonsContainerBottomRow]}>
        <Text style={[styles.timestamp, { fontFamily: 'cutive-mono-regular' }]}>Rate:</Text>
        <Slider
          style={styles.rateSlider}
          trackImage={ICON_TRACK_1.module}
          thumbImage={ICON_THUMB_1.module}
          value={this.state.rate / RATE_SCALE}
          onSlidingComplete={this._onRateSliderSlidingComplete}
          disabled={!this.state.isPlaybackAllowed || this.state.isLoading}
        />
        <TouchableHighlight
          underlayColor={BACKGROUND_COLOR}
          style={styles.wrapper}
          onPress={this._onPitchCorrectionPressed}
          disabled={!this.state.isPlaybackAllowed || this.state.isLoading}>
          <Text style={[{ fontFamily: 'cutive-mono-regular' }]}>
            PC: {this.state.shouldCorrectPitch ? 'yes' : 'no'}
          </Text>
        </TouchableHighlight>
      </View>
      <View />
    </View>
  </View>
);

}
}

const styles = StyleSheet.create({
emptyContainer: {
alignSelf: ‘stretch’,
backgroundColor: BACKGROUND_COLOR,
},
container: {
flex: 1,
flexDirection: ‘column’,
justifyContent: ‘space-between’,
alignItems: ‘center’,
alignSelf: ‘stretch’,
backgroundColor: BACKGROUND_COLOR,
minHeight: DEVICE_HEIGHT,
maxHeight: DEVICE_HEIGHT,
},
noPermissionsText: {
textAlign: ‘center’,
},
wrapper: {},
halfScreenContainer: {
flex: 1,
flexDirection: ‘column’,
justifyContent: ‘space-between’,
alignItems: ‘center’,
alignSelf: ‘stretch’,
minHeight: DEVICE_HEIGHT / 2.0,
maxHeight: DEVICE_HEIGHT / 2.0,
},
recordingContainer: {
flex: 1,
flexDirection: ‘row’,
justifyContent: ‘space-between’,
alignItems: ‘center’,
alignSelf: ‘stretch’,
minHeight: ICON_RECORD_BUTTON.height,
maxHeight: ICON_RECORD_BUTTON.height,
},
recordingDataContainer: {
flex: 1,
flexDirection: ‘column’,
justifyContent: ‘space-between’,
alignItems: ‘center’,
minHeight: ICON_RECORD_BUTTON.height,
maxHeight: ICON_RECORD_BUTTON.height,
minWidth: ICON_RECORD_BUTTON.width * 3.0,
maxWidth: ICON_RECORD_BUTTON.width * 3.0,
},
recordingDataRowContainer: {
flex: 1,
flexDirection: ‘row’,
justifyContent: ‘space-between’,
alignItems: ‘center’,
minHeight: ICON_RECORDING.height,
maxHeight: ICON_RECORDING.height,
},
playbackContainer: {
flex: 1,
flexDirection: ‘column’,
justifyContent: ‘space-between’,
alignItems: ‘center’,
alignSelf: ‘stretch’,
minHeight: ICON_THUMB_1.height * 2.0,
maxHeight: ICON_THUMB_1.height * 2.0,
},
playbackSlider: {
alignSelf: ‘stretch’,
},
liveText: {
color: LIVE_COLOR,
},
recordingTimestamp: {
paddingLeft: 20,
},
playbackTimestamp: {
textAlign: ‘right’,
alignSelf: ‘stretch’,
paddingRight: 20,
},
image: {
backgroundColor: BACKGROUND_COLOR,
},
textButton: {
backgroundColor: BACKGROUND_COLOR,
padding: 10,
},
buttonsContainerBase: {
flex: 1,
flexDirection: ‘row’,
alignItems: ‘center’,
justifyContent: ‘space-between’,
},
buttonsContainerTopRow: {
maxHeight: ICON_MUTED_BUTTON.height,
alignSelf: ‘stretch’,
paddingRight: 20,
},
playStopContainer: {
flex: 1,
flexDirection: ‘row’,
alignItems: ‘center’,
justifyContent: ‘space-between’,
minWidth: (ICON_PLAY_BUTTON.width + ICON_STOP_BUTTON.width) * 3.0 / 2.0,
maxWidth: (ICON_PLAY_BUTTON.width + ICON_STOP_BUTTON.width) * 3.0 / 2.0,
},
volumeContainer: {
flex: 1,
flexDirection: ‘row’,
alignItems: ‘center’,
justifyContent: ‘space-between’,
minWidth: DEVICE_WIDTH / 2.0,
maxWidth: DEVICE_WIDTH / 2.0,
},
volumeSlider: {
width: DEVICE_WIDTH / 2.0 - ICON_MUTED_BUTTON.width,
},
buttonsContainerBottomRow: {
maxHeight: ICON_THUMB_1.height,
alignSelf: ‘stretch’,
paddingRight: 20,
paddingLeft: 20,
},
rateSlider: {
width: DEVICE_WIDTH / 2.0,
},
});