diff --git a/.changeset/match-swift-play-and-record-options.md b/.changeset/match-swift-play-and-record-options.md new file mode 100644 index 00000000..fedb83ca --- /dev/null +++ b/.changeset/match-swift-play-and-record-options.md @@ -0,0 +1,21 @@ +--- +'@livekit/react-native': minor +--- + +iOS: align the default `playAndRecord` presets with the LiveKit Swift SDK. + +The duplex (recording) presets now request `mixWithOthers`, `allowBluetooth`, +`allowBluetoothA2DP` and `allowAirPlay`, matching `playAndRecordOptions` in the +Swift SDK. Previously only `allowBluetooth` and `mixWithOthers` were requested, +so A2DP output devices and AirPlay routes were unavailable during a call on this +platform but available on others. + +When `preferSpeakerOutput` is set, `defaultToSpeaker` is now requested as well. +The `videoChat` mode implies a speaker route, but iOS may rewrite the mode when +Voice Processing I/O is instantiated - the Swift SDK observed it switching to +`voiceChat`, which routes to the receiver. Requesting the option explicitly +keeps the speaker route across that rewrite. + +This affects the defaults used by `setupIOSAudioManagement` and the deprecated +`getDefaultAppleAudioConfigurationForMode`. A custom `IOSAudioSessionPolicy` or +`AppleAudioConfiguration` is unaffected - those supply their own options. diff --git a/.changeset/prefixed-webrtc-symbols.md b/.changeset/prefixed-webrtc-symbols.md new file mode 100644 index 00000000..00bdb51f --- /dev/null +++ b/.changeset/prefixed-webrtc-symbols.md @@ -0,0 +1,13 @@ +--- +'@livekit/react-native': major +--- + +Move to the LiveKit prefixed WebRTC builds: `io.github.webrtc-sdk:android-prefixed` on Android and the `LiveKitWebRTC` pod on iOS. This prevents collisions against any other WebRTC implementations. + +Apps that only use the JavaScript API need no changes. + +Native integrations must migrate to the prefixed symbols: + +* Android: `org.webrtc.*` becomes `livekit.org.webrtc.*`. + Custom `-keep class org.webrtc.**` ProGuard rules must be updated to `livekit.org.webrtc.**`. +* iOS: types gain an `LK` prefix (`RTCAudioRenderer` becomes `LKRTCAudioRenderer`, `RTCAudioBuffer` becomes `LKRTCAudioBuffer`, and so on). diff --git a/.changeset/voice-processing-aware-session-mode.md b/.changeset/voice-processing-aware-session-mode.md new file mode 100644 index 00000000..0334ec92 --- /dev/null +++ b/.changeset/voice-processing-aware-session-mode.md @@ -0,0 +1,30 @@ +--- +'@livekit/react-native': minor +--- + +iOS: pick the audio session mode based on Apple Voice Processing I/O state. + +iOS applies a reduced, call-tuned speaker gain while capture is active under the +`voiceChat`/`videoChat` session modes. Apple Voice Processing I/O compensates +with its own loudness stage, so with VPIO off remote audio played back +noticeably quieter. + +`LiveKitWebRTC` 144.7559.15 reports the resolved VPIO state to the audio device +module's `willEnableEngine` hook. The default recording configuration now uses +that: with VPIO running it keeps `voiceChat`/`videoChat` as before, and with VPIO +off (for example after `AudioDeviceModule.setVoiceProcessingEnabled(false)`) it +switches to the `default` mode, which keeps media gain. That mode routes to the +receiver, so `preferSpeakerOutput` is expressed through the `defaultToSpeaker` +category option instead of the implicit routing the chat modes provide. This +matches what the LiveKit Swift SDK already does. + +`IOSAudioSessionPolicy` gains an optional `recordingWithoutVoiceProcessing` +configuration for the VPIO-off case. Policies that omit it keep using their +`recording` configuration for both, so existing setups are unaffected. +`AudioEngineConfigurationState`, passed to the deprecated +`setupIOSAudioManagement` callback form, gains a matching +`isVoiceProcessingEnabled` field. + +Requires `@livekit/react-native-webrtc` with the five-argument `willEnableEngine` +delegate method. Older versions crash with an unrecognized selector when the +audio engine first enables. diff --git a/android/build.gradle b/android/build.gradle index 4d1faae3..83ea8830 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -128,7 +128,7 @@ dependencies { api 'com.facebook.react:react-native:+' implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" api 'com.github.davidliu:audioswitch:89582c47c9a04c62f90aa5e57251af4800a62c9a' - api 'io.github.webrtc-sdk:android:144.7559.04' + api 'io.github.webrtc-sdk:android-prefixed:144.7559.15' implementation "io.livekit:noise:2.0.0" implementation project(':livekit_react-native-webrtc') implementation "androidx.annotation:annotation:1.9.1" diff --git a/android/src/main/java/com/livekit/reactnative/LiveKitReactNative.kt b/android/src/main/java/com/livekit/reactnative/LiveKitReactNative.kt index 01b6e764..a54aa27e 100644 --- a/android/src/main/java/com/livekit/reactnative/LiveKitReactNative.kt +++ b/android/src/main/java/com/livekit/reactnative/LiveKitReactNative.kt @@ -11,7 +11,7 @@ import com.livekit.reactnative.audio.processing.CustomAudioProcessingController import com.livekit.reactnative.video.CustomVideoDecoderFactory import com.livekit.reactnative.video.CustomVideoEncoderFactory import com.oney.WebRTCModule.WebRTCModuleOptions -import org.webrtc.audio.JavaAudioDeviceModule +import livekit.org.webrtc.audio.JavaAudioDeviceModule import java.util.concurrent.Callable object LiveKitReactNative { diff --git a/android/src/main/java/com/livekit/reactnative/LivekitReactNativeModule.kt b/android/src/main/java/com/livekit/reactnative/LivekitReactNativeModule.kt index f78e7289..ece4fcb5 100644 --- a/android/src/main/java/com/livekit/reactnative/LivekitReactNativeModule.kt +++ b/android/src/main/java/com/livekit/reactnative/LivekitReactNativeModule.kt @@ -18,7 +18,7 @@ import com.livekit.reactnative.audio.processing.AudioSinkProcessor import com.livekit.reactnative.audio.processing.MultibandVolumeProcessor import com.livekit.reactnative.audio.processing.VolumeProcessor import com.oney.WebRTCModule.WebRTCModuleOptions -import org.webrtc.audio.WebRtcAudioTrackHelper +import livekit.org.webrtc.audio.WebRtcAudioTrackHelper import java.lang.Thread.sleep import kotlin.concurrent.thread import kotlin.time.Duration.Companion.milliseconds diff --git a/android/src/main/java/com/livekit/reactnative/audio/processing/AudioRecordSamplesDispatcher.kt b/android/src/main/java/com/livekit/reactnative/audio/processing/AudioRecordSamplesDispatcher.kt index 13d8585b..85c80724 100644 --- a/android/src/main/java/com/livekit/reactnative/audio/processing/AudioRecordSamplesDispatcher.kt +++ b/android/src/main/java/com/livekit/reactnative/audio/processing/AudioRecordSamplesDispatcher.kt @@ -18,8 +18,8 @@ package com.livekit.reactnative.audio.processing import android.media.AudioFormat import android.os.SystemClock -import org.webrtc.AudioTrackSink -import org.webrtc.audio.JavaAudioDeviceModule +import livekit.org.webrtc.AudioTrackSink +import livekit.org.webrtc.audio.JavaAudioDeviceModule import java.nio.ByteBuffer import java.util.Collections diff --git a/android/src/main/java/com/livekit/reactnative/audio/processing/AudioSinkManager.kt b/android/src/main/java/com/livekit/reactnative/audio/processing/AudioSinkManager.kt index 4ee41868..ab28b4e5 100644 --- a/android/src/main/java/com/livekit/reactnative/audio/processing/AudioSinkManager.kt +++ b/android/src/main/java/com/livekit/reactnative/audio/processing/AudioSinkManager.kt @@ -3,8 +3,8 @@ package com.livekit.reactnative.audio.processing import com.facebook.react.bridge.ReactContext import com.livekit.reactnative.LiveKitReactNative import com.oney.WebRTCModule.WebRTCModule -import org.webrtc.AudioTrack -import org.webrtc.AudioTrackSink +import livekit.org.webrtc.AudioTrack +import livekit.org.webrtc.AudioTrackSink import java.util.Collections import java.util.UUID diff --git a/android/src/main/java/com/livekit/reactnative/audio/processing/AudioSinkProcessor.kt b/android/src/main/java/com/livekit/reactnative/audio/processing/AudioSinkProcessor.kt index ca8c671c..02e197c1 100644 --- a/android/src/main/java/com/livekit/reactnative/audio/processing/AudioSinkProcessor.kt +++ b/android/src/main/java/com/livekit/reactnative/audio/processing/AudioSinkProcessor.kt @@ -4,7 +4,7 @@ import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactContext import com.facebook.react.modules.core.DeviceEventManagerModule import com.livekit.reactnative.audio.events.Events -import org.webrtc.AudioTrackSink +import livekit.org.webrtc.AudioTrackSink import java.nio.ByteBuffer import java.util.Arrays import kotlin.io.encoding.Base64 diff --git a/android/src/main/java/com/livekit/reactnative/audio/processing/CustomAudioProcessingFactory.kt b/android/src/main/java/com/livekit/reactnative/audio/processing/CustomAudioProcessingFactory.kt index 17f506dd..44ee38e5 100644 --- a/android/src/main/java/com/livekit/reactnative/audio/processing/CustomAudioProcessingFactory.kt +++ b/android/src/main/java/com/livekit/reactnative/audio/processing/CustomAudioProcessingFactory.kt @@ -1,6 +1,6 @@ package com.livekit.reactnative.audio.processing -import org.webrtc.ExternalAudioProcessingFactory +import livekit.org.webrtc.ExternalAudioProcessingFactory import java.nio.ByteBuffer /** diff --git a/android/src/main/java/com/livekit/reactnative/audio/processing/MultibandVolumeProcessor.kt b/android/src/main/java/com/livekit/reactnative/audio/processing/MultibandVolumeProcessor.kt index b41314e8..397599a2 100644 --- a/android/src/main/java/com/livekit/reactnative/audio/processing/MultibandVolumeProcessor.kt +++ b/android/src/main/java/com/livekit/reactnative/audio/processing/MultibandVolumeProcessor.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.transform import kotlinx.coroutines.launch -import org.webrtc.AudioTrackSink +import livekit.org.webrtc.AudioTrackSink import java.nio.ByteBuffer import kotlin.math.pow import kotlin.math.round diff --git a/android/src/main/java/com/livekit/reactnative/audio/processing/VolumeProcessor.kt b/android/src/main/java/com/livekit/reactnative/audio/processing/VolumeProcessor.kt index 802eae86..df73fe68 100644 --- a/android/src/main/java/com/livekit/reactnative/audio/processing/VolumeProcessor.kt +++ b/android/src/main/java/com/livekit/reactnative/audio/processing/VolumeProcessor.kt @@ -4,7 +4,7 @@ import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.ReactContext import com.facebook.react.modules.core.DeviceEventManagerModule import com.livekit.reactnative.audio.events.Events -import org.webrtc.AudioTrackSink +import livekit.org.webrtc.AudioTrackSink import java.nio.ByteBuffer import kotlin.math.round import kotlin.math.sqrt diff --git a/android/src/main/java/com/livekit/reactnative/video/CustomVideoDecoderFactory.kt b/android/src/main/java/com/livekit/reactnative/video/CustomVideoDecoderFactory.kt index 9b318e08..ed193432 100644 --- a/android/src/main/java/com/livekit/reactnative/video/CustomVideoDecoderFactory.kt +++ b/android/src/main/java/com/livekit/reactnative/video/CustomVideoDecoderFactory.kt @@ -17,12 +17,12 @@ package com.livekit.reactnative.video import com.oney.WebRTCModule.webrtcutils.SoftwareVideoDecoderFactoryProxy -import org.webrtc.EglBase -import org.webrtc.SoftwareVideoDecoderFactory -import org.webrtc.VideoCodecInfo -import org.webrtc.VideoDecoder -import org.webrtc.VideoDecoderFactory -import org.webrtc.WrappedVideoDecoderFactory +import livekit.org.webrtc.EglBase +import livekit.org.webrtc.SoftwareVideoDecoderFactory +import livekit.org.webrtc.VideoCodecInfo +import livekit.org.webrtc.VideoDecoder +import livekit.org.webrtc.VideoDecoderFactory +import livekit.org.webrtc.WrappedVideoDecoderFactory open class CustomVideoDecoderFactory( private var forceSWCodec: Boolean = false, diff --git a/android/src/main/java/com/livekit/reactnative/video/CustomVideoEncoderFactory.kt b/android/src/main/java/com/livekit/reactnative/video/CustomVideoEncoderFactory.kt index 741798d6..1259f90a 100644 --- a/android/src/main/java/com/livekit/reactnative/video/CustomVideoEncoderFactory.kt +++ b/android/src/main/java/com/livekit/reactnative/video/CustomVideoEncoderFactory.kt @@ -17,11 +17,11 @@ package com.livekit.reactnative.video import com.oney.WebRTCModule.webrtcutils.SoftwareVideoEncoderFactoryProxy -import org.webrtc.EglBase -import org.webrtc.SoftwareVideoEncoderFactory -import org.webrtc.VideoCodecInfo -import org.webrtc.VideoEncoder -import org.webrtc.VideoEncoderFactory +import livekit.org.webrtc.EglBase +import livekit.org.webrtc.SoftwareVideoEncoderFactory +import livekit.org.webrtc.VideoCodecInfo +import livekit.org.webrtc.VideoEncoder +import livekit.org.webrtc.VideoEncoderFactory open class CustomVideoEncoderFactory( sharedContext: EglBase.Context?, diff --git a/android/src/main/java/com/livekit/reactnative/video/SimulcastVideoEncoderFactoryWrapper.kt b/android/src/main/java/com/livekit/reactnative/video/SimulcastVideoEncoderFactoryWrapper.kt index 0025ba30..d035ac5a 100644 --- a/android/src/main/java/com/livekit/reactnative/video/SimulcastVideoEncoderFactoryWrapper.kt +++ b/android/src/main/java/com/livekit/reactnative/video/SimulcastVideoEncoderFactoryWrapper.kt @@ -2,17 +2,17 @@ package com.livekit.reactnative.video import android.util.Log import com.oney.WebRTCModule.webrtcutils.SoftwareVideoEncoderFactoryProxy -import org.webrtc.EglBase -import org.webrtc.HardwareVideoEncoderFactory -import org.webrtc.SimulcastVideoEncoderFactory -import org.webrtc.SoftwareVideoEncoderFactory -import org.webrtc.VideoCodecInfo -import org.webrtc.VideoCodecStatus -import org.webrtc.VideoEncoder -import org.webrtc.VideoEncoderFactory -import org.webrtc.VideoEncoderFallback -import org.webrtc.VideoFrame -import org.webrtc.WrappedNativeVideoEncoder +import livekit.org.webrtc.EglBase +import livekit.org.webrtc.HardwareVideoEncoderFactory +import livekit.org.webrtc.SimulcastVideoEncoderFactory +import livekit.org.webrtc.SoftwareVideoEncoderFactory +import livekit.org.webrtc.VideoCodecInfo +import livekit.org.webrtc.VideoCodecStatus +import livekit.org.webrtc.VideoEncoder +import livekit.org.webrtc.VideoEncoderFactory +import livekit.org.webrtc.VideoEncoderFallback +import livekit.org.webrtc.VideoFrame +import livekit.org.webrtc.WrappedNativeVideoEncoder import java.util.concurrent.Callable import java.util.concurrent.ExecutorService import java.util.concurrent.Executors diff --git a/android/src/main/java/com/livekit/reactnative/video/WrappedVideoDecoderFactoryProxy.kt b/android/src/main/java/com/livekit/reactnative/video/WrappedVideoDecoderFactoryProxy.kt index 48386610..372d3b49 100644 --- a/android/src/main/java/com/livekit/reactnative/video/WrappedVideoDecoderFactoryProxy.kt +++ b/android/src/main/java/com/livekit/reactnative/video/WrappedVideoDecoderFactoryProxy.kt @@ -1,10 +1,10 @@ package com.livekit.reactnative.video import com.oney.WebRTCModule.EglUtils -import org.webrtc.VideoCodecInfo -import org.webrtc.VideoDecoder -import org.webrtc.VideoDecoderFactory -import org.webrtc.WrappedVideoDecoderFactory +import livekit.org.webrtc.VideoCodecInfo +import livekit.org.webrtc.VideoDecoder +import livekit.org.webrtc.VideoDecoderFactory +import livekit.org.webrtc.WrappedVideoDecoderFactory class WrappedVideoDecoderFactoryProxy : VideoDecoderFactory { diff --git a/android/src/main/java/org/webrtc/audio/WebRtcAudioTrackHelper.kt b/android/src/main/java/livekit/org/webrtc/audio/WebRtcAudioTrackHelper.kt similarity index 94% rename from android/src/main/java/org/webrtc/audio/WebRtcAudioTrackHelper.kt rename to android/src/main/java/livekit/org/webrtc/audio/WebRtcAudioTrackHelper.kt index 7b256af2..af069167 100644 --- a/android/src/main/java/org/webrtc/audio/WebRtcAudioTrackHelper.kt +++ b/android/src/main/java/livekit/org/webrtc/audio/WebRtcAudioTrackHelper.kt @@ -1,4 +1,4 @@ -package org.webrtc.audio +package livekit.org.webrtc.audio import android.media.AudioAttributes diff --git a/ci/ios/Podfile.lock b/ci/ios/Podfile.lock index ab891f78..50ee9a2b 100644 --- a/ci/ios/Podfile.lock +++ b/ci/ios/Podfile.lock @@ -8,7 +8,7 @@ PODS: - hermes-engine (0.82.1): - hermes-engine/Pre-built (= 0.82.1) - hermes-engine/Pre-built (0.82.1) - - livekit-react-native (2.11.0): + - livekit-react-native (2.12.0): - boost - DoubleConversion - fast_float @@ -37,9 +37,10 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - livekit-react-native-webrtc (144.1.1): + - livekit-react-native-webrtc (144.2.0): + - LiveKitWebRTC (= 144.7559.15) - React-Core - - WebRTC-SDK (= 144.7559.10) + - LiveKitWebRTC (144.7559.15) - RCT-Folly (2024.11.18.00): - boost - DoubleConversion @@ -2441,7 +2442,6 @@ PODS: - React-utils (= 0.82.1) - SocketRocket - SocketRocket (0.7.1) - - WebRTC-SDK (144.7559.10) - Yoga (0.0.0) DEPENDENCIES: @@ -2526,8 +2526,8 @@ DEPENDENCIES: SPEC REPOS: trunk: + - LiveKitWebRTC - SocketRocket - - WebRTC-SDK EXTERNAL SOURCES: boost: @@ -2692,8 +2692,9 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 273e30e7fb618279934b0b95ffab60ecedb7acf5 - livekit-react-native: b8a86fb1c57270437f8a010437c0570585a13625 - livekit-react-native-webrtc: 9f52dbcc5edee327cdc8c9b458b6d6fd8ece1561 + livekit-react-native: 27001a5d9454cc8d27894317425aa511801154e2 + livekit-react-native-webrtc: 3cd501a18d7ee8b95c21764dd8502201a0bc4cb3 + LiveKitWebRTC: 6abc86e190722e223fc870284547b6f229288c46 RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 RCTDeprecation: f17e2ebc07876ca9ab8eb6e4b0a4e4647497ae3a RCTRequired: e2c574c1b45231f7efb0834936bd609d75072b63 @@ -2761,7 +2762,6 @@ SPEC CHECKSUMS: ReactCodegen: 878add6c7d8ff8cea87697c44d29c03b79b6f2d9 ReactCommon: 804dc80944fa90b86800b43c871742ec005ca424 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - WebRTC-SDK: 1d7fc2ae91da6edd63e25f3882ff24f86a8d9d8b Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb PODFILE CHECKSUM: a7f2ec66657cfb895ebb19b63b9033e6419495f9 diff --git a/ci/package.json b/ci/package.json index 70e63775..74694831 100644 --- a/ci/package.json +++ b/ci/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@livekit/react-native": "*", - "@livekit/react-native-webrtc": "^144.1.2", + "@livekit/react-native-webrtc": "^144.2.0", "@react-native/new-app-screen": "0.82.1", "livekit-client": "^2.19.0", "react": "19.1.1", diff --git a/ci/yarn.lock b/ci/yarn.lock index bcf3c3d6..6815603a 100644 --- a/ci/yarn.lock +++ b/ci/yarn.lock @@ -1952,15 +1952,15 @@ __metadata: languageName: node linkType: hard -"@livekit/react-native-webrtc@npm:^144.1.2": - version: 144.1.2 - resolution: "@livekit/react-native-webrtc@npm:144.1.2" +"@livekit/react-native-webrtc@npm:^144.2.0": + version: 144.2.0 + resolution: "@livekit/react-native-webrtc@npm:144.2.0" dependencies: base64-js: "npm:1.5.1" debug: "npm:4.3.4" peerDependencies: react-native: ">=0.60.0" - checksum: 10/ed4a3594c060404c89020568f0602ad48c51d9738b5479ef9fdecd330ba49349b98afa5b4db7c9faea394cc62edea993a4e8b39cde1181e79966a1c1da65a97a + checksum: 10/fb2db76db47d976cd1ebfff16eef7ba49123a6b0c7af76c6cde671889276c7a5c8c6ed43d4402cf7412ff9905dbf4d986eaf029dd0190cd1bb6fb1897075dd72 languageName: node linkType: hard @@ -3550,7 +3550,7 @@ __metadata: "@babel/preset-env": "npm:^7.25.3" "@babel/runtime": "npm:^7.25.0" "@livekit/react-native": "npm:*" - "@livekit/react-native-webrtc": "npm:^144.1.2" + "@livekit/react-native-webrtc": "npm:^144.2.0" "@react-native-community/cli": "npm:20.0.0" "@react-native-community/cli-platform-android": "npm:20.0.0" "@react-native-community/cli-platform-ios": "npm:20.0.0" diff --git a/eslint.config.mjs b/eslint.config.mjs index 09f2d08a..9bd580de 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -33,6 +33,6 @@ export default defineConfig([ }, }, { - ignores: ['node_modules/', 'lib/', 'docs/', 'src/polyfills/'], + ignores: ['node_modules/', 'lib/', 'docs/', 'src/polyfills/', '**/.yalc/'], }, ]); diff --git a/example/ios/LivekitReactNativeExample.xcodeproj/project.pbxproj b/example/ios/LivekitReactNativeExample.xcodeproj/project.pbxproj index 44c2c70e..bc4a24cc 100644 --- a/example/ios/LivekitReactNativeExample.xcodeproj/project.pbxproj +++ b/example/ios/LivekitReactNativeExample.xcodeproj/project.pbxproj @@ -366,12 +366,12 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-LivekitReactNativeExample/Pods-LivekitReactNativeExample-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/WebRTC-SDK/WebRTC.framework/WebRTC", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/LiveKitWebRTC/LiveKitWebRTC.framework/LiveKitWebRTC", "${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermesvm.framework/hermesvm", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/WebRTC.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LiveKitWebRTC.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermesvm.framework", ); runOnlyForDeploymentPostprocessing = 0; diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 1e77af90..0249e254 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -8,7 +8,7 @@ PODS: - hermes-engine (0.82.1): - hermes-engine/Pre-built (= 0.82.1) - hermes-engine/Pre-built (0.82.1) - - livekit-react-native (2.11.0): + - livekit-react-native (2.12.0): - boost - DoubleConversion - fast_float @@ -37,9 +37,10 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - livekit-react-native-webrtc (144.1.2): + - livekit-react-native-webrtc (144.2.0): + - LiveKitWebRTC (= 144.7559.15) - React-Core - - WebRTC-SDK (= 144.7559.10) + - LiveKitWebRTC (144.7559.15) - RCT-Folly (2024.11.18.00): - boost - DoubleConversion @@ -2530,7 +2531,6 @@ PODS: - SocketRocket - Yoga - SocketRocket (0.7.1) - - WebRTC-SDK (144.7559.10) - Yoga (0.0.0) DEPENDENCIES: @@ -2618,8 +2618,8 @@ DEPENDENCIES: SPEC REPOS: trunk: + - LiveKitWebRTC - SocketRocket - - WebRTC-SDK EXTERNAL SOURCES: boost: @@ -2790,8 +2790,9 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 273e30e7fb618279934b0b95ffab60ecedb7acf5 - livekit-react-native: b8a86fb1c57270437f8a010437c0570585a13625 - livekit-react-native-webrtc: 5ff3e8d229a3fcd34f99fe4cb1c2c93f92017b0c + livekit-react-native: 27001a5d9454cc8d27894317425aa511801154e2 + livekit-react-native-webrtc: 3cd501a18d7ee8b95c21764dd8502201a0bc4cb3 + LiveKitWebRTC: 6abc86e190722e223fc870284547b6f229288c46 RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f RCTDeprecation: f17e2ebc07876ca9ab8eb6e4b0a4e4647497ae3a RCTRequired: e2c574c1b45231f7efb0834936bd609d75072b63 @@ -2862,7 +2863,6 @@ SPEC CHECKSUMS: RNCAsyncStorage: 01e4301688a611936e3644746ffe3325d3181952 RNScreens: 0bbf16c074ae6bb1058a7bf2d1ae017f4306797c SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - WebRTC-SDK: 1d7fc2ae91da6edd63e25f3882ff24f86a8d9d8b Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb PODFILE CHECKSUM: a921a659e37cd7dfb12da519493e10c9f38400a1 diff --git a/example/package.json b/example/package.json index fb5ffa03..5f35ed79 100644 --- a/example/package.json +++ b/example/package.json @@ -10,7 +10,7 @@ "postinstall": "patch-package" }, "dependencies": { - "@livekit/react-native-webrtc": "^144.1.2", + "@livekit/react-native-webrtc": "^144.2.0", "@react-native-async-storage/async-storage": "^1.17.10", "@react-navigation/native": "^7.1.18", "@react-navigation/native-stack": "^7.3.27", diff --git a/ios/Headers/LKAudioProcessingAdapter.h b/ios/Headers/LKAudioProcessingAdapter.h index f7327080..12bbfb19 100644 --- a/ios/Headers/LKAudioProcessingAdapter.h +++ b/ios/Headers/LKAudioProcessingAdapter.h @@ -1,17 +1,17 @@ #import -#import +#import @protocol LKExternalAudioProcessingDelegate - (void)audioProcessingInitializeWithSampleRate:(size_t)sampleRateHz channels:(size_t)channels; -- (void)audioProcessingProcess:(RTC_OBJC_TYPE(RTCAudioBuffer) * _Nonnull)audioBuffer; +- (void)audioProcessingProcess:(LKRTCAudioBuffer * _Nonnull)audioBuffer; - (void)audioProcessingRelease; @end -@interface LKAudioProcessingAdapter : NSObject +@interface LKAudioProcessingAdapter : NSObject - (nonnull instancetype)init; @@ -19,8 +19,8 @@ - (void)removeProcessing:(id _Nonnull)processor; -- (void)addAudioRenderer:(nonnull id)renderer; +- (void)addAudioRenderer:(nonnull id)renderer; -- (void)removeAudioRenderer:(nonnull id)renderer; +- (void)removeAudioRenderer:(nonnull id)renderer; @end \ No newline at end of file diff --git a/ios/Headers/LKAudioProcessingManager.h b/ios/Headers/LKAudioProcessingManager.h index 332b85c5..e86a5c20 100644 --- a/ios/Headers/LKAudioProcessingManager.h +++ b/ios/Headers/LKAudioProcessingManager.h @@ -1,10 +1,10 @@ #import -#import +#import #import "LKAudioProcessingAdapter.h" @interface LKAudioProcessingManager : NSObject -@property(nonatomic, strong) RTCDefaultAudioProcessingModule* _Nonnull audioProcessingModule; +@property(nonatomic, strong) LKRTCDefaultAudioProcessingModule* _Nonnull audioProcessingModule; @property(nonatomic, strong) LKAudioProcessingAdapter* _Nonnull capturePostProcessingAdapter; @@ -13,13 +13,13 @@ + (_Nonnull instancetype)sharedInstance; -- (void)addLocalAudioRenderer:(nonnull id)renderer; +- (void)addLocalAudioRenderer:(nonnull id)renderer; -- (void)removeLocalAudioRenderer:(nonnull id)renderer; +- (void)removeLocalAudioRenderer:(nonnull id)renderer; -- (void)addRemoteAudioRenderer:(nonnull id)renderer; +- (void)addRemoteAudioRenderer:(nonnull id)renderer; -- (void)removeRemoteAudioRenderer:(nonnull id)renderer; +- (void)removeRemoteAudioRenderer:(nonnull id)renderer; - (void)addCapturePostProcessor:(nonnull id)processor; diff --git a/ios/LKAudioProcessingAdapter.m b/ios/LKAudioProcessingAdapter.m index 1d638246..e80b4238 100644 --- a/ios/LKAudioProcessingAdapter.m +++ b/ios/LKAudioProcessingAdapter.m @@ -1,9 +1,9 @@ #import "LKAudioProcessingAdapter.h" -#import +#import #import @implementation LKAudioProcessingAdapter { - NSMutableArray>* _renderers; + NSMutableArray>* _renderers; NSMutableArray>* _processors; os_unfair_lock _lock; BOOL _isAudioProcessingInitialized; @@ -16,7 +16,7 @@ - (instancetype)init { if (self) { _isAudioProcessingInitialized = NO; _lock = OS_UNFAIR_LOCK_INIT; - _renderers = [[NSMutableArray> alloc] init]; + _renderers = [[NSMutableArray> alloc] init]; _processors = [[NSMutableArray> alloc] init]; } return self; @@ -41,13 +41,13 @@ - (void)removeProcessing:(id _Nonnull)process os_unfair_lock_unlock(&_lock); } -- (void)addAudioRenderer:(nonnull id)renderer { +- (void)addAudioRenderer:(nonnull id)renderer { os_unfair_lock_lock(&_lock); [_renderers addObject:renderer]; os_unfair_lock_unlock(&_lock); } -- (void)removeAudioRenderer:(nonnull id)renderer { +- (void)removeAudioRenderer:(nonnull id)renderer { os_unfair_lock_lock(&_lock); _renderers = [[_renderers filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, @@ -69,7 +69,7 @@ - (void)audioProcessingInitializeWithSampleRate:(size_t)sampleRateHz channels:(s os_unfair_lock_unlock(&_lock); } -- (AVAudioPCMBuffer*)toPCMBuffer:(RTC_OBJC_TYPE(RTCAudioBuffer) *)audioBuffer { +- (AVAudioPCMBuffer*)toPCMBuffer:(LKRTCAudioBuffer *)audioBuffer { AVAudioFormat* format = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatInt16 sampleRate:audioBuffer.frames * 100.0 @@ -93,13 +93,13 @@ - (AVAudioPCMBuffer*)toPCMBuffer:(RTC_OBJC_TYPE(RTCAudioBuffer) *)audioBuffer { return pcmBuffer; } -- (void)audioProcessingProcess:(RTC_OBJC_TYPE(RTCAudioBuffer) *)audioBuffer { +- (void)audioProcessingProcess:(LKRTCAudioBuffer *)audioBuffer { os_unfair_lock_lock(&_lock); for (id processor in _processors) { [processor audioProcessingProcess:audioBuffer]; } - for (id renderer in _renderers) { + for (id renderer in _renderers) { [renderer renderPCMBuffer:[self toPCMBuffer:audioBuffer]]; } os_unfair_lock_unlock(&_lock); diff --git a/ios/LKAudioProcessingManager.m b/ios/LKAudioProcessingManager.m index 083fa778..cd20309d 100644 --- a/ios/LKAudioProcessingManager.m +++ b/ios/LKAudioProcessingManager.m @@ -14,7 +14,7 @@ + (instancetype)sharedInstance { - (instancetype)init { if (self = [super init]) { - _audioProcessingModule = [[RTCDefaultAudioProcessingModule alloc] init]; + _audioProcessingModule = [[LKRTCDefaultAudioProcessingModule alloc] init]; _capturePostProcessingAdapter = [[LKAudioProcessingAdapter alloc] init]; _renderPreProcessingAdapter = [[LKAudioProcessingAdapter alloc] init]; _audioProcessingModule.capturePostProcessingDelegate = _capturePostProcessingAdapter; @@ -23,19 +23,19 @@ - (instancetype)init { return self; } -- (void)addLocalAudioRenderer:(nonnull id)renderer { +- (void)addLocalAudioRenderer:(nonnull id)renderer { [_capturePostProcessingAdapter addAudioRenderer:renderer]; } -- (void)removeLocalAudioRenderer:(nonnull id)renderer { +- (void)removeLocalAudioRenderer:(nonnull id)renderer { [_capturePostProcessingAdapter removeAudioRenderer:renderer]; } -- (void)addRemoteAudioRenderer:(nonnull id)renderer { +- (void)addRemoteAudioRenderer:(nonnull id)renderer { [_renderPreProcessingAdapter addAudioRenderer:renderer]; } -- (void)removeRemoteAudioRenderer:(nonnull id)renderer { +- (void)removeRemoteAudioRenderer:(nonnull id)renderer { [_renderPreProcessingAdapter removeAudioRenderer:renderer]; } diff --git a/ios/LiveKitReactNativeModule.swift b/ios/LiveKitReactNativeModule.swift index e85be6e1..435b339b 100644 --- a/ios/LiveKitReactNativeModule.swift +++ b/ios/LiveKitReactNativeModule.swift @@ -27,7 +27,7 @@ public class LivekitReactNativeModule: RCTEventEmitter { @objc public override init() { super.init() - let config = RTCAudioSessionConfiguration() + let config = LKRTCAudioSessionConfiguration() config.category = AVAudioSession.Category.playAndRecord.rawValue // Gated on compiler version (not iOS deploy target): .allowBluetoothHFP // is only declared in the Xcode 26 / Swift 6.2 SDK, which replaces the @@ -39,7 +39,7 @@ public class LivekitReactNativeModule: RCTEventEmitter { #endif config.mode = AVAudioSession.Mode.videoChat.rawValue - RTCAudioSessionConfiguration.setWebRTC(config) + LKRTCAudioSessionConfiguration.setWebRTC(config) } @objc @@ -49,8 +49,8 @@ public class LivekitReactNativeModule: RCTEventEmitter { @objc public static func setup() { - let videoEncoderFactory = RTCDefaultVideoEncoderFactory() - let simulcastVideoEncoderFactory = RTCVideoEncoderFactorySimulcast(primary: videoEncoderFactory, fallback: videoEncoderFactory) + let videoEncoderFactory = LKRTCDefaultVideoEncoderFactory() + let simulcastVideoEncoderFactory = LKRTCVideoEncoderFactorySimulcast(primary: videoEncoderFactory, fallback: videoEncoderFactory) let options = WebRTCModuleOptions.sharedInstance() options.videoEncoderFactory = simulcastVideoEncoderFactory options.audioProcessingModule = LKAudioProcessingManager.sharedInstance().audioProcessingModule @@ -65,7 +65,7 @@ public class LivekitReactNativeModule: RCTEventEmitter { let defaultOutput = iOSConfig["defaultOutput"] as? String ?? "speaker" - let rtcConfig = RTCAudioSessionConfiguration() + let rtcConfig = LKRTCAudioSessionConfiguration() rtcConfig.category = AVAudioSession.Category.playAndRecord.rawValue if (defaultOutput == "earpiece") { @@ -75,12 +75,12 @@ public class LivekitReactNativeModule: RCTEventEmitter { rtcConfig.categoryOptions = [.allowAirPlay, .allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker] rtcConfig.mode = AVAudioSession.Mode.videoChat.rawValue } - RTCAudioSessionConfiguration.setWebRTC(rtcConfig) + LKRTCAudioSessionConfiguration.setWebRTC(rtcConfig) } @objc(startAudioSession:withRejecter:) public func startAudioSession(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { - let session = RTCAudioSession.sharedInstance() + let session = LKRTCAudioSession.sharedInstance() session.lockForConfiguration() defer { session.unlockForConfiguration() @@ -96,7 +96,7 @@ public class LivekitReactNativeModule: RCTEventEmitter { @objc(stopAudioSession:withRejecter:) public func stopAudioSession(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { - let session = RTCAudioSession.sharedInstance() + let session = LKRTCAudioSession.sharedInstance() session.lockForConfiguration() defer { session.unlockForConfiguration() @@ -149,8 +149,8 @@ public class LivekitReactNativeModule: RCTEventEmitter { @objc(setAppleAudioConfiguration:withResolver:withRejecter:) public func setAppleAudioConfiguration(_ configuration: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { - let session = RTCAudioSession.sharedInstance() - let config = RTCAudioSessionConfiguration.webRTC() + let session = LKRTCAudioSession.sharedInstance() + let config = LKRTCAudioSessionConfiguration.webRTC() let appleAudioCategory = configuration["audioCategory"] as? String let appleAudioCategoryOptions = configuration["audioCategoryOptions"] as? [String] diff --git a/ios/LivekitReactNative.m b/ios/LivekitReactNative.m index b857eca6..ea7b21eb 100644 --- a/ios/LivekitReactNative.m +++ b/ios/LivekitReactNative.m @@ -6,8 +6,8 @@ @implementation LivekitReactNative +(void)setup { - RTCDefaultVideoEncoderFactory *videoEncoderFactory = [[RTCDefaultVideoEncoderFactory alloc] init]; - RTCVideoEncoderFactorySimulcast *simulcastVideoEncoderFactory = [[RTCVideoEncoderFactorySimulcast alloc] initWithPrimary:videoEncoderFactory fallback:videoEncoderFactory]; + LKRTCDefaultVideoEncoderFactory *videoEncoderFactory = [[LKRTCDefaultVideoEncoderFactory alloc] init]; + LKRTCVideoEncoderFactorySimulcast *simulcastVideoEncoderFactory = [[LKRTCVideoEncoderFactorySimulcast alloc] initWithPrimary:videoEncoderFactory fallback:videoEncoderFactory]; WebRTCModuleOptions *options = [WebRTCModuleOptions sharedInstance]; options.videoEncoderFactory = simulcastVideoEncoderFactory; options.audioProcessingModule = LKAudioProcessingManager.sharedInstance.audioProcessingModule; diff --git a/ios/audio/AudioProcessing.swift b/ios/audio/AudioProcessing.swift index f5cc4ed3..19910d85 100644 --- a/ios/audio/AudioProcessing.swift +++ b/ios/audio/AudioProcessing.swift @@ -25,7 +25,7 @@ public struct AudioLevel { public let peak: Float } -public extension RTCAudioBuffer { +public extension LKRTCAudioBuffer { /// Convert to AVAudioPCMBuffer Int16 format. @objc func toAVAudioPCMBuffer() -> AVAudioPCMBuffer? { diff --git a/ios/audio/AudioRendererManager.swift b/ios/audio/AudioRendererManager.swift index fc7bab49..8ee80a52 100644 --- a/ios/audio/AudioRendererManager.swift +++ b/ios/audio/AudioRendererManager.swift @@ -3,14 +3,14 @@ import livekit_react_native_webrtc @objc public class AudioRendererManager: NSObject { private let bridge: RCTBridge - public private(set) var renderers: [String: RTCAudioRenderer] = [:] + public private(set) var renderers: [String: LKRTCAudioRenderer] = [:] init(bridge: RCTBridge) { self.bridge = bridge } @objc - public func registerRenderer(_ audioRenderer: RTCAudioRenderer) -> String { + public func registerRenderer(_ audioRenderer: LKRTCAudioRenderer) -> String { let reactTag = NSUUID().uuidString self.renderers[reactTag] = audioRenderer return reactTag @@ -22,14 +22,14 @@ public class AudioRendererManager: NSObject { } @objc - public func unregisterRenderer(_ audioRenderer: RTCAudioRenderer) { + public func unregisterRenderer(_ audioRenderer: LKRTCAudioRenderer) { self.renderers = self.renderers.filter({ $0.value !== audioRenderer }) } @objc - public func attach(renderer: RTCAudioRenderer, pcId: NSNumber, trackId: String) { + public func attach(renderer: LKRTCAudioRenderer, pcId: NSNumber, trackId: String) { let webrtcModule = self.bridge.module(for: WebRTCModule.self) as! WebRTCModule - guard let track = webrtcModule.track(forId: trackId, pcId: pcId) as? RTCAudioTrack + guard let track = webrtcModule.track(forId: trackId, pcId: pcId) as? LKRTCAudioTrack else { lklog("couldn't find audio track: pcId: \(pcId), trackId: \(trackId)") return @@ -54,9 +54,9 @@ public class AudioRendererManager: NSObject { } @objc - public func detach(renderer: RTCAudioRenderer, pcId: NSNumber, trackId: String) { + public func detach(renderer: LKRTCAudioRenderer, pcId: NSNumber, trackId: String) { let webrtcModule = self.bridge.module(for: WebRTCModule.self) as! WebRTCModule - guard let track = webrtcModule.track(forId: trackId, pcId: pcId) as? RTCAudioTrack + guard let track = webrtcModule.track(forId: trackId, pcId: pcId) as? LKRTCAudioTrack else { lklog("couldn't find audio track: pcId: \(pcId), trackId: \(trackId)") return diff --git a/ios/audio/AudioSinkRenderer.swift b/ios/audio/AudioSinkRenderer.swift index c5d99c6a..ded72d8c 100644 --- a/ios/audio/AudioSinkRenderer.swift +++ b/ios/audio/AudioSinkRenderer.swift @@ -31,7 +31,7 @@ public class AudioSinkRenderer: BaseAudioSinkRenderer { } } -public class BaseAudioSinkRenderer: NSObject, RTCAudioRenderer { +public class BaseAudioSinkRenderer: NSObject, LKRTCAudioRenderer { public override init() { super.init() diff --git a/ios/audio/MultibandVolumeAudioRenderer.swift b/ios/audio/MultibandVolumeAudioRenderer.swift index 64f9ccf6..8247415f 100644 --- a/ios/audio/MultibandVolumeAudioRenderer.swift +++ b/ios/audio/MultibandVolumeAudioRenderer.swift @@ -34,7 +34,7 @@ public class MultibandVolumeAudioRenderer: BaseMultibandVolumeAudioRenderer { } -public class BaseMultibandVolumeAudioRenderer: NSObject, RTCAudioRenderer { +public class BaseMultibandVolumeAudioRenderer: NSObject, LKRTCAudioRenderer { private let frameInterval: Int private var skippedFrames = 0 private let audioProcessor: AudioVisualizeProcessor diff --git a/ios/audio/VolumeAudioRenderer.swift b/ios/audio/VolumeAudioRenderer.swift index f2506f28..dca4fabd 100644 --- a/ios/audio/VolumeAudioRenderer.swift +++ b/ios/audio/VolumeAudioRenderer.swift @@ -25,7 +25,7 @@ public class VolumeAudioRenderer: BaseVolumeAudioRenderer { } } -public class BaseVolumeAudioRenderer: NSObject, RTCAudioRenderer { +public class BaseVolumeAudioRenderer: NSObject, LKRTCAudioRenderer { private let frameInterval: Int private var skippedFrames = 0 public init(intervalMs: Double = 30) { diff --git a/package.json b/package.json index 9e3d5c2c..f1ed7501 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.35.0", "@livekit/changesets-changelog-github": "^0.0.4", - "@livekit/react-native-webrtc": "^144.1.2", + "@livekit/react-native-webrtc": "^144.2.0", "@react-native/babel-preset": "0.83.0", "@react-native/eslint-config": "0.83.0", "@types/fastestsmallesttextencoderdecoder": "^1.0.0", @@ -93,7 +93,7 @@ "typescript": "^5.9.2" }, "peerDependencies": { - "@livekit/react-native-webrtc": "^144.1.2", + "@livekit/react-native-webrtc": "^144.2.0", "livekit-client": "^2.19.0", "react": "*", "react-native": "*" diff --git a/src/audio/AudioManager.ts b/src/audio/AudioManager.ts index 3eb5dd5a..9f42efe8 100644 --- a/src/audio/AudioManager.ts +++ b/src/audio/AudioManager.ts @@ -1,5 +1,8 @@ import { Platform } from 'react-native'; -import AudioSession, { type AppleAudioConfiguration } from './AudioSession'; +import AudioSession, { + type AppleAudioCategoryOption, + type AppleAudioConfiguration, +} from './AudioSession'; import { log } from '../logger'; import { audioDeviceModuleEvents, @@ -9,6 +12,13 @@ import { export type AudioEngineConfigurationState = { isPlayoutEnabled: boolean; isRecordingEnabled: boolean; + /** + * Whether Apple Voice Processing I/O is running for this engine state. The + * `voiceChat`/`videoChat` modes engage iOS's call-tuned speaker gain, which + * only VPIO compensates for, so a configuration derived for recording should + * account for this. + */ + isVoiceProcessingEnabled: boolean; preferSpeakerOutput: boolean; }; @@ -17,6 +27,16 @@ export type AudioEngineConfigurationState = { */ export type IOSAudioSessionPolicy = { recording: AppleAudioConfiguration; + /** + * Recording configuration to apply while Apple Voice Processing I/O is off, + * for example after `AudioDeviceModule.setVoiceProcessingEnabled(false)`. + * A policy whose {@link recording} config uses `voiceChat`/`videoChat` should + * set a `default`-mode variant here, otherwise remote audio plays back + * noticeably quieter without VPIO to compensate for the call-tuned gain. + * Defaults to {@link recording}, so a policy that does not set this behaves + * exactly as before. + */ + recordingWithoutVoiceProcessing?: AppleAudioConfiguration; playout: AppleAudioConfiguration; /** * Whether to deactivate the audio session when both playout and recording @@ -157,6 +177,19 @@ function setupNativePolicyPath( getDefaultAppleAudioConfigurationForAudioState({ isPlayoutEnabled: true, isRecordingEnabled: true, + isVoiceProcessingEnabled: true, + preferSpeakerOutput, + }), + // A policy that supplies `recording` but not the no-voice-processing variant + // keeps using its own recording config for both, rather than silently picking + // up an SDK default it never asked for. + recordingWithoutVoiceProcessing: + policy?.recordingWithoutVoiceProcessing ?? + policy?.recording ?? + getDefaultAppleAudioConfigurationForAudioState({ + isPlayoutEnabled: true, + isRecordingEnabled: true, + isVoiceProcessingEnabled: false, preferSpeakerOutput, }), playout: @@ -164,6 +197,7 @@ function setupNativePolicyPath( getDefaultAppleAudioConfigurationForAudioState({ isPlayoutEnabled: true, isRecordingEnabled: false, + isVoiceProcessingEnabled: false, preferSpeakerOutput, }), deactivateOnStop: policy?.deactivateOnStop ?? true, @@ -198,6 +232,7 @@ function setupCustomCallbackPath( let audioEngineState: AudioEngineConfigurationState = { isPlayoutEnabled: false, isRecordingEnabled: false, + isVoiceProcessingEnabled: false, preferSpeakerOutput, }; @@ -226,14 +261,17 @@ function setupCustomCallbackPath( const handleEngineStateUpdate = async ({ isPlayoutEnabled, isRecordingEnabled, + isVoiceProcessingEnabled, }: { isPlayoutEnabled: boolean; isRecordingEnabled: boolean; + isVoiceProcessingEnabled: boolean; }) => { const oldState = audioEngineState; const newState: AudioEngineConfigurationState = { isPlayoutEnabled, isRecordingEnabled, + isVoiceProcessingEnabled, preferSpeakerOutput: audioEngineState.preferSpeakerOutput, }; @@ -269,16 +307,59 @@ function setupCustomCallbackPath( }); } +// Category options for the duplex (playAndRecord) presets. Mirrors +// `playAndRecordOptions` / `playAndRecordSpeakerOptions` in the Swift SDK +// (Sources/LiveKit/Types/AudioSessionConfiguration.swift). +const playAndRecordOptions: AppleAudioCategoryOption[] = [ + 'mixWithOthers', + 'allowBluetooth', + 'allowBluetoothA2DP', + 'allowAirPlay', +]; + +// Explicit speaker preference for the speaker preset. The chat modes imply a +// speaker route, but iOS may rewrite the mode when Voice Processing I/O is +// instantiated (the Swift SDK observed it switching videoChat to voiceChat, +// which routes to the receiver), and `default` mode routes to the receiver +// without it. +const playAndRecordSpeakerOptions: AppleAudioCategoryOption[] = [ + ...playAndRecordOptions, + 'defaultToSpeaker', +]; + // Kept in sync with `getDefaultAppleAudioConfigurationForMode` in // `./AudioManagerLegacy.ts`. If you change the defaults in one place, -// update the other so the legacy path and the new path stay aligned. +// update the other so the legacy path and the new path stay aligned. The +// legacy function has no voice-processing input, so it matches only the +// voice-processing-enabled results here. function getDefaultAppleAudioConfigurationForAudioState( configurationState: AudioEngineConfigurationState ): AppleAudioConfiguration { if (configurationState.isRecordingEnabled) { + const audioCategoryOptions = configurationState.preferSpeakerOutput + ? [...playAndRecordSpeakerOptions] + : [...playAndRecordOptions]; + + if (!configurationState.isVoiceProcessingEnabled) { + // iOS applies a reduced, call-tuned speaker gain while capture is active + // under voiceChat/videoChat. Apple Voice Processing I/O adds a loudness + // stage that compensates for it; with VPIO off (WebRTC's own software + // processing) nothing does, and remote audio plays back noticeably + // quieter. The default mode keeps media gain. + // + // Mirrors playAndRecordSpeakerMedia / playAndRecordReceiverMedia in the + // Swift SDK (Sources/LiveKit/Types/AudioSessionConfiguration.swift). + return { + audioCategory: 'playAndRecord', + audioCategoryOptions, + audioMode: 'default', + }; + } + + // Mirrors playAndRecordSpeaker / playAndRecordReceiver in the Swift SDK. return { audioCategory: 'playAndRecord', - audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'], + audioCategoryOptions, audioMode: configurationState.preferSpeakerOutput ? 'videoChat' : 'voiceChat', diff --git a/src/audio/AudioManagerLegacy.ts b/src/audio/AudioManagerLegacy.ts index 8483e61c..a0bb55c1 100644 --- a/src/audio/AudioManagerLegacy.ts +++ b/src/audio/AudioManagerLegacy.ts @@ -85,6 +85,8 @@ export function useIOSAudioManagement( * Kept in sync with `getDefaultAppleAudioConfigurationForAudioState` in * `./AudioManager.ts`. If you change the defaults in one place, update the * other so the legacy path and the new path produce the same configuration. + * `AudioTrackState` carries no voice-processing state, so this matches the + * voice-processing-enabled results of that function only. */ export function getDefaultAppleAudioConfigurationForMode( mode: AudioTrackState, @@ -99,7 +101,20 @@ export function getDefaultAppleAudioConfigurationForMode( } else if (mode === 'localAndRemote' || mode === 'localOnly') { return { audioCategory: 'playAndRecord', - audioCategoryOptions: ['allowBluetooth', 'mixWithOthers'], + audioCategoryOptions: preferSpeakerOutput + ? [ + 'mixWithOthers', + 'allowBluetooth', + 'allowBluetoothA2DP', + 'allowAirPlay', + 'defaultToSpeaker', + ] + : [ + 'mixWithOthers', + 'allowBluetooth', + 'allowBluetoothA2DP', + 'allowAirPlay', + ], audioMode: preferSpeakerOutput ? 'videoChat' : 'voiceChat', }; } diff --git a/yarn.lock b/yarn.lock index d8edbe92..1663f733 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3612,15 +3612,15 @@ __metadata: languageName: node linkType: hard -"@livekit/react-native-webrtc@npm:^144.1.2": - version: 144.1.2 - resolution: "@livekit/react-native-webrtc@npm:144.1.2" +"@livekit/react-native-webrtc@npm:^144.2.0": + version: 144.2.0 + resolution: "@livekit/react-native-webrtc@npm:144.2.0" dependencies: base64-js: "npm:1.5.1" debug: "npm:4.3.4" peerDependencies: react-native: ">=0.60.0" - checksum: 10/ed4a3594c060404c89020568f0602ad48c51d9738b5479ef9fdecd330ba49349b98afa5b4db7c9faea394cc62edea993a4e8b39cde1181e79966a1c1da65a97a + checksum: 10/fb2db76db47d976cd1ebfff16eef7ba49123a6b0c7af76c6cde671889276c7a5c8c6ed43d4402cf7412ff9905dbf4d986eaf029dd0190cd1bb6fb1897075dd72 languageName: node linkType: hard @@ -3635,7 +3635,7 @@ __metadata: "@livekit/changesets-changelog-github": "npm:^0.0.4" "@livekit/components-react": "npm:^2.9.17" "@livekit/mutex": "npm:^1.1.1" - "@livekit/react-native-webrtc": "npm:^144.1.2" + "@livekit/react-native-webrtc": "npm:^144.2.0" "@react-native/babel-preset": "npm:0.83.0" "@react-native/eslint-config": "npm:0.83.0" "@types/fastestsmallesttextencoderdecoder": "npm:^1.0.0" @@ -3662,7 +3662,7 @@ __metadata: web-streams-polyfill: "npm:^4.3.0" well-known-symbols: "npm:^4.1.0" peerDependencies: - "@livekit/react-native-webrtc": ^144.1.2 + "@livekit/react-native-webrtc": ^144.2.0 livekit-client: ^2.19.0 react: "*" react-native: "*" @@ -9538,7 +9538,7 @@ __metadata: "@babel/core": "npm:^7.25.2" "@babel/preset-env": "npm:^7.25.3" "@babel/runtime": "npm:^7.25.0" - "@livekit/react-native-webrtc": "npm:^144.1.2" + "@livekit/react-native-webrtc": "npm:^144.2.0" "@react-native-async-storage/async-storage": "npm:^1.17.10" "@react-native-community/cli": "npm:20.0.0" "@react-native-community/cli-platform-android": "npm:20.0.0"