diff --git a/CMakeLists.txt b/CMakeLists.txt index 549c3b80..2472b75e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,8 @@ find_package(GLEW REQUIRED) find_package(OpenSSL REQUIRED) find_package(Threads REQUIRED) find_package(SQLite3 REQUIRED) +find_package(PkgConfig REQUIRED) +pkg_check_modules(FFMPEG REQUIRED libavformat libavcodec libswscale libavutil) # GLM (header-only math library) find_package(glm QUIET) @@ -139,6 +141,7 @@ set(WOWEE_SOURCES src/rendering/world_map.cpp src/rendering/swim_effects.cpp src/rendering/loading_screen.cpp + src/rendering/video_player.cpp # UI src/ui/ui_manager.cpp @@ -226,6 +229,7 @@ set(WOWEE_HEADERS include/rendering/character_preview.hpp include/rendering/wmo_renderer.hpp include/rendering/loading_screen.hpp + include/rendering/video_player.hpp include/ui/ui_manager.hpp include/ui/auth_screen.hpp @@ -245,6 +249,7 @@ target_include_directories(wowee PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/extern + ${FFMPEG_INCLUDE_DIRS} ) # Link libraries @@ -265,6 +270,11 @@ else() target_link_libraries(wowee PRIVATE ${SQLite3_LIBRARIES}) endif() +target_link_libraries(wowee PRIVATE ${FFMPEG_LIBRARIES}) +if (FFMPEG_LIBRARY_DIRS) + target_link_directories(wowee PRIVATE ${FFMPEG_LIBRARY_DIRS}) +endif() + # Platform-specific libraries if(WIN32) target_link_libraries(wowee PRIVATE ws2_32) diff --git a/assets/startscreen.mp4 b/assets/startscreen.mp4 new file mode 100755 index 00000000..3faea727 Binary files /dev/null and b/assets/startscreen.mp4 differ diff --git a/include/rendering/video_player.hpp b/include/rendering/video_player.hpp new file mode 100644 index 00000000..3d093715 --- /dev/null +++ b/include/rendering/video_player.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include + +typedef unsigned int GLuint; + +namespace wowee { +namespace rendering { + +class VideoPlayer { +public: + VideoPlayer(); + ~VideoPlayer(); + + bool open(const std::string& path); + void update(float deltaTime); + void close(); + + bool isReady() const { return textureReady; } + GLuint getTextureId() const { return textureId; } + int getWidth() const { return width; } + int getHeight() const { return height; } + +private: + bool decodeNextFrame(); + void uploadFrame(); + + void* formatCtx = nullptr; + void* codecCtx = nullptr; + void* frame = nullptr; + void* rgbFrame = nullptr; + void* packet = nullptr; + void* swsCtx = nullptr; + + int videoStreamIndex = -1; + int width = 0; + int height = 0; + double frameTime = 1.0 / 30.0; + double accumulator = 0.0; + bool eof = false; + + GLuint textureId = 0; + bool textureReady = false; + std::string sourcePath; + std::vector rgbBuffer; +}; + +} // namespace rendering +} // namespace wowee diff --git a/include/ui/auth_screen.hpp b/include/ui/auth_screen.hpp index 1d94c787..d519a2d6 100644 --- a/include/ui/auth_screen.hpp +++ b/include/ui/auth_screen.hpp @@ -1,6 +1,7 @@ #pragma once #include "auth/auth_handler.hpp" +#include "rendering/video_player.hpp" #include #include @@ -83,6 +84,10 @@ private: void loadLoginInfo(); static std::string getConfigPath(); bool loginInfoLoaded = false; + + // Background video + bool videoInitAttempted = false; + rendering::VideoPlayer backgroundVideo; }; }} // namespace wowee::ui diff --git a/src/rendering/video_player.cpp b/src/rendering/video_player.cpp new file mode 100644 index 00000000..c11dd799 --- /dev/null +++ b/src/rendering/video_player.cpp @@ -0,0 +1,253 @@ +#include "rendering/video_player.hpp" +#include "core/logger.hpp" +#include + +extern "C" { +#include +#include +#include +#include +} + +namespace wowee { +namespace rendering { + +VideoPlayer::VideoPlayer() = default; + +VideoPlayer::~VideoPlayer() { + close(); +} + +bool VideoPlayer::open(const std::string& path) { + if (!path.empty() && sourcePath == path && formatCtx) return true; + close(); + + sourcePath = path; + AVFormatContext* fmt = nullptr; + if (avformat_open_input(&fmt, path.c_str(), nullptr, nullptr) != 0) { + LOG_WARNING("VideoPlayer: failed to open ", path); + return false; + } + if (avformat_find_stream_info(fmt, nullptr) < 0) { + LOG_WARNING("VideoPlayer: failed to read stream info for ", path); + avformat_close_input(&fmt); + return false; + } + + int streamIndex = -1; + for (unsigned int i = 0; i < fmt->nb_streams; i++) { + if (fmt->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { + streamIndex = static_cast(i); + break; + } + } + if (streamIndex < 0) { + LOG_WARNING("VideoPlayer: no video stream in ", path); + avformat_close_input(&fmt); + return false; + } + + AVCodecParameters* codecpar = fmt->streams[streamIndex]->codecpar; + const AVCodec* codec = avcodec_find_decoder(codecpar->codec_id); + if (!codec) { + LOG_WARNING("VideoPlayer: unsupported codec for ", path); + avformat_close_input(&fmt); + return false; + } + + AVCodecContext* ctx = avcodec_alloc_context3(codec); + if (!ctx) { + avformat_close_input(&fmt); + return false; + } + if (avcodec_parameters_to_context(ctx, codecpar) < 0) { + avcodec_free_context(&ctx); + avformat_close_input(&fmt); + return false; + } + if (avcodec_open2(ctx, codec, nullptr) < 0) { + avcodec_free_context(&ctx); + avformat_close_input(&fmt); + return false; + } + + AVFrame* f = av_frame_alloc(); + AVFrame* rgb = av_frame_alloc(); + AVPacket* pkt = av_packet_alloc(); + if (!f || !rgb || !pkt) { + if (pkt) av_packet_free(&pkt); + if (rgb) av_frame_free(&rgb); + if (f) av_frame_free(&f); + avcodec_free_context(&ctx); + avformat_close_input(&fmt); + return false; + } + + width = ctx->width; + height = ctx->height; + if (width <= 0 || height <= 0) { + av_packet_free(&pkt); + av_frame_free(&rgb); + av_frame_free(&f); + avcodec_free_context(&ctx); + avformat_close_input(&fmt); + return false; + } + + int bufferSize = av_image_get_buffer_size(AV_PIX_FMT_RGB24, width, height, 1); + rgbBuffer.resize(static_cast(bufferSize)); + av_image_fill_arrays(rgb->data, rgb->linesize, + rgbBuffer.data(), AV_PIX_FMT_RGB24, width, height, 1); + + SwsContext* sws = sws_getContext(width, height, ctx->pix_fmt, + width, height, AV_PIX_FMT_RGB24, + SWS_BILINEAR, nullptr, nullptr, nullptr); + if (!sws) { + av_packet_free(&pkt); + av_frame_free(&rgb); + av_frame_free(&f); + avcodec_free_context(&ctx); + avformat_close_input(&fmt); + return false; + } + + AVRational fr = fmt->streams[streamIndex]->avg_frame_rate; + if (fr.num <= 0 || fr.den <= 0) { + fr = fmt->streams[streamIndex]->r_frame_rate; + } + double fps = (fr.num > 0 && fr.den > 0) ? static_cast(fr.num) / fr.den : 30.0; + if (fps <= 0.0) fps = 30.0; + frameTime = 1.0 / fps; + accumulator = 0.0; + eof = false; + + formatCtx = fmt; + codecCtx = ctx; + frame = f; + rgbFrame = rgb; + packet = pkt; + swsCtx = sws; + videoStreamIndex = streamIndex; + + textureReady = false; + return true; +} + +void VideoPlayer::close() { + if (textureId) { + glDeleteTextures(1, &textureId); + textureId = 0; + } + textureReady = false; + + if (packet) { + av_packet_free(reinterpret_cast(&packet)); + packet = nullptr; + } + if (rgbFrame) { + av_frame_free(reinterpret_cast(&rgbFrame)); + rgbFrame = nullptr; + } + if (frame) { + av_frame_free(reinterpret_cast(&frame)); + frame = nullptr; + } + if (codecCtx) { + avcodec_free_context(reinterpret_cast(&codecCtx)); + codecCtx = nullptr; + } + if (formatCtx) { + avformat_close_input(reinterpret_cast(&formatCtx)); + formatCtx = nullptr; + } + if (swsCtx) { + sws_freeContext(reinterpret_cast(swsCtx)); + swsCtx = nullptr; + } + videoStreamIndex = -1; + width = 0; + height = 0; + rgbBuffer.clear(); +} + +void VideoPlayer::update(float deltaTime) { + if (!formatCtx || !codecCtx) return; + accumulator += deltaTime; + while (accumulator >= frameTime) { + if (!decodeNextFrame()) break; + accumulator -= frameTime; + } +} + +bool VideoPlayer::decodeNextFrame() { + AVFormatContext* fmt = reinterpret_cast(formatCtx); + AVCodecContext* ctx = reinterpret_cast(codecCtx); + AVFrame* f = reinterpret_cast(frame); + AVFrame* rgb = reinterpret_cast(rgbFrame); + AVPacket* pkt = reinterpret_cast(packet); + SwsContext* sws = reinterpret_cast(swsCtx); + + while (true) { + int ret = av_read_frame(fmt, pkt); + if (ret < 0) { + if (av_seek_frame(fmt, videoStreamIndex, 0, AVSEEK_FLAG_BACKWARD) >= 0) { + avcodec_flush_buffers(ctx); + continue; + } + return false; + } + + if (pkt->stream_index != videoStreamIndex) { + av_packet_unref(pkt); + continue; + } + + if (avcodec_send_packet(ctx, pkt) < 0) { + av_packet_unref(pkt); + continue; + } + av_packet_unref(pkt); + + ret = avcodec_receive_frame(ctx, f); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { + continue; + } + if (ret < 0) { + continue; + } + + sws_scale(sws, + f->data, f->linesize, + 0, ctx->height, + rgb->data, rgb->linesize); + + uploadFrame(); + return true; + } +} + +void VideoPlayer::uploadFrame() { + if (width <= 0 || height <= 0) return; + if (!textureId) { + glGenTextures(1, &textureId); + glBindTexture(GL_TEXTURE_2D, textureId); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, + GL_RGB, GL_UNSIGNED_BYTE, rgbBuffer.data()); + glBindTexture(GL_TEXTURE_2D, 0); + textureReady = true; + return; + } + + glBindTexture(GL_TEXTURE_2D, textureId); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, + GL_RGB, GL_UNSIGNED_BYTE, rgbBuffer.data()); + glBindTexture(GL_TEXTURE_2D, 0); + textureReady = true; +} + +} // namespace rendering +} // namespace wowee diff --git a/src/ui/auth_screen.cpp b/src/ui/auth_screen.cpp index b619fd92..75ee8d79 100644 --- a/src/ui/auth_screen.cpp +++ b/src/ui/auth_screen.cpp @@ -38,6 +38,39 @@ void AuthScreen::render(auth::AuthHandler& authHandler) { loginInfoLoaded = true; } + if (!videoInitAttempted) { + videoInitAttempted = true; + backgroundVideo.open("assets/startscreen.mp4"); + } + backgroundVideo.update(ImGui::GetIO().DeltaTime); + if (backgroundVideo.isReady()) { + ImVec2 screen = ImGui::GetIO().DisplaySize; + float screenW = screen.x; + float screenH = screen.y; + float videoW = static_cast(backgroundVideo.getWidth()); + float videoH = static_cast(backgroundVideo.getHeight()); + if (videoW > 0.0f && videoH > 0.0f) { + float screenAspect = screenW / screenH; + float videoAspect = videoW / videoH; + ImVec2 uv0(0.0f, 0.0f); + ImVec2 uv1(1.0f, 1.0f); + if (videoAspect > screenAspect) { + float scale = screenAspect / videoAspect; + float crop = (1.0f - scale) * 0.5f; + uv0.x = crop; + uv1.x = 1.0f - crop; + } else if (videoAspect < screenAspect) { + float scale = videoAspect / screenAspect; + float crop = (1.0f - scale) * 0.5f; + uv0.y = crop; + uv1.y = 1.0f - crop; + } + ImDrawList* bg = ImGui::GetBackgroundDrawList(); + bg->AddImage(static_cast(static_cast(backgroundVideo.getTextureId())), + ImVec2(0, 0), ImVec2(screenW, screenH), uv0, uv1); + } + } + ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver); ImGui::Begin("WoW 3.3.5a Authentication", nullptr, ImGuiWindowFlags_NoCollapse);