第三十三章 后端的鼠标键盘等事件实现

基础情况

在流式系统中,大部分的情况,数据源是从后端准备好后推送到前端的,此时大部分的情形是前端被动的显示后端流化传过来的内容。有一些场景,在前端需要接受用户的操作,从而影响后端的数据内容和表现,此时大部分的行为将通过传统的键盘,鼠标来进行操作或者交互内容。

之前Web前端部分介绍过了,在前端如何处理这些键盘鼠标事件,后端需要针对这些数据,格式的传入进行解析后,在云服务器上真实操作,从而产生实际的效果作用。之后产生的数据才是准确的内容。

代码讲解

首先来看下头文件中的主要函数内容和作用

#pragma once

#include <myjsoncpp/json/json.h>

namespace QL
{
        class QLKeyboardAndMouse
        {

        public:
                QLKeyboardAndMouse(int screenWidth,int screenHeight);
                ~QLKeyboardAndMouse();

                void ProcessKeyboardAndMouseMessage(const uint8_t* data, uint32_t len, bool binary);
                void initKeys();

                void SetMainWindowForOffset(long uiHander);
                void GetUIOffset();

        protected:
                void codeConversion(Json::Value& json);
                void combinationKey(const Json::Value& json);

        private:
                // 键盘
                void OnKeyDwon(const Json::Value& msg);
                void OnKeyUp(const Json::Value& json);
                // 鼠标
                void OnMouseDown(const Json::Value& json);
                void OnMouseUp(const Json::Value& json);
                // 鼠标移动
                void OnMouseMove(const Json::Value& json);
                // 鼠标滑轮
                void OnMouseWheel(const Json::Value& json);

        private:
                bool mIsCtrlPressed{ false };
                bool mIsShiftPressed{ false };
                bool mIsAltPressed{ false };
                bool meta_{ false };

                //屏幕宽度,外部传入,内部维护
                int mScreenWidth;
                //屏幕高度,外部传入,内部维护
                int mScreenHeight;

                //主窗体针对整个屏幕的偏移
                long mMainUIHandler;
                bool isInProcessMode; //是否在进程模式,此时发送鼠标键盘事件到进程主窗体
                //进程内主窗体在整个屏幕空间的左上角偏移。计算鼠标位置时做位置计算用。
                float mWinOffsetX;
                float mWinOffsetY;

        };

}

主要的行为有:

  1. 当整体的流式运转之后,针对一个用户Session的情况下,我们进行全局键盘和鼠标事件类对象的挂载。从而响应对应的事件,你也可以在其余的位置挂载,因为不同的系统不同的设计。我们目前青龙流式在全局挂载,是因为就在一个全局位置开启和关闭。
  2. 如果是多Session的情况下,因为要模拟不同用户的Session情况,不同的Session对应不同的键盘和鼠标,全局挂载一个是不行的。青龙系统曾经尝试支持这种情形,但后来发现这种基于软件Session的隔离机制,对于软件产品系统层级过于复杂了,这个后续倾向于让操作系统层面或对等软件层面去解决。比如通过Docker,VM等机制完全的隔离较好。当然甚至可以参考XRDPServer等。
  3. 单独考虑单用户Session的情况下,主要的函数有键盘的KeyPress, KeyDown , KeyUp,MouseDown, MouseMove, MouseUp 。这些函数的回调,或者Call都会根据不同的OS实现一份。 在青龙流式的系统中,我们基本上实现了Windows, Linux 2个平台。
  4. 考虑到组合按键的情况,特定用CombineKey函数进行处理。

约定键盘映射

在Web和操作系统层面都有对于不同按键的映射,这个需要以某种约定的方式进行数据传递。

我们目前通过Json格式的字符串形式传递这种内容。

在Web端的定义和实现请参考前篇,以下是在后端的代码中,针对Linux平台和Windows平台的不同映射代码参考。

Mac部分请自行补充。

#if defined(Q_OS_WIN)
        std::map<std::string, int> win2key = {
                {"Escape", 27},
                {"F1", 112},
                {"F2", 113},
                {"F3", 114},
                {"F4", 115},
                {"F5", 116},
                {"F6", 117},
                {"F7", 118},
                {"F8", 119},
                {"F9", 120},
                {"F10", 121},
                {"F11", 122},
                {"F12", 123},
                {"Pause", 19}, /*{"PrintScreen", 44},*/
                {"Delete", 46},
                {"`", 192},
                {"0", 48},
                {"1", 49},
                {"2", 50},
                {"3", 51},
                {"4", 52},
                {"5", 53},
                {"6", 54},
                {"7", 55},
                {"8", 56},
                {"9", 57},
                {"-", 189},
                {"=", 187},
                {"Backspace", 8},
                {"Tab", 9},
                {"q", 81},
                {"w", 87},
                {"e", 69},
                {"r", 82},
                {"t", 84},
                {"y", 89},
                {"u", 85},
                {"i", 73},
                {"o", 79},
                {"p", 80},
                {"[", 219},
                {"]", 221},
                {"\\", 220},
                {"CapsLock", 20},
                {"a", 65},
                {"s", 83},
                {"d", 68},
                {"f", 70},
                {"g", 71},
                {"h", 72},
                {"j", 74},
                {"k", 75},
                {"l", 76},
                {";", 186},
                {"'", 222},
                {"Enter", 13},
                {"Shift", 16},
                {"z", 90},
                {"x", 88},
                {"c", 67},
                {"v", 86},
                {"b", 66},
                {"n", 78},
                {"m", 77},
                {",", 188},
                {".", 190},
                {"/", 191},
                {"Control", 17},
                {"Meta", 91},
                {"Alt", 18},
                {" ", 32},
                {"ContextMenu", 93},
                {"ArrowLeft", 37},
                {"ArrowUp", 38},
                {"ArrowRight", 39},
                {"ArrowDown", 40},
        };
