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

news/2024/11/24 21:42:36/

一、前言

跨平台的推流工具当属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();
#endifFFmpegSaveSimple::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格式的没有ptsif (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.ppmy.cn/news/520196.html

相关文章

leetcode416. 分割等和子集(动态规划-java)

分割等和子集 leetcode416. 分割等和子集题目描述 暴力递归代码演示 动态规划解题思路代码演示 动态规划专题 leetcode416. 分割等和子集 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode.cn/problems/partition-equal-subset-sum 题目…

0325心得体会

上午学习了 table标签下的 cellspacing\cellpaddingimg标签的三个属性 src alt title 下午接着学了超链接 a标签 它的绝对路径 相对路径 锚点链接总体来说今天的学习收获还是可以的&#xff0c;自己还是需要多练习代码 及难点 易混淆 <table border"1px" width&qu…

中国互联网创业工具库Startup Tools

http://hi.baidu.com/longniao/item/39f13c31084ecdb9633aff81 中国互联网创业工具库Startup Tools 一、第三方创业投资服务平台 1、科技及投资类媒体&#xff08;原创类文章为主&#xff09; 36氪&#xff0c;http://www.36kr.com/ 银海网&#xff0c;http://www.inhai.com/ …

敏捷:从卖皮肤到融入血液

一块死去的敏捷看板 这篇文章&#xff0c;通过一些职场碎片记忆&#xff0c;讲述了我如何从狂热的Agile布道师&#xff0c;转变成一个想从心底抹去Agile烙印的生意人。 Take an economic view 1 这周&#xff0c;在朋友圈看到自己3年前在H公司的照片&#xff0c;颇为感慨。 当时…

【算法】Mice and Cheese 老鼠和奶酪 Greedy

文章目录 Mice and Cheese 老鼠和奶酪问题描述&#xff1a;分析代码 Tag Mice and Cheese 老鼠和奶酪 问题描述&#xff1a; 有两只老鼠和 n 块不同类型的奶酪&#xff0c;每块奶酪都只能被其中一只老鼠吃掉。 下标为 i 处的奶酪被吃掉的得分为&#xff1a; 如果第一只老鼠…

报名软件批次分类code不能为空_为什么金蝶入库单保存时提示批号不能为空

商贸通10.2新增、改进功能和各版本功能差异对比 一、10.2版改进功能&#xff1a; 产品功能 服装 服装普 财务增强包(同标准版10。1) 标准 普 1、基本信息 多编码功能增强 商品档案中可按往来单位类设置 √ √ √ √ 商品重复录入控制 [系统配置]可设置&#xff1a;商品编…

多服务器虚拟化 map_服务器、存储和网络虚拟化的实现与应用

虚拟化技术已经成为数据中心必备的技术之一&#xff0c;那什么叫虚拟化技术呢&#xff1f;虚拟化是一个广义的术语&#xff0c;在计算机方面通常是指计算元件在虚拟的基础上而不是真实的基础上运行。虚拟化技术可以扩大硬件的容量&#xff0c;简化软件的重新配置过程。CPU的虚拟…

中国互联网创业大咖(收藏)

一、第三方创业投资服务平台 1、科技及投资类媒体&#xff08;原创类文章为主&#xff09; 36氪&#xff0c;http://www.36kr.com/ Tech2ipo&#xff0c;http://tech2ipo.com 动点科技Technode&#xff0c;http://www.technode.com/ 爱范儿&#xff0c;http://www.ifanr.c…