@@ -136,11 +136,12 @@ static void GetMainContext()
136136
137137#if UNITY_ANDROID && ! UNITY_EDITOR
138138 /// <summary>
139- /// Get the Android application context as a raw jobject pointer.
140- /// This is passed to the native library for WebRTC audio initialization.
139+ /// Get the Android application context.
140+ /// Passed to the native library (as a raw jobject) for WebRTC audio initialization,
141+ /// and to the managed ContextUtils fallback in InitializeSdk.
141142 /// </summary>
142- /// <returns>IntPtr to the application context jobject , or IntPtr.Zero on failure</returns>
143- private static IntPtr GetAndroidApplicationContext ( )
143+ /// <returns>The application context, or null on failure. Caller owns disposal. </returns>
144+ private static AndroidJavaObject GetAndroidApplicationContext ( )
144145 {
145146 try
146147 {
@@ -151,7 +152,7 @@ private static IntPtr GetAndroidApplicationContext()
151152 if ( currentActivity == null )
152153 {
153154 Utils . Error ( "FFIServer - Failed to get Unity currentActivity" ) ;
154- return IntPtr . Zero ;
155+ return null ;
155156 }
156157
157158 // Get the application context from the activity
@@ -160,18 +161,15 @@ private static IntPtr GetAndroidApplicationContext()
160161 if ( applicationContext == null )
161162 {
162163 Utils . Error ( "FFIServer - Failed to get Android applicationContext" ) ;
163- return IntPtr . Zero ;
164+ return null ;
164165 }
165166
166- // Get the raw jobject pointer
167- // Note: We don't dispose the applicationContext here because we're passing
168- // the raw pointer to native code. The native code will create its own global ref.
169- return applicationContext . GetRawObject ( ) ;
167+ return applicationContext ;
170168 }
171169 catch ( System . Exception e )
172170 {
173171 Utils . Error ( $ "FFIServer - Failed to get Android application context: { e . Message } ") ;
174- return IntPtr . Zero ;
172+ return null ;
175173 }
176174 }
177175#endif
@@ -194,15 +192,26 @@ private static void InitializeSdk()
194192 try
195193 {
196194 IntPtr javaVmPtr = AndroidJNI . GetJavaVM ( ) ;
197- IntPtr contextPtr = GetAndroidApplicationContext ( ) ;
195+ using var applicationContext = GetAndroidApplicationContext ( ) ;
198196
199- if ( javaVmPtr != IntPtr . Zero && contextPtr != IntPtr . Zero )
197+ if ( javaVmPtr != IntPtr . Zero && applicationContext != null )
200198 {
201- bool contextInitialized = NativeMethods . LiveKitInitializeAndroidContext ( javaVmPtr , contextPtr ) ;
199+ // Disposing applicationContext after these calls is safe: the native side only
200+ // uses the jobject within the call, and Java's ContextUtils holds the Context
201+ // in a static field of its own.
202+ bool contextInitialized = NativeMethods . LiveKitInitializeAndroidContext (
203+ javaVmPtr , applicationContext . GetRawObject ( ) ) ;
202204 if ( ! contextInitialized )
203205 {
204- // JVM init still succeeded; only PlatformAudio won't work
205- Utils . Error ( "FFIServer - Android context init failed; PlatformAudio will not work" ) ;
206+ // The native init looks up ContextUtils with JNIEnv::FindClass, which fails
207+ // on Unity's main thread: it is a native thread with no Java frames, so
208+ // FindClass falls back to the system class loader, which cannot see APK
209+ // classes. Initialize the shaded ContextUtils through Unity's interop
210+ // instead — it resolves classes via the application class loader. Without
211+ // this, the first audio device use NPEs and WebRTC aborts the process.
212+ using var contextUtils = new AndroidJavaClass ( "livekit.org.webrtc.ContextUtils" ) ;
213+ contextUtils . CallStatic ( "initialize" , applicationContext ) ;
214+ Utils . Info ( "FFIServer - ContextUtils initialized via managed fallback" ) ;
206215 }
207216 }
208217 else
0 commit comments