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