Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIX] Recorded audio on Android doesn't play on iOS #2073

Merged
merged 7 commits into from
Apr 30, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions __mocks__/expo-av.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export class Sound {
loadAsync = () => {};

playAsync = () => {};

pauseAsync = () => {};

stopAsync = () => {};

setOnPlaybackStatusUpdate = () => {};

setPositionAsync = () => {};
}
export const Audio = { Sound };
4 changes: 0 additions & 4 deletions __tests__/__snapshots__/Storyshots.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -16315,7 +16315,6 @@ exports[`Storyshots Message list message 1`] = `
]
}
>
View
<View
accessible={true}
focusable={true}
Expand Down Expand Up @@ -16665,7 +16664,6 @@ exports[`Storyshots Message list message 1`] = `
]
}
>
View
<View
accessible={true}
focusable={true}
Expand Down Expand Up @@ -16888,7 +16886,6 @@ exports[`Storyshots Message list message 1`] = `
]
}
>
View
<View
accessible={true}
focusable={true}
Expand Down Expand Up @@ -17074,7 +17071,6 @@ exports[`Storyshots Message list message 1`] = `
]
}
>
View
<View
accessible={true}
focusable={true}
Expand Down
3 changes: 2 additions & 1 deletion app/containers/MessageBox/Recording.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export default class extends React.PureComponent {
SampleRate: 22050,
Channels: 1,
AudioQuality: 'Low',
AudioEncoding: 'aac'
AudioEncoding: 'aac',
OutputFormat: 'aac_adts'
});

