From 2f89643e6b895124743ed8280c328811b61dc6f0 Mon Sep 17 00:00:00 2001 From: ened Date: Tue, 14 Oct 2025 03:20:42 +0900 Subject: [PATCH] WIP --- todo29.txt | 1311 +++++++++++++++++ vav2/docs/working/mediacodec-recreation.md | 69 + .../app/src/main/res/layout/activity_main.xml | 77 +- .../app/src/main/res/values/themes.xml | 8 +- .../src/Decoder/MediaCodecAV1Decoder.cpp | 197 +-- .../src/Decoder/MediaCodecAV1Decoder.h | 1 - .../src/Decoder/MediaCodecAsyncHandler.cpp | 115 +- .../src/Decoder/MediaCodecAsyncHandler.h | 6 + .../platforms/windows/vavcore/src/VavCore.cpp | 52 +- 9 files changed, 1646 insertions(+), 190 deletions(-) create mode 100644 vav2/docs/working/mediacodec-recreation.md diff --git a/todo29.txt b/todo29.txt index c98a966..2fa2b11 100644 --- a/todo29.txt +++ b/todo29.txt @@ -342,3 +342,1314 @@ 필수 작업은 모두 완료되었습니다! 🎉 + + +● Summary + + I've discovered a fundamental issue with MediaCodec async mode and codec recreation: + + The Problem + + After deleting and recreating MediaCodec (to add ImageReader surface): + 1. Async callbacks fire correctly, providing buffer indices 0-9 + 2. BUT getInputBuffer() returns NULL for ALL indices + 3. Even after waiting 500ms+, the buffers never become valid + + Key Discovery + + - MediaCodec sends async callbacks ONLY ONCE after start() + - flush() does NOT trigger new callbacks (confirmed by testing) + - Memory address reuse (same pointer before/after delete) may cause internal state corruption + - Clearing the queue leaves it empty permanently - no new callbacks arrive + + Next Steps + + I recommend trying synchronous mode as a workaround: + + 1. After recreation, temporarily switch to synchronous dequeueInputBuffer() instead of async callbacks + 2. Or redesign to avoid recreation entirely (requires setting Vulkan device before Initialize) + + Would you like me to try the synchronous approach, or would you prefer to redesign the initialization order? + + + + + +● I can see the issue! The OLD recreation code is still running. Let me check what's in the file - it looks like my changes might not have been + applied correctly or there's another code path: + +● Bash(grep -n "Recreating MediaCodec with ImageReader surface" + D:/Project/video-av1/vav2/platforms/android/vavcore/src/Decoder/MediaCodecAV1Decoder.cpp) + ⎿  No matches found + +● Let me search for all messages related to recreation to find where this code is: + +● Bash(grep -rn "Recreating\|Recreation\|setOutputSurface" D:/Project/video-av1/vav2/platforms/android/vavcore/src/Decoder/MediaCodecAV1Decoder.cpp + | head -30) + ⎿  124: media_status_t status = AMediaCodec_setOutputSurface(m_codec, m_surface); + 471: media_status_t status = AMediaCodec_setOutputSurface(m_codec, surface); + 518: media_status_t status = AMediaCodec_setOutputSurface(m_codec, window); + … +3 lines (ctrl+o to expand) + +● Let me check which VavCore binaries are in the APK and their timestamps: + + + + + +● 문제를 정확히 파악했습니다! + + 초기화 순서 문제: + 1. Vulkan device가 VavCore에 등록됨 (02:09:42.716) + 2. 비디오 파일 로드 → 디코더 생성 및 초기화 (02:09:56.654) + 3. 이때 MediaCodec은 surface 없이 초기화됨 + 4. 디코더 초기화 완료 후 pending Vulkan device가 적용됨 + 5. setOutputSurface() 호출 → 실패 (surface 없이 초기화된 codec은 나중에 surface를 설정할 수 없음) + + 해결책: MediaCodec을 처음부터 ImageReader surface와 함께 초기화해야 합니다. SetVulkanDevice가 Initialize() 전에 호출되도록 VavCore의 초기화 + 순서를 수정하겠습니다: + +● Read(vav2\platforms\android\vavcore\src\VavCore.cpp) + ⎿  Read 100 lines + +● 문제를 찾았습니다! VavCore.cpp에서 Vulkan device를 디코더 초기화 후에 적용하고 있습니다: + + // Line 264: 디코더 초기화 + if (!player->impl->decoder->Initialize(player->impl->metadata)) { + // ... 실패 처리 ... + } + + // Line 275-284: 초기화 **후** Vulkan device 적용 + if (player->impl->has_vulkan_device) { + player->impl->decoder->SetVulkanDevice(...); + } + + 이것을 전 → 후 순서로 바꿔야 MediaCodec이 처음부터 ImageReader surface와 함께 초기화될 수 있습니다: + ⎿  Interrupted · What should Claude do instead? + +> 지금 계속 Vulkan 하고 MediaCodec 초기화 순서가 서로 얽히면서 이슈가 뱅글뱅글 돌고 있는거 아냐? + +● 정확합니다! 지금 상황을 정리하면: + + 현재 순서 (문제): + 1. 앱 시작 → Vulkan 초기화 → vavcore_set_vulkan_device() (VavCore에 저장만) + 2. 비디오 로드 → decoder->Initialize() ← Vulkan device 없음 → surface 없이 초기화 + 3. Initialize 후 → decoder->SetVulkanDevice() → setOutputSurface() 시도 → 실패 (surface 없이 초기화된 codec은 변경 불가) + 4. 또는 recreate 시도 → async callback 깨짐 → decode 실패 + + 뱅글뱅글 도는 이유: MediaCodec은 처음 configure할 때 surface가 있어야 하는데, Vulkan device가 너무 늦게 전달됨! + + 해결책: VavCore.cpp의 vavcore_open_file()에서 순서 변경: + + // 현재 (잘못됨): + decoder = CreateDecoder(); + decoder->Initialize(); // ← Vulkan 없음! + decoder->SetVulkanDevice(); // ← 너무 늦음 + + // 수정 (올바름): + decoder = CreateDecoder(); + decoder->SetVulkanDevice(); // ← 먼저! + decoder->Initialize(); // ← Vulkan 있음! + + 이렇게 하면 MediaCodec의 Initialize()가 호출될 때 이미 Vulkan device가 설정되어 있어서, 처음부터 ImageReader surface와 함께 초기화할 수 + 있습니다. + + VavCore.cpp를 수정하겠습니다: + + + + + +> Initialize()에 ImageReader 초기화 로직이외에 다른 초기화도 적절하게 순서대로 진행하고 있는건지 진단해줘. + +● 네, Initialize() 메서드의 전체 초기화 순서를 진단하겠습니다. + +● Read(vav2\platforms\android\vavcore\src\Decoder\MediaCodecAV1Decoder.cpp) + ⎿  Read 60 lines + +● Initialize() 메서드의 전체 초기화 순서를 진단하겠습니다: + + 현재 Initialize() 순서 분석 + + Step 1: ValidateInitializationParams() - 파라미터 검증 + Step 2: DetectHardwareCapabilities() - 하드웨어 능력 감지 + Step 3: Early Vulkan setup (if device already set) + ↓ + - JavaVM 설정 + - SetVideoDimensions() + - SetupImageReader() → m_surface에 저장 + Step 4: InitializeCodecWithFallback() + ↓ + - InitializeMediaCodec() + ↓ + - CreateAV1Decoder() - MediaCodec 생성 + - AMediaFormat_new() - Format 생성 + - ConfigureDecoder() → m_surface 사용 ✓ + - SetupAsyncCallbacks() + - AMediaCodec_start() + - Initialize BufferProcessor + - Prime decoder (async mode에서는 skip) + Step 5: SetupVulkanPipeline() - 중복 초기화 발생! ⚠️ + ↓ + - JavaVM 다시 설정 + - SetupImageReader() 다시 호출 + - AMediaCodec_setOutputSurface() 다시 호출 + Step 6: FinalizeInitialization() - m_initialized = true + + 🔴 발견된 문제들 + + 1. 중복 ImageReader 초기화 (Step 3 + Step 5) + + // Step 3에서 이미 ImageReader 초기화 성공 + if (m_surface_manager->GetVulkanDevice()) { + m_surface_manager->SetupImageReader(m_width, m_height); + m_surface = m_surface_manager->GetAndroidSurface(); // ✓ 성공 + } + + // Step 4에서 m_surface를 사용해 MediaCodec configure ✓ + + // Step 5에서 또 다시 ImageReader 초기화 시도! ⚠️ + if (!SetupVulkanPipeline()) { // 항상 실행됨 + // SetupImageReader() 또 호출 + // AMediaCodec_setOutputSurface() 또 호출 (불필요!) + } + + 문제: SetupVulkanPipeline()이 Step 3 성공 여부와 상관없이 무조건 실행됩니다. + + 2. setOutputSurface() 잘못된 사용 + + SetupVulkanPipeline()의 line 124를 보면: + + media_status_t status = AMediaCodec_setOutputSurface(m_codec, m_surface); + + 문제: MediaCodec가 이미 configure() 시에 surface와 함께 설정되었는데, 다시 setOutputSurface()를 호출하는 것은: + 1. 불필요 - 이미 surface가 설정됨 + 2. 비효율적 - 추가 JNI 호출 + 3. 잠재적 버그 - 일부 디바이스에서 이중 설정 오류 가능 + + 3. Step 5의 존재 이유가 불명확 + + SetupVulkanPipeline()의 주석을 보면: + + // Step 5: Setup Vulkan pipeline if not done in Step 3 + // (fallback for delayed Vulkan device registration) + + 하지만 실제 코드는: + + bool MediaCodecAV1Decoder::SetupVulkanPipeline() { + if (!m_surface_manager->GetVulkanDevice()) { + return true; // Vulkan이 없으면 그냥 성공 리턴 + } + // Vulkan이 있으면 무조건 ImageReader 재초기화 시도... + } + + Step 3에서 이미 Vulkan device가 있었다면, Step 5에서도 당연히 있습니다! 따라서 Step 5가 Step 3을 중복 실행하게 됩니다. + + ✅ 올바른 초기화 순서 (수정안) + + bool MediaCodecAV1Decoder::Initialize(const VideoMetadata& metadata) { + // Step 1: Validate + if (!ValidateInitializationParams(metadata)) return false; + + // Step 2: Detect hardware + if (!DetectHardwareCapabilities()) { + LogWarning("Hardware detection failed"); + } + + // Step 3: Early Vulkan setup (if device already set) + bool early_surface_setup = false; // 플래그 추가 ⭐ + if (m_surface_manager->GetVulkanDevice()) { + JavaVM* javaVM = GetAndroidJavaVM(); + if (javaVM) { + m_surface_manager->SetJavaVM(javaVM); + m_surface_manager->SetVideoDimensions(m_width, m_height); + if (m_surface_manager->SetupImageReader(m_width, m_height)) { + m_surface = m_surface_manager->GetAndroidSurface(); + if (m_surface) { + early_surface_setup = true; // 성공 기록 ⭐ + LogInfo("ImageReader prepared BEFORE codec init"); + } + } + } + } + + // Step 4: Initialize codec (with surface if available) + if (!InitializeCodecWithFallback()) { + return false; + } + + // Step 5: ONLY if Step 3 failed ⭐ + if (!early_surface_setup && m_surface_manager->GetVulkanDevice()) { + // Step 3에서 실패했지만 Vulkan device는 있음 - 재시도 + LogWarning("Retrying Vulkan setup after codec initialization"); + if (!SetupVulkanPipeline()) { + LogWarning("Late Vulkan setup failed - CPU fallback"); + } + } + + // Step 6: Finalize + return FinalizeInitialization(); + } + + 📋 요약 + + | 단계 | 현재 상태 | 문제 | 수정 필요 | + |--------|----------|-------------------|-------| + | Step 1 | ✅ 올바름 | 없음 | ❌ | + | Step 2 | ✅ 올바름 | 없음 | ❌ | + | Step 3 | ✅ 로직 올바름 | 성공 여부를 기록하지 않음 | ⚠️ | + | Step 4 | ✅ 올바름 | 없음 | ❌ | + | Step 5 | ❌ 항상 실행 | Step 3 성공 시에도 재실행 | ✅ 필수 | + | Step 6 | ✅ 올바름 | 없음 | ❌ | + + 결론: Initialize()의 Step 5가 문제입니다. Step 3에서 ImageReader 초기화가 성공했는지 확인하지 않고 무조건 재시도하여 중복 초기화가 + 발생합니다. 하지만 지금 당장은 VavCore.cpp 빌드 문제를 먼저 해결해야 합니다. + +> SetupVulkanPipeline() 에서 왜 "JavaVM 다시 설정, SetupImageReader() 다시 호출" 흐름이 있는가? 그리고 왜 AMediaCodec_setOutputSurface() +중복호출이 있는가? + +● 정확한 지적입니다! SetupVulkanPipeline()의 중복 호출 문제를 분석하겠습니다. + + 🔴 문제 1: SetupVulkanPipeline()의 중복 로직 + + SetupVulkanPipeline() 코드 (lines 92-133)를 보면: + + bool MediaCodecAV1Decoder::SetupVulkanPipeline() { + if (!m_surface_manager->GetVulkanDevice()) { + return true; // Vulkan 없으면 성공 리턴 + } + + // ⚠️ Vulkan device가 있으면 아래 코드 실행 + + // 1. JavaVM 다시 설정 (Step 3에서 이미 했음!) + JavaVM* javaVM = GetAndroidJavaVM(); + m_surface_manager->SetJavaVM(javaVM); + + // 2. SetupImageReader() 다시 호출 (Step 3에서 이미 했음!) + m_surface_manager->SetupImageReader(m_width, m_height); + m_surface = m_surface_manager->GetAndroidSurface(); + + // 3. setOutputSurface() 중복 호출 (Step 4 configure에서 이미 설정됨!) + AMediaCodec_setOutputSurface(m_codec, m_surface); + + return true; + } + + 실행 흐름 분석: + + [Step 3] Vulkan device 있음 → ImageReader 초기화 성공 → m_surface = 0xABCD + ↓ + [Step 4] MediaCodec configure(m_surface = 0xABCD) ✓ 성공 + ↓ + [Step 5] SetupVulkanPipeline() 호출 + → GetVulkanDevice() = true (Step 3와 동일 조건!) + → JavaVM 다시 설정 (불필요) + → SetupImageReader() 다시 호출 (불필요) + → setOutputSurface(m_surface) 다시 호출 (불필요) + + 🔴 문제 2: AMediaCodec_setOutputSurface() 잘못된 사용 + + MediaCodec API 사용법: + + // 방법 1: configure() 시에 surface 지정 (우리가 Step 4에서 한 것) + AMediaCodec_configure(codec, format, surface, nullptr, 0); + // ✓ 이후 setOutputSurface() 호출 불필요! + + // 방법 2: configure() 시에 surface 없이 생성 후 나중에 추가 + AMediaCodec_configure(codec, format, nullptr, nullptr, 0); // surface = null + // ... 나중에 + AMediaCodec_setOutputSurface(codec, surface); // 이때만 사용! + + 우리 코드의 문제: + + ConfigureDecoder() (line 621-627): + media_status_t status = AMediaCodec_configure( + m_codec, + m_format, + m_surface, // ⚠️ 이미 surface와 함께 configure! + nullptr, + 0 + ); + + SetupVulkanPipeline() (line 124): + // ❌ 이미 surface와 함께 configure된 codec에 또 setOutputSurface() 호출! + media_status_t status = AMediaCodec_setOutputSurface(m_codec, m_surface); + + 이것은 Android MediaCodec API 오용입니다! + + 🤔 왜 이런 코드가 작성되었나? + + 원래 설계 의도 (추측): + + SetupVulkanPipeline()의 주석: + // Step 5: Setup Vulkan pipeline if not done in Step 3 + // (fallback for delayed Vulkan device registration) + + 의도한 시나리오: + 1. Initialize() 시작 - Vulkan device 없음 + 2. Step 3 스킵 (Vulkan device 없음) → m_surface = nullptr + 3. Step 4에서 MediaCodec configure(surface=nullptr) + 4. 사이에 다른 스레드가 SetVulkanDevice() 호출 + 5. Step 5에서 늦게 도착한 Vulkan device로 setOutputSurface() 호출 + + 실제 흐름 (현재): + 1. Initialize() 시작 - Vulkan device 있음 (VavCore.cpp에서 먼저 설정) + 2. Step 3 실행 (Vulkan device 있음) → m_surface = 0xABCD + 3. Step 4에서 MediaCodec configure(surface=0xABCD) + 4. Step 5도 실행됨 (Vulkan device 있음) → 중복 초기화! + + 조건문의 실수: + + // Step 5 호출 (line 192-196) + if (!SetupVulkanPipeline()) { + LogWarning("Vulkan pipeline setup failed"); + } + + 문제: SetupVulkanPipeline()은 항상 true를 리턴하므로, 실제로는: + + SetupVulkanPipeline(); // 무조건 실행! + + 그리고 SetupVulkanPipeline() 내부에서: + + if (!m_surface_manager->GetVulkanDevice()) { + return true; // Vulkan 없으면 early return + } + // ⚠️ Vulkan 있으면 무조건 ImageReader 재초기화! + + Step 3과 Step 5가 같은 조건을 체크하기 때문에, Step 3에서 성공하면 Step 5도 무조건 실행됩니다! + + ✅ 올바른 수정 방법 + + bool MediaCodecAV1Decoder::Initialize(const VideoMetadata& metadata) { + // Step 1-2: Validate & Detect hardware + if (!ValidateInitializationParams(metadata)) return false; + if (!DetectHardwareCapabilities()) { + LogWarning("Hardware detection failed"); + } + + // Step 3: Early Vulkan setup + bool surface_configured_early = false; // 플래그 추가 ⭐ + if (m_surface_manager->GetVulkanDevice()) { + JavaVM* javaVM = GetAndroidJavaVM(); + if (javaVM) { + m_surface_manager->SetJavaVM(javaVM); + m_surface_manager->SetVideoDimensions(m_width, m_height); + if (m_surface_manager->SetupImageReader(m_width, m_height)) { + m_surface = m_surface_manager->GetAndroidSurface(); + if (m_surface) { + surface_configured_early = true; // ⭐ + LogInfo("ImageReader prepared BEFORE codec init"); + } + } + } + } + + // Step 4: Initialize codec + if (!InitializeCodecWithFallback()) { + return false; + } + + // Step 5: ONLY if early setup failed but Vulkan device exists + if (!surface_configured_early && m_surface_manager->GetVulkanDevice()) { + LogInfo("Retrying late Vulkan setup (early setup was skipped)"); + + // ⚠️ 주의: MediaCodec가 이미 configure되었으므로 + // setOutputSurface()만 호출하고 ImageReader 재초기화는 하지 않음! + ANativeWindow* surface = m_surface_manager->GetAndroidSurface(); + if (surface) { + media_status_t status = AMediaCodec_setOutputSurface(m_codec, surface); + if (status == AMEDIA_OK) { + m_surface = surface; + LogInfo("Late surface configuration successful"); + } else { + LogWarning("Late surface configuration failed - CPU fallback"); + } + } + } + + return FinalizeInitialization(); + } + + 그리고 SetupVulkanPipeline()을 삭제하거나 단순화: + + // 삭제하고 위 코드로 대체하거나, 또는 간단하게: + bool MediaCodecAV1Decoder::SetupVulkanPipeline() { + // 이 함수는 Step 3에서 이미 다 했으므로 아무것도 안 함 + return true; + } + + 📊 요약 + + | 중복 호출 | Step 3 | Step 5 | 필요성 | + |--------------------|--------|------------|-------------------| + | JavaVM 설정 | ✓ | ✓ 중복 | Step 3만 필요 | + | SetupImageReader() | ✓ | ✓ 중복 | Step 3만 필요 | + | configure(surface) | - | ✓ (Step 4) | 한 번만 | + | setOutputSurface() | - | ✓ 불필요 | configure에서 이미 설정 | + + 핵심 문제: Step 3과 Step 5가 같은 조건 (GetVulkanDevice())을 체크하므로, Step 3 성공 시 Step 5도 무조건 실행되어 모든 작업이 중복됩니다! + +> Step 3 의 Early Vulkan setup 에서 SetupImageReader(), GetAndroidSurface() 가 성공적으로 호출이 안되는 경우가 있을까? + +● 좋은 질문입니다! SetupImageReader()와 GetAndroidSurface()가 실패할 수 있는 경우를 실제 구현 코드로 확인하겠습니다. + +● Bash(find /d/Project/video-av1/vav2/platforms/android/vavcore -name "MediaCodecSurfaceManager.*" -type f 2>/dev/null) + ⎿  /d/Project/video-av1/vav2/platforms/android/vavcore/build-android/CMakeFiles/VavCore.dir/src/Decoder/MediaCodecSurfaceManager.cpp.o + +● Search(pattern: "**/MediaCodecSurfaceManager.cpp", path: "vav2\platforms") + ⎿  Found 1 file (ctrl+o to expand) + +● Search(pattern: "**/MediaCodecSurfaceManager.h", path: "vav2\platforms") + ⎿  Found 1 file (ctrl+o to expand) + +● Read(vav2\platforms\windows\vavcore\src\Decoder\MediaCodecSurfaceManager.cpp) + ⎿  Read 200 lines + +● 완벽합니다! SetupImageReader() 구현을 확인했습니다. 이제 실패 가능한 경우들을 분석하겠습니다. + + 🔍 SetupImageReader() 실패 가능한 경우들 + + 1. JavaVM/JNIEnv 획득 실패 (lines 492-496) + + JNIEnv* env = GetJNIEnv(); + if (!env) { + LogError("SetupImageReader: Failed to get JNI environment"); + return false; // ❌ 실패! + } + + 실패 조건: + - m_java_vm이 null (SetJavaVM()이 호출되지 않음) + - JNI thread attach 실패 + + Step 3 코드 확인: + JavaVM* javaVM = GetAndroidJavaVM(); // VavCore.cpp에서 global JavaVM 가져옴 + if (javaVM) { + m_surface_manager->SetJavaVM(javaVM); + // ... SetupImageReader() 호출 + } + + 결론: JavaVM이 null이면 SetupImageReader() 건너뛰므로, JavaVM null일 때 실패 가능 ✅ + + --- + 2. ImageReader 클래스 찾기 실패 (lines 503-511) + + jclass imageReaderClass = env->FindClass("android/media/ImageReader"); + if (!imageReaderClass) { + LogError("Failed to find ImageReader class"); + return false; // ❌ 실패! + } + + 실패 조건: + - ClassLoader 문제 (거의 없음 - 시스템 클래스) + - Android API Level < 19 (ImageReader는 API 19+) + + 결론: API Level이 너무 낮을 때 (하지만 우리는 API 29+ 타겟이므로 실패 가능성 낮음) ⚠️ + + --- + 3. ImageReader.newInstance 메서드 찾기 실패 (lines 515-529) + + jmethodID newInstanceMethod = env->GetStaticMethodID( + imageReaderClass, "newInstance", "(IIII)Landroid/media/ImageReader;"); + if (!newInstanceMethod) { + LogError("Failed to find ImageReader.newInstance method"); + return false; // ❌ 실패! + } + + 실패 조건: + - JNI signature 오타 (코드에는 없음) + - Android 버전별 API 차이 (거의 없음) + + 결론: 실패 가능성 매우 낮음 ❌ + + --- + 4. ImageReader 객체 생성 실패 (lines 545-553) + + jobject imageReader = env->CallStaticObjectMethod( + imageReaderClass, newInstanceMethod, + static_cast(width), + static_cast(height), + IMAGE_FORMAT_YUV_420_888, + MAX_IMAGES + ); + + if (!imageReader || env->ExceptionCheck()) { + LogError("Failed to create ImageReader"); + return false; // ❌ 실패! + } + + 실패 조건: + - 메모리 부족 (OOM - Out of Memory) + - 너무 큰 해상도 (8K 이상 등) + - FORMAT_YUV_420_888을 지원하지 않는 디바이스 (거의 없음) + - MAX_IMAGES=3이 너무 많음 (메모리 부족 시) + + 결론: 메모리 부족이나 초고해상도 비디오에서 실패 가능 ✅ (가능성 있음!) + + --- + 5. Surface 획득 실패 (lines 598-608) + + jobject javaSurface = env->CallObjectMethod(imageReader, getSurfaceMethod); + if (!javaSurface || env->ExceptionCheck()) { + LogError("Failed to get Surface from ImageReader"); + return false; // ❌ 실패! + } + + 실패 조건: + - ImageReader가 이미 closed됨 + - 내부 Android 오류 (드물음) + + 결론: 실패 가능성 낮음 ⚠️ + + --- + 6. ANativeWindow 변환 실패 (lines 611-618) + + ANativeWindow* nativeWindow = ANativeWindow_fromSurface(env, javaSurface); + if (!nativeWindow) { + LogError("Failed to get ANativeWindow from Surface"); + return false; // ❌ 실패! + } + + 실패 조건: + - Surface가 이미 released됨 + - JNI 환경 문제 + - 메모리 부족 + + 결론: 실패 가능성 낮지만 메모리 부족 시 가능 ⚠️ + + --- + 7. GetAndroidSurface() 실패 + + GetAndroidSurface()는 단순히 m_native_window를 리턴하는 getter이므로: + + ANativeWindow* GetAndroidSurface() const { + return m_native_window; // SetupImageReader()에서 설정됨 + } + + 실패 조건: + - SetupImageReader()가 성공했지만 m_native_window가 null (버그) + + 결론: SetupImageReader() 성공 후 null일 가능성 거의 없음 ❌ + + --- + 📊 실패 가능성 요약 + + | 단계 | 실패 조건 | 가능성 | 영향 | + |-------------------|--------------------|-------|------------------| + | JavaVM 획득 | JavaVM null | 높음 ✅ | Step 3 전체 skip | + | ImageReader 클래스 | API < 19 | 낮음 | N/A (API 29+ 타겟) | + | newInstance 메서드 | Signature 오류 | 매우 낮음 | N/A | + | ImageReader 생성 | 메모리 부족, 초고해상도 | 중간 ✅ | Step 3 실패 | + | Surface 획득 | ImageReader closed | 낮음 | Step 3 실패 | + | ANativeWindow 변환 | 메모리 부족 | 낮음 ⚠️ | Step 3 실패 | + | GetAndroidSurface | null 리턴 | 매우 낮음 | Step 3 실패 | + + ✅ 결론 + + Yes, Step 3 Early Vulkan setup에서 SetupImageReader()/GetAndroidSurface()가 실패할 수 있는 경우가 있습니다: + + 주요 실패 시나리오: + + 1. JavaVM이 null → Step 3 전체 건너뜀 (가장 흔함) + 2. 메모리 부족 → ImageReader 생성 실패 (특히 4K/8K 비디오) + 3. 초고해상도 비디오 (8K+) → ImageReader 생성 실패 + + 이러한 경우 Step 5의 역할: + + // Step 3: Early setup (JavaVM 있고 메모리 충분할 때만 성공) + bool early_success = SetupImageReader(...); // 실패 가능! + + // Step 4: MediaCodec initialize (surface=nullptr로 configure) + + // Step 5: JavaVM이 늦게 설정되었거나 Step 3에서 메모리 부족이었던 경우 재시도 + if (!early_success && GetVulkanDevice()) { + // 이번엔 성공할 수도 있음 (메모리 확보됨) + SetupImageReader(...); + setOutputSurface(...); // ⚠️ 하지만 이건 문제! (이전 분석 참조) + } + + 하지만 현재 Step 5의 문제점: + + Step 5가 Step 3 실패 여부를 체크하지 않고 무조건 실행되므로, Step 3이 성공해도 Step 5가 중복 실행됩니다! + + 수정 필요: Step 3 성공 여부를 플래그로 저장하고, 실패했을 때만 Step 5 실행! + +> 1. JavaVM 이 null 인 경우는 사용자가 설정하지 않아서 인가? 아니면, 내부 초기화 오류인가? 만약 JavaVM 이 null 이면 이후에 Vulkan 디코딩 텍스처 + 복사를 진행하지 못하는 것이 아닌가? +2. 메모리 부족일 경우, 이후에 MediaCodec 초기화도 마찬가지로 문제가 생기지 않을까? + ⎿  Interrupted · What should Claude do instead? + +> 1. JavaVM 이 null 인 경우는 사용자가 설정하지 않아서 인가? 아니면, 내부 초기화 오류인가? 만약 JavaVM 이 null 이면 이후에 Vulkan 디코딩 텍스처 + 복사를 진행하지 못하는 것이 아닌가? +2. 메모리 부족일 경우, 이후에 MediaCodec 초기화도 마찬가지로 문제가 생기지 않을까? +3. 초고해상도 비디오의 ImageReader 생성이 실패하면 이후에 MediaCodec 초기화 이후에 surface 를 위한 ImageReader 생성을 재시도하더라도, 또 실패가 + 되지 않을까? + +● 정확한 지적입니다! 제 분석에 논리적 모순이 있었습니다. 하나씩 검증하겠습니다. + + 1️⃣ JavaVM이 null인 경우 + + JavaVM 초기화 경로 확인: + +● Bash(grep -n "JNI_OnLoad\|g_android_java_vm\|vavcore_set_android_java_vm" /d/Project/video-av1/vav2/platforms/android/vavcore/src/VavCore.cpp | + head -30) + ⎿  38:static JavaVM* g_android_java_vm = nullptr; // Global JavaVM for Android JNI operations + 41:JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { + 43: g_android_java_vm = vm; // Store JavaVM for later use + … +9 lines (ctrl+o to expand) + +● Bash(grep -A 10 "vavcore_set_android_java_vm\|JNI_OnLoad" /d/Project/video-av1/time.log | head -30) + ⎿  10-14 02:26:35.808 23834 23834 I VavCore : [vavcore_set_android_java_vm] JavaVM registered successfully: 0xb4000074f0a2fed0 + 10-14 02:26:35.808 23834 23834 I VulkanJNI: JavaVM registered with VavCore successfully + 10-14 02:26:35.808 23834 23834 I VavCoreVulkanBridge: VavCoreVulkanBridge created + … +8 lines (ctrl+o to expand) + +● 완벽한 논리적 분석입니다! 제가 놓친 부분을 정확히 지적하셨습니다. 각 질문에 답변하겠습니다. + + 1️⃣ JavaVM null 분석 + + JavaVM 초기화 과정: + + // VavCore.cpp lines 41-45 + JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { + std::lock_guard lock(g_mutex); + g_android_java_vm = vm; // ⭐ Android 시스템이 자동 호출! + g_jni_loaded = true; + return JNI_VERSION_1_6; + } + + JavaVM null이 될 수 있는 경우: + + | 시나리오 | JavaVM null? | 현실성 | + |---------------------|----------------------|--------| + | libVavCore.so 로드됨 | ❌ (JNI_OnLoad 자동 호출) | 정상 경로 | + | libVavCore.so 로드 실패 | N/A (앱 크래시) | 불가능 | + | JNI_OnLoad 실행 전 | ✅ null | 가능! ⚠️ | + + 🔴 문제 발견! + + 로그를 보면: + 02:26:35.808 I VavCore : [vavcore_set_android_java_vm] JavaVM registered: 0xb4000074f0a2fed0 + + 앱이 명시적으로 vavcore_set_android_java_vm()을 호출하고 있습니다! + + 즉, JNI_OnLoad에 의존하지 않고 명시적 설정을 사용합니다. + + 실행 순서 문제: + + [App 시작] + ↓ + [libVavCore.so 로드] → JNI_OnLoad 호출 → g_android_java_vm 설정 ✓ + ↓ + [MainActivity.onCreate] + ↓ + [VulkanVideoView 생성] + ↓ + [vavcore_set_android_java_vm() 호출] → g_android_java_vm 재설정 (중복) ✓ + ↓ + [vavcore_set_vulkan_device() 호출] → Vulkan device 저장 ✓ + ↓ + [vavcore_open_file() 호출] + ↓ + [decoder->Initialize()] + ↓ + [GetAndroidJavaVM()] → g_android_java_vm 리턴 ✓ + + 결론 1: + + JavaVM이 null일 가능성은 사실상 0%입니다. JNI_OnLoad가 자동으로 설정하고, 앱도 명시적으로 설정합니다. + + 하지만 당신의 지적이 맞습니다: JavaVM null이면 ImageReader 생성 불가 → Vulkan zero-copy pipeline 완전히 불가능 → CPU fallback만 가능. + + --- + 2️⃣ 메모리 부족 시나리오 + + ImageReader vs MediaCodec 메모리 사용 비교: + + // ImageReader (lines 533-543) + const int MAX_IMAGES = 3; // ⚠️ 3개 프레임 버퍼를 **즉시** 할당! + jobject imageReader = env->CallStaticObjectMethod( + imageReaderClass, newInstanceMethod, + width, height, + IMAGE_FORMAT_YUV_420_888, + MAX_IMAGES // 3 * (width * height * 1.5) bytes 즉시 할당! + ); + + 메모리 계산: + - 1920x1080 YUV420: ~3MB per frame + - ImageReader (3 buffers): ~9MB 즉시 할당 + + // MediaCodec (ConfigureDecoder) + AMediaCodec_configure(m_codec, m_format, m_surface, nullptr, 0); + // MediaCodec는 lazy allocation - 필요할 때 버퍼 할당 + + 차이점: + + | 항목 | ImageReader | MediaCodec | + |--------|-------------|------------| + | 할당 시점 | 즉시 (eager) | 지연 (lazy) | + | 버퍼 개수 | 3개 고정 | 1-2개 (동적) | + | 실패 가능성 | 높음 | 낮음 | + + 시나리오: + + [메모리 상태: 여유 메모리 5MB] + ↓ + [Step 3: ImageReader 생성 시도] + → 9MB 필요 → ❌ OOM 실패! + ↓ + [Step 4: MediaCodec 초기화] + → 처음엔 0MB (lazy) → ✓ 성공! + ↓ + [첫 프레임 디코딩] + → 3MB 할당 → ✓ 성공 (기존 메모리 정리됨) + + 결론 2: + + 당신의 우려가 부분적으로 맞지만, 완전히 같지는 않습니다: + + - ImageReader: 즉시 9MB 할당 → 메모리 부족 시 즉시 실패 + - MediaCodec: lazy allocation → 메모리 부족해도 초기화 성공, 나중에 점진적 할당 + + 하지만: 메모리가 정말 부족하면 MediaCodec도 결국 실패합니다. 다만 타이밍 차이로 ImageReader 실패 후 MediaCodec는 성공할 가능성이 있습니다. + + --- + 3️⃣ 초고해상도 비디오 시나리오 + + Step 3 vs Step 5 비교: + + // Step 3: Initialize() 시작 시 + bool MediaCodecAV1Decoder::Initialize() { + // Step 3: Early ImageReader setup + if (GetVulkanDevice()) { + SetupImageReader(m_width, m_height); // 8K → ❌ 실패! + } + + // Step 4: MediaCodec initialize (surface=nullptr) + InitializeCodecWithFallback(); // ✓ 성공 + + // Step 5: Late ImageReader setup (retry) + if (GetVulkanDevice()) { + SetupImageReader(m_width, m_height); // 8K → ❌ 다시 실패! + } + } + + 실패 원인이 동일: + + Step 3 실패 이유: 8K (7680x4320) × 3 buffers = ~150MB → OOM + Step 5 재시도: 여전히 8K (7680x4320) × 3 buffers = ~150MB → 똑같이 실패! + + 조건이 변하지 않음: + + | 조건 | Step 3 | Step 5 | 변화? | + |--------------|--------|--------|------| + | Video width | 7680 | 7680 | ❌ 동일 | + | Video height | 4320 | 4320 | ❌ 동일 | + | MAX_IMAGES | 3 | 3 | ❌ 동일 | + | 여유 메모리 | 50MB | 50MB | ❌ 동일 | + + 결론 3: + + 당신이 100% 맞습니다! Step 3에서 실패한 이유가 해상도나 메모리 부족이면, Step 5에서 재시도해도 똑같은 이유로 실패합니다! + + Step 5의 "fallback" 로직은 사실상 무의미합니다! + + --- + 🎯 종합 결론 + + Step 5가 의미 있는 경우 (원래 설계 의도): + + [시나리오: Vulkan device가 늦게 등록되는 경우] + + Timeline: + T1: Initialize() 시작 → GetVulkanDevice() = nullptr + T2: Step 3 스킵 (Vulkan device 없음) + T3: MediaCodec configure(surface=nullptr) + T4: 다른 스레드가 SetVulkanDevice() 호출 ⭐ + T5: Step 5 실행 → GetVulkanDevice() = valid! + T6: SetupImageReader() 성공 → setOutputSurface() 호출 + + 하지만 현재 구현에서는: + + [실제 흐름] + + Timeline: + T1: MainActivity.onCreate() → SetVulkanDevice() 호출 ⭐ (먼저!) + T2: vavcore_open_file() → Initialize() 시작 + T3: Step 3 실행 → GetVulkanDevice() = valid! + T4: SetupImageReader() 성공 또는 실패 + T5: Step 4: MediaCodec configure(surface from Step 3) + T6: Step 5 실행 → GetVulkanDevice() = valid! (Step 3과 동일 조건!) + T7: SetupImageReader() 중복 호출! ❌ + + 당신의 논리 요약: + + | 질문 | 답변 | 당신의 지적 | + |-----------------|------------------------|------------------------------------| + | 1. JavaVM null? | 거의 불가능 (JNI_OnLoad 자동) | ✅ 맞음: null이면 Vulkan 불가 | + | 2. 메모리 부족? | ImageReader 먼저 실패 가능 | ⚠️ 부분적: MediaCodec lazy allocation | + | 3. 초고해상도? | Step 5 재시도해도 실패 | ✅ 100% 맞음! | + + 최종 결론: + + Step 5의 "fallback" 로직은 현재 코드에서 거의 무의미합니다! + + 이유: + 1. Vulkan device가 항상 Step 3 전에 설정됨 (VavCore.cpp 수정 후) + 2. Step 3 실패 원인이 Step 5에서도 동일 (해상도, 메모리) + 3. Step 5가 Step 3 성공 여부를 체크하지 않아 중복 실행 + + 올바른 수정: + + bool MediaCodecAV1Decoder::Initialize(const VideoMetadata& metadata) { + // Step 3: Early Vulkan setup + bool early_surface_ok = false; + if (m_surface_manager->GetVulkanDevice()) { + if (SetupImageReader(...)) { + early_surface_ok = true; // ⭐ 플래그 저장 + } + } + + // Step 4: MediaCodec initialize + InitializeCodecWithFallback(); + + // Step 5: ❌ 삭제하거나 조건 추가 + // (현재 구현에서는 의미 없음) + + return FinalizeInitialization(); + } + + 당신의 논리적 분석이 정확했습니다! 👏 + +> early_surface_ok = false 일 경우에 그냥 Initialize() 를 실패로 처리하면 되지 않을까? + +● 탁월한 질문입니다! 논리적으로는 맞지만, 설계 철학에 따라 답이 달라집니다. 분석해보겠습니다. + + 🤔 두 가지 접근법 비교 + + 접근법 1: 실패 처리 (당신의 제안) ✋ + + bool MediaCodecAV1Decoder::Initialize(const VideoMetadata& metadata) { + // Step 1-2: Validate & Detect hardware + if (!ValidateInitializationParams(metadata)) return false; + if (!DetectHardwareCapabilities()) { + LogWarning("Hardware detection failed"); + } + + // Step 3: Vulkan device 있으면 ImageReader 필수! + if (m_surface_manager->GetVulkanDevice()) { + JavaVM* javaVM = GetAndroidJavaVM(); + if (!javaVM) { + LogError("Vulkan device set but JavaVM unavailable"); + return false; // ❌ 실패! + } + + m_surface_manager->SetJavaVM(javaVM); + m_surface_manager->SetVideoDimensions(m_width, m_height); + + if (!m_surface_manager->SetupImageReader(m_width, m_height)) { + LogError("Vulkan device set but ImageReader setup failed"); + return false; // ❌ 실패! + } + + m_surface = m_surface_manager->GetAndroidSurface(); + if (!m_surface) { + LogError("Vulkan device set but Surface unavailable"); + return false; // ❌ 실패! + } + + LogInfo("Vulkan zero-copy pipeline ready"); + } + + // Step 4: Initialize codec + return InitializeCodecWithFallback() && FinalizeInitialization(); + } + + 장점: + - ✅ 명확한 오류 처리 - Vulkan 원하는데 실패하면 즉시 알림 + - ✅ 사용자 의도 존중 - "Vulkan 설정했으면 반드시 Vulkan 사용" + - ✅ 디버깅 용이 - 실패 원인 즉시 파악 + + 단점: + - ❌ 비디오 재생 불가 - CPU fallback도 없음 + - ❌ 사용자 경험 저하 - "왜 안 돼?" (Vulkan 설정만 했을 뿐인데) + - ❌ 유연성 부족 - 메모리 부족 등 일시적 문제로 전체 실패 + + --- + 접근법 2: Graceful Degradation (현재 설계) 🎯 + + bool MediaCodecAV1Decoder::Initialize(const VideoMetadata& metadata) { + // Step 1-2: Validate & Detect hardware + if (!ValidateInitializationParams(metadata)) return false; + if (!DetectHardwareCapabilities()) { + LogWarning("Hardware detection failed"); + } + + // Step 3: Vulkan pipeline 시도 (best effort) + if (m_surface_manager->GetVulkanDevice()) { + JavaVM* javaVM = GetAndroidJavaVM(); + if (javaVM) { + m_surface_manager->SetJavaVM(javaVM); + m_surface_manager->SetVideoDimensions(m_width, m_height); + + if (m_surface_manager->SetupImageReader(m_width, m_height)) { + m_surface = m_surface_manager->GetAndroidSurface(); + if (m_surface) { + LogInfo("Vulkan zero-copy pipeline enabled"); + } else { + LogWarning("ImageReader created but Surface unavailable - using CPU fallback"); + } + } else { + LogWarning("ImageReader setup failed - using CPU fallback"); + } + } else { + LogWarning("JavaVM unavailable - using CPU fallback"); + } + } + + // Step 4: Initialize codec (CPU or GPU) + return InitializeCodecWithFallback() && FinalizeInitialization(); + } + + 장점: + - ✅ 항상 재생 가능 - CPU fallback 보장 + - ✅ 유연성 - 일시적 문제(메모리 부족 등) 극복 + - ✅ 사용자 경험 - "느려도 재생은 됨" + + 단점: + - ⚠️ 성능 저하 눈치채기 어려움 - 사용자가 CPU fallback인지 모를 수 있음 + - ⚠️ 디버깅 어려움 - 로그 안 보면 문제 모름 + - ⚠️ 숨겨진 실패 - ImageReader 실패 원인 추적 어려움 + + --- + 📊 실패 시나리오별 비교 + + | 실패 원인 | 접근법 1 (실패 처리) | 접근법 2 (Graceful) | 현실성 | + |--------------------|-----------------|------------------------------|--------------------| + | JavaVM null | ❌ Initialize 실패 | ⚠️ CPU fallback | 거의 없음 (JNI_OnLoad) | + | 메모리 부족 (일시적) | ❌ Initialize 실패 | ✅ CPU fallback → 나중에 GC → 정상 | 드물음 | + | 초고해상도 (8K) | ❌ Initialize 실패 | ⚠️ CPU fallback (느림) | 매우 드물음 | + | ImageReader API 없음 | ❌ Initialize 실패 | ✅ CPU fallback | 없음 (API 29+) | + + --- + 🎯 현재 코드의 설계 의도 + + SetupVulkanPipeline() 주석을 보면: + + // lines 92-133 + bool MediaCodecAV1Decoder::SetupVulkanPipeline() { + if (!m_surface_manager->GetVulkanDevice()) { + return true; // ⭐ Not an error - Vulkan is optional + } + + if (!m_surface_manager->SetupImageReader(...)) { + LogWarning("Failed to setup ImageReader - continuing without Vulkan pipeline"); + return true; // ⭐ Not fatal - decoder can work without ImageReader + } + + return true; // ⭐ Always succeed - Vulkan pipeline is optional + } + + 명시적 설계 의도: "Vulkan은 optional이고, 실패해도 계속 진행" + + --- + ✅ 제 추천 + + 하이브리드 접근법 (Best of both worlds): + + bool MediaCodecAV1Decoder::Initialize(const VideoMetadata& metadata) { + // Step 1-2: Validate & Detect + if (!ValidateInitializationParams(metadata)) return false; + DetectHardwareCapabilities(); + + // Step 3: Vulkan setup (best effort) + bool vulkan_pipeline_active = false; + if (m_surface_manager->GetVulkanDevice()) { + JavaVM* javaVM = GetAndroidJavaVM(); + if (javaVM) { + m_surface_manager->SetJavaVM(javaVM); + m_surface_manager->SetVideoDimensions(m_width, m_height); + + if (m_surface_manager->SetupImageReader(m_width, m_height)) { + m_surface = m_surface_manager->GetAndroidSurface(); + if (m_surface) { + vulkan_pipeline_active = true; + LogInfo("✅ Vulkan zero-copy pipeline ACTIVE"); + } + } + } + + if (!vulkan_pipeline_active) { + // ⚠️ Vulkan 원했지만 실패 - 명확히 경고! + LogWarning("=========================================="); + LogWarning("⚠️ Vulkan device registered but pipeline INACTIVE"); + LogWarning("⚠️ Falling back to CPU decoding (slower)"); + LogWarning("⚠️ Possible causes:"); + LogWarning(" - JavaVM unavailable"); + LogWarning(" - ImageReader creation failed (memory?)"); + LogWarning(" - Surface acquisition failed"); + LogWarning("=========================================="); + } + } + + // Step 4: MediaCodec initialize (GPU or CPU) + if (!InitializeCodecWithFallback()) { + return false; + } + + // Step 5 삭제 (중복 제거) + + return FinalizeInitialization(); + } + + 이 접근법의 장점: + + 1. ✅ 재생은 보장 - CPU fallback으로 작동 + 2. ✅ 명확한 경고 - Vulkan 실패 시 눈에 띄는 로그 + 3. ✅ 디버깅 용이 - 실패 원인 명시 + 4. ✅ 사용자 선택 가능 - 로그 보고 "CPU로도 충분" 또는 "문제 해결 필요" 판단 + + --- + 🔥 더 나은 방법: 명시적 모드 설정 + + enum class VulkanPipelineMode { + OPTIONAL, // 실패해도 CPU fallback (현재) + REQUIRED, // 실패하면 Initialize 실패 (당신의 제안) + DISABLED // Vulkan 사용 안 함 + }; + + class MediaCodecAV1Decoder { + private: + VulkanPipelineMode m_vulkan_mode = VulkanPipelineMode::OPTIONAL; + + public: + void SetVulkanPipelineMode(VulkanPipelineMode mode) { + m_vulkan_mode = mode; + } + + bool Initialize(const VideoMetadata& metadata) { + // ... + + if (m_surface_manager->GetVulkanDevice()) { + bool success = SetupVulkanPipeline(); + + if (!success && m_vulkan_mode == VulkanPipelineMode::REQUIRED) { + LogError("Vulkan pipeline REQUIRED but setup failed"); + return false; // ❌ 실패! + } + } + + // ... + } + }; + + 사용자(앱 개발자)가 선택: + + // 옵션 1: 성능 중요 - Vulkan 필수 + decoder->SetVulkanPipelineMode(VulkanPipelineMode::REQUIRED); + + // 옵션 2: 호환성 중요 - Vulkan optional + decoder->SetVulkanPipelineMode(VulkanPipelineMode::OPTIONAL); + + --- + 최종 답변 + + 당신의 제안 ("실패 처리"): + - ✅ 논리적으로 맞음 + - ✅ 성능 중요한 앱에 적합 + - ❌ 현재 설계 철학과 다름 + + 현재 구현 ("Graceful degradation"): + - ✅ 호환성/안정성 우선 + - ❌ 성능 저하 눈치채기 어려움 + + 제 추천: + 1. 단기: 하이브리드 접근법 (경고 강화, CPU fallback 유지) + 2. 장기: 명시적 모드 설정 추가 (사용자 선택) + + 현재 프로젝트에서는: Vulkan 실패가 매우 드물기 때문에 (JavaVM 자동 설정, 충분한 메모리, 일반 해상도), Graceful degradation이 더 실용적입니다! + +> 어차피 Vulkan 없고, 하드웨어 스펙이 부족하면 실패처리하는것이 스펙이 맞다. 그이유는 이 VavCore 는 최종 탑재 제품이 게임이기 때문이다. +그리고 CPU fallback 은 애초에 구현할 생각도 없다. GPU h/w 처리 아니면 이 프로젝트는 필요가 없다. + + + + + + +> 접근법 1: 실패 처리 (당신의 제안) ✋ + + bool MediaCodecAV1Decoder::Initialize(const VideoMetadata& metadata) { + // Step 1-2: Validate & Detect hardware + if (!ValidateInitializationParams(metadata)) return false; + if (!DetectHardwareCapabilities()) { + LogWarning("Hardware detection failed"); + } + + // Step 3: Vulkan device 있으면 ImageReader 필수! + if (m_surface_manager->GetVulkanDevice()) { + JavaVM* javaVM = GetAndroidJavaVM(); + if (!javaVM) { + LogError("Vulkan device set but JavaVM unavailable"); + return false; // ❌ 실패! + } + + m_surface_manager->SetJavaVM(javaVM); + m_surface_manager->SetVideoDimensions(m_width, m_height); + + if (!m_surface_manager->SetupImageReader(m_width, m_height)) { + LogError("Vulkan device set but ImageReader setup failed"); + return false; // ❌ 실패! + } + + m_surface = m_surface_manager->GetAndroidSurface(); + if (!m_surface) { + LogError("Vulkan device set but Surface unavailable"); + return false; // ❌ 실패! + } + + LogInfo("Vulkan zero-copy pipeline ready"); + } + + // Step 4: Initialize codec + return InitializeCodecWithFallback() && FinalizeInitialization(); + } + + 장점: + - ✅ 명확한 오류 처리 - Vulkan 원하는데 실패하면 즉시 알림 + - ✅ 사용자 의도 존중 - "Vulkan 설정했으면 반드시 Vulkan 사용" + - ✅ 디버깅 용이 - 실패 원인 즉시 파악 + + 단점: + - ❌ 비디오 재생 불가 - CPU fallback도 없음 + - ❌ 사용자 경험 저하 - "왜 안 돼?" (Vulkan 설정만 했을 뿐인데) + - ❌ 유연성 부족 - 메모리 부족 등 일시적 문제로 전체 실패 + +Initialize() 의 위의 수정 진행해도 문제 없는거 맞지? + +● 네, 문제 없습니다. 오히려 이 접근법이 당신의 설계 요구사항에 정확히 맞습니다. + + ✅ 당신의 설계 철학과 완벽히 일치 + + 당신이 이전에 명확히 말했던 것: + "어차피 Vulkan 없고, 하드웨어 스펙이 부족하면 실패처리하는것이 스펙이 맞다. 그이유는 이 VavCore 는 최종 탑재 제품이 게임이기 때문이다. GPU + h/w 처리 아니면 이 프로젝트는 필요가 없다." + + 🎯 게임 환경에서는 "단점"이 아님 + + 당신이 나열한 "단점"들은 게임 배포 환경에서는 오히려 장점: + + 1. "비디오 재생 불가" → ✅ 의도된 동작 + - Vulkan 없으면 게임도 실행 안 됨 + - 비디오만 따로 CPU fallback 할 이유 없음 + 2. "사용자 경험 저하" → ✅ 명확한 시스템 요구사항 + - "GPU 필수" 스펙 충족 못하면 실행 안 되는 것이 정상 + - 게임: "최소 사양 미달" 메시지 표시 + 3. "유연성 부족" → ✅ 일관된 성능 보장 + - CPU fallback으로 5 FPS 나오는 것보다 실패가 나음 + - 게임 사양 충족 못하면 실행하지 않는 것이 맞음 + + 📐 기술적으로도 올바름 + + 현재 아키텍처 상황: + // VavCore.cpp (lines 400-420) + if (player->impl->has_vulkan_device) { + // Vulkan device를 decoder 초기화 "전에" 등록 + player->impl->decoder->SetVulkanDevice(...); + } + + // Decoder::Initialize() 호출 + player->impl->decoder->Initialize(metadata); // ← 이 시점에 ImageReader 필요 + + MediaCodec 동작 방식: + - AMediaCodec_configure() 호출 시점에 Surface 필요 (필수) + - Surface = ImageReader의 ANativeWindow + - ImageReader setup 실패 = MediaCodec configure 불가 = 재생 불가 + + 따라서: + if (m_surface_manager->GetVulkanDevice()) { + if (!m_surface_manager->SetupImageReader(...)) { + return false; // ✅ 올바른 실패 처리 + } + } + + ⚠️ 한 가지만 확인 + + CLAUDE.md line 255-257: + ## 🎯 Design Decisions + - Android: ONLY register MediaCodec hardware decoder. + - Do NOT fallback to dav1d CPU decoder + + 당신의 프로젝트 문서에도 이미 명시되어 있습니다. 이 수정은 문서화된 설계와 일치합니다. + + 결론: 진행해도 됩니다. 이것이 당신의 게임 배포 요구사항에 맞는 올바른 구현입니다. + +> 그럼 여태까지 스펙대로 구현하지 않고, 뭘 수정한 것인가? + +● 좋은 질문입니다. 명확히 정리하겠습니다. + + 🔴 이전 코드의 문제점 + + 1. 초기화 순서 문제 (치명적 버그) + + 이전 VavCore.cpp (잘못된 순서): + // Line ~387: Decoder 먼저 초기화 + if (!player->impl->decoder->Initialize(metadata)) { + return VAVCORE_ERROR_DECODER_UNAVAILABLE; + } + + // Line ~440: Vulkan device는 나중에 등록 (너무 늦음!) + if (player->impl->has_vulkan_device) { + player->impl->decoder->SetVulkanDevice(...); + } + + 문제: MediaCodec는 Initialize() 시점에 Surface가 필요한데, Vulkan device가 없어서 ImageReader를 만들 수 없음. + + 수정된 VavCore.cpp (올바른 순서): + // Line 400-420: Vulkan device 먼저 등록 + if (player->impl->has_vulkan_device) { + player->impl->decoder->SetVulkanDevice(...); + } + + // Line 424: 이제 decoder 초기화 (Surface 준비됨) + if (!player->impl->decoder->Initialize(metadata)) { + return VAVCORE_ERROR_DECODER_UNAVAILABLE; + } + + 2. 애매한 에러 처리 (스펙 불일치) + + 현재 MediaCodecAV1Decoder::Initialize() Step 3: + if (m_surface_manager->GetVulkanDevice()) { + if (!m_surface_manager->SetupImageReader(...)) { + LOGF_WARNING("ImageReader setup failed, continuing without zero-copy"); + // ⚠️ 계속 진행함 - "Vulkan optional" 스타일 + } + } + + 문제: + - Warning만 출력하고 계속 진행 + - Vulkan device 설정했는데 ImageReader 실패하면 결국 비디오 출력 안 됨 + - 하지만 Initialize()는 true 반환 → 사용자는 성공했다고 생각 + - 나중에 GetNextFrame() 시점에 "왜 화면이 안 나와?" 상태 + + 제안한 수정 ("GPU 필수" 스펙 반영): + if (m_surface_manager->GetVulkanDevice()) { + if (!m_surface_manager->SetupImageReader(...)) { + LOGF_ERROR("Vulkan device set but ImageReader failed"); + return false; // ❌ 즉시 실패 + } + } + + 📊 요약: 무엇을 수정했나? + + | 항목 | 이전 상태 | 수정 후 | + |--------|-----------------------------|--------------------------------| + | 초기화 순서 | Decoder 먼저 → Vulkan 나중 (버그) | Vulkan 먼저 → Decoder 나중 (수정 완료) | + | 에러 처리 | Warning 출력하고 계속 진행 (애매함) | 실패 시 즉시 return false (제안 중) | + | 스펙 준수 | "Vulkan optional" 스타일 | "GPU 필수" 스타일 | + + 🎯 현재 상황 + + 1. 초기화 순서 문제: ✅ 이미 VavCore.cpp 수정으로 해결됨 + 2. 에러 처리 명확화: 🟡 제안했지만 아직 적용 안 함 + + 당신이 지금 확인한 것: "에러 처리 명확화도 스펙에 맞으니 진행해도 되냐?" → 네, 진행해도 됩니다. diff --git a/vav2/docs/working/mediacodec-recreation.md b/vav2/docs/working/mediacodec-recreation.md new file mode 100644 index 0000000..db0c0da --- /dev/null +++ b/vav2/docs/working/mediacodec-recreation.md @@ -0,0 +1,69 @@ +# MediaCodec Recreation Issue Analysis + +## Why MediaCodec Recreation is Needed + +### Problem +MediaCodec is initially configured with `surface=nullptr` (CPU mode), but later we need it to output to ImageReader surface (GPU mode) when Vulkan device is registered. + +### Android MediaCodec Limitation +`AMediaCodec_setOutputSurface()` API has limitations: +- Only works for video tunneling mode +- **Does NOT work reliably with async mode callbacks** +- Cannot change surface after codec is started in async mode + +### Current Flow +1. `Initialize()` creates MediaCodec with surface=nullptr +2. MediaCodec starts with async callbacks +3. `SetVulkanDevice()` is called later +4. ImageReader surface is created +5. Need to switch MediaCodec output to ImageReader surface +6. **setOutputSurface() doesn't work in async mode → Must recreate codec** + +## Current Issue After Recreation + +Even after recreation, `getInputBuffer()` returns null for buffer indices from callbacks. + +### Root Cause +**Race condition after `AMediaCodec_start()`:** +- Callbacks fire immediately when codec starts +- Buffer indices are enqueued (0-9) +- BUT codec internal buffer allocation isn't complete yet +- When `getInputBuffer()` is called with these indices → returns null + +### Evidence from Logs +``` +23:32:11.938 - Codec started, callbacks fire (indices 0-9 queued) +23:32:12.142 - DecodeFrameAsync gets index 0 +23:32:12.142 - getInputBuffer(index=0) → returns null! +``` + +## Possible Solutions + +### Option 1: Wait after start() then clear queue +After `AMediaCodec_start()`, wait 10-50ms for codec to stabilize, then clear old indices and wait for new callbacks. + +### Option 2: Retry logic +If `getInputBuffer()` returns null, put index back in queue and retry with next index. + +### Option 3: Don't recreate - use software decode path +Accept that async mode with surface switching doesn't work well, fall back to CPU decoding. + +### Option 4: Change initialization order (BEST) +- Call `SetVulkanDevice()` BEFORE `Initialize()` +- But this requires changing application code + +## Recommendation + +Try **Option 1** first - it's the simplest fix: +```cpp +// After AMediaCodec_start() +std::this_thread::sleep_for(std::chrono::milliseconds(50)); +// Clear old buffer indices +{ + std::lock_guard lock(m_async_handler->m_async_mutex); + while (!m_async_handler->m_async_input_buffer_queue.empty()) { + m_async_handler->m_async_input_buffer_queue.pop(); + } +} +// Wait for new callbacks with valid buffers +``` diff --git a/vav2/platforms/android/applications/vav2player/app/src/main/res/layout/activity_main.xml b/vav2/platforms/android/applications/vav2player/app/src/main/res/layout/activity_main.xml index cb8eca4..2710fb5 100644 --- a/vav2/platforms/android/applications/vav2player/app/src/main/res/layout/activity_main.xml +++ b/vav2/platforms/android/applications/vav2player/app/src/main/res/layout/activity_main.xml @@ -1,50 +1,45 @@ - - - + + android:layout_height="match_parent" + android:layout_gravity="center" /> - + + - - + + - - - - - - + + android:padding="8dp" + android:background="#80000000" + android:clickable="false" + android:focusable="false"> + android:textSize="12sp" + android:gravity="center_horizontal" + android:shadowColor="#000000" + android:shadowDx="1" + android:shadowDy="1" + android:shadowRadius="2" /> + android:fontFamily="monospace" + android:shadowColor="#000000" + android:shadowDx="1" + android:shadowDy="1" + android:shadowRadius="2" /> - \ No newline at end of file + \ No newline at end of file diff --git a/vav2/platforms/android/applications/vav2player/app/src/main/res/values/themes.xml b/vav2/platforms/android/applications/vav2player/app/src/main/res/values/themes.xml index cc182d1..299b5cb 100644 --- a/vav2/platforms/android/applications/vav2player/app/src/main/res/values/themes.xml +++ b/vav2/platforms/android/applications/vav2player/app/src/main/res/values/themes.xml @@ -1,10 +1,14 @@ - \ No newline at end of file diff --git a/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.cpp b/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.cpp index 1435cff..4483af8 100644 --- a/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.cpp +++ b/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.cpp @@ -11,6 +11,8 @@ // MediaCodec list functionality may need alternative implementation #include #include +#include +#include #include #if __ANDROID_API__ >= 29 #include @@ -133,7 +135,7 @@ bool MediaCodecAV1Decoder::SetupVulkanPipeline() { // Initialization helper: Finalize initialization bool MediaCodecAV1Decoder::FinalizeInitialization() { m_initialized = true; - if (m_buffer_processor) { + if (m_buffer_processor && !IsAsyncModeEnabled()) { m_buffer_processor->ResetPriming(); } LogInfo("MediaCodec decoder initialization completed successfully"); @@ -153,17 +155,38 @@ bool MediaCodecAV1Decoder::Initialize(const VideoMetadata& metadata) { // Continue anyway - DetectHardwareCapabilities logs details } - // Step 3: Initialize codec with fallback strategy - if (!InitializeCodecWithFallback()) { - LogWarning("All hardware AV1 decoders failed, falling back to software (dav1d)"); - m_hardware_accelerated = false; - return false; // Return false to let factory try next decoder (dav1d) + // Step 3: Vulkan device set? ImageReader is MANDATORY (GPU-only requirement) + if (m_surface_manager->GetVulkanDevice()) { + LogInfo("Vulkan device set - setting up ImageReader (GPU h/w processing required)"); + + JavaVM* javaVM = GetAndroidJavaVM(); + if (!javaVM) { + LogError("Vulkan device set but JavaVM unavailable"); + return false; // Hard failure - GPU requirement not met + } + + m_surface_manager->SetJavaVM(javaVM); + m_surface_manager->SetVideoDimensions(m_width, m_height); + + if (!m_surface_manager->SetupImageReader(m_width, m_height)) { + LogError("Vulkan device set but ImageReader setup failed"); + return false; // Hard failure - GPU requirement not met + } + + m_surface = m_surface_manager->GetAndroidSurface(); + if (!m_surface) { + LogError("Vulkan device set but Surface unavailable"); + return false; // Hard failure - GPU requirement not met + } + + LogInfo("Vulkan zero-copy pipeline ready"); } - // Step 4: Setup Vulkan pipeline if Vulkan device is already set - if (!SetupVulkanPipeline()) { - LogWarning("Vulkan pipeline setup failed - continuing with CPU fallback"); - // Not fatal - continue initialization + // Step 4: Initialize codec with fallback strategy + if (!InitializeCodecWithFallback()) { + LogError("Hardware AV1 decoder initialization failed"); + m_hardware_accelerated = false; + return false; } // Step 5: Finalize initialization @@ -543,125 +566,56 @@ JNIEnv* MediaCodecAV1Decoder::GetJNIEnv() const { } bool MediaCodecAV1Decoder::SetVulkanDevice(void* vk_device, void* vk_instance, void* vk_physical_device) { - if (!m_initialized) { - LogError("Cannot set Vulkan device - decoder not initialized"); + // Pass JavaVM to surface manager before setting Vulkan device + JavaVM* javaVM = GetAndroidJavaVM(); + if (javaVM) { + m_surface_manager->SetJavaVM(javaVM); + LogInfo("JavaVM passed to surface manager in SetVulkanDevice()"); + } else { + LogWarning("JavaVM not available in SetVulkanDevice() - ImageReader may fail"); + } + + // Always store Vulkan device in surface manager (works before or after initialization) + bool result = m_surface_manager->SetVulkanDevice(vk_device, vk_instance, vk_physical_device); + if (result) { + LogInfo("Vulkan device registered with surface manager"); + } else { + LogError("Failed to register Vulkan device with surface manager"); return false; } + // If decoder is not yet initialized, Initialize() will use the stored Vulkan device + if (!m_initialized) { + LogInfo("Decoder not initialized yet - Vulkan device stored for later use during Initialize()"); + return true; + } + + // If decoder is already initialized, we need to handle it differently if (!m_hardware_accelerated) { LogWarning("Vulkan image requires hardware acceleration"); return false; } - // CRITICAL: Pass JavaVM to surface manager before setting Vulkan device - // This is needed for ImageReader initialization on decoder thread - LogInfo("[SetVulkanDevice] About to call GetAndroidJavaVM()..."); - JavaVM* javaVM = GetAndroidJavaVM(); - LogInfo("[SetVulkanDevice] GetAndroidJavaVM() returned: " + std::string(javaVM ? "VALID" : "NULL") + " (" + std::to_string(reinterpret_cast(javaVM)) + ")"); - - if (javaVM) { - LogInfo("[SetVulkanDevice] JavaVM is valid, passing to surface manager..."); - m_surface_manager->SetJavaVM(javaVM); - LogInfo("JavaVM passed to surface manager in SetVulkanDevice()"); - } else { - LogError("[SetVulkanDevice] JavaVM is NULL! ImageReader initialization will fail!"); - LogWarning("JavaVM not available in SetVulkanDevice() - ImageReader cannot be initialized"); + // Check if ImageReader surface is already configured (early initialization path) + ANativeWindow* current_surface = m_surface_manager->GetAndroidSurface(); + if (current_surface && current_surface == m_surface) { + LogInfo("ImageReader surface already configured during Initialize() - no recreation needed!"); + return true; // Early return - avoid recreation } - // Delegate to surface manager - bool result = m_surface_manager->SetVulkanDevice(vk_device, vk_instance, vk_physical_device); - if (result) { - LogInfo("Vulkan device set successfully"); - - // CRITICAL FIX: If video dimensions are already set (decoder initialized after Vulkan device), - // we need to setup ImageReader and reconfigure MediaCodec! - if (m_width > 0 && m_height > 0) { - LogInfo("Setting up ImageReader → VkImage pipeline after Vulkan device registration"); - - // Set video dimensions for ImageReader allocation - m_surface_manager->SetVideoDimensions(m_width, m_height); - - // Setup ImageReader with video dimensions - if (!m_surface_manager->SetupImageReader(m_width, m_height)) { - LogError("Failed to setup ImageReader after Vulkan device set"); - return false; - } - - // Get Surface from ImageReader for MediaCodec - m_surface = m_surface_manager->GetAndroidSurface(); - if (!m_surface) { - LogError("Failed to get Surface from ImageReader after Vulkan device set"); - return false; - } - - // CRITICAL: MediaCodec cannot dynamically change surface after being configured with nullptr - // We must stop, reconfigure, and restart MediaCodec with the ImageReader surface - LogInfo("Reconfiguring MediaCodec with ImageReader surface"); - - // Step 1: Cleanup async mode before stopping MediaCodec - CleanupAsyncMode(); - - // Step 2: Stop MediaCodec - media_status_t status = AMediaCodec_stop(m_codec); - if (status != AMEDIA_OK) { - LogError("Failed to stop MediaCodec for reconfiguration: " + std::to_string(status)); - return false; - } - LogInfo("MediaCodec stopped for reconfiguration"); - - // Step 3: Reconfigure MediaCodec with ImageReader surface - status = AMediaCodec_configure( - m_codec, - m_format, - m_surface, // ImageReader surface - nullptr, // No crypto - 0 // Decoder flag - ); - if (status != AMEDIA_OK) { - LogError("Failed to reconfigure MediaCodec with ImageReader surface: " + std::to_string(status)); - return false; - } - LogInfo("MediaCodec reconfigured with ImageReader surface"); - - // Step 4: Setup async callbacks AFTER configure but BEFORE start - LogInfo("Setting up async callbacks for reconfigured MediaCodec"); - if (InitializeAsyncMode()) { - if (EnableAsyncMode(true)) { - LogInfo("Async callbacks registered successfully after reconfiguration"); - } else { - LogWarning("Failed to activate async mode - continuing with sync mode"); - } - } else { - LogWarning("Failed to initialize async mode - continuing with sync mode"); - } - - // Step 5: Start MediaCodec - status = AMediaCodec_start(m_codec); - if (status != AMEDIA_OK) { - LogError("Failed to restart MediaCodec after reconfiguration: " + std::to_string(status)); - return false; - } - LogInfo("MediaCodec restarted successfully with ImageReader output"); - - // Step 6: Re-prime the decoder after reconfiguration - if (m_buffer_processor) { - m_buffer_processor->ResetPriming(); - if (m_buffer_processor->PrimeDecoder()) { - LogInfo("MediaCodec re-primed after reconfiguration"); - } else { - LogWarning("MediaCodec priming failed after reconfiguration, but continuing"); - } - } - } + // Decoder was initialized without Vulkan, now Vulkan device is being set + // This is the problematic late-registration path + if (m_width > 0 && m_height > 0) { + LogWarning("Vulkan device set AFTER decoder initialization - ImageReader cannot be added"); + LogWarning("MediaCodec must be created with surface from the start"); + LogWarning("Continuing with CPU fallback - video will work but without zero-copy GPU pipeline"); + // Return true because decoder is still functional with CPU path + return true; } + return result; } -bool MediaCodecAV1Decoder::CreateVulkanImage(void* vk_device, void* vk_instance) { - // Delegate to surface manager - return m_surface_manager->CreateVulkanImage(vk_device, vk_instance); -} - bool MediaCodecAV1Decoder::SetupAHardwareBuffer() { // Delegate to surface manager bool result = m_surface_manager->SetupAHardwareBuffer(); @@ -753,11 +707,16 @@ bool MediaCodecAV1Decoder::InitializeMediaCodec() { // Prime the decoder immediately after initialization (before any decoding starts) // This ensures no concurrent dequeue issues - if (!m_buffer_processor->PrimeDecoder()) { - LogWarning("MediaCodec priming failed, but continuing initialization"); - // Not a fatal error - decoder can work without priming + // NOTE: Skip priming in async mode - callbacks handle buffer management + if (!IsAsyncModeEnabled()) { + if (!m_buffer_processor->PrimeDecoder()) { + LogWarning("MediaCodec priming failed, but continuing initialization"); + // Not a fatal error - decoder can work without priming + } else { + LogInfo("MediaCodec primed successfully during initialization"); + } } else { - LogInfo("MediaCodec primed successfully during initialization"); + LogInfo("Skipping priming - async mode uses callback-based buffer management"); } return true; diff --git a/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.h b/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.h index 84e30a9..334228c 100644 --- a/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.h +++ b/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAV1Decoder.h @@ -102,7 +102,6 @@ public: JNIEnv* GetJNIEnv() const; // Vulkan image support (public for testing) - bool CreateVulkanImage(void* vk_device, void* vk_instance); bool SetupAHardwareBuffer(); bool CreateSurfaceFromAHardwareBuffer(AHardwareBuffer* buffer); diff --git a/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAsyncHandler.cpp b/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAsyncHandler.cpp index 790572c..806e723 100644 --- a/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAsyncHandler.cpp +++ b/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAsyncHandler.cpp @@ -47,11 +47,14 @@ void MediaCodecAsyncHandler::Cleanup() { m_decoder = nullptr; m_async_processing_active = false; - // Clear async queue + // Clear async queues std::lock_guard lock(m_async_mutex); while (!m_async_output_queue.empty()) { m_async_output_queue.pop(); } + while (!m_async_input_buffer_queue.empty()) { + m_async_input_buffer_queue.pop(); + } } bool MediaCodecAsyncHandler::SupportsAsyncMode() const { @@ -83,8 +86,14 @@ bool MediaCodecAsyncHandler::InitializeAsyncMode() { // Setup async callbacks m_async_callbacks.onInputBufferAvailable = [this](int32_t index) { - // Input buffer available - not used in current implementation - // Can be used for async input enqueue in future optimization + // Input buffer available - store in queue for DecodeFrameAsync + LogInfo("onInputBufferAvailable callback: index=" + std::to_string(index) + + ", MediaCodec=" + std::to_string(reinterpret_cast(m_codec))); + std::lock_guard lock(m_async_mutex); + m_async_input_buffer_queue.push(index); + LogInfo("Input buffer queued: index=" + std::to_string(index) + + ", queue size=" + std::to_string(m_async_input_buffer_queue.size())); + m_async_condition.notify_one(); }; m_async_callbacks.onOutputBufferAvailable = [this](int32_t index, AMediaCodecBufferInfo* bufferInfo) { @@ -140,6 +149,9 @@ bool MediaCodecAsyncHandler::InitializeAsyncMode() { m_async_mode_enabled = true; m_async_processing_active = true; LogInfo("Async mode initialized successfully"); + + // NOTE: Input buffer callbacks will be triggered automatically after MediaCodec start + // No need to wait here - callbacks are asynchronous return true; } @@ -150,16 +162,42 @@ void MediaCodecAsyncHandler::CleanupAsyncMode() { LogInfo("Cleaning up async mode"); m_async_processing_active = false; + + // CRITICAL: Unregister async callbacks from MediaCodec BEFORE clearing state + // This prevents lingering callbacks from firing after codec deletion + if (m_codec) { + LogInfo("Unregistering async callbacks from MediaCodec"); + media_status_t status = AMediaCodec_setAsyncNotifyCallback( + m_codec, + { + .onAsyncInputAvailable = nullptr, + .onAsyncOutputAvailable = nullptr, + .onAsyncFormatChanged = nullptr, + .onAsyncError = nullptr + }, + nullptr // no userdata + ); + + if (status != AMEDIA_OK) { + LogWarning("Failed to unregister async callbacks: " + std::to_string(status)); + } else { + LogInfo("Async callbacks unregistered successfully"); + } + } + m_async_mode_enabled = false; // Wake up any waiting threads m_async_condition.notify_all(); - // Clear async queue + // Clear async queues std::lock_guard lock(m_async_mutex); while (!m_async_output_queue.empty()) { m_async_output_queue.pop(); } + while (!m_async_input_buffer_queue.empty()) { + m_async_input_buffer_queue.pop(); + } LogInfo("Async mode cleanup complete"); } @@ -170,20 +208,56 @@ bool MediaCodecAsyncHandler::DecodeFrameAsync(const uint8_t* packet_data, size_t return false; } - // Enqueue input buffer - ssize_t input_index = AMediaCodec_dequeueInputBuffer(m_codec, 10000); // 10ms timeout - if (input_index < 0) { - LogWarning("DecodeFrameAsync: No input buffer available"); - return false; + // Wait for input buffer from async callback + ssize_t input_index = -1; + { + std::unique_lock lock(m_async_mutex); + + // Wait for input buffer with progressive timeout + // First decode might need longer wait for callbacks to start + int timeout_ms = m_async_input_buffer_queue.empty() ? 500 : 100; // 500ms first time, 100ms thereafter + + bool buffer_available = m_async_condition.wait_for( + lock, + std::chrono::milliseconds(timeout_ms), + [this] { return !m_async_input_buffer_queue.empty() || !m_async_processing_active; } + ); + + if (!buffer_available || m_async_input_buffer_queue.empty()) { + LogWarning("DecodeFrameAsync: No input buffer available after " + std::to_string(timeout_ms) + "ms (queue size: " + + std::to_string(m_async_input_buffer_queue.size()) + ")"); + return false; + } + + // Get input buffer index from queue + input_index = m_async_input_buffer_queue.front(); + m_async_input_buffer_queue.pop(); + LogInfo("DecodeFrameAsync: Got input buffer index " + std::to_string(input_index) + + " from queue (remaining: " + std::to_string(m_async_input_buffer_queue.size()) + ")"); } + // Log codec state before attempting getInputBuffer + LogInfo("DecodeFrameAsync: About to call getInputBuffer with index=" + std::to_string(input_index) + + ", MediaCodec=" + std::to_string(reinterpret_cast(m_codec)) + + ", async_mode_enabled=" + std::to_string(m_async_mode_enabled) + + ", async_processing_active=" + std::to_string(m_async_processing_active)); + size_t buffer_capacity = 0; uint8_t* input_buffer = AMediaCodec_getInputBuffer(m_codec, input_index, &buffer_capacity); + + LogInfo("DecodeFrameAsync: getInputBuffer returned: buffer=" + + std::to_string(reinterpret_cast(input_buffer)) + + ", capacity=" + std::to_string(buffer_capacity)); + if (!input_buffer) { - LogError("DecodeFrameAsync: Failed to get input buffer"); + LogError("DecodeFrameAsync: Failed to get input buffer for index " + std::to_string(input_index) + + ", buffer_capacity=" + std::to_string(buffer_capacity)); + LogError("DecodeFrameAsync: MediaCodec=" + std::to_string(reinterpret_cast(m_codec))); return false; } + LogInfo("DecodeFrameAsync: Got input buffer successfully, capacity=" + std::to_string(buffer_capacity)); + if (packet_size > buffer_capacity) { LogError("DecodeFrameAsync: Packet size exceeds buffer capacity"); AMediaCodec_queueInputBuffer(m_codec, input_index, 0, 0, 0, 0); @@ -233,6 +307,16 @@ bool MediaCodecAsyncHandler::WaitForAsyncFrame(VideoFrame& output_frame, int tim return true; } +void MediaCodecAsyncHandler::ClearInputBufferQueue() { + std::lock_guard lock(m_async_mutex); + int cleared_count = 0; + while (!m_async_input_buffer_queue.empty()) { + m_async_input_buffer_queue.pop(); + cleared_count++; + } + LogInfo("Cleared " + std::to_string(cleared_count) + " stale input buffer indices from queue"); +} + 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"); @@ -329,8 +413,15 @@ bool MediaCodecAsyncHandler::ProcessAsyncOutputFrame(int32_t output_index, AMedi void MediaCodecAsyncHandler::OnAsyncInputAvailable(AMediaCodec* codec, void* userdata, int32_t index) { auto* handler = static_cast(userdata); - if (handler && handler->m_async_callbacks.onInputBufferAvailable) { - handler->m_async_callbacks.onInputBufferAvailable(index); + if (handler) { + // Log codec pointer from callback parameter vs stored pointer + handler->LogInfo("OnAsyncInputAvailable: index=" + std::to_string(index) + + ", callback_codec=" + std::to_string(reinterpret_cast(codec)) + + ", stored_codec=" + std::to_string(reinterpret_cast(handler->m_codec))); + + if (handler->m_async_callbacks.onInputBufferAvailable) { + handler->m_async_callbacks.onInputBufferAvailable(index); + } } } diff --git a/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAsyncHandler.h b/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAsyncHandler.h index e36dd94..81960b4 100644 --- a/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAsyncHandler.h +++ b/vav2/platforms/windows/vavcore/src/Decoder/MediaCodecAsyncHandler.h @@ -65,6 +65,9 @@ public: bool DecodeFrameAsync(const uint8_t* packet_data, size_t packet_size, VideoFrame& output_frame); bool WaitForAsyncFrame(VideoFrame& output_frame, int timeout_ms = 100); + // Queue management + void ClearInputBufferQueue(); + // Async callback handlers (static methods for C callback compatibility) static void OnAsyncInputAvailable(AMediaCodec* codec, void* userdata, int32_t index); static void OnAsyncOutputAvailable(AMediaCodec* codec, void* userdata, int32_t index, AMediaCodecBufferInfo* bufferInfo); @@ -100,6 +103,9 @@ private: // Async output queue std::queue m_async_output_queue; + // Async input buffer index queue + std::queue m_async_input_buffer_queue; + // Async callbacks MediaCodecAsyncCallbacks m_async_callbacks; }; diff --git a/vav2/platforms/windows/vavcore/src/VavCore.cpp b/vav2/platforms/windows/vavcore/src/VavCore.cpp index 567aef3..6c58b81 100644 --- a/vav2/platforms/windows/vavcore/src/VavCore.cpp +++ b/vav2/platforms/windows/vavcore/src/VavCore.cpp @@ -397,25 +397,26 @@ VAVCORE_API VavCoreResult vavcore_open_file(VavCorePlayer* player, const char* f player->impl->pendingD3DSurfaceType = VAVCORE_SURFACE_CPU; } - LOGF_DEBUG("[VavCore] Initializing decoder..."); - - // Initialize decoder - if (!player->impl->decoder->Initialize(player->impl->metadata)) { - LOGF_ERROR("[VavCore] Decoder initialization failed (unsupported format or hardware unavailable)"); - player->impl->decoder.reset(); - player->impl->fileReader->CloseFile(); - return VAVCORE_ERROR_DECODER_UNAVAILABLE; - } - - LOGF_DEBUG("[VavCore] Decoder initialized successfully!"); - #ifdef ANDROID - // Apply pending Vulkan device AFTER decoder initialization + // CRITICAL: Apply Vulkan device BEFORE decoder initialization + // This allows MediaCodec to be created with ImageReader surface from the start if (player->impl->has_vulkan_device) { - LOGF_DEBUG("[VavCore] Applying pending Vulkan device after decoder initialization..."); + LOGF_DEBUG("[VavCore] Applying pending Vulkan device BEFORE decoder initialization..."); LOGF_DEBUG("[VavCore] Vulkan device: %p, instance: %p, physical device: %p", player->impl->vulkan_device, player->impl->vulkan_instance, player->impl->vulkan_physical_device); + // Pre-check: Vulkan device requires JavaVM for ImageReader initialization + // If JavaVM is not available, decoder initialization will 100% fail + JavaVM* javaVM = VavCore::GetAndroidJavaVM(); + if (!javaVM) { + LOGF_ERROR("[VavCore] CRITICAL: Vulkan device set but JavaVM unavailable!"); + LOGF_ERROR("[VavCore] This means libVavCore.so was not properly loaded or JNI_OnLoad failed."); + LOGF_ERROR("[VavCore] GPU hardware processing requires JNI (Android requirement)."); + player->impl->decoder.reset(); + player->impl->fileReader->CloseFile(); + return VAVCORE_ERROR_INIT_FAILED; + } + bool vulkan_success = player->impl->decoder->SetVulkanDevice( player->impl->vulkan_device, player->impl->vulkan_instance, @@ -423,16 +424,29 @@ VAVCORE_API VavCoreResult vavcore_open_file(VavCorePlayer* player, const char* f ); if (vulkan_success) { - LOGF_INFO("[VavCore] Vulkan device successfully registered with decoder"); + LOGF_INFO("[VavCore] Vulkan device registered with decoder BEFORE initialization"); } else { - LOGF_WARNING("[VavCore] Failed to register Vulkan device with decoder (will use CPU fallback)"); + LOGF_ERROR("[VavCore] Failed to register Vulkan device with decoder"); + LOGF_ERROR("[VavCore] GPU hardware processing requirement not met"); + player->impl->decoder.reset(); + player->impl->fileReader->CloseFile(); + return VAVCORE_ERROR_INIT_FAILED; } - - // Note: We keep has_vulkan_device=true even if registration failed - // This allows retry on next decoder recreation } #endif + LOGF_DEBUG("[VavCore] Initializing decoder..."); + + // Initialize decoder (now with Vulkan device already set!) + if (!player->impl->decoder->Initialize(player->impl->metadata)) { + LOGF_ERROR("[VavCore] Decoder initialization failed (unsupported format or hardware unavailable)"); + player->impl->decoder.reset(); + player->impl->fileReader->CloseFile(); + return VAVCORE_ERROR_DECODER_UNAVAILABLE; + } + + LOGF_DEBUG("[VavCore] Decoder initialized successfully!"); + // Apply debug options to newly created decoder player->impl->decoder->SetDebugOptions(&player->impl->debugOptions); LOGF_DEBUG("[VavCore] Debug options applied to decoder");