#elif defined(Q_OS_LINUX)
        std::map<std::string, int> linux2key = {
                {"Escape", XK_Escape},
                {"F1", XK_F1},
                {"F2", XK_F2},
                {"F3", XK_F3},
                {"F4", XK_F4},
                {"F5", XK_F5},
                {"F6", XK_F6},
                {"F7", XK_F7},
                {"F8", XK_F8},
                {"F9", XK_F9},
                {"F10", XK_F10},
                {"F11", XK_F11},
                {"F12", XK_F12},
                {"Pause", XK_Pause}, /*{"PrintScreen", 44},*/
                {"Delete", XK_Delete},
                {"`", XK_grave},
                {"0", XK_0},
                {"1", XK_1},
                {"2", XK_2},
                {"3", XK_3},
                {"4", XK_4},
                {"5", XK_5},
                {"6", XK_6},
                {"7", XK_7},
                {"8", XK_8},
                {"9", XK_9},
                {"-", XK_minus},
                {"=", XK_equal},
                {"Backspace", XK_BackSpace},
                {"Tab", XK_Tab},
                {"q", XK_Q},
                {"w", XK_W},
                {"e", XK_E},
                {"r", XK_R},
                {"t", XK_T},
                {"y", XK_Y},
                {"u", XK_U},
                {"i", XK_I},
                {"o", XK_O},
                {"p", XK_P},
                {"[", XK_bracketleft},
                {"]", XK_bracketright},
                {"\\", XK_backslash},
                {"CapsLock", XK_Caps_Lock},
                {"a", XK_A},
                {"s", XK_S},
                {"d", XK_D},
                {"f", XK_F},
                {"g", XK_G},
                {"h", XK_H},
                {"j", XK_J},
                {"k", XK_K},
                {"l", XK_L},
                {";", XK_semicolon},
                {"'", XK_quotedbl},
                {"Enter", XK_Return},
                {"Shift", XK_Shift_L},
                {"z", XK_Z},
                {"x", XK_X},
                {"c", XK_C},
                {"v", XK_V},
                {"b", XK_B},
                {"n", XK_N},
                {"m", XK_M},
                {",", XK_comma},
                {".", XK_period},
                {"/", XK_slash},
                {"Control", XK_Control_L},
                {"Meta", XK_Meta_L},
                {"Alt", XK_Alt_L},
                {" ", XK_space},
                {"ContextMenu", 93},
                {"ArrowLeft", XK_Left},
                {"ArrowUp", XK_Up},
                {"ArrowRight", XK_Right},
                {"ArrowDown", XK_Down},
        };

键盘按下演示

以下的代码,我们来演示,如何模拟键盘按下的技术实现

  1. 我们从DataChannel中拿到前端传过来的数据(这部分不在以下的代码中)
  2. 拿到string类型的整体数据内容后,通过Json解析器,拿到某些Key对应的内容。
  3. 拿到键盘按键数据(或者后续的鼠标行为和位置信息),调用平台的API进行行为操作。比如Windows平台上的SendMessage 函数。 我们的青龙系统是这么做的,但是Windows平台调用和操作键盘鼠标的API还有很多,你可以根据自己项目的需要进行修改。

比如我们这里有一个根据进行隔离的行为操作,和不是这种情况下的API调用是不同的。

        /// <summary>
        /// 處理鍵盤數據
        /// </summary>
        /// <param name="json"></param>
        void QLKeyboardAndMouse::OnKeyDwon(const Json::Value& json)
        {
                //直接從Json獲取微軟定義的鍵盤的虛擬Codes
                int keyCode = JSON_INT(json, "keyCode");
                bool ctrl = JSON_BOOL(json, "ctrlKey");
                bool shift = JSON_BOOL(json, "shiftKey");
                bool alt = JSON_BOOL(json, "altKey");

                 QL_LOG("Key down. Pressed %d. 0x:%x .Value:%s . Shift:%d,Ctrl:%d,Alt:%d.", keyCode, keyCode, key2win[keyCode].c_str(), shift, ctrl, alt);
                 combinationKey(json);
#ifdef Q_OS_WIN
                //进程模式下发送到指定主窗口接受
                //这里有个问题,这个可能不是向目标窗口发送消息,而是该控件的句柄。因此如果不是最终的输入框,可能无法正常接收到
                //
                if (isInProcessMode)
                {
                        SendMessage((HWND)mMainUIHandler, WM_KEYDOWN, keyCode, 0); // 按下'A'键
                }
                else
                {
                        //这种是发送全局的键盘消息。
                        //根據以下2個文檔的規範定義,處理鍵盤事件
                        // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-keybd_event
                        // https://developer.mozilla.org/zh-CN/docs/Web/API/KeyboardEvent/code#code_values
                        // https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
                        keybd_event(keyCode, 0, 0, 0);
                }
#endif // Q_OS_WIN
        }

