Qt/C++音视频开发52-采集本地屏幕桌面的终极设计

发布时间 2023-09-12 09:00:09作者: 飞扬青云

一、前言

最开始设计的时候,只考虑了一个屏幕的情况,这种当然是最理想的情况,实际上双屏或者多屏的用户也不在少数,比如我这两个屏幕,屏幕1是1080P,屏幕2是2K分辨率,打印两个屏幕的区域是 QRect(0,0 1920x1030), QRect(1920,-208 2560x1390),可以看到有个负数值(可以在操作系统中的排列显示器拖动调整),而且如果屏幕左右的顺序调换下,比如2K的分辨率在前面,打印的屏幕区域是 QRect(0,0 1920x1030), QRect(-2560,-185 2560x1390),可以看到2K的这个屏幕XY坐标都是负数,你以为这就是所有的情况了吗?那就想错了,还有可能是上下屏幕排列的,2K屏幕在下面打印区域 QRect(0,0 1920x1030), QRect(-639,1080 2560x1390),2K屏幕在上面打印区域是 QRect(0,0 1920x1030), QRect(-270,-1440 2560x1390),这还支持两个屏幕的情况,如果是4个或者更多呢,如果要用户获取到对应屏幕的区域然后填入桌面录制参数中,无异于难于上青天,这肯定是不可能的事情,而ffmpeg默认的参数就是要传入真实的偏移值坐标和分辨率,而用户呢又习惯于在哪个屏幕打开的程序就以当前屏幕的分辨率为基准,偏移值以左上角(0, 0)为基准,所以约定用户只需要填入分辨率和相对偏移值就行,不填入就以当前屏幕整体分辨率为准,这就需要搞一个专门的转换函数,专门获取当前屏幕区域并计算各种情况。

经过上面大费周折的计算,以为可以关机回家吃饭加鸡腿了,又想多了,用户可能输入了超过当前分辨率的区域,或者偏移值加上采集分辨率超过了当前屏幕的分辨率,这样是无法打开的,无法正常采集,程序不会执行,为了能够增强健壮性兼容性,有需要做一些调整,比如计算后发现设定的采集区域尺寸超过了屏幕的真实分辨率尺寸,就以设定的偏移值开始到右下角为准裁剪,这样无论用户怎么错,程序就是不会错,都能正常采集,以合理的方式进行调整,这才是一个好的程序设计。

演示视频:https://www.bilibili.com/video/BV1D8411B7eP

至此已实现的采集桌面的功能:

  • 支持多屏幕,可以指定屏幕索引。
  • 支持左右排列和上下排列以及自由调整屏幕位置。
  • 支持指定采集区域。
  • 自动校正超过屏幕区域的参数设定。
  • 指定相对偏移值采集,以桌面左上角为基准。
  • 支持指定采集帧率。
  • 不填写分辨率和各种参数,自动计算默认值。
  • 不指定屏幕则以鼠标所在当前屏幕为准。
  • 还有更多的细节在代码中体现。

格式说明:

  1. url地址格式说明:desktop=desktop|800x600|25|0|0|0。
  2. 参数用英文竖杠隔开,其中desktop=是固定前缀,用于区分当前地址是用来采集桌面。
  3. 第一个参数表示设备标识,比如win上填desktop,linux填:0.0+0,0,mac填0:0。
  4. 第二个参数表示采集的分辨率,不填则默认取屏幕分辨率。
  5. 第三个参数表示帧率,基本上在2-30之间,不填的话默认ffmpeg会设定一个值,有时候是30。
  6. 第四/五个参数表示偏移值XY坐标,从屏幕的左上角(0,0)开始。
  7. 第六个参数表示屏幕索引,不填的话则默认取当前鼠标所在屏幕。
  8. 写法1:desktop=desktop,当前屏幕全屏采集。
  9. 写法2:desktop=desktop||15|0|0|1,屏幕2全屏采集,帧率15。
  10. 写法3:desktop=desktop|800x600|10|50|100,鼠标所在当前屏幕采集,采集区域rect(50,100,800,600),帧率10。

二、效果图

