This commit is contained in:
2025-10-14 10:33:03 +09:00
parent 2f89643e6b
commit 1e985fd708
9 changed files with 464 additions and 116 deletions

317
todo30.txt Normal file
View File

@@ -0,0 +1,317 @@
● 문제를 찾았습니다!
에러: "Failed to start MediaCodec during async reset: -10000" (-10000 = AMEDIA_ERROR_INVALID_OPERATION)
원인: MediaCodec를 stop한 후 바로 start를 호출할 수 없습니다. MediaCodec 상태 머신에서 stop 후에는 configure를 다시 해야 start할 수 있습니다.
MediaCodec 생명주기:
Uninitialized → Configure → Start → Running
↑ ↓
└── Stop ──┘
Stop 후에는 다시 Configure → Start 순서로 가야 합니다.
더 나은 해결책: Play 버튼을 누를 때 MediaCodec를 Reset하지 않고, WebMFileReader만 처음으로 돌리는 것이 더 안전합니다. MediaCodec는 계속 실행
상태를 유지하면서 새로운 프레임을 받으면 됩니다.
VavCoreVulkanBridge의 Play 메서드를 확인하겠습니다:
● 완벽합니다! 이제 다각도로 종합 분석하겠습니다:
📊 시간순 이벤트 분석
09:33:37.160 - AsyncHandler 초기화 성공
09:33:37.168 - MediaCodec RUNNING 상태 진입
09:33:37.170 - MediaCodec이 10개 input buffer 제공 (index 0~9)
→ Queue에 10개 모두 쌓임
09:33:37.373 - Reset() 호출 (+203ms)
→ "Cleared 10 stale input buffer indices"
→ Queue 완전히 비워짐
→ MediaCodec은 건드리지 않음 (RUNNING 유지)
09:33:37.875 - DecodeFrameAsync 시도 (+502ms)
→ "No input buffer available after 500ms (queue size: 0)"
→ Queue가 여전히 비어있음
🔍 다각도 문제 분석
가능성 1: Buffer 반환 문제 ⭐⭐⭐⭐⭐
가장 유력한 원인입니다.
MediaCodec async mode의 동작 원리:
1. MediaCodec이 처음에 모든 available buffer를 제공 (0~9번)
2. 앱이 buffer를 사용하면 queueInputBuffer() 호출
3. MediaCodec이 buffer 처리 후 다시 그 buffer를 제공
우리가 한 일:
- Queue에서 index만 제거했음
- MediaCodec에게는 아무 것도 알리지 않음
- MediaCodec 입장: "모든 buffer를 이미 줬는데, 아직 반환 안 받았어"
증거:
Queue size: 10 → Reset() → Queue size: 0 → 500ms 대기 → Queue still 0
가능성 2: Callback Thread 중단 ⭐⭐⭐
MediaCodec의 callback thread가 멈췄거나, callback을 더 이상 보내지 않을 수 있음.
근거:
- Reset() 전: Callback이 정상적으로 왔음 (10개)
- Reset() 후: Callback이 전혀 오지 않음
반박:
- MediaCodec state는 여전히 RUNNING
- flush()를 호출하지 않았으므로 callback이 중단될 이유가 없음
가능성 3: 타이밍 이슈 ⭐⭐
Reset() 직후 바로 decode를 시작해서 MediaCodec이 준비되지 않았을 수 있음.
근거:
- Reset()과 DecodeFrameAsync 사이에 502ms 소요
- 충분한 시간인데도 buffer가 안 옴
반박:
- 500ms는 충분히 긴 시간
- MediaCodec RUNNING 상태가 유지됨
가능성 4: AsyncHandler 상태 문제 ⭐⭐
AsyncHandler 자체가 "대기 중" 상태로 잘못 설정되었을 수 있음.
근거:
- Queue clear 로직만 있고, AsyncHandler 상태 리셋은 없음
반박:
- DecodeFrameAsync는 정상적으로 호출됨
- 500ms timeout이 작동함 (대기는 하고 있음)
가능성 5: MediaCodec 내부 상태 불일치 ⭐⭐⭐⭐
MediaCodec이 내부적으로 "모든 buffer 할당됨" 상태라고 생각.
근거:
- flush() 없이는 MediaCodec의 내부 buffer pool이 리셋되지 않음
- Buffer lifecycle: FREE → ALLOCATED → QUEUED → PROCESSING → FREE
- 우리는 ALLOCATED 상태의 buffer를 그냥 버렸음
💡 해결 방안 비교
방안 A: Queue의 각 buffer를 빈 패킷으로 반환 ⭐⭐⭐⭐⭐
// Reset() 함수에서
while (!queue.empty()) {
int index = queue.front();
queue.pop();
// MediaCodec에게 "이 buffer 안 쓸게요" 알림
AMediaCodec_queueInputBuffer(m_codec, index, 0, 0, 0, 0);
}
장점:
- MediaCodec을 건드리지 않음 (RUNNING 유지)
- Buffer lifecycle이 정상적으로 완료됨
- MediaCodec이 buffer를 다시 제공할 것임
단점:
- 빈 패킷이 처리되는 시간 필요
- MediaCodec이 이 빈 패킷을 처리해야 함
방안 B: flush() 후 약간 대기 ⭐⭐⭐
AMediaCodec_flush(m_codec);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
m_async_handler->ClearInputQueue();
장점:
- MediaCodec 내부 상태가 완전히 리셋됨
- Buffer pool이 초기화됨
단점:
- 이전에 이미 실패했었음
- "No input buffer after 500ms" 동일한 문제
방안 C: MediaCodec 재생성 ⭐
완전히 새로 만들기 (이전에 시도했던 stop→configure→start)
장점:
- 확실한 초기화
단점:
- 이미 실패함 (error -10000)
- 너무 무거운 작업
방안 D: Dequeue 강제 호출 ⭐⭐⭐⭐
// Reset()에서 모든 buffer를 강제로 dequeue
for (int i = 0; i < 10; i++) {
AMediaCodecBufferInfo info;
ssize_t idx = AMediaCodec_dequeueInputBuffer(m_codec, 0);
if (idx >= 0) {
// 즉시 빈 상태로 반환
AMediaCodec_queueInputBuffer(m_codec, idx, 0, 0, 0, 0);
}
}
장점:
- Buffer를 명시적으로 회수
- MediaCodec에게 명확한 신호
단점:
- Async mode에서 dequeueInputBuffer 사용이 권장되지 않음
🎯 추천 해결책: 방안 A
근거:
1. MediaCodec의 buffer lifecycle을 존중
2. Async mode 원칙 위반하지 않음
3. 논리적으로 가장 명확함
구현:
bool MediaCodecAV1Decoder::Reset() {
if (m_use_async_mode && m_async_handler) {
LOGI("[Reset] Async mode detected - returning buffers and clearing queue");
// 1. Queue에 있는 모든 buffer를 빈 상태로 반환
while (!m_async_handler->IsInputQueueEmpty()) {
int buffer_index = m_async_handler->PopInputBufferIndex(0); // no timeout
if (buffer_index >= 0) {
// 빈 패킷으로 반환 (MediaCodec이 buffer를 재사용할 수 있게)
AMediaCodec_queueInputBuffer(m_codec, buffer_index, 0, 0, 0, 0);
}
}
LOGI("[Reset] All buffers returned, async mode reset complete");
return true;
}
// Sync mode...
}
이 방법이 작동할 가능성이 가장 높습니다. 구현할까요?
● 🎯 Root Cause Found - Multi-Angle Analysis
vavcore_vulkan_bridge.cpp:188-216 - The Play() method ALWAYS calls vavcore_reset() before starting playback:
bool VavCoreVulkanBridge::Play() {
LOGI("Starting playback...");
// Reset to beginning before starting playback
LOGI("Resetting video to beginning...");
VavCoreResult resetResult = vavcore_reset(m_player);
if (resetResult != VAVCORE_SUCCESS) {
LOGE("Failed to reset video: %d", resetResult);
// Continue anyway - might still work if already at beginning
} else {
LOGI("Successfully reset video to beginning");
m_currentPositionUs = 0;
m_frameNumber = 0;
}
SetPlaybackState(PlaybackState::PLAYING);
StartContinuousPlayback();
return true;
}
📊 Multi-Angle Root Cause Analysis:
1⃣ Design Perspective (★★★★★ PRIMARY CAUSE)
- Design Decision: Play() calls Reset() to ensure video always starts from beginning
- Intent: Good practice for media players (reset to frame 0 before playback)
- Problem: Works fine for synchronous mode, but async mode implementation is incomplete
2⃣ Implementation Perspective (★★★★★ CRITICAL BUG)
- Current Reset() Implementation: Clears queue but doesn't return buffers
- MediaCodec Expectation: App must call queueInputBuffer() or releaseOutputBuffer() for each buffer
- Result: MediaCodec thinks all 10 buffers are still "in use" by the app
3⃣ Lifecycle Perspective (★★★★ TIMING)
09:33:37.030 - User clicks "Test" button
09:33:37.161 - MediaCodec initializes in ASYNC mode
09:33:37.170 - MediaCodec provides 10 input buffers (indices 0-9)
09:33:37.170 - AsyncHandler queues all 10 buffers
09:33:37.373 - User clicks "Play" → Play() calls Reset() (+203ms after init)
09:33:37.373 - Reset() clears queue (10 buffers lost in limbo)
09:33:37.875 - DecodeFrameAsync: "No input buffer available" (+502ms)
4⃣ MediaCodec State Machine Perspective (★★★★ BUFFER ACCOUNTING)
- MediaCodec's View: "I gave you 10 buffers, you never returned them"
- Our View: "We cleared the queue, we don't need those buffers anymore"
- Mismatch: MediaCodec won't provide new buffers until old ones are returned
5⃣ Architecture Perspective (★★ DESIGN PATTERN)
- Async Mode Philosophy: Callback-driven buffer lifecycle
- onInputBufferAvailable: "Here's a buffer, use it and return it"
- onOutputBufferAvailable: "Here's decoded data, consume it and return it"
- Missing: Proper buffer return on Reset/Flush/Stop operations
🔧 The Complete Picture:
Why Reset() exists: Play() intentionally calls Reset() to start video from beginning
Why it fails: Reset() only clears our internal queue, doesn't notify MediaCodec
Why 203ms timing:
1. Video loads at 09:33:37.030
2. MediaCodec initializes (160ms)
3. User auto-play delay (200ms - see MainActivity.java:678)
4. Play() called → immediately calls Reset()
Why "No input buffer available":
- MediaCodec buffer pool: 10 total buffers
- Buffers "loaned out": 10 (indices 0-9)
- Buffers available: 0 (all still marked as "in use")
- Queue cleared: Yes, but MediaCodec doesn't know that
> "Play() calls Reset() to ensure video always starts from beginning" 왜 Play() 전에 Reset() 호출해야하는지 이해가 되지 않는다.
● Good question! You're right to question this design. Looking at the code in vavcore_vulkan_bridge.cpp:200-210:
// Reset to beginning before starting playback
LOGI("Resetting video to beginning...");
VavCoreResult resetResult = vavcore_reset(m_player);
The Current Design Issue:
- Play() ALWAYS calls Reset(), even when video is already at position 0 (freshly loaded)
- This is unnecessary and causes the buffer starvation problem we're fixing
- It clears state that was just established during LoadVideoFile()
Why it was done this way:
- Ensures consistent behavior: "Play button always starts from beginning"
- If user watched video to end, pressing Play starts over
- Simple state machine: STOPPED → Reset() → PLAYING
Better Design Options:
1. Conditional Reset: Only reset if m_currentPositionUs != 0
2. Separate Methods: PlayFromBeginning() vs Resume()
3. Check Decoder State: Query decoder position before resetting
For Now: I'm implementing the fix to make Reset() work properly with async mode by returning buffers to MediaCodec. This makes the current
design work, even if the design itself could be improved.
Would you like me to also add a check to skip Reset() when already at position 0? That would optimize the common case of loading a video and
immediately playing it.

