From 4c6159e4d4be8e665d48c458ce26d4070fa4e57d Mon Sep 17 00:00:00 2001 From: dijunkun Date: Thu, 7 Nov 2024 16:29:02 +0800 Subject: [PATCH] [feat] write and load thumbnails supported --- src/common/platform.cpp | 23 +++ src/common/platform.h | 1 + src/device_controller/device_controller.h | 8 +- src/single_window/render.cpp | 138 ++------------- src/single_window/render.h | 7 + src/single_window/render_callback_func.cpp | 19 +++ src/single_window/thumbnail.cpp | 187 +++++++++++++++++++++ src/single_window/thumbnail.h | 40 +++++ 8 files changed, 300 insertions(+), 123 deletions(-) create mode 100644 src/single_window/thumbnail.cpp create mode 100644 src/single_window/thumbnail.h diff --git a/src/common/platform.cpp b/src/common/platform.cpp index f0e5f8e..eb4bc87 100644 --- a/src/common/platform.cpp +++ b/src/common/platform.cpp @@ -99,4 +99,27 @@ std::string GetMac() { close(sock); #endif return mac_addr; +} + +std::string GetHostName() { + char hostname[256]; +#ifdef _WIN32 + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { + std::cerr << "WSAStartup failed." << std::endl; + return ""; + } + if (gethostname(hostname, sizeof(hostname)) == SOCKET_ERROR) { + LOG_ERROR("gethostname failed: {}", WSAGetLastError()); + WSACleanup(); + return ""; + } + WSACleanup(); +#else + if (gethostname(hostname, sizeof(hostname)) == -1) { + LOG_ERROR("gethostname failed"); + return ""; + } +#endif + return hostname; } \ No newline at end of file diff --git a/src/common/platform.h b/src/common/platform.h index f9f15ae..121dbc1 100644 --- a/src/common/platform.h +++ b/src/common/platform.h @@ -10,5 +10,6 @@ #include std::string GetMac(); +std::string GetHostName(); #endif \ No newline at end of file diff --git a/src/device_controller/device_controller.h b/src/device_controller/device_controller.h index 53c861f..37f1762 100644 --- a/src/device_controller/device_controller.h +++ b/src/device_controller/device_controller.h @@ -9,7 +9,7 @@ #include -typedef enum { mouse = 0, keyboard, audio_capture } ControlType; +typedef enum { mouse = 0, keyboard, audio_capture, host_info } ControlType; typedef enum { move = 0, left_down, left_up, right_down, right_up } MouseFlag; typedef enum { key_down = 0, key_up } KeyFlag; typedef struct { @@ -23,11 +23,17 @@ typedef struct { KeyFlag flag; } Key; +typedef struct { + char host_name[64]; + size_t host_name_size; +} HostInfo; + typedef struct { ControlType type; union { Mouse m; Key k; + HostInfo i; bool a; }; } RemoteAction; diff --git a/src/single_window/render.cpp b/src/single_window/render.cpp index d18ebe5..7531b23 100644 --- a/src/single_window/render.cpp +++ b/src/single_window/render.cpp @@ -1,5 +1,6 @@ #include "render.h" +#include #include #include #include @@ -15,12 +16,6 @@ #include "rd_log.h" #include "screen_capturer_factory.h" -#define STB_IMAGE_IMPLEMENTATION -#include "stb_image.h" -#define STB_IMAGE_WRITE_IMPLEMENTATION -#include "libyuv.h" -#include "stb_image_write.h" - // Refresh Event #define REFRESH_EVENT (SDL_USEREVENT + 1) #define NV12_BUFFER_SIZE 1280 * 720 * 3 / 2 @@ -75,61 +70,6 @@ SDL_HitTestResult Render::HitTestCallback(SDL_Window* window, return SDL_HITTEST_NORMAL; } -bool LoadTextureFromMemory(const void* data, size_t data_size, - SDL_Renderer* renderer, SDL_Texture** out_texture, - int* out_width, int* out_height) { - int image_width = 0; - int image_height = 0; - int channels = 4; - unsigned char* image_data = - stbi_load_from_memory((const unsigned char*)data, (int)data_size, - &image_width, &image_height, NULL, 4); - if (image_data == nullptr) { - fprintf(stderr, "Failed to load image: %s\n", stbi_failure_reason()); - return false; - } - - SDL_Surface* surface = SDL_CreateRGBSurfaceFrom( - (void*)image_data, image_width, image_height, channels * 8, - channels * image_width, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000); - if (surface == nullptr) { - fprintf(stderr, "Failed to create SDL surface: %s\n", SDL_GetError()); - return false; - } - - SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); - if (texture == nullptr) - fprintf(stderr, "Failed to create SDL texture: %s\n", SDL_GetError()); - - *out_texture = texture; - *out_width = image_width; - *out_height = image_height; - - SDL_FreeSurface(surface); - stbi_image_free(image_data); - - return true; -} - -// Open and read a file, then forward to LoadTextureFromMemory() -bool LoadTextureFromFile(const char* file_name, SDL_Renderer* renderer, - SDL_Texture** out_texture, int* out_width, - int* out_height) { - FILE* f = fopen(file_name, "rb"); - if (f == NULL) return false; - fseek(f, 0, SEEK_END); - size_t file_size = (size_t)ftell(f); - if (file_size == -1) return false; - fseek(f, 0, SEEK_SET); - void* file_data = IM_ALLOC(file_size); - fread(file_data, 1, file_size, f); - bool ret = LoadTextureFromMemory(file_data, file_size, renderer, out_texture, - out_width, out_height); - IM_FREE(file_data); - fclose(f); - return ret; -} - Render::Render() {} Render::~Render() {} @@ -701,47 +641,6 @@ int Render::DrawMainWindow() { return 0; } -void ScaleYUV420pToABGR(char* dst_buffer_, int video_width_, int video_height_, - int scaled_video_width_, int scaled_video_height_, - char* argb_buffer_) { - int src_y_size = video_width_ * video_height_; - int src_uv_size = (video_width_ + 1) / 2 * (video_height_ + 1) / 2; - int dst_y_size = scaled_video_width_ * scaled_video_height_; - int dst_uv_size = - (scaled_video_width_ + 1) / 2 * (scaled_video_height_ + 1) / 2; - - uint8_t* src_y = reinterpret_cast(dst_buffer_); - uint8_t* src_u = src_y + src_y_size; - uint8_t* src_v = src_u + src_uv_size; - - std::unique_ptr dst_y(new uint8_t[dst_y_size]); - std::unique_ptr dst_u(new uint8_t[dst_uv_size]); - std::unique_ptr dst_v(new uint8_t[dst_uv_size]); - - try { - libyuv::I420Scale(src_y, video_width_, src_u, (video_width_ + 1) / 2, src_v, - (video_width_ + 1) / 2, video_width_, video_height_, - dst_y.get(), scaled_video_width_, dst_u.get(), - (scaled_video_width_ + 1) / 2, dst_v.get(), - (scaled_video_width_ + 1) / 2, scaled_video_width_, - scaled_video_height_, libyuv::kFilterBilinear); - } catch (const std::exception& e) { - LOG_ERROR("I420Scale failed: %s", e.what()); - return; - } - - try { - libyuv::I420ToABGR( - dst_y.get(), scaled_video_width_, dst_u.get(), - (scaled_video_width_ + 1) / 2, dst_v.get(), - (scaled_video_width_ + 1) / 2, reinterpret_cast(argb_buffer_), - scaled_video_width_ * 4, scaled_video_width_, scaled_video_height_); - } catch (const std::exception& e) { - LOG_ERROR("I420ToBGRA failed: %s", e.what()); - return; - } -} - int Render::DrawStreamWindow() { if (!stream_ctx_) { LOG_ERROR("Stream context is null"); @@ -842,8 +741,8 @@ int Render::Run() { CreateMainWindow(); SetupMainWindow(); - const int scaled_video_width_ = 128; - const int scaled_video_height_ = 72; + const int scaled_video_width_ = 160; + const int scaled_video_height_ = 90; char* argb_buffer_ = new char[scaled_video_width_ * scaled_video_height_ * 40]; @@ -904,13 +803,9 @@ int Render::Run() { DestroyStreamWindowContext(); if (dst_buffer_) { - ScaleYUV420pToABGR((char*)dst_buffer_, video_width_, video_height_, - scaled_video_width_, scaled_video_height_, - argb_buffer_); - stbi_write_png("RecentConnectionImage01.png", scaled_video_width_, - scaled_video_height_, 4, argb_buffer_, - scaled_video_width_ * 4); - LOG_ERROR("RecentConnectionImage01.png saved"); + thumbnail_.SaveToThumbnail((char*)dst_buffer_, video_width_, + video_height_, host_name_, remote_id_); + recent_connection_image_save_time_ = SDL_GetTicks(); } LOG_INFO("[{}] Leave connection [{}]", client_id_, remote_id_); @@ -1013,18 +908,17 @@ int Render::Run() { } if (reload_recent_connections_ && main_renderer_) { - bool ret = LoadTextureFromFile( - "RecentConnectionImage01.png", main_renderer_, - &recent_connection_texture_, &recent_connection_image_width_, - &recent_connection_image_height_); - if (!ret) { - LOG_ERROR("Load recent connections image failed"); - } else { - LOG_ERROR("Load recent connections image width: {}, height: {}", - recent_connection_image_width_, - recent_connection_image_height_); + // loal recent connection thumbnails after saving for 1 second + uint32_t now_time = SDL_GetTicks(); + if (now_time - recent_connection_image_save_time_ >= 1000) { + int ret = thumbnail_.LoadThumbnail( + main_renderer_, &recent_connection_texture_, + &recent_connection_image_width_, &recent_connection_image_height_); + if (!ret) { + LOG_INFO("Load recent connection thumbnails"); + } + reload_recent_connections_ = false; } - reload_recent_connections_ = false; } if (connection_established_ && streaming_) { diff --git a/src/single_window/render.h b/src/single_window/render.h index c447987..6b8cbfa 100644 --- a/src/single_window/render.h +++ b/src/single_window/render.h @@ -21,6 +21,7 @@ #include "imgui_impl_sdlrenderer2.h" #include "screen_capturer_factory.h" #include "speaker_capturer_factory.h" +#include "thumbnail.h" class Render { public: @@ -227,11 +228,16 @@ class Render { SDL_Texture *recent_connection_texture_ = nullptr; int recent_connection_image_width_ = 128; int recent_connection_image_height_ = 72; + uint32_t recent_connection_image_save_time_ = 0; // video window SDL_Texture *stream_texture_ = nullptr; SDL_Rect stream_render_rect_; uint32_t stream_pixformat_ = 0; + std::string host_name_ = ""; + std::string image_path_ = "thumbnails"; + + Thumbnail thumbnail_; bool resizable_ = false; bool label_inited_ = false; @@ -268,6 +274,7 @@ class Render { bool is_control_bar_in_left_ = true; bool control_window_width_is_changing_ = false; bool reload_recent_connections_ = true; + bool hostname_sent_ = false; double copy_start_time_ = 0; double regenerate_password_start_time_ = 0; diff --git a/src/single_window/render_callback_func.cpp b/src/single_window/render_callback_func.cpp index 1b1460b..bdc825f 100644 --- a/src/single_window/render_callback_func.cpp +++ b/src/single_window/render_callback_func.cpp @@ -1,5 +1,6 @@ #include "device_controller.h" #include "localization.h" +#include "platform.h" #include "rd_log.h" #include "render.h" @@ -209,6 +210,11 @@ void Render::OnReceiveDataBufferCb(const char *data, size_t size, } else { render->StopSpeakerCapture(); } + } else if (ControlType::keyboard == remote_action.type) { + } else if (ControlType::host_info == remote_action.type) { + render->host_name_ = + std::string(remote_action.i.host_name, remote_action.i.host_name_size); + LOG_INFO("Remote hostname: [{}]", render->host_name_); } } @@ -261,6 +267,18 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char *user_id, render->start_screen_capture_ = true; render->start_mouse_control_ = true; } + if (!render->hostname_sent_) { + std::string host_name = GetHostName(); + RemoteAction remote_action; + remote_action.type = ControlType::host_info; + memcpy(&remote_action.i.host_name, host_name.data(), host_name.size()); + remote_action.i.host_name_size = host_name.size(); + int ret = SendData(render->peer_, DATA_TYPE::DATA, + (const char *)&remote_action, sizeof(remote_action)); + if (0 == ret) { + render->hostname_sent_ = true; + } + } } else if (ConnectionStatus::Disconnected == status) { render->connection_status_str_ = "Disconnected"; render->password_validating_time_ = 0; @@ -274,6 +292,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char *user_id, render->start_mouse_control_ = false; render->connection_established_ = false; render->control_mouse_ = false; + render->hostname_sent_ = false; if (render->audio_capture_) { render->StopSpeakerCapture(); render->audio_capture_ = false; diff --git a/src/single_window/thumbnail.cpp b/src/single_window/thumbnail.cpp new file mode 100644 index 0000000..7dec53c --- /dev/null +++ b/src/single_window/thumbnail.cpp @@ -0,0 +1,187 @@ +#include "thumbnail.h" + +#include +#include +#include + +#include "libyuv.h" +#include "rd_log.h" + +#define STB_IMAGE_IMPLEMENTATION +#include "stb_image.h" +#define STB_IMAGE_WRITE_IMPLEMENTATION +#include "stb_image_write.h" + +void ScaleYUV420pToABGR(char* dst_buffer_, int video_width_, int video_height_, + int scaled_video_width_, int scaled_video_height_, + char* rgba_buffer_) { + int src_y_size = video_width_ * video_height_; + int src_uv_size = (video_width_ + 1) / 2 * (video_height_ + 1) / 2; + int dst_y_size = scaled_video_width_ * scaled_video_height_; + int dst_uv_size = + (scaled_video_width_ + 1) / 2 * (scaled_video_height_ + 1) / 2; + + uint8_t* src_y = reinterpret_cast(dst_buffer_); + uint8_t* src_u = src_y + src_y_size; + uint8_t* src_v = src_u + src_uv_size; + + std::unique_ptr dst_y(new uint8_t[dst_y_size]); + std::unique_ptr dst_u(new uint8_t[dst_uv_size]); + std::unique_ptr dst_v(new uint8_t[dst_uv_size]); + + try { + libyuv::I420Scale(src_y, video_width_, src_u, (video_width_ + 1) / 2, src_v, + (video_width_ + 1) / 2, video_width_, video_height_, + dst_y.get(), scaled_video_width_, dst_u.get(), + (scaled_video_width_ + 1) / 2, dst_v.get(), + (scaled_video_width_ + 1) / 2, scaled_video_width_, + scaled_video_height_, libyuv::kFilterBilinear); + } catch (const std::exception& e) { + LOG_ERROR("I420Scale failed: %s", e.what()); + return; + } + + try { + libyuv::I420ToABGR( + dst_y.get(), scaled_video_width_, dst_u.get(), + (scaled_video_width_ + 1) / 2, dst_v.get(), + (scaled_video_width_ + 1) / 2, reinterpret_cast(rgba_buffer_), + scaled_video_width_ * 4, scaled_video_width_, scaled_video_height_); + } catch (const std::exception& e) { + LOG_ERROR("I420ToRGBA failed: %s", e.what()); + return; + } +} + +Thumbnail::Thumbnail() { std::filesystem::create_directory(image_path_); } + +Thumbnail::~Thumbnail() { + if (rgba_buffer_) { + delete[] rgba_buffer_; + rgba_buffer_ = nullptr; + } +} + +int Thumbnail::SaveToThumbnail(const char* yuv420p, int width, int height, + const std::string& host_name, + const std::string& remote_id) { + if (!rgba_buffer_) { + rgba_buffer_ = new char[thumbnail_width_ * thumbnail_height_ * 4]; + } + + if (yuv420p) { + ScaleYUV420pToABGR((char*)yuv420p, width, height, thumbnail_width_, + thumbnail_height_, rgba_buffer_); + std::string image_name = + image_path_ + "/" + host_name + "@" + remote_id + ".png"; + stbi_write_png(image_name.data(), thumbnail_width_, thumbnail_height_, 4, + rgba_buffer_, thumbnail_width_ * 4); + } + return 0; +} + +bool LoadTextureFromMemory(const void* data, size_t data_size, + SDL_Renderer* renderer, SDL_Texture** out_texture, + int* out_width, int* out_height) { + int image_width = 0; + int image_height = 0; + int channels = 4; + unsigned char* image_data = + stbi_load_from_memory((const unsigned char*)data, (int)data_size, + &image_width, &image_height, NULL, 4); + if (image_data == nullptr) { + fprintf(stderr, "Failed to load image: %s\n", stbi_failure_reason()); + return false; + } + + // ABGR + SDL_Surface* surface = SDL_CreateRGBSurfaceFrom( + (void*)image_data, image_width, image_height, channels * 8, + channels * image_width, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000); + if (surface == nullptr) { + fprintf(stderr, "Failed to create SDL surface: %s\n", SDL_GetError()); + return false; + } + + SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface); + if (texture == nullptr) + fprintf(stderr, "Failed to create SDL texture: %s\n", SDL_GetError()); + + *out_texture = texture; + *out_width = image_width; + *out_height = image_height; + + SDL_FreeSurface(surface); + stbi_image_free(image_data); + + return true; +} + +bool LoadTextureFromFile(const char* file_name, SDL_Renderer* renderer, + SDL_Texture** out_texture, int* out_width, + int* out_height) { + std::filesystem::path file_path(file_name); + if (!std::filesystem::exists(file_path)) return false; + std::ifstream file(file_path, std::ios::binary); + if (!file) return false; + file.seekg(0, std::ios::end); + size_t file_size = file.tellg(); + file.seekg(0, std::ios::beg); + if (file_size == -1) return false; + char* file_data = new char[file_size]; + if (!file_data) return false; + file.read(file_data, file_size); + bool ret = LoadTextureFromMemory(file_data, file_size, renderer, out_texture, + out_width, out_height); + delete[] file_data; + + return ret; +} + +std::vector Thumbnail::FindThumbnailPath( + const std::filesystem::path& directory) { + std::vector thumbnails_path; + std::string image_extensions = ".png"; + + if (!std::filesystem::is_directory(directory)) { + LOG_ERROR("No such directory [{}]", directory.string()); + return thumbnails_path; + } + + thumbnails_sorted_by_write_time_.clear(); + + for (const auto& entry : std::filesystem::directory_iterator(directory)) { + if (entry.is_regular_file()) { + std::time_t last_write_time = std::chrono::system_clock::to_time_t( + time_point_cast( + entry.last_write_time() - + std::filesystem::file_time_type::clock::now() + + std::chrono::system_clock::now())); + + if (entry.path().extension() == image_extensions) { + thumbnails_sorted_by_write_time_[last_write_time] = entry.path(); + } + + for (const auto& pair : thumbnails_sorted_by_write_time_) { + thumbnails_path.push_back(pair.second); + } + } + } + + return thumbnails_path; +} + +int Thumbnail::LoadThumbnail(SDL_Renderer* renderer, SDL_Texture** texture, + int* width, int* height) { + std::vector image_path = + FindThumbnailPath(image_path_); + + if (image_path.size() == 0) { + LOG_INFO("No thumbnail saved", image_path_); + return -1; + } else { + LoadTextureFromFile(image_path[0].string().c_str(), renderer, texture, + width, height); + return 0; + } +} \ No newline at end of file diff --git a/src/single_window/thumbnail.h b/src/single_window/thumbnail.h new file mode 100644 index 0000000..58d654b --- /dev/null +++ b/src/single_window/thumbnail.h @@ -0,0 +1,40 @@ +/* + * @Author: DI JUNKUN + * @Date: 2024-11-07 + * Copyright (c) 2024 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _THUMBNAIL_H_ +#define _THUMBNAIL_H_ + +#include + +#include +#include + +class Thumbnail { + public: + Thumbnail(); + ~Thumbnail(); + + public: + int SaveToThumbnail(const char* yuv420p, int width, int height, + const std::string& host_name, + const std::string& remote_id); + + int LoadThumbnail(SDL_Renderer* renderer, SDL_Texture** texture, int* width, + int* height); + + private: + std::vector FindThumbnailPath( + const std::filesystem::path& directory); + + private: + int thumbnail_width_ = 160; + int thumbnail_height_ = 90; + char* rgba_buffer_ = nullptr; + std::string image_path_ = "thumbnails"; + std::map thumbnails_sorted_by_write_time_; +}; + +#endif \ No newline at end of file