Skip to content

Commit c5620f7

Browse files
committed
feat: add support for playing audio with expo-video
1 parent f0e806f commit c5620f7

File tree

9 files changed

+209
-70
lines changed

9 files changed

+209
-70
lines changed

examples/ExpoMessaging/app.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,11 @@
6262
"supportsBackgroundPlayback": true,
6363
"supportsPictureInPicture": true
6464
}
65-
]
65+
],
66+
"expo-audio",
67+
{
68+
"microphonePermission": "$(PRODUCT_NAME) would like to use your microphone for voice recording."
69+
}
6670
]
6771
}
6872
}

examples/ExpoMessaging/components/ChatWrapper.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const streami18n = new Streami18n({
1616
});
1717

1818
SqliteClient.logger = (level, message, extraData) => {
19-
console.log(level, `SqliteClient: ${message}`, extraData);
19+
// console.log(level, `SqliteClient: ${message}`, extraData);
2020
};
2121

2222
export const ChatWrapper = ({ children }: PropsWithChildren<{}>) => {

examples/ExpoMessaging/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"@react-native-community/netinfo": "11.4.1",
1515
"@react-navigation/elements": "^1.3.31",
1616
"expo": "^53.0.12",
17+
"expo-audio": "~0.4.6",
1718
"expo-av": "~15.1.6",
1819
"expo-clipboard": "~7.1.4",
1920
"expo-constants": "~17.1.6",

examples/ExpoMessaging/yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3646,6 +3646,11 @@ expo-asset@~11.1.5:
36463646
"@expo/image-utils" "^0.7.4"
36473647
expo-constants "~17.1.5"
36483648

3649+
expo-audio@~0.4.6:
3650+
version "0.4.6"
3651+
resolved "https://registry.yarnpkg.com/expo-audio/-/expo-audio-0.4.6.tgz#39def71b91f2cb5c4d21a51aef6ffabcc4421ad4"
3652+
integrity sha512-/pgz0AnQHnyJWkPfTp/3gBDib6FZYnscpktFZgmPeTeCK8KkPV4+eV2oEDDbSe2ngUrqDA0WVO4MX2Mfeec9ZA==
3653+
36493654
expo-av@~15.1.6:
36503655
version "15.1.6"
36513656
resolved "https://registry.yarnpkg.com/expo-av/-/expo-av-15.1.6.tgz#f1c4a404672500feb0274144a64bb3a956e85bdd"

package/expo-package/package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"types": "types/index.d.ts",
1212
"dependencies": {
1313
"mime": "^4.0.7",
14-
"stream-chat-react-native-core": "7.1.1"
14+
"stream-chat-react-native-core": "link:../"
1515
},
1616
"peerDependencies": {
1717
"expo": ">=51.0.0",
@@ -24,12 +24,16 @@
2424
"expo-image-picker": "*",
2525
"expo-media-library": "*",
2626
"expo-sharing": "*",
27-
"expo-video": "*"
27+
"expo-video": "*",
28+
"expo-audio": "*"
2829
},
2930
"peerDependenciesMeta": {
3031
"expo-av": {
3132
"optional": true
3233
},
34+
"expo-audio": {
35+
"optional": true
36+
},
3337
"expo-video": {
3438
"optional": true
3539
},
@@ -57,7 +61,8 @@
5761
},
5862
"devDependencies": {
5963
"expo": "^53.0.12",
60-
"expo-image-manipulator": "^12.0.5"
64+
"expo-image-manipulator": "^12.0.5",
65+
"expo-audio": "~0.4.6"
6166
},
6267
"scripts": {
6368
"prepack": " cp ../../README.md .",
Lines changed: 145 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,154 @@
1+
import type { PlaybackStatus, SoundReturnType } from 'stream-chat-react-native-core';
2+
13
import { AudioComponent } from './AudioVideo';
24

5+
let ExpoAudioComponent;
6+
let expoCreateSoundPlayer;
7+
8+
try {
9+
const { createAudioPlayer, AudioModule } = require('expo-audio');
10+
ExpoAudioComponent = AudioModule;
11+
expoCreateSoundPlayer = createAudioPlayer;
12+
} catch (e) {
13+
// do nothing
14+
}
15+
316
export const Sound = {
4-
initializeSound: AudioComponent
17+
initializeSound: ExpoAudioComponent
518
? async (source, initialStatus, onPlaybackStatusUpdate: (playbackStatus) => void) => {
6-
await AudioComponent.setAudioModeAsync({
7-
playsInSilentModeIOS: true,
19+
await ExpoAudioComponent.setAudioModeAsync({
20+
playsInSilentMode: true,
821
});
9-
const { sound } = await AudioComponent.Sound.createAsync(
10-
source,
11-
initialStatus,
12-
onPlaybackStatusUpdate,
13-
);
22+
const sound = new ExpoAudioSoundAdapter();
23+
await sound.loadAsync(source, initialStatus);
24+
sound.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
1425
return sound;
1526
}
16-
: null,
27+
: AudioComponent
28+
? async (source, initialStatus, onPlaybackStatusUpdate: (playbackStatus) => void) => {
29+
await AudioComponent.setAudioModeAsync({
30+
playsInSilentModeIOS: true,
31+
});
32+
const { sound } = await AudioComponent.Sound.createAsync(
33+
source,
34+
initialStatus,
35+
onPlaybackStatusUpdate,
36+
);
37+
return sound;
38+
}
39+
: null,
1740
Player: null,
1841
};
42+
43+
type ExpoAudioPlaybackStatus = {
44+
currentTime: number;
45+
didJustFinish: boolean;
46+
duration: number;
47+
id: number;
48+
isBuffering: boolean;
49+
isLoaded: boolean;
50+
loop: boolean;
51+
mute: boolean;
52+
playbackRate: number;
53+
playbackState: string;
54+
playing: boolean;
55+
reasonForWaitingToPlay: string;
56+
shouldCorrectPitch: boolean;
57+
timeControlStatus: string;
58+
};
59+
60+
class ExpoAudioSoundAdapter {
61+
private player;
62+
private statusEventListener;
63+
private initialPitchCorrectionQuality;
64+
private initialShouldCorrectPitch;
65+
66+
constructor() {}
67+
68+
setOnPlaybackStatusUpdate = (
69+
onPlaybackStatusUpdate: (playbackStatus: PlaybackStatus) => void,
70+
) => {
71+
this.statusEventListener = this.player.addListener(
72+
'playbackStatusUpdate',
73+
(playbackStatus: ExpoAudioPlaybackStatus) => {
74+
onPlaybackStatusUpdate(expoAudioToExpoAvStatusAdapter(playbackStatus));
75+
},
76+
);
77+
};
78+
79+
// eslint-disable-next-line require-await
80+
loadAsync = async (source, initialStatus) => {
81+
this.player = expoCreateSoundPlayer?.(source, initialStatus.progressUpdateIntervalMillis);
82+
this.initialShouldCorrectPitch = initialStatus.shouldCorrectPitch;
83+
this.initialPitchCorrectionQuality = initialStatus.pitchCorrectionQuality;
84+
};
85+
86+
// eslint-disable-next-line require-await
87+
stopAsync: SoundReturnType['stopAsync'] = async () => {
88+
this.player.seekTo(0);
89+
this.player.pause();
90+
};
91+
92+
// eslint-disable-next-line require-await
93+
unloadAsync: SoundReturnType['unloadAsync'] = async () => {
94+
this.statusEventListener.remove();
95+
this.player.remove();
96+
};
97+
98+
// eslint-disable-next-line require-await
99+
playAsync: SoundReturnType['playAsync'] = async () => {
100+
this.player.play();
101+
};
102+
103+
// eslint-disable-next-line require-await
104+
pauseAsync: SoundReturnType['pauseAsync'] = async () => {
105+
this.player.pause();
106+
};
107+
108+
// eslint-disable-next-line require-await
109+
replayAsync: SoundReturnType['replayAsync'] = async () => {
110+
this.player.seekTo(0);
111+
};
112+
113+
// eslint-disable-next-line require-await
114+
setPositionAsync: SoundReturnType['setPositionAsync'] = async (milliseconds) => {
115+
const seconds = milliseconds / 1000;
116+
this.player.seekTo(seconds);
117+
};
118+
119+
// eslint-disable-next-line require-await
120+
setRateAsync: SoundReturnType['setRateAsync'] = async (
121+
rate,
122+
shouldCorrectPitch = this.initialShouldCorrectPitch,
123+
pitchCorrectionQuality = this.initialPitchCorrectionQuality,
124+
) => {
125+
if (shouldCorrectPitch && pitchCorrectionQuality) {
126+
this.player.setPlaybackRate(rate, pitchCorrectionQuality);
127+
return;
128+
}
129+
this.player.setPlaybackRate(rate);
130+
};
131+
}
132+
133+
const expoAudioToExpoAvStatusAdapter = (
134+
playbackStatus: ExpoAudioPlaybackStatus,
135+
): PlaybackStatus => {
136+
const { currentTime, didJustFinish, duration, isBuffering, isLoaded, loop, mute, playing } =
137+
playbackStatus;
138+
139+
return {
140+
currentPosition: undefined, // not present in the expo-av api, breaks things if set
141+
didJustFinish,
142+
duration: undefined, // not present in the expo-av api, breaks things if set
143+
durationMillis: duration * 1000,
144+
error: null, // TODO: check how we can see if there is an error
145+
isBuffering,
146+
isLoaded,
147+
isLooping: loop,
148+
isMuted: mute,
149+
isPlaying: playing,
150+
isSeeking: false, // we don't use this anywhere, so just defaulting to a safe value since nothing similar exists in expo-audio
151+
positionMillis: currentTime * 1000,
152+
shouldPlay: undefined, // we cannot determine whether the audio should be playing or not
153+
};
154+
};

0 commit comments

Comments
 (0)