#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(); }