From b71873f2a516b29f034ff9dbb80eeff0e9712fec Mon Sep 17 00:00:00 2001 From: komaruworld Date: Sat, 5 Sep 2026 20:06:02 +0300 Subject: [PATCH] Maybe fix vc --- include/jnivm/jnivm.h | 46 +++ .../mocktail/audio/webrtc_jni_audio_bridge.h | 6 +- src/audio/webrtc_jni_audio_bridge.cc | 79 +++++ src/jnivm/jnivm.cc | 269 ++++++++++++++- tests/CMakeLists.txt | 10 + tests/fmod_jni_audio_bridge_test.cc | 305 +++++++++++++++++- tests/jnivm_webrtc_audio_manager_test.cc | 297 +++++++++++++++++ 7 files changed, 1004 insertions(+), 8 deletions(-) create mode 100644 tests/jnivm_webrtc_audio_manager_test.cc diff --git a/include/jnivm/jnivm.h b/include/jnivm/jnivm.h index 535e370..e8f5dd1 100644 --- a/include/jnivm/jnivm.h +++ b/include/jnivm/jnivm.h @@ -82,6 +82,32 @@ struct FmodAudioDeviceCallbacks { void (*shutdown)(void *context) = nullptr; }; +// Parameters passed by WebRtcAudioManager's constructor to the guest's +// nativeCacheAudioParameters(IIIZZZZZZZIIJ)V. Buffer sizes are PCM frames, +// not bytes. Querying them must not open a microphone. +struct WebRtcAudioManagerParameters { + int sample_rate_hz = 0; + int output_channels = 0; + int input_channels = 0; + bool hardware_aec = false; + bool hardware_agc = false; + bool hardware_ns = false; + bool low_latency_output = false; + bool low_latency_input = false; + bool pro_audio = false; + bool aaudio = false; + int output_buffer_size_frames = 0; + int input_buffer_size_frames = 0; +}; + +struct WebRtcAudioManagerCallbacks { + bool (*get_parameters)(void* context, + WebRtcAudioManagerParameters* parameters) = nullptr; + bool (*init)(void* context, const void* identity) = nullptr; + void (*dispose)(void* context, const void* identity) = nullptr; + void (*set_microphone_mute)(void* context, bool muted) = nullptr; +}; + using WebRtcAudioRecordDataCallback = void (*)(void *context, const void *identity, std::size_t size_bytes); @@ -290,6 +316,18 @@ public: const std::uint8_t *data, std::size_t size); bool DispatchFmodAudioDeviceClose(const void *identity); + void SetWebRtcAudioManagerCallbacks( + std::shared_ptr context, + const WebRtcAudioManagerCallbacks& callbacks); + void ClearWebRtcAudioManagerCallbacks(); + void RegisterWebRtcAudioManagerNative(const char* name, const char* signature, + void* function); + bool DispatchWebRtcAudioManagerConstruct(jobject manager, + jlong native_audio_manager); + bool DispatchWebRtcAudioManagerInit(jobject manager); + void DispatchWebRtcAudioManagerDispose(jobject manager); + void DispatchWebRtcAudioManagerMicrophoneMute(jobject manager, bool muted); + void SetWebRtcAudioRecordCallbacks(std::shared_ptr context, const WebRtcAudioRecordCallbacks &callbacks); @@ -432,6 +470,14 @@ private: mutable std::mutex fmod_audio_device_mutex_; FmodAudioDeviceBinding fmod_audio_device_binding_; + struct WebRtcAudioManagerBinding { + std::shared_ptr context; + WebRtcAudioManagerCallbacks callbacks; + }; + mutable std::mutex webrtc_audio_manager_mutex_; + WebRtcAudioManagerBinding webrtc_audio_manager_binding_; + void* webrtc_cache_audio_parameters_native_ = nullptr; + struct WebRtcAudioRecordBinding { std::shared_ptr context; WebRtcAudioRecordCallbacks callbacks; diff --git a/include/mocktail/audio/webrtc_jni_audio_bridge.h b/include/mocktail/audio/webrtc_jni_audio_bridge.h index 1b294eb..f812ac6 100644 --- a/include/mocktail/audio/webrtc_jni_audio_bridge.h +++ b/include/mocktail/audio/webrtc_jni_audio_bridge.h @@ -9,8 +9,10 @@ class VM; namespace mocktail::audio { -// Installs the host sides of WebRtcAudioRecord and WebRtcAudioTrack. SDL's -// audio subsystem must remain alive until ShutdownWebRtcJniAudioBridge. +// Installs WebRtcAudioManager, WebRtcAudioRecord and WebRtcAudioTrack. The +// manager only advertises the host PCM contract; capture opens on recording +// init, never on manager construction. SDL's audio subsystem must remain alive +// until ShutdownWebRtcJniAudioBridge. Status InstallWebRtcJniAudioBridge(jnivm::VM* vm); Status ShutdownWebRtcJniAudioBridge(jnivm::VM* vm); diff --git a/src/audio/webrtc_jni_audio_bridge.cc b/src/audio/webrtc_jni_audio_bridge.cc index b2088ad..f2081e0 100644 --- a/src/audio/webrtc_jni_audio_bridge.cc +++ b/src/audio/webrtc_jni_audio_bridge.cc @@ -1,8 +1,10 @@ #include "mocktail/audio/webrtc_jni_audio_bridge.h" #include +#include #include #include +#include #include #include #include @@ -11,6 +13,7 @@ #include #include #include +#include #include #include @@ -26,6 +29,7 @@ struct CaptureSession { jnivm::WebRtcAudioRecordDataCallback data_callback = nullptr; void* data_context = nullptr; std::unique_ptr capture; + std::shared_ptr> microphone_muted; }; struct PlayoutSession { @@ -46,6 +50,9 @@ struct PlayoutSession { struct WebRtcJniAudioBridgeContext { std::mutex mutex; + std::unordered_set initialized_managers; + std::shared_ptr> microphone_muted = + std::make_shared>(false); std::unordered_map> capture_sessions; std::unordered_map> @@ -54,9 +61,71 @@ struct WebRtcJniAudioBridgeContext { bool shutting_down = false; }; +bool GetManagerParameters(void* opaque_context, + jnivm::WebRtcAudioManagerParameters* parameters) { + auto* context = static_cast(opaque_context); + if (context == nullptr || parameters == nullptr) return false; + std::lock_guard lock(context->mutex); + if (context->shutting_down) return false; + // SDL converts between this WebRTC-facing format and the actual device. + // Match WebRtcAudioManager's default mono input/output and 10 ms frames. + // Advertising the format never opens a capture device, including when the + // microphone is disabled. Recording init remains the permission boundary. + *parameters = {}; + parameters->sample_rate_hz = 48000; + parameters->output_channels = 1; + parameters->input_channels = 1; + parameters->output_buffer_size_frames = 480; + parameters->input_buffer_size_frames = 480; + // Android hardware effects, low-latency paths and AAudio are not emulated; + // WebRTC must retain its software processing and Java audio transport. + return true; +} + +bool InitManager(void* opaque_context, const void* identity) { + auto* context = static_cast(opaque_context); + if (context == nullptr || identity == nullptr) return false; + bool inserted = false; + { + std::lock_guard lock(context->mutex); + if (context->shutting_down) return false; + inserted = context->initialized_managers.insert(identity).second; + } + if (inserted) { + std::cout << " [mocktail][audio] WebRTC audio manager initialized\n" + << std::flush; + } + return true; +} + +void DisposeManager(void* opaque_context, const void* identity) { + auto* context = static_cast(opaque_context); + if (context == nullptr) return; + std::lock_guard lock(context->mutex); + if (context->initialized_managers.erase(identity) != 0) { + std::cout << " [mocktail][audio] WebRTC audio manager disposed\n" + << std::flush; + } + // Record/track objects own their streams and stop them independently. +} + +void SetMicrophoneMute(void* opaque_context, bool muted) { + auto* context = static_cast(opaque_context); + if (context == nullptr) return; + context->microphone_muted->store(muted, std::memory_order_release); +} + void OnCapturedAudio(void* opaque_session, std::size_t size_bytes) { auto* session = static_cast(opaque_session); if (session != nullptr && session->data_callback != nullptr) { + if (session->microphone_muted != nullptr && + session->microphone_muted->load(std::memory_order_acquire)) { + if (session->capture == nullptr || + size_bytes > session->capture->buffer_size_bytes()) { + return; + } + std::memset(session->capture->buffer_data(), 0, size_bytes); + } session->data_callback(session->data_context, session->identity, size_bytes); } @@ -104,6 +173,7 @@ int Init(void* opaque_context, const void* identity, int sample_rate_hz, session->identity = identity; session->data_callback = data_callback; session->data_context = data_context; + session->microphone_muted = context->microphone_muted; SdlAudioCaptureOptions options; options.output_spec.sample_rate_hz = sample_rate_hz; @@ -480,6 +550,7 @@ void Shutdown(void* opaque_context) { std::lock_guard lock(context->mutex); context->recording_enabled = false; context->shutting_down = true; + context->initialized_managers.clear(); capture_sessions.swap(context->capture_sessions); playout_sessions.swap(context->playout_sessions); } @@ -544,6 +615,13 @@ Status InstallWebRtcJniAudioBridge(jnivm::VM* vm) { record_callbacks.shutdown = &Shutdown; vm->SetWebRtcAudioRecordCallbacks(context, record_callbacks); + jnivm::WebRtcAudioManagerCallbacks manager_callbacks; + manager_callbacks.get_parameters = &GetManagerParameters; + manager_callbacks.init = &InitManager; + manager_callbacks.dispose = &DisposeManager; + manager_callbacks.set_microphone_mute = &SetMicrophoneMute; + vm->SetWebRtcAudioManagerCallbacks(context, manager_callbacks); + jnivm::WebRtcAudioTrackCallbacks track_callbacks; track_callbacks.init = &InitPlayout; track_callbacks.buffer_size_frames = &PlayoutBufferSizeFrames; @@ -561,6 +639,7 @@ Status ShutdownWebRtcJniAudioBridge(jnivm::VM* vm) { } // Drop dispatch first, then let the recording binding's shared context stop // both playout workers and capture streams. + vm->ClearWebRtcAudioManagerCallbacks(); vm->ClearWebRtcAudioTrackCallbacks(); vm->ClearWebRtcAudioRecordCallbacks(); return Status::Ok(); diff --git a/src/jnivm/jnivm.cc b/src/jnivm/jnivm.cc index aaa4f54..38fd36e 100644 --- a/src/jnivm/jnivm.cc +++ b/src/jnivm/jnivm.cc @@ -2944,6 +2944,78 @@ bool IsFmodAudioDeviceMethod(jobject obj, jmethodID method_id, ObjectClassName(obj) == "org/fmod/AudioDevice"; } +bool IsWebRtcAudioManagerMethod(jobject obj, jmethodID method_id, + const char* name, const char* signature) { + return obj != nullptr && method_id != nullptr && + std::strcmp(MethodName(method_id), name) == 0 && + std::strcmp(MethodSignature(method_id), signature) == 0 && + ObjectClassName(obj) == "org/webrtc/voiceengine/WebRtcAudioManager"; +} + +bool HandleWebRtcAudioManagerBooleanMethod(jobject obj, jmethodID method_id, + jboolean* result) { + VM* vm = CurrentVM(); + if (IsWebRtcAudioManagerMethod(obj, method_id, "init", "()Z")) { + *result = vm != nullptr && vm->DispatchWebRtcAudioManagerInit(obj) + ? JNI_TRUE + : JNI_FALSE; + } else if (IsWebRtcAudioManagerMethod(obj, method_id, + "isDeviceBlacklistedForOpenSLESUsage", + "()Z")) { + // Host recording is provided by AudioRecord JNI, not Android OpenSL ES. + *result = JNI_TRUE; + } else if (IsWebRtcAudioManagerMethod(obj, method_id, + "isCommunicationModeEnabled", "()Z")) { + *result = BooleanFieldValue(obj, "initialized"); + } else if (IsWebRtcAudioManagerMethod(obj, method_id, + "isLowLatencyOutputSupported", "()Z")) { + *result = BooleanFieldValue(obj, "lowLatencyOutput"); + } else if (IsWebRtcAudioManagerMethod(obj, method_id, + "isLowLatencyInputSupported", "()Z")) { + *result = BooleanFieldValue(obj, "lowLatencyInput"); + } else if (IsWebRtcAudioManagerMethod(obj, method_id, "isProAudioSupported", + "()Z")) { + *result = BooleanFieldValue(obj, "proAudio"); + } else if (IsWebRtcAudioManagerMethod(obj, method_id, "isAAudioSupported", + "()Z")) { + *result = BooleanFieldValue(obj, "aAudio"); + } else { + return false; + } + return true; +} + +bool HandleWebRtcAudioManagerVoidMethodV(jobject obj, jmethodID method_id, + va_list args) { + VM* vm = CurrentVM(); + if (IsWebRtcAudioManagerMethod(obj, method_id, "dispose", "()V")) { + if (vm != nullptr) vm->DispatchWebRtcAudioManagerDispose(obj); + } else if (IsWebRtcAudioManagerMethod(obj, method_id, "setMicrophoneMute", + "(Z)V")) { + const bool muted = va_arg(args, jint) != JNI_FALSE; + if (vm != nullptr) vm->DispatchWebRtcAudioManagerMicrophoneMute(obj, muted); + } else { + return false; + } + return true; +} + +bool HandleWebRtcAudioManagerVoidMethodA(jobject obj, jmethodID method_id, + const jvalue* args) { + VM* vm = CurrentVM(); + if (IsWebRtcAudioManagerMethod(obj, method_id, "dispose", "()V")) { + if (vm != nullptr) vm->DispatchWebRtcAudioManagerDispose(obj); + } else if (IsWebRtcAudioManagerMethod(obj, method_id, "setMicrophoneMute", + "(Z)V")) { + if (vm != nullptr && args != nullptr) { + vm->DispatchWebRtcAudioManagerMicrophoneMute(obj, args[0].z != JNI_FALSE); + } + } else { + return false; + } + return true; +} + bool IsWebRtcAudioRecordMethod(jobject obj, jmethodID method_id, const char *name, const char *signature) { return obj != nullptr && method_id != nullptr && @@ -4463,6 +4535,7 @@ void JNICALL CallVoidMethod(JNIEnv * /*env*/, jobject obj, jmethodID methodID, va_list args; va_start(args, methodID); if (!HandleRobloxExperienceLifecycleVoidMethod(obj, methodID) && + !HandleWebRtcAudioManagerVoidMethodV(obj, methodID, args) && !HandleRobloxOpenWebActivityMethodV(obj, methodID, args) && !HandleRobloxTextInputInstanceVoidMethodV(obj, methodID, args) && !HandleFmodAudioDeviceVoidMethodV(obj, methodID, args) && @@ -4493,8 +4566,11 @@ jboolean JNICALL CallBooleanMethod(JNIEnv* /*env*/, jobject obj, va_list args; va_start(args, methodID); jboolean result = JNI_FALSE; - bool handled = - HandleWebRtcAudioRecordBooleanMethodV(obj, methodID, args, &result); + bool handled = HandleWebRtcAudioManagerBooleanMethod(obj, methodID, &result); + if (!handled) { + handled = + HandleWebRtcAudioRecordBooleanMethodV(obj, methodID, args, &result); + } if (!handled) { handled = HandleWebRtcAudioTrackBooleanMethodV(obj, methodID, args, &result); @@ -4566,6 +4642,14 @@ jobject ConstructObjectV(jclass clazz, jmethodID methodID, va_list args) { std::strcmp(MethodName(methodID), "") == 0 && std::strcmp(MethodSignature(methodID), "(J)V") == 0) { SetLongFieldRaw(object, "ref", va_arg(args, jlong)); + } else if (object_class->GetName() == + "org/webrtc/voiceengine/WebRtcAudioManager" && + std::strcmp(MethodName(methodID), "") == 0 && + std::strcmp(MethodSignature(methodID), "(J)V") == 0) { + const jlong native_audio_manager = va_arg(args, jlong); + if (VM* vm = CurrentVM()) { + vm->DispatchWebRtcAudioManagerConstruct(object, native_audio_manager); + } } else if (object_class->GetName() == "org/webrtc/voiceengine/WebRtcAudioRecord" && std::strcmp(MethodName(methodID), "") == 0 && @@ -4648,6 +4732,13 @@ jobject ConstructObjectA(jclass clazz, jmethodID methodID, const jvalue *args) { std::strcmp(MethodName(methodID), "") == 0 && std::strcmp(MethodSignature(methodID), "(J)V") == 0) { SetLongFieldRaw(object, "ref", args[0].j); + } else if (object_class->GetName() == + "org/webrtc/voiceengine/WebRtcAudioManager" && + std::strcmp(MethodName(methodID), "") == 0 && + std::strcmp(MethodSignature(methodID), "(J)V") == 0) { + if (VM* vm = CurrentVM()) { + vm->DispatchWebRtcAudioManagerConstruct(object, args[0].j); + } } else if (object_class->GetName() == "org/webrtc/voiceengine/WebRtcAudioRecord" && std::strcmp(MethodName(methodID), "") == 0 && @@ -4853,6 +4944,7 @@ VM::~VM() { ClearRobloxCredentialProvider(); ClearRobloxTextInputCallbacks(); ClearAndroidWindowCallbacks(); + ClearWebRtcAudioManagerCallbacks(); ClearWebRtcAudioTrackCallbacks(); ClearWebRtcAudioRecordCallbacks(); ClearFmodAudioDeviceCallbacks(); @@ -5472,6 +5564,163 @@ void OnWebRtcAudioRecordData(void *context, const void *identity, } // namespace +void VM::SetWebRtcAudioManagerCallbacks( + std::shared_ptr context, + const WebRtcAudioManagerCallbacks& callbacks) { + WebRtcAudioManagerBinding previous; + { + std::lock_guard lock(webrtc_audio_manager_mutex_); + previous = std::move(webrtc_audio_manager_binding_); + webrtc_audio_manager_binding_ = {std::move(context), callbacks}; + } +} + +void VM::ClearWebRtcAudioManagerCallbacks() { + WebRtcAudioManagerBinding previous; + { + std::lock_guard lock(webrtc_audio_manager_mutex_); + previous = std::move(webrtc_audio_manager_binding_); + webrtc_audio_manager_binding_ = {}; + } +} + +void VM::RegisterWebRtcAudioManagerNative(const char* name, + const char* signature, + void* function) { + if (name == nullptr || signature == nullptr || function == nullptr || + std::strcmp(name, "nativeCacheAudioParameters") != 0 || + std::strcmp(signature, "(IIIZZZZZZZIIJ)V") != 0) { + return; + } + std::lock_guard lock(webrtc_audio_manager_mutex_); + webrtc_cache_audio_parameters_native_ = function; +} + +bool VM::DispatchWebRtcAudioManagerConstruct(jobject manager, + jlong native_audio_manager) { + const auto fail = [](const char* reason) { + std::cerr << " [mocktail][audio] WebRTC audio manager parameters_failed " + << "reason=" << reason << '\n'; + return false; + }; + if (ObjectClassName(manager) != "org/webrtc/voiceengine/WebRtcAudioManager") { + return fail("invalid receiver"); + } + SetLongFieldRaw(manager, "nativeAudioManager", native_audio_manager); + SetBooleanFieldRaw(manager, "initialized", JNI_FALSE); + SetBooleanFieldRaw(manager, "mocktailAudioParametersCached", JNI_FALSE); + WebRtcAudioManagerBinding binding; + void* native = nullptr; + { + std::lock_guard lock(webrtc_audio_manager_mutex_); + binding = webrtc_audio_manager_binding_; + native = webrtc_cache_audio_parameters_native_; + } + if (native == nullptr || native_audio_manager == 0) { + return fail("missing nativeCacheAudioParameters or native handle"); + } + WebRtcAudioManagerParameters parameters; + if (binding.context == nullptr || + binding.callbacks.get_parameters == nullptr || + !binding.callbacks.get_parameters(binding.context.get(), ¶meters)) { + return fail("host audio manager unavailable"); + } + if (parameters.sample_rate_hz <= 0 || parameters.output_channels < 1 || + parameters.output_channels > 2 || parameters.input_channels < 1 || + parameters.input_channels > 2 || + parameters.output_buffer_size_frames <= 0 || + parameters.input_buffer_size_frames <= 0) { + return fail("invalid host PCM parameters"); + } + SetIntFieldRaw(manager, "sampleRate", parameters.sample_rate_hz); + SetIntFieldRaw(manager, "outputChannels", parameters.output_channels); + SetIntFieldRaw(manager, "inputChannels", parameters.input_channels); + SetIntFieldRaw(manager, "outputBufferSize", + parameters.output_buffer_size_frames); + SetIntFieldRaw(manager, "inputBufferSize", + parameters.input_buffer_size_frames); + SetBooleanFieldRaw(manager, "lowLatencyOutput", + parameters.low_latency_output); + SetBooleanFieldRaw(manager, "lowLatencyInput", parameters.low_latency_input); + SetBooleanFieldRaw(manager, "proAudio", parameters.pro_audio); + SetBooleanFieldRaw(manager, "aAudio", parameters.aaudio); + + // Execute the constructor's Java -> native callback before returning the + // object. The guest needs these formats even before it calls init(). No VM + // locks are held here: the native callback may make nested JNI calls. + using CacheAudioParameters = void(JNICALL*)( + JNIEnv*, jobject, jint, jint, jint, jboolean, jboolean, jboolean, + jboolean, jboolean, jboolean, jboolean, jint, jint, jlong); + reinterpret_cast(native)( + GetJNIEnv(), manager, parameters.sample_rate_hz, + parameters.output_channels, parameters.input_channels, + parameters.hardware_aec, parameters.hardware_agc, parameters.hardware_ns, + parameters.low_latency_output, parameters.low_latency_input, + parameters.pro_audio, parameters.aaudio, + parameters.output_buffer_size_frames, parameters.input_buffer_size_frames, + native_audio_manager); + SetBooleanFieldRaw(manager, "mocktailAudioParametersCached", JNI_TRUE); + std::cout << " [mocktail][audio] WebRTC audio manager parameters " + << "sample_rate_hz=" << parameters.sample_rate_hz + << " output_channels=" << parameters.output_channels + << " input_channels=" << parameters.input_channels << '\n' + << std::flush; + return true; +} + +bool VM::DispatchWebRtcAudioManagerInit(jobject manager) { + WebRtcAudioManagerBinding binding; + { + std::lock_guard lock(webrtc_audio_manager_mutex_); + binding = webrtc_audio_manager_binding_; + } + const bool initialized = + ObjectClassName(manager) == "org/webrtc/voiceengine/WebRtcAudioManager" && + BooleanFieldValue(manager, "mocktailAudioParametersCached") == JNI_TRUE && + binding.context != nullptr && binding.callbacks.init != nullptr && + (BooleanFieldValue(manager, "initialized") == JNI_TRUE || + binding.callbacks.init(binding.context.get(), manager)); + SetBooleanFieldRaw(manager, "initialized", + initialized ? JNI_TRUE : JNI_FALSE); + if (!initialized) { + std::cerr << " [mocktail][audio] WebRTC audio manager init_failed " + "reason=parameters or host manager unavailable\n"; + } + return initialized; +} + +void VM::DispatchWebRtcAudioManagerDispose(jobject manager) { + if (ObjectClassName(manager) != "org/webrtc/voiceengine/WebRtcAudioManager" || + BooleanFieldValue(manager, "initialized") != JNI_TRUE) { + return; + } + SetBooleanFieldRaw(manager, "initialized", JNI_FALSE); + WebRtcAudioManagerBinding binding; + { + std::lock_guard lock(webrtc_audio_manager_mutex_); + binding = webrtc_audio_manager_binding_; + } + if (binding.context != nullptr && binding.callbacks.dispose != nullptr) { + binding.callbacks.dispose(binding.context.get(), manager); + } +} + +void VM::DispatchWebRtcAudioManagerMicrophoneMute(jobject manager, bool muted) { + if (ObjectClassName(manager) != "org/webrtc/voiceengine/WebRtcAudioManager" || + BooleanFieldValue(manager, "mocktailAudioParametersCached") != JNI_TRUE) { + return; + } + WebRtcAudioManagerBinding binding; + { + std::lock_guard lock(webrtc_audio_manager_mutex_); + binding = webrtc_audio_manager_binding_; + } + if (binding.context != nullptr && + binding.callbacks.set_microphone_mute != nullptr) { + binding.callbacks.set_microphone_mute(binding.context.get(), muted); + } +} + void VM::SetWebRtcAudioRecordCallbacks( std::shared_ptr context, const WebRtcAudioRecordCallbacks &callbacks) { @@ -6232,6 +6481,14 @@ void VM::InitJNIFunctionTables() { } else if (std::strcmp(name, "onTrimMemoryNative") == 0) { mocktail_gameactivity_on_trim_memory_native = methods[i].fnPtr; } + if (cls != nullptr && + cls->GetName() == + "org/webrtc/voiceengine/WebRtcAudioManager") { + if (VM* vm = CurrentVM()) { + vm->RegisterWebRtcAudioManagerNative( + methods[i].name, methods[i].signature, methods[i].fnPtr); + } + } if (cls != nullptr && cls->GetName() == "org/webrtc/voiceengine/WebRtcAudioRecord") { @@ -6578,6 +6835,7 @@ void VM::InitJNIFunctionTables() { std::cout << " [JNI] CallVoidMethodV: " << MethodName(methodID) << '\n'; } if (!HandleRobloxExperienceLifecycleVoidMethod(obj, methodID) && + !HandleWebRtcAudioManagerVoidMethodV(obj, methodID, args) && !HandleRobloxOpenWebActivityMethodV(obj, methodID, args) && !HandleRobloxTextInputInstanceVoidMethodV(obj, methodID, args) && !HandleFmodAudioDeviceVoidMethodV(obj, methodID, args) && @@ -6594,6 +6852,7 @@ void VM::InitJNIFunctionTables() { std::cout << " [JNI] CallVoidMethodA: " << MethodName(methodID) << '\n'; } if (!HandleRobloxExperienceLifecycleVoidMethod(obj, methodID) && + !HandleWebRtcAudioManagerVoidMethodA(obj, methodID, args) && !HandleRobloxOpenWebActivityMethodA(obj, methodID, args) && !HandleRobloxTextInputInstanceVoidMethodA(obj, methodID, args) && !HandleFmodAudioDeviceVoidMethodA(obj, methodID, args) && @@ -6669,6 +6928,9 @@ void VM::InitJNIFunctionTables() { std::cout << " [JNI] CallBooleanMethodV: " << MethodName(methodID) << '\n'; } jboolean result = JNI_FALSE; + if (HandleWebRtcAudioManagerBooleanMethod(obj, methodID, &result)) { + return result; + } if (HandleWebRtcAudioRecordBooleanMethodV(obj, methodID, args, &result)) { return result; } @@ -6698,6 +6960,9 @@ void VM::InitJNIFunctionTables() { std::cout << " [JNI] CallBooleanMethodA: " << MethodName(methodID) << '\n'; } jboolean result = JNI_FALSE; + if (HandleWebRtcAudioManagerBooleanMethod(obj, methodID, &result)) { + return result; + } if (HandleWebRtcAudioRecordBooleanMethodA(obj, methodID, args, &result)) { return result; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5ac8ac2..9aff60f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -379,6 +379,15 @@ target_link_libraries(jnivm_native_text_box_info_test PRIVATE ) mocktail_apply_compile_options(jnivm_native_text_box_info_test) +add_executable(jnivm_webrtc_audio_manager_test + jnivm_webrtc_audio_manager_test.cc +) +target_link_libraries(jnivm_webrtc_audio_manager_test PRIVATE + Mocktail::LegacyJni + GTest::gtest_main +) +mocktail_apply_compile_options(jnivm_webrtc_audio_manager_test) + add_executable(vulkan_present_observer_test vulkan_present_observer_test.cc ) @@ -890,6 +899,7 @@ gtest_discover_tests(roblox_experience_composition_test) gtest_discover_tests(jnivm_message_bus_test) gtest_discover_tests(jnivm_experience_lifecycle_test) gtest_discover_tests(jnivm_native_text_box_info_test) +gtest_discover_tests(jnivm_webrtc_audio_manager_test) gtest_discover_tests(vulkan_present_observer_test) gtest_discover_tests(input_pump_pacer_test) gtest_discover_tests(vulkan_surface_recovery_gate_test) diff --git a/tests/fmod_jni_audio_bridge_test.cc b/tests/fmod_jni_audio_bridge_test.cc index fc87649..a17199a 100644 --- a/tests/fmod_jni_audio_bridge_test.cc +++ b/tests/fmod_jni_audio_bridge_test.cc @@ -1,10 +1,14 @@ #include "mocktail/audio/fmod_jni_audio_bridge.h" -#include "mocktail/audio/sdl_audio_capture.h" -#include "mocktail/audio/webrtc_jni_audio_bridge.h" +#include +#include +#include + +#include #include #include #include +#include #include #include #include @@ -12,10 +16,10 @@ #include #include -#include - #include "jnivm/jnivm.h" +#include "mocktail/audio/sdl_audio_capture.h" #include "mocktail/audio/sdl_audio_sink.h" +#include "mocktail/audio/webrtc_jni_audio_bridge.h" namespace mocktail::audio { namespace { @@ -53,6 +57,72 @@ public: ~ScopedSdlAudioShutdown() { (void)ShutdownSdlAudioSubsystem(); } }; +class ScopedSdlHint final { + public: + ScopedSdlHint(const char* name, const char* value) : name_(name) { + const char* previous = SDL_GetHint(name); + had_value_ = previous != nullptr; + if (had_value_) previous_ = previous; + EXPECT_TRUE(SDL_SetHintWithPriority(name, value, SDL_HINT_OVERRIDE)); + } + ~ScopedSdlHint() { + if (had_value_) { + SDL_SetHintWithPriority(name_.c_str(), previous_.c_str(), + SDL_HINT_OVERRIDE); + } else { + SDL_ResetHint(name_.c_str()); + } + } + + private: + std::string name_; + std::string previous_; + bool had_value_ = false; +}; + +class ScopedWebRtcBridgeShutdown final { + public: + explicit ScopedWebRtcBridgeShutdown(jnivm::VM* vm) : vm_(vm) {} + ~ScopedWebRtcBridgeShutdown() { + if (vm_ != nullptr) (void)ShutdownWebRtcJniAudioBridge(vm_); + } + void Release() { vm_ = nullptr; } + + private: + jnivm::VM* vm_; +}; + +struct WebRtcManagerProbe { + int callbacks = 0; + jnivm::WebRtcAudioManagerParameters parameters; +}; + +void JNICALL CacheWebRtcParameters(JNIEnv*, jobject, jint rate, + jint output_channels, jint input_channels, + jboolean aec, jboolean agc, jboolean ns, + jboolean low_output, jboolean low_input, + jboolean pro, jboolean aaudio, + jint output_frames, jint input_frames, + jlong native_manager) { + auto* probe = reinterpret_cast(native_manager); + ++probe->callbacks; + probe->parameters = { + rate, output_channels, input_channels, aec != 0, + agc != 0, ns != 0, low_output != 0, low_input != 0, + pro != 0, aaudio != 0, output_frames, input_frames}; +} + +jobject CreateWebRtcManager(JNIEnv* env, WebRtcManagerProbe* probe) { + jclass cls = env->FindClass("org/webrtc/voiceengine/WebRtcAudioManager"); + const JNINativeMethod cache = { + const_cast("nativeCacheAudioParameters"), + const_cast("(IIIZZZZZZZIIJ)V"), + reinterpret_cast(&CacheWebRtcParameters)}; + EXPECT_EQ(env->RegisterNatives(cls, &cache, 1), JNI_OK); + return env->NewObject(cls, env->GetMethodID(cls, "", "(J)V"), + reinterpret_cast(probe)); +} + struct WebRtcCaptureProbe { std::mutex mutex; std::condition_variable cv; @@ -60,6 +130,8 @@ struct WebRtcCaptureProbe { jlong capacity = -1; int callbacks = 0; int last_size = 0; + int non_silent_callbacks = 0; + int silent_callbacks = 0; }; struct WebRtcPlayoutProbe { @@ -87,6 +159,16 @@ void JNICALL WebRtcDataIsRecorded(JNIEnv *, jobject, jint size, std::lock_guard lock(probe->mutex); ++probe->callbacks; probe->last_size = size; + if (probe->buffer != nullptr && size > 0 && size <= probe->capacity) { + const auto* bytes = static_cast(probe->buffer); + const bool silent = + std::all_of(bytes, bytes + size, + [](unsigned char sample) { return sample == 0; }); + if (silent) + ++probe->silent_callbacks; + else + ++probe->non_silent_callbacks; + } } probe->cv.notify_all(); } @@ -225,6 +307,13 @@ TEST(WebRtcJniAudioBridgeTest, DisabledMicrophoneRejectsRecordingInit) { ASSERT_TRUE(install_status.ok()) << install_status.message(); JNIEnv *env = vm.GetJNIEnv(); + WebRtcManagerProbe manager_probe; + jobject manager = CreateWebRtcManager(env, &manager_probe); + jclass manager_class = env->GetObjectClass(manager); + EXPECT_EQ(manager_probe.callbacks, 1); + EXPECT_EQ(env->CallBooleanMethod( + manager, env->GetMethodID(manager_class, "init", "()Z")), + JNI_TRUE); jclass recorder_class = env->FindClass("org/webrtc/voiceengine/WebRtcAudioRecord"); ASSERT_NE(recorder_class, nullptr); @@ -307,6 +396,214 @@ TEST(WebRtcJniAudioBridgeTest, PlaysTenMillisecondFramesThroughExactJni) { EXPECT_TRUE(ShutdownSdlAudioSubsystem().ok()); } +TEST(WebRtcJniAudioBridgeTest, + StartsDuplexAudioFromManagerParametersAndReopens) { + ScopedEnvironment input("MOCKTAIL_AUDIO_INPUT_DEVICE", "default"); + ScopedSdlAudioShutdown shutdown; + jnivm::VM vm; + ASSERT_TRUE(InstallWebRtcJniAudioBridge(&vm).ok()); + JNIEnv* env = vm.GetJNIEnv(); + // Repeat the complete object lifecycle, as when leaving and rejoining VC. + for (int iteration = 0; iteration != 2; ++iteration) { + WebRtcManagerProbe manager_probe; + WebRtcCaptureProbe capture_probe; + WebRtcPlayoutProbe playout_probe; + ScopedWebRtcBridgeShutdown close_on_failure(&vm); + jobject manager = CreateWebRtcManager(env, &manager_probe); + ASSERT_NE(manager, nullptr); + ASSERT_EQ(manager_probe.callbacks, 1); + const auto& format = manager_probe.parameters; + EXPECT_EQ(format.sample_rate_hz, 48000); + EXPECT_EQ(format.input_buffer_size_frames, format.sample_rate_hz / 100); + EXPECT_FALSE(format.hardware_aec || format.hardware_agc || + format.hardware_ns || format.low_latency_output || + format.low_latency_input || format.pro_audio || format.aaudio); + jclass manager_class = env->GetObjectClass(manager); + ASSERT_EQ(env->CallBooleanMethod( + manager, env->GetMethodID(manager_class, "init", "()Z")), + JNI_TRUE); + + jclass record_class = + env->FindClass("org/webrtc/voiceengine/WebRtcAudioRecord"); + jclass track_class = + env->FindClass("org/webrtc/voiceengine/WebRtcAudioTrack"); + const JNINativeMethod record_methods[] = { + {const_cast("nativeCacheDirectBufferAddress"), + const_cast("(Ljava/nio/ByteBuffer;J)V"), + reinterpret_cast(&CacheWebRtcBuffer)}, + {const_cast("nativeDataIsRecorded"), const_cast("(IJ)V"), + reinterpret_cast(&WebRtcDataIsRecorded)}}; + const JNINativeMethod track_methods[] = { + {const_cast("nativeCacheDirectBufferAddress"), + const_cast("(Ljava/nio/ByteBuffer;J)V"), + reinterpret_cast(&CacheWebRtcPlayoutBuffer)}, + {const_cast("nativeGetPlayoutData"), const_cast("(IJ)V"), + reinterpret_cast(&WebRtcGetPlayoutData)}}; + ASSERT_EQ(env->RegisterNatives(record_class, record_methods, 2), JNI_OK); + ASSERT_EQ(env->RegisterNatives(track_class, track_methods, 2), JNI_OK); + jobject record = env->NewObject( + record_class, env->GetMethodID(record_class, "", "(J)V"), + reinterpret_cast(&capture_probe)); + jobject track = env->NewObject( + track_class, env->GetMethodID(track_class, "", "(J)V"), + reinterpret_cast(&playout_probe)); + ASSERT_EQ( + env->CallIntMethod( + record, env->GetMethodID(record_class, "initRecording", "(II)I"), + format.sample_rate_hz, format.input_channels), + format.input_buffer_size_frames); + ASSERT_GT(env->CallIntMethod( + track, env->GetMethodID(track_class, "initPlayout", "(IID)I"), + format.sample_rate_hz, format.output_channels, 1.0), + 0); + ASSERT_EQ( + env->CallBooleanMethod( + record, env->GetMethodID(record_class, "startRecording", "()Z")), + JNI_TRUE); + ASSERT_EQ(env->CallBooleanMethod( + track, env->GetMethodID(track_class, "startPlayout", "()Z")), + JNI_TRUE); + { + std::unique_lock lock(capture_probe.mutex); + ASSERT_TRUE(capture_probe.cv.wait_for(lock, std::chrono::seconds(2), [&] { + return capture_probe.callbacks >= 3; + })); + EXPECT_EQ(capture_probe.last_size, + format.input_buffer_size_frames * format.input_channels * 2); + } + { + std::unique_lock lock(playout_probe.mutex); + ASSERT_TRUE(playout_probe.cv.wait_for(lock, std::chrono::seconds(2), [&] { + return playout_probe.callbacks >= 3; + })); + EXPECT_EQ(playout_probe.last_size, + format.output_buffer_size_frames * format.output_channels * 2); + EXPECT_FALSE(playout_probe.invalid_buffer); + } + ASSERT_EQ( + env->CallBooleanMethod( + record, env->GetMethodID(record_class, "stopRecording", "()Z")), + JNI_TRUE); + ASSERT_EQ(env->CallBooleanMethod( + track, env->GetMethodID(track_class, "stopPlayout", "()Z")), + JNI_TRUE); + env->CallVoidMethod(manager, + env->GetMethodID(manager_class, "dispose", "()V")); + env->DeleteLocalRef(record); + env->DeleteLocalRef(track); + env->DeleteLocalRef(manager); + close_on_failure.Release(); + } + ASSERT_TRUE(ShutdownWebRtcJniAudioBridge(&vm).ok()); +} + +TEST(WebRtcJniAudioBridgeTest, + ManagerMuteSilencesNonzeroPcmAndUnmuteRestoresIt) { + // Read synthetic PCM through SDL's disk driver, never a physical microphone. + // A nonzero source is essential: dummy-driver silence cannot test muting. + bool have_disk_driver = false; + for (int index = 0; index < SDL_GetNumAudioDrivers(); ++index) { + if (std::strcmp(SDL_GetAudioDriver(index), "disk") == 0) + have_disk_driver = true; + } + if (!have_disk_driver) GTEST_SKIP() << "SDL disk audio driver is not built"; + ASSERT_TRUE(ShutdownSdlAudioSubsystem().ok()); + struct TemporaryPcm { + char path[40] = "/tmp/mocktail-webrtc-pcm-XXXXXX"; + ~TemporaryPcm() { + if (created) std::remove(path); + } + bool created = false; + } pcm; + const int fd = mkstemp(pcm.path); + ASSERT_GE(fd, 0); + pcm.created = true; + FILE* file = fdopen(fd, "wb"); + if (file == nullptr) close(fd); + ASSERT_NE(file, nullptr); + // 0x3f is nonzero in both the S16 stream format and SDL's F32 device format. + const std::vector samples(4 * 1024 * 1024, 0x3f); + const std::size_t written = + std::fwrite(samples.data(), 1, samples.size(), file); + const int closed = std::fclose(file); + ASSERT_EQ(written, samples.size()); + ASSERT_EQ(closed, 0); + // SDL caches environment values, so set hints explicitly when switching + // drivers within a test process that already initialized dummy audio. + ScopedSdlHint driver(SDL_HINT_AUDIO_DRIVER, "disk"); + ScopedSdlHint disk_input(SDL_HINT_AUDIO_DISK_INPUT_FILE, pcm.path); + ScopedSdlHint disk_output(SDL_HINT_AUDIO_DISK_OUTPUT_FILE, "/dev/null"); + ScopedEnvironment input("MOCKTAIL_AUDIO_INPUT_DEVICE", "default"); + ScopedSdlAudioShutdown shutdown; + jnivm::VM vm; + WebRtcManagerProbe manager_probe; + WebRtcCaptureProbe capture_probe; + ScopedWebRtcBridgeShutdown close_before_probes(&vm); + ASSERT_TRUE(InstallWebRtcJniAudioBridge(&vm).ok()); + ASSERT_STREQ(SDL_GetCurrentAudioDriver(), "disk"); + JNIEnv* env = vm.GetJNIEnv(); + jobject manager = CreateWebRtcManager(env, &manager_probe); + jclass manager_class = env->GetObjectClass(manager); + ASSERT_EQ(env->CallBooleanMethod( + manager, env->GetMethodID(manager_class, "init", "()Z")), + JNI_TRUE); + jclass record_class = + env->FindClass("org/webrtc/voiceengine/WebRtcAudioRecord"); + const JNINativeMethod methods[] = { + {const_cast("nativeCacheDirectBufferAddress"), + const_cast("(Ljava/nio/ByteBuffer;J)V"), + reinterpret_cast(&CacheWebRtcBuffer)}, + {const_cast("nativeDataIsRecorded"), const_cast("(IJ)V"), + reinterpret_cast(&WebRtcDataIsRecorded)}}; + ASSERT_EQ(env->RegisterNatives(record_class, methods, 2), JNI_OK); + jobject record = env->NewObject( + record_class, env->GetMethodID(record_class, "", "(J)V"), + reinterpret_cast(&capture_probe)); + const auto& format = manager_probe.parameters; + ASSERT_EQ( + env->CallIntMethod( + record, env->GetMethodID(record_class, "initRecording", "(II)I"), + format.sample_rate_hz, format.input_channels), + 480); + ASSERT_EQ( + env->CallBooleanMethod( + record, env->GetMethodID(record_class, "startRecording", "()Z")), + JNI_TRUE); + { + std::unique_lock lock(capture_probe.mutex); + ASSERT_TRUE(capture_probe.cv.wait_for(lock, std::chrono::seconds(2), [&] { + return capture_probe.non_silent_callbacks >= 3; + })); + } + const jmethodID mute = + env->GetMethodID(manager_class, "setMicrophoneMute", "(Z)V"); + env->CallVoidMethod(manager, mute, JNI_TRUE); + { + std::unique_lock lock(capture_probe.mutex); + int silent_before = capture_probe.silent_callbacks; + ASSERT_TRUE(capture_probe.cv.wait_for(lock, std::chrono::seconds(2), [&] { + return capture_probe.silent_callbacks >= silent_before + 3; + })); + // Allow the one already-running callback at the mute boundary to finish, + // then verify an additional three frames contain only silence. + const int non_silent_before = capture_probe.non_silent_callbacks; + silent_before = capture_probe.silent_callbacks; + ASSERT_TRUE(capture_probe.cv.wait_for(lock, std::chrono::seconds(2), [&] { + return capture_probe.silent_callbacks >= silent_before + 3; + })); + EXPECT_EQ(capture_probe.non_silent_callbacks, non_silent_before); + env->CallVoidMethod(manager, mute, JNI_FALSE); + ASSERT_TRUE(capture_probe.cv.wait_for(lock, std::chrono::seconds(2), [&] { + return capture_probe.non_silent_callbacks >= non_silent_before + 3; + })); + } + EXPECT_EQ(env->CallBooleanMethod( + record, env->GetMethodID(record_class, "stopRecording", "()Z")), + JNI_TRUE); + env->CallVoidMethod(manager, + env->GetMethodID(manager_class, "dispose", "()V")); +} + TEST(SdlAudioCaptureTest, ResolvesDefaultIdAndUnambiguousDeviceNames) { const std::vector devices = { {11, "USB Microphone"}, diff --git a/tests/jnivm_webrtc_audio_manager_test.cc b/tests/jnivm_webrtc_audio_manager_test.cc new file mode 100644 index 0000000..903c192 --- /dev/null +++ b/tests/jnivm_webrtc_audio_manager_test.cc @@ -0,0 +1,297 @@ +#include + +#include +#include +#include + +#include "jnivm/jnivm.h" + +namespace jnivm { +namespace { + +constexpr char kManagerClass[] = "org/webrtc/voiceengine/WebRtcAudioManager"; +constexpr char kCacheSignature[] = "(IIIZZZZZZZIIJ)V"; + +struct ManagerProbe { + WebRtcAudioManagerParameters parameters; + bool available = true; + bool init_succeeds = true; + int cache_calls = 0; + int init_calls = 0; + int dispose_calls = 0; + jobject receiver = nullptr; + jlong native_handle = 0; + std::array cached_format{}; + std::array cached_features{}; + std::vector mute_calls; +}; + +void JNICALL CacheParameters(JNIEnv* env, jobject manager, jint rate, + jint output_channels, jint input_channels, + jboolean aec, jboolean agc, jboolean ns, + jboolean low_output, jboolean low_input, + jboolean pro, jboolean aaudio, jint output_frames, + jint input_frames, jlong native_handle) { + auto* probe = reinterpret_cast(native_handle); + ++probe->cache_calls; + probe->receiver = manager; + probe->native_handle = native_handle; + probe->cached_format = {rate, output_channels, input_channels, output_frames, + input_frames}; + probe->cached_features = {aec, agc, ns, low_output, low_input, pro, aaudio}; + // Native callbacks can re-enter JNI. This also checks constructor ordering. + jclass cls = env->GetObjectClass(manager); + EXPECT_EQ(env->GetLongField(manager, + env->GetFieldID(cls, "nativeAudioManager", "J")), + native_handle); + env->DeleteLocalRef(cls); +} + +WebRtcAudioManagerCallbacks ProbeCallbacks() { + WebRtcAudioManagerCallbacks callbacks; + callbacks.get_parameters = [](void* context, + WebRtcAudioManagerParameters* parameters) { + auto* probe = static_cast(context); + *parameters = probe->parameters; + return probe->available; + }; + callbacks.init = [](void* context, const void*) { + auto* probe = static_cast(context); + ++probe->init_calls; + return probe->init_succeeds; + }; + callbacks.dispose = [](void* context, const void*) { + ++static_cast(context)->dispose_calls; + }; + callbacks.set_microphone_mute = [](void* context, bool muted) { + static_cast(context)->mute_calls.push_back(muted); + }; + return callbacks; +} + +enum class JniRoute { kDirect, kVariadic, kArray }; + +class JniVmWebRtcAudioManagerTest : public testing::TestWithParam { + protected: + void SetUp() override { + env_ = vm_.GetJNIEnv(); + cls_ = env_->FindClass(kManagerClass); + probe_ = std::make_shared(); + probe_->parameters.sample_rate_hz = 48000; + probe_->parameters.output_channels = 2; + probe_->parameters.input_channels = 1; + probe_->parameters.output_buffer_size_frames = 960; + probe_->parameters.input_buffer_size_frames = 480; + vm_.SetWebRtcAudioManagerCallbacks(probe_, ProbeCallbacks()); + RegisterCache(kCacheSignature); + } + + void RegisterCache(const char* signature) { + JNINativeMethod method = {const_cast("nativeCacheAudioParameters"), + const_cast(signature), + reinterpret_cast(&CacheParameters)}; + ASSERT_EQ(env_->RegisterNatives(cls_, &method, 1), JNI_OK); + } + + jobject Construct(jlong handle, const char* signature = "(J)V") { + jmethodID ctor = env_->GetMethodID(cls_, "", signature); + if (GetParam() == JniRoute::kDirect) { + return env_->functions->NewObject(env_, cls_, ctor, handle); + } + if (GetParam() == JniRoute::kVariadic) { + return env_->NewObject(cls_, ctor, handle); + } + jvalue argument{}; + argument.j = handle; + return env_->NewObjectA(cls_, ctor, &argument); + } + + jobject Construct() { + return Construct(reinterpret_cast(probe_.get())); + } + + jboolean Boolean(jobject manager, const char* name) { + jmethodID method = env_->GetMethodID(cls_, name, "()Z"); + if (GetParam() == JniRoute::kDirect) { + return env_->functions->CallBooleanMethod(env_, manager, method); + } + if (GetParam() == JniRoute::kVariadic) { + return env_->CallBooleanMethod(manager, method); + } + return env_->CallBooleanMethodA(manager, method, nullptr); + } + + void Void(jobject manager, const char* name, const char* signature = "()V", + jboolean value = JNI_FALSE) { + jmethodID method = env_->GetMethodID(cls_, name, signature); + if (GetParam() == JniRoute::kDirect) { + env_->functions->CallVoidMethod(env_, manager, method, + static_cast(value)); + } else if (GetParam() == JniRoute::kVariadic) { + env_->CallVoidMethod(manager, method, static_cast(value)); + } else { + jvalue argument{}; + argument.z = value; + env_->CallVoidMethodA(manager, method, &argument); + } + } + + VM vm_; + JNIEnv* env_ = nullptr; + jclass cls_ = nullptr; + std::shared_ptr probe_; +}; + +TEST_P(JniVmWebRtcAudioManagerTest, + ConstructorCachesExactParametersBeforeInit) { + // Alternating bits catch swapped/missing booleans in the long JNI signature. + probe_->parameters.hardware_aec = true; + probe_->parameters.hardware_ns = true; + probe_->parameters.low_latency_input = true; + probe_->parameters.aaudio = true; + jobject manager = Construct(); + ASSERT_NE(manager, nullptr); + EXPECT_EQ(probe_->cache_calls, 1); + EXPECT_EQ(probe_->init_calls, 0); + EXPECT_EQ(probe_->receiver, manager); + EXPECT_EQ(probe_->native_handle, reinterpret_cast(probe_.get())); + EXPECT_EQ(probe_->cached_format, + (std::array{48000, 2, 1, 960, 480})); + EXPECT_EQ(probe_->cached_features, + (std::array{1, 0, 1, 0, 1, 0, 1})); + EXPECT_EQ(Boolean(manager, "init"), JNI_TRUE); + EXPECT_EQ(Boolean(manager, "init"), JNI_TRUE); + EXPECT_EQ(probe_->init_calls, 1); + EXPECT_EQ(probe_->cache_calls, 1); + EXPECT_EQ(Boolean(manager, "isCommunicationModeEnabled"), JNI_TRUE); + EXPECT_EQ(Boolean(manager, "isLowLatencyOutputSupported"), JNI_FALSE); + EXPECT_EQ(Boolean(manager, "isLowLatencyInputSupported"), JNI_TRUE); + EXPECT_EQ(Boolean(manager, "isAAudioSupported"), JNI_TRUE); + EXPECT_EQ(Boolean(manager, "isDeviceBlacklistedForOpenSLESUsage"), JNI_TRUE); +} + +TEST_P(JniVmWebRtcAudioManagerTest, + DisposeIsIdempotentAndAllowsReinitialization) { + jobject manager = Construct(); + ASSERT_EQ(Boolean(manager, "init"), JNI_TRUE); + Void(manager, "dispose"); + Void(manager, "dispose"); + EXPECT_EQ(probe_->dispose_calls, 1); + EXPECT_EQ(Boolean(manager, "isCommunicationModeEnabled"), JNI_FALSE); + EXPECT_EQ(Boolean(manager, "init"), JNI_TRUE); + EXPECT_EQ(probe_->init_calls, 2); + EXPECT_EQ(probe_->cache_calls, 1); +} + +TEST_P(JniVmWebRtcAudioManagerTest, ForwardsMuteAndUnmute) { + jobject manager = Construct(); + ASSERT_EQ(Boolean(manager, "init"), JNI_TRUE); + Void(manager, "setMicrophoneMute", "(Z)V", JNI_TRUE); + Void(manager, "setMicrophoneMute", "(Z)V", JNI_FALSE); + EXPECT_EQ(probe_->mute_calls, (std::vector{true, false})); +} + +TEST_P(JniVmWebRtcAudioManagerTest, FailedHostInitDoesNotReportSuccess) { + probe_->init_succeeds = false; + jobject manager = Construct(); + EXPECT_EQ(Boolean(manager, "init"), JNI_FALSE); + Void(manager, "dispose"); + EXPECT_EQ(probe_->dispose_calls, 0); + probe_->init_succeeds = true; + EXPECT_EQ(Boolean(manager, "init"), JNI_TRUE); +} + +TEST_P(JniVmWebRtcAudioManagerTest, MissingHostAndInvalidParametersFailClosed) { + probe_->available = false; + jobject unavailable = Construct(); + EXPECT_EQ(Boolean(unavailable, "init"), JNI_FALSE); + probe_->available = true; + probe_->parameters.input_channels = 0; + jobject invalid = Construct(); + EXPECT_EQ(Boolean(invalid, "init"), JNI_FALSE); + probe_->parameters.input_channels = 1; + probe_->parameters.input_buffer_size_frames = 0; + EXPECT_EQ(Boolean(Construct(), "init"), JNI_FALSE); + vm_.ClearWebRtcAudioManagerCallbacks(); + EXPECT_EQ(Boolean(Construct(), "init"), JNI_FALSE); + EXPECT_EQ(probe_->cache_calls, 0); + EXPECT_EQ(probe_->init_calls, 0); +} + +TEST_P(JniVmWebRtcAudioManagerTest, NullHandleAndWrongConstructorDoNotCache) { + EXPECT_EQ(Boolean(Construct(0), "init"), JNI_FALSE); + EXPECT_EQ( + Boolean(Construct(reinterpret_cast(probe_.get()), "()V"), "init"), + JNI_FALSE); + EXPECT_EQ(probe_->cache_calls, 0); +} + +TEST_P(JniVmWebRtcAudioManagerTest, ClearingBindingStopsFurtherDispatch) { + jobject manager = Construct(); + ASSERT_EQ(Boolean(manager, "init"), JNI_TRUE); + vm_.ClearWebRtcAudioManagerCallbacks(); + EXPECT_EQ(Boolean(manager, "init"), JNI_FALSE); + Void(manager, "setMicrophoneMute", "(Z)V", JNI_TRUE); + Void(manager, "dispose"); + EXPECT_TRUE(probe_->mute_calls.empty()); + EXPECT_EQ(probe_->dispose_calls, 0); + EXPECT_EQ(probe_->init_calls, 1); +} + +TEST_P(JniVmWebRtcAudioManagerTest, + UnrelatedClassAndWrongSignaturesAreNotHandled) { + jclass other_class = env_->FindClass("com/example/AudioManager"); + jobject other = env_->NewObject( + other_class, env_->GetMethodID(other_class, "", "()V")); + EXPECT_EQ(Boolean(other, "init"), JNI_FALSE); + jobject manager = Construct(); + jmethodID wrong_init = env_->GetMethodID(cls_, "init", "(I)Z"); + EXPECT_EQ(env_->CallBooleanMethod(manager, wrong_init, 1), JNI_FALSE); + Void(manager, "setMicrophoneMute", "(I)V", JNI_TRUE); + EXPECT_TRUE(probe_->mute_calls.empty()); + EXPECT_EQ(probe_->init_calls, 0); +} + +TEST(JniVmWebRtcAudioManagerRegistrationTest, + RejectsWrongClassAndNativeSignature) { + VM vm; + JNIEnv* env = vm.GetJNIEnv(); + auto probe = std::make_shared(); + probe->parameters.sample_rate_hz = 48000; + probe->parameters.input_channels = probe->parameters.output_channels = 1; + probe->parameters.input_buffer_size_frames = + probe->parameters.output_buffer_size_frames = 480; + vm.SetWebRtcAudioManagerCallbacks(probe, ProbeCallbacks()); + jclass cls = env->FindClass(kManagerClass); + jclass other = env->FindClass("com/example/AudioManager"); + JNINativeMethod native = {const_cast("nativeCacheAudioParameters"), + const_cast(kCacheSignature), + reinterpret_cast(&CacheParameters)}; + ASSERT_EQ(env->RegisterNatives(other, &native, 1), JNI_OK); + native.signature = const_cast("(IIIJ)V"); + ASSERT_EQ(env->RegisterNatives(cls, &native, 1), JNI_OK); + jobject manager = env->NewObject(cls, env->GetMethodID(cls, "", "(J)V"), + reinterpret_cast(probe.get())); + EXPECT_EQ( + env->CallBooleanMethod(manager, env->GetMethodID(cls, "init", "()Z")), + JNI_FALSE); + EXPECT_EQ(probe->cache_calls, 0); +} + +INSTANTIATE_TEST_SUITE_P(AllJniRoutes, JniVmWebRtcAudioManagerTest, + testing::Values(JniRoute::kDirect, JniRoute::kVariadic, + JniRoute::kArray), + [](const testing::TestParamInfo& info) { + switch (info.param) { + case JniRoute::kDirect: + return "Direct"; + case JniRoute::kVariadic: + return "Variadic"; + case JniRoute::kArray: + return "Array"; + } + return "Unknown"; + }); + +} // namespace +} // namespace jnivm