1import { diff } from 'deep-object-diff'; 2import { Asset } from 'expo-asset'; 3import { Audio, AVMetadata, AVPlaybackStatus } from 'expo-av'; 4import React from 'react'; 5import { StyleProp, View, ViewStyle } from 'react-native'; 6 7import { AndroidImplementationSelector } from './AndroidImplementationSelector'; 8import { JsiAudioBar } from './JsiAudioBar'; 9import Player from './Player'; 10 11type PlaybackSource = 12 | number 13 | { 14 uri: string; 15 overrideFileExtensionAndroid?: string; 16 headers?: { 17 [fieldName: string]: string; 18 }; 19 } 20 | Asset; 21 22interface Props { 23 style?: StyleProp<ViewStyle>; 24 source: PlaybackSource; 25} 26 27interface State { 28 androidImplementation: string; 29 isLoaded: boolean; 30 isLooping: boolean; 31 isPlaying: boolean; 32 errorMessage?: string; 33 positionMillis: number; 34 durationMillis: number; 35 rate: number; 36 volume: number; 37 audioPan: number; 38 isMuted: boolean; 39 shouldCorrectPitch: boolean; 40 metadata: AVMetadata; 41} 42 43export default class AudioPlayer extends React.Component<Props, State> { 44 readonly state: State = { 45 androidImplementation: 'SimpleExoPlayer', 46 isMuted: false, 47 isLoaded: false, 48 isLooping: false, 49 isPlaying: false, 50 positionMillis: 0, 51 durationMillis: 0, 52 rate: 1, 53 volume: 1, 54 audioPan: 0, 55 shouldCorrectPitch: false, 56 metadata: {}, 57 }; 58 59 _sound?: Audio.Sound; 60 private prevStatus?: AVPlaybackStatus; 61 62 componentDidMount() { 63 this._loadSoundAsync(this.props.source); 64 } 65 66 componentDidUpdate(prevProps: Props) { 67 if (prevProps.source !== this.props.source) { 68 this._loadSoundAsync(this.props.source); 69 } 70 } 71 72 componentWillUnmount() { 73 if (this._sound) { 74 this._clearJsiAudioSampleCallback(); 75 this._sound.unloadAsync(); 76 } 77 } 78 79 _loadSoundAsync = async (source: PlaybackSource, androidImplementation?: string) => { 80 const soundObject = new Audio.Sound(); 81 try { 82 await soundObject.loadAsync(source, { 83 progressUpdateIntervalMillis: 150, 84 androidImplementation, 85 }); 86 soundObject.setOnPlaybackStatusUpdate(this._updateStateToStatus); 87 soundObject.setOnMetadataUpdate(this._updateMetadata); 88 const status = await soundObject.getStatusAsync(); 89 this._updateStateToStatus(status); 90 this._sound = soundObject; 91 } catch (error) { 92 this.setState({ errorMessage: error.message }); 93 } 94 }; 95 96 _updateStateToStatus = (status: any) => { 97 console.log('onPlaybackStatusUpdate: ', diff(this.prevStatus || {}, status)); 98 this.prevStatus = status; 99 this.setState(status); 100 }; 101 102 _updateMetadata = (metadata: AVMetadata) => { 103 this.setState({ metadata }); 104 }; 105 106 _playAsync = async () => { 107 this._sound!.playAsync(); 108 }; 109 110 _pauseAsync = async () => { 111 this._clearJsiAudioSampleCallback(); 112 this._sound!.pauseAsync(); 113 }; 114 115 _replayAsync = async () => this._sound!.replayAsync(); 116 117 _setPositionAsync = async (position: number) => this._sound!.setPositionAsync(position); 118 119 _setIsLoopingAsync = async (isLooping: boolean) => this._sound!.setIsLoopingAsync(isLooping); 120 121 _setIsMutedAsync = async (isMuted: boolean) => this._sound!.setIsMutedAsync(isMuted); 122 123 _setVolumeAsync = async (volume: number, audioPan?: number) => 124 this._sound!.setVolumeAsync(volume, audioPan); 125 126 _setRateAsync = async ( 127 rate: number, 128 shouldCorrectPitch: boolean, 129 pitchCorrectionQuality = Audio.PitchCorrectionQuality.Low 130 ) => { 131 await this._sound!.setRateAsync(rate, shouldCorrectPitch, pitchCorrectionQuality); 132 }; 133 134 _clearJsiAudioSampleCallback = () => { 135 // it throws UnavailabilityError when platform is not supported 136 // ignore this, we set it here to null anyway 137 try { 138 this._sound?.setOnAudioSampleReceived(null); 139 } catch {} 140 }; 141 142 _isMediaPlayerImplementation = () => this.state.androidImplementation === 'MediaPlayer'; 143 144 _toggleAndroidImplementation = async () => { 145 if (this.state.isPlaying) { 146 await this._pauseAsync(); 147 } 148 if (this.state.isLoaded) { 149 await this._sound?.unloadAsync(); 150 } 151 await this._loadSoundAsync( 152 this.props.source, 153 this._isMediaPlayerImplementation() ? 'SimpleExoPlayer' : 'MediaPlayer' 154 ); 155 }; 156 157 render() { 158 return ( 159 <View> 160 <AndroidImplementationSelector 161 onToggle={this._toggleAndroidImplementation} 162 title={`Use ${this._isMediaPlayerImplementation() ? 'SimpleExoPlayer' : 'MediaPlayer'}`} 163 toggled={this._isMediaPlayerImplementation()} 164 /> 165 166 <Player 167 {...this.state} 168 style={this.props.style} 169 playAsync={this._playAsync} 170 pauseAsync={this._pauseAsync} 171 replayAsync={this._replayAsync} 172 setPositionAsync={this._setPositionAsync} 173 setIsLoopingAsync={this._setIsLoopingAsync} 174 setRateAsync={this._setRateAsync} 175 setIsMutedAsync={this._setIsMutedAsync} 176 setVolume={this._setVolumeAsync} 177 extraIndicator={<JsiAudioBar isPlaying={this.state.isPlaying} sound={this._sound!} />} 178 /> 179 </View> 180 ); 181 } 182} 183