@sendbird/uikit-react-native 3.11.0 → 3.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +7 -5
  2. package/lib/commonjs/platform/createMediaService.expo.js +83 -12
  3. package/lib/commonjs/platform/createMediaService.expo.js.map +1 -1
  4. package/lib/commonjs/platform/createPlayerService.expo.js +214 -113
  5. package/lib/commonjs/platform/createPlayerService.expo.js.map +1 -1
  6. package/lib/commonjs/platform/createRecorderService.expo.js +248 -130
  7. package/lib/commonjs/platform/createRecorderService.expo.js.map +1 -1
  8. package/lib/commonjs/utils/expoBackwardUtils.js +23 -0
  9. package/lib/commonjs/utils/expoBackwardUtils.js.map +1 -1
  10. package/lib/commonjs/utils/expoPermissionGranted.js.map +1 -1
  11. package/lib/commonjs/version.js +1 -1
  12. package/lib/commonjs/version.js.map +1 -1
  13. package/lib/module/platform/createMediaService.expo.js +82 -13
  14. package/lib/module/platform/createMediaService.expo.js.map +1 -1
  15. package/lib/module/platform/createPlayerService.expo.js +214 -113
  16. package/lib/module/platform/createPlayerService.expo.js.map +1 -1
  17. package/lib/module/platform/createRecorderService.expo.js +249 -131
  18. package/lib/module/platform/createRecorderService.expo.js.map +1 -1
  19. package/lib/module/utils/expoBackwardUtils.js +23 -0
  20. package/lib/module/utils/expoBackwardUtils.js.map +1 -1
  21. package/lib/module/utils/expoPermissionGranted.js.map +1 -1
  22. package/lib/module/version.js +1 -1
  23. package/lib/module/version.js.map +1 -1
  24. package/lib/typescript/src/containers/SendbirdUIKitContainer.d.ts +1 -1
  25. package/lib/typescript/src/platform/createMediaService.expo.d.ts +2 -2
  26. package/lib/typescript/src/platform/createPlayerService.expo.d.ts +2 -2
  27. package/lib/typescript/src/platform/createRecorderService.expo.d.ts +2 -2
  28. package/lib/typescript/src/utils/expoBackwardUtils.d.ts +10 -0
  29. package/lib/typescript/src/utils/expoPermissionGranted.d.ts +1 -1
  30. package/lib/typescript/src/version.d.ts +1 -1
  31. package/package.json +16 -5
  32. package/src/platform/createMediaService.expo.tsx +87 -9
  33. package/src/platform/createPlayerService.expo.tsx +242 -109
  34. package/src/platform/createRecorderService.expo.tsx +267 -110
  35. package/src/utils/expoBackwardUtils.ts +29 -0
  36. package/src/utils/expoPermissionGranted.ts +3 -1
  37. package/src/version.ts +1 -1
@@ -1,6 +1,9 @@
1
+ import type * as ExpoAudio from 'expo-audio';
2
+ import type * as ExpoAV from 'expo-av';
1
3
  import type * as ExpoDocumentPicker from 'expo-document-picker';
2
4
  import type * as ExpoFs from 'expo-file-system';
3
5
  import type * as ExpoImagePicker from 'expo-image-picker';
6
+ import type * as ExpoVideo from 'expo-video';
4
7
  import type { FilePickerResponse } from '../platform/types';
