1 |
|
2 |
|
3 | import {
|
4 | NativeModules,
|
5 | } from 'react-native';
|
6 | import Asset from './Asset';
|
7 |
|
8 | type SoundStatus = {
|
9 | isPlaying: boolean,
|
10 | positionMillis: number,
|
11 | rate: number,
|
12 | shouldCorrectPitch: boolean,
|
13 | volume: number,
|
14 | isMuted: boolean,
|
15 | isLooping: boolean,
|
16 | }
|
17 |
|
18 | var _enabled: boolean = false;
|
19 | const _DISABLED_ERROR: Error = new Error('Cannot complete operation because audio is not enabled.');
|
20 | const _NOT_LOADED_ERROR: Error = new Error('Cannot complete operation because sound is not loaded.');
|
21 | const _DEFAULT_POLLING_TIMEOUT_MILLIS: number = 500;
|
22 |
|
23 | export async function setIsEnabledAsync(value: boolean): Promise<void> {
|
24 | _enabled = value;
|
25 | await NativeModules.ExponentAudio.setIsEnabled(value);
|
26 | }
|
27 |
|
28 | export class Sound {
|
29 | uri: string;
|
30 | loaded: boolean;
|
31 | key: number;
|
32 | durationMillis: number;
|
33 | statusChangeCallback: ?(status: SoundStatus) => void;
|
34 | userPlaybackFinishedCallback: ?() => void;
|
35 | statusPollingTimeoutVariable: ?number;
|
36 | statusPollingTimeoutMillis: number;
|
37 |
|
38 | constructor({ source } : { source: number | string | Asset }) {
|
39 | if (typeof source === 'number') {
|
40 | let asset = Asset.fromModule(source);
|
41 | this.uri = asset.localUri || asset.uri;
|
42 | } else if (typeof source === 'string') {
|
43 | this.uri = source;
|
44 | } else {
|
45 | this.uri = source.localUri || source.uri;
|
46 | }
|
47 |
|
48 | this.loaded = false;
|
49 | this.key = -1;
|
50 | this.durationMillis = 0;
|
51 | this.statusChangeCallback = null;
|
52 | this.userPlaybackFinishedCallback = null;
|
53 | this.statusPollingTimeoutVariable = null;
|
54 | this.statusPollingTimeoutMillis = _DEFAULT_POLLING_TIMEOUT_MILLIS;
|
55 | }
|
56 |
|
57 | _statusPollingLoop = () => {
|
58 | if (!_enabled) {
|
59 | return;
|
60 | }
|
61 | if (this.statusChangeCallback != null) {
|
62 | this.getStatusAsync();
|
63 | }
|
64 | if (this.loaded) {
|
65 | this.statusPollingTimeoutVariable = setTimeout(this._statusPollingLoop, this.statusPollingTimeoutMillis);
|
66 | }
|
67 | }
|
68 |
|
69 | _disableStatusPolling() {
|
70 | if (this.statusPollingTimeoutVariable != null) {
|
71 | clearTimeout(this.statusPollingTimeoutVariable);
|
72 | this.statusPollingTimeoutVariable = null;
|
73 | }
|
74 | }
|
75 |
|
76 | _enableStatusPolling() {
|
77 | if (_enabled) {
|
78 | this._disableStatusPolling();
|
79 | this._statusPollingLoop();
|
80 | }
|
81 | }
|
82 |
|
83 | _tryCallStatusChangeCallbackForStatus(status: SoundStatus) {
|
84 | if (this.statusChangeCallback != null) {
|
85 | this.statusChangeCallback(status);
|
86 | }
|
87 | }
|
88 |
|
89 | _internalPlaybackFinishedCallback = (status: SoundStatus) => {
|
90 | this._tryCallStatusChangeCallbackForStatus(status);
|
91 | if (this.userPlaybackFinishedCallback != null) {
|
92 | this.userPlaybackFinishedCallback();
|
93 | }
|
94 | this._setInternalPlaybackFinishedCallback();
|
95 | }
|
96 |
|
97 | _setInternalPlaybackFinishedCallback() {
|
98 | if (this.loaded) {
|
99 | NativeModules.ExponentAudio.setPlaybackFinishedCallback(this.key, this._internalPlaybackFinishedCallback);
|
100 | }
|
101 | }
|
102 |
|
103 | async _performOperationAndUpdateStatusAsync(operation: () => Promise<{ status: SoundStatus }>): Promise<SoundStatus> {
|
104 | if (!_enabled) {
|
105 | throw _DISABLED_ERROR;
|
106 | }
|
107 | if (this.loaded) {
|
108 | const { status } : { status: SoundStatus } = await operation();
|
109 | this._tryCallStatusChangeCallbackForStatus(status);
|
110 | return status;
|
111 | } else {
|
112 | throw _NOT_LOADED_ERROR;
|
113 | }
|
114 | }
|
115 |
|
116 | async loadAsync(): Promise<void> {
|
117 | if (!_enabled) {
|
118 | throw _DISABLED_ERROR;
|
119 | }
|
120 | if (!this.loaded) {
|
121 | const result: {
|
122 | key: number,
|
123 | durationMillis: number,
|
124 | status: SoundStatus
|
125 | } = await NativeModules.ExponentAudio.load(this.uri);
|
126 | this.key = result.key;
|
127 | this.durationMillis = result.durationMillis;
|
128 | this.loaded = true;
|
129 | this._setInternalPlaybackFinishedCallback();
|
130 | }
|
131 | }
|
132 |
|
133 | isLoaded(): boolean {
|
134 | return this.loaded;
|
135 | }
|
136 |
|
137 | getDurationMillis(): ?number {
|
138 | return this.loaded ? this.durationMillis : null;
|
139 | }
|
140 |
|
141 | setStatusChangeCallback(callback: ?(status: SoundStatus) => void) {
|
142 | this.statusChangeCallback = callback;
|
143 | if (callback == null) {
|
144 | this._disableStatusPolling();
|
145 | } else {
|
146 | this._enableStatusPolling();
|
147 | }
|
148 | }
|
149 |
|
150 | setPlaybackFinishedCallback(callback: ?() => void) {
|
151 | this.userPlaybackFinishedCallback = callback;
|
152 | }
|
153 |
|
154 | setStatusPollingTimeoutMillis(value: number) {
|
155 | this.statusPollingTimeoutMillis = value;
|
156 | }
|
157 |
|
158 | async unloadAsync(): Promise<void> {
|
159 | if (this.loaded) {
|
160 | this.loaded = false;
|
161 | this._disableStatusPolling();
|
162 | this.userPlaybackFinishedCallback = null;
|
163 | this.statusChangeCallback = null;
|
164 | return await NativeModules.ExponentAudio.unload(this.key);
|
165 | }
|
166 | }
|
167 |
|
168 | async playAsync(): Promise<SoundStatus> {
|
169 | return this._performOperationAndUpdateStatusAsync(
|
170 | () => NativeModules.ExponentAudio.play(this.key));
|
171 | }
|
172 |
|
173 | async pauseAsync(): Promise<SoundStatus> {
|
174 | return this._performOperationAndUpdateStatusAsync(
|
175 | () => NativeModules.ExponentAudio.pause(this.key));
|
176 | }
|
177 |
|
178 | async stopAsync(): Promise<SoundStatus> {
|
179 | return this._performOperationAndUpdateStatusAsync(
|
180 | () => NativeModules.ExponentAudio.stop(this.key));
|
181 | }
|
182 |
|
183 | async setPositionAsync(millis: number): Promise<SoundStatus> {
|
184 | return this._performOperationAndUpdateStatusAsync(
|
185 | () => NativeModules.ExponentAudio.setPosition(this.key, millis));
|
186 | }
|
187 |
|
188 | async setRateAsync(value: number, shouldCorrectPitch: boolean): Promise<SoundStatus> {
|
189 | if (value < 0.0 || value > 32.0) {
|
190 | throw new Error('Rate value must be between 0.0 and 32.0.');
|
191 | }
|
192 | return this._performOperationAndUpdateStatusAsync(
|
193 | () => NativeModules.ExponentAudio.setRate(this.key, value, shouldCorrectPitch));
|
194 | }
|
195 |
|
196 | async setVolumeAsync(value: number): Promise<SoundStatus> {
|
197 | if (value < 0.0 || value > 1.0) {
|
198 | throw new Error('Volume value must be between 0.0 and 1.0.');
|
199 | }
|
200 | return this._performOperationAndUpdateStatusAsync(
|
201 | () => NativeModules.ExponentAudio.setVolume(this.key, value));
|
202 | }
|
203 |
|
204 | async setIsMutedAsync(value: boolean): Promise<SoundStatus> {
|
205 | return this._performOperationAndUpdateStatusAsync(
|
206 | () => NativeModules.ExponentAudio.setIsMuted(this.key, value));
|
207 | }
|
208 |
|
209 | async setIsLoopingAsync(value: boolean): Promise<SoundStatus> {
|
210 | return this._performOperationAndUpdateStatusAsync(
|
211 | () => NativeModules.ExponentAudio.setIsLooping(this.key, value));
|
212 | }
|
213 |
|
214 | async getStatusAsync(): Promise<SoundStatus> {
|
215 | return this._performOperationAndUpdateStatusAsync(
|
216 | () => NativeModules.ExponentAudio.getStatus(this.key));
|
217 | }
|
218 | }
|