Qt/C++编写跨平台的推流工具(支持win/linux/mac/嵌入式linux/安卓等)

news2024/11/15 7:19:53

一、前言

跨平台的推流工具当属OBS最牛逼,功能也是最强大的,唯一的遗憾就是多路推流需要用到插件,而且CPU占用比较高,默认OBS的规则是将对应画布中的视频画面和设定的音频一起重新编码再推流,意味着肯定占用不少CPU资源,而很多时候尤其是视频监控行业,往往源头的摄像头的视频流就是标准的264/265/aac格式,其实无需重新编码就可以推流,就相当写文件一样,只需要极低极低的CPU运算资源,一路1080P基本上占用在0.1%以下,而OBS可以直接干到5%,这就差距有点大了,几十倍的差距,毕竟重新编码哪怕是硬编码,肯定需要占用不少的CPU。而用户的真实需求可能是要求500路摄像头推流,用OBS肯定是达不到的,10路都卡成屎,毕竟OBS面对的不是视频监控行业的应用场景,而是视频直播行业,视频直播行业要的是各种特效,画面的自定义,音频的自定义,要求在一路上面可以各种各样的自定义。为此本工具应运而生,主要针对极低资源占用的多路推流,同时能够跨平台运行,包括但不限于windows/linux/mac/安卓/嵌入式linux/树莓派等。

既然要求跨平台,那用Qt开发是首选,OBS本身也是Qt开发的,在各个平台上也能正常编译运行。只要代码中没有特别依赖对应操作系统的API接口函数的,源码都可以直接在其他平台编译运行。其实用ffmpeg做推流,核心就是保存文件,只需要将保存文件的地址改成推流地址即可,是不是很简单,最开始没有做过的时候还以为有多复杂,吓得都不敢开工,估计也有不少人和我一样。

二、效果图

在这里插入图片描述
在这里插入图片描述

三、体验地址

  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. 支持Qt4/Qt5/Qt6任意版本,支持任意系统(windows/linux/macos/android/嵌入式linux等)。

五、相关代码

#include "ffmpegsavesimple.h"

//用法示例(保存文件/推流)
#if 0
FFmpegSaveSimple *f = new FFmpegSaveSimple(this);
f->setUrl("f:/mp4/1.mp4", "f:/1.mp4");
f->setUrl("f:/mp4/1.mp4", "rtmp://127.0.0.1:6908/stream");
f->start();
#endif

FFmpegSaveSimple::FFmpegSaveSimple(QObject *parent) : QThread(parent)
{
    stopped = false;
    audioIndex = -1;
    videoIndex = -1;

    formatCtxIn = NULL;
    formatCtxOut = NULL;

    //初始化ffmpeg的库
    FFmpegHelper::initLib();
}

FFmpegSaveSimple::~FFmpegSaveSimple()
{
    this->stop();
    this->close();
}

void FFmpegSaveSimple::run()
{
    if (!this->open()) {
        this->close();
        return;
    }

    int ret;
    AVPacket packet;
    qint64 videoCount = 0;
    qint64 startTime = av_gettime();

    while (!stopped) {
        //读取一帧
        ret = av_read_frame(formatCtxIn, &packet);
        int index = packet.stream_index;
        if (ret < 0) {
            if (ret == AVERROR_EOF || ret == AVERROR_EXIT) {
                debug(ret, "文件结束");
                break;
            } else {
                debug(ret, "读取出错");
                continue;
            }
        }

        //取出输入输出流的时间基
        AVStream *streamIn = formatCtxIn->streams[index];
        AVStream *streamOut = formatCtxOut->streams[index];
        AVRational timeBaseIn = streamIn->time_base;
        AVRational timeBaseOut = streamOut->time_base;

        if (index == videoIndex) {
            videoCount++;
        }

        //纠正有些文件比如h264格式的没有pts
        if (packet.pts == AV_NOPTS_VALUE) {
            qreal fps = av_q2d(formatCtxIn->streams[videoIndex]->r_frame_rate);
            FFmpegHelper::rescalePacket(&packet, timeBaseIn, videoCount, fps);
        }

        //延时(防止数据太快给缓存造成太大压力)
        if (index == videoIndex) {
            AVRational timeBase = {1, AV_TIME_BASE};
            int64_t ptsTime = av_rescale_q(packet.dts, timeBaseIn, timeBase);
            int64_t nowTime = av_gettime() - startTime;
            if (ptsTime > nowTime) {
                av_usleep(ptsTime - nowTime);
            }
        }

        //重新调整时间基准
        FFmpegHelper::rescalePacket(&packet, timeBaseIn, timeBaseOut);

        qDebug() << TIMEMS << "发送一帧" << videoCount << packet.flags << packet.pts << packet.dts;
        //ret = av_write_frame(formatCtxOut, &packet);
        ret = av_interleaved_write_frame(formatCtxOut, &packet);
        if (ret < 0) {
            debug(ret, "写数据包");
            break;
        }

        av_packet_unref(&packet);
    }

    //写文件尾
    av_write_trailer(formatCtxOut);
    this->close();
}

bool FFmpegSaveSimple::open()
{
    if (urlIn.isEmpty() || urlOut.isEmpty()) {
        return false;
    }

    int ret = -1;
    if ((ret = avformat_open_input(&formatCtxIn, urlIn.toUtf8().constData(), 0, NULL)) < 0) {
        debug(ret, "打开输入");
        return false;
    }
    if ((ret = avformat_find_stream_info(formatCtxIn, 0)) < 0) {
        debug(ret, "无流信息");
        return false;
    }

    videoIndex = av_find_best_stream(formatCtxIn, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if (videoIndex < 0) {
        debug(ret, "无视频流");
        return false;
    }

    //既可以是保存到文件也可以是推流(对应格式要区分)
    const char *format = "mp4";
    if (urlOut.startsWith("rtmp://")) {
        format = "flv";
    } else if (urlOut.startsWith("rtsp://")) {
        format = "rtsp";
    }

    QByteArray urlData = urlOut.toUtf8();
    const char *url = urlData.constData();
    ret = avformat_alloc_output_context2(&formatCtxOut, NULL, format, url);
    if (!formatCtxOut) {
        debug(ret, "创建输出");
        return false;
    }

    //根据输入流创建输出流
    for (int i = 0; i < formatCtxIn->nb_streams; i++) {
        AVStream *streamIn = formatCtxIn->streams[i];
        AVStream *streamOut = avformat_new_stream(formatCtxOut, NULL);
        if (!streamOut) {
            return false;
        }

        //复制各种参数
        ret = FFmpegHelper::copyContext(streamIn, streamOut);
        if (ret < 0) {
            debug(ret, "复制参数");
            return false;
        }        
    }

    //打开输出
    if (!(formatCtxOut->oformat->flags & AVFMT_NOFILE)) {
        ret = avio_open(&formatCtxOut->pb, url, AVIO_FLAG_WRITE);
        if (ret < 0) {
            debug(ret, "打开输出");
            return false;
        }
    }

    //写文件头
    ret = avformat_write_header(formatCtxOut, NULL);
    if (ret < 0) {
        debug(ret, "写文件头");
        return false;
    }

    debug(0, QString("打开成功: %1").arg(urlIn));
    if (format != "mp4") {
        debug(0, QString("开始推流: %1").arg(urlOut));
    } else {
        debug(0, QString("开始录像: %1").arg(urlOut));
    }

    return true;
}

void FFmpegSaveSimple::close()
{
    stopped = false;
    if (formatCtxOut) {
        debug(0, QString("关闭成功: %1").arg(urlIn));
    }

    if (formatCtxIn) {
        avformat_close_input(&formatCtxIn);
        formatCtxIn = NULL;
    }

    if (formatCtxOut && !(formatCtxOut->oformat->flags & AVFMT_NOFILE)) {
        avio_close(formatCtxOut->pb);
    }

    if (formatCtxOut) {
        avformat_free_context(formatCtxOut);
        formatCtxOut = NULL;
    }
}

void FFmpegSaveSimple::debug(int ret, const QString &msg)
{
    QString text = (ret < 0 ? QString("%1 错误: %2").arg(msg).arg(FFmpegHelper::getError(ret)) : msg);
    qDebug() << TIMEMS << text;
}

void FFmpegSaveSimple::setUrl(const QString &urlIn, const QString &urlOut)
{
    this->urlIn = urlIn;
    this->urlOut = urlOut;
}

void FFmpegSaveSimple::stop()
{
    this->stopped = true;
    this->wait();
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DragGAN开源:生成图像流形上的基于点的交互式操作

文旨在解决生成对抗网络&#xff08;GAN&#xff09;中控制生成图像的问题。通过“拖动”图像中的任意点&#xff0c;实现用户交互式精确控制生成图像的姿态、形状、表情和布局。 这个名叫DragGAN的模型&#xff0c;本质上是为各种GAN开发的一种交互式图像操作方法。论文以Sty…

215. 数组中的第K个最大元素

题目描述&#xff1a; 主要思路&#xff1a; 利用堆排序实现第k大的数的查找。 class Solution { public:void maxHeapify(vector<int>& a,int i,int heapsize){int li*2,ri*21,lagesti;if(l<heapsize&&a[l]>a[lagest])lagestl;if(r<heapsize&…

iOS应用上架全攻略

目录 引言 一、基本需求信息。 二、证书 一.证书管理 二.新建证书 三.使用appuploader服务同步证书 三、打包 三、审核 四、整体架构流程 五、代码实现 六、总结 引言 上架IOS应用到app store&#xff0c;需要正式的打包证书、证书profile文件和需要使用专用的工具…

越来越“变态”的验证码,到底在验证什么?

验证码要验证的是它所面对的是真实的人还是计算机程序。最开始的验证码非常的简单&#xff0c;只要输入几个数字就可以。不知道从何时开始见证了变得越来越变态&#xff0c;变得花样不断的验证&#xff0c;验证码就不仅仅是视力的挑战了&#xff0c;有的时候已经是视力及智力的…

CB5309高集成国产2.4 GHz射频前端放大器功放芯片

目录 什么是射频前端&#xff1f;CB5309简介芯片特性 什么是射频前端&#xff1f; 射频前端是射频收发器和天线之间的一系列组件&#xff0c;主要包括功率放大器(PA)、天线开关(Switch)、滤波器(Filter)、双工器(Duplexer和Diplexer)和低噪声放大器(LNA)等&#xff0c;对射频信…

【Nginx】第七章 Nginx原理与优化参数配置

7.1 Nginx原理 master-workers的机制的好处 首先&#xff0c;对于每个worker进程来说&#xff0c;独立的进程&#xff0c;不需要加锁&#xff0c;所以省掉了锁带来的开销&#xff0c;同时在编程以及问题查找时&#xff0c;也会方便很多。 其次&#xff0c;采用独立的进程&…

如何直接在线抠图人像?掌握这两个方法,轻松编辑你的照片!

在日常工作和生活中&#xff0c;我们经常需要对照片进行抠图操作&#xff0c;特别是对人像进行抠图&#xff0c;以便更换背景或添加特效。然而&#xff0c;对于那些没有接受过专门培训的人来说&#xff0c;使用复杂的图像编辑软件可能会感到非常困惑和无所适从。别担心&#xf…

如何设置微信小程序启动页及其全屏背景色?

一、设置启动页 打开微信小程序就会进入pages里面的第一个页面&#xff0c;所以只需要在pages.json中&#xff0c;把启动页写在pages的第一项就可以了 二、去掉导航栏&#xff0c;实现全屏显示效果 先清除全局的导航栏标题&#xff0c;在需要全屏的页面&#xff0c;添加以下代…

初级应急响应-Windows-常用命令

命令&#xff1a;regedit 说明&#xff1a;注册表 命令&#xff1a;Taskmgr 说明&#xff1a;任务管理器 命令&#xff1a;Msconfig 说明&#xff1a;系统配置&#xff08;包含启动项&#xff09; 命令&#xff1a;eventvwr.msc 说明&#xff1a;事件查看器 命令&#xff1a;co…

前端(vue)npm如何发布自己的包

1.首先vue create xxx创建一个空的项目&#xff08;lib和local文件夹怎么来看后面的步骤&#xff09; 2.将自己的方法或者组建文件夹放在src同层目录下&#xff0c;我这边是local文件夹 3.在APP.vue页面引入本地文件测试自己的方法有没有问题 4.在package.json中的scripts中配置…

3D轻量化引擎HOOPS Communicator中的反向代理

一、HOOPS Communicator概述 HOOPS Communicator由三个主要组件组成&#xff1a;Web查看器、服务器和数据创作工具 &#xff08;1&#xff09;Web GL Viewer&#xff1a;该组件嵌入在客户端的Web浏览器中&#xff0c;负责显示CAD数据、PMI视图、属性、测量、数据标记等。 服…

【Flutter】包管理(7)Flutter 状态管理 BLoC 从基础到实践

文章目录 一、前言二、BLoC 的基本概念三、在 Flutter 中使用 BLoC四、BLoC 的高级用法五、BLoC 的最佳实践六、购物车应用的实例七、总结一、前言 在 Flutter 开发中,状态管理是一个非常重要的话题。正确的状态管理策略可以使我们的代码更加清晰,更易于维护。 本文将深入探…

【Linux】详解进程控制 ( 再谈进程退出 | 程序替换exec*类型函数 )

再谈进程退出进程程序替换引入程序替换原理有哪些替换函数execl:execlp&#xff1a;execv:execvp:execle: execve: 接续上篇博客 “详解进程控制 ( fork函数 | 写时拷贝 | 进程退出 | 进程等待 )” 再谈进程退出 进程退出会变成僵尸状态&#xff0c;将自己的推出结果写入task_…

芯片等高科技制造业 如何实现安全的跨网数据交换?

芯片是信息产业的基础&#xff0c;一直以来占据全球半导体产品超过80%的销售额&#xff0c;在计算机、家用电器、数码电子、自动化、电气、通信、交通、医疗、航空航天等几乎所有的电子设备领域中都有使用。 所以&#xff0c;对于芯片这种高科技制造业来说&#xff0c;数据的安…

装饰模式(Decorator)

别名 装饰者模式&#xff08;Wrapper&#xff09;。 定义 装饰是一种结构型设计模式&#xff0c;允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。 前言 1. 问题 假设你正在开发一个提供通知功能的库&#xff0c;其他程序可使用它向用户发送关于重…

Debezium系列之:Debezium 通知

Debezium系列之&#xff1a;Debezium 通知 一、概述二、Debezium 通知格式三、可用的通知四、启用 Debezium 通知五、访问 Debezium JMX 通知六、自定义通知渠道七、配置自定义通知渠道八、Debezium 核心模块依赖项九、部署自定义通知渠道十、配置连接器以使用自定义通知通道 一…

MFC加载3ds模型初步

网上下一个资源&#xff0c;名为 OpenGL三维场景绘制.rar&#xff1b; 看一下它是用MFC和opengl&#xff0c;自己绘制三维场景&#xff1b; 运行一下&#xff0c;有一个exe可以运行&#xff1b; 有一个较新版本的不能运行&#xff1b;这应是缺少VC运行库&#xff1b; 下面单独…

Linux下RPM软件包管理

目录 1、软件包管理介绍1.1、软件包分类1.2、源码包1.3、RPM包 2、RPM包管理-包命名和依赖性2.1、RPM命名规则2.2、RPM包依赖性 3、RPM包管理-安装升级和与卸载3.1、包全名与包名3.2、RPM安装3.3、RPM包升级3.4、卸载 4、RPM包管理-查询4.1、查询是否安装4.2、查询软件包详细信…

Gitlab将本地代码推送到远程空仓库

目录 引言 1、设置Git为源代码管理插件 2、创建Git仓库 3、设置多个远程仓库 引言 如果我们的本地代码想上传到公司内部的服务器&#xff0c;首先我们需要在VS2022中创建Git仓库&#xff0c;然后设置远程仓库的地址&#xff0c;才能将本地代码推送到远端。在远端会根据你本地…

表格式表单-table式from表单-合并行-合并列

效果: 使用【colspan】合并行 和【rowspan】合并列 html: <!-- 添加或修改报告数据库对话框 --><el-dialog :title"title" :visible.sync"open" width"1500px" append-to-body><el-form ref"form" :model"form&q…