# MediaCodec Android Decoder 개선 분석 **작성일**: 2025-10-11 (Updated: 2025-10-11 19:30 KST) **대상**: Android MediaCodec AV1 Decoder **참고**: NVDEC DecodeToSurface() 스펙 변경사항 반영 **상태**: ✅ **Phase 1-2 구현 완료** (State Machine + DecodeToSurface 리팩토링) --- ## 📋 Executive Summary NVDEC 개선 과정에서 `DecodeToSurface()` API 스펙이 크게 변경되었습니다. MediaCodec도 동일한 설계 원칙을 따라 개선이 필요합니다. ### 🎯 Implementation Status (2025-10-11) ✅ **Phase 1-2 Completed**: Core improvements implemented and ready for testing - State Machine: READY → BUFFERING → DECODING → FLUSHING - MediaCodec API-compliant DecodeToSurface() implementation - Always calls ProcessOutputBuffer() regardless of buffering state - Surface configured BEFORE input queueing (MediaCodec requirement) - State-based return logic (false for PACKET_ACCEPTED/END_OF_STREAM) ⏳ **Phase 5 Pending**: Android platform testing required **핵심 변경사항**: 1. **CUDA DPB 도입**: NVDEC은 내부 CUDA DPB를 통한 B-frame 리오더링 지원 2. **State Machine**: READY → BUFFERING → DECODING → FLUSHING 명확한 상태 관리 3. **False Return**: 프레임 미출력 시 `false` 반환 (VAVCORE_PACKET_ACCEPTED로 변환) 4. **PTS 기반 리오더링**: DisplayQueue를 통한 표시 순서 관리 --- ## 🔍 1. 현재 MediaCodec 구현 상태 ### 1.1 DecodeToSurface() 구현 (MediaCodecAV1Decoder.cpp:195-287) ```cpp bool MediaCodecAV1Decoder::DecodeToSurface(const uint8_t* packet_data, size_t packet_size, VavCoreSurfaceType target_type, void* target_surface, VideoFrame& output_frame) { if (!m_initialized) { LogError("Decoder not initialized"); return false; } if (target_type == VAVCORE_SURFACE_ANDROID_NATIVE_WINDOW) { // Set output surface for hardware acceleration ANativeWindow* native_surface = static_cast(target_surface); if (native_surface && native_surface != m_surface) { media_status_t status = AMediaCodec_setOutputSurface(m_codec, native_surface); if (status != AMEDIA_OK) { LogError("Failed to set output surface: " + std::to_string(status)); return false; } m_surface = native_surface; } // Process input buffer if (!ProcessInputBuffer(packet_data, packet_size)) { LogError("Failed to process input buffer for surface rendering"); return false; } // ❌ 문제: Output buffer dequeue 없이 즉시 리턴! // Output will be rendered directly to surface // No need to copy frame data IncrementFramesDecoded(); return true; // ← 프레임 출력 여부와 무관하게 항상 true } // ... (OpenGL ES, Vulkan 경로 유사) } ``` **문제점**: - ❌ **Output buffer dequeue 누락**: `ProcessOutputBuffer()` 호출 없음 - ❌ **항상 true 반환**: 프레임 출력 여부 확인 없이 무조건 true 반환 - ❌ **State Machine 없음**: BUFFERING/DECODING 구분 없음 - ❌ **동기화 부족**: MediaCodec 비동기 처리 특성 무시 --- ## 🎯 2. NVDEC DecodeToSurface() 설계 (참고 모델) ### 2.1 핵심 설계 원칙 ```cpp // NVDECAV1Decoder.cpp:381-613 bool NVDECAV1Decoder::DecodeToSurface(const uint8_t* packet_data, size_t packet_size, VavCoreSurfaceType target_type, void* target_surface, VideoFrame& output_frame) { // Step 1: Handle NULL packet as flush mode if (!packet_data || packet_size == 0) { m_state = DecoderState::FLUSHING; } // Step 2: Submit packet to NVDEC parser // ... // Step 3: Check if initial buffering is needed { std::lock_guard lock(m_displayMutex); // Transition from READY to BUFFERING on first packet if (m_state == DecoderState::READY && m_displayQueue.empty()) { m_state = DecoderState::BUFFERING; } // During initial buffering, accept packets until display queue has frames if (m_displayQueue.empty() && m_state == DecoderState::BUFFERING) { // Return false to indicate no frame yet (still buffering) return false; // ← VAVCORE_PACKET_ACCEPTED로 변환됨 } // Once we have frames in queue, transition to DECODING if (!m_displayQueue.empty() && m_state == DecoderState::BUFFERING) { m_state = DecoderState::DECODING; } } // Step 4: Pop from display queue to get picture_index (PTS-ordered) DisplayQueueEntry entry; { std::lock_guard lock(m_displayMutex); if (m_displayQueue.empty()) { if (m_state == DecoderState::FLUSHING) { // Return false - VAVCORE_END_OF_STREAM로 변환됨 return false; } } // Pop from priority queue (PTS-ordered) entry = m_displayQueue.top(); m_displayQueue.pop(); } // Step 5: Copy from CUDA DPB to target surface if (!CopyFromCUDADPB(pic_idx, slot.surface_type, slot.target_surface, output_frame)) { return false; } return true; // Frame successfully rendered } ``` **핵심 특징**: - ✅ **State Machine**: READY → BUFFERING → DECODING → FLUSHING - ✅ **False Return**: 버퍼링/플러싱 시 false 반환 (정상 동작) - ✅ **DisplayQueue**: PTS 기반 min-heap으로 B-frame 리오더링 - ✅ **Late Binding**: target_surface를 출력 직전에 업데이트 --- ## 🚀 3. MediaCodec 개선 방향 ### 3.1 State Machine 도입 ```cpp // MediaCodecAV1Decoder.h에 추가 enum class DecoderState { READY, // Initialized and ready for first packet BUFFERING, // Initial buffering (MediaCodec warming up) DECODING, // Normal frame-by-frame decoding FLUSHING // End-of-file reached, draining MediaCodec }; private: DecoderState m_state = DecoderState::READY; std::mutex m_stateMutex; ``` ### 3.2 DecodeToSurface() 리팩토링 (MediaCodec API 스펙 준수) **핵심 원칙**: MediaCodec은 비동기 파이프라인 - Input/Output 분리 ```cpp bool MediaCodecAV1Decoder::DecodeToSurface(const uint8_t* packet_data, size_t packet_size, VavCoreSurfaceType target_type, void* target_surface, VideoFrame& output_frame) { if (!m_initialized) { LogError("Decoder not initialized"); return false; } // Step 1: Handle NULL packet as flush mode if (!packet_data || packet_size == 0) { LOGF_DEBUG("[DecodeToSurface] NULL packet - flush mode (end of file)"); std::lock_guard lock(m_stateMutex); m_state = DecoderState::FLUSHING; } // Step 2: Update target surface BEFORE processing // (MediaCodec needs surface configured before queueing input) if (target_type == VAVCORE_SURFACE_ANDROID_NATIVE_WINDOW) { ANativeWindow* native_surface = static_cast(target_surface); if (native_surface && native_surface != m_surface) { media_status_t status = AMediaCodec_setOutputSurface(m_codec, native_surface); if (status != AMEDIA_OK) { LogError("Failed to set output surface: " + std::to_string(status)); return false; } m_surface = native_surface; LOGF_DEBUG("[DecodeToSurface] Output surface updated: %p", m_surface); } } // Step 3: Process input buffer (feed packet to MediaCodec) if (m_state != DecoderState::FLUSHING) { if (!ProcessInputBuffer(packet_data, packet_size)) { LogError("Failed to process input buffer"); return false; } } // Step 4: Check decoder state transition { std::lock_guard lock(m_stateMutex); // Transition from READY to BUFFERING on first packet if (m_state == DecoderState::READY) { m_state = DecoderState::BUFFERING; m_bufferingPacketCount = 0; LOGF_DEBUG("[DecodeToSurface] State transition: READY → BUFFERING"); } } // Step 5: Try to dequeue output buffer // CRITICAL: MediaCodec is ASYNCHRONOUS - input/output are decoupled // We must ALWAYS try dequeue, regardless of buffering state bool hasFrame = ProcessOutputBuffer(output_frame); if (!hasFrame) { std::lock_guard lock(m_stateMutex); // Check state to determine return semantic if (m_state == DecoderState::BUFFERING) { m_bufferingPacketCount++; LOGF_DEBUG("[DecodeToSurface] BUFFERING: packet %d accepted, no output yet", m_bufferingPacketCount); // Transition to DECODING when we get first output // (will happen on next call when ProcessOutputBuffer succeeds) return false; // VAVCORE_PACKET_ACCEPTED } if (m_state == DecoderState::FLUSHING) { // Flush complete - no more frames LOGF_INFO("[DecodeToSurface] Flush complete: all frames drained"); return false; // VAVCORE_END_OF_STREAM } // DECODING state but no output ready LOGF_DEBUG("[DecodeToSurface] DECODING: packet accepted, output not ready"); return false; // VAVCORE_PACKET_ACCEPTED } // Step 6: First frame received - transition to DECODING { std::lock_guard lock(m_stateMutex); if (m_state == DecoderState::BUFFERING) { m_state = DecoderState::DECODING; LOGF_INFO("[DecodeToSurface] State transition: BUFFERING → DECODING (first frame)"); } } // Step 7: Frame successfully decoded - setup metadata output_frame.width = m_width; output_frame.height = m_height; if (target_type == VAVCORE_SURFACE_ANDROID_NATIVE_WINDOW) { output_frame.color_space = ColorSpace::EXTERNAL_OES; // Android SurfaceTexture } IncrementFramesDecoded(); LOGF_DEBUG("[DecodeToSurface] Frame %llu decoded successfully", m_stats.frames_decoded); return true; // Frame successfully rendered } ``` **주요 수정사항**: 1. ✅ **Surface 먼저 설정**: Input 큐잉 전에 target_surface 업데이트 2. ✅ **항상 Output Dequeue**: 버퍼링 중에도 `ProcessOutputBuffer()` 호출 3. ✅ **State 기반 Return**: BUFFERING/DECODING/FLUSHING에 따라 false 반환 4. ✅ **First Frame Transition**: 첫 프레임 출력 시 BUFFERING → DECODING ### 3.3 초기 버퍼링 제거 (MediaCodec API 특성) **중요**: MediaCodec은 NVDEC과 다르게 **고정 버퍼링 카운트가 불필요**합니다. **이유**: ```cpp // MediaCodec은 비동기 파이프라인 - Input/Output 완전 분리 // - Input: dequeueInputBuffer → queueInputBuffer (즉시 리턴) // - Output: dequeueOutputBuffer (프레임 준비 시 리턴) // // 첫 프레임 출력까지 자동으로 버퍼링하므로 별도 카운팅 불필요! ``` **개선된 State Transition**: ```cpp // State는 Output 상태로만 판단 READY → BUFFERING (첫 packet 입력) BUFFERING → DECODING (첫 frame 출력) ← ProcessOutputBuffer() 성공 시 DECODING → FLUSHING (NULL packet 입력) ``` **제거할 코드**: ```cpp // ❌ 삭제: 불필요한 버퍼링 카운트 // #define VAVCORE_MEDIACODEC_INITIAL_BUFFERING 5 // int m_bufferingPacketCount; ``` --- ## 🔄 4. B-frame 리오더링 고려사항 ### 4.1 MediaCodec의 자동 리오더링 **NVDEC vs MediaCodec 차이**: - **NVDEC**: 수동 리오더링 필요 (DisplayQueue + PTS 우선순위 큐) - **MediaCodec**: 자동 리오더링 지원 (`AMediaCodec_getOutputBuffer()` 내부 처리) **결론**: MediaCodec은 DisplayQueue 불필요! - MediaCodec이 내부적으로 PTS 기반 리오더링 수행 - `BufferInfo.presentationTimeUs` 필드로 PTS 제공 - VavCore는 MediaCodec 출력 순서를 그대로 사용하면 됨 ### 4.2 PTS 전달 개선 ```cpp // ProcessOutputBuffer 내부에서 PTS 추출 및 설정 bool MediaCodecAV1Decoder::ProcessOutputBuffer(VideoFrame& frame) { // ... existing code ... AMediaCodecBufferInfo bufferInfo; ssize_t bufferIndex = AMediaCodec_dequeueOutputBuffer(m_codec, &bufferInfo, timeoutUs); if (bufferIndex >= 0) { // Extract PTS from MediaCodec int64_t pts_us = bufferInfo.presentationTimeUs; // Set frame metadata frame.timestamp_ns = static_cast(pts_us * 1000); // Convert µs to ns frame.timestamp_seconds = static_cast(pts_us) / 1000000.0; // ... rest of processing ... } } ``` --- ## 📊 5. 구현 우선순위 ### Phase 1: State Machine 도입 (필수) ✅ **COMPLETED** (2025-10-11) - [x] `DecoderState` enum 정의 - MediaCodecAV1Decoder.h:33-38 - [x] `m_state` 멤버 변수 추가 - MediaCodecAV1Decoder.h:188 - [x] `m_stateMutex` 추가 - MediaCodecAV1Decoder.h:189 - [x] State transition 로직 구현 - MediaCodecAV1Decoder.cpp:44 (constructor) ### Phase 2: DecodeToSurface() 핵심 수정 (필수) ✅ **COMPLETED** (2025-10-11) - [x] **Surface 먼저 설정**: Input 큐잉 전에 `AMediaCodec_setOutputSurface()` 호출 - line 220-229 - [x] **Output Dequeue 추가**: 버퍼링 중에도 `ProcessOutputBuffer()` 호출 - line 254 - [x] **State 기반 Return**: hasFrame 여부와 m_state로 false/true 결정 - line 256-271 - [x] **NULL packet 처리**: FLUSHING state transition - line 206-210 - [x] **State transition logic**: READY → BUFFERING → DECODING 구현 - line 242-280 ### Phase 3: ProcessOutputBuffer() 활용 (필수) ✅ **ALREADY IMPLEMENTED** - [x] `m_buffer_processor->DequeueOutputBuffer()` 반환값 확인 - line 838 - [x] PTS 메타데이터 이미 추출됨 (BufferProcessor에서 처리) - MediaCodecBufferProcessor.cpp - [x] Surface rendering 시 `render=true` 플래그 확인 - MediaCodecBufferProcessor.cpp ### Phase 4: 불필요한 코드 제거 (권장) ⚠️ **NOT REQUIRED** - [x] ~~MEDIACODEC_INITIAL_BUFFERING 상수~~ - 애초에 존재하지 않음 - [x] ~~m_bufferingPacketCount~~ - 애초에 존재하지 않음 (Output 상태로만 판단하는 설계 적용됨) ### Phase 5: 테스트 및 검증 (필수) ⏳ **PENDING** - [ ] 단일 프레임 디코딩 테스트 - [ ] 초기 버퍼링 동작 검증 (자동 처리 확인) - [ ] Flush mode 테스트 (EOF 처리) - [ ] B-frame 비디오 재생 확인 (MediaCodec 자동 리오더링) --- ## ⚠️ 6. 주의사항 ### 6.1 MediaCodec API 특성 (CRITICAL) **MediaCodec은 비동기 파이프라인 - Input/Output 완전 분리**: ```cpp // Input Pipeline (즉시 리턴) AMediaCodec_dequeueInputBuffer() // 빈 버퍼 얻기 AMediaCodec_queueInputBuffer() // 패킷 큐잉 (즉시 리턴!) // Output Pipeline (프레임 준비 시 리턴) AMediaCodec_dequeueOutputBuffer() // 디코딩된 프레임 얻기 (대기 가능) AMediaCodec_releaseOutputBuffer() // 프레임 렌더링 or 해제 ``` **핵심 차이**: - **NVDEC**: `cuvidParseVideoData()` → 동기 콜백 → 즉시 프레임 출력 - **MediaCodec**: `queueInputBuffer()` → 비동기 디코딩 → 나중에 `dequeueOutputBuffer()` **설계 함의**: 1. ✅ `ProcessInputBuffer()` 성공 ≠ 프레임 출력 성공 2. ✅ 항상 `ProcessOutputBuffer()` 호출해야 프레임 얻을 수 있음 3. ✅ 초기 몇 개 패킷은 출력 없이 입력만 가능 (파이프라인 filling) 4. ✅ Flush 시에도 `dequeueOutputBuffer()` 호출해서 남은 프레임 드레인 ### 6.2 NVDEC과의 차이점 | 항목 | NVDEC | MediaCodec | |------|-------|------------| | DPB 관리 | 수동 (CUDA DPB) | 자동 (MediaCodec 내부) | | B-frame 리오더링 | 수동 (DisplayQueue) | 자동 (내부 처리) | | 초기 버퍼링 | 16 프레임 | 5 프레임 (권장) | | Flush 처리 | ENDOFSTREAM flag | `AMediaCodec_flush()` | | 동기화 | cuvidGetDecodeStatus | dequeueOutputBuffer | ### 6.3 False Return 의미 변경 **기존 (잘못된 가정)**: ```cpp // false = 에러 발생 // true = 성공 ``` **개선 후 (NVDEC 모델 적용)**: ```cpp // false = 프레임 없음 (버퍼링 중 or EOF) // → VAVCORE_PACKET_ACCEPTED or VAVCORE_END_OF_STREAM // true = 프레임 출력 성공 // → VAVCORE_SUCCESS ``` --- ## 🎯 7. 예상 개선 효과 ### 7.1 API 일관성 - ✅ NVDEC과 동일한 DecodeToSurface() 동작 - ✅ C API 래퍼에서 동일한 반환값 처리 - ✅ Vav2Player와의 통합 간소화 ### 7.2 안정성 향상 - ✅ 초기 버퍼링 명확한 처리 - ✅ EOF/Flush 정확한 감지 - ✅ State Machine으로 예측 가능한 동작 ### 7.3 성능 최적화 - ✅ 불필요한 디코딩 시도 제거 - ✅ 버퍼링 중 CPU 사용량 감소 - ✅ 프레임 드롭 최소화 --- ## 📝 8. Next Actions ### Immediate (이번 작업) 1. State Machine enum 및 멤버 변수 추가 2. DecodeToSurface() 리팩토링 (false 반환 로직) 3. ProcessOutputBuffer() PTS 추출 개선 ### Short-term (다음 작업) 1. 단위 테스트 작성 및 실행 2. Android Vulkan Player 통합 테스트 3. B-frame 비디오 검증 ### Long-term (향후 개선) 1. Async mode 최적화 (MediaCodecAsyncHandler) 2. HardwareBuffer 연동 강화 3. Multi-codec 지원 (VP9, H.264) --- **문서 버전**: 1.0 **최종 수정**: 2025-10-11 **작성자**: Claude Code (Sonnet 4.5)