From 25e1067794760523a331c07c1c0b23922b9644f2 Mon Sep 17 00:00:00 2001 From: fieldofcows Date: Sun, 4 Dec 2016 23:47:34 +0000 Subject: [PATCH] Add video view that is based on detail view but allows themes to include a video preview of the selected game along with a marquee image --- CMake/Packages/FindVLC.cmake | 55 ++ CMake/Packages/MacroEnsureVersion.cmake | 117 ++++ CMakeLists.txt | 3 + README.md | 5 +- es-app/CMakeLists.txt | 2 + es-app/src/FileData.cpp | 26 + es-app/src/FileData.h | 2 + es-app/src/MetaData.cpp | 17 +- es-app/src/MetaData.h | 2 +- es-app/src/views/ViewController.cpp | 39 +- .../src/views/gamelist/VideoGameListView.cpp | 345 ++++++++++++ es-app/src/views/gamelist/VideoGameListView.h | 53 ++ es-core/CMakeLists.txt | 2 + es-core/src/GuiComponent.cpp | 14 + es-core/src/GuiComponent.h | 3 + es-core/src/ThemeData.cpp | 10 +- es-core/src/ThemeData.h | 1 + es-core/src/Window.cpp | 5 + es-core/src/components/VideoComponent.cpp | 518 ++++++++++++++++++ es-core/src/components/VideoComponent.h | 112 ++++ 20 files changed, 1318 insertions(+), 13 deletions(-) create mode 100644 CMake/Packages/FindVLC.cmake create mode 100644 CMake/Packages/MacroEnsureVersion.cmake create mode 100644 es-app/src/views/gamelist/VideoGameListView.cpp create mode 100644 es-app/src/views/gamelist/VideoGameListView.h create mode 100644 es-core/src/components/VideoComponent.cpp create mode 100644 es-core/src/components/VideoComponent.h diff --git a/CMake/Packages/FindVLC.cmake b/CMake/Packages/FindVLC.cmake new file mode 100644 index 000000000..eec5c5709 --- /dev/null +++ b/CMake/Packages/FindVLC.cmake @@ -0,0 +1,55 @@ +# - Try to find VLC library +# Once done this will define +# +# VLC_FOUND - system has VLC +# VLC_INCLUDE_DIR - The VLC include directory +# VLC_LIBRARIES - The libraries needed to use VLC +# VLC_DEFINITIONS - Compiler switches required for using VLC +# +# Copyright (C) 2008, Tanguy Krotoff +# Copyright (C) 2008, Lukas Durfina +# Copyright (c) 2009, Fathi Boudra +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. +# + +if(VLC_INCLUDE_DIR AND VLC_LIBRARIES) + # in cache already + set(VLC_FIND_QUIETLY TRUE) +endif(VLC_INCLUDE_DIR AND VLC_LIBRARIES) + +# use pkg-config to get the directories and then use these values +# in the FIND_PATH() and FIND_LIBRARY() calls +if(NOT WIN32) + find_package(PkgConfig) + pkg_check_modules(VLC libvlc>=1.0.0) + set(VLC_DEFINITIONS ${VLC_CFLAGS}) + set(VLC_LIBRARIES ${VLC_LDFLAGS}) +endif(NOT WIN32) + +# TODO add argument support to pass version on find_package +include(MacroEnsureVersion) +macro_ensure_version(1.0.0 ${VLC_VERSION} VLC_VERSION_OK) +if(VLC_VERSION_OK) + set(VLC_FOUND TRUE) + message(STATUS "VLC library found") +else(VLC_VERSION_OK) + set(VLC_FOUND FALSE) + message(FATAL_ERROR "VLC library not found") +endif(VLC_VERSION_OK) + +find_path(VLC_INCLUDE_DIR + NAMES vlc.h + PATHS ${VLC_INCLUDE_DIRS} + PATH_SUFFIXES vlc) + +find_library(VLC_LIBRARIES + NAMES vlc + PATHS ${VLC_LIBRARY_DIRS}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(VLC DEFAULT_MSG VLC_INCLUDE_DIR VLC_LIBRARIES) + +# show the VLC_INCLUDE_DIR and VLC_LIBRARIES variables only in the advanced view +mark_as_advanced(VLC_INCLUDE_DIR VLC_LIBRARIES) diff --git a/CMake/Packages/MacroEnsureVersion.cmake b/CMake/Packages/MacroEnsureVersion.cmake new file mode 100644 index 000000000..6797e5b7d --- /dev/null +++ b/CMake/Packages/MacroEnsureVersion.cmake @@ -0,0 +1,117 @@ +# This file defines the following macros for developers to use in ensuring +# that installed software is of the right version: +# +# MACRO_ENSURE_VERSION - test that a version number is greater than +# or equal to some minimum +# MACRO_ENSURE_VERSION_RANGE - test that a version number is greater than +# or equal to some minimum and less than some +# maximum +# MACRO_ENSURE_VERSION2 - deprecated, do not use in new code +# + +# MACRO_ENSURE_VERSION +# This macro compares version numbers of the form "x.y.z" or "x.y" +# MACRO_ENSURE_VERSION( FOO_MIN_VERSION FOO_VERSION_FOUND FOO_VERSION_OK) +# will set FOO_VERSION_OK to true if FOO_VERSION_FOUND >= FOO_MIN_VERSION +# Leading and trailing text is ok, e.g. +# MACRO_ENSURE_VERSION( "2.5.31" "flex 2.5.4a" VERSION_OK) +# which means 2.5.31 is required and "flex 2.5.4a" is what was found on the system + +# Copyright (c) 2006, David Faure, +# Copyright (c) 2007, Will Stephenson +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +# MACRO_ENSURE_VERSION_RANGE +# This macro ensures that a version number of the form +# "x.y.z" or "x.y" falls within a range defined by +# min_version <= found_version < max_version. +# If this expression holds, FOO_VERSION_OK will be set TRUE +# +# Example: MACRO_ENSURE_VERSION_RANGE3( "0.1.0" ${FOOCODE_VERSION} "0.7.0" FOO_VERSION_OK ) +# +# This macro will break silently if any of x,y,z are greater than 100. +# +# Copyright (c) 2007, Will Stephenson +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +# NORMALIZE_VERSION +# Helper macro to convert version numbers of the form "x.y.z" +# to an integer equal to 10^4 * x + 10^2 * y + z +# +# This macro will break silently if any of x,y,z are greater than 100. +# +# Copyright (c) 2006, David Faure, +# Copyright (c) 2007, Will Stephenson +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + +# CHECK_RANGE_INCLUSIVE_LOWER +# Helper macro to check whether x <= y < z +# +# Copyright (c) 2007, Will Stephenson +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + + +MACRO(NORMALIZE_VERSION _requested_version _normalized_version) + STRING(REGEX MATCH "[^0-9]*[0-9]+\\.[0-9]+\\.[0-9]+.*" _threePartMatch "${_requested_version}") + if (_threePartMatch) + # parse the parts of the version string + STRING(REGEX REPLACE "[^0-9]*([0-9]+)\\.[0-9]+\\.[0-9]+.*" "\\1" _major_vers "${_requested_version}") + STRING(REGEX REPLACE "[^0-9]*[0-9]+\\.([0-9]+)\\.[0-9]+.*" "\\1" _minor_vers "${_requested_version}") + STRING(REGEX REPLACE "[^0-9]*[0-9]+\\.[0-9]+\\.([0-9]+).*" "\\1" _patch_vers "${_requested_version}") + else (_threePartMatch) + STRING(REGEX REPLACE "([0-9]+)\\.[0-9]+" "\\1" _major_vers "${_requested_version}") + STRING(REGEX REPLACE "[0-9]+\\.([0-9]+)" "\\1" _minor_vers "${_requested_version}") + set(_patch_vers "0") + endif (_threePartMatch) + + # compute an overall version number which can be compared at once + MATH(EXPR ${_normalized_version} "${_major_vers}*10000 + ${_minor_vers}*100 + ${_patch_vers}") +ENDMACRO(NORMALIZE_VERSION) + +MACRO(MACRO_CHECK_RANGE_INCLUSIVE_LOWER _lower_limit _value _upper_limit _ok) + if (${_value} LESS ${_lower_limit}) + set( ${_ok} FALSE ) + elseif (${_value} EQUAL ${_lower_limit}) + set( ${_ok} TRUE ) + elseif (${_value} EQUAL ${_upper_limit}) + set( ${_ok} FALSE ) + elseif (${_value} GREATER ${_upper_limit}) + set( ${_ok} FALSE ) + else (${_value} LESS ${_lower_limit}) + set( ${_ok} TRUE ) + endif (${_value} LESS ${_lower_limit}) +ENDMACRO(MACRO_CHECK_RANGE_INCLUSIVE_LOWER) + +MACRO(MACRO_ENSURE_VERSION requested_version found_version var_too_old) + NORMALIZE_VERSION( ${requested_version} req_vers_num ) + NORMALIZE_VERSION( ${found_version} found_vers_num ) + + if (found_vers_num LESS req_vers_num) + set( ${var_too_old} FALSE ) + else (found_vers_num LESS req_vers_num) + set( ${var_too_old} TRUE ) + endif (found_vers_num LESS req_vers_num) + +ENDMACRO(MACRO_ENSURE_VERSION) + +MACRO(MACRO_ENSURE_VERSION2 requested_version2 found_version2 var_too_old2) + MACRO_ENSURE_VERSION( ${requested_version2} ${found_version2} ${var_too_old2}) +ENDMACRO(MACRO_ENSURE_VERSION2) + +MACRO(MACRO_ENSURE_VERSION_RANGE min_version found_version max_version var_ok) + NORMALIZE_VERSION( ${min_version} req_vers_num ) + NORMALIZE_VERSION( ${found_version} found_vers_num ) + NORMALIZE_VERSION( ${max_version} max_vers_num ) + + MACRO_CHECK_RANGE_INCLUSIVE_LOWER( ${req_vers_num} ${found_vers_num} ${max_vers_num} ${var_ok}) +ENDMACRO(MACRO_ENSURE_VERSION_RANGE) + + diff --git a/CMakeLists.txt b/CMakeLists.txt index 784c2d0ac..48143a523 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,7 @@ find_package(SDL2 REQUIRED) find_package(Boost REQUIRED COMPONENTS system filesystem date_time locale) find_package(Eigen3 REQUIRED) find_package(CURL REQUIRED) +find_package(VLC REQUIRED) #add ALSA for Linux if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") @@ -99,6 +100,7 @@ set(COMMON_INCLUDE_DIRS ${Boost_INCLUDE_DIRS} ${EIGEN3_INCLUDE_DIR} ${CURL_INCLUDE_DIR} + ${VLC_INCLUDE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/external ${CMAKE_CURRENT_SOURCE_DIR}/es-core/src ) @@ -148,6 +150,7 @@ set(COMMON_LIBRARIES ${FreeImage_LIBRARIES} ${SDL2_LIBRARY} ${CURL_LIBRARIES} + ${VLC_LIBRARIES} pugixml nanosvg ) diff --git a/README.md b/README.md index ff02eae13..1113c6d03 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,10 @@ EmulationStation has a few dependencies. For building, you'll need CMake, SDL2, **On Debian/Ubuntu:** All of this be easily installed with apt-get: ```bash -sudo apt-get install libsdl2-dev libboost-system-dev libboost-filesystem-dev libboost-date-time-dev libboost-locale-dev libfreeimage-dev libfreetype6-dev libeigen3-dev libcurl4-openssl-dev libasound2-dev libgl1-mesa-dev build-essential cmake fonts-droid +sudo apt-get install libsdl2-dev libboost-system-dev libboost-filesystem-dev libboost-date-time-dev \ + libboost-locale-dev libfreeimage-dev libfreetype6-dev libeigen3-dev libcurl4-openssl-dev \ + libasound2-dev libgl1-mesa-dev build-essential cmake fonts-droid \ + libvlc-dev libvlccore-dev vlc-nox ``` Then, generate and build the Makefile with CMake: diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 6f1de88be..47dfd131c 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -38,6 +38,7 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/IGameListView.h ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/ISimpleGameListView.h ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/GridGameListView.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/VideoGameListView.h ${CMAKE_CURRENT_SOURCE_DIR}/src/views/SystemView.h ${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.h @@ -84,6 +85,7 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/IGameListView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/ISimpleGameListView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/GridGameListView.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/VideoGameListView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/views/SystemView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.cpp ) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 0b151b6e2..c8409d4d5 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -81,6 +81,32 @@ const std::string& FileData::getThumbnailPath() const return metadata.get("image"); } +const std::string& FileData::getVideoPath() const +{ + if (mType == GAME) + { + return metadata.get("video"); + } + else + { + static std::string empty; + return empty; + } +} + +const std::string& FileData::getMarqueePath() const +{ + if (mType == GAME) + { + return metadata.get("marquee"); + } + else + { + static std::string empty; + return empty; + } +} + std::vector FileData::getFilesRecursive(unsigned int typeMask) const { diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index f21ca8f9d..bd59ac071 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -45,6 +45,8 @@ public: inline SystemData* getSystem() const { return mSystem; } virtual const std::string& getThumbnailPath() const; + virtual const std::string& getVideoPath() const; + virtual const std::string& getMarqueePath() const; std::vector getFilesRecursive(unsigned int typeMask) const; diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index 76c82b9d6..3f5da45c6 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -9,8 +9,10 @@ MetaDataDecl gameDecls[] = { // key, type, default, statistic, name in GuiMetaDataEd, prompt in GuiMetaDataEd {"name", MD_STRING, "", false, "name", "enter game name"}, {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, - {"image", MD_IMAGE_PATH, "", false, "image", "enter path to image"}, - {"thumbnail", MD_IMAGE_PATH, "", false, "thumbnail", "enter path to thumbnail"}, + {"image", MD_PATH, "", false, "image", "enter path to image"}, + {"video", MD_PATH , "", false, "video", "enter path to video"}, + {"marquee", MD_PATH, "", false, "marquee", "enter path to marquee"}, + {"thumbnail", MD_PATH, "", false, "thumbnail", "enter path to thumbnail"}, {"rating", MD_RATING, "0.000000", false, "rating", "enter rating"}, {"releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date"}, {"developer", MD_STRING, "unknown", false, "developer", "enter game developer"}, @@ -25,8 +27,8 @@ const std::vector gameMDD(gameDecls, gameDecls + sizeof(gameDecls) MetaDataDecl folderDecls[] = { {"name", MD_STRING, "", false}, {"desc", MD_MULTILINE_STRING, "", false}, - {"image", MD_IMAGE_PATH, "", false}, - {"thumbnail", MD_IMAGE_PATH, "", false}, + {"image", MD_PATH, "", false}, + {"thumbnail", MD_PATH, "", false}, }; const std::vector folderMDD(folderDecls, folderDecls + sizeof(folderDecls) / sizeof(folderDecls[0])); @@ -68,9 +70,10 @@ MetaDataList MetaDataList::createFromXML(MetaDataListType type, pugi::xml_node n { // if it's a path, resolve relative paths std::string value = md.text().get(); - if(iter->type == MD_IMAGE_PATH) + if (iter->type == MD_PATH) + { value = resolvePath(value, relativeTo, true).generic_string(); - + } mdl.set(iter->key, value); }else{ mdl.set(iter->key, iter->defaultValue); @@ -96,7 +99,7 @@ void MetaDataList::appendToXML(pugi::xml_node parent, bool ignoreDefaults, const // try and make paths relative if we can std::string value = mapIter->second; - if(mddIter->type == MD_IMAGE_PATH) + if (mddIter->type == MD_PATH) value = makeRelativePath(value, relativeTo, true).generic_string(); parent.append_child(mapIter->first.c_str()).text().set(value.c_str()); diff --git a/es-app/src/MetaData.h b/es-app/src/MetaData.h index d111cec78..136a8b2f6 100644 --- a/es-app/src/MetaData.h +++ b/es-app/src/MetaData.h @@ -16,7 +16,7 @@ enum MetaDataType //specialized types MD_MULTILINE_STRING, - MD_IMAGE_PATH, + MD_PATH, MD_RATING, MD_DATE, MD_TIME //used for lastplayed diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 5a94d97c2..1e10c9175 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -5,12 +5,14 @@ #include "views/gamelist/BasicGameListView.h" #include "views/gamelist/DetailedGameListView.h" +#include "views/gamelist/VideoGameListView.h" #include "views/gamelist/GridGameListView.h" #include "guis/GuiMenu.h" #include "guis/GuiMsgBox.h" #include "animations/LaunchAnimation.h" #include "animations/MoveCameraAnimation.h" #include "animations/LambdaAnimation.h" +#include ViewController* ViewController::sInstance = NULL; @@ -55,6 +57,12 @@ int ViewController::getSystemId(SystemData* system) void ViewController::goToSystemView(SystemData* system) { + // Tell any current view it's about to be hidden + if (mCurrentView) + { + mCurrentView->onHide(); + } + mState.viewing = SYSTEM_SELECT; mState.system = system; @@ -99,7 +107,15 @@ void ViewController::goToGameList(SystemData* system) mState.viewing = GAME_LIST; mState.system = system; + if (mCurrentView) + { + mCurrentView->onHide(); + } mCurrentView = getGameListView(system); + if (mCurrentView) + { + mCurrentView->onShow(); + } playViewTransition(); } @@ -163,6 +179,10 @@ void ViewController::launch(FileData* game, Eigen::Vector3f center) return; } + // Hide the current view + if (mCurrentView) + mCurrentView->onHide(); + Eigen::Affine3f origCamera = mCamera; origCamera.translation() = -mCurrentView->getPosition(); @@ -210,17 +230,26 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste //decide type bool detailed = false; + bool video = false; std::vector files = system->getRootFolder()->getFilesRecursive(GAME | FOLDER); for(auto it = files.begin(); it != files.end(); it++) { - if(!(*it)->getThumbnailPath().empty()) + if(!(*it)->getVideoPath().empty()) + { + video = true; + break; + } + else if(!(*it)->getThumbnailPath().empty()) { detailed = true; - break; + // Don't break out in case any subsequent files have video } } - if(detailed) + if (video) + // Create the view + view = std::shared_ptr(new VideoGameListView(mWindow, system->getRootFolder())); + else if(detailed) view = std::shared_ptr(new DetailedGameListView(mWindow, system->getRootFolder())); else view = std::shared_ptr(new BasicGameListView(mWindow, system->getRootFolder())); @@ -347,6 +376,10 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) break; } } + // Redisplay the current view + if (mCurrentView) + mCurrentView->onShow(); + } void ViewController::reloadAll() diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp new file mode 100644 index 000000000..2a270be02 --- /dev/null +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -0,0 +1,345 @@ +#include "views/gamelist/VideoGameListView.h" +#include "views/ViewController.h" +#include "Window.h" +#include "animations/LambdaAnimation.h" +#include +#include + +VideoGameListView::VideoGameListView(Window* window, FileData* root) : + BasicGameListView(window, root), + mDescContainer(window), mDescription(window), + mMarquee(window), + mImage(window), + mVideo(window), + mVideoPlaying(false), + + mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), + mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), + + mRating(window), mReleaseDate(window), mDeveloper(window), mPublisher(window), + mGenre(window), mPlayers(window), mLastPlayed(window), mPlayCount(window) +{ + const float padding = 0.01f; + + mList.setPosition(mSize.x() * (0.50f + padding), mList.getPosition().y()); + mList.setSize(mSize.x() * (0.50f - padding), mList.getSize().y()); + mList.setAlignment(TextListComponent::ALIGN_LEFT); + mList.setCursorChangedCallback([&](const CursorState& state) { updateInfoPanel(); }); + + // Marquee + mMarquee.setOrigin(0.5f, 0.5f); + mMarquee.setPosition(mSize.x() * 0.25f, mSize.y() * 0.10f); + mMarquee.setMaxSize(mSize.x() * (0.5f - 2*padding), mSize.y() * 0.18f); + addChild(&mMarquee); + + // Image + mImage.setOrigin(0.5f, 0.5f); + // Default to off the screen + mImage.setPosition(2.0f, 2.0f); + mImage.setMaxSize(1.0f, 1.0f); + addChild(&mImage); + + // video + mVideo.setOrigin(0.5f, 0.5f); + mVideo.setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); + mVideo.setSize(mSize.x() * (0.5f - 2*padding), mSize.y() * 0.4f); + addChild(&mVideo); + + // We want the video to be in front of the background but behind any 'extra' images + for (std::vector::iterator it = mChildren.begin(); it != mChildren.end(); ++it) + { + if (*it == &mThemeExtras) + { + mChildren.insert(it, &mVideo); + mChildren.pop_back(); + break; + } + } + + // metadata labels + values + mLblRating.setText("Rating: "); + addChild(&mLblRating); + addChild(&mRating); + mLblReleaseDate.setText("Released: "); + addChild(&mLblReleaseDate); + addChild(&mReleaseDate); + mLblDeveloper.setText("Developer: "); + addChild(&mLblDeveloper); + addChild(&mDeveloper); + mLblPublisher.setText("Publisher: "); + addChild(&mLblPublisher); + addChild(&mPublisher); + mLblGenre.setText("Genre: "); + addChild(&mLblGenre); + addChild(&mGenre); + mLblPlayers.setText("Players: "); + addChild(&mLblPlayers); + addChild(&mPlayers); + mLblLastPlayed.setText("Last played: "); + addChild(&mLblLastPlayed); + mLastPlayed.setDisplayMode(DateTimeComponent::DISP_RELATIVE_TO_NOW); + addChild(&mLastPlayed); + mLblPlayCount.setText("Times played: "); + addChild(&mLblPlayCount); + addChild(&mPlayCount); + + mDescContainer.setPosition(mSize.x() * padding, mSize.y() * 0.65f); + mDescContainer.setSize(mSize.x() * (0.50f - 2*padding), mSize.y() - mDescContainer.getPosition().y()); + mDescContainer.setAutoScroll(true); + addChild(&mDescContainer); + + mDescription.setFont(Font::get(FONT_SIZE_SMALL)); + mDescription.setSize(mDescContainer.getSize().x(), 0); + mDescContainer.addChild(&mDescription); + + initMDLabels(); + initMDValues(); + updateInfoPanel(); +} + +VideoGameListView::~VideoGameListView() +{ +} + +void VideoGameListView::onThemeChanged(const std::shared_ptr& theme) +{ + BasicGameListView::onThemeChanged(theme); + + using namespace ThemeFlags; + mMarquee.applyTheme(theme, getName(), "md_marquee", POSITION | ThemeFlags::SIZE); + mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE); + mVideo.applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY); + + initMDLabels(); + std::vector labels = getMDLabels(); + assert(labels.size() == 8); + const char* lblElements[8] = { + "md_lbl_rating", "md_lbl_releasedate", "md_lbl_developer", "md_lbl_publisher", + "md_lbl_genre", "md_lbl_players", "md_lbl_lastplayed", "md_lbl_playcount" + }; + + for(unsigned int i = 0; i < labels.size(); i++) + { + labels[i]->applyTheme(theme, getName(), lblElements[i], ALL); + } + + + initMDValues(); + std::vector values = getMDValues(); + assert(values.size() == 8); + const char* valElements[8] = { + "md_rating", "md_releasedate", "md_developer", "md_publisher", + "md_genre", "md_players", "md_lastplayed", "md_playcount" + }; + + for(unsigned int i = 0; i < values.size(); i++) + { + values[i]->applyTheme(theme, getName(), valElements[i], ALL ^ ThemeFlags::TEXT); + } + + mDescContainer.applyTheme(theme, getName(), "md_description", POSITION | ThemeFlags::SIZE); + mDescription.setSize(mDescContainer.getSize().x(), 0); + mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | TEXT)); +} + +void VideoGameListView::initMDLabels() +{ + using namespace Eigen; + + std::vector components = getMDLabels(); + + const unsigned int colCount = 2; + const unsigned int rowCount = components.size() / 2; + + Vector3f start(mSize.x() * 0.01f, mSize.y() * 0.625f, 0.0f); + + const float colSize = (mSize.x() * 0.48f) / colCount; + const float rowPadding = 0.01f * mSize.y(); + + for(unsigned int i = 0; i < components.size(); i++) + { + const unsigned int row = i % rowCount; + Vector3f pos(0.0f, 0.0f, 0.0f); + if(row == 0) + { + pos = start + Vector3f(colSize * (i / rowCount), 0, 0); + }else{ + // work from the last component + GuiComponent* lc = components[i-1]; + pos = lc->getPosition() + Vector3f(0, lc->getSize().y() + rowPadding, 0); + } + + components[i]->setFont(Font::get(FONT_SIZE_SMALL)); + components[i]->setPosition(pos); + } +} + +void VideoGameListView::initMDValues() +{ + using namespace Eigen; + + std::vector labels = getMDLabels(); + std::vector values = getMDValues(); + + std::shared_ptr defaultFont = Font::get(FONT_SIZE_SMALL); + mRating.setSize(defaultFont->getHeight() * 5.0f, (float)defaultFont->getHeight()); + mReleaseDate.setFont(defaultFont); + mDeveloper.setFont(defaultFont); + mPublisher.setFont(defaultFont); + mGenre.setFont(defaultFont); + mPlayers.setFont(defaultFont); + mLastPlayed.setFont(defaultFont); + mPlayCount.setFont(defaultFont); + + float bottom = 0.0f; + + const float colSize = (mSize.x() * 0.48f) / 2; + for(unsigned int i = 0; i < labels.size(); i++) + { + const float heightDiff = (labels[i]->getSize().y() - values[i]->getSize().y()) / 2; + values[i]->setPosition(labels[i]->getPosition() + Vector3f(labels[i]->getSize().x(), heightDiff, 0)); + values[i]->setSize(colSize - labels[i]->getSize().x(), values[i]->getSize().y()); + + float testBot = values[i]->getPosition().y() + values[i]->getSize().y(); + if(testBot > bottom) + bottom = testBot; + } + + mDescContainer.setPosition(mDescContainer.getPosition().x(), bottom + mSize.y() * 0.01f); + mDescContainer.setSize(mDescContainer.getSize().x(), mSize.y() - mDescContainer.getPosition().y()); +} + + + +void VideoGameListView::updateInfoPanel() +{ + FileData* file = (mList.size() == 0 || mList.isScrolling()) ? NULL : mList.getSelected(); + + bool fadingOut; + if(file == NULL) + { + mVideo.setVideo(""); + mVideo.setImage(""); + mVideoPlaying = false; + //mMarquee.setImage(""); + //mDescription.setText(""); + fadingOut = true; + + }else{ + std::string video_path; + std::string marquee_path; + std::string thumbnail_path; + video_path = file->getVideoPath(); + marquee_path = file->getMarqueePath(); + thumbnail_path = file->getThumbnailPath(); + + if (!video_path.empty() && (video_path[0] == '~')) + { + video_path.erase(0, 1); + video_path.insert(0, getHomePath()); + } + if (!marquee_path.empty() && (marquee_path[0] == '~')) + { + marquee_path.erase(0, 1); + marquee_path.insert(0, getHomePath()); + } + if (!thumbnail_path.empty() && (thumbnail_path[0] == '~')) + { + thumbnail_path.erase(0, 1); + thumbnail_path.insert(0, getHomePath()); + } + if (!mVideo.setVideo(video_path)) + mVideo.setDefaultVideo(); + mVideoPlaying = true; + + mVideo.setImage(thumbnail_path); + mMarquee.setImage(marquee_path); + mImage.setImage(thumbnail_path); + + mDescription.setText(file->metadata.get("desc")); + mDescContainer.reset(); + + if(file->getType() == GAME) + { + mRating.setValue(file->metadata.get("rating")); + mReleaseDate.setValue(file->metadata.get("releasedate")); + mDeveloper.setValue(file->metadata.get("developer")); + mPublisher.setValue(file->metadata.get("publisher")); + mGenre.setValue(file->metadata.get("genre")); + mPlayers.setValue(file->metadata.get("players")); + mLastPlayed.setValue(file->metadata.get("lastplayed")); + mPlayCount.setValue(file->metadata.get("playcount")); + } + + fadingOut = false; + } + + std::vector comps = getMDValues(); + comps.push_back(&mMarquee); + comps.push_back(&mVideo); + comps.push_back(&mDescription); + comps.push_back(&mImage); + std::vector labels = getMDLabels(); + comps.insert(comps.end(), labels.begin(), labels.end()); + + for(auto it = comps.begin(); it != comps.end(); it++) + { + GuiComponent* comp = *it; + // an animation is playing + // then animate if reverse != fadingOut + // an animation is not playing + // then animate if opacity != our target opacity + if((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) || + (!comp->isAnimationPlaying(0) && comp->getOpacity() != (fadingOut ? 0 : 255))) + { + auto func = [comp](float t) + { + comp->setOpacity((unsigned char)(lerp(0.0f, 1.0f, t)*255)); + }; + comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut); + } + } +} + +void VideoGameListView::launch(FileData* game) +{ + Eigen::Vector3f target(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0); + if(mMarquee.hasImage()) + target << mVideo.getCenter().x(), mVideo.getCenter().y(), 0; + + ViewController::get()->launch(game, target); +} + +std::vector VideoGameListView::getMDLabels() +{ + std::vector ret; + ret.push_back(&mLblRating); + ret.push_back(&mLblReleaseDate); + ret.push_back(&mLblDeveloper); + ret.push_back(&mLblPublisher); + ret.push_back(&mLblGenre); + ret.push_back(&mLblPlayers); + ret.push_back(&mLblLastPlayed); + ret.push_back(&mLblPlayCount); + return ret; +} + +std::vector VideoGameListView::getMDValues() +{ + std::vector ret; + ret.push_back(&mRating); + ret.push_back(&mReleaseDate); + ret.push_back(&mDeveloper); + ret.push_back(&mPublisher); + ret.push_back(&mGenre); + ret.push_back(&mPlayers); + ret.push_back(&mLastPlayed); + ret.push_back(&mPlayCount); + return ret; +} + +void VideoGameListView::update(int deltaTime) +{ + BasicGameListView::update(deltaTime); + mVideo.update(deltaTime); +} diff --git a/es-app/src/views/gamelist/VideoGameListView.h b/es-app/src/views/gamelist/VideoGameListView.h new file mode 100644 index 000000000..b0673d396 --- /dev/null +++ b/es-app/src/views/gamelist/VideoGameListView.h @@ -0,0 +1,53 @@ +#pragma once + +#include "views/gamelist/BasicGameListView.h" +#include "components/ScrollableContainer.h" +#include "components/RatingComponent.h" +#include "components/DateTimeComponent.h" +#include "components/VideoComponent.h" + +class VideoGameListView : public BasicGameListView +{ +public: + VideoGameListView(Window* window, FileData* root); + virtual ~VideoGameListView(); + + virtual void onThemeChanged(const std::shared_ptr& theme) override; + + virtual const char* getName() const override { return "video"; } + +protected: + virtual void launch(FileData* game) override; + + virtual void update(int deltaTime) override; + +private: + void updateInfoPanel(); + + void initMDLabels(); + void initMDValues(); + + ImageComponent mMarquee; + VideoComponent mVideo; + ImageComponent mImage; + + TextComponent mLblRating, mLblReleaseDate, mLblDeveloper, mLblPublisher, mLblGenre, mLblPlayers, mLblLastPlayed, mLblPlayCount; + + RatingComponent mRating; + DateTimeComponent mReleaseDate; + TextComponent mDeveloper; + TextComponent mPublisher; + TextComponent mGenre; + TextComponent mPlayers; + DateTimeComponent mLastPlayed; + TextComponent mPlayCount; + + std::vector getMDLabels(); + std::vector getMDValues(); + + ScrollableContainer mDescContainer; + TextComponent mDescription; + + bool mVideoPlaying; + +}; diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index 09219d673..cf0cfb3ea 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -42,6 +42,7 @@ set(CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/components/SwitchComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextEditComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoComponent.h # Guis ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiDetectDevice.h @@ -96,6 +97,7 @@ set(CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/components/SwitchComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextEditComponent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoComponent.cpp # Guis ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiDetectDevice.cpp diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index d079930de..6ba531f2b 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -345,3 +345,17 @@ bool GuiComponent::isProcessing() const { return mIsProcessing; } + +void GuiComponent::onShow() +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->onShow(); +} + +void GuiComponent::onHide() +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->onHide(); +} + + diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 566b2a615..e37abe69f 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -77,6 +77,9 @@ public: virtual void onFocusGained() {}; virtual void onFocusLost() {}; + + virtual void onShow(); + virtual void onHide(); // Default implementation just handles and tags as normalized float pairs. // You probably want to keep this behavior for any derived classes as well as add your own. diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index fce86628f..871e7dead 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -82,7 +82,15 @@ std::map< std::string, ElementMapType > ThemeData::sElementMap = boost::assign:: ("textColor", COLOR) ("iconColor", COLOR) ("fontPath", PATH) - ("fontSize", FLOAT))); + ("fontSize", FLOAT))) + ("video", makeMap(boost::assign::map_list_of + ("pos", NORMALIZED_PAIR) + ("size", NORMALIZED_PAIR) + ("origin", NORMALIZED_PAIR) + ("default", PATH) + ("delay", FLOAT) + ("showSnapshotNoVideo", BOOLEAN) + ("showSnapshotDelay", BOOLEAN))); namespace fs = boost::filesystem; diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 268f0753f..32702a15c 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -37,6 +37,7 @@ namespace ThemeFlags TEXT = 512, FORCE_UPPERCASE = 1024, LINE_SPACING = 2048, + DELAY = 4096, ALL = 0xFFFFFFFF }; diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index e5622e8ca..761df4abc 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -90,6 +90,11 @@ bool Window::init(unsigned int width, unsigned int height) void Window::deinit() { + // Hide all GUI elements on uninitialisation - this disable + for(auto i = mGuiStack.begin(); i != mGuiStack.end(); i++) + { + (*i)->onHide(); + } InputManager::getInstance()->deinit(); ResourceManager::getInstance()->unloadAll(); Renderer::deinit(); diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp new file mode 100644 index 000000000..6f41ffd78 --- /dev/null +++ b/es-core/src/components/VideoComponent.cpp @@ -0,0 +1,518 @@ +#include "components/VideoComponent.h" +#include "Renderer.h" +#include "ThemeData.h" +#include "Util.h" +#ifdef WIN32 +#include +#endif + +#define FADE_TIME_MS 200 + +libvlc_instance_t* VideoComponent::mVLC = NULL; + +// VLC prepares to render a video frame. +static void *lock(void *data, void **p_pixels) { + struct VideoContext *c = (struct VideoContext *)data; + SDL_LockMutex(c->mutex); + SDL_LockSurface(c->surface); + *p_pixels = c->surface->pixels; + return NULL; // Picture identifier, not needed here. +} + +// VLC just rendered a video frame. +static void unlock(void *data, void *id, void *const *p_pixels) { + struct VideoContext *c = (struct VideoContext *)data; + SDL_UnlockSurface(c->surface); + SDL_UnlockMutex(c->mutex); +} + +// VLC wants to display a video frame. +static void display(void *data, void *id) { + //Data to be displayed +} + +VideoComponent::VideoComponent(Window* window) : + GuiComponent(window), + mStaticImage(window), + mMediaPlayer(nullptr), + mVideoHeight(0), + mVideoWidth(0), + mStartDelayed(false), + mIsPlaying(false), + mShowing(false) +{ + memset(&mContext, 0, sizeof(mContext)); + + // Setup the default configuration + mConfig.showSnapshotDelay = false; + mConfig.showSnapshotNoVideo = false; + mConfig.startDelay = 0; + + // Get an empty texture for rendering the video + mTexture = TextureResource::get(""); + + // Make sure VLC has been initialised + setupVLC(); +} + +VideoComponent::~VideoComponent() +{ + // Stop any currently running video + stopVideo(); +} + +void VideoComponent::setOrigin(float originX, float originY) +{ + mOrigin << originX, originY; + + // Update the embeded static image + mStaticImage.setOrigin(originX, originY); +} + +Eigen::Vector2f VideoComponent::getCenter() const +{ + return Eigen::Vector2f(mPosition.x() - (getSize().x() * mOrigin.x()) + getSize().x() / 2, + mPosition.y() - (getSize().y() * mOrigin.y()) + getSize().y() / 2); +} + +void VideoComponent::onSizeChanged() +{ + // Update the embeded static image + mStaticImage.onSizeChanged(); +} + +bool VideoComponent::setVideo(std::string path) +{ + // Convert the path into a format VLC can understand + boost::filesystem::path fullPath = getCanonicalPath(path); + fullPath.make_preferred().native(); + + // Check that it's changed + if (fullPath == mVideoPath) + return !path.empty(); + + // Store the path + mVideoPath = fullPath; + + // If the file exists then set the new video + if (!fullPath.empty() && ResourceManager::getInstance()->fileExists(fullPath.generic_string())) + { + // Return true to show that we are going to attempt to play a video + return true; + } + // Return false to show that no video will be displayed + return false; +} + +void VideoComponent::setImage(std::string path) +{ + // Check that the image has changed + if (path == mStaticImagePath) + return; + + mStaticImage.setImage(path); + // Make the image stretch to fill the video region + mStaticImage.setSize(getSize()); + mFadeIn = 0.0f; + mStaticImagePath = path; +} + +void VideoComponent::setDefaultVideo() +{ + setVideo(mConfig.defaultVideoPath); +} + +void VideoComponent::setOpacity(unsigned char opacity) +{ + mOpacity = opacity; + // Update the embeded static image + mStaticImage.setOpacity(opacity); +} + +void VideoComponent::render(const Eigen::Affine3f& parentTrans) +{ + float x, y; + + Eigen::Affine3f trans = parentTrans * getTransform(); + GuiComponent::renderChildren(trans); + + Renderer::setMatrix(trans); + + // Handle the case where the video is delayed + handleStartDelay(); + + // Handle looping of the video + handleLooping(); + + if (mIsPlaying && mContext.valid) + { + float tex_offs_x = 0.0f; + float tex_offs_y = 0.0f; + float x2; + float y2; + + x = -(float)mSize.x() * mOrigin.x(); + y = -(float)mSize.y() * mOrigin.y(); + x2 = x+mSize.x(); + y2 = y+mSize.y(); + + // Define a structure to contain the data for each vertex + struct Vertex + { + Eigen::Vector2f pos; + Eigen::Vector2f tex; + Eigen::Vector4f colour; + } vertices[6]; + + // We need two triangles to cover the rectangular area + vertices[0].pos[0] = x; vertices[0].pos[1] = y; + vertices[1].pos[0] = x; vertices[1].pos[1] = y2; + vertices[2].pos[0] = x2; vertices[2].pos[1] = y; + + vertices[3].pos[0] = x2; vertices[3].pos[1] = y; + vertices[4].pos[0] = x; vertices[4].pos[1] = y2; + vertices[5].pos[0] = x2; vertices[5].pos[1] = y2; + + // Texture coordinates + vertices[0].tex[0] = -tex_offs_x; vertices[0].tex[1] = -tex_offs_y; + vertices[1].tex[0] = -tex_offs_x; vertices[1].tex[1] = 1.0f + tex_offs_y; + vertices[2].tex[0] = 1.0f + tex_offs_x; vertices[2].tex[1] = -tex_offs_y; + + vertices[3].tex[0] = 1.0f + tex_offs_x; vertices[3].tex[1] = -tex_offs_y; + vertices[4].tex[0] = -tex_offs_x; vertices[4].tex[1] = 1.0f + tex_offs_y; + vertices[5].tex[0] = 1.0f + tex_offs_x; vertices[5].tex[1] = 1.0f + tex_offs_y; + + // Colours - use this to fade the video in and out + for (int i = 0; i < (4 * 6); ++i) { + if ((i%4) < 3) + vertices[i / 4].colour[i % 4] = mFadeIn; + else + vertices[i / 4].colour[i % 4] = 1.0f; + } + + glEnable(GL_TEXTURE_2D); + + // Build a texture for the video frame + mTexture->initFromPixels((unsigned char*)mContext.surface->pixels, mContext.surface->w, mContext.surface->h); + mTexture->bind(); + + // Render it + glEnableClientState(GL_COLOR_ARRAY); + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + + glColorPointer(4, GL_FLOAT, sizeof(Vertex), &vertices[0].colour); + glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &vertices[0].pos); + glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &vertices[0].tex); + + glDrawArrays(GL_TRIANGLES, 0, 6); + + glDisableClientState(GL_VERTEX_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + glDisableClientState(GL_COLOR_ARRAY); + + glDisable(GL_TEXTURE_2D); + } + else + { + // This is the case where the video is not currently being displayed. Work out + // if we need to display a static image + if ((mConfig.showSnapshotNoVideo && mVideoPath.empty()) || (mStartDelayed && mConfig.showSnapshotDelay)) + { + // Display the static image instead + mStaticImage.setOpacity((unsigned char)(mFadeIn * 255.0f)); + mStaticImage.render(parentTrans); + } + } + +} + +void VideoComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) +{ + using namespace ThemeFlags; + + const ThemeData::ThemeElement* elem = theme->getElement(view, element, "video"); + if(!elem) + { + return; + } + + Eigen::Vector2f scale = getParent() ? getParent()->getSize() : Eigen::Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + + if ((properties & POSITION) && elem->has("pos")) + { + Eigen::Vector2f denormalized = elem->get("pos").cwiseProduct(scale); + setPosition(Eigen::Vector3f(denormalized.x(), denormalized.y(), 0)); + } + + if ((properties & ThemeFlags::SIZE) && elem->has("size")) + { + setSize(elem->get("size").cwiseProduct(scale)); + } + + // position + size also implies origin + if (((properties & ORIGIN) || ((properties & POSITION) && (properties & ThemeFlags::SIZE))) && elem->has("origin")) + setOrigin(elem->get("origin")); + + if(elem->has("default")) + mConfig.defaultVideoPath = elem->get("default"); + + if((properties & ThemeFlags::DELAY) && elem->has("delay")) + mConfig.startDelay = (unsigned)(elem->get("delay") * 1000.0f); + + if (elem->has("showSnapshotNoVideo")) + mConfig.showSnapshotNoVideo = elem->get("showSnapshotNoVideo"); + + if (elem->has("showSnapshotDelay")) + mConfig.showSnapshotDelay = elem->get("showSnapshotDelay"); + + // Update the embeded static image + mStaticImage.setPosition(getPosition()); + mStaticImage.setMaxSize(getSize()); + mStaticImage.setSize(getSize()); +} + +std::vector VideoComponent::getHelpPrompts() +{ + std::vector ret; + ret.push_back(HelpPrompt("a", "select")); + return ret; +} + +void VideoComponent::setupContext() +{ + if (!mContext.valid) + { + // Create an RGBA surface to render the video into + mContext.surface = SDL_CreateRGBSurface(SDL_SWSURFACE, (int)mVideoWidth, (int)mVideoHeight, 32, 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff); + mContext.mutex = SDL_CreateMutex(); + mContext.valid = true; + } +} + +void VideoComponent::freeContext() +{ + if (mContext.valid) + { + SDL_FreeSurface(mContext.surface); + SDL_DestroyMutex(mContext.mutex); + mContext.valid = false; + } +} + +void VideoComponent::setupVLC() +{ + // If VLC hasn't been initialised yet then do it now + if (!mVLC) + { + const char* args[] = { "--quiet" }; + mVLC = libvlc_new(sizeof(args) / sizeof(args[0]), args); + } +} + +void VideoComponent::handleStartDelay() +{ + // Only play if any delay has timed out + if (mStartDelayed) + { + if (mStartTime > SDL_GetTicks()) + { + // Timeout not yet completed + return; + } + // Completed + mStartDelayed = false; + // Clear the playing flag so startVideo works + mIsPlaying = false; + startVideo(); + } +} + +void VideoComponent::handleLooping() +{ + if (mIsPlaying && mMediaPlayer) + { + libvlc_state_t state = libvlc_media_player_get_state(mMediaPlayer); + if (state == libvlc_Ended) + { + //libvlc_media_player_set_position(mMediaPlayer, 0.0f); + libvlc_media_player_set_media(mMediaPlayer, mMedia); + libvlc_media_player_play(mMediaPlayer); + } + } +} + +void VideoComponent::startVideo() +{ + if (!mIsPlaying) { + mVideoWidth = 0; + mVideoHeight = 0; + +#ifdef WIN32 + std::wstring_convert, wchar_t> wton; + std::string path = wton.to_bytes(mVideoPath.c_str()); +#else + std::string path(mVideoPath.c_str()); +#endif + // Make sure we have a video path + if (mVLC && (path.size() > 0)) + { + // Set the video that we are going to be playing so we don't attempt to restart it + mPlayingVideoPath = mVideoPath; + + // Open the media + mMedia = libvlc_media_new_path(mVLC, path.c_str()); + if (mMedia) + { + unsigned track_count; + // Get the media metadata so we can find the aspect ratio + libvlc_media_parse(mMedia); + libvlc_media_track_t** tracks; + track_count = libvlc_media_tracks_get(mMedia, &tracks); + for (unsigned track = 0; track < track_count; ++track) + { + if (tracks[track]->i_type == libvlc_track_video) + { + mVideoWidth = tracks[track]->video->i_width; + mVideoHeight = tracks[track]->video->i_height; + break; + } + } + libvlc_media_tracks_release(tracks, track_count); + + // Make sure we found a valid video track + if ((mVideoWidth > 0) && (mVideoHeight > 0)) + { + setupContext(); + + // Setup the media player + mMediaPlayer = libvlc_media_player_new_from_media(mMedia); + libvlc_media_player_play(mMediaPlayer); + libvlc_video_set_callbacks(mMediaPlayer, lock, unlock, display, (void*)&mContext); + libvlc_video_set_format(mMediaPlayer, "RGBA", (int)mVideoWidth, (int)mVideoHeight, (int)mVideoWidth * 4); + + // Update the playing state + mIsPlaying = true; + mFadeIn = 0.0f; + } + } + } + } +} + +void VideoComponent::startVideoWithDelay() +{ + // If not playing then either start the video or initiate the delay + if (!mIsPlaying) + { + // Set the video that we are going to be playing so we don't attempt to restart it + mPlayingVideoPath = mVideoPath; + + if (mConfig.startDelay == 0) + { + // No delay. Just start the video + mStartDelayed = false; + startVideo(); + } + else + { + // Configure the start delay + mStartDelayed = true; + mFadeIn = 0.0f; + mStartTime = SDL_GetTicks() + mConfig.startDelay; + } + mIsPlaying = true; + } +} + +void VideoComponent::stopVideo() +{ + mIsPlaying = false; + mStartDelayed = false; + // Release the media player so it stops calling back to us + if (mMediaPlayer) + { + libvlc_media_player_stop(mMediaPlayer); + libvlc_media_player_release(mMediaPlayer); + libvlc_media_release(mMedia); + mMediaPlayer = NULL; + freeContext(); + } +} + +void VideoComponent::update(int deltaTime) +{ + manageState(); + + // If the video start is delayed and there is less than the fade time then set the image fade + // accordingly + if (mStartDelayed) + { + Uint32 ticks = SDL_GetTicks(); + if (mStartTime > ticks) + { + Uint32 diff = mStartTime - ticks; + if (diff < FADE_TIME_MS) + { + mFadeIn = (float)diff / (float)FADE_TIME_MS; + return; + } + } + } + // If the fade in is less than 1 then increment it + if (mFadeIn < 1.0f) + { + mFadeIn += deltaTime / (float)FADE_TIME_MS; + if (mFadeIn > 1.0f) + mFadeIn = 1.0f; + } + GuiComponent::update(deltaTime); +} + +void VideoComponent::manageState() +{ + // We will only show if the component is on display + bool show = mShowing; + + // See if we're already playing + if (mIsPlaying) + { + // If we are not on display then stop the video from playing + if (!show) + { + stopVideo(); + } + else + { + if (mVideoPath != mPlayingVideoPath) + { + // Path changed. Stop the video. We will start it again below because + // mIsPlaying will be modified by stopVideo to be false + stopVideo(); + } + } + } + // Need to recheck variable rather than 'else' because it may be modified above + if (!mIsPlaying) + { + // If we are on display then see if we should start the video + if (show && !mVideoPath.empty()) + { + startVideoWithDelay(); + } + } +} + +void VideoComponent::onShow() +{ + mShowing = true; + manageState(); +} + +void VideoComponent::onHide() +{ + mShowing = false; + manageState(); +} + + diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h new file mode 100644 index 000000000..f8376cfe9 --- /dev/null +++ b/es-core/src/components/VideoComponent.h @@ -0,0 +1,112 @@ +#ifndef _VIDEOCOMPONENT_H_ +#define _VIDEOCOMPONENT_H_ + +#include "platform.h" +#include GLHEADER + +#include "GuiComponent.h" +#include "ImageComponent.h" +#include +#include +#include "resources/TextureResource.h" +#include +#include +#include +#include + +struct VideoContext { + SDL_Surface* surface; + SDL_mutex* mutex; + bool valid; +}; + +class VideoComponent : public GuiComponent +{ + // Structure that groups together the configuration of the video component + struct Configuration + { + unsigned startDelay; + bool showSnapshotNoVideo; + bool showSnapshotDelay; + std::string defaultVideoPath; + }; + +public: + static void setupVLC(); + + VideoComponent(Window* window); + virtual ~VideoComponent(); + + // Loads the video at the given filepath + bool setVideo(std::string path); + // Loads a static image that is displayed if the video cannot be played + void setImage(std::string path); + + // Configures the component to show the default video + void setDefaultVideo(); + + virtual void onShow() override; + virtual void onHide() override; + + //Sets the origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) + void setOrigin(float originX, float originY); + inline void setOrigin(Eigen::Vector2f origin) { setOrigin(origin.x(), origin.y()); } + + void onSizeChanged() override; + void setOpacity(unsigned char opacity) override; + + void render(const Eigen::Affine3f& parentTrans) override; + + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; + + virtual std::vector getHelpPrompts() override; + + // Returns the center point of the video (takes origin into account). + Eigen::Vector2f getCenter() const; + + virtual void update(int deltaTime); + +private: + // Start the video Immediately + void startVideo(); + // Start the video after any configured delay + void startVideoWithDelay(); + // Stop the video + void stopVideo(); + + void setupContext(); + void freeContext(); + + // Handle any delay to the start of playing the video clip. Must be called periodically + void handleStartDelay(); + + // Handle looping the video. Must be called periodically + void handleLooping(); + + // Manage the playing state of the component + void manageState(); + +private: + static libvlc_instance_t* mVLC; + libvlc_media_t* mMedia; + libvlc_media_player_t* mMediaPlayer; + VideoContext mContext; + unsigned mVideoWidth; + unsigned mVideoHeight; + Eigen::Vector2f mOrigin; + std::shared_ptr mTexture; + float mFadeIn; + std::string mStaticImagePath; + ImageComponent mStaticImage; + + boost::filesystem::path mVideoPath; + boost::filesystem::path mPlayingVideoPath; + bool mStartDelayed; + unsigned mStartTime; + bool mIsPlaying; + bool mShowing; + + Configuration mConfig; +}; + +#endif