From 03de610304ddb2067a2ef622db9714921fcee780 Mon Sep 17 00:00:00 2001 From: ened Date: Tue, 23 Sep 2025 05:52:19 +0900 Subject: [PATCH] Build unit test environment --- vav2/CLAUDE.md | 23 + vav2/UNIT_TEST_REFACTORING_PLAN.md | 717 ++++++++++++++++++ .../Vav2Player/Vav2UnitTest.vcxproj | 38 +- .../Vav2Player/VideoPlayerControl.xaml.cpp | 2 +- .../Vav2Player/VideoPlayerControl.xaml.h | 6 +- .../headless/SimpleHeadlessMain.cpp | 9 +- .../Vav2Player/src/Common/VideoTypes.h | 17 +- .../Vav2Player/src/FileIO/IWebMFileReader.h | 77 ++ .../Vav2Player/src/FileIO/WebMFileReader.cpp | 90 +-- .../Vav2Player/src/FileIO/WebMFileReader.h | 82 +- .../Vav2Player/src/Rendering/IVideoRenderer.h | 45 ++ .../src/Rendering/SimpleGPURenderer.cpp | 45 +- .../src/Rendering/SimpleGPURenderer.h | 34 +- .../Vav2Player/unit-test/AV1DecoderTest.cpp | 214 ++++++ .../unit-test/MockVideoRenderer.cpp | 198 +++++ .../Vav2Player/unit-test/MockVideoRenderer.h | 98 +++ .../unit-test/MockWebMFileReader.cpp | 247 ++++++ .../Vav2Player/unit-test/MockWebMFileReader.h | 84 ++ .../unit-test/VideoPlayerControlTest.cpp | 245 ++++++ .../unit-test/VideoRendererTest.cpp | 288 +++++++ .../unit-test/WebMFileReaderTest.cpp | 220 ++++++ vav2/Vav2Player/Vav2Player/unit-test/pch.h | 51 +- 22 files changed, 2697 insertions(+), 133 deletions(-) create mode 100644 vav2/UNIT_TEST_REFACTORING_PLAN.md create mode 100644 vav2/Vav2Player/Vav2Player/src/FileIO/IWebMFileReader.h create mode 100644 vav2/Vav2Player/Vav2Player/src/Rendering/IVideoRenderer.h create mode 100644 vav2/Vav2Player/Vav2Player/unit-test/AV1DecoderTest.cpp create mode 100644 vav2/Vav2Player/Vav2Player/unit-test/MockVideoRenderer.cpp create mode 100644 vav2/Vav2Player/Vav2Player/unit-test/MockVideoRenderer.h create mode 100644 vav2/Vav2Player/Vav2Player/unit-test/MockWebMFileReader.cpp create mode 100644 vav2/Vav2Player/Vav2Player/unit-test/MockWebMFileReader.h create mode 100644 vav2/Vav2Player/Vav2Player/unit-test/VideoPlayerControlTest.cpp create mode 100644 vav2/Vav2Player/Vav2Player/unit-test/VideoRendererTest.cpp create mode 100644 vav2/Vav2Player/Vav2Player/unit-test/WebMFileReaderTest.cpp diff --git a/vav2/CLAUDE.md b/vav2/CLAUDE.md index b80251c..c0a5329 100644 --- a/vav2/CLAUDE.md +++ b/vav2/CLAUDE.md @@ -9,6 +9,29 @@ - **방식**: "삭제 우선" - 고급 파이프라인 전면 제거 - **이유**: 방어 코딩으로 증상만 치료하는 대신 근본 원인 해결 +### 📋 **최우선 작업 순서 (2025-09-23 업데이트)** + +#### **✅ MAJOR_REFACTORING_GUIDE.md 완료** (완료됨) +- [x] 복잡한 파이프라인 파일들 삭제 (ThreadedDecoder, OverlappedProcessor, DependencyScheduler) +- [x] VideoPlayerControl.xaml.h 멤버 변수 대폭 정리 (10개 이상 → 3-4개) +- [x] VideoPlayerControl.xaml.cpp ProcessSingleFrame() 단순화 (1000줄 → 25줄) +- [x] 빌드 테스트 및 기본 비디오 재생 동작 확인 + +#### **✅ GPU 파이프라인 재설계 완료** (완료됨) +- [x] 단순 GPU 파이프라인 설계 (CPU Thread → GPU Thread) +- [x] SimpleGPURenderer 구현 +- [x] CPU-GPU 하이브리드 fallback 구조 +- [x] 성능 최적화 및 안정성 확보 + +#### **🔥 현재 작업: Unit Test 구현** (최고 우선순위) +- **참조 문서**: [UNIT_TEST_REFACTORING_PLAN.md](./UNIT_TEST_REFACTORING_PLAN.md) +- **의사결정 필요**: Option A (인터페이스+Mock) vs Option B (직접 테스트) +- [ ] 인터페이스 리팩토링 (Option A 선택 시) +- [ ] Mock 시스템 구축 (MockWebMFileReader, MockVideoRenderer) +- [ ] 핵심 컴포넌트 테스트 작성 (WebMFileReader, AV1Decoder, VideoDecoderFactory) +- [ ] GPU 렌더링 테스트 (SimpleGPURenderer) +- [ ] 통합 테스트 구현 (전체 파이프라인 검증) + --- ## 🚀 현재 작업 단계: 고급 파이프라인 디버깅 및 검증 (CRITICAL) diff --git a/vav2/UNIT_TEST_REFACTORING_PLAN.md b/vav2/UNIT_TEST_REFACTORING_PLAN.md new file mode 100644 index 0000000..ddbc8dd --- /dev/null +++ b/vav2/UNIT_TEST_REFACTORING_PLAN.md @@ -0,0 +1,717 @@ +# 🧪 Unit Test 구현 계획 - 2025-09-23 개정판 + +**✅ 전제 조건**: MAJOR_REFACTORING_GUIDE 완료, GPU 파이프라인 재설계 완료 +**🎯 현재 목표**: 단순화된 아키텍처 (734줄)에 대한 포괄적 Unit Test 구현 + +--- + +## 🔍 **현재 상황 (2025-09-23)** + +### **✅ 완료된 작업** +- **MAJOR_REFACTORING_GUIDE**: 복잡한 파이프라인 제거, ProcessSingleFrame() 25줄 단순화 +- **GPU 파이프라인**: SimpleGPURenderer 구현, CPU-GPU 하이브리드 아키텍처 +- **기본 Unit Test**: VideoTypesTest.cpp 완료 +- **헤드리스 프로젝트**: 빌드 및 실행 성공 + +### **📋 현재 Unit Test 구조** +``` +D:\Project\video-av1\vav2\Vav2Player\Vav2Player\ +├── Vav2UnitTest.vcxproj # ✅ 기존 테스트 프로젝트 +├── unit-test/ +│ ├── pch.h / pch.cpp # ✅ 테스트 전용 PCH +│ └── VideoTypesTest.cpp # ✅ 기본 데이터 구조체 테스트 +└── x64/Debug/UnitTest/ # ✅ 빌드 출력 +``` + +### **🎯 추가 필요한 테스트** +- **WebMFileReader**: 파일 파싱 로직 (실제 구체 클래스 테스트) +- **AV1Decoder**: 디코딩 로직 +- **VideoDecoderFactory**: 팩토리 패턴 +- **SimpleGPURenderer**: GPU 렌더링 (스텁 모드) +- **통합 테스트**: 전체 파이프라인 + +--- + +## 🔍 **인터페이스 분석 (Unit Test 전 필수 검토)** + +### **현재 인터페이스 상황** +```cpp +✅ IVideoDecoder.h // 이미 인터페이스화 완료 +❌ WebMFileReader.h // 구체 클래스, 인터페이스 없음 +❌ SimpleGPURenderer.h // 구체 클래스, 인터페이스 없음 +✅ VideoDecoderFactory.h // 팩토리 패턴, 인터페이스 기반 +``` + +### **🔧 Option A: 인터페이스 추가 리팩토링 (권장)** + +#### **A.1 IWebMFileReader 인터페이스 생성** +```cpp +// src/FileIO/IWebMFileReader.h (신규 생성) +#pragma once +#include "../Common/VideoTypes.h" +#include +#include + +namespace Vav2Player { + +class IWebMFileReader { +public: + virtual ~IWebMFileReader() = default; + + // 파일 열기/닫기 + virtual bool OpenFile(const std::string& file_path) = 0; + virtual void CloseFile() = 0; + virtual bool IsFileOpen() const = 0; + + // 파일 및 스트림 정보 + virtual const VideoMetadata& GetVideoMetadata() const = 0; + virtual std::string GetFilePath() const = 0; + + // 비디오 트랙 관리 + virtual std::vector GetVideoTracks() const = 0; + virtual bool SelectVideoTrack(int track_number) = 0; + + // 패킷 읽기 + virtual bool ReadNextPacket(VideoPacket& packet) = 0; + virtual bool SeekToTime(double time_seconds) = 0; + virtual bool SeekToFrame(uint64_t frame_number) = 0; + virtual bool Reset() = 0; +}; + +} +``` + +#### **A.2 WebMFileReader 리팩토링** +```cpp +// src/FileIO/WebMFileReader.h 수정 +class WebMFileReader : public IWebMFileReader { + // 기존 구현을 virtual 메서드로 변경 + // 인터페이스 상속으로 변경 +}; +``` + +#### **A.3 IVideoRenderer 인터페이스 생성** +```cpp +// src/Rendering/IVideoRenderer.h (신규 생성) +#pragma once +#include "../Common/VideoTypes.h" +#include + +namespace Vav2Player { + +class IVideoRenderer { +public: + virtual ~IVideoRenderer() = default; + + // 초기화 + virtual HRESULT Initialize(uint32_t width, uint32_t height) = 0; + virtual bool IsInitialized() const = 0; + virtual void Cleanup() = 0; + + // 렌더링 + virtual bool TryRenderFrame(const VideoFrame& frame) = 0; + virtual HRESULT RenderVideoFrame(const VideoFrame& frame) = 0; + + // 설정 + virtual void SetAspectFitMode(bool enabled) = 0; + virtual void UpdateContainerSize(uint32_t width, uint32_t height) = 0; +}; + +} +``` + +### **🚀 Option B: 인터페이스 없이 직접 테스트 (빠른 시작)** + +#### **B.1 장점** +- 즉시 테스트 작성 시작 가능 +- 기존 코드 수정 없음 +- 단순한 접근법 + +#### **B.2 단점** +- Mock 생성 어려움 +- 의존성 주입 불가능 +- 테스트 격리 한계 + +--- + +## 📋 **단계별 구현 계획** + +### **🔄 Phase 0: 인터페이스 리팩토링 (선택적)** + +#### **0.1 인터페이스 필요성 평가** +```cpp +// 테스트 복잡도 vs 인터페이스 도입 비용 +Component | Mock 필요도 | 인터페이스 우선순위 +--------------------|----------|---------------- +WebMFileReader | 높음 | ⭐⭐⭐ 권장 +SimpleGPURenderer | 중간 | ⭐⭐ 선택적 +VideoDecoderFactory | 낮음 | ⭐ 이미 충분 +``` + +#### **0.2 인터페이스 리팩토링 (선택 시)** +1. **IWebMFileReader.h 생성** (2시간) +2. **WebMFileReader.h 수정** (1시간) +3. **IVideoRenderer.h 생성** (1시간) +4. **SimpleGPURenderer.h 수정** (1시간) +5. **VideoPlayerControl.xaml.h 의존성 업데이트** (30분) +6. **빌드 테스트** (30분) + +**총 예상 시간: 6시간** + +### **🔧 Phase 1: 핵심 컴포넌트 테스트 (우선순위 1)** + +#### **1.1 WebMFileReader 테스트 (인터페이스 기반)** +```cpp +// unit-test/WebMFileReaderTest.cpp +#include "pch.h" +#include "../src/FileIO/WebMFileReader.h" + +// Option A를 선택한 경우: Mock을 사용한 테스트 +#ifdef USE_INTERFACE_APPROACH +#include "../unit-test/mocks/MockWebMFileReader.h" +#endif + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace Vav2Player; + +TEST_CLASS(WebMFileReaderTest) +{ +public: + // Option A: 인터페이스 기반 테스트 (Mock 사용 가능) + TEST_METHOD(OpenFile_ValidFile_ShouldReturnTrue_WithMock) + { +#ifdef USE_INTERFACE_APPROACH + // Arrange + MockWebMFileReader mockReader; + mockReader.SetOpenFileResult(true); + + // Act + bool result = mockReader.OpenFile("test.webm"); + + // Assert + Assert::IsTrue(result); +#endif + } + + // Option B: 직접 테스트 (인터페이스 없이) + TEST_METHOD(Constructor_Default_ShouldInitializeCorrectly) + { + // Arrange & Act + WebMFileReader reader; + + // Assert + Assert::IsFalse(reader.IsFileOpen()); + } + + TEST_METHOD(OpenFile_NonExistentFile_ShouldReturnFalse) + { + // Arrange + WebMFileReader reader; + std::string nonExistentFile = "nonexistent_file.webm"; + + // Act + bool result = reader.OpenFile(nonExistentFile); + + // Assert + Assert::IsFalse(result); + } + + TEST_METHOD(OpenFile_EmptyFilename_ShouldReturnFalse) + { + // Arrange + WebMFileReader reader; + std::string emptyFile = ""; + + // Act + bool result = reader.OpenFile(emptyFile); + + // Assert + Assert::IsFalse(result); + } +}; +``` + +#### **1.2 AV1Decoder 테스트** +```cpp +// unit-test/AV1DecoderTest.cpp +#include "pch.h" +#include "../src/Decoder/AV1Decoder.h" + +TEST_CLASS(AV1DecoderTest) +{ +public: + TEST_METHOD(Constructor_Default_ShouldInitializeCorrectly) + { + // Arrange & Act + AV1Decoder decoder; + + // Assert + Assert::IsFalse(decoder.IsInitialized()); + } + + TEST_METHOD(Initialize_ValidMetadata_ShouldReturnTrue) + { + // Arrange + AV1Decoder decoder; + VideoMetadata metadata; + metadata.width = 1920; + metadata.height = 1080; + metadata.frame_rate = 30.0; + metadata.codec_type = VideoCodecType::AV1; + + // Act + bool result = decoder.Initialize(metadata); + + // Assert + Assert::IsTrue(result); + Assert::IsTrue(decoder.IsInitialized()); + } + + TEST_METHOD(Initialize_InvalidCodecType_ShouldReturnFalse) + { + // Arrange + AV1Decoder decoder; + VideoMetadata metadata; + metadata.codec_type = VideoCodecType::VP9; // Wrong codec + + // Act + bool result = decoder.Initialize(metadata); + + // Assert + Assert::IsFalse(result); + } +}; +``` + +#### **1.3 VideoDecoderFactory 테스트** +```cpp +// unit-test/VideoDecoderFactoryTest.cpp +#include "pch.h" +#include "../src/Decoder/VideoDecoderFactory.h" + +TEST_CLASS(VideoDecoderFactoryTest) +{ +public: + TEST_METHOD(CreateDecoder_AV1Software_ShouldReturnAV1Decoder) + { + // Act + auto decoder = VideoDecoderFactory::CreateDecoder( + VideoCodecType::AV1, + VideoDecoderFactory::DecoderType::SOFTWARE + ); + + // Assert + Assert::IsNotNull(decoder.get()); + } + + TEST_METHOD(CreateDecoder_AV1Auto_ShouldReturnDecoder) + { + // Act + auto decoder = VideoDecoderFactory::CreateDecoder( + VideoCodecType::AV1, + VideoDecoderFactory::DecoderType::AUTO + ); + + // Assert + Assert::IsNotNull(decoder.get()); + } + + TEST_METHOD(IsCodecSupported_AV1_ShouldReturnTrue) + { + // Act + bool isSupported = VideoDecoderFactory::IsCodecSupported(VideoCodecType::AV1); + + // Assert + Assert::IsTrue(isSupported); + } +}; +``` + +### **🔧 Phase 1.5: Mock 시스템 구현 (Option A 선택 시)** + +#### **1.5.1 MockWebMFileReader 구현** +```cpp +// unit-test/mocks/MockWebMFileReader.h +#pragma once +#include "../../src/FileIO/IWebMFileReader.h" +#include +#include + +namespace Vav2Player { + +class MockWebMFileReader : public IWebMFileReader { +private: + bool m_openFileResult = true; + bool m_isFileOpen = false; + VideoMetadata m_metadata; + std::vector m_testPackets; + size_t m_currentPacketIndex = 0; + +public: + // Mock 설정 메서드들 + void SetOpenFileResult(bool result) { m_openFileResult = result; } + void SetMetadata(const VideoMetadata& metadata) { m_metadata = metadata; } + void AddTestPacket(const VideoPacket& packet) { m_testPackets.push_back(packet); } + void ResetPackets() { m_testPackets.clear(); m_currentPacketIndex = 0; } + + // IWebMFileReader 구현 + bool OpenFile(const std::string& file_path) override { + m_isFileOpen = m_openFileResult; + return m_openFileResult; + } + + void CloseFile() override { + m_isFileOpen = false; + m_currentPacketIndex = 0; + } + + bool IsFileOpen() const override { + return m_isFileOpen; + } + + const VideoMetadata& GetVideoMetadata() const override { + return m_metadata; + } + + std::string GetFilePath() const override { + return "mock_file.webm"; + } + + std::vector GetVideoTracks() const override { + std::vector tracks; + VideoTrackInfo track; + track.track_number = 1; + track.codec_type = VideoCodecType::AV1; + track.width = 1920; + track.height = 1080; + tracks.push_back(track); + return tracks; + } + + bool SelectVideoTrack(int track_number) override { + return track_number == 1; + } + + bool ReadNextPacket(VideoPacket& packet) override { + if (m_currentPacketIndex >= m_testPackets.size()) { + return false; + } + packet = m_testPackets[m_currentPacketIndex++]; + return true; + } + + bool SeekToTime(double time_seconds) override { + m_currentPacketIndex = 0; + return true; + } + + bool SeekToFrame(uint64_t frame_number) override { + m_currentPacketIndex = static_cast(frame_number); + return m_currentPacketIndex < m_testPackets.size(); + } + + bool Reset() override { + m_currentPacketIndex = 0; + return true; + } +}; + +} +``` + +#### **1.5.2 MockVideoRenderer 구현** +```cpp +// unit-test/mocks/MockVideoRenderer.h +#pragma once +#include "../../src/Rendering/IVideoRenderer.h" + +namespace Vav2Player { + +class MockVideoRenderer : public IVideoRenderer { +private: + bool m_initialized = false; + bool m_tryRenderResult = true; + HRESULT m_renderResult = S_OK; + uint32_t m_width = 0; + uint32_t m_height = 0; + +public: + // Mock 설정 + void SetTryRenderResult(bool result) { m_tryRenderResult = result; } + void SetRenderResult(HRESULT result) { m_renderResult = result; } + + // 테스트 검증용 + uint32_t GetLastWidth() const { return m_width; } + uint32_t GetLastHeight() const { return m_height; } + + // IVideoRenderer 구현 + HRESULT Initialize(uint32_t width, uint32_t height) override { + m_width = width; + m_height = height; + m_initialized = (width > 0 && height > 0); + return m_initialized ? S_OK : E_INVALIDARG; + } + + bool IsInitialized() const override { + return m_initialized; + } + + void Cleanup() override { + m_initialized = false; + m_width = 0; + m_height = 0; + } + + bool TryRenderFrame(const VideoFrame& frame) override { + return m_initialized && m_tryRenderResult; + } + + HRESULT RenderVideoFrame(const VideoFrame& frame) override { + return m_initialized ? m_renderResult : E_NOT_VALID_STATE; + } + + void SetAspectFitMode(bool enabled) override { + // Mock implementation - do nothing + } + + void UpdateContainerSize(uint32_t width, uint32_t height) override { + // Mock implementation - store values + m_width = width; + m_height = height; + } +}; + +} +``` + +### **⚡ Phase 2: GPU 렌더링 테스트** + +#### **2.1 SimpleGPURenderer 테스트 (스텁 모드)** +```cpp +// unit-test/SimpleGPURendererTest.cpp +#include "pch.h" +#include "../src/Rendering/SimpleGPURenderer.h" + +TEST_CLASS(SimpleGPURendererTest) +{ +public: + TEST_METHOD(Constructor_Default_ShouldInitializeCorrectly) + { + // Arrange & Act + SimpleGPURenderer renderer; + + // Assert + Assert::IsFalse(renderer.IsInitialized()); + } + + TEST_METHOD(Initialize_ValidDimensions_ShouldReturnTrue) + { + // Arrange + SimpleGPURenderer renderer; + + // Act + HRESULT hr = renderer.Initialize(1920, 1080); + + // Assert + // Note: May fail in headless environment, but should not crash + // Success depends on D3D12 availability + Assert::IsTrue(SUCCEEDED(hr) || hr == DXGI_ERROR_UNSUPPORTED); + } +}; +``` + +### **🧪 Phase 3: 통합 테스트** + +#### **3.1 간단한 통합 테스트** +```cpp +// unit-test/IntegrationTest.cpp +#include "pch.h" +#include "../src/FileIO/WebMFileReader.h" +#include "../src/Decoder/VideoDecoderFactory.h" + +TEST_CLASS(IntegrationTest) +{ +public: + TEST_METHOD(BasicPipeline_ComponentCreation_ShouldWork) + { + // Arrange & Act + auto fileReader = std::make_unique(); + auto decoder = VideoDecoderFactory::CreateDecoder( + VideoCodecType::AV1, + VideoDecoderFactory::DecoderType::SOFTWARE + ); + + // Assert + Assert::IsNotNull(fileReader.get()); + Assert::IsNotNull(decoder.get()); + } + + TEST_METHOD(VideoFrame_AllocationAndDeallocation_ShouldNotLeak) + { + // Memory leak test + for (int i = 0; i < 100; i++) { + VideoFrame frame; + bool result = frame.AllocateYUV420P(1920, 1080); + Assert::IsTrue(result); + } + // Frames should automatically deallocate + } +}; +``` + +--- + +## 📁 **최종 테스트 프로젝트 구조** + +### **목표 구조 (Phase 3 완료 후)** +``` +D:\Project\video-av1\vav2\Vav2Player\Vav2Player\ +├── Vav2UnitTest.vcxproj +├── unit-test/ +│ ├── pch.h / pch.cpp # ✅ 기존 +│ ├── VideoTypesTest.cpp # ✅ 기존 +│ ├── WebMFileReaderTest.cpp # 🆕 Phase 1 +│ ├── AV1DecoderTest.cpp # 🆕 Phase 1 +│ ├── VideoDecoderFactoryTest.cpp # 🆕 Phase 1 +│ ├── SimpleGPURendererTest.cpp # 🆕 Phase 2 +│ └── IntegrationTest.cpp # 🆕 Phase 3 +└── x64/Debug/UnitTest/ +``` + +### **프로젝트 파일 업데이트** +```xml + + + + + + +``` + +--- + +## ⚙️ **구현 상세** + +### **1. Mock 없는 실제 클래스 테스트** +- **접근법**: 구체 클래스를 직접 테스트, Mock 복잡성 제거 +- **외부 의존성**: 실패 케이스 테스트로 격리 (존재하지 않는 파일 등) +- **GPU 의존성**: 실패해도 크래시하지 않는지 검증 + +### **2. 테스트 데이터 최소화** +- **파일 의존성**: 실제 파일 대신 존재하지 않는 파일명으로 에러 케이스 테스트 +- **메모리 테스트**: 동적 할당/해제 반복으로 메모리 누수 검증 +- **단순한 입력**: 기본값과 경계값을 활용한 테스트 + +### **3. CI/CD 통합** +```bash +# 자동 테스트 스크립트 +cd "D:\Project\video-av1\vav2\Vav2Player\Vav2Player" + +# 1. 유닛 테스트 빌드 +MSBuild Vav2UnitTest.vcxproj //p:Configuration=Debug //p:Platform=x64 //v:minimal + +# 2. 테스트 실행 +vstest.console.exe "x64\Debug\UnitTest\Vav2UnitTest.dll" +``` + +--- + +## 🤔 **의사결정 가이드** + +### **Option A vs Option B 선택 기준** + +#### **Option A 선택 (인터페이스 + Mock)** +**다음 조건 중 하나라도 해당하면 권장:** +- 의존성 주입 패턴을 사용하고 싶은 경우 +- 복잡한 시나리오 테스트가 필요한 경우 +- 외부 의존성 격리가 중요한 경우 +- 장기적인 테스트 확장성이 필요한 경우 + +**예상 시간**: Phase 0 (6시간) + Phase 1-3 (4일) = **5일** + +#### **Option B 선택 (직접 테스트)** +**다음 조건에 해당하면 권장:** +- 빠른 테스트 구현이 우선인 경우 +- 기존 코드 수정을 최소화하려는 경우 +- 단순한 기능 검증만 필요한 경우 +- 리소스(시간/인력)가 제한적인 경우 + +**예상 시간**: Phase 1-3 (4일) = **4일** + +### **권장사항** +``` +현재 상황 고려 시 🎯 Option A 권장 + +이유: +1. WebMFileReader는 파일 I/O 의존성이 높음 +2. GPU 렌더링은 환경 의존성이 높음 +3. 장기적인 유지보수성 필요 +4. MAJOR_REFACTORING_GUIDE 완료로 안정적인 기반 확보됨 +``` + +## 🕐 **실행 스케줄** + +### **Option A 선택 시 (권장)** +- **Phase 0**: 1일 (인터페이스 리팩토링) +- **Phase 1.5**: 1일 (Mock 시스템) +- **Phase 1**: 2일 (핵심 컴포넌트 테스트) +- **Phase 2**: 1일 (GPU 렌더링 테스트) +- **Phase 3**: 1일 (통합 테스트) + +**총 6일** + +### **Option B 선택 시** +- **Phase 1**: 2-3일 (핵심 컴포넌트 테스트) +- **Phase 2**: 1일 (GPU 렌더링 테스트) +- **Phase 3**: 1일 (통합 테스트) + +**총 4-5일** + +### **우선순위** +1. **의사결정**: Option A vs B 선택 +2. **WebMFileReaderTest.cpp** (가장 핵심적) +3. **AV1DecoderTest.cpp** (디코딩 로직) +4. **VideoDecoderFactoryTest.cpp** (팩토리 패턴) +5. **SimpleGPURendererTest.cpp** (GPU 안정성) +6. **IntegrationTest.cpp** (전체 검증) + +--- + +## 🎯 **성공 지표** + +### **정량적 목표** +- **테스트 커버리지**: 핵심 public 메서드 80% 이상 +- **테스트 실행 시간**: 30초 이내 +- **테스트 파일 수**: 6개 (기존 1개 + 신규 5개) +- **빌드 성공률**: 100% + +### **정성적 목표** +- **크래시 방지**: 모든 테스트가 크래시 없이 완료 +- **회귀 방지**: 기존 기능 변경 시 테스트로 검증 가능 +- **유지보수성**: 새로운 컴포넌트 추가 시 테스트 패턴 재사용 가능 + +--- + +## ⚠️ **주의사항** + +### **1. 환경 의존성 최소화** +- **D3D12**: GPU 없는 환경에서도 실패하지 않도록 처리 +- **dav1d**: 라이브러리 로드 실패 시에도 테스트 완료 +- **MediaFoundation**: 지원되지 않는 환경에서 graceful failure + +### **2. 테스트 격리** +- **파일 시스템**: 실제 파일에 의존하지 않는 테스트 우선 +- **전역 상태**: 테스트 간 상호 영향 최소화 +- **메모리**: 각 테스트 후 리소스 정리 + +### **3. 실용적 접근** +- **완벽한 Coverage보다 핵심 기능 안정성** +- **복잡한 Mock보다 간단한 실제 테스트** +- **외부 의존성 Mock보다 에러 케이스 활용** + +--- + +**🎯 핵심 메시지: "단순하고 실용적인 테스트로 안정성 확보"** + +*문서 작성일: 2025-09-23* +*이전 문서 대체: UNIT_TEST_REFACTORING_PLAN.md (구식)* \ No newline at end of file diff --git a/vav2/Vav2Player/Vav2Player/Vav2UnitTest.vcxproj b/vav2/Vav2Player/Vav2Player/Vav2UnitTest.vcxproj index 98854c5..0675daa 100644 --- a/vav2/Vav2Player/Vav2Player/Vav2UnitTest.vcxproj +++ b/vav2/Vav2Player/Vav2Player/Vav2UnitTest.vcxproj @@ -63,7 +63,7 @@ Level3 true $(ProjectDir)unit-test;$(ProjectDir)src;D:\Project\video-av1\include\libwebm;D:\Project\video-av1\include\dav1d;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) - WIN32;_DEBUG;%(PreprocessorDefinitions) + WIN32;_DEBUG;UNIT_TEST_BUILD;%(PreprocessorDefinitions) true $(IntDir)pch.pch stdcpp17 @@ -71,7 +71,7 @@ Windows D:\Project\video-av1\lib\libwebm;D:\Project\video-av1\lib\dav1d;$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) - webm.lib;dav1d.lib;mfplat.lib;mfuuid.lib;d3d12.lib;dxgi.lib;%(AdditionalDependencies) + webm-debug.lib;dav1d-debug.lib;mfplat.lib;mfuuid.lib;d3d12.lib;dxgi.lib;%(AdditionalDependencies) @@ -83,7 +83,7 @@ true true $(ProjectDir)unit-test;$(ProjectDir)src;D:\Project\video-av1\include\libwebm;D:\Project\video-av1\include\dav1d;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) - WIN32;NDEBUG;%(PreprocessorDefinitions) + WIN32;NDEBUG;UNIT_TEST_BUILD;%(PreprocessorDefinitions) true $(IntDir)pch.pch stdcpp17 @@ -98,6 +98,8 @@ + + @@ -105,20 +107,30 @@ Create + + + + + + - + - - - - - - - - + + + + + + + + + + + + + - diff --git a/vav2/Vav2Player/Vav2Player/VideoPlayerControl.xaml.cpp b/vav2/Vav2Player/Vav2Player/VideoPlayerControl.xaml.cpp index bce4a96..8e3fc41 100644 --- a/vav2/Vav2Player/Vav2Player/VideoPlayerControl.xaml.cpp +++ b/vav2/Vav2Player/Vav2Player/VideoPlayerControl.xaml.cpp @@ -588,7 +588,7 @@ namespace winrt::Vav2Player::implementation return; } - HRESULT hr = m_gpuRenderer->Initialize(VideoSwapChainPanel(), containerWidth, containerHeight); + HRESULT hr = m_gpuRenderer->InitializeWithSwapChain(VideoSwapChainPanel(), containerWidth, containerHeight); if (!SUCCEEDED(hr)) { // GPU initialization failed - temporarily use CPU rendering diff --git a/vav2/Vav2Player/Vav2Player/VideoPlayerControl.xaml.h b/vav2/Vav2Player/Vav2Player/VideoPlayerControl.xaml.h index 6d8008f..58cfcaa 100644 --- a/vav2/Vav2Player/Vav2Player/VideoPlayerControl.xaml.h +++ b/vav2/Vav2Player/Vav2Player/VideoPlayerControl.xaml.h @@ -1,9 +1,11 @@ #pragma once #include "VideoPlayerControl.g.h" +#include "src/FileIO/IWebMFileReader.h" #include "src/FileIO/WebMFileReader.h" #include "src/Decoder/VideoDecoderFactory.h" #include "src/Common/VideoTypes.h" +#include "src/Rendering/IVideoRenderer.h" #include "src/Rendering/SimpleGPURenderer.h" #include #include @@ -58,13 +60,13 @@ namespace winrt::Vav2Player::implementation private: // Video processing components - std::unique_ptr m_fileReader; + std::unique_ptr m_fileReader; std::unique_ptr m_decoder; // Video rendering components winrt::Microsoft::UI::Xaml::Media::Imaging::WriteableBitmap m_renderBitmap{ nullptr }; std::vector m_bgraBuffer; - std::unique_ptr m_gpuRenderer; + std::unique_ptr m_gpuRenderer; bool m_useHardwareRendering = true; // Default to GPU pipeline // Playback timer for continuous frame processing winrt::Microsoft::UI::Xaml::DispatcherTimer m_playbackTimer; diff --git a/vav2/Vav2Player/Vav2Player/headless/SimpleHeadlessMain.cpp b/vav2/Vav2Player/Vav2Player/headless/SimpleHeadlessMain.cpp index ebb9a6e..487ceef 100644 --- a/vav2/Vav2Player/Vav2Player/headless/SimpleHeadlessMain.cpp +++ b/vav2/Vav2Player/Vav2Player/headless/SimpleHeadlessMain.cpp @@ -45,11 +45,14 @@ int main(int argc, char* argv[]) auto metadata = fileReader->GetVideoMetadata(); std::cout << "Video: " << metadata.width << "x" << metadata.height << " @ " << metadata.frame_rate << " fps" << std::endl; + std::cout << "Codec: " << (metadata.codec_type == VideoCodecType::AV1 ? "AV1" : + metadata.codec_type == VideoCodecType::VP9 ? "VP9" : "Other") << std::endl; - // Test decoder creation - auto decoder = Vav2Player::VideoDecoderFactory::CreateDecoder(Vav2Player::VideoCodecType::AV1, Vav2Player::VideoDecoderFactory::DecoderType::AUTO); + // Test decoder creation using detected codec type + auto decoder = Vav2Player::VideoDecoderFactory::CreateDecoder(metadata.codec_type, Vav2Player::VideoDecoderFactory::DecoderType::AUTO); if (!decoder) { - std::cout << "Failed to create AV1 decoder" << std::endl; + std::cout << "Failed to create " << (metadata.codec_type == VideoCodecType::AV1 ? "AV1" : + metadata.codec_type == VideoCodecType::VP9 ? "VP9" : "Other") << " decoder" << std::endl; return 1; } diff --git a/vav2/Vav2Player/Vav2Player/src/Common/VideoTypes.h b/vav2/Vav2Player/Vav2Player/src/Common/VideoTypes.h index 3a0015c..b6a725e 100644 --- a/vav2/Vav2Player/Vav2Player/src/Common/VideoTypes.h +++ b/vav2/Vav2Player/Vav2Player/src/Common/VideoTypes.h @@ -20,7 +20,20 @@ enum class ColorSpace { YUV422P, YUV444P, RGB24, - RGB32 + RGB32, + BT709, + BT2020 +}; + +// 픽셀 포맷 정의 +enum class PixelFormat { + YUV420P, + YUV422P, + YUV444P, + NV12, + RGB24, + RGB32, + BGRA32 }; // 비디오 메타데이터 @@ -56,8 +69,10 @@ struct VideoFrame { // 프레임 메타데이터 uint64_t frame_index = 0; double timestamp_seconds = 0.0; + uint64_t timestamp_ns = 0; // For compatibility uint32_t width = 0; uint32_t height = 0; + PixelFormat format = PixelFormat::YUV420P; // Pixel format ColorSpace color_space = ColorSpace::YUV420P; // YUV 데이터 (각 플레인별) diff --git a/vav2/Vav2Player/Vav2Player/src/FileIO/IWebMFileReader.h b/vav2/Vav2Player/Vav2Player/src/FileIO/IWebMFileReader.h new file mode 100644 index 0000000..bdbf665 --- /dev/null +++ b/vav2/Vav2Player/Vav2Player/src/FileIO/IWebMFileReader.h @@ -0,0 +1,77 @@ +#pragma once +#include "../Common/VideoTypes.h" +#include +#include + +namespace Vav2Player { + +// Forward declaration for VideoTrackInfo +struct VideoTrackInfo { + uint64_t track_number; + VideoCodecType codec_type; + std::string codec_id; + std::string codec_name; + uint32_t width; + uint32_t height; + double frame_rate; + uint64_t frame_count; + bool is_default; + bool is_enabled = true; // For mock compatibility +}; + +// Error codes for WebM file operations +enum class WebMErrorCode { + Success, + FileNotFound, + InvalidFormat, + UnsupportedCodec, + NoVideoTrack, + ReadError, + SeekError, + FileNotOpen, + InvalidTrack, + SeekFailed, + Unknown +}; + +// Interface for WebM/MKV file readers +// Provides common abstraction for parsing WebM containers and extracting AV1 video streams +class IWebMFileReader { +public: + virtual ~IWebMFileReader() = default; + + // File operations + virtual bool OpenFile(const std::string& file_path) = 0; + virtual void CloseFile() = 0; + virtual bool IsFileOpen() const = 0; + + // File and stream information + virtual const VideoMetadata& GetVideoMetadata() const = 0; + virtual std::string GetFilePath() const = 0; + + // Video track management + virtual std::vector GetVideoTracks() const = 0; + virtual bool SelectVideoTrack(uint64_t track_number) = 0; + virtual uint64_t GetSelectedTrackNumber() const = 0; + + // Packet reading + virtual bool ReadNextPacket(VideoPacket& packet) = 0; + virtual bool SeekToFrame(uint64_t frame_index) = 0; + virtual bool SeekToTime(double timestamp_seconds) = 0; + + // Position information + virtual uint64_t GetCurrentFrameIndex() const = 0; + virtual double GetCurrentTimestamp() const = 0; + virtual bool IsEndOfFile() const = 0; + + // File navigation and statistics + virtual bool Reset() = 0; + virtual uint64_t GetTotalFrames() const = 0; + virtual double GetDuration() const = 0; + + // Error handling + virtual WebMErrorCode GetLastError() const = 0; + virtual std::string GetLastErrorString() const = 0; +}; + +} // namespace Vav2Player \ No newline at end of file diff --git a/vav2/Vav2Player/Vav2Player/src/FileIO/WebMFileReader.cpp b/vav2/Vav2Player/Vav2Player/src/FileIO/WebMFileReader.cpp index 0a6e8cb..16871d6 100644 --- a/vav2/Vav2Player/Vav2Player/src/FileIO/WebMFileReader.cpp +++ b/vav2/Vav2Player/Vav2Player/src/FileIO/WebMFileReader.cpp @@ -97,7 +97,7 @@ struct WebMFileReader::InternalState { bool end_of_file = false; // Error handling - ErrorCode last_error = ErrorCode::Success; + WebMErrorCode last_error = WebMErrorCode::Success; std::string last_error_message; InternalState() : reader(std::make_unique()) {} @@ -114,13 +114,13 @@ bool WebMFileReader::OpenFile(const std::string& file_path) { CloseFile(); if (file_path.empty()) { - SetLastError(ErrorCode::FileNotFound, "File path is empty"); + SetLastError(WebMErrorCode::FileNotFound, "File path is empty"); return false; } // Open file if (!m_state->reader->Open(file_path)) { - SetLastError(ErrorCode::FileNotFound, "Cannot open file: " + file_path); + SetLastError(WebMErrorCode::FileNotFound, "Cannot open file: " + file_path); return false; } @@ -135,7 +135,7 @@ bool WebMFileReader::OpenFile(const std::string& file_path) { // Extract video tracks if (!ExtractVideoTracks()) { CloseFile(); - SetLastError(ErrorCode::NoVideoTrack, "No supported video tracks found"); + SetLastError(WebMErrorCode::NoVideoTrack, "No supported video tracks found"); return false; } @@ -149,7 +149,7 @@ bool WebMFileReader::OpenFile(const std::string& file_path) { if (m_state->selected_track_number == 0) { CloseFile(); - SetLastError(ErrorCode::UnsupportedCodec, "No supported video codecs found"); + SetLastError(WebMErrorCode::UnsupportedCodec, "No supported video codecs found"); return false; } @@ -162,7 +162,7 @@ bool WebMFileReader::OpenFile(const std::string& file_path) { // Initialize reading position Reset(); - SetLastError(ErrorCode::Success); + SetLastError(WebMErrorCode::Success); return true; } @@ -193,13 +193,13 @@ std::string WebMFileReader::GetFilePath() const { return m_state ? m_state->file_path : ""; } -std::vector WebMFileReader::GetVideoTracks() const { +std::vector WebMFileReader::GetVideoTracks() const { return m_state ? m_state->video_tracks : std::vector(); } bool WebMFileReader::SelectVideoTrack(uint64_t track_number) { if (!IsFileOpen()) { - SetLastError(ErrorCode::ReadError, "File not open"); + SetLastError(WebMErrorCode::ReadError, "File not open"); return false; } @@ -210,14 +210,14 @@ bool WebMFileReader::SelectVideoTrack(uint64_t track_number) { }); if (it == m_state->video_tracks.end()) { - SetLastError(ErrorCode::ReadError, "Track not found: " + std::to_string(track_number)); + SetLastError(WebMErrorCode::ReadError, "Track not found: " + std::to_string(track_number)); return false; } m_state->selected_track_number = track_number; Reset(); // Initialize reading position - SetLastError(ErrorCode::Success); + SetLastError(WebMErrorCode::Success); return true; } @@ -230,7 +230,7 @@ bool WebMFileReader::ReadNextPacket(VideoPacket& packet) { // Enhanced null safety check inside mutex if (!m_state) { - SetLastError(ErrorCode::ReadError, "WebMFileReader state is null"); + SetLastError(WebMErrorCode::ReadError, "WebMFileReader state is null"); return false; } @@ -240,7 +240,7 @@ bool WebMFileReader::ReadNextPacket(VideoPacket& packet) { // Fail if no track selected if (m_state->selected_track_number == 0) { - SetLastError(ErrorCode::ReadError, "No video track selected"); + SetLastError(WebMErrorCode::ReadError, "No video track selected"); return false; } @@ -252,13 +252,13 @@ bool WebMFileReader::ReadNextPacket(VideoPacket& packet) { // Read packet from current block if (!m_state->current_block_entry || !m_state->current_block_entry->GetBlock()) { - SetLastError(ErrorCode::ReadError, "Invalid block entry"); + SetLastError(WebMErrorCode::ReadError, "Invalid block entry"); return false; } const mkvparser::Block* block = m_state->current_block_entry->GetBlock(); if (!ReadPacketFromBlock(block, packet)) { - SetLastError(ErrorCode::ReadError, "Failed to read packet from block"); + SetLastError(WebMErrorCode::ReadError, "Failed to read packet from block"); return false; } @@ -281,20 +281,20 @@ bool WebMFileReader::ReadNextPacket(VideoPacket& packet) { packet.frame_index = m_state->current_frame_index - 1; packet.is_keyframe = block->IsKey(); - SetLastError(ErrorCode::Success); + SetLastError(WebMErrorCode::Success); return true; } bool WebMFileReader::SeekToFrame(uint64_t frame_index) { if (!IsFileOpen()) { - SetLastError(ErrorCode::SeekError, "File not open"); + SetLastError(WebMErrorCode::SeekError, "File not open"); return false; } std::lock_guard lock(m_access_mutex); if (m_state->selected_track_number == 0) { - SetLastError(ErrorCode::SeekError, "No video track selected"); + SetLastError(WebMErrorCode::SeekError, "No video track selected"); return false; } @@ -319,31 +319,31 @@ bool WebMFileReader::SeekToFrame(uint64_t frame_index) { while (m_state->current_frame_index < frame_index && !m_state->end_of_file) { if (!AdvanceToNextFrame()) { m_state->end_of_file = true; - SetLastError(ErrorCode::SeekError, "Cannot reach target frame"); + SetLastError(WebMErrorCode::SeekError, "Cannot reach target frame"); return false; } m_state->current_frame_index++; } - SetLastError(ErrorCode::Success); + SetLastError(WebMErrorCode::Success); return m_state->current_frame_index == frame_index; } bool WebMFileReader::SeekToTime(double timestamp_seconds) { if (!IsFileOpen()) { - SetLastError(ErrorCode::SeekError, "File not open"); + SetLastError(WebMErrorCode::SeekError, "File not open"); return false; } std::lock_guard lock(m_access_mutex); if (m_state->selected_track_number == 0) { - SetLastError(ErrorCode::SeekError, "No video track selected"); + SetLastError(WebMErrorCode::SeekError, "No video track selected"); return false; } if (timestamp_seconds < 0.0) { - SetLastError(ErrorCode::SeekError, "Invalid timestamp"); + SetLastError(WebMErrorCode::SeekError, "Invalid timestamp"); return false; } @@ -355,13 +355,13 @@ bool WebMFileReader::SeekToTime(double timestamp_seconds) { // 세그먼트 정보에서 타임코드 스케일 가져오기 const mkvparser::SegmentInfo* info = m_state->segment->GetInfo(); if (!info) { - SetLastError(ErrorCode::SeekError, "No segment info available"); + SetLastError(WebMErrorCode::SeekError, "No segment info available"); return false; } long long timecode_scale = info->GetTimeCodeScale(); if (timecode_scale <= 0) { - SetLastError(ErrorCode::SeekError, "Invalid timecode scale"); + SetLastError(WebMErrorCode::SeekError, "Invalid timecode scale"); return false; } @@ -371,7 +371,7 @@ bool WebMFileReader::SeekToTime(double timestamp_seconds) { // 타겟 시간에 가장 가까운 클러스터 찾기 const mkvparser::Cluster* target_cluster = FindClusterByTime(timestamp_seconds); if (!target_cluster) { - SetLastError(ErrorCode::SeekError, "Cannot find target cluster"); + SetLastError(WebMErrorCode::SeekError, "Cannot find target cluster"); return false; } @@ -397,7 +397,7 @@ bool WebMFileReader::SeekToTime(double timestamp_seconds) { if (current_time >= timestamp_seconds) { m_state->current_timestamp = current_time; - SetLastError(ErrorCode::Success); + SetLastError(WebMErrorCode::Success); return true; } } @@ -405,7 +405,7 @@ bool WebMFileReader::SeekToTime(double timestamp_seconds) { m_state->current_frame_index++; } - SetLastError(ErrorCode::SeekError, "Cannot reach target time"); + SetLastError(WebMErrorCode::SeekError, "Cannot reach target time"); return false; } @@ -429,7 +429,7 @@ bool WebMFileReader::Reset() { // 첫 번째 클러스터로 이동 const mkvparser::Cluster* cluster = m_state->segment->GetFirst(); if (!cluster) { - SetLastError(ErrorCode::ReadError, "No clusters found"); + SetLastError(WebMErrorCode::ReadError, "No clusters found"); return false; } @@ -450,8 +450,8 @@ double WebMFileReader::GetDuration() const { return m_state ? m_state->metadata.duration_seconds : 0.0; } -WebMFileReader::ErrorCode WebMFileReader::GetLastError() const { - return m_state ? m_state->last_error : ErrorCode::Unknown; +WebMErrorCode WebMFileReader::GetLastError() const { + return m_state ? m_state->last_error : WebMErrorCode::Unknown; } std::string WebMFileReader::GetLastErrorString() const { @@ -500,7 +500,7 @@ bool WebMFileReader::InitializeParser() { detailed_error += hex_dump; } - SetLastError(ErrorCode::InvalidFormat, detailed_error); + SetLastError(WebMErrorCode::InvalidFormat, detailed_error); return false; } @@ -513,7 +513,7 @@ bool WebMFileReader::InitializeParser() { error_msg += ". EBML header details: version=" + std::to_string(ebml_header.m_version) + ", docTypeVersion=" + std::to_string(ebml_header.m_docTypeVersion); - SetLastError(ErrorCode::InvalidFormat, error_msg); + SetLastError(WebMErrorCode::InvalidFormat, error_msg); return false; } @@ -523,21 +523,21 @@ bool WebMFileReader::InitializeParser() { m_state->reader.get(), pos, segment_ptr); if (create_status < 0 || !segment_ptr) { - SetLastError(ErrorCode::InvalidFormat, "Cannot create segment"); + SetLastError(WebMErrorCode::InvalidFormat, "Cannot create segment"); return false; } m_state->segment.reset(segment_ptr); if (!m_state->segment) { - SetLastError(ErrorCode::InvalidFormat, "Cannot create segment"); + SetLastError(WebMErrorCode::InvalidFormat, "Cannot create segment"); return false; } // Segment 정보 로드 long load_status = m_state->segment->Load(); if (load_status < 0) { - SetLastError(ErrorCode::InvalidFormat, "Cannot load segment"); + SetLastError(WebMErrorCode::InvalidFormat, "Cannot load segment"); return false; } @@ -574,7 +574,7 @@ bool WebMFileReader::ExtractVideoTracks() { // 디버깅: 실제 코덱 ID를 파일에 출력 std::string debug_msg = "Found video track #" + std::to_string(info.track_number) + " with codec_id: '" + info.codec_id + "'"; - SetLastError(ErrorCode::Success, debug_msg); // 임시로 이 메시지를 상태에 표시 + SetLastError(WebMErrorCode::Success, debug_msg); // 임시로 이 메시지를 상태에 표시 // 파일에 코덱 ID 기록 std::ofstream codec_debug("codec_debug.log", std::ios::app); @@ -667,22 +667,22 @@ double WebMFileReader::CalculateFrameRate(const mkvparser::VideoTrack* video_tra return 30.0; // 임시 기본값 } -void WebMFileReader::SetLastError(ErrorCode error, const std::string& message) { +void WebMFileReader::SetLastError(WebMErrorCode error, const std::string& message) { if (m_state) { m_state->last_error = error; m_state->last_error_message = message; } } -std::string WebMFileReader::ErrorCodeToString(ErrorCode error) const { +std::string WebMFileReader::ErrorCodeToString(WebMErrorCode error) const { switch (error) { - case ErrorCode::Success: return "Success"; - case ErrorCode::FileNotFound: return "File not found"; - case ErrorCode::InvalidFormat: return "Invalid format"; - case ErrorCode::UnsupportedCodec: return "Unsupported codec"; - case ErrorCode::NoVideoTrack: return "No video track"; - case ErrorCode::ReadError: return "Read error"; - case ErrorCode::SeekError: return "Seek error"; + case WebMErrorCode::Success: return "Success"; + case WebMErrorCode::FileNotFound: return "File not found"; + case WebMErrorCode::InvalidFormat: return "Invalid format"; + case WebMErrorCode::UnsupportedCodec: return "Unsupported codec"; + case WebMErrorCode::NoVideoTrack: return "No video track"; + case WebMErrorCode::ReadError: return "Read error"; + case WebMErrorCode::SeekError: return "Seek error"; default: return "Unknown error"; } } diff --git a/vav2/Vav2Player/Vav2Player/src/FileIO/WebMFileReader.h b/vav2/Vav2Player/Vav2Player/src/FileIO/WebMFileReader.h index 6ccecff..41dcb6b 100644 --- a/vav2/Vav2Player/Vav2Player/src/FileIO/WebMFileReader.h +++ b/vav2/Vav2Player/Vav2Player/src/FileIO/WebMFileReader.h @@ -1,5 +1,6 @@ #pragma once #include "../Common/VideoTypes.h" +#include "IWebMFileReader.h" #include #include #include @@ -10,7 +11,7 @@ namespace Vav2Player { // WebM/MKV 파일을 파싱하여 AV1 비디오 스트림을 추출하는 클래스 // libwebm의 mkvparser를 사용하여 구현 -class WebMFileReader { +class WebMFileReader : public IWebMFileReader { public: WebMFileReader(); ~WebMFileReader(); @@ -19,61 +20,38 @@ public: WebMFileReader(const WebMFileReader&) = delete; WebMFileReader& operator=(const WebMFileReader&) = delete; - // 파일 열기/닫기 - bool OpenFile(const std::string& file_path); - void CloseFile(); - bool IsFileOpen() const; + // 파일 열기/닫기 (interface implementation) + bool OpenFile(const std::string& file_path) override; + void CloseFile() override; + bool IsFileOpen() const override; - // 파일 및 스트림 정보 - const VideoMetadata& GetVideoMetadata() const; - std::string GetFilePath() const; + // 파일 및 스트림 정보 (interface implementation) + const VideoMetadata& GetVideoMetadata() const override; + std::string GetFilePath() const override; - // 지원되는 비디오 트랙들 (AV1, VP9 등) - struct VideoTrackInfo { - uint64_t track_number; - VideoCodecType codec_type; - std::string codec_id; - std::string codec_name; - uint32_t width; - uint32_t height; - double frame_rate; - uint64_t frame_count; - bool is_default; - }; + // 비디오 트랙 관리 (interface implementation) + std::vector GetVideoTracks() const override; + bool SelectVideoTrack(uint64_t track_number) override; + uint64_t GetSelectedTrackNumber() const override; - std::vector GetVideoTracks() const; - bool SelectVideoTrack(uint64_t track_number); - uint64_t GetSelectedTrackNumber() const; + // 패킷 읽기 (interface implementation) + bool ReadNextPacket(VideoPacket& packet) override; + bool SeekToFrame(uint64_t frame_index) override; + bool SeekToTime(double timestamp_seconds) override; - // 프레임별 패킷 읽기 - bool ReadNextPacket(VideoPacket& packet); - bool SeekToFrame(uint64_t frame_index); - bool SeekToTime(double timestamp_seconds); + // 위치 정보 (interface implementation) + uint64_t GetCurrentFrameIndex() const override; + double GetCurrentTimestamp() const override; + bool IsEndOfFile() const override; - // 현재 위치 정보 - uint64_t GetCurrentFrameIndex() const; - double GetCurrentTimestamp() const; - bool IsEndOfFile() const; + // 파일 탐색 및 통계 (interface implementation) + bool Reset() override; + uint64_t GetTotalFrames() const override; + double GetDuration() const override; - // 파일 탐색 및 통계 - bool Reset(); // 처음으로 되돌아가기 - uint64_t GetTotalFrames() const; - double GetDuration() const; - - // 에러 처리 - enum class ErrorCode { - Success, - FileNotFound, - InvalidFormat, - UnsupportedCodec, - NoVideoTrack, - ReadError, - SeekError, - Unknown - }; - - ErrorCode GetLastError() const; - std::string GetLastErrorString() const; + // 에러 처리 (interface implementation) + WebMErrorCode GetLastError() const override; + std::string GetLastErrorString() const override; // libwebm 관련 정보 static std::string GetLibWebMVersion(); @@ -112,8 +90,8 @@ private: const mkvparser::Block* FindBlockByFrame(uint64_t frame_index); // 에러 처리 - void SetLastError(ErrorCode error, const std::string& message = ""); - std::string ErrorCodeToString(ErrorCode error) const; + void SetLastError(WebMErrorCode error, const std::string& message = ""); + std::string ErrorCodeToString(WebMErrorCode error) const; // 유틸리티 함수들 static bool IsVideoCodecSupported(const std::string& codec_id); diff --git a/vav2/Vav2Player/Vav2Player/src/Rendering/IVideoRenderer.h b/vav2/Vav2Player/Vav2Player/src/Rendering/IVideoRenderer.h new file mode 100644 index 0000000..ebc0f70 --- /dev/null +++ b/vav2/Vav2Player/Vav2Player/src/Rendering/IVideoRenderer.h @@ -0,0 +1,45 @@ +#pragma once +#include "../Common/VideoTypes.h" +#include + +// Conditional WinUI includes for unit testing compatibility +#ifndef UNIT_TEST_BUILD +#include +#endif + +namespace Vav2Player { + +// Interface for video renderers +// Provides common abstraction for rendering video frames to display +class IVideoRenderer { +public: + virtual ~IVideoRenderer() = default; + + // Core lifecycle + virtual HRESULT Initialize(uint32_t width, uint32_t height) = 0; + virtual void Shutdown() = 0; + virtual bool IsInitialized() const = 0; + + // Video rendering + virtual HRESULT RenderVideoFrame(const VideoFrame& frame) = 0; + virtual bool TryRenderFrame(const VideoFrame& frame) = 0; // Returns true if successful + virtual HRESULT Present() = 0; + + // Size management + virtual HRESULT Resize(uint32_t width, uint32_t height) = 0; + virtual uint32_t GetWidth() const = 0; + virtual uint32_t GetHeight() const = 0; + + // Additional configuration (optional for implementations) + virtual void SetAspectFitMode(bool enabled) {} + virtual void UpdateContainerSize(uint32_t container_width, uint32_t container_height) {} + + // WinUI-specific methods (optional implementation) +#ifndef UNIT_TEST_BUILD + virtual HRESULT InitializeWithSwapChain(winrt::Microsoft::UI::Xaml::Controls::SwapChainPanel const& panel, + uint32_t width, uint32_t height) { return E_NOTIMPL; } + virtual void SetSwapChainPanel(winrt::Microsoft::UI::Xaml::Controls::SwapChainPanel const& panel) { } +#endif +}; + +} // namespace Vav2Player \ No newline at end of file diff --git a/vav2/Vav2Player/Vav2Player/src/Rendering/SimpleGPURenderer.cpp b/vav2/Vav2Player/Vav2Player/src/Rendering/SimpleGPURenderer.cpp index 5b7b2b7..4b54bf2 100644 --- a/vav2/Vav2Player/Vav2Player/src/Rendering/SimpleGPURenderer.cpp +++ b/vav2/Vav2Player/Vav2Player/src/Rendering/SimpleGPURenderer.cpp @@ -31,7 +31,33 @@ SimpleGPURenderer::~SimpleGPURenderer() } } -HRESULT SimpleGPURenderer::Initialize(winrt::Microsoft::UI::Xaml::Controls::SwapChainPanel const& panel, +// Interface implementation - for headless/testing scenarios +HRESULT SimpleGPURenderer::Initialize(uint32_t width, uint32_t height) { + // Store dimensions + m_width = width; + m_height = height; + + // Try to initialize D3D12 without SwapChain (for testing) + HRESULT hr = CreateDevice(); + if (FAILED(hr)) { + return hr; + } + + hr = CreateCommandQueue(); + if (FAILED(hr)) { + return hr; + } + + // Mark as initialized (SwapChain can be set later) + m_initialized = true; + return S_OK; +} + +void SimpleGPURenderer::SetSwapChainPanel(winrt::Microsoft::UI::Xaml::Controls::SwapChainPanel const& panel) { + m_swapChainPanel = panel; +} + +HRESULT SimpleGPURenderer::InitializeWithSwapChain(winrt::Microsoft::UI::Xaml::Controls::SwapChainPanel const& panel, uint32_t width, uint32_t height) { if (m_initialized) @@ -1499,4 +1525,21 @@ HRESULT SimpleGPURenderer::RenderWithAspectFitInternal() return S_OK; } +HRESULT SimpleGPURenderer::Resize(uint32_t width, uint32_t height) +{ + if (!m_initialized) + return E_FAIL; + + m_width = width; + m_height = height; + + // Recreate render targets if swap chain exists + if (m_swapChain) + { + return CreateRenderTargets(); + } + + return S_OK; +} + } // namespace Vav2Player \ No newline at end of file diff --git a/vav2/Vav2Player/Vav2Player/src/Rendering/SimpleGPURenderer.h b/vav2/Vav2Player/Vav2Player/src/Rendering/SimpleGPURenderer.h index ace21fc..41e0e51 100644 --- a/vav2/Vav2Player/Vav2Player/src/Rendering/SimpleGPURenderer.h +++ b/vav2/Vav2Player/Vav2Player/src/Rendering/SimpleGPURenderer.h @@ -8,6 +8,7 @@ #include #include #include "../Common/VideoTypes.h" +#include "IVideoRenderer.h" using Microsoft::WRL::ComPtr; @@ -27,27 +28,29 @@ struct AspectFitConstants // Simple, clean GPU renderer for AV1 video playback // Phase 3: Designed from scratch with proper architecture -class SimpleGPURenderer +class SimpleGPURenderer : public IVideoRenderer { public: SimpleGPURenderer(); ~SimpleGPURenderer(); - // Core lifecycle - HRESULT Initialize(winrt::Microsoft::UI::Xaml::Controls::SwapChainPanel const& panel, - uint32_t width, uint32_t height); - void Shutdown(); - bool IsInitialized() const { return m_initialized; } + // IVideoRenderer interface implementation + HRESULT Initialize(uint32_t width, uint32_t height) override; + void Shutdown() override; + bool IsInitialized() const override { return m_initialized; } - // Video rendering - HRESULT RenderVideoFrame(const VideoFrame& frame); - bool TryRenderFrame(const VideoFrame& frame); // Returns true if successful - HRESULT Present(); + HRESULT RenderVideoFrame(const VideoFrame& frame) override; + bool TryRenderFrame(const VideoFrame& frame) override; + HRESULT Present() override; - // Size management - HRESULT Resize(uint32_t width, uint32_t height); - uint32_t GetWidth() const { return m_width; } - uint32_t GetHeight() const { return m_height; } + HRESULT Resize(uint32_t width, uint32_t height) override; + uint32_t GetWidth() const override { return m_width; } + uint32_t GetHeight() const override { return m_height; } + + // SimpleGPURenderer specific methods + HRESULT InitializeWithSwapChain(winrt::Microsoft::UI::Xaml::Controls::SwapChainPanel const& panel, + uint32_t width, uint32_t height); + void SetSwapChainPanel(winrt::Microsoft::UI::Xaml::Controls::SwapChainPanel const& panel); private: // D3D12 core objects @@ -107,6 +110,9 @@ private: UINT m_srvUavDescriptorSize = 0; uint64_t m_totalFramesRendered = 0; + // WinUI integration + winrt::Microsoft::UI::Xaml::Controls::SwapChainPanel m_swapChainPanel{ nullptr }; + // Helper methods HRESULT CreateDevice(); HRESULT CreateCommandQueue(); diff --git a/vav2/Vav2Player/Vav2Player/unit-test/AV1DecoderTest.cpp b/vav2/Vav2Player/Vav2Player/unit-test/AV1DecoderTest.cpp new file mode 100644 index 0000000..ee32c29 --- /dev/null +++ b/vav2/Vav2Player/Vav2Player/unit-test/AV1DecoderTest.cpp @@ -0,0 +1,214 @@ +#include "pch.h" +#include "MockWebMFileReader.h" +#include "../src/Decoder/AV1Decoder.h" +#include "../src/Decoder/VideoDecoderFactory.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace Vav2Player; +using namespace Vav2PlayerUnitTests; + +namespace Vav2PlayerUnitTests +{ + TEST_CLASS(AV1DecoderTest) + { + public: + TEST_METHOD(AV1Decoder_Initialize_ValidMetadata_ShouldReturnTrue) + { + // Arrange + auto decoder = VideoDecoderFactory::CreateDecoder(VideoCodecType::AV1, VideoDecoderFactory::DecoderType::SOFTWARE); + + VideoMetadata metadata; + metadata.width = 1920; + metadata.height = 1080; + metadata.codec_type = VideoCodecType::AV1; + metadata.frame_rate = 30.0; + + // Act + bool result = decoder->Initialize(metadata); + + // Assert + Assert::IsTrue(result); + Assert::IsTrue(decoder->IsInitialized()); + } + + TEST_METHOD(AV1Decoder_Initialize_InvalidMetadata_ShouldReturnFalse) + { + // Arrange + auto decoder = VideoDecoderFactory::CreateDecoder(VideoCodecType::AV1, VideoDecoderFactory::DecoderType::SOFTWARE); + + VideoMetadata metadata; + metadata.width = 0; // Invalid width + metadata.height = 1080; + metadata.codec_type = VideoCodecType::AV1; + + // Act + bool result = decoder->Initialize(metadata); + + // Assert + Assert::IsFalse(result); + Assert::IsFalse(decoder->IsInitialized()); + } + + TEST_METHOD(VideoDecoderFactory_CreateAV1Decoder_ShouldReturnValidDecoder) + { + // Act + auto decoder = VideoDecoderFactory::CreateDecoder(VideoCodecType::AV1, VideoDecoderFactory::DecoderType::SOFTWARE); + + // Assert + Assert::IsNotNull(decoder.get()); + Assert::IsFalse(decoder->IsInitialized()); // Should not be initialized until Initialize() is called + } + + TEST_METHOD(VideoDecoderFactory_CreateAutoDecoder_ShouldReturnValidDecoder) + { + // Act + auto decoder = VideoDecoderFactory::CreateDecoder(VideoCodecType::AV1, VideoDecoderFactory::DecoderType::AUTO); + + // Assert + Assert::IsNotNull(decoder.get()); + Assert::IsFalse(decoder->IsInitialized()); + } + + TEST_METHOD(AV1Decoder_Reset_AfterInitialization_ShouldSucceed) + { + // Arrange + auto decoder = VideoDecoderFactory::CreateDecoder(VideoCodecType::AV1, VideoDecoderFactory::DecoderType::SOFTWARE); + + VideoMetadata metadata; + metadata.width = 1920; + metadata.height = 1080; + metadata.codec_type = VideoCodecType::AV1; + metadata.frame_rate = 30.0; + + decoder->Initialize(metadata); + + // Act + bool result = decoder->Reset(); + + // Assert + Assert::IsTrue(result); + Assert::IsTrue(decoder->IsInitialized()); // Should remain initialized after reset + } + + TEST_METHOD(AV1Decoder_Cleanup_ShouldSetNotInitialized) + { + // Arrange + auto decoder = VideoDecoderFactory::CreateDecoder(VideoCodecType::AV1, VideoDecoderFactory::DecoderType::SOFTWARE); + + VideoMetadata metadata; + metadata.width = 1920; + metadata.height = 1080; + metadata.codec_type = VideoCodecType::AV1; + metadata.frame_rate = 30.0; + + decoder->Initialize(metadata); + Assert::IsTrue(decoder->IsInitialized()); + + // Act + decoder->Cleanup(); + + // Assert + Assert::IsFalse(decoder->IsInitialized()); + } + + // Note: GetSupportedFormats and GetAvailableDecoders methods don't exist yet + // Commenting out until these methods are implemented + /* + TEST_METHOD(AV1Decoder_GetSupportedFormats_ShouldReturnAV1) + { + // Arrange + auto decoder = VideoDecoderFactory::CreateDecoder(VideoCodecType::AV1, VideoDecoderFactory::DecoderType::SOFTWARE); + + // Act + auto formats = decoder->GetSupportedFormats(); + + // Assert + Assert::IsTrue(formats.size() > 0); + + // Check if AV1 is in the supported formats + bool foundAV1 = false; + for (const auto& format : formats) { + if (format == VideoCodecType::AV1) { + foundAV1 = true; + break; + } + } + Assert::IsTrue(foundAV1); + } + + TEST_METHOD(VideoDecoderFactory_GetAvailableDecoders_ShouldIncludeAV1) + { + // Act + auto decoders = VideoDecoderFactory::GetAvailableDecoders(); + + // Assert + Assert::IsTrue(decoders.size() > 0); + + // Check if AV1 decoder info is present + bool foundAV1 = false; + for (const auto& decoderInfo : decoders) { + if (decoderInfo.codec_type == VideoCodecType::AV1) { + foundAV1 = true; + Assert::IsFalse(decoderInfo.name.empty()); + break; + } + } + Assert::IsTrue(foundAV1); + } + */ + + TEST_METHOD(AV1Decoder_HardwareDecoder_CreationTest) + { + // Act - Try to create hardware decoder (may fail on systems without hardware support) + auto decoder = VideoDecoderFactory::CreateDecoder(VideoCodecType::AV1, VideoDecoderFactory::DecoderType::HARDWARE_MF); + + // Assert - Should either succeed or return nullptr (graceful failure) + // This test mainly verifies that factory doesn't crash + if (decoder) { + // If hardware decoder is available, test basic functionality + VideoMetadata metadata; + metadata.width = 1920; + metadata.height = 1080; + metadata.codec_type = VideoCodecType::AV1; + metadata.frame_rate = 30.0; + + // Hardware decoder may fail initialization on systems without support + // This is expected behavior, not a test failure + decoder->Initialize(metadata); + } + + // Test passes if we reach here without crashing + Assert::IsTrue(true); + } + + TEST_METHOD(AV1Decoder_Integration_WithMockReader) + { + // Arrange + MockWebMFileReader mockReader; + auto decoder = VideoDecoderFactory::CreateDecoder(VideoCodecType::AV1, VideoDecoderFactory::DecoderType::SOFTWARE); + + mockReader.OpenFile("test.webm"); + mockReader.SelectVideoTrack(1); + + const auto& metadata = mockReader.GetVideoMetadata(); + decoder->Initialize(metadata); + + // Act - Try to decode first packet + VideoPacket packet; + bool readResult = mockReader.ReadNextPacket(packet); + + VideoFrame frame; + bool decodeResult = false; + if (readResult && packet.data && packet.size > 0) { + // Note: This will likely fail with mock data since it's not real AV1 + // But the test verifies the integration doesn't crash + decodeResult = decoder->DecodeFrame(packet, frame); + } + + // Assert + Assert::IsTrue(readResult); // Mock reader should succeed + Assert::IsTrue(decoder->IsInitialized()); + // decodeResult may be false since mock data isn't real AV1 - this is expected + } + }; +} \ No newline at end of file diff --git a/vav2/Vav2Player/Vav2Player/unit-test/MockVideoRenderer.cpp b/vav2/Vav2Player/Vav2Player/unit-test/MockVideoRenderer.cpp new file mode 100644 index 0000000..a449843 --- /dev/null +++ b/vav2/Vav2Player/Vav2Player/unit-test/MockVideoRenderer.cpp @@ -0,0 +1,198 @@ +#include "pch.h" +#include "MockVideoRenderer.h" +#include +#include +#include + +namespace Vav2PlayerUnitTests { + +MockVideoRenderer::MockVideoRenderer() { +} + +HRESULT MockVideoRenderer::Initialize(uint32_t width, uint32_t height) { + m_initializeCallCount++; + m_lastInitializeWidth = width; + m_lastInitializeHeight = height; + + if (FAILED(m_initializeResult)) { + return m_initializeResult; + } + + m_width = width; + m_height = height; + m_initialized = true; + return S_OK; +} + +void MockVideoRenderer::Shutdown() { + m_initialized = false; + m_width = 0; + m_height = 0; + m_renderedFrames.clear(); +} + +bool MockVideoRenderer::IsInitialized() const { + return m_initialized; +} + +HRESULT MockVideoRenderer::RenderVideoFrame(const Vav2Player::VideoFrame& frame) { + m_renderFrameCallCount++; + + if (!m_initialized) { + return E_FAIL; + } + + if (FAILED(m_renderFrameResult)) { + return m_renderFrameResult; + } + + // Store frame for test verification + Vav2Player::VideoFrame frameCopy; + CopyFrame(frame, frameCopy); + m_renderedFrames.push_back(std::move(frameCopy)); + + // Simulate rendering delay if configured + SimulateRenderDelay(); + + return S_OK; +} + +bool MockVideoRenderer::TryRenderFrame(const Vav2Player::VideoFrame& frame) { + m_tryRenderFrameCallCount++; + + if (!m_initialized || !m_tryRenderFrameResult) { + return false; + } + + // Store frame for test verification + Vav2Player::VideoFrame frameCopy; + CopyFrame(frame, frameCopy); + m_renderedFrames.push_back(std::move(frameCopy)); + + // Simulate rendering delay if configured + SimulateRenderDelay(); + + return true; +} + +HRESULT MockVideoRenderer::Present() { + m_presentCallCount++; + + if (!m_initialized) { + return E_FAIL; + } + + return m_presentResult; +} + +HRESULT MockVideoRenderer::Resize(uint32_t width, uint32_t height) { + m_resizeCallCount++; + + if (!m_initialized) { + return E_FAIL; + } + + if (FAILED(m_resizeResult)) { + return m_resizeResult; + } + + m_width = width; + m_height = height; + return S_OK; +} + +uint32_t MockVideoRenderer::GetWidth() const { + return m_width; +} + +uint32_t MockVideoRenderer::GetHeight() const { + return m_height; +} + +void MockVideoRenderer::SetAspectFitMode(bool enabled) { + m_aspectFitEnabled = enabled; +} + +void MockVideoRenderer::UpdateContainerSize(uint32_t container_width, uint32_t container_height) { + m_containerWidth = container_width; + m_containerHeight = container_height; +} + +// Mock control methods +void MockVideoRenderer::SetInitializeResult(HRESULT result) { + m_initializeResult = result; +} + +void MockVideoRenderer::SetRenderFrameResult(HRESULT result) { + m_renderFrameResult = result; +} + +void MockVideoRenderer::SetTryRenderFrameResult(bool success) { + m_tryRenderFrameResult = success; +} + +void MockVideoRenderer::SetPresentResult(HRESULT result) { + m_presentResult = result; +} + +void MockVideoRenderer::SetResizeResult(HRESULT result) { + m_resizeResult = result; +} + +void MockVideoRenderer::SimulateInitializationFailure(bool fail) { + m_initializeResult = fail ? E_FAIL : S_OK; +} + +void MockVideoRenderer::SimulateRenderingFailure(bool fail) { + m_renderFrameResult = fail ? E_FAIL : S_OK; + m_tryRenderFrameResult = !fail; +} + +// Private helper methods +void MockVideoRenderer::CopyFrame(const Vav2Player::VideoFrame& source, Vav2Player::VideoFrame& dest) { + dest.width = source.width; + dest.height = source.height; + dest.format = source.format; + dest.color_space = source.color_space; + dest.timestamp_ns = source.timestamp_ns; + dest.frame_index = source.frame_index; + + // Copy stride information + dest.y_stride = source.y_stride; + dest.u_stride = source.u_stride; + dest.v_stride = source.v_stride; + + // Copy Y plane + if (source.y_plane && source.y_stride > 0 && source.height > 0) { + size_t y_size = static_cast(source.y_stride) * source.height; + dest.y_plane = std::make_unique(y_size); + std::memcpy(dest.y_plane.get(), source.y_plane.get(), y_size); + } + + // Copy U plane + if (source.u_plane && source.u_stride > 0 && source.height > 0) { + size_t u_size = static_cast(source.u_stride) * (source.height / 2); // YUV420 assumption + dest.u_plane = std::make_unique(u_size); + std::memcpy(dest.u_plane.get(), source.u_plane.get(), u_size); + } + + // Copy V plane + if (source.v_plane && source.v_stride > 0 && source.height > 0) { + size_t v_size = static_cast(source.v_stride) * (source.height / 2); // YUV420 assumption + dest.v_plane = std::make_unique(v_size); + std::memcpy(dest.v_plane.get(), source.v_plane.get(), v_size); + } +} + +void MockVideoRenderer::SimulateRenderDelay() { + if (m_renderDelayMs > 0) { + auto start = std::chrono::high_resolution_clock::now(); + std::this_thread::sleep_for(std::chrono::milliseconds(m_renderDelayMs)); + auto end = std::chrono::high_resolution_clock::now(); + + auto duration = std::chrono::duration_cast(end - start); + m_totalRenderTimeMs += static_cast(duration.count()); + } +} + +} // namespace Vav2PlayerUnitTests \ No newline at end of file diff --git a/vav2/Vav2Player/Vav2Player/unit-test/MockVideoRenderer.h b/vav2/Vav2Player/Vav2Player/unit-test/MockVideoRenderer.h new file mode 100644 index 0000000..9a655b1 --- /dev/null +++ b/vav2/Vav2Player/Vav2Player/unit-test/MockVideoRenderer.h @@ -0,0 +1,98 @@ +#pragma once +#include "../src/Rendering/IVideoRenderer.h" +#include +#include + +namespace Vav2PlayerUnitTests { + +// Mock implementation of IVideoRenderer for unit testing +// Provides controllable rendering behavior without actual D3D12/GPU operations +class MockVideoRenderer : public Vav2Player::IVideoRenderer { +public: + MockVideoRenderer(); + virtual ~MockVideoRenderer() = default; + + // IVideoRenderer interface implementation + HRESULT Initialize(uint32_t width, uint32_t height) override; + void Shutdown() override; + bool IsInitialized() const override; + + HRESULT RenderVideoFrame(const Vav2Player::VideoFrame& frame) override; + bool TryRenderFrame(const Vav2Player::VideoFrame& frame) override; + HRESULT Present() override; + + HRESULT Resize(uint32_t width, uint32_t height) override; + uint32_t GetWidth() const override; + uint32_t GetHeight() const override; + + void SetAspectFitMode(bool enabled) override; + void UpdateContainerSize(uint32_t container_width, uint32_t container_height) override; + + // Mock-specific control methods for testing + void SetInitializeResult(HRESULT result); + void SetRenderFrameResult(HRESULT result); + void SetTryRenderFrameResult(bool success); + void SetPresentResult(HRESULT result); + void SetResizeResult(HRESULT result); + + void SimulateInitializationFailure(bool fail = true); + void SimulateRenderingFailure(bool fail = true); + + // Test verification methods + uint32_t GetInitializeCallCount() const { return m_initializeCallCount; } + uint32_t GetRenderFrameCallCount() const { return m_renderFrameCallCount; } + uint32_t GetTryRenderFrameCallCount() const { return m_tryRenderFrameCallCount; } + uint32_t GetPresentCallCount() const { return m_presentCallCount; } + uint32_t GetResizeCallCount() const { return m_resizeCallCount; } + + uint32_t GetLastInitializeWidth() const { return m_lastInitializeWidth; } + uint32_t GetLastInitializeHeight() const { return m_lastInitializeHeight; } + + const std::vector& GetRenderedFrames() const { return m_renderedFrames; } + uint32_t GetTotalRenderedFrames() const { return static_cast(m_renderedFrames.size()); } + + bool GetAspectFitMode() const { return m_aspectFitEnabled; } + uint32_t GetContainerWidth() const { return m_containerWidth; } + uint32_t GetContainerHeight() const { return m_containerHeight; } + + // Performance simulation + void SetRenderDelay(uint32_t milliseconds) { m_renderDelayMs = milliseconds; } + uint64_t GetTotalRenderTime() const { return m_totalRenderTimeMs; } + +private: + // Mock state + bool m_initialized = false; + uint32_t m_width = 0; + uint32_t m_height = 0; + bool m_aspectFitEnabled = false; + uint32_t m_containerWidth = 0; + uint32_t m_containerHeight = 0; + + // Mock behavior control + HRESULT m_initializeResult = S_OK; + HRESULT m_renderFrameResult = S_OK; + bool m_tryRenderFrameResult = true; + HRESULT m_presentResult = S_OK; + HRESULT m_resizeResult = S_OK; + uint32_t m_renderDelayMs = 0; + + // Call tracking for test verification + mutable uint32_t m_initializeCallCount = 0; + mutable uint32_t m_renderFrameCallCount = 0; + mutable uint32_t m_tryRenderFrameCallCount = 0; + mutable uint32_t m_presentCallCount = 0; + mutable uint32_t m_resizeCallCount = 0; + + uint32_t m_lastInitializeWidth = 0; + uint32_t m_lastInitializeHeight = 0; + + // Frame storage for test verification + std::vector m_renderedFrames; + uint64_t m_totalRenderTimeMs = 0; + + // Helper methods + void CopyFrame(const Vav2Player::VideoFrame& source, Vav2Player::VideoFrame& dest); + void SimulateRenderDelay(); +}; + +} // namespace Vav2PlayerUnitTests \ No newline at end of file diff --git a/vav2/Vav2Player/Vav2Player/unit-test/MockWebMFileReader.cpp b/vav2/Vav2Player/Vav2Player/unit-test/MockWebMFileReader.cpp new file mode 100644 index 0000000..a75645e --- /dev/null +++ b/vav2/Vav2Player/Vav2Player/unit-test/MockWebMFileReader.cpp @@ -0,0 +1,247 @@ +#include "pch.h" +#include "MockWebMFileReader.h" +#include + +namespace Vav2PlayerUnitTests { + +MockWebMFileReader::MockWebMFileReader() { + CreateDefaultMockData(); +} + +bool MockWebMFileReader::OpenFile(const std::string& file_path) { + m_openFileCallCount++; + m_lastOpenedFile = file_path; + + if (!m_openFileResult) { + m_mockError = Vav2Player::WebMErrorCode::FileNotFound; + m_mockErrorMessage = "Mock file not found: " + file_path; + return false; + } + + m_isOpen = true; + m_currentFilePath = file_path; + m_currentFrame = 0; + m_mockError = Vav2Player::WebMErrorCode::Success; + m_mockErrorMessage.clear(); + return true; +} + +void MockWebMFileReader::CloseFile() { + m_isOpen = false; + m_currentFilePath.clear(); + m_currentFrame = 0; +} + +bool MockWebMFileReader::IsFileOpen() const { + return m_isOpen; +} + +const Vav2Player::VideoMetadata& MockWebMFileReader::GetVideoMetadata() const { + return m_mockMetadata; +} + +std::string MockWebMFileReader::GetFilePath() const { + return m_currentFilePath; +} + +std::vector MockWebMFileReader::GetVideoTracks() const { + return m_mockTracks; +} + +bool MockWebMFileReader::SelectVideoTrack(uint64_t track_number) { + if (!m_isOpen) { + m_mockError = Vav2Player::WebMErrorCode::FileNotOpen; + return false; + } + + // Validate track exists + for (const auto& track : m_mockTracks) { + if (track.track_number == track_number) { + m_selectedTrack = track_number; + return true; + } + } + + m_mockError = Vav2Player::WebMErrorCode::InvalidTrack; + return false; +} + +uint64_t MockWebMFileReader::GetSelectedTrackNumber() const { + return m_selectedTrack; +} + +bool MockWebMFileReader::ReadNextPacket(Vav2Player::VideoPacket& packet) { + m_readPacketCallCount++; + + if (!m_isOpen) { + m_mockError = Vav2Player::WebMErrorCode::FileNotOpen; + return false; + } + + if (m_currentFrame >= m_endOfFileFrame || m_currentFrame >= m_mockPackets.size()) { + return false; // End of file + } + + // Create packet from mock data + const auto& mockData = m_mockPackets[m_currentFrame]; + packet.data = std::make_unique(mockData.size()); + packet.size = mockData.size(); + std::memcpy(packet.data.get(), mockData.data(), mockData.size()); + + packet.timestamp_seconds = static_cast(m_currentFrame) / m_mockMetadata.frame_rate; + packet.frame_index = m_currentFrame; + packet.is_keyframe = (m_currentFrame % 30 == 0); // Every 30th frame is keyframe + + m_currentFrame++; + return true; +} + +bool MockWebMFileReader::SeekToFrame(uint64_t frame_index) { + if (!m_isOpen) { + m_mockError = Vav2Player::WebMErrorCode::FileNotOpen; + return false; + } + + if (frame_index >= m_mockPackets.size()) { + m_mockError = Vav2Player::WebMErrorCode::SeekFailed; + return false; + } + + m_currentFrame = frame_index; + return true; +} + +bool MockWebMFileReader::SeekToTime(double timestamp_seconds) { + if (!m_isOpen) { + m_mockError = Vav2Player::WebMErrorCode::FileNotOpen; + return false; + } + + uint64_t frame_index = static_cast(timestamp_seconds * m_mockMetadata.frame_rate); + return SeekToFrame(frame_index); +} + +uint64_t MockWebMFileReader::GetCurrentFrameIndex() const { + return m_currentFrame; +} + +double MockWebMFileReader::GetCurrentTimestamp() const { + return static_cast(m_currentFrame) / m_mockMetadata.frame_rate; +} + +bool MockWebMFileReader::IsEndOfFile() const { + return m_currentFrame >= m_endOfFileFrame || m_currentFrame >= m_mockPackets.size(); +} + +bool MockWebMFileReader::Reset() { + if (!m_isOpen) { + m_mockError = Vav2Player::WebMErrorCode::FileNotOpen; + return false; + } + + m_currentFrame = 0; + return true; +} + +uint64_t MockWebMFileReader::GetTotalFrames() const { + return m_mockPackets.size(); +} + +double MockWebMFileReader::GetDuration() const { + return m_mockMetadata.duration_seconds; +} + +Vav2Player::WebMErrorCode MockWebMFileReader::GetLastError() const { + return m_mockError; +} + +std::string MockWebMFileReader::GetLastErrorString() const { + return m_mockErrorMessage; +} + +// Mock control methods +void MockWebMFileReader::SetMockVideoMetadata(const Vav2Player::VideoMetadata& metadata) { + m_mockMetadata = metadata; +} + +void MockWebMFileReader::SetMockVideoTracks(const std::vector& tracks) { + m_mockTracks = tracks; +} + +void MockWebMFileReader::SetMockPackets(const std::vector>& packets) { + m_mockPackets = packets; + // Update metadata to match packet count + m_mockMetadata.total_frames = packets.size(); + m_mockMetadata.duration_seconds = static_cast(packets.size()) / m_mockMetadata.frame_rate; +} + +void MockWebMFileReader::SetMockError(Vav2Player::WebMErrorCode error, const std::string& message) { + m_mockError = error; + m_mockErrorMessage = message; +} + +void MockWebMFileReader::SetOpenFileResult(bool success) { + m_openFileResult = success; +} + +void MockWebMFileReader::SetEndOfFileAtFrame(uint64_t frame_index) { + m_endOfFileFrame = frame_index; +} + +// Private helper methods +void MockWebMFileReader::CreateDefaultMockData() { + // Default metadata for 1920x1080 30fps AV1 video + m_mockMetadata.width = 1920; + m_mockMetadata.height = 1080; + m_mockMetadata.frame_rate = 30.0; + m_mockMetadata.codec_type = Vav2Player::VideoCodecType::AV1; + m_mockMetadata.color_space = Vav2Player::ColorSpace::BT709; + m_mockMetadata.total_frames = 100; // 100 frames = ~3.33 seconds + m_mockMetadata.duration_seconds = 100.0 / 30.0; + + // Default video track + Vav2Player::VideoTrackInfo track; + track.track_number = 1; + track.codec_id = "V_AV01"; + track.codec_name = "AV1"; + track.codec_type = Vav2Player::VideoCodecType::AV1; + track.width = 1920; + track.height = 1080; + track.frame_rate = 30.0; + track.is_enabled = true; + m_mockTracks.push_back(track); + + // Create 100 mock AV1 packets + m_mockPackets.clear(); + for (uint64_t i = 0; i < 100; ++i) { + m_mockPackets.push_back(CreateMockAV1Packet(i)); + } + + m_selectedTrack = 1; +} + +std::vector MockWebMFileReader::CreateMockAV1Packet(uint64_t frame_index) { + // Create a minimal mock AV1 packet + // Real AV1 packets are complex, but for testing we just need valid data + std::vector packet; + + // AV1 OBU header pattern (simplified) + packet.push_back(0x0A); // OBU type (frame) + packet.push_back(0x00); // OBU extension flag + + // Mock frame size (varies by frame index for realism) + uint32_t frame_size = 1000 + (frame_index % 500); // 1000-1500 bytes + + // Add frame size in LEB128 format (simplified) + packet.push_back(static_cast(frame_size & 0x7F)); + packet.push_back(static_cast((frame_size >> 7) & 0x7F)); + + // Fill with mock data + for (uint32_t i = 0; i < frame_size; ++i) { + packet.push_back(static_cast((frame_index + i) & 0xFF)); + } + + return packet; +} + +} // namespace Vav2PlayerUnitTests \ No newline at end of file diff --git a/vav2/Vav2Player/Vav2Player/unit-test/MockWebMFileReader.h b/vav2/Vav2Player/Vav2Player/unit-test/MockWebMFileReader.h new file mode 100644 index 0000000..40ee5e6 --- /dev/null +++ b/vav2/Vav2Player/Vav2Player/unit-test/MockWebMFileReader.h @@ -0,0 +1,84 @@ +#pragma once +#include "../src/FileIO/IWebMFileReader.h" +#include +#include +#include + +namespace Vav2PlayerUnitTests { + +// Mock implementation of IWebMFileReader for unit testing +// Provides controllable test data without actual file I/O +class MockWebMFileReader : public Vav2Player::IWebMFileReader { +public: + MockWebMFileReader(); + virtual ~MockWebMFileReader() = default; + + // IWebMFileReader interface implementation + bool OpenFile(const std::string& file_path) override; + void CloseFile() override; + bool IsFileOpen() const override; + + const Vav2Player::VideoMetadata& GetVideoMetadata() const override; + std::string GetFilePath() const override; + + std::vector GetVideoTracks() const override; + bool SelectVideoTrack(uint64_t track_number) override; + uint64_t GetSelectedTrackNumber() const override; + + bool ReadNextPacket(Vav2Player::VideoPacket& packet) override; + bool SeekToFrame(uint64_t frame_index) override; + bool SeekToTime(double timestamp_seconds) override; + + uint64_t GetCurrentFrameIndex() const override; + double GetCurrentTimestamp() const override; + bool IsEndOfFile() const override; + + bool Reset() override; + uint64_t GetTotalFrames() const override; + double GetDuration() const override; + + Vav2Player::WebMErrorCode GetLastError() const override; + std::string GetLastErrorString() const override; + + // Mock-specific control methods for testing + void SetMockVideoMetadata(const Vav2Player::VideoMetadata& metadata); + void SetMockVideoTracks(const std::vector& tracks); + void SetMockPackets(const std::vector>& packets); + void SetMockError(Vav2Player::WebMErrorCode error, const std::string& message = ""); + void SetOpenFileResult(bool success); + void SetEndOfFileAtFrame(uint64_t frame_index); + + // Test verification methods + uint32_t GetOpenFileCallCount() const { return m_openFileCallCount; } + uint32_t GetReadPacketCallCount() const { return m_readPacketCallCount; } + std::string GetLastOpenedFile() const { return m_lastOpenedFile; } + +private: + // Mock state + bool m_isOpen = false; + bool m_openFileResult = true; + std::string m_currentFilePath; + std::string m_lastOpenedFile; + uint64_t m_selectedTrack = 0; + uint64_t m_currentFrame = 0; + uint64_t m_endOfFileFrame = UINT64_MAX; + + // Mock data + Vav2Player::VideoMetadata m_mockMetadata; + std::vector m_mockTracks; + std::vector> m_mockPackets; + + // Error simulation + Vav2Player::WebMErrorCode m_mockError = Vav2Player::WebMErrorCode::Success; + std::string m_mockErrorMessage; + + // Call tracking for test verification + mutable uint32_t m_openFileCallCount = 0; + mutable uint32_t m_readPacketCallCount = 0; + + // Helper methods + void CreateDefaultMockData(); + std::vector CreateMockAV1Packet(uint64_t frame_index); +}; + +} // namespace Vav2PlayerUnitTests \ No newline at end of file diff --git a/vav2/Vav2Player/Vav2Player/unit-test/VideoPlayerControlTest.cpp b/vav2/Vav2Player/Vav2Player/unit-test/VideoPlayerControlTest.cpp new file mode 100644 index 0000000..35be417 --- /dev/null +++ b/vav2/Vav2Player/Vav2Player/unit-test/VideoPlayerControlTest.cpp @@ -0,0 +1,245 @@ +#include "pch.h" +#include "MockWebMFileReader.h" +#include "MockVideoRenderer.h" +// Note: VideoPlayerControl integration testing would require WinUI3 mocking +// For now, this focuses on testing the integration patterns and dependency injection + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace Vav2Player; +using namespace Vav2PlayerUnitTests; + +namespace Vav2PlayerUnitTests +{ + TEST_CLASS(VideoPlayerControlTest) + { + public: + TEST_METHOD(DependencyInjection_MockComponents_ShouldWorkTogether) + { + // Arrange + auto mockReader = std::make_unique(); + auto mockRenderer = std::make_unique(); + + MockWebMFileReader* readerPtr = mockReader.get(); + MockVideoRenderer* rendererPtr = mockRenderer.get(); + + // Set up mock data + VideoMetadata metadata; + metadata.width = 1920; + metadata.height = 1080; + metadata.frame_rate = 30.0; + metadata.codec_type = VideoCodecType::AV1; + readerPtr->SetMockVideoMetadata(metadata); + + // Act - Simulate VideoPlayerControl workflow + bool openResult = readerPtr->OpenFile("test_video.webm"); + auto tracks = readerPtr->GetVideoTracks(); + bool selectResult = readerPtr->SelectVideoTrack(tracks[0].track_number); + + HRESULT initResult = rendererPtr->Initialize(metadata.width, metadata.height); + + // Read and "render" first frame + VideoPacket packet; + bool readResult = readerPtr->ReadNextPacket(packet); + + // Create mock video frame for rendering test + VideoFrame frame; + frame.width = metadata.width; + frame.height = metadata.height; + frame.format = PixelFormat::YUV420P; + frame.frame_index = 0; + + bool renderResult = rendererPtr->TryRenderFrame(frame); + + // Assert + Assert::IsTrue(openResult); + Assert::IsTrue(selectResult); + Assert::AreEqual(S_OK, initResult); + Assert::IsTrue(readResult); + Assert::IsTrue(renderResult); + + // Verify call counts + Assert::AreEqual(1u, readerPtr->GetOpenFileCallCount()); + Assert::AreEqual(1u, readerPtr->GetReadPacketCallCount()); + Assert::AreEqual(1u, rendererPtr->GetInitializeCallCount()); + Assert::AreEqual(1u, rendererPtr->GetTryRenderFrameCallCount()); + } + + TEST_METHOD(ErrorHandling_FileNotFound_ShouldPropagateError) + { + // Arrange + auto mockReader = std::make_unique(); + mockReader->SetOpenFileResult(false); + + // Act + bool result = mockReader->OpenFile("nonexistent.webm"); + + // Assert + Assert::IsFalse(result); + Assert::AreEqual(WebMErrorCode::FileNotFound, mockReader->GetLastError()); + Assert::IsFalse(mockReader->GetLastErrorString().empty()); + } + + TEST_METHOD(ErrorHandling_RendererInitializationFailure_ShouldBeHandled) + { + // Arrange + auto mockRenderer = std::make_unique(); + mockRenderer->SimulateInitializationFailure(true); + + // Act + HRESULT result = mockRenderer->Initialize(1920, 1080); + + // Assert + Assert::AreEqual(E_FAIL, result); + Assert::IsFalse(mockRenderer->IsInitialized()); + } + + TEST_METHOD(PlaybackSimulation_MultipleFrames_ShouldMaintainState) + { + // Arrange + auto mockReader = std::make_unique(); + auto mockRenderer = std::make_unique(); + + mockReader->OpenFile("test.webm"); + mockReader->SelectVideoTrack(1); + mockRenderer->Initialize(1920, 1080); + + // Act - Simulate playing 10 frames + for (int i = 0; i < 10; ++i) { + VideoPacket packet; + bool readResult = mockReader->ReadNextPacket(packet); + + if (readResult) { + // Create frame from packet (simplified) + VideoFrame frame; + frame.width = 1920; + frame.height = 1080; + frame.frame_index = packet.frame_index; + frame.timestamp_ns = static_cast(packet.timestamp_seconds * 1000000000.0); + + mockRenderer->TryRenderFrame(frame); + } + } + + // Assert + Assert::AreEqual(10u, mockReader->GetReadPacketCallCount()); + Assert::AreEqual(10u, mockRenderer->GetTryRenderFrameCallCount()); + Assert::AreEqual(static_cast(10u), mockReader->GetCurrentFrameIndex()); + Assert::AreEqual(10u, mockRenderer->GetTotalRenderedFrames()); + } + + TEST_METHOD(SeekOperation_ShouldUpdateReaderState) + { + // Arrange + auto mockReader = std::make_unique(); + mockReader->OpenFile("test.webm"); + mockReader->SelectVideoTrack(1); + + // Read some packets first + for (int i = 0; i < 5; ++i) { + VideoPacket packet; + mockReader->ReadNextPacket(packet); + } + + // Act - Seek to frame 50 + bool seekResult = mockReader->SeekToFrame(50); + + // Assert + Assert::IsTrue(seekResult); + Assert::AreEqual(50u, static_cast(mockReader->GetCurrentFrameIndex())); + + // Next packet should be frame 50 + VideoPacket packet; + bool readResult = mockReader->ReadNextPacket(packet); + Assert::IsTrue(readResult); + Assert::AreEqual(50u, static_cast(packet.frame_index)); + } + + TEST_METHOD(AspectRatioHandling_ShouldUpdateRenderer) + { + // Arrange + auto mockRenderer = std::make_unique(); + mockRenderer->Initialize(1920, 1080); + + // Act - Simulate container size changes (AspectFit scenario) + mockRenderer->UpdateContainerSize(1280, 720); // 16:9 container + mockRenderer->SetAspectFitMode(true); + + // Assert + Assert::AreEqual(1280u, mockRenderer->GetContainerWidth()); + Assert::AreEqual(720u, mockRenderer->GetContainerHeight()); + Assert::IsTrue(mockRenderer->GetAspectFitMode()); + } + + TEST_METHOD(ResourceCleanup_ShouldReleaseResources) + { + // Arrange + auto mockReader = std::make_unique(); + auto mockRenderer = std::make_unique(); + + mockReader->OpenFile("test.webm"); + mockRenderer->Initialize(1920, 1080); + + // Render some frames + for (int i = 0; i < 3; ++i) { + VideoFrame frame; + frame.width = 1920; + frame.height = 1080; + frame.frame_index = i; + mockRenderer->RenderVideoFrame(frame); + } + + // Act - Cleanup + mockReader->CloseFile(); + mockRenderer->Shutdown(); + + // Assert + Assert::IsFalse(mockReader->IsFileOpen()); + Assert::IsFalse(mockRenderer->IsInitialized()); + Assert::AreEqual(0u, mockRenderer->GetTotalRenderedFrames()); // Should be cleared + } + + TEST_METHOD(PerformanceMetrics_ShouldBeTracked) + { + // Arrange + auto mockRenderer = std::make_unique(); + mockRenderer->Initialize(1920, 1080); + mockRenderer->SetRenderDelay(5); // 5ms per frame + + // Act - Render frames with timing + auto start = std::chrono::high_resolution_clock::now(); + + for (int i = 0; i < 5; ++i) { + VideoFrame frame; + frame.width = 1920; + frame.height = 1080; + frame.frame_index = i; + mockRenderer->RenderVideoFrame(frame); + } + + auto end = std::chrono::high_resolution_clock::now(); + auto totalTime = std::chrono::duration_cast(end - start); + + // Assert + Assert::IsTrue(totalTime.count() >= 25); // At least 25ms (5ms * 5 frames) + Assert::IsTrue(mockRenderer->GetTotalRenderTime() >= 25); + Assert::AreEqual(5u, mockRenderer->GetTotalRenderedFrames()); + } + + TEST_METHOD(InterfaceCompatibility_ShouldWorkWithBasePointers) + { + // Arrange - Test polymorphism through interfaces + std::unique_ptr reader = std::make_unique(); + std::unique_ptr renderer = std::make_unique(); + + // Act - Use through interface pointers + bool openResult = reader->OpenFile("test.webm"); + HRESULT initResult = renderer->Initialize(1920, 1080); + + // Assert + Assert::IsTrue(openResult); + Assert::AreEqual(S_OK, initResult); + Assert::IsTrue(reader->IsFileOpen()); + Assert::IsTrue(renderer->IsInitialized()); + } + }; +} \ No newline at end of file diff --git a/vav2/Vav2Player/Vav2Player/unit-test/VideoRendererTest.cpp b/vav2/Vav2Player/Vav2Player/unit-test/VideoRendererTest.cpp new file mode 100644 index 0000000..5210fcc --- /dev/null +++ b/vav2/Vav2Player/Vav2Player/unit-test/VideoRendererTest.cpp @@ -0,0 +1,288 @@ +#include "pch.h" +#include "MockVideoRenderer.h" +#include "../src/Common/VideoTypes.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace Vav2Player; +using namespace Vav2PlayerUnitTests; + +namespace Vav2PlayerUnitTests +{ + TEST_CLASS(VideoRendererTest) + { + public: + TEST_METHOD(MockVideoRenderer_Initialize_Success_ShouldReturnSOK) + { + // Arrange + MockVideoRenderer mockRenderer; + + // Act + HRESULT result = mockRenderer.Initialize(1920, 1080); + + // Assert + Assert::AreEqual(S_OK, result); + Assert::IsTrue(mockRenderer.IsInitialized()); + Assert::AreEqual(1920u, mockRenderer.GetWidth()); + Assert::AreEqual(1080u, mockRenderer.GetHeight()); + Assert::AreEqual(1u, mockRenderer.GetInitializeCallCount()); + } + + TEST_METHOD(MockVideoRenderer_Initialize_Failure_ShouldReturnError) + { + // Arrange + MockVideoRenderer mockRenderer; + mockRenderer.SetInitializeResult(E_FAIL); + + // Act + HRESULT result = mockRenderer.Initialize(1920, 1080); + + // Assert + Assert::AreEqual(E_FAIL, result); + Assert::IsFalse(mockRenderer.IsInitialized()); + } + + TEST_METHOD(MockVideoRenderer_RenderVideoFrame_WhenInitialized_ShouldSucceed) + { + // Arrange + MockVideoRenderer mockRenderer; + mockRenderer.Initialize(1920, 1080); + + VideoFrame testFrame; + testFrame.width = 1920; + testFrame.height = 1080; + testFrame.format = PixelFormat::YUV420P; + testFrame.frame_index = 0; + testFrame.timestamp_ns = 0; + + // Create test YUV data + testFrame.y_stride = 1920; + testFrame.u_stride = 960; + testFrame.v_stride = 960; + + size_t y_size = testFrame.y_stride * testFrame.height; + size_t uv_size = testFrame.u_stride * (testFrame.height / 2); + + testFrame.y_plane = std::make_unique(y_size); + testFrame.u_plane = std::make_unique(uv_size); + testFrame.v_plane = std::make_unique(uv_size); + + // Fill with test pattern + std::memset(testFrame.y_plane.get(), 128, y_size); + std::memset(testFrame.u_plane.get(), 64, uv_size); + std::memset(testFrame.v_plane.get(), 192, uv_size); + + // Act + HRESULT result = mockRenderer.RenderVideoFrame(testFrame); + + // Assert + Assert::AreEqual(S_OK, result); + Assert::AreEqual(1u, mockRenderer.GetRenderFrameCallCount()); + Assert::AreEqual(1u, mockRenderer.GetTotalRenderedFrames()); + + const auto& renderedFrames = mockRenderer.GetRenderedFrames(); + Assert::AreEqual(1u, static_cast(renderedFrames.size())); + Assert::AreEqual(1920u, renderedFrames[0].width); + Assert::AreEqual(1080u, renderedFrames[0].height); + } + + TEST_METHOD(MockVideoRenderer_TryRenderFrame_WhenInitialized_ShouldReturnTrue) + { + // Arrange + MockVideoRenderer mockRenderer; + mockRenderer.Initialize(1920, 1080); + + VideoFrame testFrame; + testFrame.width = 1920; + testFrame.height = 1080; + testFrame.format = PixelFormat::YUV420P; + + // Act + bool result = mockRenderer.TryRenderFrame(testFrame); + + // Assert + Assert::IsTrue(result); + Assert::AreEqual(1u, mockRenderer.GetTryRenderFrameCallCount()); + } + + TEST_METHOD(MockVideoRenderer_TryRenderFrame_WhenNotInitialized_ShouldReturnFalse) + { + // Arrange + MockVideoRenderer mockRenderer; + + VideoFrame testFrame; + testFrame.width = 1920; + testFrame.height = 1080; + + // Act + bool result = mockRenderer.TryRenderFrame(testFrame); + + // Assert + Assert::IsFalse(result); + } + + TEST_METHOD(MockVideoRenderer_Present_WhenInitialized_ShouldReturnSOK) + { + // Arrange + MockVideoRenderer mockRenderer; + mockRenderer.Initialize(1920, 1080); + + // Act + HRESULT result = mockRenderer.Present(); + + // Assert + Assert::AreEqual(S_OK, result); + Assert::AreEqual(1u, mockRenderer.GetPresentCallCount()); + } + + TEST_METHOD(MockVideoRenderer_Resize_ValidSize_ShouldUpdateDimensions) + { + // Arrange + MockVideoRenderer mockRenderer; + mockRenderer.Initialize(1920, 1080); + + // Act + HRESULT result = mockRenderer.Resize(3840, 2160); + + // Assert + Assert::AreEqual(S_OK, result); + Assert::AreEqual(3840u, mockRenderer.GetWidth()); + Assert::AreEqual(2160u, mockRenderer.GetHeight()); + Assert::AreEqual(1u, mockRenderer.GetResizeCallCount()); + } + + TEST_METHOD(MockVideoRenderer_Shutdown_ShouldResetState) + { + // Arrange + MockVideoRenderer mockRenderer; + mockRenderer.Initialize(1920, 1080); + + VideoFrame testFrame; + testFrame.width = 1920; + testFrame.height = 1080; + mockRenderer.RenderVideoFrame(testFrame); + + // Act + mockRenderer.Shutdown(); + + // Assert + Assert::IsFalse(mockRenderer.IsInitialized()); + Assert::AreEqual(0u, mockRenderer.GetWidth()); + Assert::AreEqual(0u, mockRenderer.GetHeight()); + Assert::AreEqual(0u, mockRenderer.GetTotalRenderedFrames()); + } + + TEST_METHOD(MockVideoRenderer_SetAspectFitMode_ShouldUpdateState) + { + // Arrange + MockVideoRenderer mockRenderer; + + // Act + mockRenderer.SetAspectFitMode(true); + + // Assert + Assert::IsTrue(mockRenderer.GetAspectFitMode()); + + // Act + mockRenderer.SetAspectFitMode(false); + + // Assert + Assert::IsFalse(mockRenderer.GetAspectFitMode()); + } + + TEST_METHOD(MockVideoRenderer_UpdateContainerSize_ShouldUpdateDimensions) + { + // Arrange + MockVideoRenderer mockRenderer; + + // Act + mockRenderer.UpdateContainerSize(1280, 720); + + // Assert + Assert::AreEqual(1280u, mockRenderer.GetContainerWidth()); + Assert::AreEqual(720u, mockRenderer.GetContainerHeight()); + } + + TEST_METHOD(MockVideoRenderer_SimulateFailures_ShouldBehaveCorrectly) + { + // Arrange + MockVideoRenderer mockRenderer; + + // Test initialization failure + mockRenderer.SimulateInitializationFailure(true); + HRESULT initResult = mockRenderer.Initialize(1920, 1080); + Assert::AreEqual(E_FAIL, initResult); + Assert::IsFalse(mockRenderer.IsInitialized()); + + // Reset and test rendering failure + mockRenderer.SimulateInitializationFailure(false); + mockRenderer.Initialize(1920, 1080); + + mockRenderer.SimulateRenderingFailure(true); + + VideoFrame testFrame; + testFrame.width = 1920; + testFrame.height = 1080; + + // Act + HRESULT renderResult = mockRenderer.RenderVideoFrame(testFrame); + bool tryRenderResult = mockRenderer.TryRenderFrame(testFrame); + + // Assert + Assert::AreEqual(E_FAIL, renderResult); + Assert::IsFalse(tryRenderResult); + } + + TEST_METHOD(MockVideoRenderer_PerformanceSimulation_ShouldTrackTime) + { + // Arrange + MockVideoRenderer mockRenderer; + mockRenderer.Initialize(1920, 1080); + mockRenderer.SetRenderDelay(10); // 10ms delay + + VideoFrame testFrame; + testFrame.width = 1920; + testFrame.height = 1080; + + // Act + auto startTime = std::chrono::high_resolution_clock::now(); + mockRenderer.RenderVideoFrame(testFrame); + auto endTime = std::chrono::high_resolution_clock::now(); + + auto actualDuration = std::chrono::duration_cast(endTime - startTime); + + // Assert + Assert::IsTrue(actualDuration.count() >= 10); // Should take at least 10ms + Assert::IsTrue(mockRenderer.GetTotalRenderTime() >= 10); // Should track the time + } + + TEST_METHOD(MockVideoRenderer_MultipleFrames_ShouldTrackAll) + { + // Arrange + MockVideoRenderer mockRenderer; + mockRenderer.Initialize(1920, 1080); + + // Act - Render 5 frames + for (uint32_t i = 0; i < 5; ++i) { + VideoFrame testFrame; + testFrame.width = 1920; + testFrame.height = 1080; + testFrame.frame_index = i; + testFrame.timestamp_ns = i * 33333333; // ~30fps + + mockRenderer.RenderVideoFrame(testFrame); + } + + // Assert + Assert::AreEqual(5u, mockRenderer.GetRenderFrameCallCount()); + Assert::AreEqual(5u, mockRenderer.GetTotalRenderedFrames()); + + const auto& renderedFrames = mockRenderer.GetRenderedFrames(); + Assert::AreEqual(5u, static_cast(renderedFrames.size())); + + // Verify frame indices are correct + for (uint32_t i = 0; i < 5; ++i) { + Assert::AreEqual(static_cast(i), renderedFrames[i].frame_index); + } + } + }; +} \ No newline at end of file diff --git a/vav2/Vav2Player/Vav2Player/unit-test/WebMFileReaderTest.cpp b/vav2/Vav2Player/Vav2Player/unit-test/WebMFileReaderTest.cpp new file mode 100644 index 0000000..90b6a2f --- /dev/null +++ b/vav2/Vav2Player/Vav2Player/unit-test/WebMFileReaderTest.cpp @@ -0,0 +1,220 @@ +#include "pch.h" +#include "MockWebMFileReader.h" +#include "../src/FileIO/WebMFileReader.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; +using namespace Vav2Player; +using namespace Vav2PlayerUnitTests; + +namespace Vav2PlayerUnitTests +{ + TEST_CLASS(WebMFileReaderTest) + { + public: + TEST_METHOD(MockWebMFileReader_OpenFile_Success_ShouldReturnTrue) + { + // Arrange + MockWebMFileReader mockReader; + std::string testFile = "test_video.webm"; + + // Act + bool result = mockReader.OpenFile(testFile); + + // Assert + Assert::IsTrue(result); + Assert::IsTrue(mockReader.IsFileOpen()); + Assert::AreEqual(testFile, mockReader.GetFilePath()); + Assert::AreEqual(1u, mockReader.GetOpenFileCallCount()); + } + + TEST_METHOD(MockWebMFileReader_OpenFile_Failure_ShouldReturnFalse) + { + // Arrange + MockWebMFileReader mockReader; + mockReader.SetOpenFileResult(false); + std::string testFile = "nonexistent.webm"; + + // Act + bool result = mockReader.OpenFile(testFile); + + // Assert + Assert::IsFalse(result); + Assert::IsFalse(mockReader.IsFileOpen()); + Assert::AreEqual(WebMErrorCode::FileNotFound, mockReader.GetLastError()); + } + + TEST_METHOD(MockWebMFileReader_GetVideoTracks_ShouldReturnDefaultTrack) + { + // Arrange + MockWebMFileReader mockReader; + mockReader.OpenFile("test.webm"); + + // Act + auto tracks = mockReader.GetVideoTracks(); + + // Assert + Assert::AreEqual(1u, static_cast(tracks.size())); + Assert::AreEqual(1u, static_cast(tracks[0].track_number)); + Assert::AreEqual(VideoCodecType::AV1, tracks[0].codec_type); + Assert::AreEqual(1920u, tracks[0].width); + Assert::AreEqual(1080u, tracks[0].height); + } + + TEST_METHOD(MockWebMFileReader_SelectVideoTrack_ValidTrack_ShouldReturnTrue) + { + // Arrange + MockWebMFileReader mockReader; + mockReader.OpenFile("test.webm"); + + // Act + bool result = mockReader.SelectVideoTrack(1); + + // Assert + Assert::IsTrue(result); + Assert::AreEqual(1u, static_cast(mockReader.GetSelectedTrackNumber())); + } + + TEST_METHOD(MockWebMFileReader_SelectVideoTrack_InvalidTrack_ShouldReturnFalse) + { + // Arrange + MockWebMFileReader mockReader; + mockReader.OpenFile("test.webm"); + + // Act + bool result = mockReader.SelectVideoTrack(999); + + // Assert + Assert::IsFalse(result); + Assert::AreEqual(WebMErrorCode::InvalidTrack, mockReader.GetLastError()); + } + + TEST_METHOD(MockWebMFileReader_ReadNextPacket_ShouldReturnValidPackets) + { + // Arrange + MockWebMFileReader mockReader; + mockReader.OpenFile("test.webm"); + mockReader.SelectVideoTrack(1); + + // Act + VideoPacket packet; + bool result = mockReader.ReadNextPacket(packet); + + // Assert + Assert::IsTrue(result); + Assert::IsNotNull(packet.data.get()); + Assert::IsTrue(packet.size > 0); + Assert::AreEqual(0u, static_cast(packet.frame_index)); + // Note: track_number field doesn't exist in VideoPacket, skipping this assertion + Assert::AreEqual(1u, mockReader.GetReadPacketCallCount()); + } + + TEST_METHOD(MockWebMFileReader_ReadMultiplePackets_ShouldIncrementFrameIndex) + { + // Arrange + MockWebMFileReader mockReader; + mockReader.OpenFile("test.webm"); + mockReader.SelectVideoTrack(1); + + // Act & Assert + for (uint64_t i = 0; i < 5; ++i) { + VideoPacket packet; + bool result = mockReader.ReadNextPacket(packet); + + Assert::IsTrue(result); + Assert::AreEqual(i, packet.frame_index); + Assert::AreEqual(i + 1, mockReader.GetCurrentFrameIndex()); + } + + Assert::AreEqual(5u, mockReader.GetReadPacketCallCount()); + } + + TEST_METHOD(MockWebMFileReader_SeekToFrame_ValidFrame_ShouldUpdatePosition) + { + // Arrange + MockWebMFileReader mockReader; + mockReader.OpenFile("test.webm"); + mockReader.SelectVideoTrack(1); + + // Act + bool result = mockReader.SeekToFrame(50); + + // Assert + Assert::IsTrue(result); + Assert::AreEqual(50u, static_cast(mockReader.GetCurrentFrameIndex())); + } + + TEST_METHOD(MockWebMFileReader_SeekToTime_ValidTime_ShouldUpdatePosition) + { + // Arrange + MockWebMFileReader mockReader; + mockReader.OpenFile("test.webm"); + mockReader.SelectVideoTrack(1); + + // Act (Seek to 1 second = frame 30 at 30fps) + bool result = mockReader.SeekToTime(1.0); + + // Assert + Assert::IsTrue(result); + Assert::AreEqual(30u, static_cast(mockReader.GetCurrentFrameIndex())); + } + + TEST_METHOD(MockWebMFileReader_Reset_ShouldReturnToBeginning) + { + // Arrange + MockWebMFileReader mockReader; + mockReader.OpenFile("test.webm"); + mockReader.SelectVideoTrack(1); + mockReader.SeekToFrame(50); + + // Act + bool result = mockReader.Reset(); + + // Assert + Assert::IsTrue(result); + Assert::AreEqual(0u, static_cast(mockReader.GetCurrentFrameIndex())); + } + + TEST_METHOD(MockWebMFileReader_GetVideoMetadata_ShouldReturnValidData) + { + // Arrange + MockWebMFileReader mockReader; + mockReader.OpenFile("test.webm"); + + // Act + const VideoMetadata& metadata = mockReader.GetVideoMetadata(); + + // Assert + Assert::AreEqual(1920u, metadata.width); + Assert::AreEqual(1080u, metadata.height); + Assert::AreEqual(30.0, metadata.frame_rate, 0.01); + Assert::AreEqual(VideoCodecType::AV1, metadata.codec_type); + Assert::AreEqual(ColorSpace::BT709, metadata.color_space); + Assert::AreEqual(100u, static_cast(metadata.total_frames)); + } + + TEST_METHOD(MockWebMFileReader_EndOfFile_ShouldDetectCorrectly) + { + // Arrange + MockWebMFileReader mockReader; + mockReader.SetEndOfFileAtFrame(3); // Set EOF at frame 3 + mockReader.OpenFile("test.webm"); + mockReader.SelectVideoTrack(1); + + // Act & Assert + for (int i = 0; i < 3; ++i) { + VideoPacket packet; + bool result = mockReader.ReadNextPacket(packet); + Assert::IsTrue(result); + Assert::IsFalse(mockReader.IsEndOfFile()); + } + + // Should now be at EOF + Assert::IsTrue(mockReader.IsEndOfFile()); + + // Next read should fail + VideoPacket packet; + bool result = mockReader.ReadNextPacket(packet); + Assert::IsFalse(result); + } + }; +} \ No newline at end of file diff --git a/vav2/Vav2Player/Vav2Player/unit-test/pch.h b/vav2/Vav2Player/Vav2Player/unit-test/pch.h index 588452b..8a421dc 100644 --- a/vav2/Vav2Player/Vav2Player/unit-test/pch.h +++ b/vav2/Vav2Player/Vav2Player/unit-test/pch.h @@ -32,4 +32,53 @@ extern "C" { // Project headers (common types and interfaces) #include "../src/Common/VideoTypes.h" -#include "../src/Decoder/IVideoDecoder.h" \ No newline at end of file +#include "../src/Decoder/IVideoDecoder.h" +#include "../src/FileIO/IWebMFileReader.h" + +// ToString specializations for custom types +namespace Microsoft::VisualStudio::CppUnitTestFramework +{ + template<> inline std::wstring ToString(const Vav2Player::WebMErrorCode& t) + { + switch (t) { + case Vav2Player::WebMErrorCode::Success: return L"Success"; + case Vav2Player::WebMErrorCode::FileNotFound: return L"FileNotFound"; + case Vav2Player::WebMErrorCode::InvalidFormat: return L"InvalidFormat"; + case Vav2Player::WebMErrorCode::UnsupportedCodec: return L"UnsupportedCodec"; + case Vav2Player::WebMErrorCode::NoVideoTrack: return L"NoVideoTrack"; + case Vav2Player::WebMErrorCode::ReadError: return L"ReadError"; + case Vav2Player::WebMErrorCode::SeekError: return L"SeekError"; + case Vav2Player::WebMErrorCode::FileNotOpen: return L"FileNotOpen"; + case Vav2Player::WebMErrorCode::InvalidTrack: return L"InvalidTrack"; + case Vav2Player::WebMErrorCode::SeekFailed: return L"SeekFailed"; + case Vav2Player::WebMErrorCode::Unknown: return L"Unknown"; + default: return L"Unknown"; + } + } + + template<> inline std::wstring ToString(const Vav2Player::VideoCodecType& t) + { + switch (t) { + case Vav2Player::VideoCodecType::AV1: return L"AV1"; + case Vav2Player::VideoCodecType::VP9: return L"VP9"; + case Vav2Player::VideoCodecType::VP8: return L"VP8"; + case Vav2Player::VideoCodecType::H264: return L"H264"; + case Vav2Player::VideoCodecType::H265: return L"H265"; + default: return L"Unknown"; + } + } + + template<> inline std::wstring ToString(const Vav2Player::ColorSpace& t) + { + switch (t) { + case Vav2Player::ColorSpace::YUV420P: return L"YUV420P"; + case Vav2Player::ColorSpace::YUV422P: return L"YUV422P"; + case Vav2Player::ColorSpace::YUV444P: return L"YUV444P"; + case Vav2Player::ColorSpace::RGB24: return L"RGB24"; + case Vav2Player::ColorSpace::RGB32: return L"RGB32"; + case Vav2Player::ColorSpace::BT709: return L"BT709"; + case Vav2Player::ColorSpace::BT2020: return L"BT2020"; + default: return L"Unknown"; + } + } +} \ No newline at end of file