571 lines
20 KiB
C++
571 lines
20 KiB
C++
#include "pch.h"
|
|
#include "VideoPlayerControl2.xaml.h"
|
|
#if __has_include("VideoPlayerControl2.g.cpp")
|
|
#include "VideoPlayerControl2.g.cpp"
|
|
#endif
|
|
|
|
#include <winrt/Microsoft.UI.Dispatching.h>
|
|
#include <chrono>
|
|
#include <algorithm>
|
|
|
|
// D3D12 for GPU surface decoding
|
|
#include <d3d12.h>
|
|
#include <wrl/client.h>
|
|
using Microsoft::WRL::ComPtr;
|
|
|
|
// Include log manager for logging
|
|
#include "src/Logger/LogManager.h"
|
|
|
|
// Using alias to avoid namespace conflicts
|
|
using LogMgr = Vav2Player::LogManager;
|
|
|
|
using namespace winrt;
|
|
using namespace winrt::Microsoft::UI::Xaml;
|
|
using namespace winrt::Microsoft::UI::Xaml::Controls;
|
|
|
|
namespace winrt::Vav2Player::implementation
|
|
{
|
|
VideoPlayerControl2::VideoPlayerControl2()
|
|
{
|
|
try {
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"Constructor START - creating core components");
|
|
|
|
// Create core components
|
|
m_playbackController = std::make_unique<::Vav2Player::PlaybackController>();
|
|
m_frameProcessor = std::make_unique<::Vav2Player::FrameProcessor>();
|
|
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"Constructor END - core components created successfully");
|
|
}
|
|
catch (const std::exception& e) {
|
|
std::wstring error = L"Constructor exception: " + std::wstring(e.what(), e.what() + strlen(e.what()));
|
|
LogMgr::GetInstance().LogError(L"VideoPlayerControl2", error);
|
|
throw;
|
|
}
|
|
catch (...) {
|
|
LogMgr::GetInstance().LogError(L"VideoPlayerControl2", L"Constructor unknown exception");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
VideoPlayerControl2::~VideoPlayerControl2()
|
|
{
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"Destructor called");
|
|
|
|
// Stop playback and cleanup
|
|
if (m_playbackController) {
|
|
m_playbackController->Stop();
|
|
}
|
|
|
|
CleanupRenderer();
|
|
}
|
|
|
|
// ========================================
|
|
// XAML Event Handlers
|
|
// ========================================
|
|
|
|
void VideoPlayerControl2::UserControl_Loaded(IInspectable const&, RoutedEventArgs const&)
|
|
{
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"UserControl_Loaded");
|
|
|
|
try {
|
|
// Initialize renderer with SwapChainPanel
|
|
InitializeRenderer();
|
|
|
|
// Connect FrameProcessor to renderer and dispatcher
|
|
if (m_frameProcessor && m_gpuRenderer) {
|
|
m_frameProcessor->SetRenderer(m_gpuRenderer.get());
|
|
m_frameProcessor->SetDispatcherQueue(DispatcherQueue());
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"FrameProcessor configured");
|
|
}
|
|
|
|
m_initialized = true;
|
|
UpdateStatus(L"Ready");
|
|
}
|
|
catch (const std::exception& e) {
|
|
std::wstring error = L"Initialization failed: " + std::wstring(e.what(), e.what() + strlen(e.what()));
|
|
LogMgr::GetInstance().LogError(L"VideoPlayerControl2", error);
|
|
UpdateStatus(L"Initialization failed");
|
|
m_initialized = false; // Ensure we stay in uninitialized state on failure
|
|
}
|
|
catch (...) {
|
|
LogMgr::GetInstance().LogError(L"VideoPlayerControl2", L"Initialization failed: unknown exception");
|
|
UpdateStatus(L"Initialization failed");
|
|
m_initialized = false;
|
|
}
|
|
}
|
|
|
|
void VideoPlayerControl2::UserControl_Unloaded(IInspectable const&, RoutedEventArgs const&)
|
|
{
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"UserControl_Unloaded");
|
|
|
|
// Stop playback
|
|
if (m_playbackController) {
|
|
m_playbackController->Stop();
|
|
m_playbackController->Unload();
|
|
}
|
|
|
|
// Cleanup renderer
|
|
CleanupRenderer();
|
|
|
|
m_initialized = false;
|
|
}
|
|
|
|
void VideoPlayerControl2::UserControl_SizeChanged(IInspectable const&, SizeChangedEventArgs const& e)
|
|
{
|
|
auto newSize = e.NewSize();
|
|
auto oldSize = e.PreviousSize();
|
|
|
|
// Ignore trivial size changes (< 1 pixel) to avoid unnecessary work
|
|
if (std::abs(newSize.Width - oldSize.Width) < 1.0 &&
|
|
std::abs(newSize.Height - oldSize.Height) < 1.0) {
|
|
return;
|
|
}
|
|
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2",
|
|
L"SizeChanged: " + std::to_wstring((int)newSize.Width) + L"x" + std::to_wstring((int)newSize.Height));
|
|
|
|
// Update renderer size
|
|
if (m_gpuRenderer && m_gpuRenderer->IsInitialized()) {
|
|
HRESULT hr = m_gpuRenderer->Resize(
|
|
static_cast<uint32_t>(newSize.Width),
|
|
static_cast<uint32_t>(newSize.Height)
|
|
);
|
|
|
|
if (FAILED(hr)) {
|
|
LogMgr::GetInstance().LogError(L"VideoPlayerControl2", L"Failed to resize renderer");
|
|
}
|
|
}
|
|
|
|
// Update AspectFit if video is loaded
|
|
if (m_playbackController && m_playbackController->IsLoaded()) {
|
|
UpdateVideoImageAspectFit(
|
|
m_playbackController->GetVideoWidth(),
|
|
m_playbackController->GetVideoHeight()
|
|
);
|
|
}
|
|
}
|
|
|
|
void VideoPlayerControl2::HoverDetector_PointerEntered(IInspectable const&, Input::PointerRoutedEventArgs const&)
|
|
{
|
|
// Future: Show video controls on hover
|
|
}
|
|
|
|
void VideoPlayerControl2::HoverDetector_PointerExited(IInspectable const&, Input::PointerRoutedEventArgs const&)
|
|
{
|
|
// Future: Hide video controls
|
|
}
|
|
|
|
// ========================================
|
|
// Public Properties
|
|
// ========================================
|
|
|
|
winrt::hstring VideoPlayerControl2::VideoSource()
|
|
{
|
|
return m_videoSource;
|
|
}
|
|
|
|
void VideoPlayerControl2::VideoSource(winrt::hstring const& value)
|
|
{
|
|
if (m_videoSource != value) {
|
|
m_videoSource = value;
|
|
|
|
if (!value.empty()) {
|
|
if (m_initialized) {
|
|
LoadVideo(value);
|
|
} else {
|
|
LogMgr::GetInstance().LogWarning(L"VideoPlayerControl2",
|
|
L"VideoSource set before initialization, will load on Loaded event");
|
|
}
|
|
} else {
|
|
// Empty source - unload current video
|
|
if (m_playbackController && m_playbackController->IsLoaded()) {
|
|
Stop();
|
|
m_playbackController->Unload();
|
|
UpdateStatus(L"No video");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool VideoPlayerControl2::ShowControls()
|
|
{
|
|
return m_showControls;
|
|
}
|
|
|
|
void VideoPlayerControl2::ShowControls(bool value)
|
|
{
|
|
m_showControls = value;
|
|
}
|
|
|
|
bool VideoPlayerControl2::AutoPlay()
|
|
{
|
|
return m_autoPlay;
|
|
}
|
|
|
|
void VideoPlayerControl2::AutoPlay(bool value)
|
|
{
|
|
m_autoPlay = value;
|
|
}
|
|
|
|
Vav2Player::VideoDecoderType VideoPlayerControl2::DecoderType()
|
|
{
|
|
if (!m_playbackController) {
|
|
return static_cast<Vav2Player::VideoDecoderType>(0); // Auto
|
|
}
|
|
|
|
VavCoreDecoderType coreType = m_playbackController->GetDecoderType();
|
|
|
|
// Convert VavCoreDecoderType to VideoDecoderType
|
|
// Use switch on C enum (compile-time constant), return WinRT enum as integer
|
|
// WinRT enum values: Auto=0, NVDEC=1, VPL=2, AMF=3, DAV1D=4, MediaFoundation=5
|
|
switch (coreType) {
|
|
case VAVCORE_DECODER_NVDEC:
|
|
return static_cast<Vav2Player::VideoDecoderType>(1); // NVDEC
|
|
case VAVCORE_DECODER_VPL:
|
|
return static_cast<Vav2Player::VideoDecoderType>(2); // VPL
|
|
case VAVCORE_DECODER_AMF:
|
|
return static_cast<Vav2Player::VideoDecoderType>(3); // AMF
|
|
case VAVCORE_DECODER_DAV1D:
|
|
return static_cast<Vav2Player::VideoDecoderType>(4); // DAV1D
|
|
case VAVCORE_DECODER_MEDIA_FOUNDATION:
|
|
return static_cast<Vav2Player::VideoDecoderType>(5); // MediaFoundation
|
|
default:
|
|
return static_cast<Vav2Player::VideoDecoderType>(0); // Auto
|
|
}
|
|
}
|
|
|
|
void VideoPlayerControl2::DecoderType(Vav2Player::VideoDecoderType value)
|
|
{
|
|
if (!m_playbackController) {
|
|
return;
|
|
}
|
|
|
|
// Convert VideoDecoderType to VavCoreDecoderType
|
|
// WinRT enums are projected as integers: Auto=0, NVDEC=1, VPL=2, AMF=3, DAV1D=4, MediaFoundation=5
|
|
VavCoreDecoderType coreType = VAVCORE_DECODER_AUTO;
|
|
int32_t enumValue = static_cast<int32_t>(value);
|
|
|
|
if (enumValue == 1) { // NVDEC
|
|
coreType = VAVCORE_DECODER_NVDEC;
|
|
} else if (enumValue == 2) { // VPL
|
|
coreType = VAVCORE_DECODER_VPL;
|
|
} else if (enumValue == 3) { // AMF
|
|
coreType = VAVCORE_DECODER_AMF;
|
|
} else if (enumValue == 4) { // DAV1D
|
|
coreType = VAVCORE_DECODER_DAV1D;
|
|
} else if (enumValue == 5) { // MediaFoundation
|
|
coreType = VAVCORE_DECODER_MEDIA_FOUNDATION;
|
|
}
|
|
|
|
m_playbackController->SetDecoderType(coreType);
|
|
}
|
|
|
|
// ========================================
|
|
// Public Methods
|
|
// ========================================
|
|
|
|
void VideoPlayerControl2::LoadVideo(winrt::hstring const& filePath)
|
|
{
|
|
if (!m_initialized) {
|
|
LogMgr::GetInstance().LogError(L"VideoPlayerControl2", L"Cannot load video: not initialized");
|
|
return;
|
|
}
|
|
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"Loading video: " + std::wstring(filePath));
|
|
UpdateStatus(L"Loading...");
|
|
|
|
// Stop current playback
|
|
if (m_playbackController->IsPlaying()) {
|
|
m_playbackController->Stop();
|
|
}
|
|
|
|
// --- Start of Corrected Initialization Order ---
|
|
|
|
// 1. Pass D3D12 device to the PlaybackController (with null check)
|
|
if (m_gpuRenderer) {
|
|
ID3D12Device* d3dDevice = m_gpuRenderer->GetD3D12Device();
|
|
if (d3dDevice) {
|
|
m_playbackController->SetD3DDevice(d3dDevice, VAVCORE_SURFACE_D3D12_RESOURCE);
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"D3D12 device will be set by PlaybackController.");
|
|
} else {
|
|
LogMgr::GetInstance().LogWarning(L"VideoPlayerControl2", L"D3D12 device is null");
|
|
}
|
|
} else {
|
|
LogMgr::GetInstance().LogWarning(L"VideoPlayerControl2",
|
|
L"Cannot set D3D device: renderer not initialized");
|
|
}
|
|
|
|
// 2. Load the video file. VavCore will now see the pending D3D device
|
|
// and initialize the decoder in the correct GPU-accelerated mode.
|
|
std::wstring filePathStr(filePath.c_str());
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"About to call LoadVideo with path: " + std::wstring(filePath));
|
|
|
|
// Safety check: Verify m_playbackController is valid
|
|
if (!m_playbackController) {
|
|
LogMgr::GetInstance().LogError(L"VideoPlayerControl2", L"CRITICAL: m_playbackController is nullptr!");
|
|
UpdateStatus(L"Load failed - internal error");
|
|
return;
|
|
}
|
|
|
|
bool success = m_playbackController->LoadVideo(filePathStr);
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2",
|
|
success ? L"LoadVideo returned TRUE" : L"LoadVideo returned FALSE");
|
|
|
|
if (success) {
|
|
// 3. Get video dimensions (which are now available).
|
|
uint32_t videoWidth = m_playbackController->GetVideoWidth();
|
|
uint32_t videoHeight = m_playbackController->GetVideoHeight();
|
|
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2",
|
|
L"Video loaded: " + std::to_wstring(videoWidth) + L"x" + std::to_wstring(videoHeight));
|
|
|
|
// Prepare video texture before decoding (via FrameProcessor)
|
|
if (m_frameProcessor) {
|
|
m_frameProcessor->PrepareVideoTexture(videoWidth, videoHeight);
|
|
}
|
|
|
|
// Update AspectFit
|
|
UpdateVideoImageAspectFit(videoWidth, videoHeight);
|
|
|
|
UpdateStatus(L"Loaded");
|
|
|
|
// Auto-play if enabled
|
|
if (m_autoPlay) {
|
|
Play();
|
|
}
|
|
} else {
|
|
LogMgr::GetInstance().LogError(L"VideoPlayerControl2", L"Failed to load video");
|
|
UpdateStatus(L"Load failed");
|
|
}
|
|
}
|
|
|
|
void VideoPlayerControl2::Play()
|
|
{
|
|
if (!m_initialized || !m_playbackController->IsLoaded()) {
|
|
LogMgr::GetInstance().LogError(L"VideoPlayerControl2", L"Cannot play: video not loaded");
|
|
return;
|
|
}
|
|
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"Starting playback");
|
|
|
|
// Start playback with frame-ready callback
|
|
auto weakThis = get_weak();
|
|
m_playbackController->Play([weakThis]() {
|
|
if (auto strongThis = weakThis.get()) {
|
|
strongThis->OnFrameReady();
|
|
}
|
|
});
|
|
|
|
UpdateStatus(L"Playing");
|
|
}
|
|
|
|
void VideoPlayerControl2::Pause()
|
|
{
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"Pausing playback");
|
|
m_playbackController->Pause();
|
|
UpdateStatus(L"Paused");
|
|
}
|
|
|
|
void VideoPlayerControl2::Stop()
|
|
{
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"Stopping playback");
|
|
m_playbackController->Stop();
|
|
UpdateStatus(L"Stopped");
|
|
}
|
|
|
|
void VideoPlayerControl2::Seek(double timeSeconds)
|
|
{
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"Seeking to " + std::to_wstring(timeSeconds) + L"s");
|
|
m_playbackController->Seek(timeSeconds);
|
|
}
|
|
|
|
// ========================================
|
|
// Status Queries
|
|
// ========================================
|
|
|
|
bool VideoPlayerControl2::IsVideoPlaying()
|
|
{
|
|
return m_playbackController->IsPlaying();
|
|
}
|
|
|
|
bool VideoPlayerControl2::IsVideoLoaded()
|
|
{
|
|
return m_playbackController->IsLoaded();
|
|
}
|
|
|
|
double VideoPlayerControl2::CurrentTime()
|
|
{
|
|
return m_playbackController->GetCurrentTime();
|
|
}
|
|
|
|
double VideoPlayerControl2::Duration()
|
|
{
|
|
return m_playbackController->GetDuration();
|
|
}
|
|
|
|
winrt::hstring VideoPlayerControl2::Status()
|
|
{
|
|
return m_status;
|
|
}
|
|
|
|
// ========================================
|
|
// Private Helper Methods
|
|
// ========================================
|
|
|
|
void VideoPlayerControl2::InitializeRenderer()
|
|
{
|
|
// Cleanup existing renderer if not properly initialized
|
|
if (m_gpuRenderer) {
|
|
if (m_gpuRenderer->IsInitialized()) {
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"Renderer already initialized");
|
|
return;
|
|
} else {
|
|
LogMgr::GetInstance().LogWarning(L"VideoPlayerControl2",
|
|
L"Cleaning up partially initialized renderer");
|
|
CleanupRenderer();
|
|
}
|
|
}
|
|
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"Initializing renderer");
|
|
|
|
m_gpuRenderer = std::make_unique<::Vav2Player::D3D12VideoRenderer>();
|
|
|
|
// Get SwapChainPanel size
|
|
auto panelSize = VideoSwapChainPanel().ActualSize();
|
|
uint32_t width = static_cast<uint32_t>(panelSize.x);
|
|
uint32_t height = static_cast<uint32_t>(panelSize.y);
|
|
|
|
// Initialize with SwapChainPanel
|
|
HRESULT hr = m_gpuRenderer->InitializeWithSwapChain(
|
|
VideoSwapChainPanel(),
|
|
width > 0 ? width : 1920,
|
|
height > 0 ? height : 1080
|
|
);
|
|
|
|
if (SUCCEEDED(hr)) {
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"Renderer initialized successfully");
|
|
} else {
|
|
LogMgr::GetInstance().LogError(L"VideoPlayerControl2", L"Failed to initialize renderer");
|
|
m_gpuRenderer.reset();
|
|
}
|
|
}
|
|
|
|
void VideoPlayerControl2::CleanupRenderer()
|
|
{
|
|
if (m_gpuRenderer) {
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2", L"Cleaning up renderer");
|
|
m_gpuRenderer->Shutdown();
|
|
m_gpuRenderer.reset();
|
|
}
|
|
}
|
|
|
|
void VideoPlayerControl2::UpdateStatus(winrt::hstring const& message)
|
|
{
|
|
m_status = message;
|
|
|
|
// Update UI on UI thread
|
|
DispatcherQueue().TryEnqueue([weakThis = get_weak(), message]() {
|
|
if (auto strongThis = weakThis.get()) {
|
|
strongThis->StatusText().Text(message);
|
|
strongThis->StatusOverlay().Visibility(Visibility::Visible);
|
|
}
|
|
});
|
|
}
|
|
|
|
void VideoPlayerControl2::UpdateVideoImageAspectFit(int videoWidth, int videoHeight)
|
|
{
|
|
if (videoWidth == 0 || videoHeight == 0) {
|
|
return;
|
|
}
|
|
|
|
// Get container size
|
|
auto containerSize = VideoSwapChainPanel().ActualSize();
|
|
double containerWidth = containerSize.x;
|
|
double containerHeight = containerSize.y;
|
|
|
|
if (containerWidth == 0 || containerHeight == 0) {
|
|
return;
|
|
}
|
|
|
|
// Calculate AspectFit dimensions
|
|
double videoAspectRatio = static_cast<double>(videoWidth) / videoHeight;
|
|
double containerAspectRatio = containerWidth / containerHeight;
|
|
|
|
double displayWidth, displayHeight;
|
|
|
|
if (videoAspectRatio > containerAspectRatio) {
|
|
// Video is wider - fit to container width
|
|
displayWidth = containerWidth;
|
|
displayHeight = containerWidth / videoAspectRatio;
|
|
} else {
|
|
// Video is taller - fit to container height
|
|
displayHeight = containerHeight;
|
|
displayWidth = containerHeight * videoAspectRatio;
|
|
}
|
|
|
|
// Update SwapChainPanel size
|
|
VideoSwapChainPanel().Width(displayWidth);
|
|
VideoSwapChainPanel().Height(displayHeight);
|
|
|
|
LogMgr::GetInstance().LogInfo(L"VideoPlayerControl2",
|
|
L"AspectFit: " + std::to_wstring((int)displayWidth) + L"x" + std::to_wstring((int)displayHeight));
|
|
}
|
|
|
|
void VideoPlayerControl2::OnFrameReady()
|
|
{
|
|
// Called from PlaybackController timing thread (30fps)
|
|
// Track consecutive decode errors for auto-recovery (function-scope static)
|
|
static int consecutiveErrors = 0;
|
|
|
|
// Thread-safe: Check if still initialized to prevent race conditions during cleanup
|
|
if (!m_initialized) {
|
|
return;
|
|
}
|
|
|
|
// Defensive: Get raw pointers atomically to avoid race with destruction
|
|
auto processor = m_frameProcessor.get();
|
|
auto controller = m_playbackController.get();
|
|
|
|
if (!processor || !controller) {
|
|
return;
|
|
}
|
|
|
|
// Process frame (decode on background, render on UI thread)
|
|
VavCorePlayer* player = controller->GetVavCorePlayer();
|
|
if (!player) {
|
|
return;
|
|
}
|
|
|
|
processor->ProcessFrame(player, [weakThis = get_weak()](bool success) {
|
|
// Static variable is already in function scope, no need to capture
|
|
if (auto strongThis = weakThis.get()) {
|
|
if (!success) {
|
|
consecutiveErrors++;
|
|
LogMgr::GetInstance().LogWarning(L"VideoPlayerControl2",
|
|
L"Frame processing failed (consecutive errors: " + std::to_wstring(consecutiveErrors) + L")");
|
|
|
|
// Auto-recovery after too many consecutive errors
|
|
if (consecutiveErrors > 10) {
|
|
LogMgr::GetInstance().LogError(L"VideoPlayerControl2",
|
|
L"Too many consecutive errors, stopping playback");
|
|
|
|
// Stop playback on UI thread
|
|
strongThis->DispatcherQueue().TryEnqueue([weakThis]() {
|
|
if (auto strongThis2 = weakThis.get()) {
|
|
strongThis2->Stop();
|
|
strongThis2->UpdateStatus(L"Playback error - stopped");
|
|
}
|
|
});
|
|
|
|
consecutiveErrors = 0;
|
|
}
|
|
} else {
|
|
// Reset error counter on success
|
|
consecutiveErrors = 0;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
} // namespace winrt::Vav2Player::implementation
|