Files
video-v1/vav2/docs/working/MediaCodec_Improvement_Analysis.md

17 KiB

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)

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<ANativeWindow*>(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 핵심 설계 원칙

// 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<std::mutex> 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<std::mutex> 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 도입

// 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 분리

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<std::mutex> 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<ANativeWindow*>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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과 다르게 고정 버퍼링 카운트가 불필요합니다.

이유:

// MediaCodec은 비동기 파이프라인 - Input/Output 완전 분리
// - Input: dequeueInputBuffer → queueInputBuffer (즉시 리턴)
// - Output: dequeueOutputBuffer (프레임 준비 시 리턴)
//
// 첫 프레임 출력까지 자동으로 버퍼링하므로 별도 카운팅 불필요!

개선된 State Transition:

// State는 Output 상태로만 판단
READY  BUFFERING ( packet 입력)
BUFFERING  DECODING ( frame 출력)  ProcessOutputBuffer() 성공 
DECODING  FLUSHING (NULL packet 입력)

제거할 코드:

// ❌ 삭제: 불필요한 버퍼링 카운트
// #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 전달 개선

// 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<uint64_t>(pts_us * 1000);  // Convert µs to ns
        frame.timestamp_seconds = static_cast<double>(pts_us) / 1000000.0;

        // ... rest of processing ...
    }
}

📊 5. 구현 우선순위

Phase 1: State Machine 도입 (필수) COMPLETED (2025-10-11)

  • DecoderState enum 정의 - MediaCodecAV1Decoder.h:33-38
  • m_state 멤버 변수 추가 - MediaCodecAV1Decoder.h:188
  • m_stateMutex 추가 - MediaCodecAV1Decoder.h:189
  • State transition 로직 구현 - MediaCodecAV1Decoder.cpp:44 (constructor)

Phase 2: DecodeToSurface() 핵심 수정 (필수) COMPLETED (2025-10-11)

  • Surface 먼저 설정: Input 큐잉 전에 AMediaCodec_setOutputSurface() 호출 - line 220-229
  • Output Dequeue 추가: 버퍼링 중에도 ProcessOutputBuffer() 호출 - line 254
  • State 기반 Return: hasFrame 여부와 m_state로 false/true 결정 - line 256-271
  • NULL packet 처리: FLUSHING state transition - line 206-210
  • State transition logic: READY → BUFFERING → DECODING 구현 - line 242-280

Phase 3: ProcessOutputBuffer() 활용 (필수) ALREADY IMPLEMENTED

  • m_buffer_processor->DequeueOutputBuffer() 반환값 확인 - line 838
  • PTS 메타데이터 이미 추출됨 (BufferProcessor에서 처리) - MediaCodecBufferProcessor.cpp
  • Surface rendering 시 render=true 플래그 확인 - MediaCodecBufferProcessor.cpp

Phase 4: 불필요한 코드 제거 (권장) ⚠️ NOT REQUIRED

  • MEDIACODEC_INITIAL_BUFFERING 상수 - 애초에 존재하지 않음
  • m_bufferingPacketCount - 애초에 존재하지 않음 (Output 상태로만 판단하는 설계 적용됨)

Phase 5: 테스트 및 검증 (필수) PENDING

  • 단일 프레임 디코딩 테스트
  • 초기 버퍼링 동작 검증 (자동 처리 확인)
  • Flush mode 테스트 (EOF 처리)
  • B-frame 비디오 재생 확인 (MediaCodec 자동 리오더링)

⚠️ 6. 주의사항

6.1 MediaCodec API 특성 (CRITICAL)

MediaCodec은 비동기 파이프라인 - Input/Output 완전 분리:

// 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 의미 변경

기존 (잘못된 가정):

// false = 에러 발생
// true = 성공

개선 후 (NVDEC 모델 적용):

// 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)