├── .gitignore ├── .gitmodules ├── BonDriver_EPGStation.yml ├── CMakeLists.txt ├── LICENSE ├── README.md ├── include ├── IBonDriver.h ├── IBonDriver2.h ├── export.hpp └── min_win32_typedef.hpp ├── src ├── blocking_buffer.cpp ├── blocking_buffer.hpp ├── bon_driver.cpp ├── bon_driver.hpp ├── config.cpp ├── config.hpp ├── epgstation_api.cpp ├── epgstation_api.hpp ├── epgstation_models.hpp ├── epgstation_models_deserialize.hpp ├── library.cpp ├── library.hpp ├── log.cpp ├── log.hpp ├── noncopyable.hpp ├── scope_guard.hpp ├── speed_sampler.cpp ├── speed_sampler.hpp ├── stream_loader.cpp ├── stream_loader.hpp ├── string_utils.cpp └── string_utils.hpp └── test ├── CMakeLists.txt └── main.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | .idea/ 4 | # User-specific stuff 5 | .idea/**/workspace.xml 6 | .idea/**/tasks.xml 7 | .idea/**/usage.statistics.xml 8 | .idea/**/dictionaries 9 | .idea/**/shelf 10 | 11 | # Generated files 12 | .idea/**/contentModel.xml 13 | 14 | # Sensitive or high-churn files 15 | .idea/**/dataSources/ 16 | .idea/**/dataSources.ids 17 | .idea/**/dataSources.local.xml 18 | .idea/**/sqlDataSources.xml 19 | .idea/**/dynamic.xml 20 | .idea/**/uiDesigner.xml 21 | .idea/**/dbnavigator.xml 22 | 23 | # Gradle 24 | .idea/**/gradle.xml 25 | .idea/**/libraries 26 | 27 | # Gradle and Maven with auto-import 28 | # When using Gradle or Maven with auto-import, you should exclude module files, 29 | # since they will be recreated, and may cause churn. Uncomment if using 30 | # auto-import. 31 | # .idea/modules.xml 32 | # .idea/*.iml 33 | # .idea/modules 34 | 35 | # Visual Studio 36 | .vs/ 37 | 38 | # compile_commands.json 39 | compile_commands.json 40 | 41 | # CMake 42 | cmake-build-*/ 43 | 44 | # build 45 | build/ 46 | 47 | # Mongo Explorer plugin 48 | .idea/**/mongoSettings.xml 49 | 50 | # File-based project format 51 | *.iws 52 | 53 | # IntelliJ 54 | out/ 55 | 56 | # mpeltonen/sbt-idea plugin 57 | .idea_modules/ 58 | 59 | # JIRA plugin 60 | atlassian-ide-plugin.xml 61 | 62 | # Cursive Clojure plugin 63 | .idea/replstate.xml 64 | 65 | # Crashlytics plugin (for Android Studio and IntelliJ) 66 | com_crashlytics_export_strings.xml 67 | crashlytics.properties 68 | crashlytics-build.properties 69 | fabric.properties 70 | 71 | # Editor-based Rest Client 72 | .idea/httpRequests 73 | 74 | # Android studio 3.1+ serialized cache file 75 | .idea/caches/build_file_checksums.ser 76 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "thirdparty/yaml-cpp"] 2 | path = thirdparty/yaml-cpp 3 | url = https://github.com/jbeder/yaml-cpp.git 4 | [submodule "thirdparty/json"] 5 | path = thirdparty/json 6 | url = https://github.com/nlohmann/json 7 | [submodule "thirdparty/cpr"] 8 | path = thirdparty/cpr 9 | url = https://github.com/whoshuu/cpr.git 10 | -------------------------------------------------------------------------------- /BonDriver_EPGStation.yml: -------------------------------------------------------------------------------- 1 | baseURL: http://127.0.0.1/ 2 | version: v1 3 | mpegTsStreamingMode: 0 4 | showInactiveServices: false -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | if(POLICY CMP0091) 4 | cmake_policy(SET CMP0091 NEW) 5 | endif() 6 | 7 | project(BonDriver_EPGStation LANGUAGES C CXX) 8 | 9 | # main project checking 10 | # determine if BonDriver_EPGStation is built as a subproject (using add_subdirectory) or if it is the main project 11 | set(MAIN_PROJECT OFF) 12 | if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) 13 | set(MAIN_PROJECT ON) 14 | endif() 15 | 16 | option(BONDRIVER_EPGSTATION_BUILD_TEST "Build test program." ${MAIN_PROJECT}) 17 | 18 | set(CMAKE_CXX_STANDARD 17) 19 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 20 | 21 | if(MSVC) 22 | set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded$<$:Debug>) 23 | endif() 24 | 25 | 26 | set(YAML_CPP_BUILD_CONTRIB OFF CACHE BOOL "Disable yaml-cpp contrib") 27 | set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "Disable yaml-cpp build tools") 28 | set(YAML_MSVC_SHARED_RT OFF CACHE BOOL "Use static linked runtime for yaml-cpp") 29 | 30 | set(BUILD_SHARED_LIBS OFF CACHE BOOL "Build cpr as static library") 31 | set(BUILD_CPR_TESTS OFF CACHE BOOL "Disable cpr tests building") 32 | 33 | if(WIN32) 34 | set(USE_SYSTEM_CURL OFF) 35 | set(USE_OPENSSL OFF) 36 | set(USE_WINSSL ON) 37 | set(CURL_STATICLIB true) 38 | add_definitions(-DCURL_STATICLIB) 39 | else() 40 | find_package(CURL) 41 | if(CURL_FOUND) 42 | set(USE_SYSTEM_CURL ON CACHE BOOL "Use the system curl for faster builds") 43 | endif() 44 | endif() 45 | 46 | add_subdirectory(thirdparty/cpr) 47 | add_subdirectory(thirdparty/json) 48 | add_subdirectory(thirdparty/yaml-cpp) 49 | 50 | add_library(BonDriver_EPGStation 51 | SHARED 52 | include/IBonDriver.h 53 | include/IBonDriver2.h 54 | include/min_win32_typedef.hpp 55 | include/export.hpp 56 | src/blocking_buffer.cpp 57 | src/blocking_buffer.hpp 58 | src/bon_driver.cpp 59 | src/bon_driver.hpp 60 | src/config.cpp 61 | src/config.hpp 62 | src/epgstation_api.cpp 63 | src/epgstation_api.hpp 64 | src/epgstation_models.hpp 65 | src/epgstation_models_deserialize.hpp 66 | src/library.cpp 67 | src/library.hpp 68 | src/log.cpp 69 | src/log.hpp 70 | src/noncopyable.hpp 71 | src/scope_guard.hpp 72 | src/speed_sampler.cpp 73 | src/speed_sampler.hpp 74 | src/stream_loader.cpp 75 | src/stream_loader.hpp 76 | src/string_utils.cpp 77 | src/string_utils.hpp 78 | ) 79 | 80 | # Remove "lib" prefix for dll filename 81 | if(WIN32) 82 | set_target_properties(BonDriver_EPGStation PROPERTIES PREFIX "") 83 | endif() 84 | 85 | # Copy yaml config file to binary output directory 86 | add_custom_command( 87 | TARGET 88 | BonDriver_EPGStation 89 | POST_BUILD 90 | COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_SOURCE_DIR}/BonDriver_EPGStation.yml ${CMAKE_BINARY_DIR}/ 91 | ) 92 | 93 | 94 | # On Windows, Set Character set (TCHAR) to Unicode 95 | if(WIN32) 96 | target_compile_definitions(BonDriver_EPGStation 97 | PRIVATE 98 | UNICODE 99 | _UNICODE 100 | ) 101 | endif() 102 | 103 | # Platform-specific compiler macro / flags 104 | if(MSVC) 105 | # For MSVC, static link runtime library (MultiThreaded) 106 | if(${CMAKE_VERSION} VERSION_LESS "3.15") 107 | target_compile_options(BonDriver_EPGStation 108 | PUBLIC 109 | $<$:-MTd> 110 | $<$:-MT> 111 | $<$:-MT> 112 | $<$:-MT> 113 | ) 114 | else() 115 | set_property( 116 | TARGET 117 | BonDriver_EPGStation 118 | PROPERTY 119 | MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" 120 | ) 121 | endif() 122 | 123 | # For MSVC, disable CRT secure warnings 124 | target_compile_definitions(BonDriver_EPGStation 125 | PRIVATE 126 | _CRT_SECURE_NO_WARNINGS=1 127 | ) 128 | elseif(MINGW) 129 | # For MinGW, static linking runtime library 130 | target_compile_options(BonDriver_EPGStation 131 | PUBLIC 132 | -static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -lwinpthread -Wl,--no-whole-archive -Wl,-Bdynamic 133 | ) 134 | endif() 135 | 136 | # Macros 137 | target_compile_definitions(BonDriver_EPGStation 138 | PRIVATE 139 | BONDRIVER_EPGSTATION_EXPORTS=1 140 | ) 141 | 142 | # Include directories 143 | target_include_directories(BonDriver_EPGStation 144 | PRIVATE 145 | ${CPR_INCLUDE_DIRS} 146 | ${JSON_INCLUDE_DIRS} 147 | ${CMAKE_CURRENT_SOURCE_DIR}/include 148 | ${CMAKE_CURRENT_SOURCE_DIR}/src 149 | ) 150 | 151 | # Link directories 152 | #target_link_directories(BonDriver_EPGStation 153 | # PRIVATE 154 | #) 155 | 156 | # Linking 157 | target_link_libraries(BonDriver_EPGStation 158 | PRIVATE 159 | ${CPR_LIBRARIES} 160 | nlohmann_json::nlohmann_json 161 | yaml-cpp 162 | ) 163 | 164 | if(MSVC) 165 | # library linkage for MSVC 166 | target_link_libraries(BonDriver_EPGStation 167 | PRIVATE 168 | # libraries 169 | ) 170 | elseif(MINGW) 171 | # library linkage for MinGW: Enable full static link 172 | target_link_libraries(BonDriver_EPGStation 173 | PRIVATE 174 | -Wl,-Bstatic 175 | # libraries 176 | -Wl,-Bdynamic 177 | ) 178 | else() 179 | # library linkage for other platforms: dynamic link by default 180 | target_link_libraries(BonDriver_EPGStation 181 | PRIVATE 182 | # libraries 183 | ) 184 | endif() 185 | 186 | if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 187 | # pthread is needed for gcc 188 | target_link_libraries(BonDriver_EPGStation 189 | PRIVATE 190 | pthread 191 | ) 192 | endif() 193 | 194 | if(WIN32) 195 | target_link_libraries(BonDriver_EPGStation 196 | PRIVATE 197 | Version 198 | Ws2_32 199 | ) 200 | endif() 201 | 202 | if(BONDRIVER_EPGSTATION_BUILD_TEST) 203 | add_subdirectory(test) 204 | endif() 205 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 xqq 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BonDriver_EPGStation 2 | ====== 3 | BonDriver for [EPGStation](https://github.com/l3tnun/EPGStation) that could be used in BonDriver-based software like TVTest, EDCB, BonRecTest, etc. 4 | 5 | ## Features 6 | - Both EPGStation v1 and v2 are supported 7 | - http / https / http2 supported 8 | - IPv6 supported 9 | - Proxy configuration 10 | - UserAgent & custom request headers configuration 11 | - CRT is static-linked so does not require extra VCRuntime 12 | - Maintainable code 13 | 14 | ## Prebuilt binaries 15 | Built in Visual Studio 2019. You must choose the right architecture (x86/x64). 16 | 17 | https://github.com/xqq/BonDriver_EPGStation/releases 18 | 19 | ## Config 20 | You must put `BonDriver_EPGStation.yml` in the same folder as the dll and keep the same file name. 21 | 22 | ``` 23 | baseURL: http://192.168.1.101 # required 24 | version: v2 # required, v1 or v2 25 | mpegTsStreamingMode: 0 # required 26 | showInactiveServices: false # optional, default to false 27 | userAgent: BonDriver_EPGStation # optional 28 | proxy: socks5://127.0.0.1:1080 # optional, protocol could be http/https/socks4/socks4a/socks5/socks5h 29 | headers: # optional 30 | X-Real-Ip: 114.514.810.893 31 | basicAuth: # optional, deprecated 32 | user: admin 33 | password: admin 34 | ``` 35 | 36 | ## Build 37 | ### Preparing 38 | CMake >=3.13 and a C++17 compatible compiler is necessary. 39 | ```bash 40 | git clone https://github.com/xqq/BonDriver_EPGStation.git 41 | cd BonDriver_EPGStation 42 | git submodule update --init --recursive 43 | ``` 44 | 45 | ### CMake generate 46 | The cmake configuring of libcurl could be extremely slow and you should just wait patiently. 47 | ```bash 48 | mkdir build 49 | cd build 50 | cmake -DCMAKE_BUILD_TYPE=MinSizeRel -A Win32 .. # Build for x86 (Win32) 51 | cmake -DCMAKE_BUILD_TYPE=MinSizeRel -A x64 .. # or Build for x64 (x64) 52 | ``` 53 | 54 | ### Compiling 55 | ```bash 56 | cmake --build . --config MinSizeRel -j8 57 | ``` 58 | 59 | Visual Studio 2019 (CMake development) or CLion (MSVC toolchain) is recommended. 60 | 61 | ## License 62 | ``` 63 | MIT License 64 | 65 | Copyright (c) 2021 magicxqq 66 | ``` 67 | -------------------------------------------------------------------------------- /include/IBonDriver.h: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_IBONDRIVER_H 6 | #define BONDRIVER_EPGSTATION_IBONDRIVER_H 7 | 8 | #include "export.hpp" 9 | 10 | #ifdef _WIN32 11 | #include 12 | #else 13 | #include "min_win32_typedef.hpp" 14 | #endif 15 | 16 | class IBonDriver { 17 | public: 18 | virtual const BOOL OpenTuner(void) = 0; 19 | virtual void CloseTuner(void) = 0; 20 | 21 | virtual const BOOL SetChannel(const BYTE bCh) = 0; 22 | virtual const float GetSignalLevel(void) = 0; 23 | 24 | virtual const DWORD WaitTsStream(const DWORD dwTimeOut = 0) = 0; 25 | virtual const DWORD GetReadyCount(void) = 0; 26 | 27 | virtual const BOOL GetTsStream(BYTE* pDst, DWORD* pdwSize, DWORD* pdwRemain) = 0; 28 | virtual const BOOL GetTsStream(BYTE** ppDst, DWORD* pdwSize, DWORD* pdwRemain) = 0; 29 | 30 | virtual void PurgeTsStream(void) = 0; 31 | 32 | virtual void Release(void) = 0; 33 | }; 34 | 35 | extern "C" EXPORT_API IBonDriver* CreateBonDriver(); 36 | 37 | #endif // BONDRIVER_EPGSTATION_IBONDRIVER_H 38 | -------------------------------------------------------------------------------- /include/IBonDriver2.h: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_IBONDRIVER2_H 6 | #define BONDRIVER_EPGSTATION_IBONDRIVER2_H 7 | 8 | #include "IBonDriver.h" 9 | 10 | class IBonDriver2 : public IBonDriver 11 | { 12 | public: 13 | // IBonDriver2 14 | virtual LPCTSTR GetTunerName(void) = 0; 15 | 16 | virtual const BOOL IsTunerOpening(void) = 0; 17 | 18 | virtual LPCTSTR EnumTuningSpace(const DWORD dwSpace) = 0; 19 | virtual LPCTSTR EnumChannelName(const DWORD dwSpace, const DWORD dwChannel) = 0; 20 | 21 | virtual const BOOL SetChannel(const DWORD dwSpace, const DWORD dwChannel) = 0; 22 | 23 | virtual const DWORD GetCurSpace(void) = 0; 24 | virtual const DWORD GetCurChannel(void) = 0; 25 | 26 | // IBonDriver 27 | virtual void Release(void) = 0; 28 | }; 29 | 30 | #endif // BONDRIVER_EPGSTATION_IBONDRIVER2_H 31 | -------------------------------------------------------------------------------- /include/export.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_EXPORT_HPP 6 | #define BONDRIVER_EPGSTATION_EXPORT_HPP 7 | 8 | #if defined(_MSC_VER) || defined(__MINGW32__) 9 | // MSVC & MinGW 10 | #ifdef BONDRIVER_EPGSTATION_EXPORTS 11 | #define EXPORT_API __declspec(dllexport) 12 | #else 13 | #define EXPORT_API __declspec(dllimport) 14 | #endif 15 | #elif defined(__GNUC__) || defined(__clang__) 16 | // GCC/Clang (Unix) 17 | #ifdef BONDRIVER_EPGSTATION_EXPORTS 18 | #define EXPORT_API __attribute__((visibility("default"))) 19 | #else 20 | #define EXPORT_API 21 | #endif 22 | #else 23 | #define EXPORT_API 24 | #endif 25 | 26 | #endif // BONDRIVER_EPGSTATION_EXPORT_HPP 27 | -------------------------------------------------------------------------------- /include/min_win32_typedef.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_MIN_WIN32_TYPEDEF_HPP 6 | #define BONDRIVER_EPGSTATION_MIN_WIN32_TYPEDEF_HPP 7 | 8 | #ifndef FALSE 9 | #define FALSE 0 10 | #endif 11 | 12 | #ifndef TRUE 13 | #define TRUE 1 14 | #endif 15 | 16 | typedef int BOOL; 17 | typedef unsigned char BYTE; 18 | typedef unsigned long DWORD; 19 | typedef const wchar_t* LPCWSTR; 20 | typedef LPCWSTR LPCTSTR; 21 | typedef wchar_t WCHAR; 22 | typedef WCHAR TCHAR; 23 | typedef const char* LPCSTR; 24 | typedef int SOCKET; 25 | typedef void* HMODULE; 26 | typedef void* LPVOID; 27 | 28 | #endif // BONDRIVER_EPGSTATION_MIN_WIN32_TYPEDEF_HPP 29 | -------------------------------------------------------------------------------- /src/blocking_buffer.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #include 6 | #include "blocking_buffer.hpp" 7 | 8 | 9 | BlockingBuffer::BlockingBuffer(size_t chunk_size) 10 | : chunk_size_(chunk_size), has_chunk_count_limit_(false), 11 | max_chunk_count_(SIZE_MAX), min_chunk_count_(0), is_exit_(false) {} 12 | 13 | BlockingBuffer::BlockingBuffer(size_t chunk_size, size_t max_chunk_count) 14 | : chunk_size_(chunk_size), has_chunk_count_limit_(true), 15 | max_chunk_count_(max_chunk_count), min_chunk_count_(0), is_exit_(false) {} 16 | 17 | BlockingBuffer::BlockingBuffer(size_t chunk_size, size_t max_chunk_count, size_t min_chunk_count) 18 | : chunk_size_(chunk_size), has_chunk_count_limit_(true), 19 | max_chunk_count_(max_chunk_count), min_chunk_count_(min_chunk_count), is_exit_(false) {} 20 | 21 | BlockingBuffer::~BlockingBuffer() { 22 | if (!is_exit_) { 23 | NotifyExit(); 24 | } 25 | } 26 | 27 | size_t BlockingBuffer::Read(uint8_t* buffer, size_t expected_bytes) { 28 | // I am the data consumer 29 | assert(buffer != nullptr); 30 | assert(expected_bytes > 0); 31 | assert(!is_exit_); 32 | 33 | std::unique_lock locker(mutex_); 34 | 35 | if (has_chunk_count_limit_ && deque_.size() < min_chunk_count_) { 36 | produce_cv_.notify_one(); 37 | // consumer standby, waiting notify message from the producer 38 | consume_cv_.wait(locker, [this] { 39 | return deque_.size() >= min_chunk_count_ || is_exit_; 40 | }); 41 | } 42 | 43 | uint8_t* out = buffer; 44 | size_t bytes_read = 0; 45 | ptrdiff_t remain_unread = static_cast(expected_bytes); 46 | 47 | while (remain_unread > 0) { 48 | if (deque_.empty() && !is_exit_) { 49 | produce_cv_.notify_one(); 50 | // Wait for producing 51 | consume_cv_.wait(locker, [this] { 52 | return !deque_.empty() || is_exit_; 53 | }); 54 | 55 | if (is_exit_ && deque_.empty()) { 56 | break; 57 | } 58 | } 59 | 60 | auto& front_chunk = deque_.front(); 61 | 62 | if (front_chunk.RemainReadable() == 0) { 63 | deque_.pop_front(); 64 | continue; 65 | } 66 | 67 | size_t request_bytes = std::min(static_cast(remain_unread), front_chunk.RemainReadable()); 68 | size_t chunk_read = front_chunk.Read(out, request_bytes); 69 | 70 | bytes_read += chunk_read; 71 | remain_unread -= chunk_read; 72 | out += chunk_read; 73 | 74 | if (front_chunk.RemainReadable() == 0) { 75 | deque_.pop_front(); 76 | } 77 | } 78 | 79 | // Notify the data producer to produce data 80 | produce_cv_.notify_one(); 81 | return bytes_read; 82 | } 83 | 84 | std::pair BlockingBuffer::ReadChunkAndRetain() { 85 | // I am the data consumer 86 | std::unique_lock locker(mutex_); 87 | 88 | if (has_chunk_count_limit_ && deque_.size() < min_chunk_count_) { 89 | produce_cv_.notify_one(); 90 | // consumer standby, waiting notify message from the producer 91 | consume_cv_.wait(locker, [this] { 92 | return deque_.size() >= min_chunk_count_ || is_exit_; 93 | }); 94 | } 95 | 96 | uint8_t* buffer_ptr = nullptr; 97 | size_t bytes = 0; 98 | 99 | while (!buffer_ptr) { 100 | if (deque_.empty() && !is_exit_) { 101 | produce_cv_.notify_one(); 102 | // Wait for producing 103 | consume_cv_.wait(locker, [this] { 104 | return !deque_.empty() || is_exit_; 105 | }); 106 | } 107 | 108 | if (deque_.empty() && is_exit_) { 109 | break; 110 | } 111 | 112 | Chunk& front_chunk = deque_.front(); 113 | 114 | if (front_chunk.RemainReadable() == 0) { 115 | deque_.pop_front(); 116 | continue; 117 | } 118 | 119 | buffer_ptr = front_chunk.vec_.data() + front_chunk.read_pos_; 120 | bytes = front_chunk.RemainReadable(); 121 | // fast-forward read_pos_ to write_pos_ (mark as consumed) 122 | front_chunk.read_pos_ = front_chunk.write_pos_; 123 | } 124 | 125 | // Notify the data producer to produce data 126 | produce_cv_.notify_one(); 127 | 128 | return {buffer_ptr, bytes}; 129 | } 130 | 131 | size_t BlockingBuffer::Write(const uint8_t *buffer, size_t bytes) { 132 | // I am the data producer 133 | assert(buffer != nullptr); 134 | assert(bytes > 0); 135 | assert(!is_exit_); 136 | 137 | std::unique_lock locker(mutex_); 138 | 139 | if (has_chunk_count_limit_ && deque_.size() >= max_chunk_count_) { 140 | consume_cv_.notify_one(); 141 | // producer standby, waiting notify message from the consumer 142 | produce_cv_.wait(locker, [this] { 143 | return deque_.size() < max_chunk_count_ || is_exit_; 144 | }); 145 | } 146 | 147 | const uint8_t* in = buffer; 148 | size_t bytes_written = 0; 149 | ptrdiff_t remain_unwrite = static_cast(bytes); 150 | 151 | while (remain_unwrite > 0) { 152 | if (deque_.size() >= max_chunk_count_) { 153 | consume_cv_.notify_one(); 154 | // Wait for consuming 155 | produce_cv_.wait(locker, [this] { 156 | return deque_.size() < max_chunk_count_ || is_exit_; 157 | }); 158 | } 159 | 160 | if (deque_.empty() || deque_.back().RemainWritable() == 0) { 161 | // No existing chunk, or back chunk is full 162 | deque_.emplace_back(chunk_size_); 163 | } 164 | 165 | auto& back_chunk = deque_.back(); 166 | 167 | size_t attempt_bytes = std::min(static_cast(remain_unwrite), back_chunk.RemainWritable()); 168 | size_t chunk_written = back_chunk.Write(in, attempt_bytes); 169 | 170 | bytes_written += chunk_written; 171 | remain_unwrite -= chunk_written; 172 | in += chunk_written; 173 | } 174 | 175 | // Notify the consumer to consume data 176 | consume_cv_.notify_one(); 177 | return bytes_written; 178 | } 179 | 180 | size_t BlockingBuffer::WriteChunk(const std::vector& vec) { 181 | // I am the data producer 182 | assert(vec.size() == chunk_size_); 183 | 184 | std::unique_lock locker(mutex_); 185 | 186 | if (has_chunk_count_limit_ && deque_.size() >= max_chunk_count_) { 187 | consume_cv_.notify_one(); 188 | // producer standby, waiting notify message from the consumer 189 | produce_cv_.wait(locker, [this] { 190 | return deque_.size() < max_chunk_count_ || is_exit_; 191 | }); 192 | } 193 | 194 | size_t bytes = vec.size(); 195 | std::vector vec_clone = vec; 196 | deque_.emplace_back(std::move(vec_clone)); 197 | 198 | // Notify the consumer to consume data 199 | consume_cv_.notify_one(); 200 | return bytes; 201 | } 202 | 203 | size_t BlockingBuffer::WriteChunk(std::vector&& vec) { 204 | // I am the data producer 205 | assert(vec.size() == chunk_size_); 206 | 207 | std::unique_lock locker(mutex_); 208 | 209 | if (has_chunk_count_limit_ && deque_.size() >= max_chunk_count_) { 210 | consume_cv_.notify_one(); 211 | // producer standby, waiting notify message from the consumer 212 | produce_cv_.wait(locker, [this] { 213 | return deque_.size() < max_chunk_count_ || is_exit_; 214 | }); 215 | } 216 | 217 | size_t bytes = vec.size(); 218 | deque_.emplace_back(std::move(vec)); 219 | 220 | // Notify the consumer to consume data 221 | consume_cv_.notify_one(); 222 | return bytes; 223 | } 224 | 225 | void BlockingBuffer::WaitUntilData() { 226 | std::unique_lock locker(mutex_); 227 | 228 | if (has_chunk_count_limit_) { 229 | consume_cv_.wait(locker, [this] { 230 | return deque_.size() >= min_chunk_count_ || is_exit_; 231 | }); 232 | } else { 233 | consume_cv_.wait(locker, [this] { 234 | return !deque_.empty() || is_exit_; 235 | }); 236 | } 237 | } 238 | 239 | void BlockingBuffer::WaitUntilEmpty() { 240 | std::unique_lock locker(mutex_); 241 | 242 | produce_cv_.wait(locker, [this] { 243 | return deque_.empty() || is_exit_; 244 | }); 245 | } 246 | 247 | void BlockingBuffer::NotifyExit() { 248 | std::lock_guard guard(mutex_); 249 | 250 | is_exit_ = true; 251 | consume_cv_.notify_all(); 252 | produce_cv_.notify_all(); 253 | } 254 | 255 | bool BlockingBuffer::IsExit() { 256 | std::lock_guard guard(mutex_); 257 | return is_exit_; 258 | } 259 | 260 | size_t BlockingBuffer::ReadableBytes() { 261 | std::lock_guard guard(mutex_); 262 | 263 | size_t readable = 0; 264 | 265 | for (const Chunk& chunk : deque_) { 266 | readable += chunk.RemainReadable(); 267 | } 268 | 269 | return readable; 270 | } 271 | 272 | void BlockingBuffer::Clear() { 273 | std::lock_guard guard(mutex_); 274 | 275 | deque_.clear(); 276 | } 277 | 278 | 279 | BlockingBuffer::Chunk::Chunk(size_t chunk_size) 280 | : chunk_size_(chunk_size), read_pos_(0), write_pos_(0), vec_(chunk_size) {} 281 | 282 | BlockingBuffer::Chunk::Chunk(std::vector&& vec) 283 | : chunk_size_(vec.size()), read_pos_(0), write_pos_(vec.size()), vec_(std::move(vec)) {} 284 | 285 | size_t BlockingBuffer::Chunk::Read(uint8_t* buffer, size_t expected_bytes) { 286 | assert(buffer != nullptr); 287 | assert(expected_bytes > 0); 288 | assert(read_pos_ <= write_pos_); 289 | 290 | size_t remain = RemainReadable(); 291 | 292 | if (remain < expected_bytes) { 293 | return 0; 294 | } 295 | 296 | uint8_t* read_ptr = vec_.data() + read_pos_; 297 | memcpy(buffer, read_ptr, expected_bytes); 298 | read_pos_ += expected_bytes; 299 | 300 | return expected_bytes; 301 | } 302 | 303 | size_t BlockingBuffer::Chunk::Write(const uint8_t *buffer, size_t bytes) { 304 | assert(buffer != nullptr); 305 | assert(bytes > 0); 306 | assert(read_pos_ <= write_pos_); 307 | 308 | size_t remain = RemainWritable(); 309 | 310 | if (remain < bytes) { 311 | return 0; 312 | } 313 | 314 | uint8_t* write_ptr = vec_.data() + write_pos_; 315 | memcpy(write_ptr, buffer, bytes); 316 | write_pos_ += bytes; 317 | 318 | return bytes; 319 | } 320 | 321 | size_t BlockingBuffer::Chunk::RemainReadable() const { 322 | return write_pos_ - read_pos_; 323 | } 324 | 325 | size_t BlockingBuffer::Chunk::RemainWritable() const { 326 | return static_cast(chunk_size_) - write_pos_; 327 | } 328 | -------------------------------------------------------------------------------- /src/blocking_buffer.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_BLOCKING_BUFFER_HPP 6 | #define BONDRIVER_EPGSTATION_BLOCKING_BUFFER_HPP 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include "noncopyable.hpp" 17 | 18 | class BlockingBuffer { 19 | public: 20 | explicit BlockingBuffer(size_t chunk_size); 21 | BlockingBuffer(size_t chunk_size, size_t max_chunk_count); 22 | BlockingBuffer(size_t chunk_size, size_t max_chunk_count, size_t min_chunk_count); 23 | ~BlockingBuffer(); 24 | size_t Read(uint8_t* buffer, size_t expected_bytes); 25 | std::pair ReadChunkAndRetain(); 26 | size_t Write(const uint8_t* buffer, size_t bytes); 27 | size_t WriteChunk(const std::vector& vec); 28 | size_t WriteChunk(std::vector&& vec); 29 | void WaitUntilData(); 30 | void WaitUntilEmpty(); 31 | void NotifyExit(); 32 | bool IsExit(); 33 | size_t ReadableBytes(); 34 | void Clear(); 35 | private: 36 | struct Chunk { 37 | public: 38 | explicit Chunk(size_t chunk_size); 39 | explicit Chunk(std::vector&& vec); 40 | Chunk(Chunk&&) = default; 41 | Chunk& operator=(Chunk&&) = default; 42 | size_t Read(uint8_t* buffer, size_t expected_bytes); 43 | size_t Write(const uint8_t* buffer, size_t bytes); 44 | size_t RemainReadable() const; 45 | size_t RemainWritable() const; 46 | private: 47 | size_t chunk_size_; 48 | public: 49 | ptrdiff_t read_pos_; 50 | ptrdiff_t write_pos_; 51 | std::vector vec_; 52 | private: 53 | DISALLOW_COPY_AND_ASSIGN(Chunk); 54 | }; 55 | private: 56 | size_t chunk_size_; 57 | bool has_chunk_count_limit_; 58 | size_t max_chunk_count_; 59 | size_t min_chunk_count_; 60 | bool is_exit_; 61 | std::deque deque_; 62 | std::mutex mutex_; 63 | std::condition_variable consume_cv_; 64 | std::condition_variable produce_cv_; 65 | private: 66 | DISALLOW_COPY_AND_ASSIGN(BlockingBuffer); 67 | }; 68 | 69 | 70 | #endif // BONDRIVER_EPGSTATION_BLOCKING_BUFFER_HPP 71 | -------------------------------------------------------------------------------- /src/bon_driver.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #include "log.hpp" 6 | #include "stream_loader.hpp" 7 | #include "bon_driver.hpp" 8 | 9 | BonDriver::BonDriver(const Config& config) : yaml_config_(config), api_(config.GetBaseURL().value(), config.GetVersion().value()) { 10 | Log::InfoF(LOG_FUNCTION); 11 | 12 | if (config.GetBasicAuth().has_value()) { 13 | api_.SetBasicAuth(config.GetBasicAuth()->user, config.GetBasicAuth()->password); 14 | } 15 | if (config.GetUserAgent().has_value()) { 16 | api_.SetUserAgent(config.GetUserAgent().value()); 17 | } 18 | if (config.GetProxy().has_value()) { 19 | api_.SetProxy(config.GetProxy().value()); 20 | } 21 | if (config.GetHeaders().has_value()) { 22 | api_.SetHeaders(config.GetHeaders().value()); 23 | } 24 | 25 | InitChannels(); 26 | } 27 | 28 | BonDriver::~BonDriver() { 29 | Log::InfoF(LOG_FUNCTION); 30 | if (stream_loader_) { 31 | CloseTuner(); 32 | } 33 | } 34 | 35 | void BonDriver::Release(void) { 36 | Log::InfoF(LOG_FUNCTION); 37 | delete this; 38 | } 39 | 40 | void BonDriver::InitChannels() { 41 | std::optional config = api_.GetConfig(); 42 | if (!config.has_value()) { 43 | Log::ErrorF("EPGStationAPI::GetConfig() failed"); 44 | return; 45 | } 46 | if (!config->enable_live_streaming) { 47 | // Server doesn't enable live streaming, return failed 48 | Log::ErrorF("config->enable_live_streaming is false"); 49 | } 50 | epgstation_config_ = config.value(); 51 | 52 | 53 | auto show_inactive_services = yaml_config_.GetShowInactiveServices(); 54 | 55 | if (show_inactive_services.has_value() && show_inactive_services.value() == true) { 56 | // showInactiveServices == true 57 | std::optional channels_holder = api_.GetChannels(); 58 | if (!channels_holder.has_value()) { 59 | Log::ErrorF("EPGStationAPI::GetChannels() failed"); 60 | return; 61 | } 62 | channels_ = std::move(channels_holder->channels); 63 | } else { 64 | std::optional channels_holder = api_.GetBroadcasting(); 65 | if (!channels_holder.has_value()) { 66 | Log::ErrorF("EPGStationAPI::GetBroadcasting() failed"); 67 | return; 68 | } 69 | channels_ = std::move(channels_holder->channels); 70 | } 71 | 72 | 73 | for (size_t i = 0; i < channels_.size(); i++) { 74 | auto& channel = channels_[i]; 75 | 76 | auto iter = space_set_.find(channel.channel_type); 77 | if (iter == space_set_.end()) { 78 | // channel type not found, insert as new space 79 | space_set_.insert(channel.channel_type); 80 | space_types_.push_back(channel.channel_type); 81 | space_channel_bases_.push_back(i); 82 | } 83 | } 84 | 85 | init_channels_succeed = true; 86 | } 87 | 88 | const BOOL BonDriver::OpenTuner(void) { 89 | Log::InfoF(LOG_FUNCTION); 90 | 91 | if (!init_channels_succeed) { 92 | Log::ErrorF("OpenTuner() failed caused by the failure of initializing channels"); 93 | return FALSE; 94 | } 95 | 96 | if (!epgstation_config_.enable_live_streaming) { 97 | Log::ErrorF("config->enable_live_streaming is false, OpenTuner() failed"); 98 | return FALSE; 99 | } 100 | 101 | if (channels_.empty()) { 102 | Log::ErrorF("Get channels failed or channel list is empty, OpenTuner() failed"); 103 | return FALSE; 104 | } 105 | 106 | return TRUE; 107 | } 108 | 109 | void BonDriver::CloseTuner(void) { 110 | Log::InfoF(LOG_FUNCTION); 111 | 112 | if (stream_loader_) { 113 | if (stream_loader_->IsPolling()) { 114 | stream_loader_->Abort(); 115 | } 116 | stream_loader_.reset(); 117 | } 118 | 119 | current_channel_ = EPGStation::Channel(); 120 | current_dwspace_ = 0; 121 | current_dwchannel_ = 0; 122 | } 123 | 124 | const BOOL BonDriver::SetChannel(const BYTE bCh) { 125 | return SetChannel(0, static_cast(bCh - 13)); 126 | } 127 | 128 | const BOOL BonDriver::SetChannel(const DWORD dwSpace, const DWORD dwChannel) { 129 | Log::InfoF("BonDriver::SetChannel(): dwSpace = %u, dwChannel = %u", dwSpace, dwChannel); 130 | 131 | if (dwSpace >= space_types_.size()) { 132 | return FALSE; 133 | } 134 | 135 | if (dwSpace < space_types_.size() - 1) { 136 | if (dwChannel >= space_channel_bases_[static_cast(dwSpace) + 1] - space_channel_bases_[dwSpace]) { 137 | return FALSE; 138 | } 139 | } 140 | 141 | size_t channel_index = space_channel_bases_[dwSpace] + dwChannel; 142 | if (channel_index >= channels_.size()) { 143 | return FALSE; 144 | } 145 | 146 | EPGStation::Channel& channel = channels_[channel_index]; 147 | 148 | if (stream_loader_) { 149 | CloseTuner(); 150 | } 151 | 152 | current_channel_ = channel; 153 | current_dwspace_ = dwSpace; 154 | current_dwchannel_ = dwChannel; 155 | 156 | stream_loader_ = std::make_unique(chunk_size_, 10, 3); 157 | 158 | std::string path_query = api_.GetMpegtsLiveStreamPathQuery(channel.id, yaml_config_.GetMpegTsStreamingMode().value()); 159 | 160 | stream_loader_->Open(yaml_config_.GetBaseURL().value(), 161 | path_query, 162 | yaml_config_.GetBasicAuth(), 163 | yaml_config_.GetUserAgent(), 164 | yaml_config_.GetProxy(), 165 | yaml_config_.GetHeaders()); 166 | 167 | return TRUE; 168 | } 169 | 170 | const float BonDriver::GetSignalLevel(void) { 171 | if (!stream_loader_) { 172 | return 0; 173 | } 174 | return stream_loader_->GetCurrentSpeedKByte() * 8 / 1000.0f; 175 | } 176 | 177 | const DWORD BonDriver::WaitTsStream(const DWORD dwTimeOut) { 178 | if (!stream_loader_) { 179 | return WAIT_ABANDONED; 180 | } 181 | 182 | auto wait_result = stream_loader_->WaitForResponse(std::chrono::milliseconds(dwTimeOut)); 183 | if (wait_result == StreamLoader::WaitResult::kWaitFailed) { 184 | return WAIT_FAILED; 185 | } else if (wait_result == StreamLoader::WaitResult::kWaitTimeout) { 186 | Log::ErrorF("BonDriver::WaitTsStream(): WaitForResponse() waiting timeout for %u ms", dwTimeOut); 187 | return WAIT_TIMEOUT; 188 | } else if (wait_result == StreamLoader::WaitResult::kResultFailed) { 189 | return WAIT_ABANDONED; 190 | } // else: wait_result == WaitResult::kResultOK 191 | 192 | wait_result = stream_loader_->WaitForData(); 193 | if (wait_result == StreamLoader::WaitResult::kWaitFailed) { 194 | return WAIT_FAILED; 195 | } else if (wait_result == StreamLoader::WaitResult::kResultFailed) { 196 | return WAIT_ABANDONED; 197 | } // else: wait_result == WaitResult::kResultOK 198 | 199 | return WAIT_OBJECT_0; 200 | } 201 | 202 | const DWORD BonDriver::GetReadyCount(void) { 203 | if (!stream_loader_) { 204 | return 0; 205 | } 206 | 207 | return static_cast(stream_loader_->RemainReadable()); 208 | } 209 | 210 | const BOOL BonDriver::GetTsStream(BYTE *pDst, DWORD *pdwSize, DWORD *pdwRemain) { 211 | if (!stream_loader_ || !stream_loader_->IsPolling()) { 212 | return FALSE; 213 | } 214 | 215 | if (stream_loader_->RemainReadable() == 0) { 216 | *pdwSize = 0; 217 | *pdwRemain = 0; 218 | return TRUE; 219 | } 220 | 221 | size_t bytes_read = stream_loader_->Read(static_cast(pDst), chunk_size_); 222 | *pdwSize = static_cast(bytes_read); 223 | *pdwRemain = static_cast(stream_loader_->RemainReadable()); 224 | 225 | return TRUE; 226 | } 227 | 228 | const BOOL BonDriver::GetTsStream(BYTE **ppDst, DWORD *pdwSize, DWORD *pdwRemain) { 229 | if (!stream_loader_ || !stream_loader_->IsPolling()) { 230 | return FALSE; 231 | } 232 | 233 | if (stream_loader_->RemainReadable() == 0) { 234 | *pdwSize = 0; 235 | *pdwRemain = 0; 236 | return TRUE; 237 | } 238 | 239 | std::pair data = stream_loader_->ReadChunkAndRetain(); 240 | 241 | *ppDst = data.first; 242 | *pdwSize = static_cast(data.second); 243 | *pdwRemain = static_cast(stream_loader_->RemainReadable()); 244 | 245 | return TRUE; 246 | } 247 | 248 | void BonDriver::PurgeTsStream(void) { 249 | // do nothing 250 | } 251 | 252 | LPCTSTR BonDriver::GetTunerName(void) { 253 | static const TCHAR* kTunerName = TEXT("BonDriver_EPGStation"); 254 | return kTunerName; 255 | } 256 | 257 | const BOOL BonDriver::IsTunerOpening(void) { 258 | return stream_loader_ && stream_loader_->IsPolling(); 259 | } 260 | 261 | LPCTSTR BonDriver::EnumTuningSpace(const DWORD dwSpace) { 262 | if (dwSpace >= space_types_.size()) { 263 | return nullptr; 264 | } 265 | 266 | static PlatformString space_buffer; 267 | space_buffer = UTF8ToPlatformString(space_types_[dwSpace]); 268 | 269 | return space_buffer.c_str(); 270 | } 271 | 272 | LPCTSTR BonDriver::EnumChannelName(const DWORD dwSpace, const DWORD dwChannel) { 273 | if (dwSpace >= space_types_.size()) { 274 | return nullptr; 275 | } 276 | 277 | if (dwSpace < space_types_.size() - 1) { 278 | if (dwChannel >= space_channel_bases_[static_cast(dwSpace) + 1] - space_channel_bases_[dwSpace]) { 279 | return nullptr; 280 | } 281 | } 282 | 283 | size_t channel_index = space_channel_bases_[dwSpace] + dwChannel; 284 | if (channel_index >= channels_.size()) { 285 | return nullptr; 286 | } 287 | 288 | EPGStation::Channel& channel = channels_[channel_index]; 289 | 290 | static PlatformString name_buffer; 291 | name_buffer = UTF8ToPlatformString(channel.name); 292 | 293 | return name_buffer.c_str(); 294 | } 295 | 296 | const DWORD BonDriver::GetCurSpace(void) { 297 | return current_dwspace_; 298 | } 299 | 300 | const DWORD BonDriver::GetCurChannel(void) { 301 | return current_dwchannel_; 302 | } 303 | -------------------------------------------------------------------------------- /src/bon_driver.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_BON_DRIVER_HPP 6 | #define BONDRIVER_EPGSTATION_BON_DRIVER_HPP 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "IBonDriver2.h" 13 | #include "config.hpp" 14 | #include "epgstation_models.hpp" 15 | #include "epgstation_api.hpp" 16 | 17 | class StreamLoader; 18 | 19 | class BonDriver : public IBonDriver2 { 20 | public: 21 | BonDriver(const Config& config); 22 | ~BonDriver(); 23 | protected: 24 | void Release(void) override; 25 | 26 | const BOOL OpenTuner(void) override; 27 | void CloseTuner(void) override; 28 | const BOOL SetChannel(const BYTE bCh) override; 29 | const float GetSignalLevel(void) override; 30 | const DWORD WaitTsStream(const DWORD dwTimeOut = 0) override; 31 | const DWORD GetReadyCount(void) override; 32 | const BOOL GetTsStream(BYTE* pDst, DWORD* pdwSize, DWORD* pdwRemain) override; 33 | const BOOL GetTsStream(BYTE** ppDst, DWORD* pdwSize, DWORD* pdwRemain) override; 34 | void PurgeTsStream(void) override; 35 | 36 | LPCTSTR GetTunerName(void) override; 37 | const BOOL IsTunerOpening(void) override; 38 | LPCTSTR EnumTuningSpace(const DWORD dwSpace) override; 39 | LPCTSTR EnumChannelName(const DWORD dwSpace, const DWORD dwChannel) override; 40 | const BOOL SetChannel(const DWORD dwSpace, const DWORD dwChannel) override; 41 | const DWORD GetCurSpace(void) override; 42 | const DWORD GetCurChannel(void) override; 43 | private: 44 | void InitChannels(); 45 | private: 46 | const Config& yaml_config_; 47 | EPGStationAPI api_; 48 | 49 | bool init_channels_succeed = false; 50 | EPGStation::Config epgstation_config_; 51 | std::vector channels_; 52 | 53 | size_t chunk_size_ = 188 * 1024; 54 | std::unique_ptr stream_loader_; 55 | 56 | EPGStation::Channel current_channel_; 57 | DWORD current_dwspace_ = 0; 58 | DWORD current_dwchannel_ = 0; 59 | 60 | std::unordered_set space_set_; 61 | std::vector space_types_; 62 | std::vector space_channel_bases_; 63 | }; 64 | 65 | 66 | #endif // BONDRIVER_EPGSTATION_BON_DRIVER_HPP 67 | -------------------------------------------------------------------------------- /src/config.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #include 6 | #include "log.hpp" 7 | #include "string_utils.hpp" 8 | #include "config.hpp" 9 | 10 | Config::Config() : is_loaded_(false) { } 11 | 12 | bool Config::LoadYamlFile(const std::string& filename) { 13 | try { 14 | YAML::Node config = YAML::LoadFile(filename); 15 | 16 | if (config["baseURL"]) { 17 | std::string base_url = config["baseURL"].as(); 18 | base_url_ = StringUtils::RemoveSuffixSlash(base_url); 19 | } else { 20 | Log::ErrorF("Missing baseURL field in config file"); 21 | return false; 22 | } 23 | 24 | if (config["version"]) { 25 | std::string version_desc = config["version"].as(); 26 | if (version_desc == "v1") { 27 | version_ = kEPGStationVersionV1; 28 | } else if (version_desc == "v2") { 29 | version_ = kEPGStationVersionV2; 30 | } else { 31 | Log::ErrorF("Incorrect EPGStation version: %s", version_desc.c_str()); 32 | return false; 33 | } 34 | } else { 35 | Log::ErrorF("Missing version field in config file"); 36 | return false; 37 | } 38 | 39 | if (config["basicAuth"]) { 40 | const YAML::Node& basic_auth_node = config["basicAuth"]; 41 | if (!basic_auth_node.IsMap() || basic_auth_node.size() != 2) { 42 | Log::ErrorF("Invalid basicAuth parameter in config file"); 43 | return false; 44 | } 45 | 46 | BasicAuth basic_auth; 47 | basic_auth.user = basic_auth_node["user"].as(); 48 | basic_auth.password = basic_auth_node["password"].as(); 49 | 50 | basic_auth_ = basic_auth; 51 | } // else: basicAuth is optional 52 | 53 | if (config["mpegTsStreamingMode"]) { 54 | mpegts_streaming_mode_ = config["mpegTsStreamingMode"].as(); 55 | } else { 56 | Log::ErrorF("Missing mpegTsStreamingMode field in config file"); 57 | return false; 58 | } 59 | 60 | if (config["showInactiveServices"]) { 61 | show_inactive_services_ = config["showInactiveServices"].as(); 62 | } // else: showInactiveServices is optional 63 | 64 | if (config["userAgent"]) { 65 | user_agent_ = config["userAgent"].as(); 66 | } else { 67 | // default UserAgent 68 | user_agent_ = "BonDriver_Mirakurun"; 69 | } 70 | 71 | if (config["proxy"]) { 72 | proxy_ = config["proxy"].as(); 73 | } // else: proxy is optional 74 | 75 | if (config["headers"]) { 76 | auto& headers_node = config["headers"]; 77 | 78 | if (headers_node.IsMap()) { 79 | std::map headers; 80 | for (auto iter = headers_node.begin(); iter != headers_node.end(); ++iter) { 81 | headers.insert({iter->first.as(), iter->second.as()}); 82 | } 83 | headers_ = headers; 84 | } else { 85 | Log::ErrorF("headers field must be a map"); 86 | } 87 | } // else: headers is optional 88 | 89 | } catch (YAML::BadFile& ex) { 90 | Log::ErrorF("Load yaml file failed, %s", ex.what()); 91 | return false; 92 | } catch (YAML::InvalidNode& ex) { 93 | Log::ErrorF("Parse yaml file failed, %s", ex.what()); 94 | return false; 95 | } 96 | 97 | is_loaded_ = true; 98 | return true; 99 | } 100 | 101 | bool Config::IsLoaded() const { 102 | return is_loaded_; 103 | } 104 | 105 | std::optional Config::GetBaseURL() const { 106 | return base_url_; 107 | } 108 | 109 | std::optional Config::GetVersion() const { 110 | return version_; 111 | } 112 | 113 | std::optional Config::GetBasicAuth() const { 114 | return basic_auth_; 115 | } 116 | 117 | std::optional Config::GetMpegTsStreamingMode() const { 118 | return mpegts_streaming_mode_; 119 | } 120 | 121 | std::optional Config::GetShowInactiveServices() const { 122 | return show_inactive_services_; 123 | } 124 | 125 | std::optional Config::GetUserAgent() const { 126 | return user_agent_; 127 | } 128 | 129 | std::optional Config::GetProxy() const { 130 | return proxy_; 131 | } 132 | 133 | std::optional> Config::GetHeaders() const { 134 | return headers_; 135 | } 136 | -------------------------------------------------------------------------------- /src/config.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_CONFIG_HPP 6 | #define BONDRIVER_EPGSTATION_CONFIG_HPP 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | enum EPGStationVersion : int { 13 | kEPGStationVersionV1 = 1, 14 | kEPGStationVersionV2 = 2, 15 | }; 16 | 17 | struct BasicAuth { 18 | std::string user; 19 | std::string password; 20 | }; 21 | 22 | class Config { 23 | public: 24 | Config(); 25 | bool LoadYamlFile(const std::string& filename); 26 | bool IsLoaded() const; 27 | 28 | [[nodiscard]] std::optional GetBaseURL() const; 29 | [[nodiscard]] std::optional GetVersion() const; 30 | [[nodiscard]] std::optional GetBasicAuth() const; 31 | [[nodiscard]] std::optional GetMpegTsStreamingMode() const; 32 | [[nodiscard]] std::optional GetShowInactiveServices() const; 33 | [[nodiscard]] std::optional GetUserAgent() const; 34 | [[nodiscard]] std::optional GetProxy() const; 35 | [[nodiscard]] std::optional> GetHeaders() const; 36 | private: 37 | bool is_loaded_; 38 | std::optional base_url_; 39 | std::optional version_; 40 | std::optional basic_auth_; 41 | std::optional mpegts_streaming_mode_; 42 | std::optional show_inactive_services_; 43 | std::optional user_agent_; 44 | std::optional proxy_; 45 | std::optional> headers_; 46 | }; 47 | 48 | #endif // BONDRIVER_EPGSTATION_CONFIG_HPP 49 | -------------------------------------------------------------------------------- /src/epgstation_api.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #include 6 | #include 7 | #include "epgstation_models_deserialize.hpp" 8 | #include "log.hpp" 9 | #include "string_utils.hpp" 10 | #include "epgstation_api.hpp" 11 | 12 | using json = nlohmann::json; 13 | 14 | static const char* kEPGStationAPI_Config = "/api/config"; 15 | static const char* kEPGStationAPI_Channels = "/api/channels"; 16 | static const char* kEPGStationAPIv1_Broadcasting = "/api/schedule/broadcasting"; 17 | static const char* kEPGStationAPIv2_Broadcasting = "/api/schedules/broadcasting?isHalfWidth=false"; 18 | static const char* kEPGStationAPI_StreamsLive = "/api/streams/live/"; 19 | 20 | EPGStationAPI::EPGStationAPI(const std::string& base_url, EPGStationVersion version) 21 | : base_url_(base_url), version_(version) {} 22 | 23 | void EPGStationAPI::SetBasicAuth(const std::string& user, const std::string& password) { 24 | has_basic_auth_ = true; 25 | basicauth_user_ = user; 26 | basicauth_password_ = password; 27 | } 28 | 29 | void EPGStationAPI::SetUserAgent(const std::string& user_agent) { 30 | has_user_agent_ = true; 31 | user_agent_ = user_agent; 32 | } 33 | 34 | void EPGStationAPI::SetProxy(const std::string& proxy) { 35 | has_proxy_ = true; 36 | proxy_ = proxy; 37 | } 38 | 39 | void EPGStationAPI::SetHeaders(const std::map& headers) { 40 | has_headers_ = true; 41 | headers_ = headers; 42 | } 43 | 44 | std::optional EPGStationAPI::GetConfig() { 45 | cpr::Session session; 46 | session.SetUrl(cpr::Url{this->base_url_ + kEPGStationAPI_Config}); 47 | 48 | if (has_basic_auth_) { 49 | session.SetAuth(cpr::Authentication{basicauth_user_, basicauth_password_}); 50 | } 51 | 52 | if (has_user_agent_) { 53 | session.SetUserAgent({user_agent_}); 54 | } 55 | 56 | if (has_proxy_) { 57 | session.SetProxies({{"http", proxy_}, 58 | {"https", proxy_}}); 59 | } 60 | 61 | if (has_headers_) { 62 | for (auto& pair : headers_) { 63 | cpr::Header header{{pair.first, pair.second}}; 64 | session.UpdateHeader(header); 65 | } 66 | } 67 | 68 | cpr::Response response = session.Get(); 69 | 70 | if (response.error) { 71 | Log::ErrorF("curl failed for %s: error_code = %d, msg = %s", kEPGStationAPI_Config, response.error.code, response.error.message.c_str()); 72 | return std::nullopt; 73 | } else if (response.status_code >= 400) { 74 | Log::ErrorF("%s error: status_code = %d, body = %s", kEPGStationAPI_Config, response.status_code, response.text.c_str()); 75 | return std::nullopt; 76 | } 77 | 78 | json j = json::parse(response.text); 79 | EPGStation::Config config = j.get(); 80 | 81 | return config; 82 | } 83 | 84 | std::optional EPGStationAPI::GetChannels() { 85 | cpr::Session session; 86 | session.SetUrl(cpr::Url{this->base_url_ + kEPGStationAPI_Channels}); 87 | 88 | if (has_basic_auth_) { 89 | session.SetAuth(cpr::Authentication{basicauth_user_, basicauth_password_}); 90 | } 91 | 92 | if (has_user_agent_) { 93 | session.SetUserAgent({user_agent_}); 94 | } 95 | 96 | if (has_proxy_) { 97 | session.SetProxies({{"http", proxy_}, 98 | {"https", proxy_}}); 99 | } 100 | 101 | if (has_headers_) { 102 | for (auto& pair : headers_) { 103 | cpr::Header header{{pair.first, pair.second}}; 104 | session.UpdateHeader(header); 105 | } 106 | } 107 | 108 | cpr::Response response = session.Get(); 109 | 110 | if (response.error) { 111 | Log::ErrorF("curl failed for %s: error_code = %d, msg = %s", kEPGStationAPI_Channels, response.error.code, response.error.message.c_str()); 112 | return std::nullopt; 113 | } else if (response.status_code >= 400) { 114 | Log::ErrorF("%s error: status_code = %d, body = %s", kEPGStationAPI_Channels, response.status_code, response.text.c_str()); 115 | return std::nullopt; 116 | } 117 | 118 | json j = json::parse(response.text); 119 | EPGStation::Channels channels = j.get(); 120 | 121 | return channels; 122 | } 123 | 124 | std::optional EPGStationAPI::GetBroadcasting() { 125 | const char* path_query = ""; 126 | 127 | if (version_ == kEPGStationVersionV1) { 128 | path_query = kEPGStationAPIv1_Broadcasting; 129 | } else if (version_ == kEPGStationVersionV2) { 130 | path_query = kEPGStationAPIv2_Broadcasting; 131 | } 132 | 133 | cpr::Session session; 134 | session.SetUrl(cpr::Url{this->base_url_ + path_query}); 135 | 136 | if (has_basic_auth_) { 137 | session.SetAuth(cpr::Authentication{basicauth_user_, basicauth_password_}); 138 | } 139 | 140 | if (has_user_agent_) { 141 | session.SetUserAgent({user_agent_}); 142 | } 143 | 144 | if (has_proxy_) { 145 | session.SetProxies({{"http", proxy_}, 146 | {"https", proxy_}}); 147 | } 148 | 149 | if (has_headers_) { 150 | for (auto& pair : headers_) { 151 | cpr::Header header{{pair.first, pair.second}}; 152 | session.UpdateHeader(header); 153 | } 154 | } 155 | 156 | cpr::Response response = session.Get(); 157 | 158 | if (response.error) { 159 | Log::ErrorF("curl failed for %s: error_code = %d, msg = %s", path_query, response.error.code, response.error.message.c_str()); 160 | return std::nullopt; 161 | } else if (response.status_code >= 400) { 162 | Log::ErrorF("%s error: status_code = %d, body = %s", path_query, response.status_code, response.text.c_str()); 163 | return std::nullopt; 164 | } 165 | 166 | json j = json::parse(response.text); 167 | EPGStation::Broadcasting broadcasting = j.get(); 168 | 169 | return broadcasting; 170 | } 171 | 172 | std::string EPGStationAPI::GetMpegtsLiveStreamPathQuery(int64_t id, int encode_mode) { 173 | std::string path_query = kEPGStationAPI_StreamsLive; 174 | path_query.append(std::to_string(id)); 175 | 176 | if (version_ == kEPGStationVersionV1) { 177 | path_query.append("/mpegts?mode="); 178 | } else if (version_ == kEPGStationVersionV2) { 179 | path_query.append("/m2ts?mode="); 180 | } 181 | 182 | path_query.append(std::to_string(encode_mode)); 183 | return path_query; 184 | } 185 | -------------------------------------------------------------------------------- /src/epgstation_api.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_EPGSTATION_API_HPP 6 | #define BONDRIVER_EPGSTATION_EPGSTATION_API_HPP 7 | 8 | #include 9 | #include "config.hpp" 10 | #include "epgstation_models.hpp" 11 | 12 | class EPGStationAPI { 13 | public: 14 | EPGStationAPI(const std::string& base_url, EPGStationVersion version); 15 | void SetBasicAuth(const std::string& user, const std::string& password); 16 | void SetUserAgent(const std::string& user_agent); 17 | void SetProxy(const std::string& proxy); 18 | void SetHeaders(const std::map& headers); 19 | std::optional GetConfig(); 20 | std::optional GetChannels(); 21 | std::optional GetBroadcasting(); 22 | std::string GetMpegtsLiveStreamPathQuery(int64_t id, int encode_mode); 23 | private: 24 | std::string base_url_; 25 | EPGStationVersion version_; 26 | 27 | bool has_basic_auth_ = false; 28 | std::string basicauth_user_; 29 | std::string basicauth_password_; 30 | 31 | bool has_user_agent_ = false; 32 | std::string user_agent_; 33 | 34 | bool has_proxy_ = false; 35 | std::string proxy_; 36 | 37 | bool has_headers_ = false; 38 | std::map headers_; 39 | }; 40 | 41 | 42 | #endif // BONDRIVER_EPGSTATION_EPGSTATION_API_HPP 43 | -------------------------------------------------------------------------------- /src/epgstation_models.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_EPGSTATION_MODELS_HPP 6 | #define BONDRIVER_EPGSTATION_EPGSTATION_MODELS_HPP 7 | 8 | #include 9 | #include 10 | #include 11 | #include "string_utils.hpp" 12 | 13 | namespace EPGStation { 14 | 15 | struct Error { 16 | int code = 0; 17 | std::string message; 18 | std::string errors; 19 | }; 20 | 21 | struct Channel { 22 | int64_t id = 0; 23 | int service_id = 0; 24 | int network_id = 0; 25 | std::string name; 26 | int remote_control_key_id = 0; 27 | bool has_logo_data = false; 28 | std::string channel_type; 29 | int channel_type_id = 0; 30 | std::string channel; 31 | int type = 0; 32 | }; 33 | 34 | struct Channels { 35 | std::vector channels; 36 | }; 37 | 38 | struct Broadcasting { 39 | std::vector channels; 40 | }; 41 | 42 | struct Config { 43 | public: 44 | struct Broadcast { 45 | bool GR = false; 46 | bool BS = false; 47 | bool CS = false; 48 | bool SKY = false; 49 | }; 50 | public: 51 | bool enable_live_streaming = false; 52 | Broadcast broadcast; 53 | }; 54 | 55 | } 56 | 57 | #endif // BONDRIVER_EPGSTATION_EPGSTATION_MODELS_HPP 58 | -------------------------------------------------------------------------------- /src/epgstation_models_deserialize.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_EPGSTATION_MODELS_DESERIALIZE_HPP 6 | #define BONDRIVER_EPGSTATION_EPGSTATION_MODELS_DESERIALIZE_HPP 7 | 8 | #include 9 | #include 10 | #include "epgstation_models.hpp" 11 | 12 | namespace EPGStation { 13 | 14 | // Error 15 | void from_json(const nlohmann::json& json, Error& error) { 16 | error.code = json.at("code").get(); 17 | error.message = json.at("message").get(); 18 | error.errors = json.at("errors").get(); 19 | } 20 | 21 | // Channel 22 | void from_json(const nlohmann::json& json, Channel& channel) { 23 | assert(json.is_object()); 24 | 25 | channel.id = json.at("id").get(); 26 | channel.service_id = json.at("serviceId").get(); 27 | channel.network_id = json.at("networkId").get(); 28 | channel.name = json.at("name").get(); 29 | channel.has_logo_data = json.at("hasLogoData").get(); 30 | channel.channel_type = json.at("channelType").get(); 31 | 32 | // optional field 33 | if (json.find("remoteControlKeyId") != json.end()) { 34 | channel.remote_control_key_id = json.at("remoteControlKeyId").get(); 35 | } 36 | 37 | if (json.find("channelTypeId") != json.end()) { 38 | channel.channel_type_id = json.at("channelTypeId").get(); 39 | } 40 | 41 | if (json.find("channel") != json.end()) { 42 | channel.channel = json.at("channel").get(); 43 | } 44 | 45 | if (json.find("type") != json.end()) { 46 | channel.type = json.at("type").get(); 47 | } 48 | } 49 | 50 | // Channels 51 | void from_json(const nlohmann::json& json, Channels& channels) { 52 | assert(json.is_array()); 53 | 54 | for (const nlohmann::json& element : json) { 55 | auto channel = element.get(); 56 | channels.channels.push_back(std::move(channel)); 57 | } 58 | } 59 | 60 | void from_json(const nlohmann::json& json, Broadcasting& broadcasting) { 61 | assert(json.is_array()); 62 | 63 | for (const nlohmann::json& element : json) { 64 | auto channel = element["channel"].get(); 65 | broadcasting.channels.push_back(std::move(channel)); 66 | } 67 | } 68 | 69 | // Config 70 | void from_json(const nlohmann::json& json, Config& config) { 71 | if (json.find("enableLiveStreaming") != json.end()) { 72 | // EPGStation v1 73 | config.enable_live_streaming = json.at("enableLiveStreaming").get(); 74 | } else if (json.find("isEnableTSLiveStream") != json.end()) { 75 | // EPGStation v2 76 | config.enable_live_streaming = json.at("isEnableTSLiveStream").get(); 77 | } 78 | 79 | config.broadcast.GR = json["broadcast"]["GR"].get(); 80 | config.broadcast.BS = json["broadcast"]["BS"].get(); 81 | config.broadcast.CS = json["broadcast"]["CS"].get(); 82 | config.broadcast.SKY = json["broadcast"]["SKY"].get(); 83 | } 84 | 85 | } 86 | 87 | #endif // BONDRIVER_EPGSTATION_EPGSTATION_MODELS_DESERIALIZE_HPP 88 | -------------------------------------------------------------------------------- /src/library.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifdef _WIN32 6 | #include 7 | #endif 8 | 9 | #include 10 | #include 11 | #include "IBonDriver.h" 12 | #include "bon_driver.hpp" 13 | #include "config.hpp" 14 | #include "log.hpp" 15 | #include "library.hpp" 16 | 17 | static HMODULE hmodule = nullptr; 18 | static Config config; 19 | 20 | static bool LoadConfigYamlFile() { 21 | Log::InfoF(LOG_FILE_FUNCTION); 22 | // Get dll file path 23 | char path_buffer[_MAX_PATH] = {0}; 24 | if (GetModuleFileNameA(hmodule, path_buffer, _MAX_PATH) == 0) { 25 | DWORD ret = GetLastError(); 26 | Log::ErrorF("GetModuleFileName failed, error = %#010x", ret); 27 | return false; 28 | } 29 | 30 | char drive[_MAX_DRIVE] = {0}; 31 | char dir[_MAX_DIR] = {0}; 32 | char fname[_MAX_FNAME] = {0}; 33 | char ext[_MAX_EXT] = {0}; 34 | 35 | _splitpath_s(path_buffer, drive, dir, fname, ext); 36 | 37 | // Generate yaml file path from dll path 38 | std::string yaml_path(_MAX_PATH, '\0'); 39 | sprintf(&yaml_path[0], "%s%s%s.yml", drive, dir, fname); 40 | Log::InfoF("Yaml FilePath: %s", yaml_path.c_str()); 41 | 42 | return config.LoadYamlFile(yaml_path); 43 | } 44 | 45 | extern "C" EXPORT_API IBonDriver* CreateBonDriver() { 46 | Log::InfoF(LOG_FILE_FUNCTION); 47 | 48 | if (!config.IsLoaded()) { 49 | if (!LoadConfigYamlFile()) { 50 | return nullptr; 51 | } 52 | } 53 | 54 | return new BonDriver(config); 55 | } 56 | 57 | BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) { 58 | switch(fdwReason) { 59 | case DLL_PROCESS_ATTACH: 60 | Log::Info("DLL_PROCESS_ATTACH"); 61 | hmodule = hinstDLL; 62 | LoadConfigYamlFile(); 63 | break; 64 | case DLL_PROCESS_DETACH: 65 | Log::Info("DLL_PROCESS_DETACH"); 66 | break; 67 | } 68 | return TRUE; 69 | } -------------------------------------------------------------------------------- /src/library.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_LIBRARY_HPP 6 | #define BONDRIVER_EPGSTATION_LIBRARY_HPP 7 | 8 | 9 | #endif //BONDRIVER_EPGSTATION_LIBRARY_HPP 10 | -------------------------------------------------------------------------------- /src/log.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #include 6 | #include 7 | #include 8 | #include "log.hpp" 9 | 10 | #ifdef _WIN32 11 | #include 12 | #endif 13 | 14 | static const char* LOG_TAG = "BonDriver_EPGStation"; 15 | static const wchar_t* LOG_TAG_W = L"BonDriver_EPGStation"; 16 | 17 | void Log::Info(const char* str) { 18 | char buffer[512] = {0}; 19 | sprintf(buffer, "[%s] INFO: %s\n", LOG_TAG, str); 20 | 21 | #ifdef _WIN32 22 | OutputDebugStringA(buffer); 23 | #else 24 | fputs(buffer, stdout); 25 | #endif 26 | } 27 | 28 | void Log::Info(const wchar_t* str) { 29 | wchar_t buffer[512] = {0}; 30 | swprintf_s(buffer, L"[%ls] INFO: %ls\n", LOG_TAG_W, str); 31 | 32 | #ifdef _WIN32 33 | OutputDebugStringW(buffer); 34 | #else 35 | fputws(buffer, stdout); 36 | #endif 37 | } 38 | 39 | void Log::InfoF(const char* format, ...) { 40 | char formatted[512] = {0}; 41 | va_list vaList; 42 | va_start(vaList, format); 43 | vsprintf(formatted, format, vaList); 44 | va_end(vaList); 45 | 46 | char buffer[512] = {0}; 47 | sprintf(buffer, "[%s] INFO: %s\n", LOG_TAG, formatted); 48 | 49 | #ifdef _WIN32 50 | OutputDebugStringA(buffer); 51 | #else 52 | fputs(buffer, stdout); 53 | #endif 54 | } 55 | 56 | void Log::InfoF(const wchar_t* format, ...) { 57 | wchar_t formatted[512] = {0}; 58 | va_list vaList; 59 | va_start(vaList, format); 60 | vswprintf_s(formatted, format, vaList); 61 | va_end(vaList); 62 | 63 | wchar_t buffer[512] = {0}; 64 | swprintf_s(buffer, L"[%ls] INFO: %ls\n", LOG_TAG_W, formatted); 65 | 66 | #ifdef _WIN32 67 | OutputDebugStringW(buffer); 68 | #else 69 | fputws(buffer, stdout); 70 | #endif 71 | } 72 | 73 | void Log::Error(const char* str) { 74 | char buffer[512] = {0}; 75 | sprintf(buffer, "[%s] ERROR: %s\n", LOG_TAG, str); 76 | 77 | #ifdef _WIN32 78 | OutputDebugStringA(buffer); 79 | #else 80 | fputs(buffer, stderr); 81 | #endif 82 | } 83 | 84 | void Log::Error(const wchar_t* str) { 85 | wchar_t buffer[512] = {0}; 86 | swprintf_s(buffer, L"[%ls] ERROR: %ls\n", LOG_TAG_W, str); 87 | 88 | #ifdef _WIN32 89 | OutputDebugStringW(buffer); 90 | #else 91 | fputws(buffer, stderr); 92 | #endif 93 | } 94 | 95 | void Log::ErrorF(const char* format, ...) { 96 | char formatted[512] = {0}; 97 | va_list vaList; 98 | va_start(vaList, format); 99 | vsprintf(formatted, format, vaList); 100 | va_end(vaList); 101 | 102 | char buffer[512] = {0}; 103 | sprintf(buffer, "[%s] ERROR: %s\n", LOG_TAG, formatted); 104 | 105 | #ifdef _WIN32 106 | OutputDebugStringA(buffer); 107 | #else 108 | fputs(buffer, stderr); 109 | #endif 110 | } 111 | 112 | void Log::ErrorF(const wchar_t* format, ...) { 113 | wchar_t formatted[512] = {0}; 114 | va_list vaList; 115 | va_start(vaList, format); 116 | vswprintf_s(formatted, format, vaList); 117 | va_end(vaList); 118 | 119 | wchar_t buffer[512] = {0}; 120 | swprintf_s(buffer, L"[%ls] ERROR: %ls\n", LOG_TAG_W, formatted); 121 | 122 | #ifdef _WIN32 123 | OutputDebugStringW(buffer); 124 | #else 125 | fputws(buffer, stderr); 126 | #endif 127 | } 128 | -------------------------------------------------------------------------------- /src/log.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_LOG_HPP 6 | #define BONDRIVER_EPGSTATION_LOG_HPP 7 | 8 | static constexpr const char* file_name(const char* path) { 9 | const char* name = path; 10 | while (*path) { 11 | const char ch = *path++; 12 | if (ch == '\\' || ch == '/') { 13 | name = path; 14 | } 15 | } 16 | return name; 17 | } 18 | 19 | #ifndef __SHORT_FILE__ 20 | #define __SHORT_FILE__ file_name(__FILE__) 21 | #endif 22 | 23 | #ifndef __FUNCTION_NAME__ 24 | #ifdef _WIN32 25 | #define __FUNCTION_NAME__ __FUNCTION__ 26 | #else 27 | #define __FUNCTION_NAME__ __func__ 28 | #endif 29 | #endif 30 | 31 | #ifndef LOG_FUNCTION 32 | #define LOG_FUNCTION "%s()",__FUNCTION_NAME__ 33 | #endif 34 | 35 | #ifndef LOG_FUNCTION_MESSAGE 36 | #define LOG_FUNCTION_MESSAGE(m) "%s(): %s",__FUNCTION_NAME__,m 37 | #endif 38 | 39 | #ifndef LOG_FILE_FUNCTION 40 | #define LOG_FILE_FUNCTION "%s:%d:%s()",__SHORT_FILE__,__LINE__,__FUNCTION_NAME__ 41 | #endif 42 | 43 | #ifndef LOG_FILE_FUNCTION_MESSAGE 44 | #define LOG_FILE_FUNCTION_MESSAGE(m) "%s:%d:%s(): %s",__SHORT_FILE__,__LINE__,__FUNCTION_NAME__,m 45 | #endif 46 | 47 | #ifndef LOG_FILE_MESSAGE 48 | #define LOG_FILE_MESSAGE(m) "%s:%d: %s",__SHORT_FILE__,__LINE__,m 49 | #endif 50 | 51 | class Log { 52 | public: 53 | static void Info(const char* str); 54 | static void Info(const wchar_t* str); 55 | 56 | static void InfoF(const char* format, ...); 57 | static void InfoF(const wchar_t* format, ...); 58 | 59 | static void Error(const char* str); 60 | static void Error(const wchar_t* str); 61 | 62 | static void ErrorF(const char* format, ...); 63 | static void ErrorF(const wchar_t* format, ...); 64 | }; 65 | 66 | 67 | #endif // BONDRIVER_EPGSTATION_LOG_HPP 68 | -------------------------------------------------------------------------------- /src/noncopyable.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_NONCOPYABLE_HPP 6 | #define BONDRIVER_EPGSTATION_NONCOPYABLE_HPP 7 | 8 | class Noncopyable { 9 | public: 10 | Noncopyable() = default; 11 | ~Noncopyable() = default; 12 | private: 13 | Noncopyable(const Noncopyable&) = delete; 14 | Noncopyable& operator=(const Noncopyable&) = delete; 15 | }; 16 | 17 | #define DISALLOW_COPY_AND_ASSIGN(TypeName) \ 18 | TypeName(const TypeName&) = delete; \ 19 | TypeName& operator=(const TypeName&) = delete; 20 | 21 | #endif // BONDRIVER_EPGSTATION_NONCOPYABLE_HPP 22 | -------------------------------------------------------------------------------- /src/scope_guard.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_SCOPE_GUARD_HPP 6 | #define BONDRIVER_EPGSTATION_SCOPE_GUARD_HPP 7 | 8 | #include 9 | #include "noncopyable.hpp" 10 | 11 | class ScopeGuard { 12 | private: 13 | using Deleter = std::function; 14 | public: 15 | template 16 | explicit ScopeGuard(F&& fn) noexcept : callback_(std::forward(fn)), dismissed_(false) {} 17 | 18 | ScopeGuard(ScopeGuard&& other) noexcept : callback_(std::move(other.callback_)), dismissed_(other.dismissed_) {} 19 | 20 | ~ScopeGuard() { 21 | if (!dismissed_) { 22 | callback_(); 23 | } 24 | } 25 | 26 | void Dismiss() noexcept { 27 | dismissed_ = true; 28 | } 29 | private: 30 | Deleter callback_; 31 | bool dismissed_; 32 | private: 33 | DISALLOW_COPY_AND_ASSIGN(ScopeGuard); 34 | }; 35 | 36 | struct ScopeGuardDriver {}; 37 | 38 | template 39 | auto operator+(ScopeGuardDriver, F&& fn) { 40 | return ScopeGuard(std::forward(fn)); 41 | } 42 | 43 | #define CONCATENATE_IMPL(part1, part2) part1##part2 44 | #define CONCATENATE(part1, part2) CONCATENATE_IMPL(part1, part2) 45 | #define ANONYMOUS_VAR(tag) CONCATENATE(tag, __LINE__) 46 | 47 | #define ON_SCOPE_EXIT \ 48 | auto ANONYMOUS_VAR(_exit_) = ScopeGuardDriver() + [&]() noexcept 49 | 50 | #define MAKE_SCOPE_GUARD \ 51 | ScopeGuardDriver() + [&]() noexcept 52 | 53 | #endif // BONDRIVER_EPGSTATION_SCOPE_GUARD_HPP 54 | -------------------------------------------------------------------------------- /src/speed_sampler.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifdef _WIN32 6 | #include 7 | #endif 8 | #include "speed_sampler.hpp" 9 | 10 | SpeedSampler::SpeedSampler() { 11 | QueryPerformanceFrequency(reinterpret_cast(&frequency_)); 12 | } 13 | 14 | void SpeedSampler::Reset() { 15 | first_checkpoint_ = 0; 16 | last_checkpoint_ = 0; 17 | total_bytes_ = 0; 18 | interval_bytes_ = 0; 19 | last_second_bytes_ = 0; 20 | } 21 | 22 | void SpeedSampler::AddBytes(size_t bytes) { 23 | if (first_checkpoint_ == 0) { 24 | // first time 25 | first_checkpoint_ = GetCurrentClock(); 26 | last_checkpoint_ = first_checkpoint_; 27 | 28 | interval_bytes_ += bytes; 29 | total_bytes_ += bytes; 30 | } else if (GetCurrentClock() - last_checkpoint_ < 1000) { 31 | // 0 < duration < 1000 32 | interval_bytes_ += bytes; 33 | total_bytes_ += bytes; 34 | } else { 35 | // duration >= 1000 36 | last_second_bytes_ = interval_bytes_; 37 | 38 | interval_bytes_ = bytes; 39 | total_bytes_ += bytes; 40 | 41 | last_checkpoint_ = GetCurrentClock(); 42 | } 43 | } 44 | 45 | float SpeedSampler::CurrentKBps() { 46 | AddBytes(0); 47 | 48 | double elapsed_seconds = (GetCurrentClock() - last_checkpoint_) / 1000.0f; 49 | if (elapsed_seconds == 0.0f) { 50 | elapsed_seconds = 1.0f; 51 | } 52 | 53 | double value = (interval_bytes_ / elapsed_seconds) / 1024.0f; 54 | 55 | return static_cast(value); 56 | } 57 | 58 | float SpeedSampler::LastSecondKBps() { 59 | AddBytes(0); 60 | 61 | if (last_second_bytes_ > 0) { 62 | return last_second_bytes_ / 1024.0f; 63 | } 64 | 65 | // last_second_bytes_ == 0 66 | if (GetCurrentClock() - last_checkpoint_ >= 500) { 67 | // if time interval since last checkpoint has exceeded 500ms 68 | // the speed is nearly accurate 69 | return CurrentKBps(); 70 | } 71 | 72 | // We don't know 73 | return 0.0f; 74 | } 75 | 76 | float SpeedSampler::AverageKBps() { 77 | double elapsed_seconds = (GetCurrentClock() - first_checkpoint_) / 1000.0f; 78 | return static_cast((total_bytes_ / elapsed_seconds) / 1024.0f); 79 | } 80 | 81 | time_t SpeedSampler::GetCurrentClock() { 82 | LARGE_INTEGER current; 83 | QueryPerformanceCounter(¤t); 84 | return current.QuadPart * 1000 / frequency_.QuadPart; 85 | } 86 | -------------------------------------------------------------------------------- /src/speed_sampler.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_SPEED_SAMPLER_HPP 6 | #define BONDRIVER_EPGSTATION_SPEED_SAMPLER_HPP 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | class SpeedSampler { 13 | private: 14 | typedef union LargeInteger { 15 | struct { 16 | uint32_t LowPart; 17 | int32_t HighPart; 18 | }; 19 | int64_t QuadPart; 20 | } LargeInteger; 21 | public: 22 | SpeedSampler(); 23 | void Reset(); 24 | void AddBytes(size_t bytes); 25 | float CurrentKBps(); 26 | float LastSecondKBps(); 27 | float AverageKBps(); 28 | private: 29 | time_t GetCurrentClock(); 30 | private: 31 | time_t first_checkpoint_ = 0; 32 | time_t last_checkpoint_ = 0; 33 | 34 | size_t interval_bytes_ = 0; 35 | size_t total_bytes_ = 0; 36 | size_t last_second_bytes_ = 0; 37 | 38 | LargeInteger frequency_; 39 | }; 40 | 41 | 42 | #endif // BONDRIVER_EPGSTATION_SPEED_SAMPLER_HPP 43 | -------------------------------------------------------------------------------- /src/stream_loader.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "log.hpp" 10 | #include "scope_guard.hpp" 11 | #include "string_utils.hpp" 12 | #include "stream_loader.hpp" 13 | 14 | using namespace std::placeholders; 15 | 16 | StreamLoader::StreamLoader(size_t chunk_size, size_t max_chunk_count, size_t min_chunk_count) : 17 | chunk_size_(chunk_size), blocking_buffer_(chunk_size, max_chunk_count, min_chunk_count) { 18 | assert(!has_requested_ && "Once requested StreamLoader cannot be reused!"); 19 | } 20 | 21 | StreamLoader::~StreamLoader() { 22 | if (IsPolling()) { 23 | Abort(); 24 | } 25 | } 26 | 27 | bool StreamLoader::Open(const std::string& base_url, 28 | const std::string& path_query, 29 | std::optional basic_auth, 30 | std::optional user_agent, 31 | std::optional proxy, 32 | std::optional> headers) { 33 | std::string url = base_url + path_query; 34 | Log::InfoF("StreamLoader::Open(): Opening %s", url.c_str()); 35 | 36 | session_.SetUrl(cpr::Url{url}); 37 | 38 | if (basic_auth) { 39 | session_.SetAuth(cpr::Authentication{basic_auth->user, basic_auth->password}); 40 | } 41 | 42 | if (user_agent) { 43 | session_.SetUserAgent({user_agent.value()}); 44 | } 45 | 46 | if (proxy) { 47 | session_.SetProxies({{"http", proxy.value()}, 48 | {"https", proxy.value()}}); 49 | } 50 | 51 | if (headers) { 52 | for (const auto& pair : headers.value()) { 53 | cpr::Header header{{pair.first, pair.second}}; 54 | session_.UpdateHeader(header); 55 | } 56 | } 57 | 58 | session_.SetHeaderCallback(cpr::HeaderCallback(std::bind(&StreamLoader::OnHeaderCallback, this, _1))); 59 | session_.SetWriteCallback(cpr::WriteCallback(std::bind(&StreamLoader::OnWriteCallback, this, _1))); 60 | 61 | auto holder = session_.GetCurlHolder(); 62 | CURL* curl = holder->handle; 63 | curl_easy_setopt(curl, CURLOPT_OPENSOCKETFUNCTION, &StreamLoader::OnOpenSocketCallback); 64 | curl_easy_setopt(curl, CURLOPT_OPENSOCKETDATA, this); 65 | 66 | has_requested_ = true; 67 | 68 | async_response_ = std::async(std::launch::async, [this] { 69 | cpr::Response response = session_.Get(); 70 | bool has_error = false; 71 | 72 | if (socket_ == INVALID_SOCKET) { 73 | Log::Info("StreamLoader::Open(): curl socket has been force closed by Abort()"); 74 | } else if (response.error && response.error.code != cpr::ErrorCode::REQUEST_CANCELLED) { 75 | has_error = true; 76 | Log::ErrorF("StreamLoader::Open(): curl failed with error_code: %d, msg = %s", 77 | response.error.code, 78 | response.error.message.c_str()); 79 | } else if (response.status_code >= 400) { 80 | has_error = true; 81 | Log::ErrorF("StreamLoader::Open(): Invalid status code: %d, body = %s", 82 | response.status_code, 83 | response.text.c_str()); 84 | } 85 | 86 | if (has_error) { 87 | std::lock_guard lock(response_mutex_); 88 | has_response_received_ = true; 89 | request_failed_ = true; 90 | response_cv_.notify_all(); 91 | } 92 | 93 | if (!has_error && !has_requested_abort_) { 94 | Log::InfoF(LOG_FILE_MESSAGE("curl_easy_perform returned, pulling completed")); 95 | has_reached_eof_ = true; 96 | } 97 | 98 | return response; 99 | }); 100 | 101 | return true; 102 | } 103 | 104 | StreamLoader::WaitResult StreamLoader::WaitForResponse(std::chrono::milliseconds timeout) { 105 | std::unique_lock lock(response_mutex_); 106 | 107 | if (!has_requested_) { 108 | return WaitResult::kWaitFailed; 109 | } 110 | 111 | if (has_response_received_ && !request_failed_) { 112 | return WaitResult::kResultOK; 113 | } 114 | 115 | bool pred = response_cv_.wait_for(lock, timeout, [this] { 116 | return has_response_received_; 117 | }); 118 | 119 | if (!pred) { 120 | return WaitResult::kWaitTimeout; 121 | } 122 | 123 | // [has_response_received_] should be true here 124 | 125 | if (request_failed_) { 126 | return WaitResult::kResultFailed; 127 | } 128 | 129 | return WaitResult::kResultOK; 130 | } 131 | 132 | StreamLoader::WaitResult StreamLoader::WaitForData() { 133 | if (!has_requested_) { 134 | return WaitResult::kWaitFailed; 135 | } 136 | 137 | if (request_failed_) { 138 | return WaitResult::kResultFailed; 139 | } 140 | 141 | blocking_buffer_.WaitUntilData(); 142 | 143 | if (request_failed_) { 144 | return WaitResult::kResultFailed; 145 | } 146 | 147 | return WaitResult::kResultOK; 148 | } 149 | 150 | curl_socket_t StreamLoader::OnOpenSocketCallback(StreamLoader* self, curlsocktype purpose, curl_sockaddr* addr) { 151 | SOCKET sock = socket(addr->family, addr->socktype, addr->protocol); 152 | self->socket_ = sock; 153 | return sock; 154 | } 155 | 156 | void StreamLoader::ForceShutdown() { 157 | if (socket_ != INVALID_SOCKET) { 158 | shutdown(socket_, SD_BOTH); 159 | closesocket(socket_); 160 | socket_ = INVALID_SOCKET; 161 | } 162 | } 163 | 164 | bool StreamLoader::OnHeaderCallback(std::string data) { 165 | if (has_requested_abort_) { 166 | // return false to cancel the transfer 167 | return false; 168 | } 169 | 170 | if (has_response_received_) { 171 | // status_code received, ignore subsequent callback 172 | return true; 173 | } 174 | 175 | ON_SCOPE_EXIT { 176 | // Acquire mutex for WaitForResponse() 177 | std::lock_guard lock(response_mutex_); 178 | has_response_received_ = true; 179 | // Notify WaitForResponse() 180 | response_cv_.notify_all(); 181 | }; 182 | 183 | auto holder = session_.GetCurlHolder(); 184 | CURL* curl = holder->handle; 185 | 186 | long status_code = 0; 187 | CURLcode ret = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status_code); 188 | 189 | // 40x error, set request_failed_ to false and cancel transfer 190 | if (ret == CURLcode::CURLE_OK && status_code >= 400) { 191 | Log::ErrorF("StreamLoader::OnHeaderCallback(): Invalid status code: %d", status_code); 192 | request_failed_ = true; 193 | return false; 194 | } 195 | 196 | Log::InfoF("StreamLoader::OnHeaderCallback(): Received response code: %d, start polling", status_code); 197 | // 20x OK, notify WaitForResponse and continue transfer 198 | return true; 199 | } 200 | 201 | bool StreamLoader::OnWriteCallback(std::string data) { 202 | if (has_requested_abort_) { 203 | // return false to cancel the transfer 204 | return false; 205 | } 206 | 207 | speed_sampler_.AddBytes(data.size()); 208 | 209 | blocking_buffer_.Write(reinterpret_cast(data.data()), data.size()); 210 | 211 | return true; 212 | } 213 | 214 | void StreamLoader::Abort() { 215 | Log::InfoF("StreamLoader::Abort(): Aborting"); 216 | has_requested_abort_ = true; 217 | blocking_buffer_.NotifyExit(); 218 | 219 | if (!has_response_received_) { 220 | // If server hasn't returned any response, force kill the underlying socket 221 | ForceShutdown(); 222 | } 223 | 224 | async_response_.wait(); 225 | } 226 | 227 | size_t StreamLoader::Read(uint8_t* buffer, size_t expected_bytes) { 228 | size_t bytes_read = blocking_buffer_.Read(buffer, expected_bytes); 229 | return bytes_read; 230 | } 231 | 232 | std::pair StreamLoader::ReadChunkAndRetain() { 233 | return blocking_buffer_.ReadChunkAndRetain(); 234 | } 235 | 236 | size_t StreamLoader::RemainReadable() { 237 | return blocking_buffer_.ReadableBytes(); 238 | } 239 | 240 | bool StreamLoader::IsPolling() { 241 | return has_requested_ && !request_failed_ && !has_reached_eof_ && !has_requested_abort_; 242 | } 243 | 244 | float StreamLoader::GetCurrentSpeedKByte() { 245 | return speed_sampler_.LastSecondKBps(); 246 | } 247 | -------------------------------------------------------------------------------- /src/stream_loader.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_STREAM_LOADER_HPP 6 | #define BONDRIVER_EPGSTATION_STREAM_LOADER_HPP 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include "blocking_buffer.hpp" 20 | #include "config.hpp" 21 | #include "speed_sampler.hpp" 22 | 23 | class StreamLoader { 24 | public: 25 | enum class WaitResult { 26 | kResultOK, 27 | kResultFailed, 28 | kWaitTimeout, 29 | kWaitFailed 30 | }; 31 | public: 32 | StreamLoader(size_t chunk_size, size_t max_chunk_count, size_t min_chunk_count); 33 | ~StreamLoader(); 34 | bool Open(const std::string& base_url, 35 | const std::string& path_query, 36 | std::optional basic_auth = std::nullopt, 37 | std::optional user_agent = std::nullopt, 38 | std::optional proxy = std::nullopt, 39 | std::optional> headers = std::nullopt); 40 | void Abort(); 41 | WaitResult WaitForResponse(std::chrono::milliseconds timeout); 42 | WaitResult WaitForData(); 43 | size_t Read(uint8_t* buffer, size_t expected_bytes); 44 | std::pair ReadChunkAndRetain(); 45 | size_t RemainReadable(); 46 | bool IsPolling(); 47 | float GetCurrentSpeedKByte(); 48 | private: 49 | static curl_socket_t OnOpenSocketCallback(StreamLoader* self, curlsocktype purpose, curl_sockaddr* addr); 50 | private: 51 | void ForceShutdown(); 52 | private: 53 | bool OnHeaderCallback(std::string data); 54 | bool OnWriteCallback(std::string data); 55 | private: 56 | size_t chunk_size_; 57 | BlockingBuffer blocking_buffer_; 58 | 59 | bool has_requested_ = false; 60 | bool has_response_received_ = false; 61 | bool has_reached_eof_ = false; 62 | bool request_failed_ = false; 63 | std::atomic has_requested_abort_ = false; 64 | 65 | SpeedSampler speed_sampler_; 66 | 67 | cpr::Session session_; 68 | SOCKET socket_ = INVALID_SOCKET; 69 | std::future async_response_; 70 | 71 | std::mutex response_mutex_; 72 | std::condition_variable response_cv_; 73 | }; 74 | 75 | 76 | #endif // BONDRIVER_EPGSTATION_STREAM_LOADER_HPP 77 | -------------------------------------------------------------------------------- /src/string_utils.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #include 6 | #include 7 | #include "string_utils.hpp" 8 | 9 | namespace StringUtils { 10 | 11 | std::wstring UTF8ToWideString(const char* input) { 12 | int length = MultiByteToWideChar(CP_UTF8, 0, input, -1, nullptr, 0); 13 | std::wstring result; 14 | if (length > 0) { 15 | result.resize(length); 16 | MultiByteToWideChar(CP_UTF8, 0, input, -1, const_cast(result.c_str()), length); 17 | } 18 | return result; 19 | } 20 | 21 | std::wstring UTF8ToWideString(const std::string &input) { 22 | return UTF8ToWideString(input.c_str()); 23 | } 24 | 25 | std::string WideStringToUTF8(const wchar_t* input) { 26 | int src_length = static_cast(wcslen(input)); 27 | int length = WideCharToMultiByte(CP_UTF8, 0, input, src_length, nullptr, 0, nullptr, nullptr); 28 | 29 | std::string result; 30 | if (length > 0) { 31 | result.resize(static_cast(length + 1)); 32 | WideCharToMultiByte(CP_UTF8, 0, input, src_length, &result[0], length, nullptr, nullptr); 33 | result[length] = '\0'; 34 | } 35 | return result; 36 | } 37 | 38 | std::string WideStringToUTF8(const std::wstring &input) { 39 | return WideStringToUTF8(input.c_str()); 40 | } 41 | 42 | std::string RemoveSuffixSlash(const std::string& input) { 43 | if (*--input.end() == '/') { 44 | return std::string(input.begin(), --input.end()); 45 | } else { 46 | return input; 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/string_utils.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #ifndef BONDRIVER_EPGSTATION_STRING_UTILS_HPP 6 | #define BONDRIVER_EPGSTATION_STRING_UTILS_HPP 7 | 8 | #include 9 | 10 | #if defined(_WIN32) && defined(_UNICODE) 11 | using PlatformString = std::basic_string, std::allocator>; 12 | #else 13 | using PlatformString = std::basic_string, std::allocator>; 14 | #endif 15 | 16 | #if defined(_WIN32) && defined(_UNICODE) 17 | #define UTF8ToPlatformString(a) StringUtils::UTF8ToWideString(a) 18 | #define PlatformStringToUTF8(a) StringUtils::WideStringToUTF8(a) 19 | #else 20 | #define UTF8ToPlatformString(a) a 21 | #define PlatformStringToUTF8(a) a 22 | #endif 23 | 24 | namespace StringUtils { 25 | 26 | std::wstring UTF8ToWideString(const char* input); 27 | std::wstring UTF8ToWideString(const std::string& input); 28 | std::string WideStringToUTF8(const wchar_t* input); 29 | std::string WideStringToUTF8(const std::wstring& input); 30 | 31 | std::string RemoveSuffixSlash(const std::string& input); 32 | 33 | } 34 | 35 | #endif // BONDRIVER_EPGSTATION_STRING_UTILS_HPP 36 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | set(CMAKE_CXX_STANDARD 17) 4 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 5 | 6 | add_executable(BonDriver_EPGStation_test 7 | EXCLUDE_FROM_ALL 8 | main.cpp 9 | ) 10 | 11 | set_target_properties(BonDriver_EPGStation_test 12 | PROPERTIES 13 | RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} 14 | ) 15 | 16 | # Platform-specific compiler macro / flags 17 | if(MSVC) 18 | # For MSVC, disable CRT secure warnings 19 | target_compile_definitions(BonDriver_EPGStation_test 20 | PRIVATE 21 | _CRT_SECURE_NO_WARNINGS=1 22 | ) 23 | elseif(MINGW) 24 | # For MinGW, static linking runtime library 25 | target_compile_options(BonDriver_EPGStation_test 26 | PRIVATE 27 | -static-libgcc -static-libstdc++ -Wl,-Bstatic,--whole-archive -lwinpthread -Wl,--no-whole-archive -Wl,-Bdynamic 28 | ) 29 | endif() 30 | 31 | if(WIN32) 32 | target_compile_definitions(BonDriver_EPGStation_test 33 | PRIVATE 34 | UNICODE 35 | _UNICODE 36 | ) 37 | endif() 38 | 39 | target_include_directories(BonDriver_EPGStation_test 40 | PRIVATE 41 | ../include 42 | ) 43 | 44 | target_link_libraries(BonDriver_EPGStation_test 45 | PRIVATE 46 | BonDriver_EPGStation 47 | ) 48 | -------------------------------------------------------------------------------- /test/main.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // @author magicxqq 3 | // 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "IBonDriver.h" 11 | #include "IBonDriver2.h" 12 | 13 | int main(int argc, char** argv) { 14 | setlocale(LC_ALL, "japanese"); 15 | printf("IsDebuggerPresent(): %d\n", IsDebuggerPresent()); 16 | 17 | auto bon = dynamic_cast(CreateBonDriver()); 18 | bon->OpenTuner(); 19 | 20 | printf("Tuner: %ls\n", bon->GetTunerName()); 21 | 22 | DWORD space = 0; 23 | DWORD channel = 0; 24 | 25 | while (true) { 26 | const wchar_t* space_name = bon->EnumTuningSpace(space++); 27 | if (space_name == nullptr) { 28 | break; 29 | } 30 | printf("%ls\n", space_name); 31 | fflush(stdout); 32 | } 33 | 34 | space = 0; 35 | 36 | while (true) { 37 | const wchar_t* space_name = bon->EnumTuningSpace(space); 38 | if (space_name == nullptr) { 39 | break; 40 | } 41 | 42 | const wchar_t* channel_name = bon->EnumChannelName(space, channel); 43 | if (channel_name == nullptr) { 44 | space++; 45 | channel = 0; 46 | continue; 47 | } else { 48 | channel++; 49 | } 50 | 51 | printf("%ls: %ls \n", space_name, channel_name); 52 | fflush(stdout); 53 | } 54 | 55 | system("pause"); 56 | 57 | bon->SetChannel(0, 0); 58 | bon->WaitTsStream(); 59 | 60 | uint8_t* stream_data = nullptr; 61 | DWORD stream_size = 0; 62 | DWORD stream_remain = 0; 63 | 64 | for (int i = 0; i < 100; i++) { 65 | BOOL ret = bon->GetTsStream(&stream_data, &stream_size, &stream_remain); 66 | float signal_level = bon->GetSignalLevel(); 67 | printf("stream_size: %u, stream_remain: %u, signal_level: %lf\n", stream_size, stream_remain, signal_level); 68 | 69 | if (!stream_remain) { 70 | Sleep(10); 71 | } 72 | } 73 | 74 | bon->CloseTuner(); 75 | bon->Release(); 76 | 77 | fflush(stdout); 78 | return 0; 79 | } 80 | --------------------------------------------------------------------------------