三、体验地址

  1. 国内站点:https://gitee.com/feiyangqingyun
  2. 国际站点:https://github.com/feiyangqingyun
  3. 个人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
  4. 体验地址:https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g 提取码:01jf 文件名:bin_video_push。

四、功能特点

  1. 支持各种本地视频文件和网络视频文件。
  2. 支持各种网络视频流,网络摄像头,协议包括rtsp、rtmp、http。
  3. 支持将本地摄像头设备推流,可指定分辨率和帧率等。
  4. 支持将本地桌面推流,可指定屏幕区域和帧率等。
  5. 自动启动流媒体服务程序,默认mediamtx(原rtsp-simple-server),可选用srs、EasyDarwin、LiveQing、ZLMediaKit等。
  6. 可实时切换预览视频文件,可切换视频文件播放进度,切换到哪里就推流到哪里。
  7. 推流的清晰度和质量可调。
  8. 可动态添加文件、目录、地址。
  9. 视频文件自动循环推流,如果视频源是视频流,在掉线后会自动重连。
  10. 网络视频流自动重连,重连成功自动继续推流。
  11. 网络视频流实时性极高,延迟极低,延迟时间大概在100ms左右。
  12. 极低CPU占用,4路主码流推流只需要占用0.2%CPU。理论上常规普通PC机器推100路毫无压力,主要性能瓶颈在网络。
  13. 推流可选推流到rtsp/rtmp两种,推流后的数据支持直接rtsp/rtmp/hls/webrtc四种方式访问,可以直接浏览器打开看实时画面。
  14. 可以推流到外网服务器,然后通过手机、电脑、平板等设备播放对应的视频流。
  15. 每个推流都可以手动指定唯一标识符(方便拉流/用户无需记忆复杂的地址),没有指定则按照策略随机生成hash值。
  16. 自动生成测试网页直接打开播放,可以看到实时效果,自动按照数量对应宫格显示。
  17. 推流过程中可以在表格中切换对应推流项,实时预览正在推流的视频,并可以切换视频文件的播放进度。
  18. 音视频同步推流,符合264/265/aac格式的自动原数据推流,不符合的自动转码再推流(会占用一定CPU)。
  19. 转码策略支持三种,自动处理(符合要求的原数据/不符合的转码),仅限文件(文件类型的转码视频),所有转码。
  20. 表格中实时显示每一路推流的分辨率和音视频数据状态,灰色表示没有输入流,黑色表示没有输出流,绿色表示原数据推流,红色表示转码后的数据推流。
  21. 自动重连视频源,自动重连流媒体服务器,保证启动后,推流地址和打开地址都实时重连,只要恢复后立即连上继续采集和推流。
  22. 提供循环推流示例,一个视频源同时推流到多个流媒体服务器,比如打开一个视频同时推流到抖音/快手/B站等,可以作为录播推流,列表循环,非常方便实用。
  23. 根据不同的流媒体服务器类型,自动生成对应的rtsp/rtmp/hls/flv/ws-flv/webrtc地址,用户可以直接复制该地址到播放器或者网页中预览查看。
  24. 编码视频格式可以选择自动处理(源头是264就264/源头是265就265),转H264(强制转264),转H265(强制转265)。
  25. 支持Qt4/Qt5/Qt6任意版本,支持任意系统(windows/linux/macos/android/嵌入式linux等)。

五、相关代码

void AbstractVideoThread::checkDeviceUrl(const QString &url, QString &deviceName, QString &resolution, int &frameRate, int &offsetX, int &offsetY, QString &encodeScale)
{
    //无论是否带分隔符第一个约定是设备名称
    QStringList list = url.split("|");
    int count = list.count();
    deviceName = list.at(0);

    //默认不指定屏幕索引
    int screenIndex = -1;
    //用一个无用的参数作为是否是本地摄像头的标志位
    bool isCamera = (encodeScale == "camera");

    //带分隔符说明还指定了分辨率或帧率
    if (count > 1) {
        QStringList sizes = WidgetHelper::getSizes(list.at(1));
        if (sizes.count() == 2) {
            int width = sizes.at(0).toInt();
            int height = sizes.at(1).toInt();
            resolution = QString("%1x%2").arg(width).arg(height);
        } else {
            resolution = "0x0";
        }

        //第三个参数是帧率
        if (count >= 3) {
            frameRate = list.at(2).toInt();
        }

        //桌面采集还需要取出其他几个参数
        if (!isCamera) {
            //XY坐标偏移值
            if (count >= 5) {
                offsetX = list.at(3).toInt();
                offsetY = list.at(4).toInt();
            }

            //屏幕索引
            if (count >= 6) {
                screenIndex = list.at(5).toInt();
            }

            //视频缩放
            if (count >= 7) {
                encodeScale = list.at(6);
            }

            WidgetHelper::checkRect(screenIndex, resolution, offsetX, offsetY);
        }
    }

    //没有设置分辨率则重新处理
    if (resolution == "0x0") {
        if (isCamera) {
            resolution = "640x480";
        } else {
            WidgetHelper::checkRect(screenIndex, resolution, offsetX, offsetY);
        }
    }
}

QList<QRect> WidgetHelper::getScreenRects()
{
    QList<QRect> rects;
#if (QT_VERSION >= QT_VERSION_CHECK(5,0,0))
    int screenCount = qApp->screens().count();
    QList<QScreen *> screens = qApp->screens();
    for (int i = 0; i < screenCount; ++i) {
        QScreen *screen = screens.at(i);
        rects << screen->geometry();
    }
#else
    int screenCount = qApp->desktop()->screenCount();
    QDesktopWidget *desk = qApp->desktop();
    for (int i = 0; i < screenCount; ++i) {
        rects << desk->screenGeometry(i);
    }
#endif
    return rects;
}

QRect WidgetHelper::getScreenRect(int screenIndex)
{
    //指定了屏幕索引则取指定的(没有指定则取当前鼠标所在屏幕)
    QList<QRect> rects = WidgetHelper::getScreenRects();
    if (screenIndex >= 0 && screenIndex < rects.count()) {
        return rects.at(screenIndex);
    } else {
        //当前屏幕区域包含当前鼠标所在坐标则说明是当前屏幕
        QPoint pos = QCursor::pos();
        foreach (QRect rect, rects) {
            if (rect.contains(pos)) {
                return rect;
            }
        }
    }
}

QString WidgetHelper::getResolution(int width, int height)
{
    //取偶数(虚拟机中很可能是奇数的分辨率)
    if (width % 2 != 0) {
        width--;
    }

    if (height % 2 != 0) {
        height--;
    }

    return QString("%1x%2").arg(width).arg(height);
}

QString WidgetHelper::getResolution(const QString &resolution)
{
    QStringList sizes = WidgetHelper::getSizes(resolution);
    return getResolution(sizes.at(0).toInt(), sizes.at(1).toInt());
}

void WidgetHelper::checkRect(int screenIndex, QString &resolution, int &offsetX, int &offsetY)
{
    QRect rect = WidgetHelper::getScreenRect(screenIndex);
    if (resolution == "0x0") {
        resolution = WidgetHelper::getResolution(rect.width(), rect.height());
    } else {
        resolution = WidgetHelper::getResolution(resolution);
    }

    //偏移值必须小于分辨率否则重置
    if (offsetX > rect.width()) {
        offsetX = 0;
    }
    if (offsetY > rect.height()) {
        offsetY = 0;
    }

    //判断设定的偏移值加上设定的分辨率是否超出了真实的分辨率
    QStringList sizes = WidgetHelper::getSizes(resolution);
    int width = sizes.at(0).toInt();
    int height = sizes.at(1).toInt();

    if (offsetX + width > rect.width()) {
        width = rect.width() - offsetX;
    }
    if (offsetY + height > rect.height()) {
        height = rect.height() - offsetY;
    }

    //如果超出了分辨率则重新设置采集的分辨率
    resolution = WidgetHelper::getResolution(width, height);

    //多个屏幕需要加上屏幕起始坐标
    if (offsetX == 0) {
        offsetX = rect.x();
    } else {
        offsetX += rect.x();
    }
    if (offsetY == 0) {
        offsetY = rect.y();
    } else {
        offsetY += rect.y();
    }

    //qDebug() << TIMEMS << screenIndex << offsetX << offsetY << resolution;
}