15 KiB
MediaCodec Async Mode Comprehensive Diagnosis
Date: 2025-10-14 Platform: Android (Qualcomm sun SoC, API 36) Decoder: MediaCodec AV1 (c2.qti.av1.decoder) Build: VavCore arm64-v8a (Debug, built 2025-10-14 13:54:50)
Executive Summary
MediaCodec async mode callbacks are triggering successfully, but frames are not being delivered to the application. The root cause has been isolated to the ProcessAsyncOutputFrame function, which appears to be either:
- Not being called at all despite the lambda being invoked
- Failing silently without producing any log output
- Crashing before the first log statement executes
This represents a critical synchronization failure between MediaCodec's output callbacks and the application's frame processing pipeline.
Timeline Analysis
Successful Codec Initialization
13:51:13.xxx - MediaCodec created successfully
13:51:13.xxx - Async callbacks registered
13:51:13.xxx - Codec configured with csd-0 (21 bytes AV1 sequence header)
13:51:13.xxx - Codec started
Input Buffer Processing
13:51:14.384 - queueInputBuffer called (index=1, size=210287 bytes)
13:51:14.388 - queueInputBuffer returned SUCCESS (status=0)
Latency: 4ms (normal)
Output Callback Triggered
13:51:14.398 - OnAsyncOutputAvailable called (index=0)
13:51:14.398 - Calling onOutputBufferAvailable lambda
Decode Latency: 10ms from input (excellent)
Frame Processing Failure
13:51:14.888 - WaitForAsyncFrame timed out after 500ms
Problem: Output callback triggered but ProcessAsyncOutputFrame never logged anything
Multi-Perspective Diagnosis
1. Code Flow Perspective
Expected Flow:
queueInputBuffer()
↓ (10ms - MediaCodec hardware decode)
OnAsyncOutputAvailable() [static callback]
↓
onOutputBufferAvailable() [lambda]
↓
ProcessAsyncOutputFrame() [should log "ENTRY"]
↓
releaseOutputBuffer(render=true)
↓
AcquireLatestImage()
↓
m_async_output_queue.push()
↓
m_async_condition.notify_one()
↓
WaitForAsyncFrame() [wakes up]
Actual Flow:
queueInputBuffer() ✅
↓
OnAsyncOutputAvailable() ✅
↓
onOutputBufferAvailable() ✅ (log shows "Calling onOutputBufferAvailable lambda")
↓
ProcessAsyncOutputFrame() ❌ (NO LOGS AT ALL - never executes or crashes immediately)
↓
[Pipeline breaks here]
↓
WaitForAsyncFrame() ❌ (times out - condition never notified)
2. Synchronization Perspective
Mutexes and Condition Variables:
m_async_mutex- protects output queuem_async_condition- notifies waiting threads
Problem: The condition variable is never notified because ProcessAsyncOutputFrame never reaches the code that pushes to the queue.
Evidence:
// This code at line 102-114 never executes the "push" because ProcessAsyncOutputFrame returns false
if (ProcessAsyncOutputFrame(index, bufferInfo, frame)) { // Returns false (or crashes)
std::lock_guard<std::mutex> lock(m_async_mutex);
// ... push to queue
m_async_condition.notify_one(); // Never reached
}
3. Lambda Capture Perspective
Lambda Definition (line 99-116):
m_async_callbacks.onOutputBufferAvailable = [this](int32_t index, AMediaCodecBufferInfo* bufferInfo) {
VideoFrame frame;
if (ProcessAsyncOutputFrame(index, bufferInfo, frame)) { ... }
};
Captured State:
this- MediaCodecAsyncHandler instance pointer
Potential Issues:
- Lambda executes on MediaCodec's callback thread (different from main thread)
- If
thispointer is invalid, undefined behavior occurs - If
ProcessAsyncOutputFrameis not properly linked, linker error or crash
4. Threading Perspective
Thread Roles:
- Decode Thread (tid=6914): Calls queueInputBuffer, waits for output
- Callback Thread (tid=6899): MediaCodec triggers OnAsyncOutputAvailable
Observations:
- Threads are different (expected for async callbacks)
- OnAsyncOutputAvailable logs show correct thread safety
- Lambda invocation logged successfully (line 4 of logs)
Problem: ProcessAsyncOutputFrame is supposed to execute on callback thread but never logs
5. Build and Deployment Perspective
Build Verification:
VavCore library: 2025-10-14 13:54:50 (5.4 MB, arm64-v8a)
APK assembly: UP-TO-DATE (Gradle cached)
JNI libs: Copied to jniLibs/arm64-v8a/
Concern: APK shows "UP-TO-DATE" which means Gradle didn't detect changes. Possible reasons:
- Library timestamp didn't change enough for Gradle to detect
- APK was using cached version
- Library wasn't properly copied during assembly
6. MediaCodec Documentation Comparison
Official Android MediaCodec Async Pattern (android.com/reference/android/media/MediaCodec):
Recommended Pattern:
// 1. Set async callbacks BEFORE configure()
AMediaCodec_setAsyncNotifyCallback(codec, callbacks, userdata);
// 2. Configure codec
AMediaCodec_configure(codec, format, surface, nullptr, 0);
// 3. Start codec
AMediaCodec_start(codec);
// 4. In onOutputBufferAvailable callback:
// - Get buffer with getOutputBuffer()
// - Process data
// - Release with releaseOutputBuffer(render=true) for surface rendering
// - Or releaseOutputBuffer(render=false) for buffer access
Current Implementation: ✅ Follows the pattern correctly
Key Documentation Points:
Buffer Lifecycle:
"When you are done with a buffer, you must return it to the codec by calling releaseOutputBuffer either with or without rendering."
Current Implementation: ✅ Calls releaseOutputBuffer(render=true) at line 332
Async Callback Thread Safety:
"Callbacks will be called on a separate thread that is managed by the framework. Applications should not block in these callbacks."
Current Implementation: ⚠️ ProcessAsyncOutputFrame calls AcquireLatestImage() which may block
Surface Rendering:
"When using output surface, you must call releaseOutputBuffer with render=true to make the buffer available for rendering. The surface will update asynchronously."
Current Implementation: ✅ Calls releaseOutputBuffer(render=true)
Potential Discrepancy:
The documentation states:
"When using a Surface, releasing a buffer with render=true does not guarantee that the frame is displayed immediately. The display timing depends on the Surface implementation."
Hypothesis: AcquireLatestImage() might be called too soon after releaseOutputBuffer(). The ImageReader may not have received the frame yet.
Root Cause Hypotheses
Hypothesis 1: Lambda Not Calling ProcessAsyncOutputFrame ❌
Evidence Against: Log shows "Calling onOutputBufferAvailable lambda" which is immediately before the function call
Hypothesis 2: ProcessAsyncOutputFrame Crashing Before First Log ⚠️
Plausibility: High Mechanism:
- Crash occurs before line 291 (first LogInfo)
- No exception handling in lambda
- Crash is silent (no FATAL log)
Test: Add try-catch or check for crashes
Hypothesis 3: Compiler Optimization Removed Function ❌
Evidence Against: Function is referenced in lambda, must be linked
Hypothesis 4: Timing Issue with ImageReader Synchronization ✅
Plausibility: Very High Mechanism:
AMediaCodec_releaseOutputBuffer(m_codec, output_index, true); // Line 332
// MediaCodec renders frame to ImageReader's Surface asynchronously
AHardwareBuffer* ahb = surface_manager->AcquireLatestImage(); // Line 356 - TOO SOON?
Supporting Evidence from Android Docs:
"Surface rendering is asynchronous. Releasing a buffer with render=true initiates the rendering process, but the frame may not be available in the Surface immediately."
Fix: Add synchronization between releaseOutputBuffer and AcquireLatestImage
Hypothesis 5: Build Cache Issue ✅
Plausibility: High Evidence: Gradle showed "UP-TO-DATE" for most tasks Mechanism: APK uses old libVavCore.so without new ProcessAsyncOutputFrame logs
Test: Force clean build
Comparison with Android MediaCodec Best Practices
✅ Correct Implementations
- Async callback registration before configure() - Line 79-82
- Using ImageReader for zero-copy GPU pipeline - Recommended for Vulkan
- releaseOutputBuffer(render=true) for surface rendering - Line 332
- Progressive timeout strategy (500ms first frame, 100ms thereafter)
⚠️ Potential Issues
-
Immediate AcquireLatestImage after releaseOutputBuffer
- Problem: No synchronization between MediaCodec rendering and ImageReader availability
- Fix: Use ImageReader.OnImageAvailableListener or add small delay
-
Blocking call in async callback
- Problem: ProcessAsyncOutputFrame calls blocking operations (AcquireLatestImage)
- Documentation: "Applications should not block in these callbacks"
- Fix: Move heavy processing to separate thread or use non-blocking acquisition
-
No frame drop handling
- Problem: If ProcessAsyncOutputFrame fails, frame is lost forever
- Fix: Implement frame buffer or retry logic
Recommended Solutions
Solution 1: Add ImageReader Synchronization (HIGH PRIORITY)
Problem: AcquireLatestImage() called immediately after releaseOutputBuffer() without waiting for frame to be ready.
Fix:
// After releaseOutputBuffer
media_status_t status = AMediaCodec_releaseOutputBuffer(m_codec, output_index, true);
// Add synchronization: wait for ImageReader to have new image
// Option A: Use ImageReader callback (requires Java setup)
// Option B: Add small delay for frame to propagate
std::this_thread::sleep_for(std::chrono::milliseconds(2)); // 2ms should be enough
AHardwareBuffer* ahb = surface_manager->AcquireLatestImage();
Solution 2: Move ProcessAsyncOutputFrame to Worker Thread (MEDIUM PRIORITY)
Problem: Blocking callback thread violates Android guidelines.
Fix:
m_async_callbacks.onOutputBufferAvailable = [this](int32_t index, AMediaCodecBufferInfo* bufferInfo) {
// Store output buffer info in queue
{
std::lock_guard<std::mutex> lock(m_async_mutex);
m_output_buffer_queue.push({index, *bufferInfo});
m_async_condition.notify_one();
}
};
// Separate worker thread processes buffers
void OutputProcessingThread() {
while (m_async_processing_active) {
// Wait for buffer
std::unique_lock<std::mutex> lock(m_async_mutex);
m_async_condition.wait(lock, [this] { return !m_output_buffer_queue.empty(); });
auto [index, bufferInfo] = m_output_buffer_queue.front();
m_output_buffer_queue.pop();
lock.unlock();
// Process frame (can block safely on worker thread)
VideoFrame frame;
if (ProcessAsyncOutputFrame(index, &bufferInfo, frame)) {
// Add to output queue
}
}
}
Solution 3: Force Clean Build (IMMEDIATE)
Problem: Gradle cached old APK without new ProcessAsyncOutputFrame logs.
Fix:
cd vav2/platforms/android/applications/vav2player
./gradlew clean
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
Solution 4: Add Exception Handling (IMMEDIATE)
Problem: Silent crashes prevent diagnosis.
Fix:
m_async_callbacks.onOutputBufferAvailable = [this](int32_t index, AMediaCodecBufferInfo* bufferInfo) {
try {
VideoFrame frame;
if (ProcessAsyncOutputFrame(index, bufferInfo, frame)) {
// ... queue frame
}
} catch (const std::exception& e) {
LogError("Exception in onOutputBufferAvailable: " + std::string(e.what()));
} catch (...) {
LogError("Unknown exception in onOutputBufferAvailable");
}
};
Android MediaCodec Documentation Key Findings
Official Async Mode Lifecycle
From developer.android.com/reference/android/media/MediaCodec:
-
Initialization:
setCallback() → configure() → start()✅ Current implementation follows this order
-
Buffer Processing:
- Input: queueInputBuffer() on app thread
- Output: onOutputBufferAvailable() on MediaCodec thread
- Release: releaseOutputBuffer() in callback
-
Surface Rendering:
"When rendering to a Surface, you must call releaseOutputBuffer with render=true. The frame will be sent to the surface asynchronously."
-
Thread Safety:
"The callbacks will be called on a separate thread managed by the framework. Applications should not block in callback methods."
ImageReader Documentation
From developer.android.com/reference/android/media/ImageReader:
Key Point:
"When a frame is rendered to the ImageReader's Surface, the OnImageAvailableListener will be called on a Handler thread. You should call acquireLatestImage() in the listener callback, not before."
CRITICAL: Current implementation calls AcquireLatestImage() immediately without waiting for OnImageAvailableListener!
Correct Pattern:
// Setup ImageReader with listener
imageReader.setOnImageAvailableListener(listener, handler);
// In MediaCodec callback
AMediaCodec_releaseOutputBuffer(codec, index, true);
// DON'T call AcquireLatestImage() here!
// In ImageReader listener (triggered automatically when frame is ready)
AHardwareBuffer* ahb = imageReader->acquireLatestImage();
Conclusions
Definitive Issues Found
-
Build Cache Problem: APK not rebuilt with new ProcessAsyncOutputFrame logs
- Evidence: Gradle "UP-TO-DATE", no logs from new code
- Fix: Clean build required
-
ImageReader Synchronization Violation: Calling AcquireLatestImage() without waiting for OnImageAvailableListener
- Evidence: Android documentation clearly states this is incorrect
- Impact: Frame not ready when acquired, returns null
- Fix: Implement proper ImageReader callback pattern
-
Blocking Callback Thread: ProcessAsyncOutputFrame performs heavy operations in MediaCodec callback
- Evidence: Android guidelines forbid blocking in callbacks
- Impact: Potential deadlock or performance degradation
- Fix: Move processing to worker thread
Likely Root Cause
Primary: ImageReader synchronization issue - AcquireLatestImage() called before frame is available in ImageReader.
Secondary: Build cache prevented testing of new diagnostic logs.
Next Steps
- Immediate: Clean build and verify new logs appear
- Critical: Implement ImageReader OnImageAvailableListener callback
- Important: Add timing/synchronization between releaseOutputBuffer and AcquireLatestImage
- Enhancement: Move ProcessAsyncOutputFrame to worker thread
Technical Specifications
Device Information
Manufacturer: Samsung
SoC: sun (Qualcomm)
Android API: 36
AV1 Hardware: MediaCodec-based (c2.qti.av1.decoder)
Vulkan: 1.1 supported
Video Information
File: simple_test.webm
Codec: AV1
Resolution: 3840x2160 (4K)
Codec Private Data: 21 bytes
Color Space: YUV420P
Build Information
VavCore: arm64-v8a Debug (5.4 MB)
Build Time: 2025-10-14 13:54:50
NDK: 26.0.10792818
Compiler: Clang 17.0.2
C++ Standard: 17
Report Generated: 2025-10-14 Diagnostic Tool: Claude Code Analysis Duration: Multi-session investigation