-
Notifications
You must be signed in to change notification settings - Fork 61
iOS Audio configuration improvements #290
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
982acc8
6edb055
70f52c3
928fc11
7b60981
f467a48
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,141 +1,86 @@ | ||
| import { useState, useEffect, useMemo } from 'react'; | ||
| import { Platform } from 'react-native'; | ||
| import { | ||
| RoomEvent, | ||
| Room, | ||
| type LocalTrackPublication, | ||
| type RemoteTrackPublication, | ||
| } from 'livekit-client'; | ||
| import AudioSession, { | ||
| getDefaultAppleAudioConfigurationForMode, | ||
| type AppleAudioConfiguration, | ||
| type AudioTrackState, | ||
| } from './AudioSession'; | ||
| import { log } from '..'; | ||
| import { audioDeviceModuleEvents } from '@livekit/react-native-webrtc'; | ||
|
|
||
| export type AudioEngineConfigurationState = { | ||
| isPlayoutEnabled: boolean; | ||
| isRecordingEnabled: boolean; | ||
| preferSpeakerOutput: boolean; | ||
| }; | ||
|
|
||
| /** | ||
| * Handles setting the appropriate AVAudioSession options automatically | ||
| * depending on the audio track states of the Room. | ||
| * | ||
| * @param room | ||
| * @param preferSpeakerOutput | ||
| * @param onConfigureNativeAudio A custom method for determining options used. | ||
| */ | ||
| export function useIOSAudioManagement( | ||
| room: Room, | ||
| preferSpeakerOutput: boolean = true, | ||
| onConfigureNativeAudio?: ( | ||
| trackState: AudioTrackState, | ||
| preferSpeakerOutput: boolean | ||
| ) => AppleAudioConfiguration | ||
| preferSpeakerOutput = true, | ||
| onConfigureNativeAudio?: (configurationState: AudioEngineConfigurationState) => AppleAudioConfiguration | ||
| ) { | ||
| const [localTrackCount, setLocalTrackCount] = useState(0); | ||
| const [remoteTrackCount, setRemoteTrackCount] = useState(0); | ||
| const trackState = useMemo( | ||
| () => computeAudioTrackState(localTrackCount, remoteTrackCount), | ||
| [localTrackCount, remoteTrackCount] | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| let recalculateTrackCounts = () => { | ||
| setLocalTrackCount(getLocalAudioTrackCount(room)); | ||
| setRemoteTrackCount(getRemoteAudioTrackCount(room)); | ||
| }; | ||
|
|
||
| recalculateTrackCounts(); | ||
|
|
||
| room.on(RoomEvent.Connected, recalculateTrackCounts); | ||
|
|
||
| return () => { | ||
| room.off(RoomEvent.Connected, recalculateTrackCounts); | ||
| }; | ||
| }, [room]); | ||
| useEffect(() => { | ||
| if (Platform.OS !== 'ios') { | ||
| return () => {}; | ||
| } | ||
| let audioEngineState: AudioEngineConfigurationState = { | ||
| isPlayoutEnabled: false, | ||
| isRecordingEnabled: false, | ||
| preferSpeakerOutput: preferSpeakerOutput, | ||
| }; | ||
|
|
||
| let onLocalPublished = (publication: LocalTrackPublication) => { | ||
| if (publication.kind === 'audio') { | ||
| setLocalTrackCount(localTrackCount + 1); | ||
| } | ||
| }; | ||
| let onLocalUnpublished = (publication: LocalTrackPublication) => { | ||
| if (publication.kind === 'audio') { | ||
| if (localTrackCount - 1 < 0) { | ||
| log.warn( | ||
| 'mismatched local audio track count! attempted to reduce track count below zero.' | ||
| ); | ||
| } | ||
| setLocalTrackCount(Math.max(localTrackCount - 1, 0)); | ||
| } | ||
| }; | ||
| let onRemotePublished = (publication: RemoteTrackPublication) => { | ||
| if (publication.kind === 'audio') { | ||
| setRemoteTrackCount(remoteTrackCount + 1); | ||
| const tryConfigure = async (newState: AudioEngineConfigurationState, oldState: AudioEngineConfigurationState) => { | ||
| if ((!newState.isPlayoutEnabled && !newState.isRecordingEnabled) && (oldState.isPlayoutEnabled || oldState.isRecordingEnabled)) { | ||
| log.info("AudioSession deactivating...") | ||
| await AudioSession.stopAudioSession() | ||
|
||
| } else if (newState.isRecordingEnabled || newState.isPlayoutEnabled) { | ||
| const config = onConfigureNativeAudio ? onConfigureNativeAudio(newState) : getDefaultAppleAudioConfigurationForAudioState(newState); | ||
| log.info("AudioSession configuring category:", config.audioCategory) | ||
| await AudioSession.setAppleAudioConfiguration(config) | ||
| if (!oldState.isPlayoutEnabled && !oldState.isRecordingEnabled) { | ||
| log.info("AudioSession activating...") | ||
| await AudioSession.startAudioSession() | ||
| } | ||
| }; | ||
| let onRemoteUnpublished = (publication: RemoteTrackPublication) => { | ||
| if (publication.kind === 'audio') { | ||
| if (remoteTrackCount - 1 < 0) { | ||
| log.warn( | ||
| 'mismatched remote audio track count! attempted to reduce track count below zero.' | ||
| ); | ||
| } | ||
| setRemoteTrackCount(Math.max(remoteTrackCount - 1, 0)); | ||
| } | ||
| }; | ||
|
|
||
| room | ||
| .on(RoomEvent.LocalTrackPublished, onLocalPublished) | ||
| .on(RoomEvent.LocalTrackUnpublished, onLocalUnpublished) | ||
| .on(RoomEvent.TrackPublished, onRemotePublished) | ||
| .on(RoomEvent.TrackUnpublished, onRemoteUnpublished); | ||
| } | ||
| }; | ||
|
|
||
| return () => { | ||
| room | ||
| .off(RoomEvent.LocalTrackPublished, onLocalPublished) | ||
| .off(RoomEvent.LocalTrackUnpublished, onLocalUnpublished) | ||
| .off(RoomEvent.TrackPublished, onRemotePublished) | ||
| .off(RoomEvent.TrackUnpublished, onRemoteUnpublished); | ||
| const handleEngineStateUpdate = async ({ isPlayoutEnabled, isRecordingEnabled }: { isPlayoutEnabled: boolean, isRecordingEnabled: boolean }) => { | ||
| const oldState = audioEngineState; | ||
| const newState = { | ||
| isPlayoutEnabled, | ||
| isRecordingEnabled, | ||
| preferSpeakerOutput: audioEngineState.preferSpeakerOutput, | ||
| }; | ||
| }, [room, localTrackCount, remoteTrackCount]); | ||
|
|
||
| useEffect(() => { | ||
| if (Platform.OS !== 'ios') { | ||
| return; | ||
| } | ||
| // If this throws, the audio engine will not continue it's operation | ||
| await tryConfigure(newState, oldState); | ||
| // Update the audio state only if configure succeeds | ||
| audioEngineState = newState; | ||
| }; | ||
|
|
||
| let configFunc = | ||
| onConfigureNativeAudio ?? getDefaultAppleAudioConfigurationForMode; | ||
| let audioConfig = configFunc(trackState, preferSpeakerOutput); | ||
| AudioSession.setAppleAudioConfiguration(audioConfig); | ||
| }, [trackState, onConfigureNativeAudio, preferSpeakerOutput]); | ||
| // Attach audio engine events | ||
| audioDeviceModuleEvents.setWillEnableEngineHandler(handleEngineStateUpdate); | ||
| audioDeviceModuleEvents.setDidDisableEngineHandler(handleEngineStateUpdate); | ||
| } | ||
|
|
||
| function computeAudioTrackState( | ||
| localTracks: number, | ||
| remoteTracks: number | ||
| ): AudioTrackState { | ||
| if (localTracks > 0 && remoteTracks > 0) { | ||
| return 'localAndRemote'; | ||
| } else if (localTracks > 0 && remoteTracks === 0) { | ||
| return 'localOnly'; | ||
| } else if (localTracks === 0 && remoteTracks > 0) { | ||
| return 'remoteOnly'; | ||
| } else { | ||
| return 'none'; | ||
| function getDefaultAppleAudioConfigurationForAudioState( | ||
| configurationState: AudioEngineConfigurationState, | ||
| ): AppleAudioConfiguration { | ||
| if (configurationState.isRecordingEnabled) { | ||
| return { | ||
| audioCategory: 'playAndRecord', | ||
| audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'], | ||
| audioMode: configurationState.preferSpeakerOutput ? 'videoChat' : 'voiceChat', | ||
| }; | ||
| } else if (configurationState.isPlayoutEnabled) { | ||
| return { | ||
| audioCategory: 'playback', | ||
| audioCategoryOptions: ['mixWithOthers'], | ||
| audioMode: 'spokenAudio', | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| function getLocalAudioTrackCount(room: Room): number { | ||
| return room.localParticipant.audioTrackPublications.size; | ||
| } | ||
|
|
||
| function getRemoteAudioTrackCount(room: Room): number { | ||
| var audioTracks = 0; | ||
| room.remoteParticipants.forEach((participant) => { | ||
| audioTracks += participant.audioTrackPublications.size; | ||
| }); | ||
| return audioTracks; | ||
| return { | ||
| audioCategory: 'soloAmbient', | ||
| audioCategoryOptions: [], | ||
| audioMode: 'default', | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changes the behavior quite a bit. Better to create a separate method and deprecate the old one.
The
usemethod prefix is sort of special in React and indicates a React Hook (something that'd only live for the lifecycle of the containing component), whereas this one looks like it's a one and done setup method. Something likesetupIOSAudioManagementwould work instead.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, I think deprecating it is better 👍