Qt+ffmpeg+x264远程协助软件Weekday技术原理及源码剖析

前言:

很久没有打理博客了。最近有点烦,teamviewer用的挺习惯的,突然不香了。虽然改用mstsc+加自己云服务器的方式也还行。但突然就萌发了为啥不自己试着写一个的想法。刚好项目空档几天,于是便有了该项目。至于为什么叫Weekday?大概因为热爱工作的缘故吧。由于只是写着玩的,所以没有大量的去做压力测试。基本功能还可以,代码风格符合本人习惯。技术涉及抓屏截图,图像处理,视频编码,网络推流,视频解码,远程控制等,学习价值大于实用价值。
看以前的博客下面有朋友留言讨论,很抱歉没有回复,因为确实没有经常关注博客,有兴趣的朋友可以加微信好友讨论技术或索要源码学习。

一、功能说明和测试

Weekday目前只支持局域网下的两台windows电脑进行远程协助(当然也可以自己用公网服务器进行双向代理实现外网远控,不懂的也可以咨询本人,这不是本文重点)。后续可以扩展一些办公的小功能,截图、录屏等等。有兴趣的朋友也可以自己扩展添加。

编译好的Weekday软件链接,感兴趣的可以免费下载测试:
https://download.csdn.net/download/u013752202/21463939

下面是release后的测试截图:
需要源码或编译好的ffmpeg+libx264动态库的可以扫描下面测试界面的二维码加微信,留言可能不能及时回复,请见谅。

  1. server启动后等待连接
    server启动后等待连接
  2. 客户端连接
    客户端连接3. client端显示server的桌面
    本地控制本地,当client和server位于同一台电脑的时候会出现“套娃”现象,这是正常的。client和server不在同一台电脑不会出现。
    本地控制本地
    client和server不在同一台电脑,没有“套娃”现象。图中红点表示鼠标。可以点击操控。
    server端桌面显示

二、技术原理

1. 通信流程
简单的通信流程大致如下:

client server 连接请求 1. 抓屏 2. 编码 屏幕码流 解码显示 控制数据 应用控制数据 client server

2. 抓屏截图
为了简单,直接使用Qt的抓屏api实现,所以帧率最快只能达到20-25帧。有志之士可以自己从网卡旁路。以 达到更高的帧率。
由于抓屏比较耗时,所以抓屏代码在WindowSource线程中运行。抓屏之后再跟进客户端需要进行缩放处理。对比了Qt的scaled和opencv的双线性差值,Qt的效率高三四倍,效果也是一样的。
抓屏代码:

void WindowSource::slotWork()
{
    while(!stopCmd){
        QPixmap vQPixmap=pQScreen->grabWindow(QApplication::desktop()->winId(),0,0,1920,1080);
        srcW=vQPixmap.width();
        srcH=vQPixmap.height();
        vQPixmap=vQPixmap.scaled(dstW,dstH,Qt::KeepAspectRatio);
        QImage vQImage=vQPixmap.toImage().convertToFormat(QImage::Format_RGB888,Qt::NoAlpha);
        vQImageVec.append(vQImage);
    }
    runFlag=0;
}

3. 编码压缩
由于网络带宽资源宝贵,获取到截图后,需要对截图进行编码,减少传输数据量。编码压缩也是十分耗时的操作,所以跟UI和抓屏都是分开线程操作。工程中应用ffmpeg+libx264进行编码。并对其进行了封装。
下面是获取屏幕截图,并进行编码:

    while(WeekdayPCState_Working==vWeekdayPCState){
        if(0==pWindowSource->vQImageVec.size()){
            QThread::usleep(20000);
            continue;
        }

        QImage vQImage=pWindowSource->vQImageVec[0];
        windowW=pWindowSource->srcW;
        windowH=pWindowSource->srcH;
        vTimeStamps.fmtChange1=TimeScaleGetMsec();
        WImageRGB242YUV420P((U8 *)vQImage.constBits(),imgw,imgh,windowNv12);

        streamBuf=NULL;
        vTimeStamps.encode=TimeScaleGetMsec();
        err=WFF_EncoderStreamGet(pEncoder,windowNv12,&streamBuf,&streamSz);
        if(err&&-11!=err){
            error(WeekdayPCError_Encoder);
            break;
        }

        if(streamBuf&&streamSz>0){
            //send to client
            vWKDFrame.seq++;
            vWKDFrame.window.mouseX=QCursor::pos().x();
            vWKDFrame.window.mouseY=QCursor::pos().y();
            vWKDFrame.window.width=pWindowSource->srcW;
            vWKDFrame.window.height=pWindowSource->srcH;
            vWKDFrame.window.pStreamBuf=streamBuf;
            vWKDFrame.window.streamSz=streamSz;

            sendWindowFrame();

            WFF_EncoderStreamRelease(pEncoder);
            //QThread::usleep(50000);
        }
        pWindowSource->vQImageVec.remove(0);
    }

4. 通信协议
为了简单,这里采用基于TCP的私有协议。
协议格式如下:

#pragma pack(1)
//**********S->C
typedef struct{
    U32 byteRate;
}Statis;

typedef struct{
    U16 mouseX;
    U16 mouseY;
    U16 width;
    U16 height;
    U32 streamSz;
    union{
        U8 streamBuf[0];
        U8 *pStreamBuf;
    };
}WKDWindow;

typedef struct{
    U32 len;
    U32 seq;
    WKDFrameType_t type;
    Statis statis;
    WKDWindow window;
}WKDFrame;

//**********C->S
typedef struct{
    U8 pressed;
    U8 val;
}WKDLKey;

typedef struct{
    U16 x;
    U16 y;
    MouseAct_t act;
}WKDMouse;