5
8
  declare const expoBackwardUtils: {
6
9
  imagePicker: {
@@ -11,6 +14,13 @@ declare const expoBackwardUtils: {
11
14
  isCanceled(result: ExpoDocumentPicker.DocumentPickerResult): boolean;
12
15
  toFilePickerResponses(result: ExpoDocumentPicker.DocumentPickerResult): Promise<FilePickerResponse[]>;
13
16
  };
17
+ expoAV: {
18
+ isLegacyAVModule(module: ExpoAudioModule | ExpoVideoModule): module is typeof ExpoAV;
19
+ isAudioModule(module: ExpoAudioModule): module is typeof ExpoAudio;
20
+ isVideoModule(module: ExpoVideoModule): module is typeof ExpoVideo;
21
+ };
14
22
  toFileSize(info: ExpoFs.FileInfo): number;
15
23
  };
24
+ export type ExpoAudioModule = typeof ExpoAV | typeof ExpoAudio;
25
+ export type ExpoVideoModule = typeof ExpoAV | typeof ExpoVideo;
16
26
  export default expoBackwardUtils;
@@ -7,7 +7,7 @@ export interface ExpoPermissionResponse {
7
7
  export interface ExpoMediaLibraryPermissionResponse extends ExpoPermissionResponse {
8
8
  accessPrivileges?: 'all' | 'limited' | 'none';
9
9
  }
10
- export interface ExpoPushPermissionResponse extends ExpoPermissionResponse, NotificationPermissionsStatus {
10
+ export interface ExpoPushPermissionResponse extends Omit<ExpoPermissionResponse, 'status'>, NotificationPermissionsStatus {
11
11
  }
12
12
  declare const expoPermissionGranted: (stats: Array<ExpoMediaLibraryPermissionResponse | ExpoPushPermissionResponse | ExpoPermissionResponse>, limitedCallback?: () => void) => boolean;
13
13
  export default expoPermissionGranted;
@@ -1,2 +1,2 @@
1
- declare const VERSION = "3.11.0";
1
+ declare const VERSION = "3.11.1";
2
2
  export default VERSION;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendbird/uikit-react-native",
3
- "version": "3.11.0",
3
+ "version": "3.11.1",
4
4
  "description": "Sendbird UIKit for React Native: A feature-rich and customizable chat UI kit with messaging, channel management, and user authentication.",
5
5
  "keywords": [
6
6
  "sendbird",
@@ -60,10 +60,10 @@
60
60
  },
61
61
  "dependencies": {
62
62
  "@openspacelabs/react-native-zoomable-view": "^2.1.5",
63
- "@sendbird/uikit-chat-hooks": "3.11.0",
64
- "@sendbird/uikit-react-native-foundation": "3.11.0",
63
+ "@sendbird/uikit-chat-hooks": "3.11.1",
64
+ "@sendbird/uikit-react-native-foundation": "3.11.1",
65
65
  "@sendbird/uikit-tools": "0.0.15",
66
- "@sendbird/uikit-utils": "3.11.0"
66
+ "@sendbird/uikit-utils": "3.11.1"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@bam.tech/react-native-image-resizer": "^3.0.4",
@@ -77,6 +77,8 @@
77
77
  "@types/react": "*",
78
78
  "@types/react-native": "*",
79
79
  "date-fns": ">=2.28.0",
80
+ "expo": "^54.0.12",
81
+ "expo-audio": "^1.0.13",
80
82
  "expo-av": "^13.2.1",
81
83
  "expo-clipboard": "^4.1.2",
82
84
  "expo-document-picker": "^11.5.3",
@@ -85,6 +87,7 @@
85
87
  "expo-image-picker": "^14.1.1",
86
88
  "expo-media-library": "^16.0.0",
87
89
  "expo-notifications": "^0.18.1",
90
+ "expo-video": "^3.0.11",
88
91
  "expo-video-thumbnails": "^7.2.1",
89
92
  "glob": "^7.2.0",
90
93
  "inquirer": "^8.2.0",
@@ -117,6 +120,7 @@
117
120
  "@sendbird/react-native-scrollview-enhancer": "*",
118
121
  "@sendbird/uikit-tools": ">=0.0.10",
119
122
  "date-fns": ">=2.28.0",
123
+ "expo-audio": ">=1.0.0",
120
124
  "expo-av": ">=12.0.4",
121
125
  "expo-clipboard": ">=2.1.1",
122
126
  "expo-document-picker": ">=10.1.3",
@@ -125,6 +129,7 @@
125
129
  "expo-image-picker": ">=12.0.2",
126
130
  "expo-media-library": ">=16.0.0",
127
131
  "expo-notifications": ">=0.14.1",
132
+ "expo-video": ">=3.0.0",
128
133
  "expo-video-thumbnails": ">=6.4.0",
129
134
  "react": ">=17.0.2",
130
135
  "react-native": ">=0.65.0",
@@ -165,6 +170,12 @@
165
170
  "expo-av": {
166
171
  "optional": true
167
172
  },
173
+ "expo-audio": {
174
+ "optional": true
175
+ },
176
+ "expo-video": {
177
+ "optional": true
178
+ },
168
179
  "expo-clipboard": {
169
180
  "optional": true
170
181
  },
@@ -231,5 +242,5 @@
231
242
  ]
232
243
  ]
233
244
  },
234
- "gitHead": "f0ffc478a073fe16f571a3f298fe101cf8359d4a"
245
+ "gitHead": "16f1f54e8c442492e79a9591ae32203d5db55c2b"
235
246
  }
