|
1 | | -import { useState, useEffect, useMemo } from 'react'; |
2 | | -import { Platform } from 'react-native'; |
3 | | -import { |
4 | | - RoomEvent, |
5 | | - Room, |
6 | | - type LocalTrackPublication, |
7 | | - type RemoteTrackPublication, |
8 | | -} from 'livekit-client'; |
9 | 1 | import AudioSession, { |
10 | | - getDefaultAppleAudioConfigurationForMode, |
11 | 2 | type AppleAudioConfiguration, |
12 | | - type AudioTrackState, |
13 | 3 | } from './AudioSession'; |
14 | 4 | import { log } from '..'; |
| 5 | +import { audioDeviceModuleEvents } from '@livekit/react-native-webrtc'; |
| 6 | + |
| 7 | +export type AudioEngineConfigurationState = { |
| 8 | + isPlayoutEnabled: boolean; |
| 9 | + isRecordingEnabled: boolean; |
| 10 | + preferSpeakerOutput: boolean; |
| 11 | +}; |
15 | 12 |
|
16 | 13 | /** |
17 | 14 | * Handles setting the appropriate AVAudioSession options automatically |
18 | 15 | * depending on the audio track states of the Room. |
19 | 16 | * |
20 | | - * @param room |
21 | 17 | * @param preferSpeakerOutput |
22 | 18 | * @param onConfigureNativeAudio A custom method for determining options used. |
23 | 19 | */ |
24 | 20 | export function useIOSAudioManagement( |
25 | | - room: Room, |
26 | | - preferSpeakerOutput: boolean = true, |
27 | | - onConfigureNativeAudio?: ( |
28 | | - trackState: AudioTrackState, |
29 | | - preferSpeakerOutput: boolean |
30 | | - ) => AppleAudioConfiguration |
| 21 | + preferSpeakerOutput = true, |
| 22 | + onConfigureNativeAudio?: (configurationState: AudioEngineConfigurationState) => AppleAudioConfiguration |
31 | 23 | ) { |
32 | | - const [localTrackCount, setLocalTrackCount] = useState(0); |
33 | | - const [remoteTrackCount, setRemoteTrackCount] = useState(0); |
34 | | - const trackState = useMemo( |
35 | | - () => computeAudioTrackState(localTrackCount, remoteTrackCount), |
36 | | - [localTrackCount, remoteTrackCount] |
37 | | - ); |
38 | | - |
39 | | - useEffect(() => { |
40 | | - let recalculateTrackCounts = () => { |
41 | | - setLocalTrackCount(getLocalAudioTrackCount(room)); |
42 | | - setRemoteTrackCount(getRemoteAudioTrackCount(room)); |
43 | | - }; |
44 | | - |
45 | | - recalculateTrackCounts(); |
46 | | - |
47 | | - room.on(RoomEvent.Connected, recalculateTrackCounts); |
48 | | - |
49 | | - return () => { |
50 | | - room.off(RoomEvent.Connected, recalculateTrackCounts); |
51 | | - }; |
52 | | - }, [room]); |
53 | | - useEffect(() => { |
54 | | - if (Platform.OS !== 'ios') { |
55 | | - return () => {}; |
56 | | - } |
| 24 | + let audioEngineState: AudioEngineConfigurationState = { |
| 25 | + isPlayoutEnabled: false, |
| 26 | + isRecordingEnabled: false, |
| 27 | + preferSpeakerOutput: preferSpeakerOutput, |
| 28 | + }; |
57 | 29 |
|
58 | | - let onLocalPublished = (publication: LocalTrackPublication) => { |
59 | | - if (publication.kind === 'audio') { |
60 | | - setLocalTrackCount(localTrackCount + 1); |
61 | | - } |
62 | | - }; |
63 | | - let onLocalUnpublished = (publication: LocalTrackPublication) => { |
64 | | - if (publication.kind === 'audio') { |
65 | | - if (localTrackCount - 1 < 0) { |
66 | | - log.warn( |
67 | | - 'mismatched local audio track count! attempted to reduce track count below zero.' |
68 | | - ); |
69 | | - } |
70 | | - setLocalTrackCount(Math.max(localTrackCount - 1, 0)); |
71 | | - } |
72 | | - }; |
73 | | - let onRemotePublished = (publication: RemoteTrackPublication) => { |
74 | | - if (publication.kind === 'audio') { |
75 | | - setRemoteTrackCount(remoteTrackCount + 1); |
| 30 | + const tryConfigure = async (newState: AudioEngineConfigurationState, oldState: AudioEngineConfigurationState) => { |
| 31 | + if ((!newState.isPlayoutEnabled && !newState.isRecordingEnabled) && (oldState.isPlayoutEnabled || oldState.isRecordingEnabled)) { |
| 32 | + log.info("AudioSession deactivating...") |
| 33 | + await AudioSession.stopAudioSession() |
| 34 | + } else if (newState.isRecordingEnabled || newState.isPlayoutEnabled) { |
| 35 | + const config = onConfigureNativeAudio ? onConfigureNativeAudio(newState) : getDefaultAppleAudioConfigurationForAudioState(newState); |
| 36 | + log.info("AudioSession configuring category:", config.audioCategory) |
| 37 | + await AudioSession.setAppleAudioConfiguration(config) |
| 38 | + if (!oldState.isPlayoutEnabled && !oldState.isRecordingEnabled) { |
| 39 | + log.info("AudioSession activating...") |
| 40 | + await AudioSession.startAudioSession() |
76 | 41 | } |
77 | | - }; |
78 | | - let onRemoteUnpublished = (publication: RemoteTrackPublication) => { |
79 | | - if (publication.kind === 'audio') { |
80 | | - if (remoteTrackCount - 1 < 0) { |
81 | | - log.warn( |
82 | | - 'mismatched remote audio track count! attempted to reduce track count below zero.' |
83 | | - ); |
84 | | - } |
85 | | - setRemoteTrackCount(Math.max(remoteTrackCount - 1, 0)); |
86 | | - } |
87 | | - }; |
88 | | - |
89 | | - room |
90 | | - .on(RoomEvent.LocalTrackPublished, onLocalPublished) |
91 | | - .on(RoomEvent.LocalTrackUnpublished, onLocalUnpublished) |
92 | | - .on(RoomEvent.TrackPublished, onRemotePublished) |
93 | | - .on(RoomEvent.TrackUnpublished, onRemoteUnpublished); |
| 42 | + } |
| 43 | + }; |
94 | 44 |
|
95 | | - return () => { |
96 | | - room |
97 | | - .off(RoomEvent.LocalTrackPublished, onLocalPublished) |
98 | | - .off(RoomEvent.LocalTrackUnpublished, onLocalUnpublished) |
99 | | - .off(RoomEvent.TrackPublished, onRemotePublished) |
100 | | - .off(RoomEvent.TrackUnpublished, onRemoteUnpublished); |
| 45 | + const handleEngineStateUpdate = async ({ isPlayoutEnabled, isRecordingEnabled }: { isPlayoutEnabled: boolean, isRecordingEnabled: boolean }) => { |
| 46 | + const oldState = audioEngineState; |
| 47 | + const newState = { |
| 48 | + isPlayoutEnabled, |
| 49 | + isRecordingEnabled, |
| 50 | + preferSpeakerOutput: audioEngineState.preferSpeakerOutput, |
101 | 51 | }; |
102 | | - }, [room, localTrackCount, remoteTrackCount]); |
103 | 52 |
|
104 | | - useEffect(() => { |
105 | | - if (Platform.OS !== 'ios') { |
106 | | - return; |
107 | | - } |
| 53 | + // If this throws, the audio engine will not continue it's operation |
| 54 | + await tryConfigure(newState, oldState); |
| 55 | + // Update the audio state only if configure succeeds |
| 56 | + audioEngineState = newState; |
| 57 | + }; |
108 | 58 |
|
109 | | - let configFunc = |
110 | | - onConfigureNativeAudio ?? getDefaultAppleAudioConfigurationForMode; |
111 | | - let audioConfig = configFunc(trackState, preferSpeakerOutput); |
112 | | - AudioSession.setAppleAudioConfiguration(audioConfig); |
113 | | - }, [trackState, onConfigureNativeAudio, preferSpeakerOutput]); |
| 59 | + // Attach audio engine events |
| 60 | + audioDeviceModuleEvents.setWillEnableEngineHandler(handleEngineStateUpdate); |
| 61 | + audioDeviceModuleEvents.setDidDisableEngineHandler(handleEngineStateUpdate); |
114 | 62 | } |
115 | 63 |
|
116 | | -function computeAudioTrackState( |
117 | | - localTracks: number, |
118 | | - remoteTracks: number |
119 | | -): AudioTrackState { |
120 | | - if (localTracks > 0 && remoteTracks > 0) { |
121 | | - return 'localAndRemote'; |
122 | | - } else if (localTracks > 0 && remoteTracks === 0) { |
123 | | - return 'localOnly'; |
124 | | - } else if (localTracks === 0 && remoteTracks > 0) { |
125 | | - return 'remoteOnly'; |
126 | | - } else { |
127 | | - return 'none'; |
| 64 | +function getDefaultAppleAudioConfigurationForAudioState( |
| 65 | + configurationState: AudioEngineConfigurationState, |
| 66 | +): AppleAudioConfiguration { |
| 67 | + if (configurationState.isRecordingEnabled) { |
| 68 | + return { |
| 69 | + audioCategory: 'playAndRecord', |
| 70 | + audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'], |
| 71 | + audioMode: configurationState.preferSpeakerOutput ? 'videoChat' : 'voiceChat', |
| 72 | + }; |
| 73 | + } else if (configurationState.isPlayoutEnabled) { |
| 74 | + return { |
| 75 | + audioCategory: 'playback', |
| 76 | + audioCategoryOptions: ['mixWithOthers'], |
| 77 | + audioMode: 'spokenAudio', |
| 78 | + }; |
128 | 79 | } |
129 | | -} |
130 | | - |
131 | | -function getLocalAudioTrackCount(room: Room): number { |
132 | | - return room.localParticipant.audioTrackPublications.size; |
133 | | -} |
134 | 80 |
|
135 | | -function getRemoteAudioTrackCount(room: Room): number { |
136 | | - var audioTracks = 0; |
137 | | - room.remoteParticipants.forEach((participant) => { |
138 | | - audioTracks += participant.audioTrackPublications.size; |
139 | | - }); |
140 | | - return audioTracks; |
| 81 | + return { |
| 82 | + audioCategory: 'soloAmbient', |
| 83 | + audioCategoryOptions: [], |
| 84 | + audioMode: 'default', |
| 85 | + }; |
141 | 86 | } |
0 commit comments