Build unit test environment

This commit is contained in:
2025-09-23 05:52:19 +09:00
parent 0756849f6d
commit 03de610304
22 changed files with 2697 additions and 133 deletions

View File

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

View File

@@ -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 <string>
#include <vector>
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<VideoTrackInfo> 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 <windows.h>
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 <vector>
#include <string>
namespace Vav2Player {
class MockWebMFileReader : public IWebMFileReader {
private:
bool m_openFileResult = true;
bool m_isFileOpen = false;
VideoMetadata m_metadata;
std::vector<VideoPacket> 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<VideoTrackInfo> GetVideoTracks() const override {
std::vector<VideoTrackInfo> 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<size_t>(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<WebMFileReader>();
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
<!-- Vav2UnitTest.vcxproj에 추가할 항목들 -->
<ClCompile Include="unit-test\WebMFileReaderTest.cpp" />
<ClCompile Include="unit-test\AV1DecoderTest.cpp" />
<ClCompile Include="unit-test\VideoDecoderFactoryTest.cpp" />
<ClCompile Include="unit-test\SimpleGPURendererTest.cpp" />
<ClCompile Include="unit-test\IntegrationTest.cpp" />
```
---
## ⚙️ **구현 상세**
### **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 (구식)*

View File

@@ -63,7 +63,7 @@
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<AdditionalIncludeDirectories>$(ProjectDir)unit-test;$(ProjectDir)src;D:\Project\video-av1\include\libwebm;D:\Project\video-av1\include\dav1d;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>WIN32;_DEBUG;UNIT_TEST_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<UseFullPaths>true</UseFullPaths>
<PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile>
<LanguageStandard>stdcpp17</LanguageStandard>
@@ -71,7 +71,7 @@
<Link>
<SubSystem>Windows</SubSystem>
<AdditionalLibraryDirectories>D:\Project\video-av1\lib\libwebm;D:\Project\video-av1\lib\dav1d;$(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>webm.lib;dav1d.lib;mfplat.lib;mfuuid.lib;d3d12.lib;dxgi.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>webm-debug.lib;dav1d-debug.lib;mfplat.lib;mfuuid.lib;d3d12.lib;dxgi.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
@@ -83,7 +83,7 @@
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<AdditionalIncludeDirectories>$(ProjectDir)unit-test;$(ProjectDir)src;D:\Project\video-av1\include\libwebm;D:\Project\video-av1\include\dav1d;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PreprocessorDefinitions>WIN32;NDEBUG;UNIT_TEST_BUILD;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<UseFullPaths>true</UseFullPaths>
<PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile>
<LanguageStandard>stdcpp17</LanguageStandard>
@@ -98,6 +98,8 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="unit-test\pch.h" />
<ClInclude Include="unit-test\MockWebMFileReader.h" />
<ClInclude Include="unit-test\MockVideoRenderer.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="unit-test\pch.cpp">
@@ -105,20 +107,30 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="unit-test\VideoTypesTest.cpp" />
<ClCompile Include="unit-test\MockWebMFileReader.cpp" />
<ClCompile Include="unit-test\MockVideoRenderer.cpp" />
<ClCompile Include="unit-test\WebMFileReaderTest.cpp" />
<ClCompile Include="unit-test\AV1DecoderTest.cpp" />
<ClCompile Include="unit-test\VideoRendererTest.cpp" />
<ClCompile Include="unit-test\VideoPlayerControlTest.cpp" />
</ItemGroup>
<!-- Common source files (shared with main project) -->
<!-- Only include basic files for unit testing -->
<!-- Include files needed for Unit Testing -->
<ItemGroup Label="Common Sources">
<!-- Basic utility classes only -->
<ClCompile Include="src\Common\FramePool.cpp" />
<ClCompile Include="src\Common\PacketPool.cpp" />
<!-- Skip all complex components with D3D12/WinUI dependencies for now -->
<!-- <ClCompile Include="src\Common\PermissionUtils.cpp" /> -->
<!-- <ClCompile Include="src\Decoder\VideoDecoderFactory.cpp" /> -->
<!-- <ClCompile Include="src\FileIO\WebMFileReader.cpp" /> -->
<!-- <ClCompile Include="src\Output\FileOutput.cpp" /> -->
<!-- Core components for testing -->
<ClCompile Include="src\Decoder\VideoDecoderFactory.cpp" />
<ClCompile Include="src\Decoder\AV1Decoder.cpp" />
<ClCompile Include="src\Decoder\MediaFoundationAV1Decoder.cpp" />
<ClCompile Include="src\FileIO\WebMFileReader.cpp" />
<!-- Skip utility classes that don't exist yet -->
<!-- <ClCompile Include="src\Common\FramePool.cpp" /> -->
<!-- <ClCompile Include="src\Common\PacketPool.cpp" /> -->
<!-- Skip complex pipeline components with WinUI dependencies for now -->
<!-- <ClCompile Include="src\Rendering\SimpleGPURenderer.cpp" /> -->
<!-- <ClCompile Include="src\Rendering\D3D12VideoRenderer.cpp" /> -->
<!-- <ClCompile Include="src\Pipeline\DependencyScheduler.cpp" /> -->
<!-- <ClCompile Include="src\Pipeline\FrameBuffer.cpp" /> -->
<!-- <ClCompile Include="src\Pipeline\OverlappedProcessor.cpp" /> -->
<!-- <ClCompile Include="src\Pipeline\StreamingPipeline.cpp" /> -->
</ItemGroup>

View File

@@ -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

View File

@@ -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 <memory>
#include <string>
@@ -58,13 +60,13 @@ namespace winrt::Vav2Player::implementation
private:
// Video processing components
std::unique_ptr<WebMFileReader> m_fileReader;
std::unique_ptr<IWebMFileReader> m_fileReader;
std::unique_ptr<IVideoDecoder> m_decoder;
// Video rendering components
winrt::Microsoft::UI::Xaml::Media::Imaging::WriteableBitmap m_renderBitmap{ nullptr };
std::vector<uint8_t> m_bgraBuffer;
std::unique_ptr<SimpleGPURenderer> m_gpuRenderer;
std::unique_ptr<IVideoRenderer> m_gpuRenderer;
bool m_useHardwareRendering = true; // Default to GPU pipeline
// Playback timer for continuous frame processing
winrt::Microsoft::UI::Xaml::DispatcherTimer m_playbackTimer;

View File

@@ -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;
}

View File

@@ -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 데이터 (각 플레인별)

View File

@@ -0,0 +1,77 @@
#pragma once
#include "../Common/VideoTypes.h"
#include <string>
#include <vector>
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<VideoTrackInfo> 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

View File

@@ -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<MkvReader>()) {}
@@ -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::VideoTrackInfo> WebMFileReader::GetVideoTracks() const {
std::vector<VideoTrackInfo> WebMFileReader::GetVideoTracks() const {
return m_state ? m_state->video_tracks : std::vector<VideoTrackInfo>();
}
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<std::mutex> 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<std::mutex> 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";
}
}

View File

@@ -1,5 +1,6 @@
#pragma once
#include "../Common/VideoTypes.h"
#include "IWebMFileReader.h"
#include <mkvparser.hpp>
#include <string>
#include <memory>
@@ -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<VideoTrackInfo> GetVideoTracks() const override;
bool SelectVideoTrack(uint64_t track_number) override;
uint64_t GetSelectedTrackNumber() const override;
std::vector<VideoTrackInfo> 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);

View File

@@ -0,0 +1,45 @@
#pragma once
#include "../Common/VideoTypes.h"
#include <windows.h>
// Conditional WinUI includes for unit testing compatibility
#ifndef UNIT_TEST_BUILD
#include <winrt/Microsoft.UI.Xaml.Controls.h>
#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

View File

@@ -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

View File

@@ -8,6 +8,7 @@
#include <winrt/Microsoft.UI.Xaml.Controls.h>
#include <microsoft.ui.xaml.media.dxinterop.h>
#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();

View File

@@ -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
}
};
}

View File

@@ -0,0 +1,198 @@
#include "pch.h"
#include "MockVideoRenderer.h"
#include <chrono>
#include <thread>
#include <cstring>
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<size_t>(source.y_stride) * source.height;
dest.y_plane = std::make_unique<uint8_t[]>(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<size_t>(source.u_stride) * (source.height / 2); // YUV420 assumption
dest.u_plane = std::make_unique<uint8_t[]>(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<size_t>(source.v_stride) * (source.height / 2); // YUV420 assumption
dest.v_plane = std::make_unique<uint8_t[]>(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<std::chrono::milliseconds>(end - start);
m_totalRenderTimeMs += static_cast<uint64_t>(duration.count());
}
}
} // namespace Vav2PlayerUnitTests

View File

@@ -0,0 +1,98 @@
#pragma once
#include "../src/Rendering/IVideoRenderer.h"
#include <vector>
#include <memory>
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<Vav2Player::VideoFrame>& GetRenderedFrames() const { return m_renderedFrames; }
uint32_t GetTotalRenderedFrames() const { return static_cast<uint32_t>(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<Vav2Player::VideoFrame> m_renderedFrames;
uint64_t m_totalRenderTimeMs = 0;
// Helper methods
void CopyFrame(const Vav2Player::VideoFrame& source, Vav2Player::VideoFrame& dest);
void SimulateRenderDelay();
};
} // namespace Vav2PlayerUnitTests

View File

@@ -0,0 +1,247 @@
#include "pch.h"
#include "MockWebMFileReader.h"
#include <cstring>
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<Vav2Player::VideoTrackInfo> 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<uint8_t[]>(mockData.size());
packet.size = mockData.size();
std::memcpy(packet.data.get(), mockData.data(), mockData.size());
packet.timestamp_seconds = static_cast<double>(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<uint64_t>(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<double>(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<Vav2Player::VideoTrackInfo>& tracks) {
m_mockTracks = tracks;
}
void MockWebMFileReader::SetMockPackets(const std::vector<std::vector<uint8_t>>& packets) {
m_mockPackets = packets;
// Update metadata to match packet count
m_mockMetadata.total_frames = packets.size();
m_mockMetadata.duration_seconds = static_cast<double>(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<uint8_t> 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<uint8_t> 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<uint8_t>(frame_size & 0x7F));
packet.push_back(static_cast<uint8_t>((frame_size >> 7) & 0x7F));
// Fill with mock data
for (uint32_t i = 0; i < frame_size; ++i) {
packet.push_back(static_cast<uint8_t>((frame_index + i) & 0xFF));
}
return packet;
}
} // namespace Vav2PlayerUnitTests

View File

@@ -0,0 +1,84 @@
#pragma once
#include "../src/FileIO/IWebMFileReader.h"
#include <vector>
#include <string>
#include <memory>
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<Vav2Player::VideoTrackInfo> 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<Vav2Player::VideoTrackInfo>& tracks);
void SetMockPackets(const std::vector<std::vector<uint8_t>>& 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<Vav2Player::VideoTrackInfo> m_mockTracks;
std::vector<std::vector<uint8_t>> 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<uint8_t> CreateMockAV1Packet(uint64_t frame_index);
};
} // namespace Vav2PlayerUnitTests

View File

@@ -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<MockWebMFileReader>();
auto mockRenderer = std::make_unique<MockVideoRenderer>();
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<MockWebMFileReader>();
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<MockVideoRenderer>();
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<MockWebMFileReader>();
auto mockRenderer = std::make_unique<MockVideoRenderer>();
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<uint64_t>(packet.timestamp_seconds * 1000000000.0);
mockRenderer->TryRenderFrame(frame);
}
}
// Assert
Assert::AreEqual(10u, mockReader->GetReadPacketCallCount());
Assert::AreEqual(10u, mockRenderer->GetTryRenderFrameCallCount());
Assert::AreEqual(static_cast<uint64_t>(10u), mockReader->GetCurrentFrameIndex());
Assert::AreEqual(10u, mockRenderer->GetTotalRenderedFrames());
}
TEST_METHOD(SeekOperation_ShouldUpdateReaderState)
{
// Arrange
auto mockReader = std::make_unique<MockWebMFileReader>();
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<uint32_t>(mockReader->GetCurrentFrameIndex()));
// Next packet should be frame 50
VideoPacket packet;
bool readResult = mockReader->ReadNextPacket(packet);
Assert::IsTrue(readResult);
Assert::AreEqual(50u, static_cast<uint32_t>(packet.frame_index));
}
TEST_METHOD(AspectRatioHandling_ShouldUpdateRenderer)
{
// Arrange
auto mockRenderer = std::make_unique<MockVideoRenderer>();
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<MockWebMFileReader>();
auto mockRenderer = std::make_unique<MockVideoRenderer>();
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<MockVideoRenderer>();
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<std::chrono::milliseconds>(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<IWebMFileReader> reader = std::make_unique<MockWebMFileReader>();
std::unique_ptr<IVideoRenderer> renderer = std::make_unique<MockVideoRenderer>();
// 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());
}
};
}

View File

@@ -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<uint8_t[]>(y_size);
testFrame.u_plane = std::make_unique<uint8_t[]>(uv_size);
testFrame.v_plane = std::make_unique<uint8_t[]>(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<uint32_t>(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<std::chrono::milliseconds>(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<uint32_t>(renderedFrames.size()));
// Verify frame indices are correct
for (uint32_t i = 0; i < 5; ++i) {
Assert::AreEqual(static_cast<uint64_t>(i), renderedFrames[i].frame_index);
}
}
};
}

View File

@@ -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<uint32_t>(tracks.size()));
Assert::AreEqual(1u, static_cast<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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);
}
};
}

View File

@@ -32,4 +32,53 @@ extern "C" {
// Project headers (common types and interfaces)
#include "../src/Common/VideoTypes.h"
#include "../src/Decoder/IVideoDecoder.h"
#include "../src/Decoder/IVideoDecoder.h"
#include "../src/FileIO/IWebMFileReader.h"
// ToString specializations for custom types
namespace Microsoft::VisualStudio::CppUnitTestFramework
{
template<> inline std::wstring ToString<Vav2Player::WebMErrorCode>(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<Vav2Player::VideoCodecType>(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<Vav2Player::ColorSpace>(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";
}
}
}