├── .gitignore ├── experiments ├── wemeet-x11-behaviour │ ├── .gitignore │ ├── CMakeLists.txt │ ├── 1_basic_hook.md │ ├── 1_basic_hook.cpp │ ├── run_hook.py │ ├── original_funcs.hpp │ ├── 2_xdamage_hook.md │ └── 2_xdamage_hook.cpp ├── krfb-virualmon-calling │ ├── CMakeLists.txt │ └── main.cpp └── wemeet-qscreen-behaviour │ ├── CMakeLists.txt │ ├── qscreen_hook.cpp │ └── run_hook.py ├── .gitmodules ├── resource ├── instruction-1.png ├── instruction-2.png ├── instruction-3.png ├── supported_DEs.png ├── instruction-3-new.png ├── instruction-figures.dps └── diagram.drawio ├── tests ├── CMakeLists.txt ├── test_main.cpp └── framebuf_test.cpp ├── helpers.hpp ├── LICENSE ├── framebuf.hpp ├── hook.hpp ├── interface.hpp ├── CMakeLists.txt ├── hook_opencv.hpp ├── format.hpp ├── README.md ├── hook.cpp ├── payload.cpp └── payload.hpp /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | .vscode/ 3 | build/ -------------------------------------------------------------------------------- /experiments/wemeet-x11-behaviour/.gitignore: -------------------------------------------------------------------------------- 1 | .cache/ 2 | build/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "stb"] 2 | path = stb 3 | url = https://github.com/nothings/stb.git 4 | -------------------------------------------------------------------------------- /resource/instruction-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuwd1/wemeet-wayland-screenshare/HEAD/resource/instruction-1.png -------------------------------------------------------------------------------- /resource/instruction-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuwd1/wemeet-wayland-screenshare/HEAD/resource/instruction-2.png -------------------------------------------------------------------------------- /resource/instruction-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuwd1/wemeet-wayland-screenshare/HEAD/resource/instruction-3.png -------------------------------------------------------------------------------- /resource/supported_DEs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuwd1/wemeet-wayland-screenshare/HEAD/resource/supported_DEs.png -------------------------------------------------------------------------------- /resource/instruction-3-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuwd1/wemeet-wayland-screenshare/HEAD/resource/instruction-3-new.png -------------------------------------------------------------------------------- /resource/instruction-figures.dps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xuwd1/wemeet-wayland-screenshare/HEAD/resource/instruction-figures.dps -------------------------------------------------------------------------------- /experiments/krfb-virualmon-calling/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.26) 2 | 3 | project(krfb-virualmon-calling LANGUAGES C CXX) 4 | 5 | 6 | add_executable(main main.cpp) -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | function(make_exe FILENAME) 2 | # target name is filename minus ".cpp" 3 | string(REGEX REPLACE ".cpp" "" TARGET_NAME ${FILENAME}) 4 | add_executable(${TARGET_NAME} ${FILENAME}) 5 | add_compile_options_for(${TARGET_NAME}) 6 | endfunction() 7 | 8 | make_exe(framebuf_test.cpp) 9 | 10 | make_exe(test_main.cpp) 11 | target_link_libraries(test_main PRIVATE hook) -------------------------------------------------------------------------------- /experiments/wemeet-qscreen-behaviour/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.26) 2 | project(wemeet-qscreen-behaviour LANGUAGES C CXX) 3 | 4 | find_package(Qt5 REQUIRED COMPONENTS Gui) 5 | 6 | add_library(qscreen_hook SHARED qscreen_hook.cpp) 7 | set_target_properties(qscreen_hook PROPERTIES POSITION_INDEPENDENT_CODE ON) 8 | target_link_libraries(qscreen_hook PRIVATE Qt5::Gui) 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/test_main.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | 5 | 6 | void XShmAttachHook(); 7 | void XShmDetachHook(); 8 | 9 | int main(){ 10 | XShmAttachHook(); 11 | std::this_thread::sleep_for(std::chrono::seconds(5)); 12 | XShmDetachHook(); 13 | 14 | std::this_thread::sleep_for(std::chrono::seconds(3)); 15 | 16 | XShmAttachHook(); 17 | std::this_thread::sleep_for(std::chrono::seconds(5)); 18 | XShmDetachHook(); 19 | } -------------------------------------------------------------------------------- /experiments/wemeet-x11-behaviour/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.23) 2 | 3 | project(wemeet-x11-behaviour LANGUAGES C CXX) 4 | 5 | find_package(X11 REQUIRED) 6 | 7 | function(make_hooklib FILENAME) 8 | # TARGET_NAME is FILENAME without extension 9 | get_filename_component(TARGET_NAME ${FILENAME} NAME_WE) 10 | add_library(${TARGET_NAME} SHARED ${FILENAME}) 11 | set_target_properties(${TARGET_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) 12 | target_link_libraries(${TARGET_NAME} PRIVATE X11::X11) 13 | endfunction() 14 | 15 | make_hooklib(1_basic_hook.cpp) 16 | make_hooklib(2_xdamage_hook.cpp) -------------------------------------------------------------------------------- /tests/framebuf_test.cpp: -------------------------------------------------------------------------------- 1 | #include "format.hpp" 2 | #include "framebuf.hpp" 3 | 4 | 5 | int main(){ 6 | auto framebuf = FrameBuffer{1080, 1920, SpaVideoFormat_e::RGBx}; 7 | 8 | printf("framebuf data_size: %ld\n", framebuf.data_size); 9 | printf("framebuf height: %d\n", framebuf.height); 10 | printf("framebuf width: %d\n", framebuf.width); 11 | printf("framebuf row_byte_stride: %d\n", framebuf.row_byte_stride); 12 | printf("framebuf format: %d\n", static_cast(framebuf.format)); 13 | 14 | // framebuf.blocking_imshow(); 15 | 16 | framebuf.update_param(1440, 2560, SpaVideoFormat_e::BGR); 17 | 18 | printf("framebuf data_size: %ld\n", framebuf.data_size); 19 | printf("framebuf height: %d\n", framebuf.height); 20 | printf("framebuf width: %d\n", framebuf.width); 21 | printf("framebuf row_byte_stride: %d\n", framebuf.row_byte_stride); 22 | printf("framebuf format: %d\n", static_cast(framebuf.format)); 23 | 24 | 25 | // framebuf.blocking_imshow(); 26 | 27 | } -------------------------------------------------------------------------------- /helpers.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | #include 5 | #include 6 | 7 | // Inline function to colorize text 8 | inline std::string color_text(const std::string& text, const std::string& color_code) { 9 | return "\033[" + color_code + "m" + text + "\033[0m"; 10 | } 11 | 12 | // Helper functions for specific colors 13 | inline std::string red_text(const std::string& text) { 14 | return color_text(text, "31"); 15 | } 16 | 17 | inline std::string green_text(const std::string& text) { 18 | return color_text(text, "32"); 19 | } 20 | 21 | inline std::string yellow_text(const std::string& text) { 22 | return color_text(text, "33"); 23 | } 24 | 25 | inline std::string int_to_hexstr(int value) { 26 | std::stringstream ss; 27 | ss << std::hex << value; 28 | return ss.str(); 29 | } 30 | 31 | inline std::string toLowerString(const std::string& str) { 32 | std::string lower_str = str; 33 | for (char& c : lower_str) { 34 | c = std::tolower(c); 35 | } 36 | return lower_str; 37 | } -------------------------------------------------------------------------------- /experiments/krfb-virualmon-calling/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | 7 | void call_with_std_system() 8 | { 9 | std::system( 10 | "krfb-virtualmonitor --resolution 1920x1080 --name vv --password 123 --port 12345" 11 | ); 12 | } 13 | 14 | void call_with_execvp(){ 15 | char* args[] = { 16 | (char*) "krfb-virtualmonitor", 17 | (char*) "--resolution", 18 | (char*) "1920x1080", 19 | (char*) "--name", 20 | (char*) "wemeetapp-virtualmonitor", 21 | (char*) "--password", 22 | (char*) "123456", 23 | (char*) "--port", 24 | (char*) "54321", 25 | NULL // this NULL is a must. dang it. 26 | }; 27 | int errcode = execvp( 28 | args[0], 29 | args 30 | ); 31 | 32 | printf("control reached a wrong place\n"); 33 | printf("errcode is %d\n", errcode); 34 | 35 | // get the error code 36 | printf("errno is %d\n", errno); 37 | 38 | 39 | 40 | // control should never reach here 41 | throw std::runtime_error("execvp failed"); 42 | } 43 | 44 | int main(){ 45 | 46 | 47 | call_with_execvp(); 48 | 49 | 50 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 David Xu 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 | -------------------------------------------------------------------------------- /experiments/wemeet-x11-behaviour/1_basic_hook.md: -------------------------------------------------------------------------------- 1 | # 1. 基本的hook 2 | 3 | 4 | 将`lib1_basic_hook.so`挂到wemeetapp后,开始一个快速会议,并开启屏幕共享,我们会在stderr看到如下信息: 5 | 6 | ```bash 7 | 8 | wemeetapp called XShmAttach(0x714af0003800, 0x714af0022d20) 9 | wemeetapp called XShmGetImage(0x714af0003800, 1089, 0x714af00136d0, 0, 0, 18446744073709551615) 10 | wemeetapp called XShmDetach(0x714af0003800, 0x714af0022d20) 11 | 12 | ``` 13 | 14 | 可以看到,按照我们预期地,wemeetapp绕过操作系统上层接口,非常hacky地直接和X11 server通信,而这也正是它不可能在wayland上正常运作的原因。 15 | 16 | > do you think mofo tencent's ever gonna fix this? Don't be stupid my son. 17 | 18 | 对于上述log,更为具体地: 19 | 20 | 1. 我们会注意到,在我们点击屏幕共享按钮,并随后选择了一个屏幕进行共享**后的一瞬间**,`XShmAttach`函数被调用了 21 | 22 | 2. 我们同时也会看到,`XShmGetImage`紧接着被调用了一次,但随后我们的stderr就没有更新的消息被打印出来了 23 | 24 | 3. 直到我们点击结束共享,`XShmDetach`才被调用 25 | 26 | 这里的主要问题是,为什么`XShmGetImage`只被调用了一次?在录屏时,难道wemeetapp不需要反复获取屏幕内容吗?问题的答案是: 27 | 28 | - wemeetapp使用了XDamage拓展. 腾讯的开发者编写的逻辑是,利用XDamage,只抓取被录制的屏幕中被更新的区域. 这不得不说是一个很好的设计. 29 | - 然而,在Xwayland环境下这套逻辑并没法work. wemeetapp在Xwayland上从XDamage中获取到的信息永远都在告诉他没有任何屏幕内容存在更新. 也因此,wemeetapp从头至尾只调用了一次`XShmGetImage`,用来获取那最初的一帧图像. 30 | - 因此,我们当前版本的hook就是直接把XDamage的逻辑破坏掉,强迫wemeetapp去反复获取完整的帧图像. 31 | -------------------------------------------------------------------------------- /experiments/wemeet-x11-behaviour/1_basic_hook.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "original_funcs.hpp" 3 | 4 | 5 | extern "C" 6 | { 7 | 8 | // we first hookup XShmAttach 9 | // this is expected to be called when you start screencast 10 | Bool XShmAttach(Display* dpy, XShmSegmentInfo* shminfo) 11 | { 12 | fprintf(stderr, "wemeetapp called XShmAttach(%p, %p)\n", dpy, shminfo); 13 | Bool ret = XShmAttachFunc(dpy, shminfo); 14 | return ret; 15 | } 16 | 17 | // then we hookup XShmDetach 18 | // this is expected to be called when you stop screencast 19 | Bool XShmDetach(Display* dpy, XShmSegmentInfo* shminfo) 20 | { 21 | fprintf(stderr, "wemeetapp called XShmDetach(%p, %p)\n", dpy, shminfo); 22 | Bool ret = XShmDetachFunc(dpy, shminfo); 23 | return ret; 24 | } 25 | 26 | // and finally the main function XShmGetImage 27 | // this is expected to be called when wemeetapp want to get the image 28 | Bool XShmGetImage(Display* dpy, Drawable d, XImage* image, int x, int y, unsigned long plane_mask) 29 | { 30 | fprintf(stderr, "wemeetapp called XShmGetImage(%p, %lu, %p, %d, %d, %lu)\n", dpy, d, image, x, y, plane_mask); 31 | Bool ret = XShmGetImageFunc(dpy, d, image, x, y, plane_mask); 32 | return ret; 33 | } 34 | 35 | 36 | } -------------------------------------------------------------------------------- /experiments/wemeet-qscreen-behaviour/qscreen_hook.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | auto dl_handle = dlopen("libQt5Gui.so", RTLD_LAZY); 10 | typedef QPixmap (*GrabWindowFunc)(QScreen* self, WId window, int x, int y, int w, int h); 11 | GrabWindowFunc originalFunction = (GrabWindowFunc)dlsym(dl_handle, "_ZN7QPixmap10grabWindowEyiiii"); 12 | 13 | QPixmap QScreen::grabWindow(WId window, int x, int y, int w, int h) 14 | { 15 | fprintf(stderr, "This pointer: %p\n", this); 16 | fprintf(stderr, "QScreen::grabWindow hooked\n"); 17 | fprintf(stderr, "window: %llu, x: %d, y: %d, w: %d, h: %d\n", window, x, y, w, h); 18 | fprintf(stderr, "original function: %p\n", originalFunction); 19 | 20 | 21 | QScreen* screen = this; 22 | printf("Screen: %p\n", screen); 23 | printf("name: %s\n", screen->name().toStdString().c_str()); 24 | printf("manufacturer: %s\n", screen->manufacturer().toStdString().c_str()); 25 | printf("model: %s\n", screen->model().toStdString().c_str()); 26 | printf("serialNumber: %s\n", screen->serialNumber().toStdString().c_str()); 27 | printf("depth: %d\n", screen->depth()); 28 | printf("size: %d x %d\n", screen->size().width(), screen->size().height()); 29 | printf("geometry: %d x %d\n", screen->geometry().width(), screen->geometry().height()); 30 | 31 | 32 | return originalFunction(this, window, x, y, w, h); 33 | } -------------------------------------------------------------------------------- /framebuf.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "format.hpp" 8 | 9 | constexpr uint32_t DEFAULT_FB_ALLOC_HEIGHT = 8192; 10 | constexpr uint32_t DEFAULT_FB_ALLOC_WIDTH = 8192; 11 | 12 | 13 | struct FrameBuffer { 14 | 15 | FrameBuffer() = delete; 16 | 17 | inline FrameBuffer( 18 | uint32_t allocation_height, 19 | uint32_t allocation_width, 20 | uint32_t init_height, 21 | uint32_t init_width, 22 | SpaVideoFormat_e const& format 23 | ){ 24 | data_size = allocation_height * allocation_width * 4; 25 | data.reset(new uint8_t[data_size]); 26 | update_param(init_height, init_width, format); 27 | } 28 | 29 | inline void update_param( 30 | uint32_t height, 31 | uint32_t width, 32 | SpaVideoFormat_e const& format 33 | ){ 34 | 35 | int bytes_per_pixel = spa_videoformat_bytesize(format); 36 | if (bytes_per_pixel == -1) { 37 | throw std::runtime_error("Invalid format"); 38 | } 39 | 40 | // always store in (height, width):(stride, 1) layout 41 | uint32_t needed_stride = (width * bytes_per_pixel + 4 - 1) / 4 * 4; 42 | this->height = height; 43 | this->width = width; 44 | this->row_byte_stride = needed_stride; 45 | this->format = format; 46 | } 47 | 48 | std::unique_ptr data{nullptr}; 49 | size_t data_size{0}; 50 | uint32_t height{0}; 51 | uint32_t width{0}; 52 | uint32_t row_byte_stride{0}; 53 | SpaVideoFormat_e format{SpaVideoFormat_e::INVALID}; 54 | 55 | int crop_x{0}, crop_y{0}, crop_width{0}, crop_height{0}; 56 | int rotate{0}; 57 | int flip{0}; 58 | 59 | }; 60 | -------------------------------------------------------------------------------- /experiments/wemeet-x11-behaviour/run_hook.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | 3 | import os 4 | import subprocess 5 | import signal 6 | import pathlib 7 | import sys 8 | import time 9 | import argparse 10 | 11 | wemeetapp_pkill_name = "wemeetapp" 12 | safety_kill_time = 30 13 | 14 | 15 | def kill_wemeetapp(): 16 | os.system(f"pkill -f {wemeetapp_pkill_name}") 17 | 18 | def signal_handler(sig, frame): 19 | print('SIGINT received, now kill wemeetapp and exit') 20 | kill_wemeetapp() 21 | sys.exit(0) 22 | 23 | signal.signal(signal.SIGINT, signal_handler) 24 | 25 | 26 | def run_hooked_wemeetapp( 27 | hook_so_path: pathlib.Path, 28 | wemeet_startup_script_name = "wemeet-x11", 29 | 30 | ): 31 | hook_so_path = hook_so_path.absolute() 32 | _result = subprocess.run( 33 | ["which", wemeet_startup_script_name], 34 | capture_output=True, text=True 35 | ) 36 | wemeet_startup_script_full_path = pathlib.Path(_result.stdout.strip()).absolute() 37 | 38 | env = os.environ.copy() 39 | env["LD_PRELOAD"] = str(hook_so_path) 40 | p = subprocess.Popen( 41 | [wemeet_startup_script_full_path], 42 | env=env 43 | ) 44 | 45 | # wait for 20 secs 46 | time.sleep(safety_kill_time) 47 | 48 | print("safety time is up, now kill wemeetapp and exit") 49 | 50 | p.terminate() 51 | kill_wemeetapp() 52 | 53 | 54 | argparser = argparse.ArgumentParser() 55 | argparser.add_argument("hook_so_path", type=pathlib.Path) 56 | args = argparser.parse_args() 57 | 58 | hook_so_path = pathlib.Path(args.hook_so_path) 59 | assert hook_so_path.exists(), f"hook_so_path does not exist: {hook_so_path}" 60 | 61 | run_hooked_wemeetapp(hook_so_path) 62 | -------------------------------------------------------------------------------- /experiments/wemeet-qscreen-behaviour/run_hook.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python3 2 | 3 | import os 4 | import subprocess 5 | import signal 6 | import pathlib 7 | import sys 8 | import time 9 | import argparse 10 | 11 | wemeetapp_pkill_name = "wemeetapp" 12 | safety_kill_time = 30 13 | 14 | 15 | def kill_wemeetapp(): 16 | os.system(f"pkill -f {wemeetapp_pkill_name}") 17 | 18 | def signal_handler(sig, frame): 19 | print('SIGINT received, now kill wemeetapp and exit') 20 | kill_wemeetapp() 21 | sys.exit(0) 22 | 23 | signal.signal(signal.SIGINT, signal_handler) 24 | 25 | 26 | def run_hooked_wemeetapp( 27 | hook_so_path: pathlib.Path, 28 | wemeet_startup_script_name = "wemeet-x11", 29 | 30 | ): 31 | hook_so_path = hook_so_path.absolute() 32 | _result = subprocess.run( 33 | ["which", wemeet_startup_script_name], 34 | capture_output=True, text=True 35 | ) 36 | wemeet_startup_script_full_path = pathlib.Path(_result.stdout.strip()).absolute() 37 | 38 | env = os.environ.copy() 39 | env["LD_PRELOAD"] = str(hook_so_path) 40 | p = subprocess.Popen( 41 | [wemeet_startup_script_full_path], 42 | env=env 43 | ) 44 | 45 | # wait for 20 secs 46 | time.sleep(safety_kill_time) 47 | 48 | print("safety time is up, now kill wemeetapp and exit") 49 | 50 | p.terminate() 51 | kill_wemeetapp() 52 | 53 | 54 | argparser = argparse.ArgumentParser() 55 | argparser.add_argument("hook_so_path", type=pathlib.Path) 56 | args = argparser.parse_args() 57 | 58 | hook_so_path = pathlib.Path(args.hook_so_path) 59 | assert hook_so_path.exists(), f"hook_so_path does not exist: {hook_so_path}" 60 | 61 | run_hooked_wemeetapp(hook_so_path) 62 | -------------------------------------------------------------------------------- /hook.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | template 15 | struct OriginalFunction{ 16 | 17 | OriginalFunction(std::string const& funcName, std::string const& libN){ 18 | dl_handle = dlopen(libN.c_str(), RTLD_LAZY); 19 | if(dl_handle == nullptr){ 20 | fprintf(stderr, "Failed to open library %s\n", libN.c_str()); 21 | return; 22 | } 23 | func = (FType*)dlsym(dl_handle, funcName.c_str()); 24 | if(func == nullptr){ 25 | fprintf(stderr, "Failed to find function %s\n", funcName.c_str()); 26 | return; 27 | } 28 | } 29 | 30 | ~OriginalFunction(){ 31 | if(dl_handle != nullptr){ 32 | dlclose(dl_handle); 33 | } 34 | } 35 | 36 | template 37 | auto operator()(Args... args){ 38 | return func(args...); 39 | } 40 | 41 | std::function func; 42 | void* dl_handle; 43 | 44 | }; // struct OriginalFunction 45 | 46 | using XShmAttachType = Bool(Display*, XShmSegmentInfo*); 47 | inline auto XShmAttachFunc = OriginalFunction{"XShmAttach", "libXext.so"}; 48 | 49 | using XShmDetachType = Bool(Display*, XShmSegmentInfo*); 50 | inline auto XShmDetachFunc = OriginalFunction{"XShmDetach", "libXext.so"}; 51 | 52 | using XShmGetImageType = Bool(Display*, Drawable, XImage*, int, int, unsigned long); 53 | inline auto XShmGetImageFunc = OriginalFunction{"XShmGetImage", "libXext.so"}; 54 | 55 | using XDamageQueryExtensionType = Bool(Display*, int*, int*); 56 | inline auto XDamageQueryExtensionFunc = OriginalFunction{"XDamageQueryExtension", "libXdamage.so"}; -------------------------------------------------------------------------------- /interface.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "format.hpp" 6 | #include "framebuf.hpp" 7 | 8 | 9 | 10 | struct Interface; 11 | struct XdpScreencastPortal; 12 | struct PipewireScreenCast; 13 | 14 | struct InterfaceSingleton{ 15 | 16 | using THIS_CLASS = InterfaceSingleton; 17 | 18 | 19 | inline static InterfaceSingleton& getSingleton(){ 20 | static InterfaceSingleton singleton; 21 | return singleton; 22 | } 23 | 24 | // the pointer to the actual interface 25 | // should be allocated, and freed by the hook 26 | std::atomic interface_handle{nullptr}; 27 | 28 | // the pointer to the screencast portal object 29 | // should be allocated, and freed by the hook 30 | std::atomic portal_handle{nullptr}; 31 | 32 | // the pointer to the pipewire screencast object 33 | // should be allocated, and freed by the hook 34 | std::atomic pipewire_handle{nullptr}; 35 | 36 | 37 | private: 38 | 39 | InterfaceSingleton() = default; 40 | 41 | 42 | }; 43 | 44 | struct Interface { 45 | 46 | Interface() = delete; 47 | Interface( 48 | uint32_t fb_allocation_height, 49 | uint32_t fb_allocation_width, 50 | uint32_t init_frame_height, 51 | uint32_t init_frame_width, 52 | SpaVideoFormat_e const& init_frame_format 53 | ):framebuf(fb_allocation_height, fb_allocation_width, init_frame_height, init_frame_width, init_frame_format), 54 | pw_stop_flag(false), 55 | payload_pw_stop_confirm(false), 56 | payload_gio_stop_confirm(false), 57 | x11_sanitizer_stop_flag(false) 58 | {} 59 | 60 | // atomic flag for the hook to stop the payload 61 | // should only be written by the hook, and read by the payload 62 | std::atomic pw_stop_flag; 63 | 64 | // payload pw stop confirmation flag 65 | // must be set by payload main thread 66 | std::atomic payload_pw_stop_confirm; 67 | 68 | // payload gio stop confirmation flag 69 | // must be set by payload main thread 70 | std::atomic payload_gio_stop_confirm; 71 | 72 | // payload x11 redirect sanitizer stop flag 73 | // managed by the payload, the hook does not need to care 74 | std::atomic x11_sanitizer_stop_flag; 75 | 76 | // the framebuffer between the hook and the payload 77 | FrameBuffer framebuf; 78 | }; -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.23) 2 | 3 | project(wemeet-wayland-screencast LANGUAGES C CXX) 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | # Find PkgConfig 8 | find_package(PkgConfig REQUIRED) 9 | 10 | # Find the gio-2.0 package 11 | pkg_check_modules(GIO REQUIRED gio-2.0) 12 | # libportal 13 | pkg_check_modules(LIBPORTAL REQUIRED libportal) 14 | # pipewire 15 | pkg_check_modules(PIPEWIRE REQUIRED libpipewire-0.3) 16 | 17 | 18 | find_package(X11 REQUIRED) 19 | 20 | find_package(OpenCV QUIET) 21 | if(NOT OpenCV_FOUND) 22 | set(OpenCV_NOTFOUND_ERROR_MESSAGE "CMake did not find a package provided by \"OpenCV\". Please install develepment package for OpenCV. Split packages may suffice if you REALLY know what that means. Otherwise meta package is recommended.") 23 | 24 | # We only need libopencv_core.so, libopencv_imgproc.so and relevant headers 25 | find_library(LIB_OPENCV_CORE NAMES opencv_core) 26 | if(NOT LIB_OPENCV_CORE) 27 | message(FATAL_ERROR "${OpenCV_NOTFOUND_ERROR_MESSAGE}") 28 | endif(NOT LIB_OPENCV_CORE) 29 | find_library(LIB_OPENCV_IMGPROC NAMES opencv_imgproc) 30 | if(NOT LIB_OPENCV_IMGPROC) 31 | message(FATAL_ERROR "${OpenCV_NOTFOUND_ERROR_MESSAGE}") 32 | endif(NOT LIB_OPENCV_IMGPROC) 33 | find_path(OpenCV_INCLUDE_DIRS NAMES opencv2/core/core_c.h PATH_SUFFIXES opencv4) 34 | if(NOT OpenCV_INCLUDE_DIRS) 35 | message(FATAL_ERROR "${OpenCV_NOTFOUND_ERROR_MESSAGE}") 36 | endif(NOT OpenCV_INCLUDE_DIRS) 37 | 38 | message(STATUS "CMake did not find a package provided by \"OpenCV\"") 39 | message(STATUS "Detect OpenCV_INCLUDE_DIRS = ${OpenCV_INCLUDE_DIRS}") 40 | endif(NOT OpenCV_FOUND) 41 | 42 | 43 | function(add_compile_options_for TARGET_NAME) 44 | 45 | 46 | target_link_libraries(${TARGET_NAME} PRIVATE ${GIO_LIBRARIES}) 47 | target_include_directories(${TARGET_NAME} PRIVATE ${GIO_INCLUDE_DIRS}) 48 | 49 | target_link_libraries(${TARGET_NAME} PRIVATE ${LIBPORTAL_LIBRARIES}) 50 | target_include_directories(${TARGET_NAME} PRIVATE ${LIBPORTAL_INCLUDE_DIRS}) 51 | 52 | target_link_libraries(${TARGET_NAME} PRIVATE ${PIPEWIRE_LIBRARIES}) 53 | target_include_directories(${TARGET_NAME} PRIVATE ${PIPEWIRE_INCLUDE_DIRS}) 54 | target_compile_options(${TARGET_NAME} PRIVATE ${PIPEWIRE_CFLAGS_OTHER}) 55 | 56 | # include current directory 57 | target_include_directories(${TARGET_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) 58 | 59 | # opencv 60 | target_include_directories(${TARGET_NAME} PRIVATE ${OpenCV_INCLUDE_DIRS}) 61 | 62 | target_link_libraries(${TARGET_NAME} PRIVATE X11::X11) 63 | target_link_libraries(${TARGET_NAME} PRIVATE X11::Xrandr) 64 | 65 | endfunction() 66 | 67 | # wayland screencast hook 68 | 69 | add_library(hook SHARED hook.cpp payload.cpp) 70 | set_property(TARGET hook PROPERTY POSITION_INDEPENDENT_CODE ON) 71 | add_compile_options_for(hook) 72 | install(TARGETS hook DESTINATION lib/wemeet/) 73 | -------------------------------------------------------------------------------- /experiments/wemeet-x11-behaviour/original_funcs.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | 16 | template 17 | struct OriginalFunction{ 18 | 19 | OriginalFunction(std::string const& funcName, std::string const& libN){ 20 | dl_handle = dlopen(libN.c_str(), RTLD_LAZY); 21 | if(dl_handle == nullptr){ 22 | fprintf(stderr, "Failed to open library %s\n", libN.c_str()); 23 | return; 24 | } 25 | func = (FType*)dlsym(dl_handle, funcName.c_str()); 26 | if(func == nullptr){ 27 | fprintf(stderr, "Failed to find function %s\n", funcName.c_str()); 28 | return; 29 | } 30 | } 31 | 32 | ~OriginalFunction(){ 33 | if(dl_handle != nullptr){ 34 | dlclose(dl_handle); 35 | } 36 | } 37 | 38 | template 39 | auto operator()(Args... args){ 40 | return func(args...); 41 | } 42 | 43 | std::function func; 44 | void* dl_handle; 45 | 46 | }; 47 | 48 | using XShmQueryVersionType = Bool(Display*, int*, int*, int*); 49 | inline auto XShmQueryVersionFunc = OriginalFunction{"XShmQueryVersion", "libXext.so"}; 50 | 51 | using XShmAttachType = Bool(Display*, XShmSegmentInfo*); 52 | inline auto XShmAttachFunc = OriginalFunction{"XShmAttach", "libXext.so"}; 53 | 54 | using XShmDetachType = Bool(Display*, XShmSegmentInfo*); 55 | inline auto XShmDetachFunc = OriginalFunction{"XShmDetach", "libXext.so"}; 56 | 57 | using XCreateGCType = GC(Display*, Drawable, unsigned long, XGCValues*); 58 | inline auto XCreateGCFunc = OriginalFunction{"XCreateGC", "libX11.so"}; 59 | 60 | using XFreeGCType = int(Display*, GC); 61 | inline auto XFreeGCFunc = OriginalFunction{"XFreeGC", "libX11.so"}; 62 | 63 | using XCompositeRedirectWindowType = void(Display*, Window, int); 64 | inline auto XCompositeRedirectWindowFunc = OriginalFunction{"XCompositeRedirectWindow", "libXcomposite.so"}; 65 | 66 | using XCompositeUnredirectWindowType = void(Display*, Window, int); 67 | inline auto XCompositeUnredirectWindowFunc = OriginalFunction{"XCompositeUnredirectWindow", "libXcomposite.so"}; 68 | 69 | using XShmGetImageType = Bool(Display*, Drawable, XImage*, int, int, unsigned long); 70 | inline auto XShmGetImageFunc = OriginalFunction{"XShmGetImage", "libXext.so"}; 71 | 72 | using XGetImageType = XImage*(Display*, Drawable, int, int, unsigned int, unsigned int, unsigned long, int); 73 | inline auto XGetImageFunc = OriginalFunction{"XGetImage", "libX11.so"}; 74 | 75 | using XDamageQueryExtensionType = Bool(Display*, int*, int*); 76 | inline auto XDamageQueryExtensionFunc = OriginalFunction{"XDamageQueryExtension", "libXdamage.so"}; 77 | -------------------------------------------------------------------------------- /experiments/wemeet-x11-behaviour/2_xdamage_hook.md: -------------------------------------------------------------------------------- 1 | # 2. xdamage欺骗hook 2 | 3 | 上一个例子中我们发现xdamage的正常运作会使得腾讯会议仅调用一次`XShmGetImage`. 因此,这个版本的hook在之前的基础上,劫持`XDamageQueryExtension`函数,欺骗腾讯会议`XDamge`拓展不可用. 4 | 5 | 将`lib2_xdamage_hook.so`挂在腾讯会议上,发起会议并开启屏幕共享,此时stderr上的log则变为如下: 6 | 7 | 8 | ```bash 9 | 10 | wemeetapp called XShmAttach(0x78b3c8003800, 0x78b3c8022d90) 11 | now we are printing the info about this xdisplay 12 | dpy->fd: 211 13 | dpy->proto_major_version: 11 14 | dpy->proto_minor_version: 0 15 | dpy->vendor: The X.Org Foundation 16 | dpy->qlen: 0 17 | dpy->last_request_read: 10 18 | dpy->request: 10 19 | dpy->max_request_size: 65535 20 | dpy->display_name: :0 21 | dpy->default_screen: 0 22 | dpy->nscreens: 1 23 | dpy->motion_buffer: 256 24 | dpy->min_keycode: 8 25 | dpy->max_keycode: 255 26 | dpy->xdefaults: Xcursor.size: 56 27 | Xcursor.theme: Notwaita-Black 28 | Xft.antialias: 1 29 | Xft.dpi: 168 30 | Xft.hinting: 1 31 | Xft.hintstyle: hintslight 32 | Xft.rgba: rgb 33 | 34 | dpy->screens: 0x78b3c8003120 35 | and now we are going to print some more info about shminfo 36 | shminfo->shmid: 40 37 | shminfo->shmaddr: 0x78b3a8438000 38 | shminfo->readOnly: 0 39 | XDamageQueryExtension(0x78b3c8003800, 0x78b3c8001088, 0x78b3c800108c) 40 | XDamageQueryExtension returned 1 41 | But we will return 0 42 | wemeetapp called XShmGetImage(0x78b3c8003800, 1089, 0x78b3c80136d0, 295, 2016, 18446744073709551615) 43 | image->width: 2880 44 | image->height: 1800 45 | image->xoffset: 0 46 | image->format: 2 # indicates ZPixmap 47 | image->data: 0x78b3a8438000 48 | image->byte_order: 0 49 | image->bitmap_unit: 32 50 | image->bitmap_bit_order: 0 51 | image->bitmap_pad: 32 52 | image->depth: 24 53 | image->bytes_per_line: 11520 54 | image->bits_per_pixel: 32 # 4 bytes per pixel, ZPixmap, thus RGBA or BGRA 55 | wemeetapp called XShmGetImage(0x78b3c8003800, 1089, 0x78b3c80136d0, 295, 2016, 18446744073709551615) 56 | wemeetapp called XShmGetImage(0x78b3c8003800, 1089, 0x78b3c80136d0, 295, 2016, 18446744073709551615) 57 | wemeetapp called XShmGetImage(0x78b3c8003800, 1089, 0x78b3c80136d0, 295, 2016, 18446744073709551615) 58 | wemeetapp called XShmGetImage(0x78b3c8003800, 1089, 0x78b3c80136d0, 295, 2016, 18446744073709551615) 59 | wemeetapp called XShmGetImage(0x78b3c8003800, 1089, 0x78b3c80136d0, 295, 2016, 18446744073709551615) 60 | wemeetapp called XShmGetImage(0x78b3c8003800, 1089, 0x78b3c80136d0, 295, 2016, 18446744073709551615) 61 | wemeetapp called XShmGetImage(0x78b3c8003800, 1089, 0x78b3c80136d0, 295, 2016, 18446744073709551615) 62 | wemeetapp called XShmGetImage(0x78b3c8003800, 1089, 0x78b3c80136d0, 295, 2016, 18446744073709551615) 63 | wemeetapp called XShmGetImage(0x78b3c8003800, 1089, 0x78b3c80136d0, 295, 2016, 18446744073709551615) 64 | wemeetapp called XShmGetImage(0x78b3c8003800, 1089, 0x78b3c80136d0, 295, 2016, 18446744073709551615) 65 | wemeetapp called XShmGetImage(0x78b3c8003800, 1089, 0x78b3c80136d0, 295, 2016, 18446744073709551615) 66 | wemeetapp called XShmGetImage(0x78b3c8003800, 1089, 0x78b3c80136d0, 295, 2016, 18446744073709551615) 67 | 68 | ... 69 | 70 | wemeetapp called XShmDetach(0x78b3c8003800, 0x78b3c8022d90) 71 | 72 | ``` 73 | 74 | 如上,可以看到如同我们的预期,此时腾讯会议会通过反复调用`XShmGetImage`来获取屏幕内容的每一帧. 我们最终的hook就是要利用这个机制,把正确的图像放入`XshmGetImage`返回的`XImage`结构体中,从而实现录屏. -------------------------------------------------------------------------------- /experiments/wemeet-x11-behaviour/2_xdamage_hook.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "original_funcs.hpp" 3 | 4 | #include "X11/Xlib.h" 5 | #include "X11/Xlibint.h" 6 | 7 | 8 | bool initial_shm_getimage_reported = false; 9 | 10 | extern "C" 11 | { 12 | 13 | // we first hookup XShmAttach 14 | // this is expected to be called when you start screencast 15 | Bool XShmAttach(Display* dpy, XShmSegmentInfo* shminfo) 16 | { 17 | fprintf(stderr, "wemeetapp called XShmAttach(%p, %p)\n", dpy, shminfo); 18 | fprintf(stderr, "now we are printing the info about this xdisplay\n"); 19 | fprintf(stderr, "dpy->fd: %d\n", dpy->fd); 20 | fprintf(stderr, "dpy->proto_major_version: %d\n", dpy->proto_major_version); 21 | fprintf(stderr, "dpy->proto_minor_version: %d\n", dpy->proto_minor_version); 22 | fprintf(stderr, "dpy->vendor: %s\n", dpy->vendor); 23 | fprintf(stderr, "dpy->qlen: %d\n", dpy->qlen); 24 | fprintf(stderr, "dpy->last_request_read: %lu\n", dpy->last_request_read); 25 | fprintf(stderr, "dpy->request: %lu\n", dpy->request); 26 | fprintf(stderr, "dpy->max_request_size: %u\n", dpy->max_request_size); 27 | fprintf(stderr, "dpy->display_name: %s\n", dpy->display_name); 28 | fprintf(stderr, "dpy->default_screen: %d\n", dpy->default_screen); 29 | fprintf(stderr, "dpy->nscreens: %d\n", dpy->nscreens); 30 | fprintf(stderr, "dpy->motion_buffer: %lu\n", dpy->motion_buffer); 31 | fprintf(stderr, "dpy->min_keycode: %d\n", dpy->min_keycode); 32 | fprintf(stderr, "dpy->max_keycode: %d\n", dpy->max_keycode); 33 | fprintf(stderr, "dpy->xdefaults: %s\n", dpy->xdefaults); 34 | fprintf(stderr, "dpy->screens: %p\n", dpy->screens); 35 | 36 | fprintf(stderr, "and now we are going to print some more info about shminfo\n"); 37 | fprintf(stderr, "shminfo->shmid: %d\n", shminfo->shmid); 38 | fprintf(stderr, "shminfo->shmaddr: %p\n", shminfo->shmaddr); 39 | fprintf(stderr, "shminfo->readOnly: %d\n", shminfo->readOnly); 40 | 41 | 42 | Bool ret = XShmAttachFunc(dpy, shminfo); 43 | return ret; 44 | } 45 | 46 | // then we hookup XShmDetach 47 | // this is expected to be called when you stop screencast 48 | Bool XShmDetach(Display* dpy, XShmSegmentInfo* shminfo) 49 | { 50 | fprintf(stderr, "wemeetapp called XShmDetach(%p, %p)\n", dpy, shminfo); 51 | Bool ret = XShmDetachFunc(dpy, shminfo); 52 | return ret; 53 | } 54 | 55 | // and finally the main function XShmGetImage 56 | // this is expected to be called when wemeetapp want to get the image 57 | Bool XShmGetImage(Display* dpy, Drawable d, XImage* image, int x, int y, unsigned long plane_mask) 58 | { 59 | fprintf(stderr, "wemeetapp called XShmGetImage(%p, %lu, %p, %d, %d, %lu)\n", dpy, d, image, x, y, plane_mask); 60 | 61 | if (!initial_shm_getimage_reported) { 62 | 63 | fprintf(stderr, "image->width: %d\n", image->width); 64 | fprintf(stderr, "image->height: %d\n", image->height); 65 | fprintf(stderr, "image->xoffset: %d\n", image->xoffset); 66 | fprintf(stderr, "image->format: %d\n", image->format); 67 | fprintf(stderr, "image->data: %p\n", image->data); 68 | fprintf(stderr, "image->byte_order: %d\n", image->byte_order); 69 | fprintf(stderr, "image->bitmap_unit: %d\n", image->bitmap_unit); 70 | fprintf(stderr, "image->bitmap_bit_order: %d\n", image->bitmap_bit_order); 71 | fprintf(stderr, "image->bitmap_pad: %d\n", image->bitmap_pad); 72 | fprintf(stderr, "image->depth: %d\n", image->depth); 73 | fprintf(stderr, "image->bytes_per_line: %d\n", image->bytes_per_line); 74 | fprintf(stderr, "image->bits_per_pixel: %d\n", image->bits_per_pixel); 75 | initial_shm_getimage_reported = true; 76 | } 77 | 78 | Bool ret = XShmGetImageFunc(dpy, d, image, x, y, plane_mask); 79 | return ret; 80 | } 81 | 82 | Bool XDamageQueryExtension(Display *dpy, int *event_base_return, int *error_base_return) { 83 | fprintf(stderr, "XDamageQueryExtension(%p, %p, %p)\n", dpy, event_base_return, error_base_return); 84 | auto ret = XDamageQueryExtensionFunc(dpy, event_base_return, error_base_return); 85 | fprintf(stderr, "XDamageQueryExtension returned %d\n", ret); 86 | fprintf(stderr, "But we will return 0\n"); 87 | return 0; 88 | } 89 | 90 | 91 | } -------------------------------------------------------------------------------- /hook_opencv.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | struct OpencvDLFCNSingleton{ 12 | 13 | using THIS_CLASS = OpencvDLFCNSingleton; 14 | 15 | private: 16 | OpencvDLFCNSingleton(){ 17 | libopencv_core_handle = dlopen("libopencv_core.so", RTLD_LOCAL|RTLD_LAZY); 18 | if(libopencv_core_handle == nullptr){ 19 | throw std::runtime_error("Failed to open library libopencv_core.so"); 20 | } 21 | libopencv_imgproc_handle = dlopen("libopencv_imgproc.so", RTLD_LOCAL|RTLD_LAZY); 22 | if(libopencv_imgproc_handle == nullptr){ 23 | throw std::runtime_error("Failed to open library libopencv_imgproc.so"); 24 | } 25 | } 26 | 27 | public: 28 | 29 | inline static OpencvDLFCNSingleton& getSingleton(){ 30 | static OpencvDLFCNSingleton singleton; 31 | return singleton; 32 | } 33 | 34 | ~OpencvDLFCNSingleton(){ 35 | if (libopencv_core_handle != nullptr){ 36 | dlclose(libopencv_core_handle); 37 | } 38 | if (libopencv_imgproc_handle != nullptr){ 39 | dlclose(libopencv_imgproc_handle); 40 | } 41 | } 42 | 43 | static inline CvMat* cvInitMatHeader( 44 | CvMat* mat, int rows, int cols, 45 | int type, void* data = NULL, 46 | int step = CV_AUTOSTEP 47 | ){ 48 | using FType = CvMat*(CvMat*, int, int, int, void*, int); 49 | auto& singleton = getSingleton(); 50 | static auto func = (FType*)dlsym(singleton.libopencv_core_handle, "cvInitMatHeader"); 51 | return func(mat, rows, cols, type, data, step); 52 | } 53 | 54 | static inline CvMat* cvCreateMat( 55 | int rows, int cols, int type 56 | ){ 57 | using FType = CvMat*(int, int, int); 58 | auto& singleton = getSingleton(); 59 | static auto func = (FType*)dlsym(singleton.libopencv_core_handle, "cvCreateMat"); 60 | return func(rows, cols, type); 61 | } 62 | 63 | static inline void cvReleaseMat(CvMat** mat){ 64 | using FType = void(CvMat**); 65 | auto& singleton = getSingleton(); 66 | static auto func = (FType*)dlsym(singleton.libopencv_core_handle, "cvReleaseMat"); 67 | return func(mat); 68 | } 69 | 70 | static inline CvMat* cvGetSubRect( 71 | const CvArr* arr, CvMat* submat, CvRect rect 72 | ){ 73 | using FType = CvMat*(const CvArr*, CvMat*, CvRect); 74 | auto& singleton = getSingleton(); 75 | static auto func = (FType*)dlsym(singleton.libopencv_core_handle, "cvGetSubRect"); 76 | return func(arr, submat, rect); 77 | } 78 | 79 | static inline void cvResize( 80 | const CvArr* src, CvArr* dst, 81 | int interpolation 82 | ){ 83 | using FType = void(const CvArr*, CvArr*, int); 84 | auto& singleton = getSingleton(); 85 | static auto func = (FType*)dlsym(singleton.libopencv_imgproc_handle, "cvResize"); 86 | return func(src, dst, interpolation); 87 | } 88 | 89 | static inline void cvSetZero(CvArr* arr){ 90 | using FType = void(CvArr*); 91 | auto& singleton = getSingleton(); 92 | static auto func = (FType*)dlsym(singleton.libopencv_core_handle, "cvSetZero"); 93 | return func(arr); 94 | } 95 | 96 | static inline void cvCopy(const CvArr* src, CvArr* dst){ 97 | using FType = void(const CvArr*, CvArr*); 98 | auto& singleton = getSingleton(); 99 | static auto func = (FType*)dlsym(singleton.libopencv_core_handle, "cvCopy"); 100 | return func(src, dst); 101 | } 102 | 103 | static inline void cvCvtColor( 104 | const CvArr* src, CvArr* dst, 105 | int code 106 | ){ 107 | using FType = void(const CvArr*, CvArr*, int); 108 | auto& singleton = getSingleton(); 109 | static auto func = (FType*)dlsym(singleton.libopencv_imgproc_handle, "cvCvtColor"); 110 | return func(src, dst, code); 111 | } 112 | 113 | static inline void cvTranspose(const CvArr* src, CvArr* dst){ 114 | using FType = void(const CvArr*, CvArr*); 115 | auto& singleton = getSingleton(); 116 | static auto func = (FType*)dlsym(singleton.libopencv_core_handle, "cvTranspose"); 117 | return func(src, dst); 118 | } 119 | 120 | static inline void cvFlip( 121 | const CvArr* src, CvArr* dst = nullptr, 122 | int flip_mode = 0 123 | ){ 124 | using FType = void(const CvArr*, CvArr*, int); 125 | auto& singleton = getSingleton(); 126 | static auto func = (FType*)dlsym(singleton.libopencv_core_handle, "cvFlip"); 127 | return func(src, dst, flip_mode); 128 | } 129 | 130 | static inline void cvRotate( 131 | const CvArr *src, CvArr* dst, 132 | int angle_clockwise 133 | ) { 134 | switch (angle_clockwise) { 135 | case 90: 136 | cvTranspose(src, dst); 137 | cvFlip(dst, dst, 1); 138 | break; 139 | case 180: 140 | cvFlip(src, dst, -1); 141 | break; 142 | case -90: 143 | cvTranspose(src, dst); 144 | cvFlip(dst, dst, 0); 145 | break; 146 | default: 147 | break; 148 | } 149 | } 150 | 151 | void* libopencv_core_handle{nullptr}; 152 | void* libopencv_imgproc_handle{nullptr}; 153 | 154 | 155 | }; 156 | -------------------------------------------------------------------------------- /format.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | enum class SpaVideoFormat_e { 9 | RGBx = SPA_VIDEO_FORMAT_RGBx, 10 | BGRx = SPA_VIDEO_FORMAT_BGRx, 11 | RGBA = SPA_VIDEO_FORMAT_RGBA, 12 | BGRA = SPA_VIDEO_FORMAT_BGRA, 13 | RGB = SPA_VIDEO_FORMAT_RGB, 14 | BGR = SPA_VIDEO_FORMAT_BGR, 15 | INVALID = -1 16 | }; 17 | 18 | inline std::string spa_to_string(SpaVideoFormat_e const& format){ 19 | switch (format) { 20 | case SpaVideoFormat_e::RGBx: 21 | return "RGBx"; 22 | case SpaVideoFormat_e::BGRx: 23 | return "BGRx"; 24 | case SpaVideoFormat_e::RGBA: 25 | return "RGBA"; 26 | case SpaVideoFormat_e::BGRA: 27 | return "BGRA"; 28 | case SpaVideoFormat_e::RGB: 29 | return "RGB"; 30 | case SpaVideoFormat_e::BGR: 31 | return "BGR"; 32 | default: 33 | return "INVALID"; 34 | } 35 | } 36 | 37 | // A VERY tedious color convert code getter 38 | // This can be handled more gracefully using something like string matching... 39 | // however let it be it for now 40 | // note: -1 means no conversion is needed 41 | inline int get_opencv_cAPI_color_convert_code( 42 | SpaVideoFormat_e const& src_format, 43 | SpaVideoFormat_e const& dst_format 44 | ){ 45 | // shortcut1: src and dst match exactly 46 | if (src_format == dst_format) { 47 | return -1; 48 | } 49 | // shortcut2: RGBA == RGBx 50 | if (src_format == SpaVideoFormat_e::RGBA && dst_format == SpaVideoFormat_e::RGBx || 51 | src_format == SpaVideoFormat_e::RGBx && dst_format == SpaVideoFormat_e::RGBA 52 | ) { 53 | return -1; 54 | } 55 | // shortcut3: BGRA == BGRx 56 | if (src_format == SpaVideoFormat_e::BGRA && dst_format == SpaVideoFormat_e::BGRx || 57 | src_format == SpaVideoFormat_e::BGRx && dst_format == SpaVideoFormat_e::BGRA 58 | ) { 59 | return -1; 60 | } 61 | if (src_format == SpaVideoFormat_e::RGB){ 62 | // RGB -> BGR 63 | if (dst_format == SpaVideoFormat_e::BGR){ 64 | return CV_RGB2BGR; 65 | } 66 | // RGB -> BGRA / BGRx 67 | if (dst_format == SpaVideoFormat_e::BGRA || dst_format == SpaVideoFormat_e::BGRx){ 68 | return CV_RGB2BGRA; 69 | } 70 | // RGB -> RGBA / RGBx 71 | if (dst_format == SpaVideoFormat_e::RGBA || dst_format == SpaVideoFormat_e::RGBx){ 72 | return CV_RGB2RGBA; 73 | } 74 | } 75 | 76 | if (src_format == SpaVideoFormat_e::BGR){ 77 | // BGR -> RGB 78 | if (dst_format == SpaVideoFormat_e::RGB){ 79 | return CV_BGR2RGB; 80 | } 81 | // BGR -> BGRA / BGRx 82 | if (dst_format == SpaVideoFormat_e::BGRA || dst_format == SpaVideoFormat_e::BGRx){ 83 | return CV_BGR2BGRA; 84 | } 85 | // BGR -> RGBA / RGBx 86 | if (dst_format == SpaVideoFormat_e::RGBA || dst_format == SpaVideoFormat_e::RGBx){ 87 | return CV_BGR2RGBA; 88 | } 89 | } 90 | 91 | if (src_format == SpaVideoFormat_e::RGBA || src_format == SpaVideoFormat_e::RGBx){ 92 | // RGBA/RGBx -> RGB 93 | if (dst_format == SpaVideoFormat_e::RGB){ 94 | return CV_RGBA2RGB; 95 | } 96 | // RGBA/RGBx -> BGR 97 | if (dst_format == SpaVideoFormat_e::BGR){ 98 | return CV_RGBA2BGR; 99 | } 100 | // RGBA/RGBx -> BGRA/BGRx 101 | if (dst_format == SpaVideoFormat_e::BGRA || dst_format == SpaVideoFormat_e::BGRx){ 102 | return CV_RGBA2BGRA; 103 | } 104 | } 105 | 106 | if (src_format == SpaVideoFormat_e::BGRA || src_format == SpaVideoFormat_e::BGRx){ 107 | // BGRA/BGRx -> RGB 108 | if (dst_format == SpaVideoFormat_e::RGB){ 109 | return CV_BGRA2RGB; 110 | } 111 | // BGRA/BGRx -> BGR 112 | if (dst_format == SpaVideoFormat_e::BGR){ 113 | return CV_BGRA2BGR; 114 | } 115 | // BGRA/BGRx -> RGBA/RGBx 116 | if (dst_format == SpaVideoFormat_e::RGBA || dst_format == SpaVideoFormat_e::RGBx){ 117 | return CV_BGRA2RGBA; 118 | } 119 | } 120 | 121 | // guard 122 | return -1; 123 | } 124 | 125 | 126 | inline auto spa_videoformat_bytesize(const SpaVideoFormat_e& format) -> int { 127 | switch (format) { 128 | case SpaVideoFormat_e::RGBx: 129 | return 4; 130 | case SpaVideoFormat_e::BGRx: 131 | return 4; 132 | case SpaVideoFormat_e::RGBA: 133 | return 4; 134 | case SpaVideoFormat_e::BGRA: 135 | return 4; 136 | case SpaVideoFormat_e::RGB: 137 | return 3; 138 | case SpaVideoFormat_e::BGR: 139 | return 3; 140 | default: 141 | return -1; 142 | } 143 | } 144 | 145 | inline auto ximage_to_spa(const XImage& ximage) -> SpaVideoFormat_e { 146 | if (ximage.format != 2){ 147 | // we only support ZPixmap 148 | return SpaVideoFormat_e::INVALID; 149 | } 150 | if (ximage.bits_per_pixel == 32) { 151 | // possibly RGBA, BGRA, RGBx or BGRx 152 | // we just combine RGBx and BGRx to RGBA and BGRA, respectively 153 | if (ximage.red_mask == 0xff0000 && ximage.green_mask == 0xff00 && ximage.blue_mask == 0xff) { 154 | return SpaVideoFormat_e::BGRA; 155 | } else if (ximage.red_mask == 0xff && ximage.green_mask == 0xff00 && ximage.blue_mask == 0xff0000) { 156 | return SpaVideoFormat_e::RGBA; 157 | } else { 158 | return SpaVideoFormat_e::INVALID; 159 | } 160 | } else { 161 | // possibly RGB or BGR 162 | if (ximage.red_mask == 0xff0000 && ximage.green_mask == 0xff00 && ximage.blue_mask == 0xff) { 163 | return SpaVideoFormat_e::BGR; 164 | } else if (ximage.red_mask == 0xff && ximage.green_mask == 0xff00 && ximage.blue_mask == 0xff0000) { 165 | return SpaVideoFormat_e::RGB; 166 | } else { 167 | return SpaVideoFormat_e::INVALID; 168 | } 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # --> !! Deprecation Notice !! <-- 3 | 4 | **25-9-12:** 5 | 6 | 目前,腾讯会议官方已经为Linux更新了最新版`wemeet`(在今日最新版本为`V 3.26.10(400)`),其已经**直接支持了Wayland下屏幕共享的功能**. 不仅如此,新版本界面也更加美观现代,基本达到了其余操作系统版本的水平。因此,本项目已经完成了其任务,**从现在起不再维护**,转入归档状态. 7 | 8 | 因此,所有用户现在应该**安装/升级**到最新版的腾讯会议`wemeet`,并**卸载**本项目. 9 | 10 | 最后,衷心谢谢各位贡献者的支持,特别是`DerryAlex`、`novel2430`和`Coekjan`,以及AUR上的`sukanka`. 他们的贡献解决了多个突出问题,让我们能在短暂的半年时间内获得一个相对稳定和良好的功能和使用体验。由于我个人的怠惰和心理问题,此前有许多MR和Issue没有及时处理和回复,真的抱歉啦!希望这个项目也曾帮助到过大家。 11 | 12 | 13 | # wemeet-wayland-screenshare--实现Wayland下腾讯会议屏幕共享(非虚拟相机) 14 | 15 | 长期以来,由于腾讯会议开发者的不作为,腾讯会议一直无法实现在Wayland下的屏幕共享,给Linux用户造成了极大的不便。但现在,很自豪地,本项目首次实现了在大部分Wayland环境下使用腾讯会议的屏幕共享功能! 16 | 17 | 特别地,有别于其他方案,**本项目不使用虚拟相机**,而是特别实现了一个hook库,使得用户可以在大部分Wayland环境下正常使用腾讯会议的屏幕共享功能. 18 | 19 | 20 | 21 | ## ✨使用效果 22 | 23 | 在几位贡献者的努力下,本项目现在已经可以支持大部分的DE/WM下的腾讯会议屏幕共享功能. 目前确认可用的DE/WM包括: 24 | 25 | - KDE Wayland 26 | - GNOME Wayland 27 | - Hyprland 28 | - wlroots-based (tested: sway, wayfire, labwc, river, maomao) 29 | 30 | 下面的图片展示了使用步骤和效果: 31 | 32 | ![Inst1](./resource/instruction-1.png "instruction-1") 33 | ![Inst2](./resource/instruction-2.png "instruction-2") 34 | ![Inst3](./resource/instruction-3-new.png "instruction-3") 35 | ![Support](./resource/supported_DEs.png "support") 36 | 37 | 38 | ## ⚒️编译、安装和使用 39 | 40 | 在几位贡献者的努力下,本项目现在已经可以同时支持KDE Wayland和GNOME Wayland下的腾讯会议屏幕共享功能. 特别地,下面给出在ArchLinux上的编译和安装方法. 如果你使用的是其他distro,还请自行adapt,但总体上应该相当容易. 41 | 42 | ### 手动测试/安装 43 | 44 | 1. 安装AUR package [wemeet-bin](https://aur.archlinux.org/packages/wemeet-bin): 45 | 46 | ```bash 47 | # Use whatever AUR helper you like, or even build locally 48 | yay -S wemeet-bin 49 | ``` 50 | 51 | 2. 安装依赖 52 | 53 | ```bash 54 | sudo pacman -S wireplumber 55 | sudo pacman -S libportal xdg-desktop-portal xdg-desktop-portal-impl xwaylandvideobridge opencv 56 | ``` 57 | 58 | - 注意:本项目在之前的版本中必须依赖于`pipewire-media-session`. 而现在经过测试已经确定`wireplumber`下可用. 如果系统中已经安装`pipewire-media-session`,pacman会在安装`wireplumber`时提示替换,你基本可以毫无顾虑地同意替换. 关于此问题具体的implication,还请自行查阅相关资料. 59 | 60 | 3. 编译本项目: 61 | 62 | ```bash 63 | # 1. clone this repo 64 | git clone --recursive https://github.com/xuwd1/wemeet-wayland-screenshare.git 65 | cd wemeet-wayland-screenshare 66 | 67 | # 2. build the project 68 | mkdir build 69 | cd build 70 | cmake .. -GNinja -DCMAKE_BUILD_TYPE=Release 71 | ninja 72 | 73 | ``` 74 | 75 | - 编译完成后,`build`目录下可见有`libhook.so` 76 | 77 | 4. 将`libhook.so`预加载并钩住`wemeet`: 78 | 79 | ```bash 80 | # make sure you are in the build directory 81 | LD_PRELOAD=$(readlink -f ./libhook.so) wemeet-x11 82 | ``` 83 | 84 | 按照上面的使用方法,你应该可以在Wayland下正常使用腾讯会议的屏幕共享功能了! 85 | - 注意:推荐使用`wemeet-x11`. 具体原因请见后文[兼容性和稳定性类](#兼容性和稳定性类-high-priority)部分. 86 | 87 | 88 | 5. (optional) 将`libhook.so`安装到系统目录 89 | 90 | ```bash 91 | sudo ninja install 92 | ``` 93 | 默认情况下,`libhook.so`会被安装到`/usr/lib/wemeet`下. 你随后可以相应地自行编写一个启动脚本,或者修改`wemeet-bin`的启动脚本,使得`libhook.so`按如上方式被预加载并钩住`wemeetapp`. 94 | 95 | ### Flatpak 96 | 97 | Flatpak 版腾讯会议已集成本项目,直接从 [Flathub](https://flathub.org/apps/com.tencent.wemeet) 安装即可: 98 | 99 | 100 | Download on Flathub 101 | 102 | 103 | ### Arch Only: 使用AUR包 `wemeet-wayland-screenshare-git` 104 | 105 | 如果你使用的是ArchLinux,更方便的安装方法是直接安装AUR包`wemeet-wayland-screenshare-git`: 106 | 107 | ```bash 108 | # Use whatever AUR helper you like, or even build locally 109 | yay -S wemeet-wayland-screenshare-git 110 | 111 | ``` 112 | 113 | 随后,在命令行执行`wemeet-wayland-screenshare`,或者直接在应用菜单中搜索`WemeetApp(Wayland Screenshare)`,打开即可. 114 | 115 | ## 🔬原理概述 116 | 117 | 下面是本项目概念上的系统框图. 118 | 119 | ![System Diagram](./resource/diagram.svg "system diagram") 120 | 121 | 事实上,本项目实际上开发的是一个X11的hack,而不是wemeetapp的hack. 其钩住X11的`XShmAttach`,`XShmGetImage`和`XShmDetach`函数,分别实现: 122 | 123 | - 在`XShmAttach`被调用时,hook会启动payload thread,启动xdg portal session,并进一步启动gio thread和pipewire thread,开始屏幕录制,并将frame不断写入framebuffer. 此外,一个x11 overlay sanitizer会被启动,使得X11模式下(`wemeet-x11`),开启屏幕共享时wemeet的overlay被强制最小化,进而让用户的鼠标可以自由地点击包括xdg portal窗口在内的任何屏幕内容. 124 | 125 | - 在`XShmGetImage`被调用时,hook会从framebuffer中读取图像,并将其写入`XImage`结构体中,让wemeetapp获取到正确的屏幕图像 126 | 127 | - 在`XShmDetach`被调用时,hook会指示payload thread停止xdg portal session,并进一步join gio thread和pipewire thread,结束屏幕录制. 128 | 129 | 此外,hook同时还会劫持`XDamageQueryExtension`函数,使得上层应用认为`XDamage`扩展并未被支持,从而强迫其不断使用`XShmGetImage`获取新的完整图像. 130 | 131 | 如果你对此感兴趣,也可以进一步查阅`experiments`目录下的代码和文档,以了解更多细节. 132 | 133 | 134 | 135 | ## 🆘请帮帮本项目! 136 | 137 | 本项目当前还是非常实验性质的,其还有诸多不足和许多亟待解决的问题. 如果你有兴趣,欢迎向本项目贡献代码,或者提出建议!下面是一个简要的问题列表: 138 | 139 | 140 | ### 性能与效果类(Low priority) 141 | 142 | 1. framebuffer中的mutex导致的功耗偏高的问题已经在`Coekjan`的PR [#13](https://github.com/xuwd1/wemeet-wayland-screenshare/pull/13)中得到解决. 目前观察到对于[灵耀16Air(UM5606)](https://wiki.archlinux.org/title/ASUS_Zenbook_UM5606) Ryzen AI HX 370, 屏幕共享时的最低封装功耗可以低至4.7W左右,和Windows下的屏幕共享功耗基本相当. 143 | 144 | 145 | 2. opencv的链接问题已经根据`lilydjwg`的issue [#1](https://github.com/xuwd1/wemeet-wayland-screenshare/issues/1)得到了解决. 现在,借助opencv,本项目可以在保证aspect ratio不变的情况下对图像进行缩放. 146 | 147 | 148 | 149 | ### 兼容性和稳定性类 (High priority) 150 | 151 | 152 | 1. 本项目目前只在以下环境下测试过: 153 | - **EndeavourOS ArchLinux KDE Wayland** + `wireplumber/pipewire-media-session` 正常工作 154 | - **EndeavourOS ArchLinux GNOME 47 Wayland** + `wireplumber` 正常工作 155 | - 根据贡献者`DerryAlex`的测试结果,**GNOME 43** + `wireplumber` (Unknown distro) 正常工作 156 | - 根据[#4](https://github.com/xuwd1/wemeet-wayland-screenshare/pull/4)中反馈的结果,**Manjaro GNOME 47** (+ possibly `wireplumber`) 正常工作 157 | - 根据`falser`的反馈,**ArchLinux Hyprland** + `wireplumber`正常工作 158 | - 根据`novel2430`在[#9](https://github.com/xuwd1/wemeet-wayland-screenshare/issues/9)中的测试,典型的**wlroots-based DE/WM**下 (tested: sway, wayfire, labwc, river) 正常工作 159 | 160 | 2. 目前,本项目只基于AUR package [wemeet-bin](https://aur.archlinux.org/packages/wemeet-bin)测试过. 特别地,在纯Wayland模式下(使用`wemeet`启动),wemeet本身存在一个恶性bug:尽管搭配本项目时,Linux用户可以将屏幕共享给其他用户,但当其他用户发起屏幕共享时,wemeet则会直接崩溃. 因此,本项目推荐启动X11模式的wemeet(使用`wemeet-x11`启动). 161 | 162 | - 此时,本项目仍然可以确保屏幕共享功能正常运行. 163 | - 而这主要得益于本项目新增加的x11 sanitizer,其会在屏幕共享时强制最小化wemeet的overlay(开始屏幕共享后2秒后生效),使得用户可以自由地点击包括xdg portal窗口在内的任何屏幕内容. 164 | 165 | 166 | 167 | ## 🙏致谢 168 | 169 | - 感谢AUR package [wemeet-bin](https://aur.archlinux.org/packages/wemeet-bin)的维护者`sukanka`以及贡献者`Sam L. Yes`. 他们出色的工作基本解决了腾讯会议在Wayland下的正常运行问题,造福了众多Linux用户. 170 | 171 | - 感谢`nothings`开发的[stb](https://github.com/nothings/stb)库. 相较于opencv的臃肿和CImg富有想象力的memory layout, `stb`库提供了一个轻量且直接的解决方案,使得本项目得以实现. 172 | 173 | - 感谢`lilydjwg`提出的issue. 他的建议解决了本项目无法链接到opencv库的问题,改善了本项目的性能和效果. 174 | 175 | - 感谢`DerryAlex`贡献的GNOME支持代码. 他出色的工作使得本项目可以在GNOME下正常工作,改进了x11 sanitizer的效果,并额外解决了项目中存在的一些问题. 176 | 177 | - 感谢`novel2430`的帮助. 他花费了大量时间和精力测试了本项目在wlroots-based DE/WM下的兼容性,并帮助了我们解决在这些环境下的一些问题. 178 | 179 | - 感谢`Coekjan`贡献的hugebuffer代码. 他的工作帮助本项目解决了framebuffer中的mutex导致的功耗偏高的问题. 180 | -------------------------------------------------------------------------------- /hook.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // got to include this before X11 headers 12 | #include "hook_opencv.hpp" 13 | 14 | #include 15 | 16 | #include "framebuf.hpp" 17 | #include "payload.hpp" 18 | #include "interface.hpp" 19 | #include "helpers.hpp" 20 | #include "hook.hpp" 21 | 22 | /* 23 | 24 | Why I'm using STB here: 25 | Initially I tried to use opencv as I actually have worked with it before 26 | However for *some* reason long as the opencv is linked to the hook 27 | the hook would always crash wemeetapp. I suspect that maybe the wemeetapp 28 | itself is also using opencv and thereby some conflict arises. 29 | 30 | I also tried the CImg library but its in-memory image layout is just ABSURD, 31 | which could cause severe performance issue. 32 | 33 | So I eventually picked STB, and I'm happy with it now. 34 | 35 | */ 36 | 37 | // #define STB_IMAGE_RESIZE_IMPLEMENTATION 38 | // #include 39 | 40 | constexpr uint32_t DEFAULT_FRAME_HEIGHT = 1080; 41 | constexpr uint32_t DEFAULT_FRAME_WIDTH = 1920; 42 | 43 | void XShmAttachHook(){ 44 | 45 | auto& interface_singleton = InterfaceSingleton::getSingleton(); 46 | 47 | // initialize interface singleton: 48 | // (1) allocate the interface object 49 | interface_singleton.interface_handle = new Interface( 50 | DEFAULT_FB_ALLOC_HEIGHT, DEFAULT_FB_ALLOC_WIDTH, 51 | DEFAULT_FRAME_HEIGHT, DEFAULT_FRAME_WIDTH, SpaVideoFormat_e::RGBA 52 | ); 53 | // (2) allocate the screencast portal object 54 | interface_singleton.portal_handle = new XdpScreencastPortal(); 55 | 56 | // start the payload thread 57 | std::thread payload_thread = std::thread(payload_main); 58 | fprintf(stderr, "%s", green_text("[hook] payload thread started\n").c_str()); 59 | 60 | while(interface_singleton.portal_handle.load()->status.load(std::memory_order_seq_cst) == XdpScreencastPortalStatus::kInit ) { 61 | std::this_thread::sleep_for(std::chrono::milliseconds(20)); 62 | }; 63 | auto payload_status = interface_singleton.portal_handle.load()->status.load(std::memory_order_seq_cst); 64 | std::string payload_status_str = 65 | payload_status == XdpScreencastPortalStatus::kCancelled ? "cancelled" : 66 | payload_status == XdpScreencastPortalStatus::kRunning ? "running" : 67 | "unknown"; 68 | if (payload_status == XdpScreencastPortalStatus::kRunning) { 69 | // things are good 70 | fprintf(stderr, "%s", green_text("[hook] portal status: " + payload_status_str + "\n").c_str()); 71 | } else { 72 | // things are bad, we have to de-initialize and exit 73 | fprintf(stderr, "%s", red_text("[hook] portal status: " + payload_status_str + "\n").c_str()); 74 | fprintf(stderr, "%s", red_text("[hook] <<> Hook is now exiting.\n").c_str()); 75 | // payload thread should have quitted via g_main_loop_quit 76 | payload_thread.join(); 77 | delete interface_singleton.interface_handle.load(); 78 | delete interface_singleton.portal_handle.load(); 79 | interface_singleton.interface_handle.store(nullptr); 80 | interface_singleton.portal_handle.store(nullptr); 81 | return; 82 | } 83 | 84 | 85 | while(interface_singleton.portal_handle.load()->pipewire_fd.load(std::memory_order_seq_cst) == -1){ 86 | std::this_thread::sleep_for(std::chrono::milliseconds(20)); 87 | } 88 | fprintf(stderr, "%s", green_text("[hook SYNC] pipewire_fd acquired: " + std::to_string(interface_singleton.portal_handle.load()->pipewire_fd.load()) + "\n").c_str()); 89 | 90 | interface_singleton.pipewire_handle = new PipewireScreenCast(interface_singleton.portal_handle.load()->pipewire_fd.load(), interface_singleton.portal_handle.load()->pipewire_node_ids.at(0)); 91 | fprintf(stderr, "%s", green_text("[hook SYNC] pipewire screencast object allocated\n").c_str()); 92 | 93 | payload_thread.detach(); 94 | 95 | } 96 | 97 | template 98 | struct remove_pointer_cvref { 99 | using type = std::remove_cv_t>>; 100 | }; 101 | 102 | template 103 | using remove_pointer_cvref_t = typename remove_pointer_cvref::type; 104 | 105 | 106 | // returns: ximage_width_offset, ximage_height_offset, target_width, target_height 107 | std::tuple get_resize_param( 108 | uint32_t ximage_width, 109 | uint32_t ximage_height, 110 | uint32_t framebuffer_width, 111 | uint32_t framebuffer_height 112 | ){ 113 | // keep the framebuffer aspect ratio 114 | double framebuffer_aspect_ratio = static_cast(framebuffer_width) / static_cast(framebuffer_height); 115 | double ximage_aspect_ratio = static_cast(ximage_width) / static_cast(ximage_height); 116 | 117 | uint32_t target_width = 0; 118 | uint32_t target_height = 0; 119 | uint32_t ximage_width_offset = 0; 120 | uint32_t ximage_height_offset = 0; 121 | 122 | if (framebuffer_aspect_ratio > ximage_aspect_ratio) { 123 | // framebuffer is wider than ximage 124 | target_width = ximage_width; 125 | target_height = (ximage_width * framebuffer_height) / framebuffer_width; 126 | ximage_height_offset = (ximage_height - target_height) / 2; 127 | } else { 128 | // framebuffer is taller than ximage 129 | target_height = ximage_height; 130 | target_width = ximage_height * framebuffer_width / framebuffer_height; 131 | ximage_width_offset = (ximage_width - target_width) / 2; 132 | } 133 | 134 | return std::make_tuple(ximage_width_offset, ximage_height_offset, target_width, target_height); 135 | } 136 | 137 | 138 | void XShmGetImageHook(XImage& image){ 139 | 140 | auto& interface_singleton = InterfaceSingleton::getSingleton(); 141 | 142 | if (interface_singleton.interface_handle.load() == nullptr){ 143 | fprintf(stderr, "%s", red_text("[hook] hook will NOT work as you have cancelled the screencast!!!\n").c_str()); 144 | return; 145 | } 146 | 147 | auto ximage_spa_format = ximage_to_spa(image); 148 | auto ximage_width = image.width; 149 | auto ximage_height = image.height; 150 | size_t ximage_bytes_per_line = image.bytes_per_line; 151 | 152 | CvMat ximage_cvmat; 153 | OpencvDLFCNSingleton::cvInitMatHeader( 154 | &ximage_cvmat, ximage_height, ximage_width, 155 | CV_8UC4, image.data, ximage_bytes_per_line 156 | ); 157 | OpencvDLFCNSingleton::cvSetZero(&ximage_cvmat); 158 | 159 | auto& framebuffer = interface_singleton.interface_handle.load()->framebuf; 160 | auto framebuffer_spa_format = framebuffer.format; 161 | auto framebuffer_width = framebuffer.width; 162 | auto framebuffer_height = framebuffer.height; 163 | auto framebuffer_row_byte_stride = framebuffer.row_byte_stride; 164 | 165 | CvMat framebuffer_cvmat; 166 | OpencvDLFCNSingleton::cvInitMatHeader( 167 | &framebuffer_cvmat, framebuffer_height, framebuffer_width, 168 | CV_8UC4, framebuffer.data.get(), framebuffer_row_byte_stride 169 | ); 170 | CvMat *framebuffer_cvmat_ptr = &framebuffer_cvmat; 171 | if (framebuffer.crop_height && framebuffer.crop_width) { 172 | OpencvDLFCNSingleton::cvGetSubRect(framebuffer_cvmat_ptr, framebuffer_cvmat_ptr, {framebuffer.crop_x, framebuffer.crop_y, framebuffer.crop_width, framebuffer.crop_height}); 173 | framebuffer_width = framebuffer.crop_width; 174 | framebuffer_height = framebuffer.crop_height; 175 | } 176 | if (framebuffer.rotate) { 177 | if (framebuffer.rotate != 180) { 178 | std::swap(framebuffer_width, framebuffer_height); 179 | } 180 | CvMat *framebuffer_cvmat_rotated = OpencvDLFCNSingleton::cvCreateMat(framebuffer_height, framebuffer_width, CV_8UC4); 181 | OpencvDLFCNSingleton::cvRotate(framebuffer_cvmat_ptr, framebuffer_cvmat_rotated, framebuffer.rotate); 182 | framebuffer_cvmat_ptr = framebuffer_cvmat_rotated; 183 | } 184 | if (framebuffer.flip) { 185 | OpencvDLFCNSingleton::cvFlip(framebuffer_cvmat_ptr); 186 | } 187 | 188 | 189 | // get the resize parameters 190 | auto [ximage_width_offset, ximage_height_offset, target_width, target_height] = get_resize_param( 191 | ximage_width, ximage_height, framebuffer_width, framebuffer_height 192 | ); 193 | CvMat ximage_cvmat_roi; 194 | OpencvDLFCNSingleton::cvGetSubRect( 195 | &ximage_cvmat, &ximage_cvmat_roi, 196 | cvRect(ximage_width_offset, ximage_height_offset, target_width, target_height) 197 | ); 198 | OpencvDLFCNSingleton::cvResize( 199 | framebuffer_cvmat_ptr, &ximage_cvmat_roi, CV_INTER_LINEAR 200 | ); 201 | 202 | if (framebuffer_cvmat_ptr != &framebuffer_cvmat) { 203 | OpencvDLFCNSingleton::cvReleaseMat(&framebuffer_cvmat_ptr); 204 | } 205 | 206 | // do color convert 207 | // here the code is currently mainly for wlroot WMs 208 | // maybe we could shortcut this by detecting WM? 209 | 210 | int cv_cAPI_color_cvt_code = get_opencv_cAPI_color_convert_code( 211 | framebuffer_spa_format, ximage_spa_format 212 | ); 213 | 214 | if (cv_cAPI_color_cvt_code != -1){ 215 | // non -1 code means color conversion is needed 216 | OpencvDLFCNSingleton::cvCvtColor( 217 | &ximage_cvmat_roi, &ximage_cvmat_roi, cv_cAPI_color_cvt_code 218 | ); 219 | } 220 | 221 | // legacy stb implementation 222 | // resize the framebuffer to ximage size 223 | // note: by using STBIR_BGRA_PM we are essentially ignoring the alpha channel 224 | // heck, I don't even know if the alpha channel is used in the first place 225 | // Anyway, we are just going to ignore it for now since this will be much faster 226 | // stbir_resize_uint8_srgb( 227 | // reinterpret_cast(framebuffer.data.get()), 228 | // framebuffer_width, framebuffer_height, framebuffer_row_byte_stride, 229 | // reinterpret_cast(image.data), 230 | // ximage_width, ximage_height, ximage_bytes_per_line, 231 | // stbir_pixel_layout::STBIR_BGRA_PM 232 | // ); 233 | 234 | 235 | return; 236 | 237 | } 238 | 239 | 240 | 241 | void XShmDetachStopPWLoop(){ 242 | auto& interface_singleton = InterfaceSingleton::getSingleton(); 243 | fprintf(stderr, "%s", green_text("[hook] signal pw stop.\n").c_str()); 244 | interface_singleton.interface_handle.load()->pw_stop_flag.store(true, std::memory_order_seq_cst); 245 | while(!interface_singleton.interface_handle.load()->payload_pw_stop_confirm.load(std::memory_order_seq_cst)){ 246 | std::this_thread::sleep_for(std::chrono::milliseconds(20)); 247 | } 248 | fprintf(stderr, "%s", green_text("[hook SYNC] pw stop confirmed.\n").c_str()); 249 | return; 250 | } 251 | 252 | void XShmDetachStopGIOLoop(){ 253 | auto& interface_singleton = InterfaceSingleton::getSingleton(); 254 | fprintf(stderr, "%s", green_text("[hook] stop gio main loop.\n").c_str()); 255 | g_main_loop_quit(interface_singleton.portal_handle.load()->gio_mainloop); 256 | while(!interface_singleton.interface_handle.load()->payload_gio_stop_confirm.load(std::memory_order_seq_cst)){ 257 | std::this_thread::sleep_for(std::chrono::milliseconds(20)); 258 | } 259 | fprintf(stderr, "%s", green_text("[hook SYNC] gio stop confirmed.\n").c_str()); 260 | return; 261 | } 262 | 263 | void XShmDetachHook(){ 264 | 265 | auto& interface_singleton = InterfaceSingleton::getSingleton(); 266 | 267 | // the interface_handle being non nullptr 268 | // means that the screencast has been started 269 | if (interface_singleton.interface_handle != nullptr){ 270 | 271 | XShmDetachStopPWLoop(); 272 | XShmDetachStopGIOLoop(); 273 | 274 | // normal de-initialize interface singleton: 275 | // (1) free the interface object 276 | delete interface_singleton.interface_handle.load(); 277 | interface_singleton.interface_handle.store(nullptr); 278 | // (2) free the screencast portal object 279 | delete interface_singleton.portal_handle.load(); 280 | interface_singleton.portal_handle.store(nullptr); 281 | // (3) free the pipewire screencast object 282 | delete interface_singleton.pipewire_handle.load(); 283 | interface_singleton.pipewire_handle.store(nullptr); 284 | } else { 285 | // we do nothing here since the objects (interface, portal) are already freed, 286 | // and pipewire object has never been created 287 | fprintf(stderr, "%s", red_text("[hook] objects are already freed because of cancelled screencast. exiting.\n").c_str()); 288 | } 289 | 290 | 291 | } 292 | 293 | extern "C" { 294 | 295 | Bool XShmAttach(Display* dpy, XShmSegmentInfo* shminfo){ 296 | XShmAttachHook(); 297 | return XShmAttachFunc(dpy, shminfo); 298 | } 299 | 300 | Bool XShmGetImage(Display* dpy, Drawable d, XImage* image, int x, int y, unsigned long plane_mask){ 301 | XShmGetImageHook(*image); 302 | return 1; 303 | } 304 | 305 | Bool XShmDetach(Display* dpy, XShmSegmentInfo* shminfo){ 306 | XShmDetachHook(); 307 | return XShmDetachFunc(dpy, shminfo); 308 | } 309 | 310 | Bool XDamageQueryExtension(Display *dpy, int *event_base_return, int *error_base_return) { 311 | return 0; 312 | } 313 | 314 | } 315 | -------------------------------------------------------------------------------- /payload.cpp: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include "payload.hpp" 12 | #include "interface.hpp" 13 | 14 | #include "helpers.hpp" 15 | 16 | using XWindow_t = Window; 17 | 18 | 19 | struct CandidateWindowInfo{ 20 | XWindow_t window_id; 21 | std::string window_name; 22 | int window_width; 23 | int window_height; 24 | }; 25 | 26 | 27 | std::vector x11_sanitizer_get_targets( 28 | Display* display, 29 | XWindow_t root_window 30 | ){ 31 | // hunt for direct children of root window that has override_redirect set to true 32 | Window root_return, parent_return; 33 | Window* children_return; 34 | unsigned int nchildren_return; 35 | auto xquery_status = XQueryTree(display, root_window, &root_return, &parent_return, &children_return, &nchildren_return); 36 | if (xquery_status == 0) { 37 | fprintf(stderr, "%s", red_text("[x11_sanitizer] XQueryTree failed. \n").c_str()); 38 | return {}; 39 | } 40 | std::vector targets; 41 | for (int i = 0; i < nchildren_return; i++) { 42 | XWindow_t cur_window = children_return[i]; 43 | XWindowAttributes window_attributes; 44 | auto xgetwindow_status = XGetWindowAttributes(display, cur_window, &window_attributes); 45 | if (xgetwindow_status == 0) { 46 | fprintf(stderr, "%s", red_text("[x11_sanitizer] XGetWindowAttributes failed. \n").c_str()); 47 | continue; 48 | } 49 | if (window_attributes.override_redirect == false) { 50 | continue; 51 | } 52 | 53 | // found override_redirect window 54 | // check its name 55 | 56 | std::string window_name; 57 | XTextProperty prop; 58 | Atom wmNameAtom = XInternAtom(display, "_NET_WM_NAME", True); 59 | if (wmNameAtom && XGetTextProperty(display, cur_window, &prop, wmNameAtom) && prop.value) { 60 | window_name = std::string(reinterpret_cast(prop.value)); 61 | XFree(prop.value); 62 | } 63 | 64 | // check if "wemeet" is in the window name 65 | if (window_name.find("wemeet") == std::string::npos) { 66 | continue; 67 | } 68 | 69 | 70 | // found candidate window here, add it to the list 71 | 72 | targets.push_back(CandidateWindowInfo{ 73 | .window_id = cur_window, 74 | .window_name = window_name, 75 | .window_width = window_attributes.width, 76 | .window_height = window_attributes.height 77 | }); 78 | } 79 | XFree(children_return); 80 | return targets; 81 | } 82 | 83 | 84 | std::vector> get_screen_sizes( 85 | Display* display, 86 | int screen 87 | ){ 88 | std::vector> screen_sizes; 89 | XRRScreenResources *screen_resources = XRRGetScreenResources(display, RootWindow(display, screen)); 90 | for (int i = 0; i < screen_resources->noutput; i++) { 91 | RROutput output = screen_resources->outputs[i]; 92 | XRROutputInfo *output_info = XRRGetOutputInfo(display, screen_resources, output); 93 | if (output_info->connection == RR_Connected) { 94 | XRRCrtcInfo *crtc_info = XRRGetCrtcInfo(display, screen_resources, output_info->crtc); 95 | if (crtc_info) { 96 | screen_sizes.push_back(std::make_tuple(crtc_info->width, crtc_info->height)); 97 | XRRFreeCrtcInfo(crtc_info); 98 | } 99 | } 100 | XRRFreeOutputInfo(output_info); 101 | } 102 | XRRFreeScreenResources(screen_resources); 103 | return screen_sizes; 104 | } 105 | 106 | /* 107 | Below is a *VERY CRAZY* code that get rid of the stupid asshole wemeet screenshare overlay window, which 108 | prevents the user from clicking anything on the screen. 109 | 110 | we accomplish this by: 111 | 112 | 1. First locate the right window to get rid of. This can be very tricky, since wemeet would 113 | create and destroy multiple windows with override_redirect set to true after you started screenshare. 114 | If the wrong window is picked, the whole wemeet app GUI would enter a very weird state and will eventually 115 | crash, LMFAO. However I observed that it would eventually enter a "steady state" where the right window 116 | would be the one with the largest size. DerryAlex noticed this size should also be the "fullscreen size", 117 | though with some caveats. He also digged out that the unified strategy XUnmapWindow works well. 118 | 119 | So the eventual logic is, well, a little bit over engineered. In every 100ms, we: 120 | (1) first locate some candidate windows that has "wemeet" in its name and has override_redirect set to true. 121 | (2) then see if there is any candidate window whose size is "roughly the same" as the screen size. 122 | (3) if there is one, we just select it and then exit. 123 | (4) if there isn't one, the loop continues 124 | This loop will continue until $STEADY_WAIT_TIME has passed. If things turn out like this we use the fallback 125 | strategy that picks the window with the largest size. 126 | 127 | 2. Just hide the window, using the unified strategy "XUnmapWindow" regardless of the DE type. 128 | However we keep the DE detection logic here in case we need to use different strategies for different DEs... 129 | */ 130 | 131 | void x11_sanitizer_main() 132 | { 133 | // get the current session type 134 | // if it's "wayland", then the x11 sanitizer can just exit 135 | if (get_current_session_type() == SessionType::Wayland) { 136 | fprintf(stderr, "%s", green_text("[x11_sanitizer] wayland session detected. skipping x11 sanitizer. \n").c_str()); 137 | return; 138 | } 139 | 140 | auto& interface_singleton = InterfaceSingleton::getSingleton(); 141 | auto* interface_handle = interface_singleton.interface_handle.load(); 142 | Display* display = XOpenDisplay(NULL); 143 | int screen = DefaultScreen(display); 144 | XWindow_t root_window = DefaultRootWindow(display); 145 | 146 | std::vector> screen_sizes = get_screen_sizes(display, screen); 147 | 148 | // 2 seconds steady wait time seems to be "generally safe". 149 | std::chrono::milliseconds STEADY_WAIT_TIME(2000); 150 | std::chrono::time_point steady_start_time; 151 | bool target_first_occurred = false; 152 | bool target_managed = false; 153 | XWindow_t picked_window_id = 0; 154 | 155 | while(interface_handle->x11_sanitizer_stop_flag.load(std::memory_order_seq_cst) == false){ 156 | 157 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); 158 | 159 | auto targets = x11_sanitizer_get_targets(display, root_window); 160 | auto now = std::chrono::high_resolution_clock::now(); 161 | if (targets.size() != 0 && target_first_occurred == false) { 162 | target_first_occurred = true; 163 | steady_start_time = now; 164 | } 165 | 166 | // This can be much faster than STEADY_WAIT_TIME, say 200ms 167 | for (auto target: targets) { 168 | 169 | auto f_fuzzy_matched = [&](){ 170 | for (auto [width, height]: screen_sizes) { 171 | bool matched = std::abs(target.window_width - width) < 5 && std::abs(target.window_height - height) < 5; 172 | if (matched) { 173 | return true; 174 | } 175 | } 176 | return false; 177 | }; 178 | 179 | if (f_fuzzy_matched()) { 180 | auto duration = std::chrono::duration_cast>(now - steady_start_time); 181 | picked_window_id = target.window_id; 182 | fprintf(stderr, "%s picked window 0x%lx after %ld ms.\n", green_text("[payload x11 sanitizer]").c_str(), picked_window_id, duration.count()); 183 | now = now + STEADY_WAIT_TIME; // this cheats the following logic to skip the steady wait 184 | break; 185 | } 186 | } 187 | 188 | if (target_first_occurred && (now - steady_start_time) < STEADY_WAIT_TIME) { 189 | // wait for steady state 190 | continue; 191 | } 192 | 193 | if (target_first_occurred && (now - steady_start_time) >= STEADY_WAIT_TIME) { 194 | 195 | if (!picked_window_id) { 196 | fprintf(stderr, "%s", yellow_text("[payload x11 sanitizer] steady wait done. \n").c_str()); 197 | auto comparator = [](const CandidateWindowInfo& a, const CandidateWindowInfo& b) { 198 | int64_t area_a = static_cast(a.window_width) * a.window_height; 199 | int64_t area_b = static_cast(b.window_width) * b.window_height; 200 | return area_a > area_b; 201 | }; 202 | 203 | std::sort(targets.begin(), targets.end(), comparator); 204 | picked_window_id = targets[0].window_id; 205 | fprintf(stderr, "%s", yellow_text("[payload x11 sanitizer] picked window: 0x" + int_to_hexstr(picked_window_id) + "\n").c_str()); 206 | } 207 | 208 | if (!target_managed) { 209 | DEType de_type = get_current_de_type(); 210 | switch (de_type) { 211 | case DEType::GNOME: 212 | case DEType::KDE: 213 | case DEType::Unknown: 214 | // this unified strategy works well for most DEs 215 | XUnmapWindow(display, picked_window_id); 216 | break; 217 | case DEType::Hyprland: 218 | // FIXME: hyprland may need special workarounds 219 | XUnmapWindow(display, picked_window_id); 220 | break; 221 | } 222 | target_managed = true; 223 | break; 224 | } 225 | } 226 | } 227 | XCloseDisplay(display); 228 | } 229 | 230 | 231 | 232 | 233 | std::thread payload_start_portal_gio_mainloop_thread(){ 234 | auto& interface_singleton = InterfaceSingleton::getSingleton(); 235 | auto* portal_handle = interface_singleton.portal_handle.load(); 236 | // this thread is going to be trapped in the gio mainloop 237 | std::thread portal_gio_mainloop_thread = std::thread( 238 | [portal_handle](){ 239 | g_main_loop_run(portal_handle->gio_mainloop); 240 | } 241 | ); 242 | // wait until xdpsession is up 243 | // TODO: this CAN actually fail and trap the program... 244 | // TODO: but i think it's relatively safe to assume that the session will be up for now 245 | // TODO: eventually need to deal with this 246 | while(!portal_handle->session.load()){ 247 | std::this_thread::sleep_for(std::chrono::milliseconds(20)); 248 | } 249 | return std::move(portal_gio_mainloop_thread); 250 | } 251 | 252 | constexpr float PW_MAX_CALLRATE = 60.0; 253 | constexpr int PW_MIN_CALLTIME_MS = 1000 / PW_MAX_CALLRATE; 254 | 255 | std::thread payload_start_pipewire_thread(){ 256 | auto& interface_singleton = InterfaceSingleton::getSingleton(); 257 | auto* pipewire_handle = interface_singleton.pipewire_handle.load(); 258 | // this thread is going to be trapped in the pipewire mainloop 259 | std::thread pipewire_thread = std::thread( 260 | [](){ 261 | auto& interface_singleton = InterfaceSingleton::getSingleton(); 262 | while (interface_singleton.interface_handle.load()->pw_stop_flag.load() == false) { 263 | auto* pipewire_handle = interface_singleton.pipewire_handle.load(); 264 | pw_loop_iterate(pw_main_loop_get_loop(pipewire_handle->pw_mainloop), 0); 265 | std::this_thread::sleep_for(std::chrono::milliseconds(PW_MIN_CALLTIME_MS)); 266 | } 267 | fprintf(stderr, "%s", green_text("[payload] pw stop signal received. pw stopped. \n").c_str()); 268 | } 269 | ); 270 | fprintf(stderr, "%s", green_text("[payload] pipewire thread started.\n").c_str()); 271 | return std::move(pipewire_thread); 272 | } 273 | 274 | 275 | // this payload_main will be executed by the payload_thread, which is created in XShmAttachHook 276 | // and this thread will be detached immediately after creation 277 | void payload_main(){ 278 | 279 | 280 | auto& interface_singleton = InterfaceSingleton::getSingleton(); 281 | auto* portal_handle = interface_singleton.portal_handle.load(); 282 | 283 | // start the gio mainloop thread 284 | std::thread portal_gio_mainloop_thread = payload_start_portal_gio_mainloop_thread(); 285 | 286 | // start x11 sanitizer thread 287 | std::thread x11_sanitizer_thread = std::thread(x11_sanitizer_main); 288 | 289 | // start screencast session 290 | // this will get the pipewire fd into the portal object 291 | xdp_session_start( 292 | portal_handle->session.load(), NULL, NULL, 293 | XdpScreencastPortal::screencast_session_start_cb, 294 | portal_handle 295 | ); 296 | 297 | // wait until pipewire_fd is up 298 | while(portal_handle->status.load() == XdpScreencastPortalStatus::kInit ) { 299 | std::this_thread::sleep_for(std::chrono::milliseconds(20)); 300 | }; 301 | 302 | // screencast cancelled. we stop the gio mainloop and join the gio mainloop thread 303 | if (portal_handle->status.load() == XdpScreencastPortalStatus::kCancelled) { 304 | fprintf(stderr, "%s", red_text("[payload] screencast cancelled. stop gio and join gio thread. \n").c_str()); 305 | g_main_loop_quit(portal_handle->gio_mainloop); 306 | x11_sanitizer_thread.join(); 307 | portal_gio_mainloop_thread.join(); 308 | return; 309 | } 310 | 311 | while(portal_handle->pipewire_fd.load() == -1){ 312 | std::this_thread::sleep_for(std::chrono::milliseconds(20)); 313 | } 314 | fprintf(stderr, "%s", green_text("[payload SYNC] pipewire_fd acquired: " + std::to_string(portal_handle->pipewire_fd.load()) + "\n").c_str()); 315 | 316 | 317 | while(interface_singleton.pipewire_handle.load() == nullptr){ 318 | std::this_thread::sleep_for(std::chrono::milliseconds(20)); 319 | } 320 | fprintf(stderr, "%s", green_text("[payload SYNC] got pipewire_handle.\n").c_str()); 321 | 322 | // start the pipewire thread 323 | std::thread pipewire_thread = payload_start_pipewire_thread(); 324 | 325 | // we can join the sanitizer thread here since the pipewire thread is up 326 | // which means the user has successfully started the screenshare 327 | interface_singleton.interface_handle.load()->x11_sanitizer_stop_flag.store(true, std::memory_order_seq_cst); 328 | x11_sanitizer_thread.join(); 329 | fprintf(stderr, "%s", green_text("[payload SYNC] x11 sanitizer stopped.\n").c_str()); 330 | 331 | pipewire_thread.join(); 332 | interface_singleton.interface_handle.load()->payload_pw_stop_confirm.store(true, std::memory_order_seq_cst); 333 | fprintf(stderr, "%s", green_text("[payload SYNC] pw stop confirmed.\n").c_str()); 334 | 335 | portal_gio_mainloop_thread.join(); 336 | interface_singleton.interface_handle.load()->payload_gio_stop_confirm.store(true, std::memory_order_seq_cst); 337 | fprintf(stderr, "%s", green_text("[payload SYNC] gio stop confirmed.\n").c_str()); 338 | 339 | 340 | return; 341 | 342 | } 343 | -------------------------------------------------------------------------------- /resource/diagram.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /payload.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | #include "format.hpp" 20 | #include "interface.hpp" 21 | 22 | #include "helpers.hpp" 23 | 24 | enum class DEType { 25 | GNOME, 26 | KDE, 27 | Hyprland, // Hyprland does not support XDP_CURSOR_MODE_HIDDEN, dang it... 28 | Unknown // possibly wlr or some other magical DE 29 | }; 30 | 31 | inline DEType get_current_de_type(){ 32 | // get the DE type using envvar "XDG_SESSION_DESKTOP" 33 | char* xdg_session_desktop = std::getenv("XDG_SESSION_DESKTOP"); 34 | if (xdg_session_desktop == nullptr) { 35 | return DEType::Unknown; 36 | } 37 | std::string xdg_session_desktop_str = xdg_session_desktop; 38 | std::string xdg_session_desktop_lower = toLowerString(xdg_session_desktop_str); 39 | if (std::string(xdg_session_desktop) == "KDE") { 40 | return DEType::KDE; 41 | } else if (std::string(xdg_session_desktop) == "gnome") { 42 | return DEType::GNOME; 43 | } else if (xdg_session_desktop_lower == "hyprland") { 44 | return DEType::Hyprland; 45 | } 46 | return DEType::Unknown; 47 | } 48 | 49 | enum class SessionType { 50 | Wayland, 51 | X11, 52 | Unknown // heck, unless you did not set the envar properly... 53 | }; 54 | 55 | inline SessionType get_current_session_type(){ 56 | // get the current session type using envvar "XDG_SESSION_TYPE" 57 | char* xdg_session_type = std::getenv("XDG_SESSION_TYPE"); 58 | if (xdg_session_type == nullptr) { 59 | return SessionType::Unknown; 60 | } 61 | if (std::string(xdg_session_type) == "wayland") { 62 | return SessionType::Wayland; 63 | } else if (std::string(xdg_session_type) == "x11") { 64 | return SessionType::X11; 65 | } 66 | return SessionType::Unknown; 67 | } 68 | 69 | enum class XdpScreencastPortalStatus { 70 | kInit, 71 | kRunning, 72 | kCancelled, 73 | }; 74 | 75 | struct XdpScreencastPortal { 76 | 77 | using THIS_CLASS = XdpScreencastPortal; 78 | 79 | XdpScreencastPortal() { 80 | portal = xdp_portal_new(); 81 | XdpOutputType output_type = (XdpOutputType)(XdpOutputType::XDP_OUTPUT_MONITOR | XdpOutputType::XDP_OUTPUT_WINDOW); 82 | XdpScreencastFlags cast_flags = XdpScreencastFlags::XDP_SCREENCAST_FLAG_NONE; 83 | XdpCursorMode cursor_mode = get_current_session_type() == SessionType::Wayland ? 84 | XDP_CURSOR_MODE_EMBEDDED : 85 | XDP_CURSOR_MODE_HIDDEN; 86 | 87 | // hyprland cursor mode workaround. 88 | // as hyprland does not support XDP_CURSOR_MODE_HIDDEN, we simply use XDP_CURSOR_MODE_EMBEDDED for it 89 | if (get_current_de_type() == DEType::Hyprland) { 90 | cursor_mode = XDP_CURSOR_MODE_EMBEDDED; 91 | } 92 | XdpPersistMode persist_mode = XdpPersistMode::XDP_PERSIST_MODE_NONE; 93 | xdp_portal_create_screencast_session( 94 | portal, 95 | output_type, 96 | cast_flags, 97 | cursor_mode, 98 | persist_mode, 99 | NULL, 100 | NULL, 101 | THIS_CLASS::screencast_session_create_cb, 102 | this 103 | ); 104 | gio_mainloop = g_main_loop_new(NULL, FALSE); 105 | } 106 | 107 | 108 | ~XdpScreencastPortal() { 109 | if (session) xdp_session_close(session); 110 | if (session) g_object_unref(session); 111 | if (portal) g_object_unref(portal); 112 | if (gio_mainloop) g_main_loop_unref(gio_mainloop); 113 | } 114 | 115 | 116 | GMainLoop* gio_mainloop{nullptr}; 117 | XdpPortal* portal{nullptr}; 118 | std::atomic session{nullptr}; 119 | std::atomic pipewire_fd{-1}; 120 | std::atomic status{XdpScreencastPortalStatus::kInit}; 121 | std::vector pipewire_node_ids{}; 122 | 123 | static void screencast_session_create_cb( 124 | GObject* source_object, 125 | GAsyncResult* result, 126 | gpointer user_data 127 | ){ 128 | [[maybe_unused]] auto* this_ptr = reinterpret_cast(user_data); 129 | g_autoptr(GError) error = nullptr; 130 | this_ptr->session = xdp_portal_create_screencast_session_finish( 131 | XDP_PORTAL(source_object), 132 | result, 133 | &error 134 | ); 135 | if (!this_ptr->session) { 136 | g_print("Failed to create screencast session: %s\n", error->message); 137 | return; //TODO: handle error 138 | } 139 | 140 | } 141 | 142 | static void screencast_session_start_cb( 143 | GObject* source_object, 144 | GAsyncResult* result, 145 | gpointer user_data 146 | ){ 147 | [[maybe_unused]] auto* this_ptr = reinterpret_cast(user_data); 148 | GError *error = nullptr; 149 | if (!xdp_session_start_finish(XDP_SESSION(source_object), result, &error)) { 150 | g_warning("Failed to start screencast session: %s", error->message); 151 | g_error_free(error); 152 | this_ptr->status = XdpScreencastPortalStatus::kCancelled; 153 | return; 154 | } 155 | this_ptr->status.store(XdpScreencastPortalStatus::kRunning, std::memory_order_release); 156 | this_ptr->pipewire_fd = xdp_session_open_pipewire_remote(XDP_SESSION(source_object)); 157 | 158 | // get pipewire node ids 159 | // there is only one id as XDP_SCREENCAST_FLAG_NONE is chosen 160 | GVariant *streams = xdp_session_get_streams(XDP_SESSION(source_object)); 161 | GVariantIter *iter = g_variant_iter_new(streams); 162 | unsigned node_id; 163 | while (g_variant_iter_next(iter, "(ua{sv})", &node_id, NULL)) { 164 | this_ptr->pipewire_node_ids.push_back(node_id); 165 | fprintf(stderr, "%s\n", green_text("[hook] stream node_id: " + std::to_string(node_id)).c_str()); 166 | } 167 | g_variant_iter_free(iter); 168 | } 169 | 170 | }; 171 | 172 | 173 | 174 | struct PipewireScreenCast { 175 | using THIS_CLASS = PipewireScreenCast; 176 | 177 | PipewireScreenCast(int pw_fd, int pw_node_id, double target_framerate = 20.0, uint64_t reporting_interval = 20): 178 | node_id(pw_node_id), 179 | target_framerate(target_framerate), 180 | reporting_interval(reporting_interval), 181 | processed_frame_count(0) 182 | { 183 | reset_last_frame_time(); 184 | pw_init(nullptr, nullptr); 185 | pw_mainloop = pw_main_loop_new(nullptr); 186 | pw_loop* pw_mainloop_loop = pw_main_loop_get_loop(pw_mainloop); 187 | context = pw_context_new(pw_mainloop_loop, nullptr, 0); 188 | core = pw_context_connect_fd(context, pw_fd, nullptr, 0); 189 | registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, 0); 190 | 191 | spa_zero(registry_listener); 192 | pw_registry_add_listener(registry, ®istry_listener, ®istry_events, this); 193 | } 194 | 195 | void init(const char *serial) { 196 | pw_properties *props = pw_properties_new(PW_KEY_TARGET_OBJECT, serial, NULL); 197 | stream = pw_stream_new(core, "pipewire-portal-screencast", props); 198 | 199 | stream_events = { 200 | .version = PW_VERSION_STREAM_EVENTS, 201 | .state_changed = THIS_CLASS::on_stream_state_changed, 202 | .param_changed = THIS_CLASS::on_param_changed, 203 | .process = THIS_CLASS::on_process, 204 | }; 205 | pw_stream_add_listener(stream, &listener, &stream_events, this); 206 | 207 | // set up stream params 208 | this->param_buffer.reset(new uint8_t[param_buffer_size]); 209 | b = SPA_POD_BUILDER_INIT(param_buffer.get(), param_buffer_size); 210 | 211 | auto vidsize_default = SPA_RECTANGLE(320, 240); 212 | auto vidsize_min = SPA_RECTANGLE(1, 1); 213 | auto vidsize_max = SPA_RECTANGLE(DEFAULT_FB_ALLOC_WIDTH, DEFAULT_FB_ALLOC_HEIGHT); 214 | 215 | auto vidframerate_default = SPA_FRACTION(20, 1); 216 | auto vidframerate_min = SPA_FRACTION(0, 1); 217 | auto vidframerate_max = SPA_FRACTION(1000, 1); 218 | params[0] = reinterpret_cast(spa_pod_builder_add_object(&b, 219 | SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, 220 | SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_video), 221 | SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), 222 | SPA_FORMAT_VIDEO_format, SPA_POD_CHOICE_ENUM_Id(6, 223 | SPA_VIDEO_FORMAT_RGB, 224 | SPA_VIDEO_FORMAT_BGR, 225 | SPA_VIDEO_FORMAT_RGBA, 226 | SPA_VIDEO_FORMAT_BGRA, 227 | SPA_VIDEO_FORMAT_RGBx, 228 | SPA_VIDEO_FORMAT_BGRx 229 | ), 230 | SPA_FORMAT_VIDEO_size, SPA_POD_CHOICE_RANGE_Rectangle( 231 | &vidsize_default, 232 | &vidsize_min, 233 | &vidsize_max), 234 | SPA_FORMAT_VIDEO_framerate, SPA_POD_CHOICE_RANGE_Fraction( 235 | &vidframerate_default, 236 | &vidframerate_min, 237 | &vidframerate_max))); 238 | 239 | pw_stream_connect(stream, PW_DIRECTION_INPUT, PW_ID_ANY, pw_stream_flags(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), params, 1); 240 | } 241 | 242 | static void registry_global(void *data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) { 243 | THIS_CLASS *this_ptr = reinterpret_cast(data); 244 | 245 | if (id != this_ptr->node_id) 246 | return; 247 | 248 | const spa_dict_item *serial = spa_dict_lookup_item(props, PW_KEY_OBJECT_SERIAL); 249 | if (!serial || !serial->value) { 250 | fprintf(stderr, "%s stream %u has no serial\n", red_text("[hook]").c_str(), id); 251 | return; 252 | } 253 | 254 | this_ptr->init(serial->value); 255 | } 256 | 257 | static constexpr pw_registry_events registry_events{ PW_VERSION_REGISTRY_EVENTS, THIS_CLASS::registry_global }; 258 | 259 | ~PipewireScreenCast() { 260 | if (stream) pw_stream_disconnect(stream); 261 | if (stream) pw_stream_destroy(stream); 262 | if (core) pw_core_disconnect(core); 263 | if (context) pw_context_destroy(context); 264 | if (pw_mainloop) pw_main_loop_destroy(pw_mainloop); 265 | pw_deinit(); 266 | } 267 | 268 | void reset_last_frame_time() { 269 | // reset the last frame time to a very old time 270 | last_frame_time = std::chrono::high_resolution_clock::time_point( 271 | std::chrono::seconds(1) 272 | ); 273 | } 274 | 275 | std::atomic pw_mainloop{nullptr}; // need to be freed using pw_main_loop_destroy 276 | std::atomic context{nullptr}; // need to be freed using pw_context_destroy 277 | std::atomic core{nullptr}; // need to be freed using pw_core_disconnect 278 | std::atomic stream{nullptr}; // need to be freed using pw_stream_destroy 279 | 280 | private: 281 | int node_id; 282 | pw_registry *registry; 283 | spa_hook registry_listener; 284 | std::unique_ptr param_buffer{nullptr}; 285 | static constexpr size_t param_buffer_size = 1024; 286 | spa_pod_builder b; 287 | spa_hook listener; 288 | const spa_pod* params[1]; 289 | pw_stream_events stream_events; 290 | std::chrono::time_point last_frame_time; 291 | int counter{0}; 292 | double target_framerate; 293 | uint64_t reporting_interval; 294 | uint64_t processed_frame_count; 295 | 296 | struct ActualParams { 297 | uint32_t width{0}; 298 | uint32_t height{0}; 299 | double framerate{0.0f}; 300 | double max_framerate{0.0f}; 301 | SpaVideoFormat_e format{SpaVideoFormat_e::INVALID}; 302 | bool param_good{false}; 303 | 304 | void update_from_pod(const spa_pod* pod){ 305 | spa_video_info_raw info; 306 | auto retval = spa_format_video_raw_parse(pod, &info); 307 | fprintf(stderr, "%s", yellow_text("[payload pw] spa_format_video_raw_parse retval: " + std::to_string(retval) + "\n").c_str()); 308 | width = info.size.width; 309 | height = info.size.height; 310 | framerate = static_cast(info.framerate.num) / static_cast(info.framerate.denom); 311 | max_framerate = static_cast(info.max_framerate.num) / static_cast(info.max_framerate.denom); 312 | format = SpaVideoFormat_e{static_cast(info.format)}; 313 | param_good = (width > 0) && (height > 0) && (format != SpaVideoFormat_e::INVALID); 314 | 315 | std::string reporting_str = "width: " + std::to_string(width) + " | " + 316 | "height: " + std::to_string(height) + " | " + 317 | "framerate: " + std::to_string(framerate) + " | " + 318 | "max_framerate: " + std::to_string(max_framerate) + " | " + 319 | "format: " + spa_to_string(format) + " | " + 320 | "param_good: " + std::to_string(param_good); 321 | 322 | fprintf(stderr, "%s", yellow_text("[payload pw] actual params: " + reporting_str + "\n").c_str()); 323 | } 324 | 325 | } actual_params; 326 | 327 | 328 | static void on_stream_state_changed(void* data, pw_stream_state old_state, pw_stream_state state, const char* error_message){ 329 | std::string old_state_str = pw_stream_state_as_string(old_state); 330 | std::string state_str = pw_stream_state_as_string(state); 331 | fprintf(stderr, "%s", yellow_text("[payload pw] stream state changed from " + old_state_str + " to " + state_str + "\n").c_str()); 332 | THIS_CLASS* this_ptr = reinterpret_cast(data); 333 | this_ptr->reset_last_frame_time(); 334 | if (state == PW_STREAM_STATE_ERROR) { 335 | fprintf(stderr, "%s", red_text("[payload pw] stream error: " + std::string(error_message) + "\n").c_str()); 336 | } 337 | } 338 | 339 | static void on_param_changed(void* data, uint32_t id, const struct spa_pod* param){ 340 | 341 | THIS_CLASS* this_ptr = reinterpret_cast(data); 342 | this_ptr->reset_last_frame_time(); 343 | std::string param_id_name_str = spa_debug_type_find_name(spa_type_param, id); 344 | fprintf(stderr, "%s", yellow_text("[payload pw] param changed. received param type: " + param_id_name_str + "\n").c_str()); 345 | if (param == nullptr || id != SPA_PARAM_Format) { 346 | fprintf(stderr, "%s", yellow_text("[payload pw] ignoring non-format param\n").c_str()); 347 | return; 348 | } 349 | // we gather the actual video stream params here 350 | this_ptr->actual_params.update_from_pod(param); 351 | 352 | uint8_t params_buffer[1024]; 353 | struct spa_pod_builder b = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); 354 | const struct spa_pod *params[2]; 355 | 356 | int n_params = 0; 357 | params[n_params++] = (struct spa_pod *)(spa_pod_builder_add_object(&b, 358 | SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, 359 | SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoCrop), 360 | SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_region)) 361 | )); 362 | params[n_params++] = (struct spa_pod *)(spa_pod_builder_add_object(&b, 363 | SPA_TYPE_OBJECT_ParamMeta, SPA_PARAM_Meta, 364 | SPA_PARAM_META_type, SPA_POD_Id(SPA_META_VideoTransform), 365 | SPA_PARAM_META_size, SPA_POD_Int(sizeof(struct spa_meta_videotransform)) 366 | )); 367 | pw_stream_update_params(this_ptr->stream.load(), params, n_params); 368 | } 369 | 370 | static void on_process(void* data){ 371 | THIS_CLASS* this_ptr = reinterpret_cast(data); 372 | pw_buffer* b = pw_stream_dequeue_buffer(this_ptr->stream); 373 | if (b == nullptr) { 374 | fprintf(stderr, "%s", red_text("[payload pw] received a null buffer on processing. ignoring.\n").c_str()); 375 | pw_stream_queue_buffer(this_ptr->stream, b); 376 | return; 377 | } 378 | 379 | auto cur_frame_time = std::chrono::high_resolution_clock::now(); 380 | auto last_frame_time = this_ptr->last_frame_time; 381 | 382 | if (cur_frame_time - last_frame_time < std::chrono::milliseconds(int(1000 / this_ptr->target_framerate))) { 383 | // fprintf(stderr, "%s", yellow_text("[payload pw] frame came too fast. dropped.\n").c_str()); 384 | pw_stream_queue_buffer(this_ptr->stream, b); 385 | return; 386 | } 387 | 388 | // start processing frame 389 | this_ptr->processed_frame_count++; 390 | this_ptr->last_frame_time = cur_frame_time; 391 | 392 | if (this_ptr->processed_frame_count % this_ptr->reporting_interval == 0) { 393 | fprintf(stderr, "%s", yellow_text("[payload pw] processed frame count: " + std::to_string(this_ptr->processed_frame_count) + "\n").c_str()); 394 | } 395 | 396 | 397 | // try to write to the frame buffer if the param is good 398 | if (this_ptr->actual_params.param_good){ 399 | auto& interface_singleton = InterfaceSingleton::getSingleton(); 400 | auto& framebuffer = interface_singleton.interface_handle.load()->framebuf; 401 | 402 | struct spa_meta_region *video_crop; 403 | if ((video_crop = (struct spa_meta_region *)spa_buffer_find_meta_data(b->buffer, SPA_META_VideoCrop, sizeof(*video_crop))) && spa_meta_region_is_valid(video_crop)) { 404 | framebuffer.crop_x = video_crop->region.position.x; 405 | framebuffer.crop_y = video_crop->region.position.y; 406 | framebuffer.crop_width = video_crop->region.size.width; 407 | framebuffer.crop_height = video_crop->region.size.height; 408 | } else { 409 | framebuffer.crop_width = framebuffer.crop_height = 0; 410 | } 411 | 412 | framebuffer.update_param( 413 | this_ptr->actual_params.height, 414 | this_ptr->actual_params.width, 415 | this_ptr->actual_params.format 416 | ); 417 | 418 | struct spa_meta_videotransform *video_transform; 419 | if ((video_transform = (struct spa_meta_videotransform *)spa_buffer_find_meta_data(b->buffer, SPA_META_VideoTransform, sizeof(*video_transform)))) { 420 | switch (video_transform->transform) { 421 | case SPA_META_TRANSFORMATION_None: 422 | case SPA_META_TRANSFORMATION_Flipped: 423 | framebuffer.rotate = 0; 424 | break; 425 | case SPA_META_TRANSFORMATION_90: 426 | case SPA_META_TRANSFORMATION_Flipped90: 427 | framebuffer.rotate = 90; 428 | break; 429 | case SPA_META_TRANSFORMATION_180: 430 | case SPA_META_TRANSFORMATION_Flipped180: 431 | framebuffer.rotate = 180; 432 | break; 433 | case SPA_META_TRANSFORMATION_270: 434 | case SPA_META_TRANSFORMATION_Flipped270: 435 | framebuffer.rotate = -90; 436 | break; 437 | default: 438 | break; 439 | } 440 | switch (video_transform->transform) { 441 | case SPA_META_TRANSFORMATION_None: 442 | case SPA_META_TRANSFORMATION_90: 443 | case SPA_META_TRANSFORMATION_180: 444 | case SPA_META_TRANSFORMATION_270: 445 | framebuffer.flip = 0; 446 | break; 447 | case SPA_META_TRANSFORMATION_Flipped: 448 | case SPA_META_TRANSFORMATION_Flipped90: 449 | case SPA_META_TRANSFORMATION_Flipped180: 450 | case SPA_META_TRANSFORMATION_Flipped270: 451 | framebuffer.flip = 1; 452 | break; 453 | default: 454 | break; 455 | } 456 | } else { 457 | framebuffer.rotate = framebuffer.flip = 0; 458 | } 459 | 460 | // copy the data from the pw buffer to the frame buffer 461 | uint8_t* pw_chunk_ptr = reinterpret_cast( b->buffer->datas[0].data); 462 | uint32_t pw_chunk_stride = b->buffer->datas[0].chunk->stride; 463 | uint32_t pw_chunk_offset = b->buffer->datas[0].chunk->offset % b->buffer->datas[0].maxsize; 464 | pw_chunk_ptr += pw_chunk_offset; 465 | 466 | for (int row_idx = 0; row_idx < framebuffer.height; ++row_idx) { 467 | uint8_t* framebuffer_row_start = framebuffer.data.get() + row_idx * framebuffer.row_byte_stride; 468 | uint8_t* pw_chunk_row_start = pw_chunk_ptr + row_idx * pw_chunk_stride ; 469 | memcpy(framebuffer_row_start, pw_chunk_row_start, pw_chunk_stride); 470 | } 471 | } 472 | 473 | 474 | exit: 475 | pw_stream_queue_buffer(this_ptr->stream, b); 476 | return; 477 | } 478 | 479 | 480 | }; 481 | 482 | 483 | void payload_main(); 484 | --------------------------------------------------------------------------------