diff --git a/Runtime/Scripts/Internal/FFI/FFIClient.cs b/Runtime/Scripts/Internal/FFI/FFIClient.cs index 8d360ea6..213ee1f4 100644 --- a/Runtime/Scripts/Internal/FFI/FFIClient.cs +++ b/Runtime/Scripts/Internal/FFI/FFIClient.cs @@ -136,11 +136,12 @@ static void GetMainContext() #if UNITY_ANDROID && !UNITY_EDITOR /// - /// Get the Android application context as a raw jobject pointer. - /// This is passed to the native library for WebRTC audio initialization. + /// Get the Android application context. + /// Passed to the native library (as a raw jobject) for WebRTC audio initialization, + /// and to the managed ContextUtils fallback in InitializeSdk. /// - /// IntPtr to the application context jobject, or IntPtr.Zero on failure - private static IntPtr GetAndroidApplicationContext() + /// The application context, or null on failure. Caller owns disposal. + private static AndroidJavaObject GetAndroidApplicationContext() { try { @@ -151,7 +152,7 @@ private static IntPtr GetAndroidApplicationContext() if (currentActivity == null) { Utils.Error("FFIServer - Failed to get Unity currentActivity"); - return IntPtr.Zero; + return null; } // Get the application context from the activity @@ -160,18 +161,15 @@ private static IntPtr GetAndroidApplicationContext() if (applicationContext == null) { Utils.Error("FFIServer - Failed to get Android applicationContext"); - return IntPtr.Zero; + return null; } - // Get the raw jobject pointer - // Note: We don't dispose the applicationContext here because we're passing - // the raw pointer to native code. The native code will create its own global ref. - return applicationContext.GetRawObject(); + return applicationContext; } catch (System.Exception e) { Utils.Error($"FFIServer - Failed to get Android application context: {e.Message}"); - return IntPtr.Zero; + return null; } } #endif @@ -194,15 +192,26 @@ private static void InitializeSdk() try { IntPtr javaVmPtr = AndroidJNI.GetJavaVM(); - IntPtr contextPtr = GetAndroidApplicationContext(); + using var applicationContext = GetAndroidApplicationContext(); - if (javaVmPtr != IntPtr.Zero && contextPtr != IntPtr.Zero) + if (javaVmPtr != IntPtr.Zero && applicationContext != null) { - bool contextInitialized = NativeMethods.LiveKitInitializeAndroidContext(javaVmPtr, contextPtr); + // Disposing applicationContext after these calls is safe: the native side only + // uses the jobject within the call, and Java's ContextUtils holds the Context + // in a static field of its own. + bool contextInitialized = NativeMethods.LiveKitInitializeAndroidContext( + javaVmPtr, applicationContext.GetRawObject()); if (!contextInitialized) { - // JVM init still succeeded; only PlatformAudio won't work - Utils.Error("FFIServer - Android context init failed; PlatformAudio will not work"); + // The native init looks up ContextUtils with JNIEnv::FindClass, which fails + // on Unity's main thread: it is a native thread with no Java frames, so + // FindClass falls back to the system class loader, which cannot see APK + // classes. Initialize the shaded ContextUtils through Unity's interop + // instead — it resolves classes via the application class loader. Without + // this, the first audio device use NPEs and WebRTC aborts the process. + using var contextUtils = new AndroidJavaClass("livekit.org.webrtc.ContextUtils"); + contextUtils.CallStatic("initialize", applicationContext); + Utils.Info("FFIServer - ContextUtils initialized via managed fallback"); } } else