Qt/C++编写跨平台的推流工具(支持win/linux/mac/嵌入式linux/安卓等)
一、前言
跨平台的推流工具当属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做推流,核心就是保存文件,只需要将保存文件的地址改成推流地址即可,是不是很简单,最开始没有做过的时候还以为有多复杂,吓得都不敢开工,估计也有不少人和我一样。
二、效果图
三、体验地址
- 国内站点:https://gitee.com/feiyangqingyun
- 国际站点:https://github.com/feiyangqingyun
- 个人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
- 体验地址:https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g 提取码:01jf 文件名:bin_video_push。
四、功能特点
- 支持各种本地视频文件和网络视频文件。
- 支持各种网络视频流,网络摄像头,协议包括rtsp、rtmp、http。
- 支持将本地摄像头设备推流,可指定分辨率和帧率等。
- 支持将本地桌面推流,可指定屏幕区域和帧率等。
- 自动启动流媒体服务程序,默认mediamtx(原rtsp-simple-server),可选用srs、EasyDarwin、LiveQing、ZLMediaKit等。
- 可实时切换预览视频文件。
- 推流的清晰度和质量可调。
- 可动态添加文件、目录、地址。
- 视频文件自动循环推流,如果视频源是视频流,在掉线后会自动重连。
- 网络视频流自动重连,重连成功自动继续推流。
- 网络视频流实时性极高,延迟极低,延迟时间大概在100ms左右。
- 极低CPU占用,4路主码流推流只需要占用0.2%CPU。理论上常规普通PC机器推100路毫无压力,主要性能瓶颈在网络。
- 推流可选推流到rtsp/rtmp两种,推流后的数据支持直接rtsp/rtmp/hls/webrtc四种方式访问,可以直接浏览器打开看实时画面。
- 可以推流到外网服务器,然后通过手机、电脑、平板等设备播放对应的视频流。
- 每个推流都可以手动指定唯一标识符(方便拉流/用户无需记忆复杂的地址),没有指定则按照策略随机生成hash值。
- 自动生成测试网页直接打开播放,可以看到实时效果,自动按照数量对应宫格显示。
- 推流过程中可以在表格中切换对应推流项,实时预览正在推流的视频,并可以切换视频文件的播放进度。
- 音视频同步推流,符合264/265/aac格式的自动原数据推流,不符合的自动转码再推流(会占用一定CPU)。
- 转码策略支持三种,自动处理(符合要求的原数据/不符合的转码),仅限文件(文件类型的转码视频),所有转码。
- 表格中实时显示每一路推流的分辨率和音视频数据状态,灰色表示没有输入流,黑色表示没有输出流,绿色表示原数据推流,红色表示转码后的数据推流。
- 支持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();
}
Qt/C++编写跨平台的推流工具(支持win/linux/mac/嵌入式linux/安卓等)的更多相关文章
- 亲测有效 Hyper V3.4.0 终端美化工具 支持win/mac
亲测有效 Hyper V3.4.0 终端美化工具 支持win/mac Hyper 是一款终端美化工具 基于Web技术,JS/HTML/CSS ,支持扩展增强,很不错! 且支持win,mac 下载地址 ...
- LiveBlox无需代码的开发工具--支持win macos ubuntu等开发环境--
LiveBlox无需代码的开发工具-支持windows macos ubuntu. 强大 灵活 易于使用 视频简介:LiveBlox Develop Technology Without Coding ...
- WinSetupFromUSB - 超简单制作多合一系统安装启动U盘的工具 (支持Win/PE/Linux启动盘)
很多同学都喜欢将电脑凌乱不堪的系统彻底重装以获得一个"全新的开始",但你会发现如今很多电脑都已经没有光驱了,因此制作一个U盘版的系统安装启动盘备用是非常必要的. 我们之前推荐过 I ...
- 在Quick-cocos2dx中使用云风pbc解析Protocol Buffers,支持win、mac、ios、android
本例主要介绍 如何将 pbc 集成到quick-cocos2dx框架中,让我们的cocos2dx客户端Lua拥有编解码Protocol Buffers能力. 参考: 云风pbc的用法: http:// ...
- 使用Qt installer framework制作安装包(不知道是否适合Mac和Linux?)
一.介绍 使用Qt库开发的应用程序,一般有两种发布方式:(1)静态编译发布.这种方式使得程序在编译的时候会将Qt核心库全部编译到一个可执行文件中.其优势是简单单一,所有的依赖库都集中在一起,其缺点也很 ...
- Navicat15 For Mysql最新版完美破解图文教程(支持Win和Mac)
Navicat15 For Mysql最新版完美破解 欢迎关注博主公众号[跟着Mic学架构],专注于分享Java领域技术干货,回复关键字 [面试资料] 可以获得海量面试资料. 申明,本教程 Navic ...
- 跨平台自动构建工具v1.0.2 发布
XMake是一个跨平台自动构建工具,支持在各种主流平台上构建项目,类似cmake.automake.premake,但是更加的方便易用,工程描述语法更简洁直观,支持平台更多,并且集创建.配置.编译.打 ...
- 几款抓包工具在windows,mac,linux下的支持分析
抓包工具的使用 几款抓包工具在windows,mac,linux下的支持分析 抓包工具简介 Chrome/Firefox 开发者工具: 浏览器内置,方便易用 Fiddler/Charles: 基于代理 ...
- 新成员!Visual Studio Code --跨平台的开发工具(支持OSX, Linux 和 Windows)
原文出处:新成员!Visual Studio Code --跨平台的开发工具(支持OSX, Linux 和 Windows) 这是我的文章备份 http://www.dotblogs.com.tw/ ...
- Sublime Text 2 - 性感无比的代码编辑器!程序员必备神器!跨平台支持Win/Mac/Linux
我用过的编辑器不少,真不少- 但却没有哪款让我特别心仪的,直到我遇到了 Sublime Text 2 !如果说“神器”是我能给予一款软件最高的评价,那么我很乐意为它封上这么一个称号.它小巧绿色且速度非 ...
随机推荐
- kotlin类与对象——>委托、委托属性
1.委托 1.1 委托的实现 委托模式已经证明是实现继承的一个很好的替代方式,而 Kotlin 可以零样板代码地原生支持它. Derived 类可以通过将其所有公有成员都委托给指定对象来实现一个接口 ...
- NOI 2024
Day1 T1 集合(set) 容易发现两个序列等价当且仅当,所有数字在序列中出现位置的集合构成集族相等. 考虑哈希,对于一个集合 \(S\),令它的哈希值为 \(f(S) = (\sum\limit ...
- 在 openEuler 22.03 上安装 KubeSphere 实战教程
作者:老 Z,中电信数智科技有限公司山东分公司运维架构师,云原生爱好者,目前专注于云原生运维,云原生领域技术栈涉及 Kubernetes.KubeSphere.DevOps.OpenStack.Ans ...
- OpenFunction v1.0.0 发布:集成 WasmEdge,支持 Wasm 函数和更完整的 CI/CD
OpenFunction 是一个开源的云原生 FaaS(Function as a Service,函数即服务)平台,旨在帮助开发者专注于业务逻辑的研发.今天,我们非常高兴地宣布 OpenFuncti ...
- 使用Radzen Blazor组件库开发的基于ABP框架炫酷UI主题
一.项目简介 使用过ABP框架的童鞋应该知道它也自带了一款免费的Blazor UI主题,它的页面是长这样的: 个人感觉不太美观,于是网上搜了很多Blazor开源组件库,发现有一款样式非常不错的组件库, ...
- SSIS连接Excel2007版本之后的数据源
今天我发现了新大陆,兴奋得不得了,由于原文写得太过详细与专业,我就偷偷懒直接Copy过来了,怕自己以后没地儿找,哈哈哈 原文链接:https://www.cnblogs.com/biwork/p/34 ...
- 初识GO语言--错误处理
- IDEA方法模板、类模板设置记录
类模板 设置入口 Mac:common+,进入设置->Editor->File and Code Templates->Includes 模版代码 /** * todo * * @a ...
- 接口自动化框架【python+requests+pytest+allure】需要安装的依赖包
attrs == 23.2.0 certifi == 2024.2.2 cffi == 1.16.0 charset-normalizer == 3.3.2 colorama == 0.4.6 cry ...
- 通过Java代码发送OutLook邮件
准备 我们想通过Java代码实现发送OutLook邮件,必须准备以下材料: OutLook邮箱 目标邮箱 查看OutLook邮箱信息 打开OutLook邮箱,在Settings中搜索或找到SMTP: ...