Skip to content

Commit ba6c543

Browse files
MaxHeimbrockclaude
andauthored
Platform audio for Unity (#293)
* All platform audio changes before vacation * Cleaned up no nop for mobile platforms * Set liblivekit_ffi.a for iOS to link only into iOS * Add PlatformAudio unit and integration tests Mirror the C++ PlatformAudio test suites in the Unity Test Framework: - EditMode (pure-managed, always run): AudioProcessingOptions defaults, AudioDevice struct, PlatformAudioSource null-arg guard. - PlayMode unit (ADM-backed, no server): create source/track, custom options, enumerate + select-by-GUID, out-of-range index, start/stop recording. Skips via Assert.Ignore when no platform ADM is available. - PlayMode E2E (Category=E2E): publish/unpublish round-trip, multiple sources from one manager, and media flow verified via an InboundRtp audio stat (the batch-mode-friendly substitute for the C++ frame callback). Shared PlatformAudioTestHelper.TryCreateOrIgnore mirrors GTEST_SKIP for environments without an ADM. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Remove some example usage * More docs on platform audio --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 28a34b3 commit ba6c543

26 files changed

Lines changed: 1609 additions & 16 deletions

Runtime/Plugins/iOS.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2024 LiveKit, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import <AVFoundation/AVFoundation.h>
18+
19+
extern "C" {
20+
21+
/// Configures the iOS audio session for VoIP/WebRTC use.
22+
/// This sets AVAudioSessionCategoryPlayAndRecord with VoiceChat mode,
23+
/// which enables the VPIO (Voice Processing IO) AudioUnit for:
24+
/// - Hardware echo cancellation (AEC)
25+
/// - Automatic gain control (AGC)
26+
/// - Noise suppression (NS)
27+
///
28+
/// Call this before creating PlatformAudio to ensure WebRTC can
29+
/// properly initialize the microphone and speaker.
30+
void LiveKit_ConfigureAudioSessionForVoIP() {
31+
AVAudioSession* session = [AVAudioSession sharedInstance];
32+
NSError* error = nil;
33+
34+
// Configure for VoIP with echo cancellation
35+
BOOL success = [session setCategory:AVAudioSessionCategoryPlayAndRecord
36+
mode:AVAudioSessionModeVoiceChat
37+
options:AVAudioSessionCategoryOptionDefaultToSpeaker |
38+
AVAudioSessionCategoryOptionAllowBluetooth |
39+
AVAudioSessionCategoryOptionAllowBluetoothA2DP
40+
error:&error];
41+
42+
if (!success || error) {
43+
NSLog(@"LiveKit: Failed to configure VoIP audio session: %@", error.localizedDescription);
44+
return;
45+
}
46+
47+
// Activate the audio session
48+
success = [session setActive:YES error:&error];
49+
if (!success || error) {
50+
NSLog(@"LiveKit: Failed to activate audio session: %@", error.localizedDescription);
51+
return;
52+
}
53+
54+
NSLog(@"LiveKit: Audio session configured for VoIP (PlayAndRecord + VoiceChat mode)");
55+
}
56+
57+
/// Restores the audio session to the default ambient category.
58+
/// Call this when PlatformAudio is disposed if you want to restore
59+
/// the original audio behavior.
60+
void LiveKit_RestoreDefaultAudioSession() {
61+
AVAudioSession* session = [AVAudioSession sharedInstance];
62+
NSError* error = nil;
63+
64+
[session setCategory:AVAudioSessionCategoryAmbient error:&error];
65+
if (error) {
66+
NSLog(@"LiveKit: Failed to restore default audio session: %@", error.localizedDescription);
67+
}
68+
}
69+
70+
}

Runtime/Plugins/iOS/LiveKitAudioSession.mm.meta

Lines changed: 42 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:07f507aab5a0a241d674b81460dee4a2d99b0996debb3d07a92bf41f63be783d
3+
size 564011360

Runtime/Plugins/iOS/liblivekit_ffi.a.meta

Lines changed: 82 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Runtime/Scripts/Internal/FFIClient.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,48 @@ static void GetMainContext()
133133
Utils.Debug("Main Context created");
134134
}
135135

