diff options
author | dec05eba <dec05eba@protonmail.com> | 2019-08-04 02:28:33 +0200 |
---|---|---|
committer | dec05eba <dec05eba@protonmail.com> | 2019-08-04 02:28:36 +0200 |
commit | 4b24638802385816fb5f90c95f175b30ae2398a8 (patch) | |
tree | ab6c7cbfd7d20c2065e160f6e8f20be02e4cc1b5 | |
parent | d9fb89269fd30fa44d2b3728b9ae3c7b896a77d3 (diff) |
Add youtube video playing, page navigation
-rw-r--r-- | include/Page.hpp | 10 | ||||
-rw-r--r-- | include/QuickMedia.hpp | 36 | ||||
-rw-r--r-- | include/SearchBar.hpp | 33 | ||||
-rw-r--r-- | include/VideoPlayer.hpp | 46 | ||||
-rw-r--r-- | plugins/Plugin.hpp | 6 | ||||
-rw-r--r-- | project.conf | 3 | ||||
-rw-r--r-- | src/Manganelo.cpp | 12 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 433 | ||||
-rw-r--r-- | src/SearchBar.cpp | 93 | ||||
-rw-r--r-- | src/VideoPlayer.cpp | 195 | ||||
-rw-r--r-- | src/Youtube.cpp | 17 | ||||
-rw-r--r-- | src/main.cpp | 224 |
12 files changed, 871 insertions, 237 deletions
diff --git a/include/Page.hpp b/include/Page.hpp new file mode 100644 index 0000000..ce20971 --- /dev/null +++ b/include/Page.hpp @@ -0,0 +1,10 @@ +#pragma once + +namespace QuickMedia { + enum class Page { + EXIT, + SEARCH_SUGGESTION, + SEARCH_RESULT, + VIDEO_CONTENT + }; +}
\ No newline at end of file diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp new file mode 100644 index 0000000..291afff --- /dev/null +++ b/include/QuickMedia.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "SearchBar.hpp" +#include "Page.hpp" +#include <vector> +#include <stack> +#include <memory> +#include <SFML/Graphics/Font.hpp> +#include <SFML/Graphics/RenderWindow.hpp> + +namespace QuickMedia { + class Body; + class Plugin; + + class Program { + public: + Program(); + ~Program(); + void run(); + private: + void base_event_handler(sf::Event &event); + void search_suggestion_page(); + void search_result_page(); + void video_content_page(); + private: + sf::RenderWindow window; + sf::Vector2f window_size; + sf::Font font; + Body *body; + Plugin *current_plugin; + std::unique_ptr<SearchBar> search_bar; + Page current_page; + std::string video_url; + std::stack<Page> page_view_stack; + }; +}
\ No newline at end of file diff --git a/include/SearchBar.hpp b/include/SearchBar.hpp new file mode 100644 index 0000000..c9f75f0 --- /dev/null +++ b/include/SearchBar.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include <SFML/Graphics/RenderWindow.hpp> +#include <SFML/Graphics/Font.hpp> +#include <SFML/Graphics/Text.hpp> +#include <SFML/Graphics/RectangleShape.hpp> +#include <functional> + +namespace QuickMedia { + using TextUpdateCallback = std::function<void(const sf::String &text)>; + using TextSubmitCallback = std::function<void(const sf::String &text)>; + + class SearchBar { + public: + SearchBar(sf::Font &font); + void draw(sf::RenderWindow &window); + void update(); + void onWindowResize(const sf::Vector2f &window_size); + void onTextEntered(sf::Uint32 codepoint); + void clear(); + + float getBottom() const; + + TextUpdateCallback onTextUpdateCallback; + TextSubmitCallback onTextSubmitCallback; + private: + sf::Text text; + sf::RectangleShape background; + bool show_placeholder; + bool updated_search; + sf::Clock time_since_search_update; + }; +}
\ No newline at end of file diff --git a/include/VideoPlayer.hpp b/include/VideoPlayer.hpp new file mode 100644 index 0000000..e98221e --- /dev/null +++ b/include/VideoPlayer.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include <SFML/Graphics/RenderWindow.hpp> +#include <SFML/Graphics/Texture.hpp> +#include <SFML/Graphics/Sprite.hpp> +#include <SFML/Window/Context.hpp> +#include <thread> +#include <mutex> +#include <atomic> +#include <stdexcept> + +class mpv_handle; +class mpv_opengl_cb_context; + +namespace QuickMedia { + class VideoInitializationException : public std::runtime_error { + public: + VideoInitializationException(const std::string &errMsg) : std::runtime_error(errMsg) {} + }; + + class VideoPlayer { + public: + // Throws VideoInitializationException on error + VideoPlayer(unsigned int width, unsigned int height, const char *file, bool loop = false); + ~VideoPlayer(); + + void setPosition(float x, float y); + bool resize(const sf::Vector2i &size); + void draw(sf::RenderWindow &window); + + // This counter is incremented when mpv wants to redraw content + std::atomic_int redrawCounter; + private: + sf::Context context; + mpv_handle *mpv; + mpv_opengl_cb_context *mpvGl; + std::thread renderThread; + std::mutex renderMutex; + sf::Sprite sprite; + sf::Texture texture; + sf::Uint8 *textureBuffer; + bool alive; + sf::Vector2i video_size; + sf::Vector2i desired_size; + }; +} diff --git a/plugins/Plugin.hpp b/plugins/Plugin.hpp index 9d62356..dd50998 100644 --- a/plugins/Plugin.hpp +++ b/plugins/Plugin.hpp @@ -7,12 +7,14 @@ namespace QuickMedia { class BodyItem { public: - BodyItem(const std::string &_title): title(_title) { + BodyItem(const std::string &_title): title(_title), visible(true) { } std::string title; - std::string cover_url; + std::string url; + std::string thumbnail_url; + bool visible; }; enum class SearchResult { diff --git a/project.conf b/project.conf index c24ca64..d54e94b 100644 --- a/project.conf +++ b/project.conf @@ -6,4 +6,7 @@ platforms = ["posix"] [dependencies] sfml-graphics = "2" +mpv = "1.25.0" +gl = ">=17.3" +x11 = "1.6.5" jsoncpp = "1.5"
\ No newline at end of file diff --git a/src/Manganelo.cpp b/src/Manganelo.cpp index de39c77..8bbaa9f 100644 --- a/src/Manganelo.cpp +++ b/src/Manganelo.cpp @@ -29,6 +29,7 @@ namespace QuickMedia { const char *href = quickmedia_html_node_get_attribute_value(node, "href"); const char *text = quickmedia_html_node_get_text(node); auto item = std::make_unique<BodyItem>(text); + item->url = href; item_data->result_items.push_back(std::move(item)); }, &item_data); if (result != 0) @@ -39,7 +40,7 @@ namespace QuickMedia { ItemData *item_data = (ItemData*)userdata; const char *src = quickmedia_html_node_get_attribute_value(node, "src"); if(item_data->item_index < item_data->result_items.size()) { - item_data->result_items[item_data->item_index]->cover_url = src; + item_data->result_items[item_data->item_index]->thumbnail_url = src; ++item_data->item_index; } }, &item_data); @@ -70,9 +71,6 @@ namespace QuickMedia { } SuggestionResult Manganelo::update_search_suggestions(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items) { - if(text.empty()) - return SuggestionResult::OK; - std::string url = "https://manganelo.com/home_json_search"; std::string search_term = "searchword="; search_term += url_param_encode(text); @@ -101,8 +99,10 @@ namespace QuickMedia { if(name.isString() && name.asCString()[0] != '\0') { std::string name_str = name.asString(); while(remove_html_span(name_str)) {} - auto item = std::make_unique<BodyItem>(name_str); - result_items.push_back(std::move(item)); + if(name_str != text) { + auto item = std::make_unique<BodyItem>(name_str); + result_items.push_back(std::move(item)); + } } } } diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp new file mode 100644 index 0000000..a63f430 --- /dev/null +++ b/src/QuickMedia.cpp @@ -0,0 +1,433 @@ +#include "../include/QuickMedia.hpp" +#include "../plugins/Manganelo.hpp" +#include "../plugins/Youtube.hpp" +#include "../include/VideoPlayer.hpp" + +#include <SFML/Graphics/RectangleShape.hpp> +#include <SFML/Graphics/Text.hpp> +#include <SFML/Window/Event.hpp> +#include <assert.h> + +const sf::Color front_color(43, 45, 47); +const sf::Color back_color(33, 35, 37); + +namespace QuickMedia { + class Body { + public: + Body(sf::Font &font) : title_text("", font, 14), selected_item(0) { + title_text.setFillColor(sf::Color::White); + } + + void add_item(std::unique_ptr<BodyItem> item) { + items.push_back(std::move(item)); + } + + // Select previous item, ignoring invisible items + void select_previous_item() { + if(items.empty()) + return; + + int num_items = (int)items.size(); + for(int i = 0; i < num_items; ++i) { + --selected_item; + if(selected_item < 0) + selected_item = num_items - 1; + if(items[selected_item]->visible) + return; + } + } + + // Select next item, ignoring invisible items + void select_next_item() { + if(items.empty()) + return; + + int num_items = (int)items.size(); + for(int i = 0; i < num_items; ++i) { + ++selected_item; + if(selected_item == num_items) + selected_item = 0; + if(items[selected_item]->visible) + return; + } + } + + void reset_selected() { + selected_item = 0; + } + + void clear_items() { + items.clear(); + } + + BodyItem* get_selected() const { + if(items.empty() || !items[selected_item]->visible) + return nullptr; + return items[selected_item].get(); + } + + void clamp_selection() { + int num_items = (int)items.size(); + if(items.empty()) + return; + + if(selected_item < 0) + selected_item = 0; + else if(selected_item >= num_items) + selected_item = num_items - 1; + + for(int i = selected_item; i >= 0; --i) { + if(items[i]->visible) { + selected_item = i; + return; + } + } + + for(int i = selected_item; i < num_items; ++i) { + if(items[i]->visible) { + selected_item = i; + return; + } + } + } + + void draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size) { + const float font_height = title_text.getCharacterSize() + 8.0f; + const float image_height = 50.0f; + + sf::RectangleShape image(sf::Vector2f(50, image_height)); + image.setFillColor(sf::Color::White); + + sf::RectangleShape item_background; + item_background.setFillColor(front_color); + item_background.setOutlineThickness(1.0f); + item_background.setOutlineColor(sf::Color(63, 65, 67)); + + sf::RectangleShape selected_border(sf::Vector2f(5.0f, 50)); + selected_border.setFillColor(sf::Color::Red); + + int i = 0; + for(const auto &item : items) { + if(!item->visible) { + ++i; + continue; + } + + sf::Vector2f item_pos = pos; + if(i == selected_item) { + selected_border.setPosition(pos); + window.draw(selected_border); + item_pos.x += selected_border.getSize().x; + item_background.setFillColor(front_color); + } else { + item_background.setFillColor(sf::Color(38, 40, 42)); + } + + item_background.setPosition(item_pos); + item_background.setSize(sf::Vector2f(size.x, 50)); + window.draw(item_background); + + image.setPosition(item_pos); + window.draw(image); + + title_text.setString(item->title); + title_text.setPosition(item_pos.x + 50 + 10, item_pos.y); + window.draw(title_text); + + + pos.y += 50 + 10; + ++i; + } + } + + static bool string_find_case_insensitive(const std::string &str, const std::string &substr) { + auto it = std::search(str.begin(), str.end(), substr.begin(), substr.end(), + [](char c1, char c2) { + return std::toupper(c1) == std::toupper(c2); + }); + return it != str.end(); + } + + // TODO: Make this actually fuzzy... Right now it's just a case insensitive string find. + // TODO: Highlight the part of the text that matches the search + void filter_search_fuzzy(const std::string &text) { + if(text.empty()) { + for(auto &item : items) { + item->visible = true; + } + return; + } + + for(auto &item : items) { + item->visible = string_find_case_insensitive(item->title, text); + } + } + + sf::Text title_text; + int selected_item; + std::vector<std::unique_ptr<BodyItem>> items; + }; + + Program::Program() : + window(sf::VideoMode(800, 600), "QuickMedia"), + window_size(800, 600), + body(nullptr), + current_plugin(nullptr), + current_page(Page::SEARCH_SUGGESTION) + { + window.setVerticalSyncEnabled(true); + if(!font.loadFromFile("fonts/Lato-Regular.ttf")) { + fprintf(stderr, "Failed to load font!\n"); + abort(); + } + body = new Body(font); + //current_plugin = new Manganelo(); + current_plugin = new Youtube(); + search_bar = std::make_unique<SearchBar>(font); + page_view_stack.push(current_page); + } + + Program::~Program() { + delete body; + delete current_plugin; + } + + static SearchResult search_selected_suggestion(Body *body, Plugin *plugin) { + BodyItem *selected_item = body->get_selected(); + if(!selected_item) + return SearchResult::ERR; + + std::string selected_item_title = selected_item->title; + body->clear_items(); + SearchResult search_result = plugin->search(selected_item_title, body->items); + body->reset_selected(); + return search_result; + } + + static void update_search_suggestions(const sf::String &text, Body *body, Plugin *plugin) { + body->clear_items(); + if(text.isEmpty()) + return; + + body->items.push_back(std::make_unique<BodyItem>(text)); + SuggestionResult suggestion_result = plugin->update_search_suggestions(text, body->items); + body->clamp_selection(); + } + + void Program::run() { + while(window.isOpen()) { + switch(current_page) { + case Page::EXIT: + return; + case Page::SEARCH_SUGGESTION: + search_suggestion_page(); + break; + case Page::SEARCH_RESULT: + search_result_page(); + break; + case Page::VIDEO_CONTENT: + video_content_page(); + break; + default: + return; + } + } + } + + void Program::base_event_handler(sf::Event &event) { + + } + + void Program::search_suggestion_page() { + search_bar->onTextUpdateCallback = [this](const std::string &text) { + update_search_suggestions(text, body, current_plugin); + }; + + search_bar->onTextSubmitCallback = [this](const std::string &text) { + if(search_selected_suggestion(body, current_plugin) == SearchResult::OK) { + current_page = Page::SEARCH_RESULT; + page_view_stack.push(current_page); + } + }; + + sf::Vector2f body_pos; + sf::Vector2f body_size; + bool resized = true; + + while (window.isOpen() && current_page == Page::SEARCH_SUGGESTION) { + sf::Event event; + while (window.pollEvent(event)) { + if (event.type == sf::Event::Closed) { + window.close(); + current_page = Page::EXIT; + } else if(event.type == sf::Event::Resized) { + window_size.x = event.size.width; + window_size.y = event.size.height; + sf::FloatRect visible_area(0, 0, window_size.x, window_size.y); + window.setView(sf::View(visible_area)); + resized = true; + } else if(event.type == sf::Event::KeyPressed) { + if(event.key.code == sf::Keyboard::Up) { + body->select_previous_item(); + } else if(event.key.code == sf::Keyboard::Down) { + body->select_next_item(); + } else if(event.key.code == sf::Keyboard::Escape) { + current_page = Page::EXIT; + window.close(); + } + } else if(event.type == sf::Event::TextEntered) { + search_bar->onTextEntered(event.text.unicode); + } + } + + if(resized) { + search_bar->onWindowResize(window_size); + + float body_padding_horizontal = 50.0f; + float body_padding_vertical = 50.0f; + float body_width = window_size.x - body_padding_horizontal * 2.0f; + if(body_width < 400) { + body_width = window_size.x; + body_padding_horizontal = 0.0f; + } + + float search_bottom = search_bar->getBottom(); + body_pos = sf::Vector2f(body_padding_horizontal, search_bottom + body_padding_vertical); + body_size = sf::Vector2f(body_width, window_size.y); + } + + search_bar->update(); + + window.clear(back_color); + body->draw(window, body_pos, body_size); + search_bar->draw(window); + window.display(); + } + } + + void Program::search_result_page() { + search_bar->onTextUpdateCallback = [this](const std::string &text) { + body->filter_search_fuzzy(text); + body->clamp_selection(); + }; + + search_bar->onTextSubmitCallback = [this](const std::string &text) { + BodyItem *selected_item = body->get_selected(); + printf("Selected item: %s\n", selected_item->title.c_str()); + if(!selected_item) + return; + video_url = selected_item->url; + current_page = Page::VIDEO_CONTENT; + page_view_stack.push(current_page); + }; + + sf::Vector2f body_pos; + sf::Vector2f body_size; + bool resized = true; + + while (window.isOpen() && current_page == Page::SEARCH_RESULT) { + sf::Event event; + while (window.pollEvent(event)) { + if (event.type == sf::Event::Closed) { + window.close(); + current_page = Page::EXIT; + } else if(event.type == sf::Event::Resized) { + window_size.x = event.size.width; + window_size.y = event.size.height; + sf::FloatRect visible_area(0, 0, window_size.x, window_size.y); + window.setView(sf::View(visible_area)); + resized = true; + } else if(event.type == sf::Event::KeyPressed) { + if(event.key.code == sf::Keyboard::Up) { + body->select_previous_item(); + } else if(event.key.code == sf::Keyboard::Down) { + body->select_next_item(); + } else if(event.key.code == sf::Keyboard::Escape) { + current_page = Page::SEARCH_SUGGESTION; + body->clear_items(); + body->reset_selected(); + search_bar->clear(); + } + } else if(event.type == sf::Event::TextEntered) { + search_bar->onTextEntered(event.text.unicode); + } + } + + if(resized) { + search_bar->onWindowResize(window_size); + + float body_padding_horizontal = 50.0f; + float body_padding_vertical = 50.0f; + float body_width = window_size.x - body_padding_horizontal * 2.0f; + if(body_width < 400) { + body_width = window_size.x; + body_padding_horizontal = 0.0f; + } + + float search_bottom = search_bar->getBottom(); + body_pos = sf::Vector2f(body_padding_horizontal, search_bottom + body_padding_vertical); + body_size = sf::Vector2f(body_width, window_size.y); + } + + search_bar->update(); + + window.clear(back_color); + body->draw(window, body_pos, body_size); + search_bar->draw(window); + window.display(); + } + } + + void Program::video_content_page() { + search_bar->onTextUpdateCallback = nullptr; + search_bar->onTextSubmitCallback = nullptr; + + std::unique_ptr<VideoPlayer> video_player; + try { + printf("Play video: %s\n", video_url.c_str()); + video_player = std::make_unique<VideoPlayer>(window_size.x, window_size.y, video_url.c_str()); + } catch(VideoInitializationException &e) { + fprintf(stderr, "Failed to create video player!. TODO: Show this to the user"); + } + + bool resized = false; + sf::Clock resize_timer; + + while (window.isOpen() && current_page == Page::VIDEO_CONTENT) { + sf::Event event; + while (window.pollEvent(event)) { + if (event.type == sf::Event::Closed) { + window.close(); + current_page = Page::EXIT; + } else if(event.type == sf::Event::Resized) { + window_size.x = event.size.width; + window_size.y = event.size.height; + sf::FloatRect visible_area(0, 0, window_size.x, window_size.y); + window.setView(sf::View(visible_area)); + resized = true; + resize_timer.restart(); + } else if(event.type == sf::Event::KeyPressed) { + if(event.key.code == sf::Keyboard::Escape) { + current_page = Page::SEARCH_SUGGESTION; + body->clear_items(); + body->reset_selected(); + search_bar->clear(); + } + } + } + + if(resized && resize_timer.getElapsedTime().asMilliseconds() >= 300) { + resized = false; + if(video_player) { + if(!video_player->resize(sf::Vector2i(window_size.x, window_size.y))) + video_player.release(); + } + } + + window.clear(); + if(video_player) + video_player->draw(window); + window.display(); + } + } +}
\ No newline at end of file diff --git a/src/SearchBar.cpp b/src/SearchBar.cpp new file mode 100644 index 0000000..dad9e7c --- /dev/null +++ b/src/SearchBar.cpp @@ -0,0 +1,93 @@ +#include "../include/SearchBar.hpp" + +const sf::Color text_placeholder_color(255, 255, 255, 100); +const sf::Color front_color(43, 45, 47); +const sf::Color back_color(33, 35, 37); +const float background_margin_horizontal = 8.0f; +const float background_margin_vertical = 4.0f; +const float padding_horizontal = 10.0f; +const float padding_vertical = 10.0f; + +namespace QuickMedia { + SearchBar::SearchBar(sf::Font &font) : + onTextUpdateCallback(nullptr), + onTextSubmitCallback(nullptr), + text("Search...", font, 18), + show_placeholder(true), + updated_search(false) + { + text.setFillColor(text_placeholder_color); + background.setFillColor(front_color); + background.setPosition(padding_horizontal, padding_vertical); + } + + void SearchBar::draw(sf::RenderWindow &window) { + window.draw(background); + window.draw(text); + } + + void SearchBar::update() { + if(updated_search && time_since_search_update.getElapsedTime().asMilliseconds() >= 150) { + updated_search = false; + sf::String str = text.getString(); + if(show_placeholder) + str.clear(); + if(onTextUpdateCallback) + onTextUpdateCallback(str); + } + } + + void SearchBar::onWindowResize(const sf::Vector2f &window_size) { + float font_height = text.getCharacterSize() + 8.0f; + float rect_height = font_height + background_margin_vertical * 2.0f; + background.setSize(sf::Vector2f(window_size.x - padding_horizontal * 2.0f, rect_height)); + text.setPosition(padding_horizontal + background_margin_horizontal, padding_vertical + background_margin_vertical); + } + + void SearchBar::onTextEntered(sf::Uint32 codepoint) { + if(codepoint == 8 && !show_placeholder) { // Backspace + sf::String str = text.getString(); + if(str.getSize() > 0) { + str.erase(str.getSize() - 1, 1); + text.setString(str); + if(str.getSize() == 0) { + show_placeholder = true; + text.setString("Search..."); + text.setFillColor(text_placeholder_color); + } + updated_search = true; + time_since_search_update.restart(); + } + } else if(codepoint == 13) { // Return + if(onTextSubmitCallback) + onTextSubmitCallback(text.getString()); + + if(!show_placeholder) { + show_placeholder = true; + text.setString("Search..."); + text.setFillColor(text_placeholder_color); + } + } else if(codepoint > 31) { // Non-control character + if(show_placeholder) { + show_placeholder = false; + text.setString(""); + text.setFillColor(sf::Color::White); + } + sf::String str = text.getString(); + str += codepoint; + text.setString(str); + updated_search = true; + time_since_search_update.restart(); + } + } + + void SearchBar::clear() { + show_placeholder = true; + text.setString("Search..."); + text.setFillColor(text_placeholder_color); + } + + float SearchBar::getBottom() const { + return background.getPosition().y + background.getSize().y; + } +}
\ No newline at end of file diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp new file mode 100644 index 0000000..304ef2d --- /dev/null +++ b/src/VideoPlayer.cpp @@ -0,0 +1,195 @@ +#include "../include/VideoPlayer.hpp" +#include <mpv/client.h> +#include <mpv/opengl_cb.h> +#include <clocale> + +#include <SFML/Config.hpp> + +#if defined(SFML_SYSTEM_WINDOWS) + #ifdef _MSC_VER + #include <windows.h> + #endif + #include <GL/gl.h> + #include <GL/glx.h> +#elif defined(SFML_SYSTEM_LINUX) || defined(SFML_SYSTEM_FREEBSD) + #if defined(SFML_OPENGL_ES) + #include <GLES/gl.h> + #include <GLES/glext.h> + #else + #include <GL/gl.h> + #endif + #include <GL/glx.h> + #define glGetProcAddress glXGetProcAddress +#elif defined(SFML_SYSTEM_MACOS) + #include <OpenGL/gl.h> +#elif defined (SFML_SYSTEM_IOS) + #include <OpenGLES/ES1/gl.h> + #include <OpenGLES/ES1/glext.h> +#elif defined (SFML_SYSTEM_ANDROID) + #include <GLES/gl.h> + #include <GLES/glext.h> + // We're not using OpenGL ES 2+ yet, but we can use the sRGB extension + #include <GLES2/gl2ext.h> +#endif + +using namespace std; + +namespace QuickMedia { + void* getProcAddressMpv(void *funcContext, const char *name) { + return (void*)glGetProcAddress((const GLubyte*)name); + } + + void onMpvRedraw(void *rawVideo) { + VideoPlayer *video = (VideoPlayer*)rawVideo; + ++video->redrawCounter; + } + + VideoPlayer::VideoPlayer(unsigned int width, unsigned int height, const char *file, bool loop) : + redrawCounter(0), + context(sf::ContextSettings(), width, height), + mpv(nullptr), + mpvGl(nullptr), + textureBuffer((sf::Uint8*)malloc(width * height * 4)), // 4 = red, green, blue and alpha + alive(true), + video_size(width, height), + desired_size(width, height) + { + if(!textureBuffer) + throw VideoInitializationException("Failed to allocate memory for video"); + + context.setActive(true); + + if(!texture.create(width, height)) + throw VideoInitializationException("Failed to create texture for video"); + texture.setSmooth(true); + + // mpv_create requires LC_NUMERIC to be set to "C" for some reason, see mpv_create documentation + std::setlocale(LC_NUMERIC, "C"); + mpv = mpv_create(); + if(!mpv) + throw VideoInitializationException("Failed to create mpv handle"); + + if(mpv_initialize(mpv) < 0) + throw VideoInitializationException("Failed to initialize mpv"); + + mpv_set_option_string(mpv, "input-default-bindings", "yes"); + // Enable keyboard input on the X11 window + mpv_set_option_string(mpv, "input-vo-keyboard", "yes"); + + mpv_set_option_string(mpv, "vo", "opengl-cb"); + mpv_set_option_string(mpv, "hwdec", "auto"); + if(loop) + mpv_set_option_string(mpv, "loop", "inf"); + mpvGl = (mpv_opengl_cb_context*)mpv_get_sub_api(mpv, MPV_SUB_API_OPENGL_CB); + if(!mpvGl) + throw VideoInitializationException("Failed to initialize mpv opengl render context"); + + mpv_opengl_cb_set_update_callback(mpvGl, onMpvRedraw, this); + if(mpv_opengl_cb_init_gl(mpvGl, nullptr, getProcAddressMpv, nullptr) < 0) + throw VideoInitializationException("Failed to initialize mpv gl callback func"); + + renderThread = thread([this]() { + context.setActive(true); + while(alive) { + while(true) { + mpv_event *mpvEvent = mpv_wait_event(mpv, 0.010); + if(mpvEvent->event_id == MPV_EVENT_NONE) + break; + else if(mpvEvent->event_id == MPV_EVENT_SHUTDOWN) + return; + else if(mpvEvent->event_id == MPV_EVENT_VIDEO_RECONFIG) { + int64_t w, h; + if (mpv_get_property(mpv, "dwidth", MPV_FORMAT_INT64, &w) >= 0 && + mpv_get_property(mpv, "dheight", MPV_FORMAT_INT64, &h) >= 0 && + w > 0 && h > 0 && (w != video_size.x || h != video_size.y)) + { + { + lock_guard<mutex> lock(renderMutex); + video_size.x = w; + video_size.y = h; + context.setActive(true); + if(texture.create(w, h)) { + void *newTextureBuf = realloc(textureBuffer, w * h * 4); + if(newTextureBuf) + textureBuffer = (sf::Uint8*)newTextureBuf; + } + } + resize(desired_size); + } + } + } + + if(redrawCounter > 0) { + --redrawCounter; + context.setActive(true); + lock_guard<mutex> lock(renderMutex); + auto textureSize = texture.getSize(); + //mpv_render_context_render(mpvGl, params); + mpv_opengl_cb_draw(mpvGl, 0, textureSize.x, textureSize.y); + // TODO: Instead of copying video to cpu buffer and then to texture, copy directly from video buffer to texture buffer + glReadPixels(0, 0, textureSize.x, textureSize.y, GL_RGBA, GL_UNSIGNED_BYTE, textureBuffer); + texture.update(textureBuffer); + sprite.setTexture(texture, true); + mpv_opengl_cb_report_flip(mpvGl, 0); + } + } + }); + + const char *cmd[] = { "loadfile", file, nullptr }; + mpv_command(mpv, cmd); + context.setActive(false); + } + + VideoPlayer::~VideoPlayer() { + alive = false; + renderThread.join(); + + lock_guard<mutex> lock(renderMutex); + context.setActive(true); + if(mpvGl) + mpv_opengl_cb_set_update_callback(mpvGl, nullptr, nullptr); + + free(textureBuffer); + mpv_opengl_cb_uninit_gl(mpvGl); + mpv_detach_destroy(mpv); + } + + void VideoPlayer::setPosition(float x, float y) { + sprite.setPosition(x, y); + } + + bool VideoPlayer::resize(const sf::Vector2i &size) { + lock_guard<mutex> lock(renderMutex); + float video_ratio = (double)video_size.x / (double)video_size.y; + float scale_x = 1.0f; + float scale_y = 1.0f; + if(video_ratio >= 0.0f) { + double ratio_x = (double)size.x / (double)video_size.x; + scale_x = ratio_x; + scale_y = ratio_x; + sprite.setPosition(0.0f, size.y * 0.5f - video_size.y * scale_y * 0.5f); + } else { + double ratio_y = (double)size.y / (double)video_size.y; + scale_x = ratio_y; + scale_y = ratio_y; + sprite.setPosition(size.x * 0.5f - video_size.x * scale_x * 0.5f, 0.0f); + } + sprite.setScale(scale_x, scale_y); + desired_size = size; + #if 0 + void *newTextureBuf = realloc(textureBuffer, size.x * size.y * 4); + if(!newTextureBuf) + return false; + textureBuffer = (sf::Uint8*)newTextureBuf; + if(!texture.create(size.x, size.y)) + return false; + return true; + #endif + return true; + } + + void VideoPlayer::draw(sf::RenderWindow &window) { + lock_guard<mutex> lock(renderMutex); + window.draw(sprite); + } +} diff --git a/src/Youtube.cpp b/src/Youtube.cpp index 9f9c4c2..85969b9 100644 --- a/src/Youtube.cpp +++ b/src/Youtube.cpp @@ -22,6 +22,7 @@ namespace QuickMedia { const char *href = quickmedia_html_node_get_attribute_value(node, "href"); const char *title = quickmedia_html_node_get_attribute_value(node, "title"); auto item = std::make_unique<BodyItem>(title); + item->url = std::string("https://www.youtube.com") + href; result_items->push_back(std::move(item)); }, &result_items); @@ -30,24 +31,21 @@ namespace QuickMedia { return result == 0 ? SearchResult::OK : SearchResult::ERR; } - static void iterate_suggestion_result(const Json::Value &value, std::vector<std::unique_ptr<BodyItem>> &result_items, int &ignore_count) { + static void iterate_suggestion_result(const Json::Value &value, const std::string &search_text, std::vector<std::unique_ptr<BodyItem>> &result_items) { if(value.isArray()) { for(const Json::Value &child : value) { - iterate_suggestion_result(child, result_items, ignore_count); + iterate_suggestion_result(child, search_text, result_items); } } else if(value.isString()) { - if(ignore_count > 1) { - auto item = std::make_unique<BodyItem>(value.asString()); + std::string title = value.asString(); + if(title != search_text) { + auto item = std::make_unique<BodyItem>(title); result_items.push_back(std::move(item)); } - ++ignore_count; } } SuggestionResult Youtube::update_search_suggestions(const std::string &text, std::vector<std::unique_ptr<BodyItem>> &result_items) { - if(text.empty()) - return SuggestionResult::OK; - std::string url = "https://clients1.google.com/complete/search?client=youtube&hl=en&gl=us&q="; url += url_param_encode(text); @@ -77,8 +75,7 @@ namespace QuickMedia { return SuggestionResult::ERR; } - int ignore_count = 0; - iterate_suggestion_result(json_root, result_items, ignore_count); + iterate_suggestion_result(json_root, text, result_items); return SuggestionResult::OK; } }
\ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index c3c91b7..2dc50ee 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,223 +1,9 @@ -#include "../include/Program.h" -#include "../plugins/Manganelo.hpp" -#include "../plugins/Youtube.hpp" -#include <SFML/Graphics.hpp> -#include <string> -#include <vector> - -const sf::Color front_color(43, 45, 47); -const sf::Color back_color(33, 35, 37); - -namespace QuickMedia { - class Body { - public: - Body(sf::Font &font) : title_text("", font, 14), selected_item(0) { - title_text.setFillColor(sf::Color::White); - } - - void add_item(std::unique_ptr<BodyItem> item) { - items.push_back(std::move(item)); - } - - void select_previous_item() { - selected_item = std::max(0, selected_item - 1); - } - - void select_next_item() { - const int last_item = std::max(0, (int)items.size() - 1); - selected_item = std::min(last_item, selected_item + 1); - } - - void reset_selected() { - selected_item = 0; - } - - void clear_items() { - items.clear(); - } - - void draw(sf::RenderWindow &window, sf::Vector2f pos, sf::Vector2f size) { - const float font_height = title_text.getCharacterSize() + 8.0f; - - sf::RectangleShape image(sf::Vector2f(50, 50)); - image.setFillColor(sf::Color::White); - - sf::RectangleShape item_background; - item_background.setFillColor(front_color); - item_background.setOutlineThickness(1.0f); - item_background.setOutlineColor(sf::Color(63, 65, 67)); - - sf::RectangleShape selected_border(sf::Vector2f(5.0f, 50)); - selected_border.setFillColor(sf::Color::Red); - - int i = 0; - for(const auto &item : items) { - sf::Vector2f item_pos = pos; - if(i == selected_item) { - selected_border.setPosition(pos); - window.draw(selected_border); - item_pos.x += selected_border.getSize().x; - } - - item_background.setPosition(item_pos); - item_background.setSize(sf::Vector2f(size.x, 50)); - window.draw(item_background); - - image.setPosition(item_pos); - window.draw(image); - - title_text.setString(item->title); - title_text.setPosition(item_pos.x + 50 + 10, item_pos.y); - window.draw(title_text); - - pos.y += 50 + 10; - ++i; - } - } - - sf::Text title_text; - int selected_item; - std::vector<std::unique_ptr<BodyItem>> items; - }; -} - -static void search(const sf::String &text, QuickMedia::Body *body, QuickMedia::Plugin *plugin) { - body->clear_items(); - QuickMedia::SearchResult search_result = plugin->search(text, body->items); - fprintf(stderr, "Search result: %d\n", search_result); -} - -static void update_search_suggestions(const sf::String &text, QuickMedia::Body *body, QuickMedia::Plugin *plugin) { - body->clear_items(); - QuickMedia::SuggestionResult suggestion_result = plugin->update_search_suggestions(text, body->items); - fprintf(stderr, "Suggestion result: %d\n", suggestion_result); -} +#include "../include/QuickMedia.hpp" +#include <X11/Xlib.h> int main() { - const float padding_horizontal = 10.0f; - const float padding_vertical = 10.0f; - - sf::RenderWindow window(sf::VideoMode(800, 800), "SFML works!"); - window.setVerticalSyncEnabled(true); - - sf::Font font; - if(!font.loadFromFile("fonts/Lato-Regular.ttf")) { - fprintf(stderr, "Failed to load font!\n"); - abort(); - } - - bool show_placeholder = true; - sf::Color text_placeholder_color(255, 255, 255, 100); - sf::Text search_text("Search...", font, 18); - search_text.setFillColor(text_placeholder_color); - - bool resized = true; - sf::Vector2f window_size(window.getSize().x, window.getSize().y); - - sf::RectangleShape search_background; - search_background.setFillColor(front_color); - search_background.setPosition(padding_horizontal, padding_vertical); - const float search_background_margin_horizontal = 8.0f; - const float search_background_margin_vertical = 4.0f; - - sf::RectangleShape body_background; - body_background.setFillColor(front_color); - - QuickMedia::Body body(font); - QuickMedia::Manganelo manganelo_plugin; - QuickMedia::Youtube youtube_plugin; - QuickMedia::Plugin *plugin = &manganelo_plugin; - - sf::Clock time_since_search_update; - bool updated_search = false; - - while (window.isOpen()) { - sf::Event event; - - while (window.pollEvent(event)) { - if (event.type == sf::Event::Closed) - window.close(); - else if(event.type == sf::Event::Resized) { - window_size.x = event.size.width; - window_size.y = event.size.height; - sf::FloatRect visible_area(0, 0, window_size.x, window_size.y); - window.setView(sf::View(visible_area)); - resized = true; - } else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::Up) { - body.select_previous_item(); - } else if(event.key.code == sf::Keyboard::Down) { - body.select_next_item(); - } - } else if(event.type == sf::Event::TextEntered) { - if(event.text.unicode == 8 && !show_placeholder) { // Backspace - sf::String str = search_text.getString(); - if(str.getSize() > 0) { - str.erase(str.getSize() - 1, 1); - search_text.setString(str); - if(str.getSize() == 0) { - show_placeholder = true; - search_text.setString("Search..."); - search_text.setFillColor(text_placeholder_color); - } - updated_search = true; - time_since_search_update.restart(); - } - } else if(event.text.unicode == 13 && !show_placeholder) { // Return - body.reset_selected(); - search(search_text.getString(), &body, plugin); - show_placeholder = true; - search_text.setString("Search..."); - search_text.setFillColor(text_placeholder_color); - } else if(event.text.unicode > 31) { // Non-control character - if(show_placeholder) { - show_placeholder = false; - search_text.setString(""); - search_text.setFillColor(sf::Color::White); - } - sf::String str = search_text.getString(); - str += event.text.unicode; - search_text.setString(str); - updated_search = true; - time_since_search_update.restart(); - } - } - } - - if(updated_search && time_since_search_update.getElapsedTime().asMilliseconds() >= 90) { - updated_search = false; - sf::String str = search_text.getString(); - if(show_placeholder) - str.clear(); - update_search_suggestions(str, &body, plugin); - } - - if(resized) { - resized = false; - - float font_height = search_text.getCharacterSize() + 8.0f; - float rect_height = font_height + search_background_margin_vertical * 2.0f; - search_background.setSize(sf::Vector2f(window_size.x - padding_horizontal * 2.0f, rect_height)); - search_text.setPosition(padding_horizontal + search_background_margin_horizontal, padding_vertical + search_background_margin_vertical); - - float body_padding_horizontal = 50.0f; - float body_padding_vertical = 50.0f; - float body_width = window_size.x - body_padding_horizontal * 2.0f; - if(body_width < 400) { - body_width = window_size.x; - body_padding_horizontal = 0.0f; - } - body_background.setPosition(body_padding_horizontal, search_background.getPosition().y + search_background.getSize().y + body_padding_vertical); - body_background.setSize(sf::Vector2f(body_width, window_size.y)); - } - - window.clear(back_color); - body.draw(window, body_background.getPosition(), body_background.getSize()); - window.draw(search_background); - window.draw(search_text); - window.display(); - } - + XInitThreads(); + QuickMedia::Program program; + program.run(); return 0; } - |