@@ -1,34 +1,112 @@
1
1
  import type * as ExpoAV from 'expo-av';
2
2
  import type * as ExpoFS from 'expo-file-system';
3
3
  import type * as ExpoImageManipulator from 'expo-image-manipulator';
4
+ import type { EventSubscription } from 'expo-modules-core';
5
+ import type * as ExpoVideo from 'expo-video';
6
+ import type { StatusChangeEventPayload } from 'expo-video';
4
7
  import type * as ExpoVideoThumbnail from 'expo-video-thumbnails';
5
- import React from 'react';
8
+ import React, { useEffect } from 'react';
6
9
 
7
- import { getDownscaleSize } from '@sendbird/uikit-utils';
10
+ import { Logger, getDownscaleSize } from '@sendbird/uikit-utils';
8
11
 
9
12
  import SBUUtils from '../libs/SBUUtils';
10
13
  import expoBackwardUtils from '../utils/expoBackwardUtils';
11
- import type { MediaServiceInterface } from './types';
14
+ import type { ExpoVideoModule } from '../utils/expoBackwardUtils';
15
+ import type { MediaServiceInterface, VideoProps } from './types';
12
16
 
13
17
  type Modules = {
14
- avModule: typeof ExpoAV;
18
+ avModule: ExpoVideoModule;
15
19
  thumbnailModule: typeof ExpoVideoThumbnail;
16
20
  imageManipulator: typeof ExpoImageManipulator;
17
21
  fsModule: typeof ExpoFS;
18
22
  };
19
23
 
