使用Qt连接scrcpy-server控制手机

news/2024/2/25 13:38:29/文章来源:https://blog.csdn.net/baidu_30570701/article/details/135583763

Qt连接scrcpy-server

  • 测试环境
  • 如何启动scrcpy-server
    • 1. 连接设备
    • 2. 推送scrcpy-server到手机上
    • 3. 建立Adb隧道连接
    • 4. 启动服务
    • 5. 关闭服务
  • 使用QTcpServer与scrcpy-server建立连接
  • 建立连接并视频推流完整流程
    • 1. 开启视频推流过程
    • 2. 关闭视频推流过程
  • 视频流的解码
    • 1. 数据包协议解析
    • 2. 解码流程
    • 3. 视频帧转QImage
  • 使用OpenGL渲染显示视频流
  • 控制命令的下发

测试环境

首先放一些测试环境,不保证其他环境也能够这样使用:

  • Qt库:5.12.2,mscv2019_64
  • scrcpy:2.3.1
  • FFmpeg:ffmpeg-n5.1.4-1-gae14d9c06b-win64-gpl-shared-5.1
  • Adb:34.0.5
  • Android环境:MuMu模拟器12

如何启动scrcpy-server

首先需要说明的是,我们是与scrcpy-server建立连接,而单纯想显示手机上的画面与控制,作者github发布有scrcpy.exe可以直接运行使用,而这里我们相当于做另一个scrcpy,从而达到一些自定义控制的目的。与scrcpy-server建立连接,github上开发文档也说明了https://github.com/Genymobile/scrcpy/blob/master/doc/develop.md,这里更详细的说明下与scrcpy-server建立连接的具体细节。为了更好的描述细节,下面所有操作使用Qt代码演示。

1. 连接设备

启动scrcpy-server的所有操作都是经过Adb进行的,不了解Adb命令建议先学习一下相关命令,因此,连接设备前先确保手机上打开了“USB调试”开关。连接设备使用命令adb connect,Qt中执行Adb命令使用QProcess类,这里我们封装一个Adb工具类以方便的执行命令:

//头文件
#pragma once#include <qobject.h>
#include <qprocess.h>/*** @brief Adb命令执行封装类*/
class AdbCommandRunner {
public:explicit AdbCommandRunner(const QString& deviceName = QString());~AdbCommandRunner();/*** @brief 执行Adb命令* @param cmds 参数列表* @param waitForFinished 是否等待执行完成*/void runAdb(const QStringList& cmds, bool waitForFinished = true);/*** @brief 获取执行结果的错误* @return*/QString getLastErr();QString lastFeedback; //执行结果返回的字符串private:QProcess process;QString deviceName;
};//cpp
#include "adbcommandrunner.h"#include <qdebug.h>AdbCommandRunner::AdbCommandRunner(const QString &deviceName): deviceName(deviceName)
{}AdbCommandRunner::~AdbCommandRunner()  {if (process.isOpen()) {process.kill();process.waitForFinished();}
}void AdbCommandRunner::runAdb(const QStringList &cmds, bool waitForFinished) {if (deviceName.isEmpty()) {process.start("adb/adb", cmds);} else {process.start("adb/adb", QStringList({"-s", deviceName}) + cmds);}qDebug() << "do adb execute command:" << "adb " + cmds.join(' ');if (waitForFinished) {process.waitForFinished();}lastFeedback = process.readAllStandardOutput();
}QString AdbCommandRunner::getLastErr() {QString failReason = process.readAllStandardError();if (failReason.isEmpty()) {failReason = lastFeedback;}return failReason;
}

需要注意的是,Adb服务是后台运行的,我们可以直接执行adb connect命令连接设备,adb会自动启动服务,然而启动服务是需要个几秒钟,直接QProcess执行会有个等待时间,正确的做法是,先使用adb start-server启动服务,这个过程可以在线程中执行:

QThread::create([] {QProcess process;process.start("adb/adb", {"start-server"});process.waitForFinished();if (process.exitCode() == 0 && process.exitStatus() == QProcess::NormalExit) {qDebug() << "adb server start finished!";} else {qDebug() << "adb server start failed:" << process.readAll();}
))->start();

如果服务启动成功,并且设备存在,连接时几乎没有等待时间

bool connectToDevice() {AdbCommandRunner runner;runner.runAdb({"connect", deviceAddress});if (runner.lastFeedback.contains("cannot connect to")) {qDebug() << "connect device:" << deviceAddress << "failed, error:" << runner.getLastErr();return false;}qInfo() << "connect device:" << deviceAddress << "success!";return true;
}

2. 推送scrcpy-server到手机上

推送文件自然是使用adb push命令,建议是推送到临时目录/data/local/tmp下:

