基础情况
在流式系统中,大部分的情况,数据源是从后端准备好后推送到前端的,此时大部分的情形是前端被动的显示后端流化传过来的内容。有一些场景,在前端需要接受用户的操作,从而影响后端的数据内容和表现,此时大部分的行为将通过传统的键盘,鼠标来进行操作或者交互内容。
之前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传递到后端都是坐标都是真实分辨率。
RA/SD 衍生者AI训练营。发布者:chris,转载请注明出处:https://www.shxcj.com/archives/6600