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

Posted by Ramendeus on June 2, 2024

访问 2img.ai 官网以获取更多AI/AIGC信息 访问 个人技术博客: fuqifai.github.io

本文大部分配图使用微信小程序【字形绘梦】免费生成。

AIGC技术讨论群

基础情况

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

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

代码讲解

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

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
#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部分请自行补充。

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
#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调用是不同的。

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
        /// <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则是第一个。如果有第三个我想你应该知道如何扩展。

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

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
                //這樣計算的原理是
                //前端給過來絕對位置值/屏幕實際寬度 ==== 鼠標應該在的位置/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传递到后端都是坐标都是真实分辨率。