多显示器下情况

我们有时候需要支持双显示屏,甚至更多显示器。这里非常有意思。

有一些细节,让我们来让大家避坑下。

以下的主要代码中,streamingVideo2代表从第二个显示器传递过来的鼠标位置数据,streamingVideo1则是第一个。如果有第三个我想你应该知道如何扩展。

这里我们假定 第二块屏幕在右侧,如果是其余方式的,另外增加代码逻辑处理。


                //這樣計算的原理是
                //前端給過來絕對位置值/屏幕實際寬度 ==== 鼠標應該在的位置/65536的屏幕相對值。
                //因此,API調用的鼠標位置,永遠是相對的65536的值範圍內。
                //而前端傳遞的絕對值範圍在0~屏幕寬度
                if (vid == "streamingVideo2")
                {
                        //具体的计算逻辑参考 https://ewiki.51arena.com/pages/viewpage.action?pageId=49678550
                        //因为我们目前假定第二块屏幕在右侧,如果是其余方式的,另外增加代码逻辑处理。
                        mouseFinalPosX = x * SCREEN_VIRTUAL_SPACE_SIZE / mScreenWidth + SCREEN_VIRTUAL_SPACE_SIZE;
                        mouseFinalPosY = y * SCREEN_VIRTUAL_SPACE_SIZE / mScreenHeight;
                }
                else
                {
                        mouseFinalPosX = x * SCREEN_VIRTUAL_SPACE_SIZE / mScreenWidth;
                        mouseFinalPosY = y * SCREEN_VIRTUAL_SPACE_SIZE / mScreenHeight;
                }
                
                if (isInProcessMode)
                {
                        // 模拟鼠标移动到新计算出的位置
                        LPARAM lParam = MAKELPARAM(mouseFinalPosX, mouseFinalPosY);
                        SendMessage((HWND)mMainUIHandler, WM_MOUSEMOVE, 0, lParam);
                }
                else
                {
                        // 坐标 (0,0) 映射到显示表面的左上角,(65535,65535) 映射到右下角
                        //MOUSEEVENTF_ABSOLUTE 表示我們使用絕對位置,不使用dxdy這種相對於當前鼠標位置的方式
                        mouse_event(MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE, mouseFinalPosX, mouseFinalPosY, 0, 0);
                        //沒修改坐標計算方法前這個函數運行OK ---> 直接使用X和Y的情况是前端直接传的是虚拟量化值。
                        //mouse_event(MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE, x, y, 0, 0);
                }

主要的理论细节是:

服务器端侧理论知识(windows平台)

一、Windows中接入多个显示器时,可设置为复制和扩展屏。

1、设置为复制屏幕时,多个显示器的分辨率是一样的,位置为0~分辨率值

2、设置为扩展屏幕时,显示器之间的关系比较复杂些。首先Windows系统会识别一个主显示器,这个可以在屏幕分辨率中更改。多个显示器之间的位置关系也可以在屏幕分辨率中更改。

其中主显示器的位置为(0,0)到(width,height),其他显示器位置由与主显示器的位置关系决定,在主显示器左上,则为负数,用0减去长宽;在右下,则由主显示器的分辨率加上长宽。

其中驱动或用mouse_event处理时也是一样,主显示器为0~65535,其他显示器根据主显示器的相对位置确定,屏幕范围也是从0~65535

举个例子:

2个显示器,一个左,一个右。假定左侧是主显示器,则右侧的屏幕尺寸范围是左上角 (65535, 0) ,右下角 (65535+65535, 65535) . 这个65535是虚拟空间值。就代表了无论什么尺寸和分辨率下代表了一整块屏幕。

这个计算方法在青龙流式系统中验证OK。

Web侧理论知识

在create offer时,默认可以设置多个track。在peerclient确认后webrtc才能处理对应的onTrack事件。

注意 其实answer offer时也可以改变track,在请求端发起后应答也可以协商然后发起,效果一样。优先使用发起端设置track数量,这样比较符合实际需求

Web鼠标移动计算逻辑

因为是等比例缩放,所以屏幕

videoElement.videoWidth/videoElement.clientWidth = videoElement.videoHeight/videoElement.clientHeight

所以鼠标的x,y坐标都是等比例缩放的。Web传递到后端都是坐标都是真实分辨率。

RA/SD 衍生者AI训练营。发布者:chris,转载请注明出处:https://www.shxcj.com/archives/6600

Like (0)
Previous 2024-09-30
Next 2024-09-30

相关推荐

发表回复

Please Login to Comment
本文授权以下站点有原版访问授权 https://www.shxcj.com https://www.2img.ai https://www.2video.cn