From e1c8cd4430015a307dbcb32894a030d5a13aee67 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Thu, 14 Oct 2021 16:59:27 +0200 Subject: Remove async image loader threads and instead check if the curl download process has finished --- include/AsyncImageLoader.hpp | 33 ++++++--- include/MessageQueue.hpp | 4 + src/AsyncImageLoader.cpp | 169 ++++++++++++++++++++++++++----------------- src/QuickMedia.cpp | 1 + src/plugins/Youtube.cpp | 32 ++++++++ 5 files changed, 160 insertions(+), 79 deletions(-) diff --git a/include/AsyncImageLoader.hpp b/include/AsyncImageLoader.hpp index 6258b02..77fd5b4 100644 --- a/include/AsyncImageLoader.hpp +++ b/include/AsyncImageLoader.hpp @@ -26,11 +26,19 @@ namespace QuickMedia { size_t counter = 0; }; + struct ThumbnailLoadData { + Path path; + Path thumbnail_path; + bool local; + std::shared_ptr thumbnail_data; + sf::Vector2i resize_target_size; + }; + // If |symlink_if_no_resize| is false then a copy is made from |thumbnail_path| to |thumbnail_path_resized| instead of a symlink if |thumbnail_path| is not larger than |resize_target_size|. // One example of why you might not want a symlink is if |thumbnail_path| is a temporary file. bool create_thumbnail(const Path &thumbnail_path, const Path &thumbnail_path_resized, sf::Vector2i resize_target_size, ContentType content_type, bool symlink_if_no_resize); - constexpr int NUM_IMAGE_LOAD_THREADS = 4; + constexpr int NUM_IMAGE_LOAD_PARALLEL = 4; class AsyncImageLoader { public: @@ -51,14 +59,6 @@ namespace QuickMedia { AsyncImageLoader(AsyncImageLoader &other) = delete; AsyncImageLoader& operator=(AsyncImageLoader &other) = delete; - struct ThumbnailLoadData { - Path path; - Path thumbnail_path; - bool local; - std::shared_ptr thumbnail_data; - sf::Vector2i resize_target_size; - }; - // set |resize_target_size| to {0, 0} to disable resizing. // Note: this method is not thread-safe void load_thumbnail(const std::string &url, bool local, sf::Vector2i resize_target_size, std::shared_ptr thumbnail_data); @@ -66,10 +66,19 @@ namespace QuickMedia { // Returns -1 if all threads are busy int get_free_load_index() const; private: - bool loading_image[NUM_IMAGE_LOAD_THREADS]; + struct Download { + ReadProgram read_program; + int64_t download_start = 0; + Path thumbnail_path; + std::shared_ptr thumbnail_data; + sf::Vector2i resize_target_size; + std::string url; + }; + + std::mutex download_mutex; // TODO: Use curl single-threaded multi-download feature instead - AsyncTask download_image_thread[NUM_IMAGE_LOAD_THREADS]; - AsyncTask load_image_thread; + Download downloads[NUM_IMAGE_LOAD_PARALLEL]; + AsyncTask load_thread; MessageQueue image_load_queue; std::unordered_map> thumbnails; size_t counter = 0; diff --git a/include/MessageQueue.hpp b/include/MessageQueue.hpp index 286739d..ba28431 100644 --- a/include/MessageQueue.hpp +++ b/include/MessageQueue.hpp @@ -68,6 +68,10 @@ namespace QuickMedia { ++it; } } + + bool is_running() const { + return running; + } private: std::deque data_queue; std::mutex mutex; diff --git a/src/AsyncImageLoader.cpp b/src/AsyncImageLoader.cpp index a871078..fce745a 100644 --- a/src/AsyncImageLoader.cpp +++ b/src/AsyncImageLoader.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -128,6 +129,7 @@ namespace QuickMedia { // parent + // TODO: Do not wait here and instead check if finished in |load_thread| int status = 0; if(waitpid(pid, &status, 0) == -1) { perror("waitpid failed"); @@ -176,44 +178,82 @@ namespace QuickMedia { return *instance; } + static void process_thumbnail(ThumbnailLoadData &thumbnail_load_data) { + thumbnail_load_data.thumbnail_data->image = std::make_unique(); + + Path thumbnail_path_resized = thumbnail_load_data.thumbnail_path; + if(thumbnail_load_data.resize_target_size.x != 0 && thumbnail_load_data.resize_target_size.y != 0) + thumbnail_path_resized.append("_" + std::to_string(thumbnail_load_data.resize_target_size.x) + "x" + std::to_string(thumbnail_load_data.resize_target_size.y)); + + if(get_file_type(thumbnail_path_resized) == FileType::REGULAR) { + load_image_from_file(*thumbnail_load_data.thumbnail_data->image, thumbnail_path_resized.data); + thumbnail_load_data.thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; + fprintf(stderr, "Loaded %s from thumbnail cache\n", thumbnail_path_resized.data.c_str()); + return; + } + + Path thumbnail_original_path; + if(thumbnail_load_data.local) + thumbnail_original_path = thumbnail_load_data.path; + else + thumbnail_original_path = thumbnail_load_data.thumbnail_path; + + if(thumbnail_load_data.resize_target_size.x != 0 && thumbnail_load_data.resize_target_size.y != 0) + create_thumbnail(thumbnail_original_path, thumbnail_path_resized, thumbnail_load_data.thumbnail_data.get(), thumbnail_load_data.resize_target_size); + else + load_image_from_file(*thumbnail_load_data.thumbnail_data->image, thumbnail_original_path.data); + + thumbnail_load_data.thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; + } + + static int64_t timeval_to_milliseconds(struct timeval &time) { + return (int64_t)time.tv_sec * 1000 + (int64_t)time.tv_usec / 1000; + } + AsyncImageLoader::AsyncImageLoader() { - for(int i = 0; i < NUM_IMAGE_LOAD_THREADS; ++i) { - loading_image[i] = false; + for(int i = 0; i < NUM_IMAGE_LOAD_PARALLEL; ++i) { + downloads[i].read_program.pid = -1; + downloads[i].read_program.read_fd = -1; } - load_image_thread = AsyncTask([this]() mutable { + load_thread = AsyncTask([this]() mutable { std::optional thumbnail_load_data_opt; - while(true) { - thumbnail_load_data_opt = image_load_queue.pop_wait(); - if(!thumbnail_load_data_opt) - break; - - ThumbnailLoadData &thumbnail_load_data = thumbnail_load_data_opt.value(); - thumbnail_load_data.thumbnail_data->image = std::make_unique(); - - Path thumbnail_path_resized = thumbnail_load_data.thumbnail_path; - if(thumbnail_load_data.resize_target_size.x != 0 && thumbnail_load_data.resize_target_size.y != 0) - thumbnail_path_resized.append("_" + std::to_string(thumbnail_load_data.resize_target_size.x) + "x" + std::to_string(thumbnail_load_data.resize_target_size.y)); - - if(get_file_type(thumbnail_path_resized) == FileType::REGULAR) { - load_image_from_file(*thumbnail_load_data.thumbnail_data->image, thumbnail_path_resized.data); - thumbnail_load_data.thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; - fprintf(stderr, "Loaded %s from thumbnail cache\n", thumbnail_path_resized.data.c_str()); - continue; + while(image_load_queue.is_running()) { + thumbnail_load_data_opt = image_load_queue.pop_if_available(); + if(thumbnail_load_data_opt) + process_thumbnail(thumbnail_load_data_opt.value()); + + for(int i = 0; i < NUM_IMAGE_LOAD_PARALLEL; ++i) { + Download &download = downloads[i]; + if(download.read_program.pid == -1) + continue; + + int status = 0; + if(wait_program_non_blocking(download.read_program.pid, &status)) { + Path tmp_thumbnail_path = download.thumbnail_path; + tmp_thumbnail_path.append(".tmp"); + if(status == 0 && rename_atomic(tmp_thumbnail_path.data.c_str(), download.thumbnail_path.data.c_str()) == 0) { + struct timeval time; + gettimeofday(&time, nullptr); + fprintf(stderr, "Download duration for %s: %ld ms\n", download.url.c_str(), timeval_to_milliseconds(time) - download.download_start); + + ThumbnailLoadData load_data = { std::move(download.url), std::move(download.thumbnail_path), false, download.thumbnail_data, download.resize_target_size }; + process_thumbnail(load_data); + } else { + fprintf(stderr, "Thumbnail download failed for %s\n", download.url.c_str()); + } + + std::lock_guard lock(download_mutex); + close(download.read_program.read_fd); + download.read_program.pid = -1; + download.read_program.read_fd = -1; + download.thumbnail_path.data.clear(); + download.url.c_str(); + download.thumbnail_data = nullptr; + } } - Path thumbnail_original_path; - if(thumbnail_load_data.local) - thumbnail_original_path = thumbnail_load_data.path; - else - thumbnail_original_path = thumbnail_load_data.thumbnail_path; - - if(thumbnail_load_data.resize_target_size.x != 0 && thumbnail_load_data.resize_target_size.y != 0) - create_thumbnail(thumbnail_original_path, thumbnail_path_resized, thumbnail_load_data.thumbnail_data.get(), thumbnail_load_data.resize_target_size); - else - load_image_from_file(*thumbnail_load_data.thumbnail_data->image, thumbnail_original_path.data); - - thumbnail_load_data.thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; + std::this_thread::sleep_for(std::chrono::milliseconds(2)); } }); } @@ -222,6 +262,17 @@ namespace QuickMedia { image_load_queue.close(); } + static bool download_file_async(const char *url, const char *save_filepath, ReadProgram *read_program) { + const char *args[] = { + "curl", "-H", "Accept-Language: en-US,en;q=0.5", "-H", "Connection: keep-alive", "--compressed", "-g", "-s", "-L", "-f", + "-H", "user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36", + "-o", save_filepath, + "--", url, + nullptr + }; + return exec_program_pipe(args, read_program) == 0; + } + void AsyncImageLoader::load_thumbnail(const std::string &url, bool local, sf::Vector2i resize_target_size, std::shared_ptr thumbnail_data) { if(thumbnail_data->loading_state != LoadingState::NOT_LOADED) return; @@ -239,6 +290,7 @@ namespace QuickMedia { struct stat file_stat; memset(&file_stat, 0, sizeof(file_stat)); if(stat(url.c_str(), &file_stat) != 0 || !S_ISREG(file_stat.st_mode)) { + fprintf(stderr, "Failed to load thumbnail %s: no such file\n", url.c_str()); thumbnail_data->image = std::make_unique(); thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; return; @@ -246,13 +298,13 @@ namespace QuickMedia { thumbnail_path.append("_" + std::to_string(file_stat.st_mtim.tv_sec)); thumbnail_data->loading_state = LoadingState::LOADING; - image_load_queue.push({ url, thumbnail_path, true, thumbnail_data, resize_target_size }); + image_load_queue.push({ url, std::move(thumbnail_path), true, thumbnail_data, resize_target_size }); return; } if(get_file_type(thumbnail_path) == FileType::REGULAR) { thumbnail_data->loading_state = LoadingState::LOADING; - image_load_queue.push({ url, thumbnail_path, false, thumbnail_data, resize_target_size }); + image_load_queue.push({ url, std::move(thumbnail_path), false, thumbnail_data, resize_target_size }); return; } @@ -260,39 +312,22 @@ namespace QuickMedia { if(free_index == -1) return; - loading_image[free_index] = true; + std::lock_guard lock(download_mutex); thumbnail_data->loading_state = LoadingState::LOADING; - - // TODO: Keep the thread running and use conditional variable instead to sleep until a new image should be loaded. Same in ImageViewer. - download_image_thread[free_index] = AsyncTask([this, free_index, thumbnail_path, url, resize_target_size, thumbnail_data]() mutable { - thumbnail_data->image = std::make_unique(); - - Path thumbnail_path_resized = thumbnail_path; - if(resize_target_size.x != 0 && resize_target_size.y != 0) - thumbnail_path_resized.append("_" + std::to_string(resize_target_size.x) + "x" + std::to_string(resize_target_size.y)); - - if(get_file_type(thumbnail_path_resized) == FileType::REGULAR) { - load_image_from_file(*thumbnail_data->image, thumbnail_path_resized.data); - thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; - fprintf(stderr, "Loaded %s from thumbnail cache\n", thumbnail_path_resized.data.c_str()); - return; - } - - if(get_file_type(thumbnail_path.data) == FileType::FILE_NOT_FOUND && download_to_file(url, thumbnail_path.data, {}, true) != DownloadResult::OK) { - thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; - loading_image[free_index] = false; - return; - } - - if(resize_target_size.x != 0 && resize_target_size.y != 0) - create_thumbnail(thumbnail_path, thumbnail_path_resized, thumbnail_data.get(), resize_target_size); - else - load_image_from_file(*thumbnail_data->image, thumbnail_path.data); - - thumbnail_data->loading_state = LoadingState::FINISHED_LOADING; - loading_image[free_index] = false; + Path tmp_thumbnail_path = thumbnail_path; + tmp_thumbnail_path.append(".tmp"); + if(!download_file_async(url.c_str(), tmp_thumbnail_path.data.c_str(), &downloads[free_index].read_program)) { + fprintf(stderr, "Failed to start download of %s\n", url.c_str()); return; - }); + } + + struct timeval time; + gettimeofday(&time, nullptr); + downloads[free_index].download_start = timeval_to_milliseconds(time); + downloads[free_index].thumbnail_path = std::move(thumbnail_path); + downloads[free_index].thumbnail_data = thumbnail_data; + downloads[free_index].resize_target_size = resize_target_size; + downloads[free_index].url = url; } std::shared_ptr AsyncImageLoader::get_thumbnail(const std::string &url, bool local, sf::Vector2i resize_target_size) { @@ -325,8 +360,8 @@ namespace QuickMedia { } int AsyncImageLoader::get_free_load_index() const { - for(int i = 0; i < NUM_IMAGE_LOAD_THREADS; ++i) { - if(!loading_image[i]) + for(int i = 0; i < NUM_IMAGE_LOAD_PARALLEL; ++i) { + if(downloads[i].read_program.pid == -1) return i; } return -1; diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 709bac5..03e67be 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -2870,6 +2870,7 @@ namespace QuickMedia { youtube_downloader_task = AsyncTask([&youtube_video_media_proxy, &youtube_audio_media_proxy]() { sf::Clock timer; const double sleep_time_millisec = 1; + // TODO: Poll instead of sleep while(!program_is_dead_in_current_thread()) { if(youtube_video_media_proxy) youtube_video_media_proxy->update(); diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index 012a20b..40b6970 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -425,6 +425,33 @@ namespace QuickMedia { return std::nullopt; } + static bool video_is_live(const Json::Value &video_item_json) { + if(!video_item_json.isObject()) + return false; + + const Json::Value &badges_json = video_item_json["badges"]; + if(!badges_json.isArray()) + return false; + + for(const Json::Value &badge_json : badges_json) { + if(!badge_json.isObject()) + continue; + + const Json::Value &metadata_badge_renderer_json = badge_json["metadataBadgeRenderer"]; + if(!metadata_badge_renderer_json.isObject()) + continue; + + const Json::Value &style_json = metadata_badge_renderer_json["style"]; + if(!style_json.isString()) + continue; + + if(strcmp(style_json.asCString(), "BADGE_STYLE_TYPE_LIVE_NOW") == 0) + return true; + } + + return false; + } + static std::shared_ptr parse_common_video_item(const Json::Value &video_item_json, std::unordered_set &added_videos) { const Json::Value &video_id_json = video_item_json["videoId"]; if(!video_id_json.isString()) @@ -491,6 +518,11 @@ namespace QuickMedia { desc += '\n'; desc += length.value(); } + if(video_is_live(video_item_json)) { + if(!desc.empty()) + desc += '\n'; + desc += "Live now"; + } if(owner_text) { if(!desc.empty()) desc += '\n'; -- cgit v1.2.3