bool pushServiceToDevice() {auto scrcpyFilePath = QDir::currentPath() + "/scrcpy/scrcpy-server";qDebug() << "scrcpy path:" << scrcpyFilePath;AdbCommandRunner runner;runner.runAdb({"-s", deviceAddress, "push", scrcpyFilePath, "/data/local/tmp/scrcpy-server.jar"});if (!runner.lastFeedback.contains("1 file pushed")) {qDebug() << runner.getLastErr();return false;}return true;
}

3. 建立Adb隧道连接

默认情况下,scrcpy-server是作为客户端,通过adb隧道连接到电脑端的本地Tcp服务器,如开发者文档上描述,这个角色也是可以反转的,只需要在启动服务命令里面添加tunnel_forward=true(注意不是启动scrcpy.exe的命令行参数)。默认角色下,使用adb reverse命令开启隧道连接,需要注意的是,隧道名中需要携带一个8位字符串scid作为标识,这里我们可以使用时间戳代替:

scid = QString::asprintf("%08x", QDateTime::currentSecsSinceEpoch());
AdbCommandRunner runner;
runner.runAdb({"-s", deviceAddress, "reverse", "localabstract:scrcpy_" + scid, "tcp:27183")});

记住这个27183端口,下面使用QTcpServer时正是使用这个端口监听服务的连接。

4. 启动服务

scrcpy-server本身是一个可执行的jar包,启动这个jar包,使用adb shell命令:

serverRunner = new AdbCommandRunner;
QStringList scrcpyServiceOpt;
scrcpyServiceOpt << "-s" << deviceAddress << "shell";
scrcpyServiceOpt << "CLASSPATH=/data/local/tmp/scrcpy-server.jar";
scrcpyServiceOpt << "app_process";
scrcpyServiceOpt << "/";
scrcpyServiceOpt << "com.genymobile.scrcpy.Server";
scrcpyServiceOpt << SCRCPY_VERSION;
scrcpyServiceOpt << "scid=" + scid;
scrcpyServiceOpt << "audio=false"; //不传输音频
scrcpyServiceOpt << "max_fps=" + QString::number(maxFrameRate); //最大帧率
scrcpyServiceOpt << "max_size=1920"; //视频帧最大尺寸
serverRunner->runAdb(scrcpyServiceOpt, false);

需要注意的是,这里QProcess对象需要保存,关闭服务时需要杀死对应的adb shell子进程。在上面参数中scid以及之前的参数是必要的,如果版本号和scid对应不上无法启动服务。更多的控制参数可以参考源代码scrcpy\app\src\server.c第212行开始,其中参数的默认值在scrcpy\app\src\options.c中,启动成功后就会立即通过adb与电脑端本地服务建立连接。

5. 关闭服务

关闭服务时,首先需要结束shell进程,然后关闭隧道即可:

if (serverRunner) {delete serverRunner;serverRunner = nullptr;
}AdbCommandRunner runner;
runner.runAdb({"-s", deviceAddress, "reverse", "--remove", "localabstract:scrcpy_" + scid});

关闭服务之后,scrcpy-server会自己在设备中删除,重新启动服务需要从第2步骤推送文件开始。

使用QTcpServer与scrcpy-server建立连接

上面说了,默认情况下电脑端作为tcp服务器,scrcpy-server作为客户端建立连接,因此,使用QTcpServer监听本地adb隧道连接端口即可:

ScrcpyServer::ScrcpyServer(QObject *parent): QObject(parent)
{//tcp服务tcpServer = new QTcpServer(this);connect(tcpServer, &QTcpServer::acceptError, this, [] (QAbstractSocket::SocketError socketError) {qCritical() << "scrcpy server accept error:" << socketError;});connect(tcpServer, &QTcpServer::newConnection, this, &ScrcpyServer::handleNewConnection);
}void ScrcpyServer::handleNewConnection() {auto socket = tcpServer->nextPendingConnection();//第一个socket为视频流if (!videoSocket) {videoSocket = socket;connect(socket, &QTcpSocket::readyRead, this, &ScrcpyServer::receiveVideoBuffer);qInfo() << "video socket pending connect...";} else if (!controlSocket) {controlSocket = socket;connect(socket, &QTcpSocket::readyRead, this, &ScrcpyServer::receiveControlBuffer);qInfo() << "control socket pending connect...";} else {qWarning() << "unexpect socket appending...";}connect(socket, &QTcpSocket::stateChanged, this, [=] (QAbstractSocket::SocketState state) {qDebug() << "socket state changed:" << state;if (state == QAbstractSocket::UnconnectedState) {socket->deleteLater();}});
}bool ScrcpyServer::start() {if (!tcpServer->isListening()) {bool success = tcpServer->listen(QHostAddress::AnyIPv4, 27183);if (!success) {qDebug() << "tcp server listen failed:" << tcpServer->errorString();}}
}