136+
#if UNITY_ANDROID && !UNITY_EDITOR
137+
/// <summary>
138+
/// Get the Android application context as a raw jobject pointer.
139+
/// This is passed to the native library for WebRTC audio initialization.
140+
/// </summary>
141+
/// <returns>IntPtr to the application context jobject, or IntPtr.Zero on failure</returns>
142+
private static IntPtr GetAndroidApplicationContext()
143+
{
144+
try
145+
{
146+
// Get the Unity activity
147+
using var unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
148+
using var currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
149+
150+
if (currentActivity == null)
151+
{
152+
Utils.Error("FFIServer - Failed to get Unity currentActivity");
153+
return IntPtr.Zero;
154+
}
155+
156+
// Get the application context from the activity
157+
var applicationContext = currentActivity.Call<AndroidJavaObject>("getApplicationContext");
158+
159+
if (applicationContext == null)
160+
{
161+
Utils.Error("FFIServer - Failed to get Android applicationContext");
162+
return IntPtr.Zero;
163+
}
164+
165+
// Get the raw jobject pointer
166+
// Note: We don't dispose the applicationContext here because we're passing
167+
// the raw pointer to native code. The native code will create its own global ref.
168+
return applicationContext.GetRawObject();
169+
}
170+
catch (System.Exception e)
171+
{
172+
Utils.Error($"FFIServer - Failed to get Android application context: {e.Message}");
173+
return IntPtr.Zero;
174+
}
175+
}
176+
#endif
177+
136178
private static void InitializeSdk()
137179
{
138180
#if NO_LIVEKIT_MODE
@@ -145,6 +187,34 @@ private static void InitializeSdk()
145187
const bool captureLogs = false;
146188
#endif
147189

190+
#if UNITY_ANDROID && !UNITY_EDITOR
191+
// Initialize Android WebRTC before the main FFI initialization.
192+
// This initializes the JVM and ContextUtils (required for PlatformAudio).
193+
try
194+
{
195+
IntPtr javaVmPtr = AndroidJNI.GetJavaVM();
196+
IntPtr contextPtr = GetAndroidApplicationContext();
197+
198+
if (javaVmPtr != IntPtr.Zero && contextPtr != IntPtr.Zero)
199+
{
200+
bool contextInitialized = NativeMethods.LiveKitInitializeAndroidContext(javaVmPtr, contextPtr);
201+
if (!contextInitialized)
202+
{
203+
// JVM init still succeeded; only PlatformAudio won't work
204+
Utils.Error("FFIServer - Android context init failed; PlatformAudio will not work");
205+
}
206+
}
207+
else
208+
{
209+
Utils.Error("FFIServer - Failed to get JavaVM or context for Android init");
210+
}
211+
}
212+
catch (System.Exception e)
213+
{
214+
Utils.Error($"FFIServer - Android initialization failed: {e.Message}");
215+
}
216+
#endif
217+
148218
var sdkVersion = PackageVersion.Get();
149219
NativeMethods.LiveKitInitialize(FFICallback, captureLogs, "unity", sdkVersion);
150220

Runtime/Scripts/Internal/FFIClients/FfiRequestExtensions.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,25 @@ public static void Inject<T>(this FfiRequest ffiRequest, T request)
152152
case RemixAndResampleRequest remixAndResampleRequest:
153153
ffiRequest.RemixAndResample = remixAndResampleRequest;
154154
break;
155+
// PlatformAudio
156+
case NewPlatformAudioRequest newPlatformAudioRequest:
157+
ffiRequest.NewPlatformAudio = newPlatformAudioRequest;
158+
break;
159+
case GetAudioDevicesRequest getAudioDevicesRequest:
160+
ffiRequest.GetAudioDevices = getAudioDevicesRequest;
161+
break;
162+
case SetRecordingDeviceRequest setRecordingDeviceRequest:
163+
ffiRequest.SetRecordingDevice = setRecordingDeviceRequest;
164+
break;
165+
case SetPlayoutDeviceRequest setPlayoutDeviceRequest:
166+
ffiRequest.SetPlayoutDevice = setPlayoutDeviceRequest;
167+
break;
168+
case StartRecordingRequest startRecordingRequest:
169+
ffiRequest.StartRecording = startRecordingRequest;
170+
break;
171+
case StopRecordingRequest stopRecordingRequest:
172+
ffiRequest.StopRecording = stopRecordingRequest;
173+
break;
155174
case LocalTrackMuteRequest localTrackMuteRequest:
156175
ffiRequest.LocalTrackMute = localTrackMuteRequest;
157176
break;

Runtime/Scripts/Internal/NativeMethods.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,19 @@ internal static class NativeMethods
2525

2626
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "livekit_ffi_initialize")]
2727
internal extern static FfiHandleId LiveKitInitialize(FFICallbackDelegate cb, bool captureLogs, string sdk, string sdkVersion);
28+
29+
#if UNITY_ANDROID && !UNITY_EDITOR
30+
/// <summary>
31+
/// Initialize Android WebRTC with the application context.
32+
/// This initializes both the JVM and ContextUtils, which is required for
33+
/// Android audio (microphone/speaker) to work via PlatformAudio.
34+
/// </summary>
35+
/// <param name="javaVmPtr">Pointer to the JavaVM</param>
36+
/// <param name="contextPtr">The Android application context (jobject)</param>
37+
/// <returns>true if context initialization succeeded, false otherwise.
38+
/// Note: JVM initialization happens regardless of return value.</returns>
39+
[DllImport(Lib, CallingConvention = CallingConvention.Cdecl, EntryPoint = "livekit_ffi_initialize_android_context")]
40+
internal extern static bool LiveKitInitializeAndroidContext(IntPtr javaVmPtr, IntPtr contextPtr);
41+
#endif
2842
}
2943
}

0 commit comments

Comments
 (0)