From 1e0e68f9cda51c881b32a54d9eece71c1428f7ac Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 22 Apr 2018 05:58:44 +0200 Subject: Add video and gif support Gif streams from url. Todo: Add play controls to video --- depends/odhtdb | 2 +- include/Cache.hpp | 14 +++- include/Channel.hpp | 3 + include/Chatbar.hpp | 9 +++ include/FileUtil.hpp | 18 +++++ include/Gif.hpp | 47 +++++++++++++ include/Message.hpp | 6 +- include/MessageBoard.hpp | 1 - include/MessagePart.hpp | 7 +- include/StringView.hpp | 62 +++++++++++++++++ include/Video.hpp | 45 +++++++++++++ project.conf | 4 ++ src/Cache.cpp | 87 ++++++++++++++---------- src/Channel.cpp | 50 +++++--------- src/Chatbar.cpp | 76 +++++++++++++++++++-- src/FileUtil.cpp | 35 ++++++++++ src/Gif.cpp | 169 +++++++++++++++++++++++++++++++++++++++++++++++ src/Message.cpp | 40 +++++++++-- src/MessageBoard.cpp | 106 +++++++++++++++-------------- src/MessagePart.cpp | 8 +-- src/Video.cpp | 150 +++++++++++++++++++++++++++++++++++++++++ src/main.cpp | 9 ++- 22 files changed, 807 insertions(+), 141 deletions(-) create mode 100644 include/FileUtil.hpp create mode 100644 include/Gif.hpp create mode 100644 include/StringView.hpp create mode 100644 include/Video.hpp create mode 100644 src/FileUtil.cpp create mode 100644 src/Gif.cpp create mode 100644 src/Video.cpp diff --git a/depends/odhtdb b/depends/odhtdb index 59e86b8..13718f1 160000 --- a/depends/odhtdb +++ b/depends/odhtdb @@ -1 +1 @@ -Subproject commit 59e86b8b22c5ffb925b5a68b43de4ddc92986d53 +Subproject commit 13718f15767db6774dbf562b6798838ad9058984 diff --git a/include/Cache.hpp b/include/Cache.hpp index 59d997a..0314e7e 100644 --- a/include/Cache.hpp +++ b/include/Cache.hpp @@ -14,6 +14,8 @@ namespace TinyProcessLib namespace dchat { + class Gif; + struct ImageByUrlResult { enum class Type @@ -23,9 +25,19 @@ namespace dchat FAILED_DOWNLOAD }; + ImageByUrlResult() : texture(nullptr), type(Type::DOWNLOADING), isGif(false) {} + ImageByUrlResult(sf::Texture *_texture, Type _type) : texture(_texture), type(_type), isGif(false) {} + ImageByUrlResult(Gif *_gif, Type _type) : gif(_gif), type(_type), isGif(true) {} + // @texture is null if @type is DOWNLOADING or FAILED_DOWNLOAD - sf::Texture *texture; + union + { + sf::Texture *texture; + Gif *gif; + }; + Type type; + bool isGif; }; class Cache diff --git a/include/Channel.hpp b/include/Channel.hpp index fa52a4b..b70803c 100644 --- a/include/Channel.hpp +++ b/include/Channel.hpp @@ -13,6 +13,9 @@ namespace dchat Channel(); ~Channel(); + User* getLocalUser(); + MessageBoard& getMessageBoard(); + void processEvent(const sf::Event &event); void draw(sf::RenderWindow &window, Cache &cache); private: diff --git a/include/Chatbar.hpp b/include/Chatbar.hpp index 143ebba..d24b2af 100644 --- a/include/Chatbar.hpp +++ b/include/Chatbar.hpp @@ -3,9 +3,13 @@ #include #include #include +#include +#include namespace dchat { + class Channel; + class Chatbar { public: @@ -20,11 +24,16 @@ namespace dchat void moveCaretLeft(); void moveCaretRight(); + bool isFocused() const; + + void processEvent(const sf::Event &event, Channel *channel); void draw(sf::RenderWindow &window); private: sf::Text text; sf::RectangleShape background; int caretIndex; sf::Vector2f caretOffset; + sf::Clock blinkTimer; + bool focused; }; } diff --git a/include/FileUtil.hpp b/include/FileUtil.hpp new file mode 100644 index 0000000..0cfc808 --- /dev/null +++ b/include/FileUtil.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "StringView.hpp" +#include +#include + +namespace dchat +{ + class FileException : public std::runtime_error + { + public: + FileException(const std::string &errMsg) : std::runtime_error(errMsg) {} + }; + + // Throws FileException on error. + // Returned value is allocated with malloc and should be free'd by caller. + StringView getFileContent(const boost::filesystem::path &filepath); +} diff --git a/include/Gif.hpp b/include/Gif.hpp new file mode 100644 index 0000000..1a69a52 --- /dev/null +++ b/include/Gif.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "StringView.hpp" +#include +#include +#include +#include +#include +#include +extern "C" +{ +#include +} + +namespace dchat +{ + class GifLoadException : public std::runtime_error + { + public: + GifLoadException(const std::string &errMsg) : std::runtime_error(errMsg) {} + }; + + class Gif + { + public: + // Throws GifLoadException on error + Gif(const boost::filesystem::path &filepath); + Gif(StringView &&fileContent); + ~Gif(); + + void setPosition(const sf::Vector2f &position); + void setSize(const sf::Vector2f &size); + void draw(sf::RenderWindow &window); + + static bool isDataGif(const StringView &data); + private: + void init(); + private: + gif_animation gif; + StringView fileContent; + unsigned int currentFrame; + sf::Sprite sprite; + sf::Texture texture; + double timeElapsedCs; + sf::Clock frameTimer; + }; +} diff --git a/include/Message.hpp b/include/Message.hpp index efc1f4c..7cd7fdf 100644 --- a/include/Message.hpp +++ b/include/Message.hpp @@ -13,10 +13,12 @@ namespace dchat Message(User *user); virtual ~Message(); - void addText(const std::string &text); - void addImage(const std::string &url); + void addText(const std::string &text, bool newLine = true); + void addEmoji(const std::string &url, bool newLine = true); std::vector& getParts(); + static Message* buildFromString(User *user, const std::string &str); + const User *user; private: std::vector messageParts; diff --git a/include/MessageBoard.hpp b/include/MessageBoard.hpp index 105d675..c510164 100644 --- a/include/MessageBoard.hpp +++ b/include/MessageBoard.hpp @@ -22,7 +22,6 @@ namespace dchat void draw(sf::RenderWindow &window, Cache &cache); private: sf::RenderTexture staticContentTexture; - bool useStaticContentTexture; bool dirty; bool selectingText; bool leftMouseButtonPressed; diff --git a/include/MessagePart.hpp b/include/MessagePart.hpp index d0a0a03..00544fb 100644 --- a/include/MessagePart.hpp +++ b/include/MessagePart.hpp @@ -16,7 +16,7 @@ namespace dchat EMOJI }; - MessagePart(Type _type) : type(_type) {} + MessagePart(Type _type, bool _newLine) : type(_type), newLine(_newLine) {} virtual ~MessagePart(){} static float getSizeScaled(); @@ -24,12 +24,13 @@ namespace dchat virtual sf::Vector2f getSize() const = 0; const Type type; + bool newLine; }; class MessagePartText : public MessagePart { public: - MessagePartText(const std::string &text); + MessagePartText(const std::string &text, bool newLine); static float getFontSizeScaled(); virtual sf::Vector2f getPosition() const override; @@ -41,7 +42,7 @@ namespace dchat class MessagePartEmoji : public MessagePart { public: - MessagePartEmoji(const std::string &url); + MessagePartEmoji(const std::string &url, bool newLine); static float getHeightScaled(); virtual sf::Vector2f getPosition() const override; diff --git a/include/StringView.hpp b/include/StringView.hpp new file mode 100644 index 0000000..3293358 --- /dev/null +++ b/include/StringView.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "types.hpp" +#include +#include + +namespace dchat +{ + class StringView + { + public: + StringView() : data(nullptr), size(0) + { + + } + + StringView(const StringView &other) : data(other.data), size(other.size) + { + + } + + StringView(const char *_data) : data(_data), size(strlen(_data)) + { + + } + + StringView(const char *_data, usize _size) : data(_data), size(_size) + { + + } + + StringView operator = (const StringView &other) + { + StringView result(other.data, other.size); + return result; + } + + StringView(StringView &&other) + { + data = other.data; + size = other.size; + + other.data = nullptr; + other.size = 0; + } + + bool equals(const StringView &other) const + { + if(size != other.size) return false; + return memcmp(data, other.data, size) == 0; + } + + char operator [] (usize index) const + { + assert(index < size); + return data[index]; + } + + const char *data; + usize size; + }; +} diff --git a/include/Video.hpp b/include/Video.hpp new file mode 100644 index 0000000..f148c07 --- /dev/null +++ b/include/Video.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class mpv_handle; +class mpv_render_context; + +namespace dchat +{ + class VideoInitializationException : public std::runtime_error + { + public: + VideoInitializationException(const std::string &errMsg) : std::runtime_error(errMsg) {} + }; + + class Video + { + public: + // Throws VideoInitializationException on error + Video(unsigned int width, unsigned int height, const char *file, bool loop = false); + ~Video(); + + void setPosition(float x, float y); + 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_render_context *mpvGl; + std::thread renderThread; + std::mutex renderMutex; + sf::Sprite sprite; + sf::Texture texture; + sf::Uint8 *textureBuffer; + }; +} diff --git a/project.conf b/project.conf index d8e64f9..f2d4c9f 100644 --- a/project.conf +++ b/project.conf @@ -10,3 +10,7 @@ sfml-graphics = "2.4.2" sfml-system = "2.4.2" boost-filesystem = "1.66.0" tiny-process = "2.0.0" +mpv = "1.100.0" +gl = "18.0" +x11 = "1.6.5" +libnsgif = "0.2.0" diff --git a/src/Cache.cpp b/src/Cache.cpp index ba57d4c..accd0c4 100644 --- a/src/Cache.cpp +++ b/src/Cache.cpp @@ -1,6 +1,8 @@ #include "../include/Cache.hpp" #include "../include/env.hpp" #include "../include/ResourceCache.hpp" +#include "../include/FileUtil.hpp" +#include "../include/Gif.hpp" #include #include #include @@ -58,6 +60,40 @@ namespace dchat return dchatHomeDir; } + ImageByUrlResult loadImageFromFile(const boost::filesystem::path &filepath) + { + try + { + StringView fileContent = getFileContent(filepath); + if(Gif::isDataGif(fileContent)) + { + Gif *gif = new Gif(move(fileContent)); + return { gif, ImageByUrlResult::Type::CACHED }; + } + else + { + sf::Texture *texture = new sf::Texture(); + if(texture->loadFromMemory(fileContent.data, fileContent.size)) + { + delete fileContent.data; + texture->setSmooth(true); + texture->generateMipmap(); + return { texture, ImageByUrlResult::Type::CACHED }; + } + delete fileContent.data; + } + } + catch(FileException &e) + { + + } + catch(FailedToLoadResourceException &e) + { + + } + return { (sf::Texture*)nullptr, ImageByUrlResult::Type::FAILED_DOWNLOAD }; + } + Cache::Cache() { downloadWaitThread = thread([this] @@ -71,34 +107,24 @@ namespace dchat if(it->process->try_get_exit_status(exitStatus)) { bool failed = exitStatus != 0; - ImageByUrlResult &imageByUrlResult = imageUrlCache[it->url]; - if(!failed) { boost::filesystem::path filepath = getDchatDir(); odhtdb::Hash urlHash(it->url.data(), it->url.size()); filepath /= urlHash.toString(); - - try - { - sf::Texture *texture = ResourceCache::getTexture(filepath.string()); - imageByUrlResult.texture = texture; - imageByUrlResult.type = ImageByUrlResult::Type::CACHED; - printf("Image downloaded from url: %s, texture: %u\n", it->url.c_str(), texture); - } - catch(FailedToLoadResourceException &e) + + ImageByUrlResult imageByUrlResult = loadImageFromFile(filepath); + imageUrlCache[it->url] = imageByUrlResult; + switch(imageByUrlResult.type) { - fprintf(stderr, "%s\n", e.what()); - failed = true; + case ImageByUrlResult::Type::CACHED: + printf("Downloaded image from url: %s\n", it->url.c_str()); + break; + case ImageByUrlResult::Type::FAILED_DOWNLOAD: + printf("Failed to download and load image from url: %s\n", it->url.c_str()); + break; } } - - if(failed) - { - imageByUrlResult.type = ImageByUrlResult::Type::FAILED_DOWNLOAD; - fprintf(stderr, "Image download failed for url: %s\n", it->url.c_str()); - } - it = imageDownloadProcesses.erase(it); } else @@ -127,24 +153,15 @@ namespace dchat odhtdb::Hash urlHash(url.data(), url.size()); filepath /= urlHash.toString(); - // Check if file exists because we dont want sfml spam with "Failed to load image""... - if(boost::filesystem::exists(filepath)) + ImageByUrlResult imageByUrlResult = loadImageFromFile(filepath); + if(imageByUrlResult.type == ImageByUrlResult::Type::CACHED) { - try - { - sf::Texture *texture = ResourceCache::getTexture(filepath.string()); - ImageByUrlResult result { texture, ImageByUrlResult::Type::CACHED }; - imageUrlCache[url] = result; - printf("Loading image from file cache: %s\n", url.c_str()); - return result; - } - catch(FailedToLoadResourceException &e) - { - - } + imageUrlCache[url] = imageByUrlResult; + printf("Loaded image from file cache: %s, is gif: %s\n", url.c_str(), imageByUrlResult.isGif ? "yes" : "no"); + return imageByUrlResult; } - ImageByUrlResult result { nullptr, ImageByUrlResult::Type::DOWNLOADING }; + ImageByUrlResult result((sf::Texture*)nullptr, ImageByUrlResult::Type::DOWNLOADING); imageUrlCache[url] = result; string downloadLimitBytesStr = to_string(downloadLimitBytes); diff --git a/src/Channel.cpp b/src/Channel.cpp index 664e395..5e81f37 100644 --- a/src/Channel.cpp +++ b/src/Channel.cpp @@ -11,14 +11,15 @@ namespace dchat { { Message *message = new Message(&localOfflineUser); - message->addText(u8"hello, worldåäö1!"); - message->addImage("https://discordemoji.com/assets/emoji/playtime.png"); + message->addText(u8"hello, worldåäö1!", false); + message->addEmoji("https://discordemoji.com/assets/emoji/playtime.png"); messageBoard.addMessage(message); } { Message *message = new Message(&localOfflineUser); - message->addText(u8"hello, world2!"); + message->addText(u8"hello, world2!", false); + message->addEmoji("https://discordemoji.com/assets/emoji/Feels3DMan.gif"); messageBoard.addMessage(message); } @@ -34,40 +35,19 @@ namespace dchat } + User* Channel::getLocalUser() + { + return &localOfflineUser; + } + + MessageBoard& Channel::getMessageBoard() + { + return messageBoard; + } + void Channel::processEvent(const sf::Event &event) { - if(event.type == sf::Event::TextEntered) - { - if(event.text.unicode == 8) // backspace - chatbar.removePreviousChar(); - else if(event.text.unicode == 13) // enter - { - Message *message = new Message(&localOfflineUser); - auto chatbarMsgUtf8 = chatbar.getString().toUtf8(); - string msg; - msg.resize(chatbarMsgUtf8.size()); - memcpy(&msg[0], chatbarMsgUtf8.data(), chatbarMsgUtf8.size()); - - message->addText(msg); - messageBoard.addMessage(message); - chatbar.clear(); - } - else if(event.text.unicode == 127) // delete - { - chatbar.removeNextChar(); - } - else - { - chatbar.addChar(event.text.unicode); - } - } - else if(event.type == sf::Event::KeyPressed) - { - if(event.key.code == sf::Keyboard::Left) - chatbar.moveCaretLeft(); - else if(event.key.code == sf::Keyboard::Right) - chatbar.moveCaretRight(); - } + chatbar.processEvent(event, this); messageBoard.processEvent(event); } diff --git a/src/Chatbar.cpp b/src/Chatbar.cpp index 14edba7..bdfc75d 100644 --- a/src/Chatbar.cpp +++ b/src/Chatbar.cpp @@ -1,7 +1,9 @@ #include "../include/Chatbar.hpp" #include "../include/ResourceCache.hpp" #include "../include/Settings.hpp" +#include "../include/Channel.hpp" #include +#include using namespace std; @@ -10,10 +12,13 @@ namespace dchat const float FONT_SIZE = 24; const float BOX_PADDING_X = 15.0f; const float BOX_PADDING_Y = 5.0f; + const int BLINK_TIME_VISIBLE_MS = 500; + const int BLINK_TIME_INVISIBLE_MS = 500; Chatbar::Chatbar() : text("", ResourceCache::getFont("fonts/Roboto-Regular.ttf"), FONT_SIZE * Settings::getScaling()), - caretIndex(0) + caretIndex(0), + focused(true) { text.setFillColor(sf::Color(240, 240, 240)); background.setFillColor(sf::Color(60, 60, 60)); @@ -26,6 +31,7 @@ namespace dchat text.setString(str); ++caretIndex; caretOffset = text.findCharacterPos(caretIndex) - text.getPosition(); + blinkTimer.restart(); } const sf::String& Chatbar::getString() const @@ -43,6 +49,7 @@ namespace dchat --caretIndex; caretOffset = text.findCharacterPos(caretIndex) - text.getPosition(); } + blinkTimer.restart(); } void Chatbar::removeNextChar() @@ -53,6 +60,7 @@ namespace dchat str.erase(caretIndex); text.setString(str); } + blinkTimer.restart(); } void Chatbar::clear() @@ -61,6 +69,7 @@ namespace dchat caretIndex = 0; caretOffset.x = 0.0f; caretOffset.y = 0.0f; + blinkTimer.restart(); } void Chatbar::moveCaretLeft() @@ -68,12 +77,64 @@ namespace dchat caretIndex = max(0, caretIndex - 1); // TODO: Use glyph size to optimize this, no need to iterate all glyphs caretOffset = text.findCharacterPos(caretIndex) - text.getPosition(); + blinkTimer.restart(); } void Chatbar::moveCaretRight() { caretIndex = min((int)text.getString().getSize(), caretIndex + 1); caretOffset = text.findCharacterPos(caretIndex) - text.getPosition(); + blinkTimer.restart(); + } + + bool Chatbar::isFocused() const + { + return focused; + } + + void Chatbar::processEvent(const sf::Event &event, Channel *channel) + { + if(!focused) return; + + if(event.type == sf::Event::TextEntered) + { + if(event.text.unicode == 8) // backspace + removePreviousChar(); + else if(event.text.unicode == 13) // enter + { + if(!getString().isEmpty()) + { + if(sf::Keyboard::isKeyPressed(sf::Keyboard::Key::LShift) || sf::Keyboard::isKeyPressed(sf::Keyboard::Key::RShift)) + { + addChar('\n'); + } + else + { + auto chatbarMsgUtf8 = getString().toUtf8(); + string msg; + msg.resize(chatbarMsgUtf8.size()); + memcpy(&msg[0], chatbarMsgUtf8.data(), chatbarMsgUtf8.size()); + channel->getMessageBoard().addMessage(Message::buildFromString(channel->getLocalUser(), msg)); + clear(); + } + } + } + else if(event.text.unicode == 127) // delete + { + removeNextChar(); + } + else + { + addChar(event.text.unicode); + } + } + else if(event.type == sf::Event::KeyPressed) + { + if(event.key.code == sf::Keyboard::Left) + moveCaretLeft(); + else if(event.key.code == sf::Keyboard::Right) + moveCaretRight(); + } } void Chatbar::draw(sf::RenderWindow &window) @@ -89,8 +150,15 @@ namespace dchat window.draw(background); window.draw(text); - sf::RectangleShape caretShape(sf::Vector2f(2.0f, backgroundSize.y - BOX_PADDING_Y * 2.0f)); - caretShape.setPosition(floor(text.getPosition().x + caretOffset.x), backgroundPos.y + BOX_PADDING_Y); - window.draw(caretShape); + int blinkElapsedTime = blinkTimer.getElapsedTime().asMilliseconds(); + if(focused && blinkElapsedTime <= BLINK_TIME_VISIBLE_MS) + { + sf::RectangleShape caretShape(sf::Vector2f(2.0f, backgroundSize.y - BOX_PADDING_Y * 2.0f)); + caretShape.setPosition(floor(text.getPosition().x + caretOffset.x), (caretOffset.y + backgroundPos.y + BOX_PADDING_Y)); + window.draw(caretShape); + } + + if(blinkElapsedTime > BLINK_TIME_VISIBLE_MS + BLINK_TIME_INVISIBLE_MS) + blinkTimer.restart(); } } diff --git a/src/FileUtil.cpp b/src/FileUtil.cpp new file mode 100644 index 0000000..53687dd --- /dev/null +++ b/src/FileUtil.cpp @@ -0,0 +1,35 @@ +#include "../include/FileUtil.hpp" +#include "../include/env.hpp" +#include + +using namespace std; + +namespace dchat +{ + StringView getFileContent(const boost::filesystem::path &filepath) + { +#if OS_FAMILY == OS_FAMILY_POSIX + FILE *file = fopen(filepath.c_str(), "rb"); +#else + FILE *file = _wfopen(filepath.c_str(), L"rb"); +#endif + if(!file) + { + int error = errno; + string errMsg = "Failed to open file: "; + errMsg += filepath.string(); + errMsg += "; reason: "; + errMsg += strerror(error); + throw FileException(errMsg); + } + + fseek(file, 0, SEEK_END); + size_t fileSize = ftell(file); + fseek(file, 0, SEEK_SET); + + char *fileData = new char[fileSize]; + fread(fileData, 1, fileSize, file); + fclose(file); + return { fileData, fileSize }; + } +} diff --git a/src/Gif.cpp b/src/Gif.cpp new file mode 100644 index 0000000..3f7216e --- /dev/null +++ b/src/Gif.cpp @@ -0,0 +1,169 @@ +#include "../include/Gif.hpp" +#include "../include/FileUtil.hpp" + +using namespace std; + +namespace dchat +{ + void* bitmapCreate(int width, int height) + { + return calloc(width * height, 4); + } + + void bitmapDestroy(void *bitmap) + { + free(bitmap); + } + + unsigned char* bitmapGetBuffer(void *bitmap) + { + return (unsigned char*)bitmap; + } + + void bitmapSetOpaque(void *bitmap, bool opaque) + { + + } + + bool bitmapTestOpaque(void *bitmap) + { + return false; + } + + void bitmapModified(void *bitmap) + { + + } + + const char* gifResultToString(gif_result code) + { + switch(code) + { + case GIF_INSUFFICIENT_FRAME_DATA: + return "GIF_INSUFFICIENT_FRAME_DATA"; + case GIF_FRAME_DATA_ERROR: + return "GIF_FRAME_DATA_ERROR"; + case GIF_INSUFFICIENT_DATA: + return "GIF_INSUFFICIENT_DATA"; + case GIF_DATA_ERROR: + return "GIF_DATA_ERROR"; + case GIF_INSUFFICIENT_MEMORY: + return "GIF_INSUFFICIENT_MEMORY"; + default: + return "Unknown gif result code"; + } + } + + Gif::Gif(const boost::filesystem::path &filepath) : + currentFrame(0), + timeElapsedCs(0.0) + { + try + { + fileContent = getFileContent(filepath); + } + catch(FileException &e) + { + throw GifLoadException(e.what()); + } + init(); + } + + Gif::Gif(StringView &&_fileContent) : + fileContent(move(_fileContent)), + currentFrame(0), + timeElapsedCs(0.0) + { + init(); + } + + void Gif::init() + { + gif_bitmap_callback_vt bitmapCallbacks = + { + bitmapCreate, + bitmapDestroy, + bitmapGetBuffer, + bitmapSetOpaque, + bitmapTestOpaque, + bitmapModified + }; + + gif_create(&gif, &bitmapCallbacks); + + gif_result code; + do + { + code = gif_initialise(&gif, fileContent.size, (unsigned char*)fileContent.data); + if(code != GIF_OK && code != GIF_WORKING) + { + string errMsg = "Failed to initialize gif, reason: "; + errMsg += gifResultToString(code); + throw GifLoadException(errMsg); + } + } + while(code != GIF_OK); + + if(!texture.create(gif.width, gif.height)) + throw GifLoadException("Failed to create texture for gif"); + + sprite.setTexture(texture, true); + } + + Gif::~Gif() + { + gif_finalise(&gif); + delete fileContent.data; + } + + void Gif::setPosition(const sf::Vector2f &position) + { + sprite.setPosition(position); + } + + void Gif::setSize(const sf::Vector2f &size) + { + sf::Vector2u textureSize = sprite.getTexture()->getSize(); + sprite.setScale(size.x / (float)textureSize.x, size.y / (float)textureSize.y); + } + + void Gif::draw(sf::RenderWindow &window) + { + double frameDeltaCs = (double)frameTimer.getElapsedTime().asMilliseconds() * 0.1; // Centisecond + frameTimer.restart(); + timeElapsedCs += frameDeltaCs; + + unsigned char *image = nullptr; + u32 startFrame = currentFrame; + while(true) + { + int i = currentFrame % gif.frame_count; + gif_result code = gif_decode_frame(&gif, i); + if(code != GIF_OK) + printf("Warning: gif_decode_frame: %s\n", gifResultToString(code)); + + gif_frame &frame = gif.frames[i]; + // frame_delay is in centiseconds + double frameDelay = (double)frame.frame_delay; + if(timeElapsedCs >= frameDelay) + timeElapsedCs -= frameDelay; + else + break; + + image = (unsigned char*)gif.frame_image; + ++currentFrame; + } + + if(currentFrame != startFrame) + { + texture.update(image); + sprite.setTexture(texture, true); + } + window.draw(sprite); + } + + bool Gif::isDataGif(const StringView &data) + { + return data.size >= 6 && (memcmp(data.data, "GIF87a", 6) == 0 || memcmp(data.data, "GIF89a", 6) == 0); + } +} diff --git a/src/Message.cpp b/src/Message.cpp index 2740c11..19630b3 100644 --- a/src/Message.cpp +++ b/src/Message.cpp @@ -1,4 +1,5 @@ #include "../include/Message.hpp" +#include "../include/StringView.hpp" using namespace std; @@ -18,18 +19,49 @@ namespace dchat } } - void Message::addText(const string &text) + void Message::addText(const string &text, bool newLine) { - messageParts.push_back(new MessagePartText(text)); + messageParts.push_back(new MessagePartText(text, newLine)); } - void Message::addImage(const string &url) + void Message::addEmoji(const string &url, bool newLine) { - messageParts.push_back(new MessagePartEmoji(url)); + messageParts.push_back(new MessagePartEmoji(url, newLine)); } vector& Message::getParts() { return messageParts; } + + StringView getNextNewLine(const StringView &str) + { + for(usize i = 0; i < str.size; ++i) + { + if(str[i] == '\n') + return StringView(str.data, i); + } + return StringView(); + } + + Message* Message::buildFromString(User *user, const std::string &str) + { + Message *message = new Message(user); + usize strOffset = 0; + while(strOffset < str.size()) + { + usize foundIndex = str.find('\n', strOffset); + usize lineEnd = foundIndex; + if(foundIndex == string::npos) + lineEnd = str.size(); + + message->addText(str.substr(strOffset, lineEnd - strOffset), foundIndex != string::npos); + + if(foundIndex == string::npos) + break; + else + strOffset = lineEnd + 1; + } + return message; + } } diff --git a/src/MessageBoard.cpp b/src/MessageBoard.cpp index 575ae5f..d39e8be 100644 --- a/src/MessageBoard.cpp +++ b/src/MessageBoard.cpp @@ -1,6 +1,7 @@ #include "../include/MessageBoard.hpp" #include "../include/Settings.hpp" #include "../include/ResourceCache.hpp" +#include "../include/Gif.hpp" #include #include #include @@ -32,7 +33,8 @@ namespace dchat void MessageBoard::updateStaticContentTexture(const sf::Vector2u &newSize) { - useStaticContentTexture = staticContentTexture.create(newSize.x, newSize.y); + if(!staticContentTexture.create(newSize.x, newSize.y)) + throw std::runtime_error("Failed to create render target for message board!"); dirty = true; } @@ -86,32 +88,15 @@ namespace dchat void MessageBoard::draw(sf::RenderWindow &window, Cache &cache) { - sf::RenderTarget *renderTarget = nullptr; - if(useStaticContentTexture) - { - renderTarget = &staticContentTexture; - if(window.getSize() != staticContentTexture.getSize()) - updateStaticContentTexture(window.getSize()); - } - else - { - renderTarget = &window; - dirty = true; - } + auto windowSize = window.getSize(); + sf::Vector2u backgroundSize(floor(windowSize.x * 0.7f), floor(windowSize.y)); + sf::Vector2f backgroundPos(floor(windowSize.x * 0.5f - backgroundSize.x * 0.5f), 0.0f); - auto renderTargetSize = renderTarget->getSize(); + if(backgroundSize != staticContentTexture.getSize()) + updateStaticContentTexture(backgroundSize); - if(useStaticContentTexture) - { - if(dirty) - staticContentTexture.clear(BACKGROUND_COLOR); - } - else - { - sf::RectangleShape background(sf::Vector2f(renderTargetSize.x, renderTargetSize.y)); - background.setFillColor(BACKGROUND_COLOR); - renderTarget->draw(background); - } + if(dirty) + staticContentTexture.clear(sf::Color::Transparent); const sf::Font &usernameFont = ResourceCache::getFont("fonts/Roboto-Regular.ttf"); @@ -122,9 +107,11 @@ namespace dchat usernameText.setFillColor(sf::Color(15, 192, 252)); usernameText.setPosition(position); if(dirty) - renderTarget->draw(usernameText); + staticContentTexture.draw(usernameText); position.y += usernameText.getCharacterSize() + USERNAME_PADDING_BOTTOM; + int index = 0; + int numParts = message->getParts().size(); for(MessagePart *messagePart : message->getParts()) { switch(messagePart->type) @@ -136,36 +123,50 @@ namespace dchat messagePartText->text.setCharacterSize(MessagePartText::getFontSizeScaled()); messagePartText->text.setPosition(floor(position.x), floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartText::getFontSizeScaled() * 0.5f)); if(dirty) - renderTarget->draw(messagePartText->text); + staticContentTexture.draw(messagePartText->text); position.x += messagePartText->text.getLocalBounds().width; break; } case MessagePart::Type::EMOJI: { MessagePartEmoji *messagePartEmoji = static_cast(messagePart); - // Emoji is dirty when it's created, but render target can become dirty after emoji has been added, so we need to set emoji as dirty then - if(dirty) - messagePartEmoji->dirty = true; - auto imageByUrlResult = cache.getImageByUrl(messagePartEmoji->url, 1024 * 512); position.x += 5.0f; - if(imageByUrlResult.texture) + auto imageByUrlResult = cache.getImageByUrl(messagePartEmoji->url, 1024 * 512); + bool imageDrawn = false; + if(imageByUrlResult.isGif && imageByUrlResult.gif) { - // TODO: Verify this doesn't cause lag - messagePartEmoji->sprite.setTexture(*imageByUrlResult.texture, true); - sf::Vector2f spriteSize(MessagePartEmoji::getHeightScaled(), MessagePartEmoji::getHeightScaled()); - messagePartEmoji->sprite.setScale(spriteSize.x / (float)imageByUrlResult.texture->getSize().x, spriteSize.y / (float)imageByUrlResult.texture->getSize().y); - messagePartEmoji->sprite.setPosition(floor(position.x), floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartEmoji::getHeightScaled() * 0.5f)); - if(messagePartEmoji->dirty) + sf::Vector2f pos(backgroundPos.x + floor(position.x), backgroundPos.y + floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartEmoji::getHeightScaled() * 0.5f)); + imageByUrlResult.gif->setPosition(pos); + imageByUrlResult.gif->setSize(sf::Vector2f(MessagePartEmoji::getHeightScaled(), MessagePartEmoji::getHeightScaled())); + imageByUrlResult.gif->draw(window); + imageDrawn = true; + } + else + { + // Emoji is dirty when it's created, but render target can become dirty after emoji has been added, so we need to set emoji as dirty then + if(dirty) + messagePartEmoji->dirty = true; + if(imageByUrlResult.texture) { - messagePartEmoji->dirty = false; - renderTarget->draw(messagePartEmoji->sprite); + // TODO: Verify this doesn't cause lag + messagePartEmoji->sprite.setTexture(*imageByUrlResult.texture, true); + sf::Vector2f spriteSize(MessagePartEmoji::getHeightScaled(), MessagePartEmoji::getHeightScaled()); + messagePartEmoji->sprite.setScale(spriteSize.x / (float)imageByUrlResult.texture->getSize().x, spriteSize.y / (float)imageByUrlResult.texture->getSize().y); + messagePartEmoji->sprite.setPosition(floor(position.x), floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartEmoji::getHeightScaled() * 0.5f)); + if(messagePartEmoji->dirty) + { + messagePartEmoji->dirty = false; + staticContentTexture.draw(messagePartEmoji->sprite); + } + imageDrawn = true; } } - else + + if(!imageDrawn) { // TODO: Replace this with a loading gif sf::RectangleShape emojiDownloadRect(sf::Vector2f(MessagePartEmoji::getHeightScaled(), MessagePartEmoji::getHeightScaled())); - emojiDownloadRect.setPosition(floor(position.x), floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartEmoji::getHeightScaled() * 0.5f)); + emojiDownloadRect.setPosition(backgroundPos.x + floor(position.x), backgroundPos.y + floor(position.y + MessagePart::getSizeScaled() * 0.5f - MessagePartEmoji::getHeightScaled() * 0.5f)); emojiDownloadRect.setFillColor(sf::Color::White); window.draw(emojiDownloadRect); } @@ -173,25 +174,33 @@ namespace dchat break; } } + + if(index < numParts - 1 && messagePart->newLine) + { + position.x = 0.0f; + position.y += MessagePart::getSizeScaled(); + } + ++index; } position.x = 0.0f; position.y += MessagePart::getSizeScaled() + MESSAGE_PADDING_BOTTOM; } - if(useStaticContentTexture) - staticContentTexture.display(); - + staticContentTexture.display(); dirty = false; - if(useStaticContentTexture) - window.draw(sf::Sprite(staticContentTexture.getTexture())); + // TODO: Save this, expensive to create on fly? + sf::Sprite textureSprite(staticContentTexture.getTexture()); + textureSprite.setPosition(backgroundPos); + window.draw(textureSprite); if(!selectingText) return; sf::Vector2f selectionRectStart(min((float)mousePos.x, selectingTextStart.x), min((float)mousePos.y, selectingTextStart.y)); sf::Vector2f selectionRectEnd(max((float)mousePos.x, selectingTextStart.x), max((float)mousePos.y, selectingTextStart.y)); sf::FloatRect selectionRect(selectionRectStart, selectionRectEnd - selectionRectStart); - +#if 0 + // TODO: Remove this, put logic in render loop above for(Message *message : messages) { float messagePartStartX = -999.0f; @@ -260,5 +269,6 @@ namespace dchat window.draw(selectionShape); } } +#endif } } diff --git a/src/MessagePart.cpp b/src/MessagePart.cpp index 1bde138..215c239 100644 --- a/src/MessagePart.cpp +++ b/src/MessagePart.cpp @@ -13,8 +13,8 @@ namespace dchat return MESSAGE_PART_SIZE * Settings::getScaling(); } - MessagePartText::MessagePartText(const string &_text) : - MessagePart(Type::TEXT), + MessagePartText::MessagePartText(const string &_text, bool _newLine) : + MessagePart(Type::TEXT, _newLine), text("", ResourceCache::getFont("fonts/Roboto-Regular.ttf"), MessagePartText::getFontSizeScaled()) { text.setString(sf::String::fromUtf8(_text.begin(), _text.end())); @@ -35,8 +35,8 @@ namespace dchat return sf::Vector2f(text.getLocalBounds().width, getFontSizeScaled()); } - MessagePartEmoji::MessagePartEmoji(const string &_url) : - MessagePart(Type::EMOJI), + MessagePartEmoji::MessagePartEmoji(const string &_url, bool _newLine) : + MessagePart(Type::EMOJI, _newLine), url(_url), dirty(true) { diff --git a/src/Video.cpp b/src/Video.cpp new file mode 100644 index 0000000..4ef16d6 --- /dev/null +++ b/src/Video.cpp @@ -0,0 +1,150 @@ +#include "../include/Video.hpp" +#include +#include +#include + +#include + +#if defined(SFML_SYSTEM_WINDOWS) + #ifdef _MSC_VER + #include + #endif + #include + #include +#elif defined(SFML_SYSTEM_LINUX) || defined(SFML_SYSTEM_FREEBSD) + #if defined(SFML_OPENGL_ES) + #include + #include + #else + #include + #endif + #include + #define glGetProcAddress glXGetProcAddress +#elif defined(SFML_SYSTEM_MACOS) + #include +#elif defined (SFML_SYSTEM_IOS) + #include + #include +#elif defined (SFML_SYSTEM_ANDROID) + #include + #include + // We're not using OpenGL ES 2+ yet, but we can use the sRGB extension + #include +#endif + +using namespace std; + +namespace dchat +{ + void* getProcAddressMpv(void *funcContext, const char *name) + { + return (void*)glGetProcAddress((const GLubyte*)name); + } + + void onMpvRedraw(void *rawVideo) + { + Video *video = (Video*)rawVideo; + ++video->redrawCounter; + } + + Video::Video(unsigned int width, unsigned int height, const char *file, bool loop) : + redrawCounter(0), + context(sf::ContextSettings(), width, height), + mpv(nullptr), + mpvGl(nullptr), + textureBuffer(new sf::Uint8[width * height * 4]) // 4 = red, green, blue and alpha + { + 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_opengl_init_params openglInitParams { .get_proc_address = getProcAddressMpv }; + mpv_render_param params[] = + { + { MPV_RENDER_PARAM_API_TYPE, (void*)MPV_RENDER_API_TYPE_OPENGL }, + { MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &openglInitParams }, + { (mpv_render_param_type)0, nullptr } + }; + + if(mpv_render_context_create(&mpvGl, mpv, params) < 0) + throw VideoInitializationException("Failed to initialize mpv opengl render context"); + + if(loop) + mpv_set_option_string(mpv, "loop", "inf"); + mpv_set_option_string(mpv, "hwdec", "auto"); + mpv_render_context_set_update_callback(mpvGl, onMpvRedraw, this); + + renderThread = thread([this, width, height]() + { + context.setActive(true); + while(true) + { + while(true) + { + mpv_event *mpvEvent = mpv_wait_event(mpv, 0.0); + if(mpvEvent->event_id == MPV_EVENT_NONE) + break; + else if(mpvEvent->event_id == MPV_EVENT_SHUTDOWN) + break; + } + + if(redrawCounter > 0) + { + --redrawCounter; + mpv_opengl_fbo openglFbo { .fbo = 0, .w = (int)width, .h = (int)height }; + mpv_render_param params[] = + { + { MPV_RENDER_PARAM_OPENGL_FBO, &openglFbo }, + { (mpv_render_param_type)0, nullptr } + }; + + context.setActive(true); + renderMutex.lock(); + mpv_render_context_render(mpvGl, params); + glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, textureBuffer); + texture.update(textureBuffer); + sprite.setTexture(texture, true); + renderMutex.unlock(); + } + this_thread::sleep_for(chrono::milliseconds(10)); + } + }); + renderThread.detach(); + + const char *cmd[] = { "loadfile", file, nullptr }; + mpv_command(mpv, cmd); + context.setActive(false); + } + + Video::~Video() + { + lock_guard lock(renderMutex); + delete[] textureBuffer; + mpv_render_context_free(mpvGl); + mpv_destroy(mpv); + if(renderThread.joinable()) + renderThread.join(); + } + + void Video::setPosition(float x, float y) + { + sprite.setPosition(x, y); + } + + void Video::draw(sf::RenderWindow &window) + { + lock_guard lock(renderMutex); + window.draw(sprite); + } +} diff --git a/src/main.cpp b/src/main.cpp index 284df56..3ca986f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,7 +4,7 @@ #include #include #include -#include "process.hpp" +#include using namespace std; using namespace dchat; @@ -12,16 +12,19 @@ using namespace TinyProcessLib; int main() { + XInitThreads(); sf::RenderWindow window(sf::VideoMode(1920, 1080), "dchat"); window.setVerticalSyncEnabled(false); window.setFramerateLimit(60); + //odhtdb::Database database("bootstrap.ring.cx", 4222, Cache::getDchatDir()); + Cache cache; Channel channel; ChannelSidePanel channelSidePanel; channelSidePanel.addChannel(&channel); - + while (window.isOpen()) { sf::Event event; @@ -37,7 +40,7 @@ int main() channel.processEvent(event); } - window.clear(); + window.clear(sf::Color(40, 40, 40)); channel.draw(window, cache); window.display(); } -- cgit v1.2.3