24
+ interface VideoModuleAdapter {
25
+ VideoComponent: React.ComponentType<VideoProps>;
26
+ }
27
+
28
+ class LegacyExpoAVVideoAdapter implements VideoModuleAdapter {
29
+ private readonly avModule: typeof ExpoAV;
30
+ constructor(avModule: typeof ExpoAV) {
31
+ this.avModule = avModule;
32
+ }
33
+
34
+ VideoComponent = ({ source, resizeMode, onLoad, ...props }: VideoProps) => {
35
+ // FIXME: type error https://github.com/expo/expo/issues/17101
36
+ // @ts-ignore
37
+ return <this.avModule.Video {...props} source={source} resizeMode={resizeMode} onLoad={onLoad} useNativeControls />;
38
+ };
39
+ }
40
+
41
+ class ExpoVideoAdapter implements VideoModuleAdapter {
42
+ constructor(private readonly _videoModule: typeof ExpoVideo) {}
43
+
44
+ VideoComponent = ({ source, resizeMode, onLoad, ...props }: VideoProps) => {
45
+ const player = this._videoModule.useVideoPlayer(source);
46
+
47
+ useEffect(() => {
48
+ if (onLoad && player) {
49
+ let subscription: EventSubscription | null = null;
50
+ try {
51
+ subscription = player.addListener('statusChange', (eventData: StatusChangeEventPayload) => {
52
+ const { status, error } = eventData;
53
+ if (status === 'readyToPlay' && !error) {
54
+ onLoad();
55
+ }
56
+ });
57
+ } catch (error) {
58
+ const timeout = setTimeout(() => onLoad(), 300);
59
+ return () => clearTimeout(timeout);
60
+ }
61
+
62
+ return () => {
63
+ if (subscription) {
64
+ subscription.remove();
65
+ }
66
+ };
67
+ }
68
+ return undefined;
69
+ }, [onLoad, player]);
70
+
71
+ const getContentFit = (mode: typeof resizeMode): 'cover' | 'contain' | 'fill' => {
72
+ switch (mode) {
73
+ case 'cover':
74
+ return 'cover';
75
+ case 'contain':
76
+ return 'contain';
77
+ case 'stretch':
78
+ return 'fill';
79
+ default:
80
+ return 'contain';
81
+ }
82
+ };
83
+
84
+ return React.createElement(this._videoModule.VideoView, {
85
+ ...props,
86
+ player,
87
+ contentFit: getContentFit(resizeMode),
88
+ });
89
+ };
90
+ }
91
+
20
92
  const createExpoMediaService = ({
21
93
  avModule,
22
94
  thumbnailModule,
23
95
  imageManipulator,
24
96
  fsModule,
25
97
  }: Modules): MediaServiceInterface => {
98
+ if (expoBackwardUtils.expoAV.isLegacyAVModule(avModule)) {
99
+ Logger.warn(
100
+ '[MediaService.Expo] expo-av is deprecated and will be removed in Expo 54. Please migrate to expo-video.',
101
+ );
102
+ }
103
+
104
+ const videoAdapter = expoBackwardUtils.expoAV.isVideoModule(avModule)
105
+ ? new ExpoVideoAdapter(avModule)
106
+ : new LegacyExpoAVVideoAdapter(avModule);
107
+
26
108
  return {
27
- VideoComponent({ source, resizeMode, onLoad, ...props }) {
28
- // FIXME: type error https://github.com/expo/expo/issues/17101
29
- // @ts-ignore
30
- return <avModule.Video {...props} source={source} resizeMode={resizeMode} onLoad={onLoad} useNativeControls />;
31
- },
109
+ VideoComponent: videoAdapter.VideoComponent,
32
110
  async getVideoThumbnail({ url, quality, timeMills }) {
33
111
  try {
34
112
  const { uri } = await thumbnailModule.getThumbnailAsync(url, { quality, time: timeMills });
@@ -1,148 +1,281 @@
1
+ import type * as ExpoAudio from 'expo-audio';
1
2
  import type * as ExpoAV from 'expo-av';
2
3
 
3
4
  import { Logger, matchesOneOf } from '@sendbird/uikit-utils';
4
5
 
5
- import expoPermissionGranted from '../utils/expoPermissionGranted';
6
+ import expoBackwardUtils from '../utils/expoBackwardUtils';
7
+ import type { ExpoAudioModule } from '../utils/expoBackwardUtils';
6
8
  import type { PlayerServiceInterface, Unsubscribe } from './types';
7
9
 
8
10
  type Modules = {
9
- avModule: typeof ExpoAV;
11
+ avModule: ExpoAudioModule;
10
12
  };
11
13
  type PlaybackListener = Parameters<PlayerServiceInterface['addPlaybackListener']>[number];
12
14
  type StateListener = Parameters<PlayerServiceInterface['addStateListener']>[number];
13
- const createExpoPlayerService = ({ avModule }: Modules): PlayerServiceInterface => {
14
- const sound = new avModule.Audio.Sound();
15
15
 
16
- class VoicePlayer implements PlayerServiceInterface {
17
- uri?: string;
18
- state: PlayerServiceInterface['state'] = 'idle';
16
+ interface AudioPlayerAdapter {
17
+ requestPermission(): Promise<boolean>;
18
+ play(uri: string): Promise<void>;
19
+ pause(): Promise<void>;
20
+ stop(): Promise<void>;
21
+ reset(): Promise<void>;
22
+ seek(time: number): Promise<void>;
23
+ addPlaybackListener(callback: PlaybackListener): Unsubscribe;
24
+ addStateListener(callback: StateListener): Unsubscribe;
25
+ readonly state: PlayerServiceInterface['state'];
26
+ uri?: string;
27
+ }
28
+
29
+ abstract class BaseAudioPlayerAdapter implements AudioPlayerAdapter {
30
+ uri?: string;
31
+ state: PlayerServiceInterface['state'] = 'idle';
32
+
33
+ protected readonly playbackSubscribers = new Set<PlaybackListener>();
34
+ protected readonly stateSubscribers = new Set<StateListener>();
19
35
 
20
- private readonly playbackSubscribers = new Set<PlaybackListener>();
21
- private readonly stateSubscribers = new Set<StateListener>();
36
+ protected setState = (state: PlayerServiceInterface['state']) => {
37
+ this.state = state;
38
+ this.stateSubscribers.forEach((callback) => {
39
+ callback(state);
40
+ });
41
+ };
22
42
 
23
- private setState = (state: PlayerServiceInterface['state']) => {
24
- this.state = state;
25
- this.stateSubscribers.forEach((callback) => {
26
- callback(state);
27
- });
43
+ public requestPermission = async (): Promise<boolean> => {
44
+ return true;
45
+ };
46
+
47
+ public addPlaybackListener = (callback: PlaybackListener): Unsubscribe => {
48
+ this.playbackSubscribers.add(callback);
49
+ return () => {
50
+ this.playbackSubscribers.delete(callback);
28
51
  };
52
+ };
29
53
 
30
- private setListener = () => {
31
- sound.setProgressUpdateIntervalAsync(100).catch((error) => {
32
- Logger.warn('[PlayerService.Expo] Failed to set progress update interval', error);
33
- });
34
- sound.setOnPlaybackStatusUpdate((status) => {
35
- if (status.isLoaded) {
36
- if (status.didJustFinish) {
37
- this.stop().catch((error) => {
38
- Logger.warn('[PlayerService.Expo] Failed to stop in OnPlaybackStatusUpdate', error);
39
- });
40
- }
41
- if (status.isPlaying) {
42
- this.playbackSubscribers.forEach((callback) => {
43
- callback({
44
- currentTime: status.positionMillis,
45
- duration: status.durationMillis ?? 0,
46
- stopped: status.didJustFinish,
47
- });
54
+ public addStateListener = (callback: StateListener): Unsubscribe => {
55
+ this.stateSubscribers.add(callback);
56
+ return () => {
57
+ this.stateSubscribers.delete(callback);
58
+ };
59
+ };
60
+
61
+ abstract play(uri: string): Promise<void>;
62
+ abstract pause(): Promise<void>;
63
+ abstract stop(): Promise<void>;
64
+ abstract reset(): Promise<void>;
65
+ abstract seek(time: number): Promise<void>;
66
+ }
67
+
68
+ class LegacyExpoAVPlayerAdapter extends BaseAudioPlayerAdapter {
69
+ private readonly sound: ExpoAV.Audio.Sound;
70
+
71
+ constructor(avModule: typeof ExpoAV) {
72
+ super();
73
+ this.sound = new avModule.Audio.Sound();
74
+ }
75
+
76
+ private setListener = () => {
77
+ this.sound.setProgressUpdateIntervalAsync(100).catch((error) => {
78
+ Logger.warn('[PlayerService.Expo] Failed to set progress update interval', error);
79
+ });
80
+ this.sound.setOnPlaybackStatusUpdate((status) => {
81
+ if (status.isLoaded) {
82
+ if (status.didJustFinish) {
83
+ this.stop().catch((error) => {
84
+ Logger.warn('[PlayerService.Expo] Failed to stop in OnPlaybackStatusUpdate', error);
85
+ });
86
+ }
87
+ if (status.isPlaying) {
88
+ this.playbackSubscribers.forEach((callback) => {
89
+ callback({
90
+ currentTime: status.positionMillis,
91
+ duration: status.durationMillis ?? 0,
92
+ stopped: status.didJustFinish,
48
93
  });
49
- }
94
+ });
50
95
  }
51
- });
52
- };
96
+ }
97
+ });
98
+ };
53
99
 
54
- private removeListener = () => {
55
- sound.setOnPlaybackStatusUpdate(null);
56
- };
100
+ private removeListener = () => {
101
+ this.sound.setOnPlaybackStatusUpdate(null);
102
+ };
103
+
104
+ private prepare = async (uri: string) => {
105
+ this.setState('preparing');
106
+ await this.sound.loadAsync({ uri }, { shouldPlay: false }, true);
107
+ this.uri = uri;
108
+ };
57
109
 
58
- public requestPermission = async (): Promise<boolean> => {
59
- const status = await avModule.Audio.getPermissionsAsync();
60
- if (expoPermissionGranted([status])) {
61
- return true;
62
- } else {
63
- const status = await avModule.Audio.requestPermissionsAsync();
64
- return expoPermissionGranted([status]);
110
+ public play = async (uri: string): Promise<void> => {
111
+ if (matchesOneOf(this.state, ['idle', 'stopped'])) {
112
+ try {
113
+ await this.prepare(uri);
114
+ this.setListener();
115
+ await this.sound.playAsync();
116
+ this.setState('playing');
117
+ } catch (e) {
118
+ this.setState('idle');
119
+ this.uri = undefined;
120
+ this.removeListener();
121
+ throw e;
65
122
  }
66
- };
123
+ } else if (matchesOneOf(this.state, ['paused']) && this.uri === uri) {
124
+ try {
125
+ this.setListener();
126
+ await this.sound.playAsync();
127
+ this.setState('playing');
128
+ } catch (e) {
129
+ this.removeListener();
130
+ throw e;
131
+ }
132
+ }
133
+ };
67
134
 
68
- public addPlaybackListener = (callback: PlaybackListener): Unsubscribe => {
69
- this.playbackSubscribers.add(callback);
70
- return () => {
71
- this.playbackSubscribers.delete(callback);
72
- };
73
- };
135
+ public pause = async (): Promise<void> => {
136
+ if (matchesOneOf(this.state, ['playing'])) {
137
+ await this.sound.pauseAsync();
138
+ this.removeListener();
139
+ this.setState('paused');
140
+ }
141
+ };
74
142
 
75
- public addStateListener = (callback: (state: PlayerServiceInterface['state']) => void): Unsubscribe => {
76
- this.stateSubscribers.add(callback);
77
- return () => {
78
- this.stateSubscribers.delete(callback);
79
- };
80
- };
143
+ public stop = async (): Promise<void> => {
144
+ if (matchesOneOf(this.state, ['playing', 'paused'])) {
145
+ await this.sound.stopAsync();
146
+ await this.sound.unloadAsync();
147
+ this.removeListener();
148
+ this.setState('stopped');
149
+ }
150
+ };
81
151
 
82
- private prepare = async (uri: string) => {
83
- this.setState('preparing');
84
- await sound.loadAsync({ uri }, { shouldPlay: false }, true);
85
- this.uri = uri;
86
- };
152
+ public reset = async (): Promise<void> => {
153
+ await this.stop();
154
+ this.setState('idle');
155
+ this.uri = undefined;
156
+ this.playbackSubscribers.clear();
157
+ this.stateSubscribers.clear();
158
+ };
159
+
160
+ public seek = async (time: number): Promise<void> => {
161
+ if (matchesOneOf(this.state, ['playing', 'paused'])) {
162
+ await this.sound.playFromPositionAsync(time);
163
+ }
164
+ };
165
+ }
166
+
167
+ class ExpoAudioPlayerAdapter extends BaseAudioPlayerAdapter {
168
+ private readonly audioModule: typeof ExpoAudio;
169
+ private player: ExpoAudio.AudioPlayer | null = null;
170
+
171
+ constructor(audioModule: typeof ExpoAudio) {
172
+ super();
173
+ this.audioModule = audioModule;
174
+ }
87
175
 
88
- public play = async (uri: string): Promise<void> => {
89
- if (matchesOneOf(this.state, ['idle', 'stopped'])) {
90
- try {
91
- await this.prepare(uri);
92
- this.setListener();
93
- await sound.playAsync();
94
- this.setState('playing');
95
- } catch (e) {
96
- this.setState('idle');
97
- this.uri = undefined;
98
- this.removeListener();
99
- throw e;
176
+ private setListener = () => {
177
+ if (!this.player) return;
178
+
179
+ this.player.addListener('playbackStatusUpdate', (status) => {
180
+ if (status.isLoaded) {
181
+ if (status.didJustFinish) {
182
+ this.stop().catch((error) => {
183
+ Logger.warn('[PlayerService.Expo] Failed to stop in playbackStatusUpdate', error);
184
+ });
100
185
  }
101
- } else if (matchesOneOf(this.state, ['paused']) && this.uri === uri) {
102
- try {
103
- this.setListener();
104
- await sound.playAsync();
105
- this.setState('playing');
106
- } catch (e) {
107
- this.removeListener();
108
- throw e;
186
+ if (status.playing) {
187
+ this.playbackSubscribers.forEach((callback) => {
188
+ callback({
189
+ currentTime: status.currentTime,
190
+ duration: status.duration ?? 0,
191
+ stopped: status.didJustFinish,
192
+ });
193
+ });
109
194
  }
110
195
  }
111
- };
196
+ });
197
+ };
198
+
199
+ private removeListener = () => {
200
+ if (this.player) {
201
+ this.player.remove();
202
+ }
203
+ };
204
+
205
+ private prepare = async (uri: string) => {
206
+ this.setState('preparing');
207
+ this.player = this.audioModule.createAudioPlayer(uri, { updateInterval: 100 });
208
+ this.uri = uri;
209
+ };
112
210
 
113
- public pause = async (): Promise<void> => {
114
- if (matchesOneOf(this.state, ['playing'])) {
115
- await sound.pauseAsync();
211
+ public play = async (uri: string): Promise<void> => {
212
+ if (matchesOneOf(this.state, ['idle', 'stopped'])) {
213
+ try {
214
+ await this.prepare(uri);
215
+ this.setListener();
216
+ this.player?.play();
217
+ this.setState('playing');
218
+ } catch (e) {
219
+ this.setState('idle');
220
+ this.uri = undefined;
116
221
  this.removeListener();
117
- this.setState('paused');
222
+ throw e;
118
223
  }
119
- };
120
-
121
- public stop = async (): Promise<void> => {
122
- if (matchesOneOf(this.state, ['playing', 'paused'])) {
123
- await sound.stopAsync();
124
- await sound.unloadAsync();
224
+ } else if (matchesOneOf(this.state, ['paused']) && this.uri === uri) {
225
+ try {
226
+ this.setListener();
227
+ this.player?.play();
228
+ this.setState('playing');
229
+ } catch (e) {
125
230
  this.removeListener();
126
- this.setState('stopped');
231
+ throw e;
127
232
  }
128
- };
233
+ }
234
+ };
129
235
 
130
- public reset = async (): Promise<void> => {
131
- await this.stop();
132
- this.setState('idle');
133
- this.uri = undefined;
134
- this.playbackSubscribers.clear();
135
- this.stateSubscribers.clear();
136
- };
236
+ public pause = async (): Promise<void> => {
237
+ if (matchesOneOf(this.state, ['playing'])) {
238
+ this.player?.pause();
239
+ this.removeListener();
240
+ this.setState('paused');
241
+ }
242
+ };
137
243
 
138
- public seek = async (time: number): Promise<void> => {
139
- if (matchesOneOf(this.state, ['playing', 'paused'])) {
140
- await sound.playFromPositionAsync(time);
141
- }
142
- };
244
+ public stop = async (): Promise<void> => {
245
+ if (matchesOneOf(this.state, ['playing', 'paused'])) {
246
+ this.player?.pause();
247
+ this.removeListener();
248
+ this.setState('stopped');
249
+ }
250
+ };
251
+
252
+ public reset = async (): Promise<void> => {
253
+ await this.stop();
254
+ this.player?.remove();
255
+ this.player = null;
256
+ this.setState('idle');
257
+ this.uri = undefined;
258
+ this.playbackSubscribers.clear();
259
+ this.stateSubscribers.clear();
260
+ };
261
+
262
+ public seek = async (time: number): Promise<void> => {
263
+ if (matchesOneOf(this.state, ['playing', 'paused']) && this.player) {
264
+ this.player.currentTime = time;
265
+ }
266
+ };
267
+ }
268
+
269
+ const createExpoPlayerService = ({ avModule }: Modules): PlayerServiceInterface => {
270
+ if (expoBackwardUtils.expoAV.isLegacyAVModule(avModule)) {
271
+ Logger.warn(
272
+ '[PlayerService.Expo] expo-av is deprecated and will be removed in Expo 54. Please migrate to expo-audio.',
273
+ );
143
274
  }
144
275
 
145
- return new VoicePlayer();
276
+ return expoBackwardUtils.expoAV.isAudioModule(avModule)
277
+ ? new ExpoAudioPlayerAdapter(avModule)
278
+ : new LegacyExpoAVPlayerAdapter(avModule);
146
279
  };
147
280
 
148
281
  export default createExpoPlayerService;