OpenBMC开发之obmc-ikvm代码架构
1. obmc-ikmv 文件创建了 ikvm::Args 和 ikvm::Manager 两个实例,然后运行管理器 manager.run()
int main(int argc, char* argv[])
{
// 解析命令行参数
ikvm::Args args(argc, argv);
// 创建管理器实例,传入参数
ikvm::Manager manager(args);
// 运行管理器
manager.run();
// 主函数返回值,表示程序正常退出
return 0;
}
2. ikvm_args 文件
2.1 头文件
界说了 struct CommandLine、frameRate、subsampling、udcName、videoPath、calcFrameCRC、commandLine 等几个变量
namespace ikvm
{
class Args
{
public:
struct CommandLine
{
CommandLine(int c, char** v) : argc(c), argv(v) {}
int argc;
char** argv;
};
Args(int argc, char* argv[]);
private:
void printUsage();
int frameRate;
/* @brief Desired subsampling (0: 444, 1: 420) */
int subsampling;
/* @brief Path to the USB keyboard device */
std::string keyboardPath;
/* @brief Path to the USB mouse device */
std::string pointerPath;
/* @brief Name of UDC */
std::string udcName;
/* @brief Path to the V4L2 video device */
std::string videoPath;
/* @brief Identical frames detection */
bool calcFrameCRC;
/* @brief Original command line arguments passed to the application */
CommandLine commandLine;
};
} // namespace ikvm
2.2 源文件
mian 函数中创建对象时:ikvm::Args args(argc, argv)
frameRate = 30,subsampling = 0,calcFrameCRC = false,argc 和 argv 存入 struct commandLine
struct option 为标准结构体界说,4 个数据界说分别为:长选项名称、是否必要参数、标志、短选项名称
getopt_long 的用法:分标志是否即是 0 两种环境,可见下面代码解释,再详细参考 AI 代码解释
Args::Args(int argc, char* argv[]) :
frameRate(30), subsampling(0), calcFrameCRC{false}, commandLine(argc, argv)
{
int option;
const char* opts = "f:s:h:k:p:u:v:c";
struct option lopts[] = { // 长选项名称、是否需要参数、标志、短选项名称
{"frameRate", 1, 0, 'f'}, // 设置帧率的选项
{"subsampling", 1, 0, 's'}, // 设置子采样的选项:1 = YUV420, 0 = YUV444
{"help", 0, 0, 'h'}, // 帮助选项
{"keyboard", 1, 0, 'k'}, // 指定键盘设备路径的选项:/dev/hidg0
{"mouse", 1, 0, 'p'}, // 指定鼠标设备路径的选项:/dev/hidg1
{"udcName", 1, 0, 'u'}, // 指定UDC名称的选项:1e6a0000.usb-vhub:pX
{"videoDevice", 1, 0, 'v'}, // 指定视频设备路径的选项:/dev/video0
{"calcCRC", 0, 0, 'c'}, // 启用CRC计算的选项
{0, 0, 0, 0}}; // 选项结束
// /usr/bin/obmc-ikvm -v /dev/video0 -k /dev/hidg0 -p /dev/hidg1
// 当标志Buff=0时,option返回值为Buff【3】的值,当flag=1时,option返回值为Buff【2】的值
while ((option = getopt_long(argc, argv, opts, lopts, NULL)) != -1)
{
switch (option)
{
case 'f': //optarg是getopt_long函数解析到的参数值,是 <getopt.h> 头文件中定义的一个 extern char *optarg 变量
frameRate = (int)strtol(optarg, NULL, 0);// 解析帧率
if (frameRate < 0 || frameRate > 60) // 验证帧率范围
frameRate = 30; // 如果超出范围则重置为默认值
break;
case 's':
subsampling = (int)strtol(optarg, NULL, 0);// 解析子采样
if (subsampling < 0 || subsampling > 1) // 验证子采样范围
subsampling = 0; // 如果超出范围则重置为默认值
break;
case 'h':
printUsage();// 打印使用信息
exit(0); // 打印帮助信息后退出程序
case 'k':
keyboardPath = std::string(optarg);// 设置键盘设备路径
break;
case 'p':
pointerPath = std::string(optarg); // 设置鼠标设备路径
break;
case 'u':
udcName = std::string(optarg); // 设置UDC名称
break;
case 'v':
videoPath = std::string(optarg); // 设置视频设备路径
break;
case 'c':
calcFrameCRC = true; // 启用CRC计算
break;
}
}
}
3. ikvm_manager 文件
3.1 头文件
界说的几个变量和 **input、video、server **对象如下所示,必要注意的地方有:
explicit Manager(const Args& args) 关键字,用于进步代码安全性,防止隐式转换,主要应用场景为单参数构造函数中,例如:
Args args;
Manager m(args); //表现调用构造函数,精确
**Manager m = args; **//隐式调用构造函数,错误,详细来说,如果一个构造函数只有一个参数,编译器大概会自动将该参数范例隐式转换为当前类的范例
namespace ikvm
{
class Manager
{
public:
explicit Manager(const Args& args);
~Manager() = default;
Manager(const Manager&) = default;
Manager& operator=(const Manager&) = default;
Manager(Manager&&) = default;
Manager& operator=(Manager&&) = default;
/* @brief Begins operation of the VNC server */
void run();
private:
static void serverThread(Manager* manager);
/* @brief Notifies thread waiters that RFB operations are complete */
void setServerDone();
/* @brief Notifies thread waiters that video operations are complete */
void setVideoDone();
/* @brief Blocks until RFB operations complete */
void waitServer();
/* @brief Blocks until video operations are complete */
void waitVideo();
bool continueExecuting;
/* @brief Boolean to indicate that RFB operations are complete */
bool serverDone;
/* @brief Boolean to indicate that video operations are complete */
bool videoDone;
/* @brief Input object */
Input input;
/* @brief Video object */
Video video;
/* @brief RFB server object */
Server server;
/* @brief Condition variable to enable waiting for thread completion */
std::condition_variable sync;
/* @brief Mutex for waiting on condition variable safely */
std::mutex lock;
};
} // namespace ikvm
3.2 源文件
创建 Manager 对象:input、video、server 对象初始化
continueExecuting = true,serverDone = false,videoDone = true
假设实行: ExecStart=/usr/bin/obmc-ikvm -v /dev/video0 -k /dev/hidg0 -p /dev/hidg1
getKeyboardPath = /dev/hidg0
getPointerPath = /dev/hidg1
getVideoPath = /dev/video0
Manager::Manager(const Args& args) :
continueExecuting(true), serverDone(false), videoDone(true),
input(args.getKeyboardPath(), args.getPointerPath(), args.getUdcName()),
video(args.getVideoPath(), input, args.getFrameRate(),
args.getSubsampling()),
server(args, input, video)
{}
实行管理器 Manager.run()
continueExecuting 初始化时赋值 true
server.wantsFrame()来源于 ikvm_server.hpp 中的成员函数inline bool wantsFrame() const{return server->clientHead;},只要有 kvm 会话,该值就不为 NULL
void Manager::run()
{
// 启动服务器线程,执行 serverThread 函数
std::thread run(serverThread, this);
// 主循环,持续运行直到 continueExecuting 为 false
while (continueExecuting)
{
// 检查服务器是否需要新的帧
if (server.wantsFrame())
{
// 启动视频设备
video.start();
// 获取视频帧
video.getFrame();
// 发送帧到服务器
server.sendFrame();
}
else
{
// 如果不需要帧,则停止视频设备
video.stop();
}
// 检查视频是否需要调整分辨率
if (video.needsResize())
{
// 等待服务器线程完成当前操作
waitServer();
// 标记视频操作未完成
videoDone = false;
// 调整视频分辨率
video.resize();
// 调整服务器分辨率
server.resize();
// 标记视频操作完成,并通知等待的线程
setVideoDone();
}
else
{
// 标记视频操作完成,并通知等待的线程
setVideoDone();
// 等待服务器线程完成当前操作
waitServer();
}
}
// 等待服务器线程结束
run.join();
}
**创建线程 serverThread(),实行 ****server.run()**函数,该 run 函数比力紧张,是调用 libvncserver 库rfbProcessEvents()处理函数的开始
void Manager::serverThread(Manager* manager)
{
while (manager->continueExecuting)
{
manager->server.run();
manager->setServerDone();
manager->waitVideo();
}
}
4. ikvm_server 文件
4.1 头文件
Input& input;Video& video;是引用范例的成员变量,通过server(args, input, video)传递
namespace ikvm
{
class Server
{
public:
struct ClientData
{
ClientData(int s, Input* i) : skipFrame(s), input(i), last_crc{-1}
{
needUpdate = false;
}
int skipFrame;
Input* input;
bool needUpdate;
int64_t last_crc;
};
Server(const Args& args, Input& i, Video& v);
~Server();
Server(const Server&) = default;
Server& operator=(const Server&) = default;
Server(Server&&) = default;
Server& operator=(Server&&) = default;
void resize();
void run();
void sendFrame();
inline bool wantsFrame() const
{
return server->clientHead;
}
inline const Video& getVideo() const
{
return video;
}
private:
static void clientFramebufferUpdateRequest(
rfbClientPtr cl, rfbFramebufferUpdateRequestMsg* furMsg);
static void clientGone(rfbClientPtr cl);
static enum rfbNewClientAction newClient(rfbClientPtr cl);
void doResize();
bool pendingResize;
int frameCounter;
unsigned int numClients;
long int processTime;
rfbScreenInfoPtr server;
Input& input; //创建引用,引用ikvm_manager.hpp中的定义
Video& video; //创建引用,引用ikvm_manager.hpp中的定义
std::vector<char> framebuffer;
bool calcFrameCRC;
static constexpr int cursorWidth = 20;
static constexpr int cursorHeight = 20;
};
} // namespace ikvm
4.2 源文件
ikvm_manager.hpp 中创建对象 Server,ikvm_manager.cpp 中 Manager 对象构造的时间初始化 server(args, input, video)
分辨率调整 pendingResize = false,帧数 frameCounter = 0, 会话数 numClients = 0
通过 rfbScreenInfoPtr server = rfbGetScreen( )获取紧张的屏幕缓冲结构体,该结构体是唯一的,在创建 Server 对象时创建,后面会通过 Server::run()函数中的rfbProcessEvents()与 libvncserver 创建关联,此处关系可详见<<OpenBMC开发之obmc-ikvm与libvncserver的连理关系>>
Server::Server(const Args& args, Input& i, Video& v) :
pendingResize(false), frameCounter(0), numClients(0), input(i), video(v)
{
std::string ip("localhost");
const Args::CommandLine& commandLine = args.getCommandLine();
int argc = commandLine.argc;
// ikvm_server.hpp: rfbScreenInfoPtr server
server = rfbGetScreen(&argc, commandLine.argv, video.getWidth(),
video.getHeight(), Video::bitsPerSample,
Video::samplesPerPixel, Video::bytesPerPixel);
if (!server)
{
log<level::ERR>("Failed to get VNC screen due to invalid arguments");
elog<InvalidArgument>(
xyz::openbmc_project::Common::InvalidArgument::ARGUMENT_NAME(""),
xyz::openbmc_project::Common::InvalidArgument::ARGUMENT_VALUE(""));
}
framebuffer.resize(
video.getHeight() * video.getWidth() * Video::bytesPerPixel, 0);
server->screenData = this; //Server对象
server->desktopName = "OpenBMC IKVM";
server->frameBuffer = framebuffer.data(); //数据缓冲区
server->newClientHook = newClient; //创建新连接时的回调函数,下面详细分析
server->cursor = rfbMakeXCursor(cursorWidth, cursorHeight, (char*)cursor,
(char*)cursorMask);
server->cursor->xhot = 1;
server->cursor->yhot = 1;
rfbStringToAddr(&ip, &server->listenInterface);
rfbInitServer(server);
rfbMarkRectAsModified(server, 0, 0, video.getWidth(), video.getHeight());
server->kbdAddEvent = Input::keyEvent;
server->ptrAddEvent = Input::pointerEvent;
processTime = (1000000 / video.getFrameRate()) - 100;
calcFrameCRC = args.getCalcFrameCRC();
}
newClient 回调函数处理:input.connect/disconnect 意味着开源框架当有会话时,配置 usbgadget hid 设备使能键鼠,无会话时,断开 hid udc,关闭虚拟鼠标功能
enum rfbNewClientAction Server::newClient(rfbClientPtr cl)
{
Server* server = (Server*)cl->screen->screenData;
cl->clientData =
new ClientData(server->video.getFrameRate(), &server->input); //创建ClientData结构体并赋值到cl->clientData
cl->clientGoneHook = clientGone; //定义关闭连接回调函数:释放clientData,0会话时input->disconnect()
cl->clientFramebufferUpdateRequestHook = clientFramebufferUpdateRequest; //数据帧缓冲区请求回调函数:cd->needUpdate = true
if (!server->numClients++) //仅仅在创建第一个会话时,执行input.connect()
{
server->input.connect();
server->pendingResize = false;
server->frameCounter = 0;
}
return RFB_CLIENT_ACCEPT;
}
5. ikvm_video 文件
5.1 头文件
界说了 video 关键的几个参数,例如帧率frameRate、宽width、高height、采样率subSampling 等
界说了数据缓冲区std::vector buffers,Buffer 是一个结构体,内里有四个元素,分别为:void* data,bool queued,size_t payload,size_t size
std::vector 是是 C++ 标准库中的一个动态数组容器,它可以或许存储一系列相同范例的元素,并支持动态调整大小。代码中是在 Video::resize()函数中通过buffers.resize(req.count)动态调整大小
namespace ikvm
{
class Video
{
public:
Video(const std::string& p, Input& input, int fr = 30, int sub = 0);
char* getData();
void getFrame();
bool needsResize();
void resize();
void start();
void stop();
void restart()
{
stop();
start();
}
inline int getFrameRate() const
{
return frameRate;
}
inline size_t getFrameSize() const
{
return buffers.payload;
}
inline size_t getHeight() const
{
return height;
}
inline uint32_t getPixelformat() const
{
return pixelformat;
}
inline size_t getWidth() const
{
return width;
}
inline int getSubsampling() const
{
return subSampling;
}
inline void setSubsampling(int _sub)
{
subSampling = _sub;
}
static const int bitsPerSample;
static const int bytesPerPixel;
static const int samplesPerPixel;
private:
struct Buffer
{
Buffer() : data(nullptr), queued(false), payload(0), size(0) {}
void* data;
bool queued;
size_t payload;
size_t size;
};
bool resizeAfterOpen;
bool timingsError;
int fd;
int frameRate;
int lastFrameIndex;
size_t height;
size_t width;
int subSampling;
Input& input;
const std::string path;
std::vector<Buffer> buffers;
uint32_t pixelformat;
};
} // namespace ikvm
5.2源文件
创建 Video 对象,初始化赋值:分辨率 800*600、采样率subSampling = args.getSubsampling()初始化值为 0、path = /dev/video0
界说了几个关键函数:Video::getData()、Video::getFrame()、bool Video::needsResize()、void Video::resize()、void Video::start()、void Video::stop()
Video::Video(const std::string& p, Input& input, int fr, int sub) :
resizeAfterOpen(false), timingsError(false), fd(-1), frameRate(fr),
lastFrameIndex(-1), height(600), width(800), subSampling(sub), input(input),
path(p), pixelformat(V4L2_PIX_FMT_JPEG)
{}
Video::start(),必要注意的几点是:
初次实行时 ,完全实行全部代码,创建全局句柄 fd,后续再次实行会通过判断 fd 来判断/dev/video0 是否打开,如打开就无需重复设定
然后,主要实行以下操作:叫醒输入设备、设置帧率、设置采样方式 YUV420/444、调整视频流大小、分辨率调整
/**
* @brief 启动视频设备并进行初始化
*
* 本函数负责打开视频设备,查询并设置设备的参数,如分辨率和帧率等。
* 它还负责调整视频流的格式和参数,以满足应用程序的需求。
*/
void Video::start()
{
int rc;
size_t oldHeight = height;
size_t oldWidth = width;
v4l2_capability cap;
v4l2_format fmt;
v4l2_streamparm sparm;
v4l2_control ctrl;
// 如果文件描述符fd大于等于0,则表明设备已经打开,直接返回
if (fd >= 0)
{
return;
}
// 发送唤醒数据包以激活输入设备
input.sendWakeupPacket();
// 打开视频设备文件
fd = open(path.c_str(), O_RDWR);
if (fd < 0)
{
// 如果打开设备失败,记录错误日志并抛出异常
log<level::ERR>("Failed to open video device",
entry("PATH=%s", path.c_str()),
entry("ERROR=%s", strerror(errno)));
elog<Open>(
xyz::openbmc_project::Common::File::Open::ERRNO(errno),
xyz::openbmc_project::Common::File::Open::PATH(path.c_str()));
}
// 查询视频设备的能力
memset(&cap, 0, sizeof(v4l2_capability));
rc = ioctl(fd, VIDIOC_QUERYCAP, &cap);
if (rc < 0)
{
// 如果查询设备能力失败,记录错误日志并抛出异常
log<level::ERR>("Failed to query video device capabilities",
entry("ERROR=%s", strerror(errno)));
elog<ReadFailure>(
xyz::openbmc_project::Common::Device::ReadFailure::CALLOUT_ERRNO(
errno),
xyz::openbmc_project::Common::Device::ReadFailure::CALLOUT_DEVICE_PATH(path.c_str()));
}
// 检查视频设备是否支持视频捕获和流式传输
if (!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) ||
!(cap.capabilities & V4L2_CAP_STREAMING))
{
// 如果不支持,记录错误日志并抛出异常
log<level::ERR>("Video device doesn't support this application");
elog<Open>(
xyz::openbmc_project::Common::File::Open::ERRNO(errno),
xyz::openbmc_project::Common::File::Open::PATH(path.c_str()));
}
// 查询视频设备的格式
memset(&fmt, 0, sizeof(v4l2_format));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
rc = ioctl(fd, VIDIOC_G_FMT, &fmt);
if (rc < 0)
{
// 如果查询设备格式失败,记录错误日志并抛出异常
log<level::ERR>("Failed to query video device format",
entry("ERROR=%s", strerror(errno)));
elog<ReadFailure>(
xyz::openbmc_project::Common::Device::ReadFailure::CALLOUT_ERRNO(
errno),
xyz::openbmc_project::Common::Device::ReadFailure::CALLOUT_DEVICE_PATH(path.c_str()));
}
// 设置视频设备的帧率参数
memset(&sparm, 0, sizeof(v4l2_streamparm));
sparm.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
sparm.parm.capture.timeperframe.numerator = 1;
sparm.parm.capture.timeperframe.denominator = frameRate;
rc = ioctl(fd, VIDIOC_S_PARM, &sparm);
if (rc < 0)
{
// 如果设置帧率失败,记录警告日志
log<level::WARNING>("Failed to set video device frame rate",
entry("ERROR=%s", strerror(errno)));
}
// 设置视频设备的JPEG色度抽样方式
ctrl.id = V4L2_CID_JPEG_CHROMA_SUBSAMPLING;
ctrl.value = subSampling ? V4L2_JPEG_CHROMA_SUBSAMPLING_420
: V4L2_JPEG_CHROMA_SUBSAMPLING_444;
rc = ioctl(fd, VIDIOC_S_CTRL, &ctrl);
if (rc < 0)
{
// 如果设置JPEG色度抽样失败,记录警告日志
log<level::WARNING>("Failed to set video jpeg subsampling",
entry("ERROR=%s", strerror(errno)));
}
// 更新视频设备的分辨率和像素格式
height = fmt.fmt.pix.height;
width = fmt.fmt.pix.width;
pixelformat = fmt.fmt.pix.pixelformat;
// 检查是否支持像素格式
if (pixelformat != V4L2_PIX_FMT_RGB24 && pixelformat != V4L2_PIX_FMT_JPEG)
{
// 如果不支持,记录错误日志
log<level::ERR>("Pixel Format not supported",
entry("PIXELFORMAT=%d", pixelformat));
}
// 调整视频流的大小
resize();
// 检查分辨率是否发生变化
if (oldHeight != height || oldWidth != width)
{
resizeAfterOpen = true;
}
}
munmap 用于取消之前通过 mmap 系统调用创建的内存映射。它会将指定的内存区域从进程的地址空间中移除
// 停止视频流并释放相关资源
void Video::stop()
{
int rc;
unsigned int i;
v4l2_buf_type type(V4L2_BUF_TYPE_VIDEO_CAPTURE);
// 如果文件描述符fd小于0,则不执行停止操作,直接返回
if (fd < 0)
{
return;
}
// 重置最后帧的索引
lastFrameIndex = -1;
// 停止视频流的捕获
rc = ioctl(fd, VIDIOC_STREAMOFF, &type);
if (rc)
{
// 如果停止失败,记录错误日志
log<level::ERR>("Failed to stop streaming",
entry("ERROR=%s", strerror(errno)));
}
// 释放所有缓冲区资源
for (i = 0; i < buffers.size(); ++i)
{
// 如果缓冲区已分配内存,则释放内存并重置相关标志
if (buffers.data)
{
munmap(buffers.data, buffers.size);
buffers.data = nullptr;
buffers.queued = false;
}
}
// 关闭文件描述符
close(fd);
fd = -1;
}
Video::getFrame()获取底层 Driver 数据存入数据缓冲区std::vector buffers 中,该函数主要实行以下操作
[*] 查抄设备文件描述符有效性
[*] 设置文件描述符聚集和超时时间
[*] 切换非阻塞模式制止永世阻塞
[*] 轮询设备获取有效帧数据
[*] 处理获取到的视频缓冲区 (VIDIOC_DQBUF:获取数据,以便发送到前端表现)
[*] 重新列队未使用的缓冲区 (VIDIOC_QBUF: 重排缓冲区逻辑,加入视频采集队列)
这里必要注意两个特别紧张的地方:
IOCTRL:VIDIOC_DQBUF
[*]作用 :从视频设备的队列中取出一个已经处理完毕的缓冲区,以便应用步伐可以访问其中的数据。
[*]使用场景 :通常在视频捕获时,应用步伐必要从队列中取出已经填充了数据的缓冲区,以便处理或表现这些数据。
IOCTRL:VIDIOC_QBUF
[*]作用 :将缓冲区加入视频设备的队列中,以便设备可以开始处理该缓冲区中的数据。
[*]使用场景 :通常在视频捕获或输出时,应用步伐必要将缓冲区加入队列,以便设备可以填充数据(捕获)或发送数据(输出)。
暂时切换到非阻塞模式的目标 :
代码起首使用 fcntl(fd, F_SETFL, fd_flags | O_NONBLOCK) 将文件描述符 fd 设置为非阻塞模式 。 如许做的主要目标是为了共同 select 函数实现一个超机遇制。select 函数会等待文件描述符可读(有数据到来)或超时。如果文件描述符处于阻塞模式,而且没有数据到来,select 会一直阻塞下去,这正是代码解释中提到的“防止视频信号丢失时驱动步伐永世阻塞”的环境。
恢复为阻塞模式的原因:
在 select 调用竣事后,代码立纵然用 fcntl(fd, F_SETFL, fd_flags) 将文件描述符 fd 恢复到之前保存的原始标志 fd_flags。 在暂时修改文件描述符的属性后,应该将其恢复到原始状态,以制止对步伐的其他部分产生不测的影响
**总而言之,**代码暂时将文件描述符设置为非阻塞模式是为了在 select 调用中实现超机遇制,防止步伐在没有视频信号时永世阻塞。在 select 调用竣事后,为了保持文件描述符的原始行为,并制止对其他潜在的操作产生影响,代码会将其恢复到之前的阻塞模式(或其他原始模式)。
void Video::getFrame()
{
int rc(0); // 定义返回码变量,用于存储函数调用的结果
int fd_flags; // 定义文件描述符标志变量,用于保存原始的文件描述符状态
v4l2_buffer buf; // 定义V4L2缓冲区结构体变量,用于存储从设备获取的视频帧信息
fd_set fds; // 定义文件描述符集合变量,用于select系统调用,监控文件描述符的可读性
timeval tv; // 定义时间结构体变量,用于select系统调用,设置超时时间
// 设备有效性检查
if (fd < 0)
{
return; // 如果文件描述符无效(小于0),则直接返回,表示无法获取视频帧
}
// 初始化文件描述符集合
FD_ZERO(&fds); // 清空文件描述符集合,确保集合中没有任何文件描述符
FD_SET(fd, &fds); // 将视频设备的文件描述符添加到集合中,select将监控这个文件描述符
// 设置select超时时间为1秒
tv.tv_sec = 1; // 设置秒部分为1秒
tv.tv_usec = 0; // 设置微秒部分为0,总共超时时间为1秒
// 初始化V4L2缓冲区结构
memset(&buf, 0, sizeof(v4l2_buffer)); // 将缓冲区结构体清零,确保所有成员都被初始化为0
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 设置缓冲区类型为视频捕获
buf.memory = V4L2_MEMORY_MMAP; // 设置缓冲区内存映射模式为MMAP(内存映射)
/* 切换非阻塞模式(关键安全措施):
* 防止视频信号丢失时驱动程序永久阻塞
* 保留原始文件描述符标志用于后续恢复 */
fd_flags = fcntl(fd, F_GETFL); // 获取当前文件描述符的标志(包括阻塞/非阻塞等状态)
fcntl(fd, F_SETFL, fd_flags | O_NONBLOCK); // 设置文件描述符为非阻塞模式,即使没有数据可读,read/ioctl等操作也会立即返回,避免程序永久等待
// 等待设备数据就绪(最长等待1秒)
rc = select(fd + 1, &fds, NULL, NULL, &tv); // 使用select系统调用等待视频设备文件描述符变为可读状态。fd + 1是需要监控的最大文件描述符加1,中间两个NULL表示不监控写和异常情况,tv是超时时间。
if (rc > 0) // 如果select返回值大于0,表示在超时时间内有文件描述符就绪(这里是视频设备)
{
// 循环处理所有就绪缓冲区
do
{
// 从队列中取出缓冲区
rc = ioctl(fd, VIDIOC_DQBUF, &buf); // 使用ioctl命令VIDIOC_DQBUF从驱动程序的捕获队列中取出一个已经填充好数据的缓冲区。buf中会包含帧数据的信息。
if (rc >= 0) // 如果ioctl返回值大于等于0,表示成功取出一个缓冲区
{
buffers.queued = false; // 将该缓冲区标记为未排队,表示它正在被处理
// 成功获取有效帧处理
if (!(buf.flags & V4L2_BUF_FLAG_ERROR)) // 检查缓冲区标志,如果未设置错误标志,则认为这是一个有效的视频帧
{
lastFrameIndex = buf.index; // 记录最后一个成功获取的帧的索引
buffers.payload = buf.bytesused; // 记录该帧的实际数据大小(有效载荷)
break; // 获取到有效帧后,跳出当前的do-while循环,因为我们只需要一个最新的有效帧
}
else // 错误帧处理
{
buffers.payload = 0; // 如果是错误帧,则将有效载荷大小设置为0
}
}
} while (rc >= 0); // 持续循环,直到ioctl(VIDIOC_DQBUF)返回错误(表示队列中没有更多已填充的缓冲区)
}
// 恢复原始阻塞模式设置
fcntl(fd, F_SETFL, fd_flags); // 将文件描述符恢复到之前保存的阻塞模式
/* 缓冲区重新排队逻辑:
* 1. 跳过当前使用的最后一帧缓冲区
* 2. 将所有未排队的缓冲区重新加入采集队列
* 3. 维持环形缓冲区的持续运转 */
for (unsigned int i = 0; i < buffers.size(); ++i) // 遍历所有已分配的缓冲区
{
if (i == (unsigned int)lastFrameIndex) // 如果当前遍历的缓冲区索引是刚刚使用的最后一帧的索引,则跳过,避免重复排队
{
continue;
}
if (!buffers.queued) // 如果当前缓冲区没有被排队(queued标志为false),表示它需要被重新放入采集队列
{
// 重新初始化缓冲区结构
memset(&buf, 0, sizeof(v4l2_buffer)); // 再次清空缓冲区结构体
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 设置缓冲区类型为视频捕获
buf.memory = V4L2_MEMORY_MMAP; // 设置缓冲区内存映射模式为MMAP
buf.index = i; // 设置缓冲区的索引
// 将缓冲区加入采集队列
rc = ioctl(fd, VIDIOC_QBUF, &buf); // 使用ioctl命令VIDIOC_QBUF将缓冲区重新放入驱动程序的捕获队列中,等待驱动程序填充新的视频数据
if (rc) // 如果ioctl返回值不为0,表示排队操作失败
{
log<level::ERR>("Failed to queue buffer", // 记录错误日志
entry("ERROR=%s", strerror(errno))); // 包含具体的错误信息(通过strerror获取)
}
else // 如果排队操作成功
{
buffers.queued = true; // 将该缓冲区标记为已排队
}
}
}
}
必要特别注意的一点是:当 i== lastFrameIndex 时,buffers.queued 保持不变,意味着不加入采集队列,而 Video::getData()读取的数据正是此帧数据
buffers.data 该数据会通过 Server::sendFrame()发送到 KVM 前端进行渲染表现
buffers.queued = false 暂不加入底层采集队列,必要应用层处理的帧数据
buffers.queued = true 加入底层采集队列,准备获取底层 Video 数据帧
char* Video::getData()
{
if (lastFrameIndex >= 0)
{
return (char*)buffers.data;
}
return nullptr;
}
6. ikvm_input 文件
6.1 头文件
界说了键鼠 HID 操作句柄keyboardFd、pointerFd 等几个紧张变量
namespace ikvm
{
class Input
{
public:
Input(const std::string& kbdPath, const std::string& ptrPath,
const std::string& udc);
void connect();
void disconnect();
static void keyEvent(rfbBool down, rfbKeySym key, rfbClientPtr cl);
static void pointerEvent(int buttonMask, int x, int y, rfbClientPtr cl);
void sendWakeupPacket();
private:
static constexpr int NUM_MODIFIER_BITS = 4;
static constexpr int KEY_REPORT_LENGTH = 8;
static constexpr int PTR_REPORT_LENGTH = 6;
static constexpr const char* hidUdcPath =
"/sys/kernel/config/usb_gadget/obmc_hid/UDC";
static constexpr const char* usbVirtualHubPath =
"/sys/bus/platform/devices/1e6a0000.usb-vhub";
static constexpr int HID_REPORT_RETRY_MAX = 5;
static uint8_t keyToMod(rfbKeySym key);
static uint8_t keyToScancode(rfbKeySym key);
bool writeKeyboard(const uint8_t* report);
void writePointer(const uint8_t* report);
int keyboardFd;
int pointerFd;
uint8_t keyboardReport;
uint8_t pointerReport;
std::string keyboardPath;
std::string pointerPath;
std::string udcName;
std::map<int, int> keysDown;
std::ofstream hidUdcStream;
std::mutex keyMutex;
std::mutex ptrMutex;
};
} // namespace ikvm
6.2 源文件
在 ikvm_manager.cpp 文件中创建了 Input 对象,详细的构造函数如下所示:
对象中界说了一个 std::ofstream hidUdcStream 的范例对象,用于文件输出操作
Input::Input(const std::string& kbdPath, const std::string& ptrPath,
const std::string& udc) :
keyboardFd(-1),
pointerFd(-1), keyboardReport{0}, pointerReport{0}, keyboardPath(kbdPath),
pointerPath(ptrPath), udcName(udc)
{
hidUdcStream.exceptions(std::ofstream::failbit | std::ofstream::badbit);
hidUdcStream.open(hidUdcPath, std::ios::out | std::ios::app);
}
别的,界说了几个紧张的函数,其中keyEvent 和pointerEvent 分别在创建Server 对象的时间通过回调函数的方式注册给 libvncserver。
rfbScreenInfoPtr rfbScreen->kbdAddEvent = Input::keyEvent;
rfbScreenInfoPtr rfbScreen->kbdAddEvent = Input::pointerEvent;
末了必要注意的一点是,keyEvent 还涉及键值的转换,即 KVM 前端捕获到的本地键盘或虚拟键盘键值 ASCII 通过 WSS 发送到这里,然后转换成标准的 8 字节 HID 数据,终极才通过 USB 发送到 Host 端。
void Input::connect(); //使能HID虚拟设备:echo "1e6a0000.usb-vhub:p6">/sys/kernel/config/usb_gadget/obmc_hid/UDC
void Input::disconnect(); //断开HID虚拟设备:echo " ">/sys/kernel/config/usb_gadget/obmc_hid/UDC
void Input::keyEvent(rfbBool down, rfbKeySym key, rfbClientPtr cl);
void Input::pointerEvent(int buttonMask, int x, int y, rfbClientPtr cl);
bool Input::writeKeyboard(const uint8_t* report);//键值发送函数
void Input::writePointer(const uint8_t* report); //鼠标发送函数
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]