|
| 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'; |
1 | 9 | import AudioSession, { |
| 10 | + getDefaultAppleAudioConfigurationForMode, |
2 | 11 | type AppleAudioConfiguration, |
| 12 | + type AudioTrackState, |
3 | 13 | } from './AudioSession'; |
4 | 14 | 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 | | -}; |
12 | 15 |
|
13 | 16 | /** |
14 | 17 | * Handles setting the appropriate AVAudioSession options automatically |
15 | 18 | * depending on the audio track states of the Room. |
16 | 19 | * |
| 20 | + * @param room |
17 | 21 | * @param preferSpeakerOutput |
18 | 22 | * @param onConfigureNativeAudio A custom method for determining options used. |
19 | 23 | */ |
20 | 24 | export function useIOSAudioManagement( |
21 | | - preferSpeakerOutput = true, |
22 | | - onConfigureNativeAudio?: (configurationState: AudioEngineConfigurationState) => AppleAudioConfiguration |
| 25 | + room: Room, |
| 26 | + preferSpeakerOutput: boolean = true, |
| 27 | + onConfigureNativeAudio?: ( |
| 28 | + trackState: AudioTrackState, |
| 29 | + preferSpeakerOutput: boolean |
| 30 | + ) => AppleAudioConfiguration |
23 | 31 | ) { |
24 | | - let audioEngineState: AudioEngineConfigurationState = { |
25 | | - isPlayoutEnabled: false, |
26 | | - isRecordingEnabled: false, |
27 | | - preferSpeakerOutput: preferSpeakerOutput, |
28 | | - }; |
| 32 | + const [localTrackCount, setLocalTrackCount] = useState(0); |
| 33 | + const [remoteTrackCount, setRemoteTrackCount] = useState(0); |
| 34 | + const trackState = useMemo( |
| 35 | + () => computeAudioTrackState(localTrackCount, remoteTrackCount), |
| 36 | + [localTrackCount, remoteTrackCount] |
| 37 | + ); |
29 | 38 |
|
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() |
41 | | - } |
| 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 () => {}; |
42 | 56 | } |
43 | | - }; |
44 | 57 |
|
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, |
| 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); |
| 76 | + } |
51 | 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); |
52 | 94 |
|
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 | | - }; |
| 95 | + return () => { |
| 96 | + room |
| 97 | + .off(RoomEvent.LocalTrackPublished, onLocalPublished) |
| 98 | + .off(RoomEvent.LocalTrackUnpublished, onLocalUnpublished) |
| 99 | + .off(RoomEvent.TrackPublished, onRemotePublished) |
| 100 | + .off(RoomEvent.TrackUnpublished, onRemoteUnpublished); |
| 101 | + }; |
| 102 | + }, [room, localTrackCount, remoteTrackCount]); |
| 103 | + |
| 104 | + useEffect(() => { |
| 105 | + if (Platform.OS !== 'ios') { |
| 106 | + return; |
| 107 | + } |
58 | 108 |
|
59 | | - // Attach audio engine events |
60 | | - audioDeviceModuleEvents.setWillEnableEngineHandler(handleEngineStateUpdate); |
61 | | - audioDeviceModuleEvents.setDidDisableEngineHandler(handleEngineStateUpdate); |
| 109 | + let configFunc = |
| 110 | + onConfigureNativeAudio ?? getDefaultAppleAudioConfigurationForMode; |
| 111 | + let audioConfig = configFunc(trackState, preferSpeakerOutput); |
| 112 | + AudioSession.setAppleAudioConfiguration(audioConfig); |
| 113 | + }, [trackState, onConfigureNativeAudio, preferSpeakerOutput]); |
62 | 114 | } |
63 | 115 |
|
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 | | - }; |
| 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'; |
79 | 128 | } |
| 129 | +} |
| 130 | + |
| 131 | +function getLocalAudioTrackCount(room: Room): number { |
| 132 | + return room.localParticipant.audioTrackPublications.size; |
| 133 | +} |
80 | 134 |
|
81 | | - return { |
82 | | - audioCategory: 'soloAmbient', |
83 | | - audioCategoryOptions: [], |
84 | | - audioMode: 'default', |
85 | | - }; |
| 135 | +function getRemoteAudioTrackCount(room: Room): number { |
| 136 | + var audioTracks = 0; |
| 137 | + room.remoteParticipants.forEach((participant) => { |
| 138 | + audioTracks += participant.audioTrackPublications.size; |
| 139 | + }); |
| 140 | + return audioTracks; |
86 | 141 | } |
0 commit comments