typedef struct{
    U16 delta;
}WKDWheel;

typedef struct{
    U32 len;
    U32 seq;
    WKDCmdType_t type;
    U16 scaledW;
    U16 scaledH;
    U32 byteRate;
    WKDLKey key;
    WKDMouse mouse;
    WKDWheel wheel;
}WKDCmd;

#pragma pack()

5. 解码显示
客户端接收到码流后,需要对码流进行解码然后显示到界面上。解码显示依然使用封装的ffmpeg接口进行。

            U8 naluType=pWKDFrame->window.streamBuf[4]&0x1f;
            if(7==naluType||8==naluType||5==naluType){
                spsPpsOk=1;
            }
            serverWindowWidth=pWKDFrame->window.width;
            serverWindowHeight=pWKDFrame->window.height;
            vWeekdayStatis.byteRate=pWKDFrame->statis.byteRate;
            vWKDCThreadCallback.statisComming(vWKDCThreadCallback.arg,vWeekdayStatis);
            if(spsPpsOk){
                if((err=WFF_DecoderDecode(pWFF_Decoder,pWKDFrame->window.streamBuf,
                                          pWKDFrame->window.streamSz))<0){
                    error(WKD_ERR_Decoder);
                    emit signalError((int)WKD_ERR_Decoder);
                }
            }
            dataFromServer=dataFromServer.mid(pWKDFrame->len);
        }

显示则直接使用Qt的painter进行绘制,没有使用opengl 2d,实际测试效率也还行。

void WeekdayView::paintEvent(QPaintEvent *ev)
{
    QPainter painter(this);
    mutex.lock();
    painter.drawImage(QPoint(0,0),image);
    painter.drawText(QRect(0,this->height()-20,this->width(),this->height()),text);
    QBrush brush(QColor(255,0,0));
    painter.setBrush(brush);
    painter.drawEllipse(QPoint(mouseX,mouseY),5,5);
    mutex.unlock();    
}

6. 远程控制
最后就是远控了,client端重载鼠标键盘事件并按照协议发给server,server端根据协议解析出事件并应用即可。

    vWKDCmdType=WeekdayProtocol::WKDCmdGetPacType((U8 *)buf,len);
    pWKDCmd=(WKDCmd *)buf;
    if(WKDCmdType_START==vWKDCmdType){
        pWeekdayPC->grabWindowStart(pWKDCmd);
    }
    else if(WKDCmdType_STOP==vWKDCmdType){
        pWeekdayPC->grabWindowStop();
    }
    else if(WKDCmdType_Ctl==vWKDCmdType){
        devx=SYS_DEV_POS(pWKDCmd->mouse.x,pWeekdayPC->windowW);
        devy=SYS_DEV_POS(pWKDCmd->mouse.y,pWeekdayPC->windowH);
        SysDevice::mouseMoveTo(devx,devy);

        if(MouseAct_LeftDown==pWKDCmd->mouse.act){
            SysDevice::mouseLeftDown(devx,devy);
        }
        else if(MouseAct_LeftUp==pWKDCmd->mouse.act){
            SysDevice::mouseLeftUp(devx,devy);
        }
        else if(MouseAct_LeftDoubleClick==pWKDCmd->mouse.act){
            SysDevice::mouseLeftDoubleClick(devx,devy);
        }
        else if(MouseAct_RightDown==pWKDCmd->mouse.act){
            SysDevice::mouseRightDown(devx,devy);
        }
        else if(MouseAct_RightUp==pWKDCmd->mouse.act){
            SysDevice::mouseRightUp(devx,devy);
        }
        else if(MouseAct_RightDoubleClick==pWKDCmd->mouse.act){
            SysDevice::mouseRightDoubleClick(devx,devy);
        }
        else if(MouseAct_MiddleDown==pWKDCmd->mouse.act){
            SysDevice::mouseMiddleDown(devx,devy);
        }
        else if(MouseAct_MiddleUp==pWKDCmd->mouse.act){
            SysDevice::mouseMiddleUp(devx,devy);
        }

        if(pWKDCmd->key.pressed){
            SysDevice::keyPressed(pWKDCmd->key.val);
        }
        else{
            SysDevice::keyReleased(pWKDCmd->key.val);
        }

    }

三、技术展望

本工程为爱好、学习使用,很多地方实现的比较粗糙。大致需要优化的一些点为:

1. 抓拍截图

最好直接从网卡进行旁路,一方面可以达到更高帧率,另一方面可以获取到登录界面。

2. 编码压缩

可以对编码压缩的参数进行调优,以获得带宽更稳定的码流。或更换为H265编码,同清晰度下,降低网络传输压力。

3. 网络推流

使用UDP分片传输,在网络带宽不够的情况下,降低视频延时。

4. 解码显示

通过opengl 2d纹理进行绘制,降低CPU使用率。

四、功能扩展

待实现:

1. 本地或远程录屏

对编码后的码流进行本地和远程封装成MP4或flv等格式。增加录屏功能。

2. 本地和远程剪切板访问和同步

监听剪切板状态,实现本地和远程共享剪切板。方便复制和粘贴。

3. 关于非局域网应用

如果需要外网远程控制,则需要自己的公网服务器。通过代理工具双向代理server和client的端口即可实现外网 远程控制,至于P2P内网穿透,现在路由大部分都是对称NAT,基本上不用想了。工程中数据没有加密,如果外网远 控,建议使用openssl进行数据加密后进行测试。

五、项目依赖

项目依赖ffmpeg和libx264,需要自行下载或编译动态库。

编译好的动态库(也可加微信免费提供):

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

叶落西湘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值