View File

@@ -25,7 +25,7 @@ android {
}
ndk {
abiFilters.addAll(listOf("arm64-v8a", "armeabi-v7a"))
abiFilters.addAll(listOf("arm64-v8a"))
}
}

View File

@@ -196,18 +196,10 @@ bool VavCoreVulkanBridge::Play() {
return true;
}
LOGI("Starting playback...");
// Reset to beginning before starting playback
LOGI("Resetting video to beginning...");
VavCoreResult resetResult = vavcore_reset(m_player);
if (resetResult != VAVCORE_SUCCESS) {
LOGE("Failed to reset video: %d", resetResult);
// Continue anyway - might still work if already at beginning
} else {
LOGI("Successfully reset video to beginning");
m_currentPositionUs = 0;
m_frameNumber = 0;
}
LOGI("Starting playback from current position...");
// Play from current position (no reset - that's what Stop() is for)
// This allows Resume functionality after Pause()
SetPlaybackState(PlaybackState::PLAYING);
// Start continuous playback thread

View File

@@ -389,11 +389,35 @@ bool MediaCodecAV1Decoder::Reset() {
return false;
}
// Delegate to buffer processor for reset
if (m_buffer_processor && m_buffer_processor->IsInitialized()) {
if (!m_buffer_processor->Reset()) {
LogError("Failed to reset buffer processor");
return false;
// Optimization: Skip reset if decoder is already at beginning (no frames decoded yet)
// This avoids expensive buffer return/reallocation cycle when loading video and immediately playing
if (m_stats.frames_decoded == 0) {
LogInfo("Reset(): Decoder already at beginning (frames_decoded=0), skipping reset");
return true;
}
// CRITICAL: In async mode, MediaCodec should NOT be reset
// Flush removes all queued buffers but callback registration remains intact
// Input buffer callbacks will continue to arrive as buffers become available
// Solution: Return all queued buffers to MediaCodec, then clear the queue
if (IsAsyncModeEnabled()) {
LogInfo("Reset(): Async mode detected - returning buffers to MediaCodec");
// Return all queued buffers to MediaCodec before clearing
// This tells MediaCodec "I'm done with these buffers, you can reuse them"
if (m_async_handler) {
m_async_handler->ReturnAndClearInputBuffers();
LogInfo("Reset(): Returned all buffers to MediaCodec and cleared queue");
}
LogInfo("Reset(): Async mode reset complete - MediaCodec still running, buffers returned");
} else {
// Sync mode: use normal flush
if (m_buffer_processor && m_buffer_processor->IsInitialized()) {
if (!m_buffer_processor->Reset()) {
LogError("Failed to reset buffer processor");
return false;
}
}
}
@@ -929,64 +953,9 @@ bool MediaCodecAV1Decoder::DetectHardwareCapabilities() {
}
std::vector<std::string> MediaCodecAV1Decoder::GetAvailableCodecs() {
std::vector<std::string> codecs;
// NOTE: NDK 26 removed AMediaCodecList API, so we test potential codec names directly
LogInfo("Enumerating AV1 decoders by testing codec names (NDK 26 compatibility)");
// Comprehensive list of potential AV1 decoders across different Android devices
std::vector<std::string> potential_codecs = {
// Samsung Exynos decoders (Galaxy S24 Ultra, etc.)
"c2.exynos.av1.decoder",
"c2.exynos2400.av1.decoder",
"c2.sec.av1.decoder",
"OMX.Exynos.AV1.Decoder",
// Qualcomm Snapdragon decoders
"c2.qti.av1.decoder",
"c2.qcom.av1.decoder",
"OMX.qcom.video.decoder.av1",
"OMX.qti.video.decoder.av1",
// MediaTek Dimensity decoders
"c2.mtk.av1.decoder",
"OMX.MTK.VIDEO.DECODER.AV1",
// Google standard decoders
"c2.android.av1.decoder",
"OMX.google.av1.decoder",
"c2.google.av1.decoder",
// Generic/fallback decoders
"av1.decoder",
"video.av1.decoder"
};
// Test each codec name to see if it can be created
for (const std::string& codec_name : potential_codecs) {
AMediaCodec* test_codec = AMediaCodec_createCodecByName(codec_name.c_str());
if (test_codec) {
LogInfo("Found available codec: " + codec_name);
codecs.push_back(codec_name);
AMediaCodec_delete(test_codec);
} else {
// Codec not available - this is expected behavior for cross-device compatibility
}
}
if (codecs.empty()) {
LogWarning("No AV1 decoders found by name testing");
// Final fallback: try generic type-based decoder creation
AMediaCodec* fallback_codec = AMediaCodec_createDecoderByType("video/av01");
if (fallback_codec) {
LogInfo("Found generic AV1 decoder via type");
codecs.push_back("generic-av01-decoder");
AMediaCodec_delete(fallback_codec);
}
}
return codecs;
// Delegate to MediaCodecSelector to avoid duplication
// MediaCodecSelector has the comprehensive codec list and handles caching
return m_codec_selector->GetAvailableCodecNames();
}
void MediaCodecAV1Decoder::LogError(const std::string& message) const {

View File

@@ -317,6 +317,48 @@ void MediaCodecAsyncHandler::ClearInputBufferQueue() {
LogInfo("Cleared " + std::to_string(cleared_count) + " stale input buffer indices from queue");
}
void MediaCodecAsyncHandler::ReturnAndClearInputBuffers() {
if (!m_codec) {
LogError("ReturnAndClearInputBuffers: MediaCodec not initialized");
return;
}
std::lock_guard<std::mutex> lock(m_async_mutex);
int returned_count = 0;
LogInfo("ReturnAndClearInputBuffers: Returning " + std::to_string(m_async_input_buffer_queue.size()) +
" buffers to MediaCodec");
// Return each queued buffer to MediaCodec with empty packet
while (!m_async_input_buffer_queue.empty()) {
int32_t buffer_index = m_async_input_buffer_queue.front();
m_async_input_buffer_queue.pop();
// Queue input buffer with size=0 (empty packet)
// This tells MediaCodec "I'm done with this buffer, you can reuse it"
media_status_t status = AMediaCodec_queueInputBuffer(
m_codec,
buffer_index,
0, // offset
0, // size (empty packet)
0, // timestamp
0 // flags
);
if (status == AMEDIA_OK) {
returned_count++;
LogInfo("ReturnAndClearInputBuffers: Returned buffer " + std::to_string(buffer_index) +
" to MediaCodec");
} else {
LogError("ReturnAndClearInputBuffers: Failed to return buffer " + std::to_string(buffer_index) +
" to MediaCodec: " + std::to_string(status));
}
}
LogInfo("ReturnAndClearInputBuffers: Successfully returned " + std::to_string(returned_count) +
" buffers to MediaCodec");
}
bool MediaCodecAsyncHandler::ProcessAsyncOutputFrame(int32_t output_index, AMediaCodecBufferInfo* buffer_info, VideoFrame& output_frame) {
if (!m_codec || output_index < 0 || !buffer_info) {
LogError("ProcessAsyncOutputFrame: Invalid parameters");

View File

@@ -67,6 +67,7 @@ public:
// Queue management
void ClearInputBufferQueue();
void ReturnAndClearInputBuffers(); // Returns buffers to MediaCodec before clearing queue
// Async callback handlers (static methods for C callback compatibility)
static void OnAsyncInputAvailable(AMediaCodec* codec, void* userdata, int32_t index);

View File

@@ -248,10 +248,17 @@ std::vector<MediaCodecSelector::CodecInfo> MediaCodecSelector::GetAvailableCodec
// Private: Get available codec names by testing potential names
std::vector<std::string> MediaCodecSelector::GetAvailableCodecNames() {
std::vector<std::string> codecs;
// Static cache: enumerate codecs only once per app lifecycle
static std::vector<std::string> cached_codecs;
static bool enumerated = false;
if (enumerated) {
LOGI("Using cached codec list (already enumerated)");
return cached_codecs;
}
// NOTE: NDK 26 removed AMediaCodecList API, so we test potential codec names directly
LOGI("Enumerating AV1 decoders by testing codec names (NDK 26 compatibility)");
LOGI("Enumerating AV1 decoders by testing codec names (NDK 26 compatibility) - FIRST TIME");
// Comprehensive list of potential AV1 decoders across different Android devices
std::vector<std::string> potential_codecs = {
@@ -287,17 +294,20 @@ std::vector<std::string> MediaCodecSelector::GetAvailableCodecNames() {
for (const auto& codec_name : potential_codecs) {
AMediaCodec* test_codec = AMediaCodec_createCodecByName(codec_name.c_str());
if (test_codec != nullptr) {
codecs.push_back(codec_name);
cached_codecs.push_back(codec_name);
LOGI("Found available codec: %s", codec_name.c_str());
AMediaCodec_delete(test_codec);
}
}
if (codecs.empty()) {
if (cached_codecs.empty()) {
LOGW("No AV1 codecs found on this device");
}
return codecs;
// Mark as enumerated and return cached result
enumerated = true;
LOGI("Codec enumeration complete - %zu codec(s) cached", cached_codecs.size());
return cached_codecs;
}
// Check if codec name indicates AV1 support

View File

@@ -192,6 +192,30 @@ std::vector<std::string> VideoDecoderFactory::GetAvailableDecoders(VideoCodecTyp
void VideoDecoderFactory::RegisterAV1Decoder(const DecoderRegistration& registration) {
auto& decoders = GetDecoderList(VideoCodecType::AV1);
// Check for duplicate registration (prevent same decoder being registered multiple times)
for (const auto& existing : decoders) {
if (existing.name == registration.name) {
#ifdef DEBUG
// Debug build: Crash with assertion to detect bugs early
LOGF_ERROR("[VideoDecoderFactory] FATAL: Decoder '%s' already registered - duplicate registration is a bug!",
registration.name.c_str());
#ifdef ANDROID
__android_log_assert("duplicate_decoder", "VavCore-DecoderFactory",
"Decoder '%s' already registered - this is a programming error",
registration.name.c_str());
#else
assert(false && "Duplicate decoder registration detected");
#endif
#else
// Release build: Log warning and skip silently
LOGF_WARNING("[VideoDecoderFactory] Decoder '%s' already registered - skipping duplicate registration",
registration.name.c_str());
#endif
return;
}
}
decoders.push_back(registration);
// Sort by priority (lower numbers = higher priority)

View File

@@ -23,10 +23,11 @@ using namespace VavCore;
extern "C" bool PerformSafeDllInitialization();
extern "C" bool IsDllReadyForInitialization();
// Forward declarations for decoder registration functions
extern "C" void RegisterAV1Decoders();
// Forward declaration for Android JavaVM access
#ifdef ANDROID
extern "C" void RegisterMediaCodecDecoders();
namespace VavCore {
JavaVM* GetAndroidJavaVM();
}
#endif
// Global state
@@ -58,6 +59,26 @@ JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) {
g_jni_loaded = false;
}
// Internal function to get JavaVM for use by MediaCodec decoders
// Defined here so it's available before first use
namespace VavCore {
JavaVM* GetAndroidJavaVM() {
std::lock_guard<std::mutex> lock(g_mutex);
// DEBUG: Log every call to GetAndroidJavaVM()
LOGF_INFO("[GetAndroidJavaVM] Called - g_android_java_vm = %p", g_android_java_vm);
LOGF_INFO("[GetAndroidJavaVM] g_jni_loaded = %d", g_jni_loaded);
if (g_android_java_vm != nullptr) {
LOGF_INFO("[GetAndroidJavaVM] Returning valid JavaVM: %p", g_android_java_vm);
} else {
LOGF_ERROR("[GetAndroidJavaVM] JavaVM is NULL! JNI may not have been initialized.");
}
return g_android_java_vm;
}
}
// Android-specific readiness check
static bool IsAndroidLibraryReady() {
return g_jni_loaded;
@@ -248,19 +269,12 @@ VAVCORE_API VavCoreResult vavcore_initialize(void) {
return VAVCORE_ERROR_INIT_FAILED;
}
#endif
// Register available decoders
#ifdef ANDROID
// Android: ONLY register MediaCodec hardware decoder
// Do NOT fallback to dav1d CPU decoder
RegisterMediaCodecDecoders();
#else
// Windows: Register all available decoders including dav1d fallback
RegisterAV1Decoders();
#endif
// Initialize decoder factory
// Note: InitializeFactory() handles platform-specific decoder registration internally
// - Android: Explicitly calls RegisterMediaCodecDecoders() due to JNI initialization order
// - Windows: Uses static initialization for decoder registration
VideoDecoderFactory::InitializeFactory();
g_initialized = true;
return VAVCORE_SUCCESS;
@@ -1043,25 +1057,4 @@ VAVCORE_API VavCoreResult vavcore_convert_yuv_to_rgb(
return VAVCORE_ERROR_NOT_SUPPORTED;
}
} // extern "C"
#ifdef ANDROID
// Internal function to get JavaVM for use by MediaCodec decoders
namespace VavCore {
JavaVM* GetAndroidJavaVM() {
std::lock_guard<std::mutex> lock(g_mutex);
// DEBUG: Log every call to GetAndroidJavaVM()
LOGF_INFO("[GetAndroidJavaVM] Called - g_android_java_vm = %p", g_android_java_vm);
LOGF_INFO("[GetAndroidJavaVM] g_jni_loaded = %d", g_jni_loaded);
if (g_android_java_vm != nullptr) {
LOGF_INFO("[GetAndroidJavaVM] Returning valid JavaVM: %p", g_android_java_vm);
} else {
LOGF_ERROR("[GetAndroidJavaVM] JavaVM is NULL! JNI may not have been initialized.");
}
return g_android_java_vm;
}
}
#endif
} // extern "C"