根据开发者文档描述,scrcpy-server连接到QTcpServer后,会有3个tcp连接分别用来传输:视频、音频、控制命令,这里我们在启动时设置了audio=false关闭了音频传输,因此第2个为控制socket。

建立连接并视频推流完整流程

上面讲了启动scrcpy-server和使用QTcpServer建立连接,事实上,建立连接和启动tcp服务是需要按照顺序进行的:

1. 开启视频推流过程

  • 开启QTcpServer服务,监听指定端口如27183
  • 推送scrcpy-server到手机上
  • 使用tcp服务监听的端口,和8位随机字符串作为scid,建立Adb隧道连接
  • 使用adb shell命令启动scrcpy-server服务
  • QTcpServer等待视频流和控制socket连接

2. 关闭视频推流过程

  • 结束adb shell子进程
  • 关闭Adb隧道连接
  • 关闭Tcp服务

视频流的解码

1. 数据包协议解析

文档中详细描述了视频流的数据组成,最开始视频流会传输64字节表示设备的名称,然后依次传输4字节编码方式、4字节帧图像宽度、4字节帧图像高度,接着开始传输视频帧,其中视频帧由帧头和数据组成,帧头中包含有PTS标志(8字节)和帧数据长度(4字节)两个信息,后面接收帧数据长度的数据即可,然后等待接收下一帧数据。视频默认编码为H.264,可以通过启动服务参数更改编码类型,这里我们使用FFmpeg来解析视频帧。
由于解码是个耗时任务,需要放到线程中运行,这里就需要与QTcpSocket接收到的数据进行线程同步处理,为了让解码线程看起来像是以同步方式读取数据,编写一个工具类来接收QTcpSocket发送来的数据:

//头文件
#pragma once#include <qobject.h>
#include <qmutex.h>
#include <qwaitcondition.h>#include "byteutil.h"class BufferReceiver : public QObject {
public:explicit BufferReceiver(QObject *parent = nullptr);void sendBuffer(const QByteArray& data);void endCache();template<typename T>T receive() {enum {T_Size = sizeof(T)};T value = T();receive((void*)&value, T_Size);ByteUtil::swapBits(value);return value;}void receive(void* data, int len);bool isEndReceive() const {return endBufferCache;}private:QByteArray receiveBuffer;QMutex mutex;QWaitCondition receiveWait;bool endBufferCache;
};//cpp
#include "bufferreceiver.h"BufferReceiver::BufferReceiver(QObject *parent): QObject(parent), endBufferCache(false)
{}void BufferReceiver::sendBuffer(const QByteArray &data) {QMutexLocker locker(&mutex);receiveBuffer.append(data);receiveWait.notify_all();
}void BufferReceiver::endCache() {QMutexLocker locker(&mutex);endBufferCache = true;receiveWait.notify_all();
}void BufferReceiver::receive(void *data, int len) {mutex.lock();if (endBufferCache) {mutex.unlock();return;}while (receiveBuffer.size() < len && !endBufferCache) {receiveWait.wait(&mutex);}if (!endBufferCache) {memcpy(data, receiveBuffer.data(), len);receiveBuffer = receiveBuffer.mid(len);}mutex.unlock();
}

在主线程中收到视频流数据就缓存到BufferReceiver中:

void ScrcpyServer::receiveVideoBuffer() {if (videoDecoder) {videoDecoder->appendBuffer(videoSocket->readAll());}
}

解码器线程按照协议依次接收数据包:

void VideoDecoder::run() {QByteArray remoteDeviceName(64, '\0');bufferReceiver.receive(remoteDeviceName.data(), remoteDeviceName.size());auto name = QString::fromUtf8(remoteDeviceName);if (!name.isEmpty()) {qInfo() << "device name received:" << name;}if (bufferReceiver.isEndReceive()) {return;}if (codecCtx == nullptr) {auto codecId = bufferReceiver.receive<uint32_t>();auto width = bufferReceiver.receive<int>();auto height = bufferReceiver.receive<int>();if (!codecInit(codecId, width, height)) {codecRelease();qCritical() << "video decode init failed!";return;}}qInfo() << "video decode is running...";for (;;) {if (!frameReceive()) {break;}if (!frameMerge()) {av_packet_unref(packet);break;}frameUnpack();av_packet_unref(packet);}//释放资源codecRelease();qInfo() << "video decoder exit...";
}

2. 解码流程

注意上面解码线程的读取数据步骤,在读取到解码器和帧大小时就可以进行解码器初始化了:

//初始化解码器
auto codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {qDebug() << "find codec h264 fail!";return false;
}//初始化解码器上下文
codecCtx = avcodec_alloc_context3(codec);
if (!codecCtx) {qDebug() << "allocate codec context fail!";return false;
}codecCtx->width = width;
codecCtx->height = height;
codecCtx->pix_fmt = AV_PIX_FMT_YUV420P;int ret = avcodec_open2(codecCtx, codec, nullptr);
if (ret < 0) {qDebug() << "open codec fail!";return false;
}packet = av_packet_alloc();
if (!packet) {qDebug() << "alloc packet fail!";return false;
}decodeFrame = av_frame_alloc();
if (!decodeFrame) {qDebug() << "alloc frame fail!";return false;
}

获取到帧数据时,依次读取PTS和帧数据大小,设置到AVPacket中:

bool VideoDecoder::frameReceive() {auto ptsFlags = bufferReceiver.receive<uint64_t>();auto frameLen = bufferReceiver.receive<int32_t>();if (bufferReceiver.isEndReceive()) {return false;}Q_ASSERT(frameLen != 0);if (av_new_packet(packet, frameLen)) {qDebug() << "av new packet failed!";return false;}bufferReceiver.receive(packet->data, frameLen);if (bufferReceiver.isEndReceive()) {return false;}if (ptsFlags & SC_PACKET_FLAG_CONFIG) {packet->pts = AV_NOPTS_VALUE;} else {packet->pts = ptsFlags & SC_PACKET_PTS_MASK;}if (ptsFlags & SC_PACKET_FLAG_KEY_FRAME) {packet->flags |= AV_PKT_FLAG_KEY;}packet->dts = packet->pts;return true;
}

根据PTS判断是否需要进行帧合并:

bool VideoDecoder::frameMerge() {bool isConfig = packet->pts == AV_NOPTS_VALUE;if (isConfig) {free(mergeBuffer);mergeBuffer = (uint8_t*)malloc(packet->size);if (!mergeBuffer) {qDebug() << "merge buffer malloc failed! required size:" << packet->size;return false;}memcpy(mergeBuffer, packet->data, packet->size);mergedSize = packet->size;}else if (mergeBuffer) {if (av_grow_packet(packet, mergedSize)) {qDebug() << "av grow packet failed!";return false;}memmove(packet->data + mergedSize, packet->data, packet->size);memcpy(packet->data, mergeBuffer, mergedSize);free(mergeBuffer);mergeBuffer = nullptr;}return true;
}

视频帧解包分别使用avcodec_send_packetavcodec_receive_frame,下面代码中演示了如何循环解包,然后转换为QVideoFrame对象(供后面视频渲染使用),注意这里图像格式为YUV420P

void VideoDecoder::frameUnpack() {if (packet->pts == AV_NOPTS_VALUE) {return;}int ret = avcodec_send_packet(codecCtx, packet);if (ret < 0 && ret != AVERROR(EAGAIN)) {qCritical() << "send packet error:" << ret;} else {//循环解析数据帧for (;;) {ret = avcodec_receive_frame(codecCtx, decodeFrame);if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {break;}if (ret) {qCritical() << "could not receive video frame:" << ret;break;}QVideoFrame cachedFrame(codecCtx->width * codecCtx->height * 3 / 2,QSize(codecCtx->width, codecCtx->height),codecCtx->width, QVideoFrame::Format_YUV420P);int imageSize = av_image_get_buffer_size(codecCtx->pix_fmt, codecCtx->width, codecCtx->height, 1);if (cachedFrame.map(QAbstractVideoBuffer::WriteOnly)) {uchar *dstData = cachedFrame.bits();av_image_copy_to_buffer(dstData, imageSize, decodeFrame->data, decodeFrame->linesize,codecCtx->pix_fmt,codecCtx->width, codecCtx->height, 1);cachedFrame.unmap();emit frameDecoded(cachedFrame);}av_frame_unref(decodeFrame);}}
}

3. 视频帧转QImage

有时候我们需要提取视频的一帧图像,例如截图操作,需要直接转RGB图像,这时候有两种方法,一是直接对AVFrame进行转换,也就是上面提到的decodeFrame,使用sws_scale函数,但是需要先初始化一个SwsContext,初始化可以在CodecContext初始化之后进行:

swsContext = sws_getContext(codecCtx->width, codecCtx->height, codecCtx->pix_fmt,codecCtx->width, codecCtx->height, AV_PIX_FMT_RGB24, SWS_BILINEAR,nullptr, nullptr, nullptr);

转换时根据codecCtx信息先构造一个QImage,再调用sws_scale即可:

QImage image = QImage(codecCtx->width, codecCtx->height, QImage::Format_RGB888);
auto imagePtr = image.bits();
//将YUV420p转换为RGB24
const int lineSize[4] = {3*codecCtx->width, 0, 0, 0};
sws_scale(swsContext, (const uint8_t* const*)decodeFrame->data, decodeFrame->linesize,0, codecCtx->height, (uint8_t**)&imagePtr, lineSize);

第二种方法就比较简单了,通过Qt内置的方法转换,正如上面提到,通过av_image_copy_to_buffer函数将AVFrame转成了QVideoFrame最后发送了出来,获取QImage直接调用image函数即可,此时转换出来的格式是ARGB32:

QImage image = cachedFrame.image();

使用OpenGL渲染显示视频流

显示视频最好的办法就是使用OpenGL渲染,这样不会消耗大量的CPU资源,并且原视频帧解码出来的YUV420P也可以在OpenGL中计算。Qt中使用OpenGL自然是继承QOpenGLWidget,Qt官方正好有一个显示视频的控件QVideoWidget,只是没有提供直接设置视频流的方法,仔细阅读Multimedia模块中的QVideoWidget源代码发现,如果使用GLSL,经过QPainterVideoSurface实例,最终进行渲染使用的是QVideoSurfaceGlslPainter,其中支持各种图像帧类型的渲染,其中YUV420P也包含在内,对于YUV420P转RGB使用的是BT709标准。复制源代码中multimediawidgets/qmediaopenglhelper_p.hmultimediawidgets/qpaintervideosurface_p.hmultimediawidgets/qpaintervideosurface.cpp3个文件,自定义一个VideoWidget其中实例化一个QPainterVideoSurface,刷新图片是使用QPainterVideoSurface::present函数即可:

//.h
#pragma once#include <qwidget.h>
#include <qopenglwidget.h>#include "qpaintervideosurface_p.h"class VideoWidget : public QOpenGLWidget {
public:explicit VideoWidget(QWidget *parent = nullptr);~VideoWidget();QPainterVideoSurface *videoSurface() const;QSize sizeHint() const override;public:void setAspectRatioMode(Qt::AspectRatioMode mode);protected:void hideEvent(QHideEvent *event) override;void resizeEvent(QResizeEvent *event) override;void paintEvent(QPaintEvent *event) override;private slots:void formatChanged(const QVideoSurfaceFormat &format);void frameChanged();private:void updateRects();private:QPainterVideoSurface *m_surface;Qt::AspectRatioMode m_aspectRatioMode;QRect m_boundingRect;QRectF m_sourceRect;QSize m_nativeSize;bool m_updatePaintDevice;
};//.cpp
#include "videowidget.h"#include <qevent.h>
#include <qvideosurfaceformat.h>VideoWidget::VideoWidget(QWidget *parent): QOpenGLWidget(parent), m_aspectRatioMode(Qt::KeepAspectRatio), m_updatePaintDevice(true)
{m_surface = new QPainterVideoSurface(this);connect(m_surface, &QPainterVideoSurface::frameChanged, this, &VideoWidget::frameChanged);connect(m_surface, &QPainterVideoSurface::surfaceFormatChanged, this, &VideoWidget::formatChanged);
}QPainterVideoSurface *VideoWidget::videoSurface() const {return m_surface;
}VideoWidget::~VideoWidget() {delete m_surface;
}void VideoWidget::setAspectRatioMode(Qt::AspectRatioMode mode)
{m_aspectRatioMode = mode;updateGeometry();
}QSize VideoWidget::sizeHint() const
{return m_surface->surfaceFormat().sizeHint();
}void VideoWidget::hideEvent(QHideEvent *event)
{m_updatePaintDevice = true;
}void VideoWidget::resizeEvent(QResizeEvent *event)
{updateRects();
}void VideoWidget::paintEvent(QPaintEvent *event)
{QPainter painter(this);if (testAttribute(Qt::WA_OpaquePaintEvent)) {QRegion borderRegion = event->region();borderRegion = borderRegion.subtracted(m_boundingRect);QBrush brush = palette().window();for (const QRect &r : borderRegion)painter.fillRect(r, brush);}if (m_surface->isActive() && m_boundingRect.intersects(event->rect())) {m_surface->paint(&painter, m_boundingRect, m_sourceRect);m_surface->setReady(true);} else {if (m_updatePaintDevice && (painter.paintEngine()->type() == QPaintEngine::OpenGL|| painter.paintEngine()->type() == QPaintEngine::OpenGL2)) {m_updatePaintDevice = false;m_surface->updateGLContext();if (m_surface->supportedShaderTypes() & QPainterVideoSurface::GlslShader) {m_surface->setShaderType(QPainterVideoSurface::GlslShader);} else {m_surface->setShaderType(QPainterVideoSurface::FragmentProgramShader);}}}
}void VideoWidget::formatChanged(const QVideoSurfaceFormat &format)
{m_nativeSize = format.sizeHint();updateRects();updateGeometry();update();
}void VideoWidget::frameChanged()
{update(m_boundingRect);
}void VideoWidget::updateRects()
{QRect rect = this->rect();if (m_nativeSize.isEmpty()) {m_boundingRect = QRect();} else if (m_aspectRatioMode == Qt::IgnoreAspectRatio) {m_boundingRect = rect;m_sourceRect = QRectF(0, 0, 1, 1);} else if (m_aspectRatioMode == Qt::KeepAspectRatio) {QSize size = m_nativeSize;size.scale(rect.size(), Qt::KeepAspectRatio);m_boundingRect = QRect(0, 0, size.width(), size.height());m_boundingRect.moveCenter(rect.center());m_sourceRect = QRectF(0, 0, 1, 1);} else if (m_aspectRatioMode == Qt::KeepAspectRatioByExpanding) {m_boundingRect = rect;QSizeF size = rect.size();size.scale(m_nativeSize, Qt::KeepAspectRatio);m_sourceRect = QRectF(0, 0, size.width() / m_nativeSize.width(), size.height() / m_nativeSize.height());m_sourceRect.moveCenter(QPointF(0.5, 0.5));}
}

开始视频推流之前,初始化Surface,设置使用OpenGL渲染,并指定视频格式为YUV420P:

videoWidget->videoSurface()->setShaderType(QPainterVideoSurface::GlslShader);
videoWidget->videoSurface()->start(QVideoSurfaceFormat(QSize(1920, 1080), QVideoFrame::Format_YUV420P));

从VideoDecoder获取到视频帧时发送到Surface:

connect(decorder, &VideoDecoder::frameDecoded, this, [&](const QVideoFrame& frame) {videoWidget->videoSurface()->present(frame);
});

关闭推流时,同时关闭Surface渲染:

videoWidget->videoSurface()->stop();

控制命令的下发

命令的控制是通过第二个socket发送数据,其命令的编码协议定义和编码在源代码scrcpy\app\src\control_msg.hscrcpy\app\src\control_msg.c这两个文件中。例如,发送一个点击事件:

namespace ByteUtil {/*** @brief 字节序交换* @tparam T 数值类型* @param data 转换目标数值* @param size 字节序交换大小*/template<typename T>static void swapBits(T& data, size_t size = sizeof(T)) {for (size_t i = 0; i < size / 2; i++) {char* pl = (char*)&data + i;char* pr = (char*)&data + (size - i - 1);if (*pl != *pr) {*pl ^= *pr;*pr ^= *pl;*pl ^= *pr;}}}/*** @brief char*转指定数值类型(大端序)* @tparam T 数值类型* @param data 转换目标数值* @param src 原字节数组* @param srcSize 原字节数组大小*/template<typename T>static void bitConvert(T& data, const void* src, int srcSize = sizeof(T)) {memcpy(&data, src, srcSize);swapBits(data, srcSize);}
}class ControlMsg {
public:static QByteArray injectTouchEvent(android_motionevent_action action, android_motionevent_buttons actionButton,android_motionevent_buttons buttons, uint64_t pointerId,const QSize& screenSize, const QPoint& point, float pressure) {char bytes[32];bytes[0] = SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT;bytes[1] = action;ByteUtil::bitConvert(*(uint64_t*)(bytes + 2), &pointerId);uint32_t x = point.x();ByteUtil::bitConvert(*(uint32_t*)(bytes + 10), &x);uint32_t y = point.y();ByteUtil::bitConvert(*(uint32_t*)(bytes + 14), &y);uint16_t w = screenSize.width();ByteUtil::bitConvert(*(uint16_t*)(bytes + 18), &w);uint16_t h = screenSize.height();ByteUtil::bitConvert(*(uint16_t*)(bytes + 20), &h);uint16_t pressureValue = sc_float_to_u16fp(pressure);ByteUtil::bitConvert(*(uint16_t*)(bytes + 22), &pressureValue);ByteUtil::bitConvert(*(uint32_t*)(bytes + 24), &actionButton);ByteUtil::bitConvert(*(uint32_t*)(bytes + 28), &buttons);return { bytes, 32 };}
};

注册videoWidget事件过滤器,模拟发送鼠标事件:

bool App::eventFilter(QObject *watched, QEvent *event) {if (watched == videoWidget) {if (auto mouseEvent = dynamic_cast<QMouseEvent*>(event)) {auto dstPos = QPoint(qRound(mouseEvent->x() * framePixmapRatio.width()), qRound(mouseEvent->y() * framePixmapRatio.height()));if (mouseEvent->type() == QEvent::MouseButtonPress) {scrcpyServer->sendControl(ControlMsg::injectTouchEvent(AMOTION_EVENT_ACTION_DOWN, AMOTION_EVENT_BUTTON_PRIMARY,AMOTION_EVENT_BUTTON_PRIMARY, 0,frameSrcSize, dstPos, 1.0));} else if (mouseEvent->type() == QEvent::MouseButtonRelease) {scrcpyServer->sendControl(ControlMsg::injectTouchEvent(AMOTION_EVENT_ACTION_UP, AMOTION_EVENT_BUTTON_PRIMARY,AMOTION_EVENT_BUTTON_PRIMARY, 0,frameSrcSize, dstPos, 0.0));} else if (mouseEvent->type() == QEvent::MouseMove) {scrcpyServer->sendControl(ControlMsg::injectTouchEvent(AMOTION_EVENT_ACTION_MOVE, AMOTION_EVENT_BUTTON_PRIMARY,AMOTION_EVENT_BUTTON_PRIMARY, 0,frameSrcSize, dstPos, 1.0));}}}return QObject::eventFilter(watched, event);
}//ScrcpyServer
void ScrcpyServer::sendControl(const QByteArray &controlMsg) {if (controlSocket) {controlSocket->write(controlMsg);}
}

需要注意的是,screenSize参数必须为原视频发送来的图片帧大小,如果界面上的控件进行了缩放,需要按照比例映射到原图片帧位置才能正确的点击。

在这里插入图片描述

demo程序的源代码:https://github.com/daonvshu/qt-scrcpyservice

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_925232.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【STM32】HAL库的STOP低功耗模式UART串口唤醒,第一个接收字节出错的问题(已解决)

【STM32】HAL库的STOP低功耗模式UART串口唤醒&#xff0c;第一个接收字节出错的问题&#xff08;已解决&#xff09; 文章目录 BUG复现调试代码推测原因及改进方案尝试中断时钟供电外设唤醒方式校验码硬件问题 切换到STOP0模式尝试结论和猜想解决方案附录&#xff1a;Cortex-M…

js动态设置关键侦@keyframes

js动态设置关键侦keyframes 1.前置知识 关键侦keyframes规则通过在动画序列中定义关键侦的样式来控制CSS动画序列的中间步骤 keyframes slidein {from {transform: translateX(0%);}to {transform: translateX(100%);} } // from 等价于 0%&#xff1b;to 等价与 100% // 或…

【已解决】C语言进行多线程数据切割查找数据

第一次听到多线程切割&#xff0c;笔者也没听的太懂&#xff0c;但发现多线程数据切割其实就是分出多个线程&#xff0c;进行处理查找数据的事情。而为什么切割呢&#xff0c;就是因为数据不够线程数分的&#xff0c;假如1k个数据&#xff0c;7个线程&#xff0c;这里不能够整除…

RabbitMQ的安装使用

RabbitMQ是什么&#xff1f; MQ全称为Message Queue&#xff0c;消息队列&#xff0c;在程序之间发送消息来通信&#xff0c;而不是通过彼此调用通信。 RabbitMQ 主要是为了实现系统之间的双向解耦而实现的。当生产者大量产生数据时&#xff0c;消费者无法快速消费&#xff0c;…

蓝桥杯备赛 | 洛谷做题打卡day5

蓝桥杯备赛 | 洛谷做题打卡day5 图论起航&#xff0c;一起来看看深&#xff08;广&#xff09;度优先吧 ~ 文章目录 蓝桥杯备赛 | 洛谷做题打卡day5图论起航&#xff0c;一起来看看深&#xff08;广&#xff09;度优先吧 ~【深基18.例3】查找文献题目描述 输入格式输出格式样例…

vue知识-04

计算属性computed 注意&#xff1a; 1、计算属性是基于它们的依赖进行缓存的 2、计算属性只有在它的相关依赖发生改变时才会重新求值 3、计算属性就像Python中的property&#xff0c;可以把方法/函数伪装成属性 4、computed: { ... } 5、计算属性必须要有…

MySQl Mybatis

一、MySQL 1.1 概述 1.1.1 MySQL安装 1.1.2 数据模型 1.1.3 SQL简介 1.2 DDL 1.2.1 数据库操作 1.2.2 图形化工具 1.2.3 表结构操作 &#xff08;一&#xff09;创建 &#xff08;二&#xff09;数据类型 &#xff08;1&#xff09;数值类型 age tinyint unsigned——加上…

Kubernetes 集群管理—日志架构

日志架构 应用日志可以让你了解应用内部的运行状况。日志对调试问题和监控集群活动非常有用。 大部分现代化应用都有某种日志记录机制。同样地&#xff0c;容器引擎也被设计成支持日志记录。 针对容器化应用&#xff0c;最简单且最广泛采用的日志记录方式就是写入标准输出和标…

书生·浦语大模型--第三节课笔记--基于 InternLM 和 LangChain 搭建你的知识库

文章目录 大模型开发范式RAGLangChain框架&#xff1a;构建向量数据库构建检索问答链优化建议web 部署 实践部分环境配置 大模型开发范式 LLM的局限性&#xff1a;时效性&#xff08;最新知识&#xff09;、专业能力有限&#xff08;垂直领域&#xff09;、定制化成本高&#…

测试平台出问题?看我20分钟快速定位!

今天遇到一个问题&#xff0c;感觉挺有意思&#xff0c;处理过程也非常有意义&#xff0c;希望能给大家一个借鉴吧。今天一位小姐姐找到了我们大组长&#xff0c;说测试平台添加自动化测试用例失败&#xff0c;之后我们组长把我拉到了一个群里让我去看一下&#xff0c;硬着头皮…

C++面试宝典第19题:最长公共前缀

题目 编写一个函数来查找字符串数组中的最长公共前缀,如果不存在公共前缀,返回空字符串""。说明:所有输入只包含小写字母a-z。 示例1: 输入: ["flower", "flow", "flight"]输出: "fl" 示例2: 输入: ["dog",…

如何在Windows 10/11的防火墙中禁止和允许某个应用程序,这里提供详细步骤

想阻止应用程序访问互联网吗&#xff1f;以下是如何通过简单的步骤阻止和允许Windows防火墙中的程序。​ 一般来说&#xff0c;大多数用户永远不需要担心应用程序访问互联网。然而&#xff0c;在某些情况下&#xff0c;你需要限制应用程序访问互联网。 例如&#xff0c;有问题…

vue知识-03

购物车案例 要实现的功能&#xff1a; 1、计算商品总价格 2、全选框和取消全选框 3、商品数量的增加和减少 <body> <div id"app"><div class"row"><div class"col-md-6 col-md-offset-3"><h1 class"text-center…

TinyLog iOS v3.0接入文档

1.背景 为在线教育部提供高效、安全、易用的日志组件。 2.功能介绍 2.1 日志格式化 目前输出的日志格式如下&#xff1a; 日志级别/[YYYY-MM-DD HH:MM:SS MS] TinyLog-Tag: |线程| 代码文件名:行数|函数名|日志输出内容触发flush到文件的时机&#xff1a; 每15分钟定时触发…

【Spring 篇】走进SpringMVC的世界:舞动Web的激情

嗨&#xff0c;亲爱的小白们&#xff01;欢迎来到这篇关于SpringMVC的博客&#xff0c;让我们一起探索这个舞动Web的框架&#xff0c;感受它带来的激情和便利。在这个世界里&#xff0c;我们将学到SpringMVC的概述、开发步骤以及如何快速入门&#xff0c;一切都是如此的令人兴奋…

C# Winform翻牌子记忆小游戏

效果 源码 新建一个winform项目命名为Matching Game&#xff0c;选用.net core 6框架 并把Form1.cs代码修改为 using Timer System.Windows.Forms.Timer;namespace Matching_Game {public partial class Form1 : Form{private const int row 4;private const int col 4;p…

为什么光刻要用黄光

光刻是集成电路&#xff08;IC或芯片&#xff09;制造中的重要工艺之一。简单来说&#xff0c;它是通过使用光掩膜和光刻胶在基板上复制电路图案的过程。 基板将涂覆硅二氧化层绝缘层和光刻胶。光刻胶在被紫外光照射后可以容易地用显影剂溶解&#xff0c;然后在腐蚀后&#xf…

MongoDB Compass当前版本及历史版本下载安装

mongoDB compass 当前版本下载 官网 https://www.mongodb.com/try/download/compass 官网下载一般只能下载最新版本。 github https://github.com/mongodb-js/compass MongoDB Compass与MongoDB的版本对应关系 MongoDB CompassMongoDB1.9.12MongoDB 2.6.11 Community

Redis:原理速成+项目实战——Redis企业级项目实战终结篇(HyperLogLog实现UV统计)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位大四、研0学生&#xff0c;正在努力准备大四暑假的实习 &#x1f30c;上期文章&#xff1a;Redis&#xff1a;原理速成项目实战——Redis实战14&#xff08;BitMap实现用户签到功能&#xff09; &#x1f4da;订阅专栏&am…

uniapp-uniCloud的基本使用(编写云存储的地区级联选择器)

目录 新建项目&#xff0c;创建 uniCloud 服务空间并关联 1. 新建项目 2. 创建 uniCloud 服务空间并关联 manifest.json内未配置Appld,请重新获取后再 云数据库的使用 城市选择和云数据库 介绍 云端数据 DB Schema概述 新建项目&#xff0c;创建 uniCloud 服务空间并关…