AudioRecorder.onProgress = (data) => {
Expand Down
132 changes: 100 additions & 32 deletions app/containers/message/Audio.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import {
View, StyleSheet, Text, Easing, Dimensions
} from 'react-native';
import Video from 'react-native-video';
import { Audio } from 'expo-av';
import Slider from '@react-native-community/slider';
import moment from 'moment';
import equal from 'deep-equal';
Expand All @@ -15,6 +15,17 @@ import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors';
import { isAndroid, isIOS } from '../../utils/deviceInfo';
import { withSplit } from '../../split';
import ActivityIndicator from '../ActivityIndicator';

const mode = {
allowsRecordingIOS: false,
playsInSilentModeIOS: true,
staysActiveInBackground: false,
shouldDuckAndroid: true,
playThroughEarpieceAndroid: false,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX
};

const styles = StyleSheet.create({
audioContainer: {
Expand Down Expand Up @@ -51,25 +62,32 @@ const sliderAnimationConfig = {
delay: 0
};

const Button = React.memo(({ paused, onPress, theme }) => (
const Button = React.memo(({
loading, paused, onPress, theme
}) => (
<Touchable
style={styles.playPauseButton}
onPress={onPress}
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
>
<CustomIcon name={paused ? 'play' : 'pause'} size={36} color={themes[theme].tintColor} />
{
loading
? <ActivityIndicator style={styles.playPauseButton} theme={theme} />
: <CustomIcon name={paused ? 'play' : 'pause'} size={36} color={themes[theme].tintColor} />
}
</Touchable>
));

Button.propTypes = {
loading: PropTypes.bool,
paused: PropTypes.bool,
theme: PropTypes.string,
onPress: PropTypes.func
};
Button.displayName = 'MessageAudioButton';

class Audio extends React.Component {
class MessageAudio extends React.Component {
static propTypes = {
file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
Expand All @@ -83,16 +101,33 @@ class Audio extends React.Component {
super(props);
const { baseUrl, file, user } = props;
this.state = {
loading: false,
currentTime: 0,
duration: 0,
paused: true,
uri: `${ baseUrl }${ file.audio_url }?rc_uid=${ user.id }&rc_token=${ user.token }`
};

this.sound = new Audio.Sound();
this.sound.setOnPlaybackStatusUpdate(this.onPlaybackStatusUpdate);
}

async componentDidMount() {
const { uri } = this.state;

this.setState({ loading: true });
try {
await Audio.setAudioModeAsync(mode);
await this.sound.loadAsync({ uri });
} catch {
// Do nothing
}
this.setState({ loading: false });
}

shouldComponentUpdate(nextProps, nextState) {
const {
currentTime, duration, paused, uri
currentTime, duration, paused, uri, loading
} = this.state;
const { file, split, theme } = this.props;
if (nextProps.theme !== theme) {
Expand All @@ -116,44 +151,87 @@ class Audio extends React.Component {
if (nextProps.split !== split) {
return true;
}
if (nextState.loading !== loading) {
return true;
}
return false;
}

async componentWillUnmount() {
try {
await this.sound.stopAsync();
} catch {
// Do nothing
}
}

onPlaybackStatusUpdate = (status) => {
if (status) {
this.onLoad(status);
this.onProgress(status);
this.onEnd(status);
}
}

onLoad = (data) => {
this.setState({ duration: data.duration > 0 ? data.duration : 0 });
const duration = data.durationMillis / 1000;
this.setState({ duration: duration > 0 ? duration : 0 });
}

onProgress = (data) => {
const { duration } = this.state;
if (data.currentTime <= duration) {
this.setState({ currentTime: data.currentTime });
const currentTime = data.positionMillis / 1000;
if (currentTime <= duration) {
this.setState({ currentTime });
}
}

onEnd = () => {
this.setState({ paused: true, currentTime: 0 });
requestAnimationFrame(() => {
this.player.seek(0);
});
onEnd = async(data) => {
if (data.didJustFinish) {
try {
await this.sound.stopAsync();
this.setState({ paused: true, currentTime: 0 });
} catch {
// do nothing
}
}
}

get duration() {
const { duration } = this.state;
return formatTime(duration);
const { currentTime, duration } = this.state;
return formatTime(currentTime || duration);
}

setRef = ref => this.player = ref;

togglePlayPause = () => {
const { paused } = this.state;
this.setState({ paused: !paused });
this.setState({ paused: !paused }, this.playPause);
}

onValueChange = value => this.setState({ currentTime: value });
playPause = async() => {
const { paused } = this.state;
try {
if (paused) {
await this.sound.pauseAsync();
} else {
await this.sound.playAsync();
}
} catch {
// Do nothing
}
}

onValueChange = async(value) => {
try {
this.setState({ currentTime: value });
await this.sound.setPositionAsync(value * 1000);
} catch {
// Do nothing
}
}

render() {
const {
uri, paused, currentTime, duration
loading, paused, currentTime, duration
} = this.state;
const {
user, baseUrl, file, getCustomEmoji, split, theme
Expand All @@ -173,17 +251,7 @@ class Audio extends React.Component {
split && sharedStyles.tabletContent
]}
>
<Video
ref={this.setRef}
source={{ uri }}
onLoad={this.onLoad}
onProgress={this.onProgress}
onEnd={this.onEnd}
paused={paused}
repeat={false}
ignoreSilentSwitch='ignore'
/>
<Button paused={paused} onPress={this.togglePlayPause} theme={theme} />
<Button loading={loading} paused={paused} onPress={this.togglePlayPause} theme={theme} />
<Slider
style={styles.slider}
value={currentTime}
Expand All @@ -205,4 +273,4 @@ class Audio extends React.Component {
}
}

export default withSplit(Audio);
export default withSplit(MessageAudio);
14 changes: 5 additions & 9 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -309,11 +309,6 @@ PODS:
- React
- react-native-slider (2.0.5):
- React
- react-native-video (5.0.2):
- React
- react-native-video/Video (= 5.0.2)
- react-native-video/Video (5.0.2):
- React
- react-native-webview (7.5.1):
- React
- React-RCTActionSheet (0.61.5):
Expand Down Expand Up @@ -408,6 +403,7 @@ PODS:
- SDWebImageWebPCoder (0.2.5):
- libwebp (~> 1.0)
- SDWebImage/Core (~> 5.0)
- SocketRocket (0.5.1)
- UMBarCodeScannerInterface (3.0.0)
- UMCameraInterface (3.0.0)
- UMConstantsInterface (3.0.0)
Expand Down Expand Up @@ -462,7 +458,6 @@ DEPENDENCIES:
- react-native-notifications (from `../node_modules/react-native-notifications`)
- react-native-orientation-locker (from `../node_modules/react-native-orientation-locker`)
- "react-native-slider (from `../node_modules/@react-native-community/slider`)"
- react-native-video (from `../node_modules/react-native-video`)
- react-native-webview (from `../node_modules/react-native-webview`)
- React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`)
- React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`)
Expand Down Expand Up @@ -492,6 +487,7 @@ DEPENDENCIES:
- RNScreens (from `../node_modules/react-native-screens`)
- RNUserDefaults (from `../node_modules/rn-user-defaults`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- SocketRocket (from `../node_modules/detox/ios_src/SocketRocket`)
- UMBarCodeScannerInterface (from `../node_modules/unimodules-barcode-scanner-interface/ios`)
- UMCameraInterface (from `../node_modules/unimodules-camera-interface/ios`)
- UMConstantsInterface (from `../node_modules/unimodules-constants-interface/ios`)
Expand Down Expand Up @@ -605,8 +601,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-orientation-locker"
react-native-slider:
:path: "../node_modules/@react-native-community/slider"
react-native-video:
:path: "../node_modules/react-native-video"
react-native-webview:
:path: "../node_modules/react-native-webview"
React-RCTActionSheet:
Expand Down Expand Up @@ -663,6 +657,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/rn-user-defaults"
RNVectorIcons:
:path: "../node_modules/react-native-vector-icons"
SocketRocket:
:path: "../node_modules/detox/ios_src/SocketRocket"
UMBarCodeScannerInterface:
:path: !ruby/object:Pathname
path: "../node_modules/unimodules-barcode-scanner-interface/ios"
Expand Down Expand Up @@ -759,7 +755,6 @@ SPEC CHECKSUMS:
react-native-notifications: 163ddedac6fcc8d850ea15b06abdadcacdff00f1
react-native-orientation-locker: 23918c400376a7043e752c639c122fcf6bce8f1c
react-native-slider: 39208600e44f885e2d2c0510b5c6435a0f62d087
react-native-video: d01ed7ff1e38fa7dcc6c15c94cf505e661b7bfd0
react-native-webview: 2aadbfef6b9eaa9e89b306ae3e31e6e870a6306d
React-RCTActionSheet: 600b4d10e3aea0913b5a92256d2719c0cdd26d76
React-RCTAnimation: 791a87558389c80908ed06cc5dfc5e7920dfa360
Expand Down Expand Up @@ -791,6 +786,7 @@ SPEC CHECKSUMS:
RSKImageCropper: a446db0e8444a036b34f3c43db01b2373baa4b2a
SDWebImage: 4d5c027c935438f341ed33dbac53ff9f479922ca
SDWebImageWebPCoder: 947093edd1349d820c40afbd9f42acb6cdecd987
SocketRocket: dbb1554b8fc288ef8ef370d6285aeca7361be31e
UMBarCodeScannerInterface: 84ea2d6b58ff0dc27ef9b68bab71286be18ee020
UMCameraInterface: 26b26005d1756a0d5f4f04f1e168e39ea9154535
UMConstantsInterface: 038bacb19de12b6fd328c589122c8dc977cccf61
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ios/Pods/Headers/Private/SocketRocket/SRConstants.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ios/Pods/Headers/Private/SocketRocket/SRError.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ios/Pods/Headers/Private/SocketRocket/SRHash.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ios/Pods/Headers/Private/SocketRocket/SRIOConsumer.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ios/Pods/Headers/Private/SocketRocket/SRIOConsumerPool.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ios/Pods/Headers/Private/SocketRocket/SRLog.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ios/Pods/Headers/Private/SocketRocket/SRMutex.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions ios/Pods/Headers/Private/SocketRocket/SRProxyConnect.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading