Maybe fix vc

This commit is contained in:
komaruworld committed 2026-09-05 20:06:02 +03:00
1 parent 92c7d7b12b
commit b71873f2a5
7 files changed
+1004 -8

No files matched your search

+46
View File
@@ -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<void> 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<void> 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<void> 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<void> context;
WebRtcAudioRecordCallbacks callbacks;
@@ -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);
+79
View File
@@ -1,8 +1,10 @@
#include "mocktail/audio/webrtc_jni_audio_bridge.h"
#include <algorithm>
#include <atomic>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <limits>
#include <memory>
@@ -11,6 +13,7 @@
#include <string>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -26,6 +29,7 @@ struct CaptureSession {
jnivm::WebRtcAudioRecordDataCallback data_callback = nullptr;
void* data_context = nullptr;
std::unique_ptr<AudioCapture> capture;
std::shared_ptr<std::atomic<bool>> microphone_muted;
};
struct PlayoutSession {
@@ -46,6 +50,9 @@ struct PlayoutSession {
struct WebRtcJniAudioBridgeContext {
std::mutex mutex;
std::unordered_set<const void*> initialized_managers;
std::shared_ptr<std::atomic<bool>> microphone_muted =
std::make_shared<std::atomic<bool>>(false);
std::unordered_map<const void*, std::shared_ptr<CaptureSession>>
capture_sessions;
std::unordered_map<const void*, std::shared_ptr<PlayoutSession>>
@@ -54,9 +61,71 @@ struct WebRtcJniAudioBridgeContext {
bool shutting_down = false;
};
bool GetManagerParameters(void* opaque_context,
jnivm::WebRtcAudioManagerParameters* parameters) {
auto* context = static_cast<WebRtcJniAudioBridgeContext*>(opaque_context);
if (context == nullptr || parameters == nullptr) return false;
std::lock_guard<std::mutex> 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<WebRtcJniAudioBridgeContext*>(opaque_context);
if (context == nullptr || identity == nullptr) return false;
bool inserted = false;
{
std::lock_guard<std::mutex> 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<WebRtcJniAudioBridgeContext*>(opaque_context);
if (context == nullptr) return;
std::lock_guard<std::mutex> 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<WebRtcJniAudioBridgeContext*>(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<CaptureSession*>(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<std::mutex> 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();
+267 -2
View File
@@ -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), "<init>") == 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), "<init>") == 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), "<init>") == 0 &&
@@ -4648,6 +4732,13 @@ jobject ConstructObjectA(jclass clazz, jmethodID methodID, const jvalue *args) {
std::strcmp(MethodName(methodID), "<init>") == 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), "<init>") == 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), "<init>") == 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<void> context,
const WebRtcAudioManagerCallbacks& callbacks) {
WebRtcAudioManagerBinding previous;
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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(), &parameters)) {
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<CacheAudioParameters>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<void> 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;
}
+10
View File
@@ -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)
+301 -4
View File
@@ -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 <SDL3/SDL.h>
#include <gtest/gtest.h>
#include <unistd.h>
#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <mutex>
@@ -12,10 +16,10 @@
#include <thread>
#include <vector>
#include <gtest/gtest.h>
#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<WebRtcManagerProbe*>(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<char*>("nativeCacheAudioParameters"),
const_cast<char*>("(IIIZZZZZZZIIJ)V"),
reinterpret_cast<void*>(&CacheWebRtcParameters)};
EXPECT_EQ(env->RegisterNatives(cls, &cache, 1), JNI_OK);
return env->NewObject(cls, env->GetMethodID(cls, "<init>", "(J)V"),
reinterpret_cast<jlong>(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<std::mutex> lock(probe->mutex);
++probe->callbacks;
probe->last_size = size;
if (probe->buffer != nullptr && size > 0 && size <= probe->capacity) {
const auto* bytes = static_cast<const unsigned char*>(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<char*>("nativeCacheDirectBufferAddress"),
const_cast<char*>("(Ljava/nio/ByteBuffer;J)V"),
reinterpret_cast<void*>(&CacheWebRtcBuffer)},
{const_cast<char*>("nativeDataIsRecorded"), const_cast<char*>("(IJ)V"),
reinterpret_cast<void*>(&WebRtcDataIsRecorded)}};
const JNINativeMethod track_methods[] = {
{const_cast<char*>("nativeCacheDirectBufferAddress"),
const_cast<char*>("(Ljava/nio/ByteBuffer;J)V"),
reinterpret_cast<void*>(&CacheWebRtcPlayoutBuffer)},
{const_cast<char*>("nativeGetPlayoutData"), const_cast<char*>("(IJ)V"),
reinterpret_cast<void*>(&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, "<init>", "(J)V"),
reinterpret_cast<jlong>(&capture_probe));
jobject track = env->NewObject(
track_class, env->GetMethodID(track_class, "<init>", "(J)V"),
reinterpret_cast<jlong>(&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<std::mutex> 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<std::mutex> 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<unsigned char> 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<char*>("nativeCacheDirectBufferAddress"),
const_cast<char*>("(Ljava/nio/ByteBuffer;J)V"),
reinterpret_cast<void*>(&CacheWebRtcBuffer)},
{const_cast<char*>("nativeDataIsRecorded"), const_cast<char*>("(IJ)V"),
reinterpret_cast<void*>(&WebRtcDataIsRecorded)}};
ASSERT_EQ(env->RegisterNatives(record_class, methods, 2), JNI_OK);
jobject record = env->NewObject(
record_class, env->GetMethodID(record_class, "<init>", "(J)V"),
reinterpret_cast<jlong>(&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<std::mutex> 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<std::mutex> 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<SdlRecordingDevice> devices = {
{11, "USB Microphone"},
+297
View File
@@ -0,0 +1,297 @@
#include <gtest/gtest.h>
#include <array>
#include <memory>
#include <vector>
#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<jint, 5> cached_format{};
std::array<jboolean, 7> cached_features{};
std::vector<bool> 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<ManagerProbe*>(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<ManagerProbe*>(context);
*parameters = probe->parameters;
return probe->available;
};
callbacks.init = [](void* context, const void*) {
auto* probe = static_cast<ManagerProbe*>(context);
++probe->init_calls;
return probe->init_succeeds;
};
callbacks.dispose = [](void* context, const void*) {
++static_cast<ManagerProbe*>(context)->dispose_calls;
};
callbacks.set_microphone_mute = [](void* context, bool muted) {
static_cast<ManagerProbe*>(context)->mute_calls.push_back(muted);
};
return callbacks;
}
enum class JniRoute { kDirect, kVariadic, kArray };
class JniVmWebRtcAudioManagerTest : public testing::TestWithParam<JniRoute> {
protected:
void SetUp() override {
env_ = vm_.GetJNIEnv();
cls_ = env_->FindClass(kManagerClass);
probe_ = std::make_shared<ManagerProbe>();
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<char*>("nativeCacheAudioParameters"),
const_cast<char*>(signature),
reinterpret_cast<void*>(&CacheParameters)};
ASSERT_EQ(env_->RegisterNatives(cls_, &method, 1), JNI_OK);
}
jobject Construct(jlong handle, const char* signature = "(J)V") {
jmethodID ctor = env_->GetMethodID(cls_, "<init>", 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<jlong>(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<jint>(value));
} else if (GetParam() == JniRoute::kVariadic) {
env_->CallVoidMethod(manager, method, static_cast<jint>(value));
} else {
jvalue argument{};
argument.z = value;
env_->CallVoidMethodA(manager, method, &argument);
}
}
VM vm_;
JNIEnv* env_ = nullptr;
jclass cls_ = nullptr;
std::shared_ptr<ManagerProbe> 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<jlong>(probe_.get()));
EXPECT_EQ(probe_->cached_format,
(std::array<jint, 5>{48000, 2, 1, 960, 480}));
EXPECT_EQ(probe_->cached_features,
(std::array<jboolean, 7>{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<bool>{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<jlong>(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, "<init>", "()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<ManagerProbe>();
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<char*>("nativeCacheAudioParameters"),
const_cast<char*>(kCacheSignature),
reinterpret_cast<void*>(&CacheParameters)};
ASSERT_EQ(env->RegisterNatives(other, &native, 1), JNI_OK);
native.signature = const_cast<char*>("(IIIJ)V");
ASSERT_EQ(env->RegisterNatives(cls, &native, 1), JNI_OK);
jobject manager = env->NewObject(cls, env->GetMethodID(cls, "<init>", "(J)V"),
reinterpret_cast<jlong>(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<JniRoute>& info) {
switch (info.param) {
case JniRoute::kDirect:
return "Direct";
case JniRoute::kVariadic:
return "Variadic";
case JniRoute::kArray:
return "Array";
}
return "Unknown";
});
} // namespace
} // namespace jnivm