基础情况
在流式系统中,大部分的情况,数据源是从后端准备好后推送到前端的,此时大部分的情形是前端被动的显示后端流化传过来的内容。有一些场景,在前端需要接受用户的操作,从而影响后端的数据内容和表现,此时大部分的行为将通过传统的键盘,鼠标来进行操作或者交互内容。
本文中的截图或者说配图都是用微信小程序【字形绘梦】制作,谢谢该软件的免费支持。
之前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; }; }
主要的行为有:
- 当整体的流式运转之后,针对一个用户Session的情况下,我们进行全局键盘和鼠标事件类对象的挂载。从而响应对应的事件,你也可以在其余的位置挂载,因为不同的系统不同的设计。我们目前青龙流式在全局挂载,是因为就在一个全局位置开启和关闭。
- 如果是多Session的情况下,因为要模拟不同用户的Session情况,不同的Session对应不同的键盘和鼠标,全局挂载一个是不行的。青龙系统曾经尝试支持这种情形,但后来发现这种基于软件Session的隔离机制,对于软件产品系统层级过于复杂了,这个后续倾向于让操作系统层面或对等软件层面去解决。比如通过Docker,VM等机制完全的隔离较好。当然甚至可以参考XRDPServer等。
- 单独考虑单用户Session的情况下,主要的函数有键盘的KeyPress, KeyDown , KeyUp,MouseDown, MouseMove, MouseUp 。这些函数的回调,或者Call都会根据不同的OS实现一份。 在青龙流式的系统中,我们基本上实现了Windows, Linux 2个平台。
- 考虑到组合按键的情况,特定用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}, };
键盘按下演示
以下的代码,我们来演示,如何模拟键盘按下的技术实现
- 我们从DataChannel中拿到前端传过来的数据(这部分不在以下的代码中)
- 拿到string类型的整体数据内容后,通过Json解析器,拿到某些Key对应的内容。
- 拿到键盘按键数据(或者后续的鼠标行为和位置信息),调用平台的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传递到后端都是坐标都是真实分辨率。