一、前言

这个一边推流一边录制的功能,有很多用户提到过,之前因为时间的原因,一直没有搞,年初的时候索性抽空搞了下,也着实费了些功夫。推流用的是ffmpeg这个开源的牛逼的第三方库,搞音视频开发的人应该没人不认识这个库,养活了很多程序员以及厂家,甚至不乏一些大厂,如果能把ffmpeg搞精通,在国内拿个30K以上毫无压力分分钟的事情。但是这个库也确实有一定的难度,起码对我来说还是挺难的,按照提供的示例做点简单的demo没有问题,要搞稳定搞兼容性以及兼容各种需求场景,就非常难了,没有个好几年的捣鼓,很难搞好。

推流的前提是拉流,之前已经用ffmpeg做了拉流和保存,既可以保存到本地视频文件,也可以保存到rtsp/rtmp这种地址,保存到流地址其实就是推流,以前没搞过的时候还以为多复杂,原来就是保存文件改个地址,总共就改动几行代码就行。既然已经可以推流和保存,那说明一边推流一边录制也是可行的。为了追求最简方式实现,通过在原来的基础上,增加一个信号,也就是在保存的时候发出去一个avpacket的包信号,这个包是最终要写入到文件的数据包,如果源头就是264/265这种格式的数据,这个包可以直接存入文件即可。于是一边推一边存的功能,就是重新new一个ffmpegsave类,将之前推流的类发出来的收到数据包信号,发给存储类的槽即可,非常简单方便易用。打完收工完美实现,又是去沙县加鸡腿的一天。

二、效果图

三、体验地址

  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。
  5. 视频主页:https://space.bilibili.com/687803542

四、功能特点

  1. 支持各种本地音视频文件和网络音视频文件,格式包括mp3、aac、wav、wma、mp4、mkv、rmvb、wmv、mpg、flv、asf等。
  2. 支持各种网络音视频流,网络摄像头,协议包括rtsp、rtmp、http等。
  3. 支持本地摄像头设备推流,可指定分辨率、帧率、格式等。
  4. 支持本地桌面采集推流,可指定屏幕索引、采集区域、起始坐标、帧率等,也支持指定窗口标题进行采集。
  5. 可实时切换预览视频文件,可切换音视频文件播放进度,切换到哪里就推流到哪里。预览过程中可以切换静音状态和暂停推流。
  6. 可指定重新编码推流,任意源头格式可选强转264或265格式。
  7. 可转换分辨率推流,设置等比例缩放或者指定分辨率进行转换。
  8. 推流的清晰度、质量、码率都可调,可以节约网络带宽和拉流端的压力。
  9. 音视频文件自动循环不间断推流。
  10. 音视频流有自动掉线重连机制,重连成功自动继续推流。
  11. 支持各种流媒体服务程序,包括但不限于mediamtx、ZLMediaKit、srs、LiveQing、nginx-rtmp、EasyDarwin、ABLMediaServer。
  12. 通过配置文件自动加载对应流媒体程序的协议和端口,自动生成推流地址和各种协议的拉流地址。可以通过配置文件自己增加流媒体程序。
  13. 可选rtmp、rtmp格式推流,推流成功后,支持多种格式拉流,包括但不限于rtsp、rtmp、hls、flv、ws-flv、webrtc等。
  14. 在软件上推流成功后,可以直接单击网页预览,实时预览推流后拉流的画面,多画面网页展示。
  15. 软件界面上可单击对应按钮,动态添加文件和目录,可手动输入地址。
  16. 推拉流实时性极高,延迟极低,延迟时间大概在100ms左右。
  17. 极低CPU资源占用,4路主码流推流只需要占用0.2%CPU。理论上常规普通PC机器推100路毫无压力,主要性能瓶颈在网络。
  18. 可以推流到外网服务器,然后通过手机、电脑、平板等设备播放对应的视频流。
  19. 每路推流都可以手动指定唯一标识符(方便拉流/用户无需记忆复杂的地址),没有指定则按照策略随机生成hash值。也支持自动按照指定标识后面加数字的方式递增命名。比如设置标识为字母v,策略为标识递增,则每添加一个对应的推流码命名依次是v1、v2、v3等。
  20. 根据推流协议自动转码格式,默认策略按照选择的推流协议,比如rtsp支持265而rtmp不支持,如果是265的文件而选择rtmp推流,则自动转码成264格式再推流。
  21. 音视频同步推流,在拉流和采集的时候就会自动处理好同步,同步后的数据再推流。
  22. 表格中实时显示每一路推流的分辨率和音视频数据状态,灰色表示没有输入流,黑色表示没有输出流,绿色表示原数据推流,红色表示转码后的数据推流。
  23. 自动重连视频源,自动重连流媒体服务器,保证启动后,推流地址和打开地址都实时重连,只要恢复后立即连上继续采集和推流。
  24. 根据不同的流媒体服务器类型,自动生成对应的rtsp、rtmp、hls、flv、ws-flv、webrtc拉流地址,用户可以直接复制该地址到播放器或者网页中预览查看。
  25. 添加的推流地址等信息自动存储到文件,可以手动打开进行修改,默认启动后自动加载历史记录。
  26. 可以指定生成的网页文件保存位置,方便作为网站网页发布,可以直接在浏览器中输入网址进行访问,发布后可以直接在局域网其他设备比如手机或者电脑打开对应网址访问。
  27. 可选是否开机启动、后台运行等。网络推流添加的rtsp地址可勾选是否隐藏地址中的用户信息。
  28. 自带设备推流模块,自动识别本地设备,包括本地的摄像头和桌面,可以手动选择不同的是视频和音频采集设备进行推流。
  29. 自带文件点播模块,添加文件后用户可以拉取地址点播,用户端可以任意切换播放进度。支持各种浏览器(谷歌chromium、微软edge、火狐firefox等)、各种播放器(vlc、mpv、ffplay、potplayer、mpchc等)打开请求。
  30. 文件点播模块实时统计显示每个文件对应的访问数量、总访问数量、不同IP地址访问数量。
  31. 文件点播模块采用纯QTcpSocket通信,不依赖流媒体服务程序,核心源码不到500行,注释详细,功能完整。
  32. 支持任意Qt版本(Qt4、Qt5、Qt6),支持任意系统(windows、linux、macos、android、嵌入式linux等)。

五、相关代码

#include "netpushclient.h"
#include "ffmpegthread.h"
#include "ffmpegsave.h"
#include "videohelper.h"
#include "osdgraph.h" bool NetPushClient::checkB = false;
bool NetPushClient::recordInteger = false;
int NetPushClient::recordDuration = 0;
int NetPushClient::encodeVideo = 0;
float NetPushClient::encodeVideoRatio = 1;
QString NetPushClient::encodeVideoScale = "1"; NetPushClient::NetPushClient(QObject *parent) : QObject(parent)
{
ffmpegThread = NULL;
ffmpegSave = NULL; //定时器控制多久录制一个文件
timerRecord = new QTimer(this);
timerRecord->setInterval(1000);
connect(timerRecord, SIGNAL(timeout()), this, SLOT(checkRecord()));
} NetPushClient::~NetPushClient()
{
this->stop();
} QString NetPushClient::getMediaUrl()
{
return this->mediaUrl;
} QString NetPushClient::getPushUrl()
{
return this->pushUrl;
} FFmpegThread *NetPushClient::getVideoThread()
{
return this->ffmpegThread;
} void NetPushClient::checkRecord()
{
//0. 时长单位分钟/触发条件自动重新录像/recordDuration=0/表示禁用录像
//1. recordInteger参数控制是否整数倍数录像/recordDuration参数控制录制文件时长/整数倍录像下时长为对应的模数
//2. 一般监控行业会按照整点录像/比如30分钟60分钟一个视频文件/这样录制的文件起始时间和结束时间整整齐齐
//3. 整点录像情况下除了第一个和最后一个录像文件可能时长不一样/中间的文件肯定时长都一样
//4. 非整点录像就按照录像总时长计时/所有保存的文件都是按照时长保存的 //5. recordInteger=true/recordDuration=5/表示每到5分钟的时候录制一个文件
//6. 上面录制结果: 11:01开始录制/11:05结束上一个录制并重新录制/第一个文件时长4分钟
//7. recordInteger=false/recordDuration=5/表示每过5分钟的时候录制一个文件
//8. 上面录制结果: 11:01开始录制/11:06结束上一个录制并重新录制/第一个文件时长5分钟 bool ok = false;
QDateTime now = QDateTime::currentDateTime();
qint64 offset = recordTime.msecsTo(now);
if (recordInteger && recordDuration > 1) {
QTime time = now.time();
int min = time.minute();
int sec = time.second();
min = (min == 0 ? 60 : min);
ok = ((min % recordDuration == 0) && sec >= 0 && sec <= 2);
//qDebug() << TIMEMS << min << sec << (min % recordDuration == 0) << offset << ok;
} else {
ok = (offset >= (recordDuration * 60 * 1000));
//qDebug() << TIMEMS << recordDuration << offset << ok;
} if (ok && offset >= 5000) {
this->record();
}
} void NetPushClient::record()
{
if (ffmpegSave) {
//取出推流码
QString flag = pushUrl.split("/").last();
//文件名不能包含特殊字符/需要替换成固定字母
QString pattern("[\\\\/:|*?\"<>]|[cC][oO][mM][1-9]|[lL][pP][tT][1-9]|[cC][oO][nM]|[pP][rR][nN]|[aA][uU][xX]|[nN][uU][lL]");
#if (QT_VERSION >= QT_VERSION_CHECK(6,0,0))
QRegularExpression rx(pattern);
#else
QRegExp rx(pattern);
#endif
flag.replace(rx, "X"); //文件名加上时间结尾
QString path = QString("%1/video/%2").arg(qApp->applicationDirPath()).arg(QDATE);
QString name = QString("%1/%2_%3.mp4").arg(path).arg(flag).arg(STRDATETIME); //目录不存在则新建
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(path);
} //先停止再打开重新录制
ffmpegSave->stop();
ffmpegSave->open(name);
recordTime = QDateTime::currentDateTime();
}
} void NetPushClient::receivePlayStart(int time)
{
//演示添加OSD后推流
#ifdef betaversion
int height = ffmpegThread->getVideoHeight();
QList<OsdInfo> osds = OsdGraph::getTestOsd(height);
ffmpegThread->setOsdInfo(osds);
#endif //打开后才能启动录像
ffmpegThread->recordStart(pushUrl); //推流以外还单独存储
if (!ffmpegSave && recordDuration > 0) {
//源头保存没成功就不用继续
FFmpegSave *saveFile = ffmpegThread->getSaveFile();
if (!saveFile->getIsOk()) {
return;
} ffmpegSave = new FFmpegSave(this);
//重新编码过的则取视频保存类的对象
AVStream *videoStreamIn = saveFile->getVideoEncode() ? saveFile->getVideoStream() : ffmpegThread->getVideoStream();
AVStream *audioStreamIn = saveFile->getAudioEncode() ? saveFile->getAudioStream() : ffmpegThread->getAudioStream();
ffmpegSave->setSavePara(ffmpegThread->getMediaType(), SaveVideoType_Mp4, videoStreamIn, audioStreamIn);
this->record();
timerRecord->start();
}
} void NetPushClient::receivePacket(AVPacket *packet)
{
if (ffmpegSave && ffmpegSave->getIsOk()) {
ffmpegSave->writePacket2(packet);
} FFmpegHelper::freePacket(packet);
} void NetPushClient::recorderStateChanged(const RecorderState &state, const QString &file)
{
int width = 0;
int height = 0;
int videoStatus = 0;
int audioStatus = 0;
if (ffmpegThread) {
width = ffmpegThread->getVideoWidth();
height = ffmpegThread->getVideoHeight();
FFmpegSave *saveFile = ffmpegThread->getSaveFile();
if (saveFile->getIsOk()) {
if (saveFile->getVideoIndexIn() >= 0) {
if (saveFile->getVideoIndexOut() >= 0) {
videoStatus = (saveFile->getVideoEncode() ? 3 : 2);
} else {
videoStatus = 1;
}
}
if (saveFile->getAudioIndexIn() >= 0) {
if (saveFile->getAudioIndexOut() >= 0) {
audioStatus = (saveFile->getAudioEncode() ? 3 : 2);
} else {
audioStatus = 1;
}
}
}
} //只有处于录制中才表示正常推流开始
bool start = (state == RecorderState_Recording);
emit pushStart(mediaUrl, width, height, videoStatus, audioStatus, start);
} void NetPushClient::receiveSaveStart()
{
emit pushChanged(mediaUrl, 0);
} void NetPushClient::receiveSaveFinsh()
{
emit pushChanged(mediaUrl, 1);
} void NetPushClient::receiveSaveError(int error)
{
emit pushChanged(mediaUrl, 2);
} void NetPushClient::setMediaUrl(const QString &mediaUrl)
{
this->mediaUrl = mediaUrl;
} void NetPushClient::setPushUrl(const QString &pushUrl)
{
this->pushUrl = pushUrl;
} void NetPushClient::start()
{
if (ffmpegThread || mediaUrl.isEmpty() || pushUrl.isEmpty()) {
return;
} //实例化视频采集线程
ffmpegThread = new FFmpegThread;
//关联播放开始信号用来启动推流
connect(ffmpegThread, SIGNAL(receivePlayStart(int)), this, SLOT(receivePlayStart(int)));
//关联录制信号变化用来判断是否推流成功
connect(ffmpegThread, SIGNAL(recorderStateChanged(RecorderState, QString)), this, SLOT(recorderStateChanged(RecorderState, QString)));
//设置播放地址
ffmpegThread->setMediaUrl(mediaUrl); //设置视频模式
#ifdef openglx
ffmpegThread->setVideoMode(VideoMode_Opengl);
#else
ffmpegThread->setVideoMode(VideoMode_Painter);
#endif //设置通信协议(如果是rtsp视频流建议设置tcp)
//ffmpegThread->setTransport("tcp");
//设置硬解码(和推流无关/只是为了加速显示/推流只和硬编码有关)
//ffmpegThread->setHardware("dxva2");
//设置缓存大小(如果分辨率帧率码流很大需要自行加大缓存)
ffmpegThread->setCaching(8192000);
//设置解码策略(推流的地址再拉流建议开启最快速度)
//ffmpegThread->setDecodeType(DecodeType_Fastest); //设置读取超时时间超时后会自动重连
ffmpegThread->setReadTimeout(10 * 1000);
//设置连接超时时间(0表示一直连)
ffmpegThread->setConnectTimeout(0);
//设置重复播放相当于循环推流
ffmpegThread->setPlayRepeat(true);
//设置默认不播放音频(界面上切换到哪一路就开启)
ffmpegThread->setPlayAudio(false);
//设置默认不预览视频(界面上切换到哪一路就开启)
ffmpegThread->setPushPreview(false); //设置保存视频类将数据包信号发出来用于保存文件
FFmpegSave *saveFile = ffmpegThread->getSaveFile();
saveFile->setProperty("checkB", checkB);
saveFile->setSendPacket(recordDuration > 0, false);
connect(saveFile, SIGNAL(receivePacket(AVPacket *)), this, SLOT(receivePacket(AVPacket *)));
connect(saveFile, SIGNAL(receiveSaveStart()), this, SLOT(receiveSaveStart()));
connect(saveFile, SIGNAL(receiveSaveFinsh()), this, SLOT(receiveSaveFinsh()));
connect(saveFile, SIGNAL(receiveSaveError(int)), this, SLOT(receiveSaveError(int))); //如果是本地设备或者桌面录屏要取出其他参数
VideoHelper::initVideoPara(ffmpegThread, mediaUrl, encodeVideoRatio, encodeVideoScale); //设置视频编码格式/视频压缩比率/视频缩放比例
ffmpegThread->setEncodeVideo((EncodeVideo)encodeVideo);
ffmpegThread->setEncodeVideoRatio(encodeVideoRatio);
ffmpegThread->setEncodeVideoScale(encodeVideoScale); //启动播放
ffmpegThread->play();
} void NetPushClient::stop()
{
//停止推流和采集并彻底释放对象
if (ffmpegThread) {
ffmpegThread->recordStop();
ffmpegThread->stop();
ffmpegThread->deleteLater();
ffmpegThread = NULL;
} //停止录制
if (ffmpegSave) {
timerRecord->stop();
ffmpegSave->stop();
ffmpegSave->deleteLater();
ffmpegSave = NULL;
}
}

Qt边推流边录制/实时性好延迟低/16路1080P推流加录制只占1%CPU/优化到极致的更多相关文章

  1. 【原创】有利于提高xenomai 实时性的一些配置建议

    版权声明:本文为本文为博主原创文章,转载请注明出处.如有错误,欢迎指正. @ 目录 一.影响因素 1.硬件 2.BISO(X86平台) 3.软件 4. 缓存使用策略与GPU 二.优化措施 1. BIO ...

  2. CNC系统实时性分析

    该系统有哪些强实时功能需求?需要对哪些实时事件进行实时响应,对允许的实时延迟的数量级进行估计. 答:数控系统中控制装置与现场设备通信的实时性要求,数控系统要快速而有效的对复杂而庞大的操作任务进行处理. ...

  3. Linux 2.6 内核实时性分析 (完善中...)

      经过一个月的学习,目前对linux 下驱动程序的编写有了入门的认识,现在需要着手实践,编写相关的驱动程序. 因为飞控系统对实时性有一定的要求,所以先打算学习linux 2.6 内核的实时性与任务调 ...

  4. 转:JMeter--使用代理录制Web性.能测试脚.本

    Apache JMeter是一款纯Java的应用程序,用于对软件或系统做性.能测试,如压力测试.负载测试.最初设计是用于web应用测试,由于开源其测试组件不断被扩充,逐步扩展到其他测试领域中. 接下给 ...

  5. PROFINET如何实现实时性

    平时我们都听过文艺作品要“源于生活而高于生活”.PROFINET是基于工业以太网的,用文艺范儿的词汇说就是“源于以太网而高于以太网”.那么,PROFINET是怎么做到“高于以太网”的呢? 要做到比普通 ...

  6. 什么是PROFINET IO系统的实时性

    实时系统是指系统能及时响应外部事件的请求,在规定的时间内完成对该事件的处理,并控制所有实时任务协调一致的运行. PROFINET IO系统的实时性就是指当有一个外部事件发生时,从输入信号到传输.到控制 ...

  7. javaCV开发详解之2:推流器实现,推本地摄像头视频到流媒体服务器以及摄像头录制视频功能实现(基于javaCV-FFMPEG、javaCV-openCV)

    javaCV系列文章: javacv开发详解之1:调用本机摄像头视频 javaCV开发详解之2:推流器实现,推本地摄像头视频到流媒体服务器以及摄像头录制视频功能实现(基于javaCV-FFMPEG.j ...

  8. 用Vue开发一个实时性时间转换功能,看这篇文章就够了

    前言 最近有一个说法,如果你看见某个网站的某个功能,你就大概能猜出背后的业务逻辑是怎么样的,以及你能动手开发一个一毛一样的功能,那么你的前端技能算是进阶中高级水平了.比如咱们今天要聊的这个话题:如何用 ...

  9. 针对Properties中实时性要求不高的配置参数,用Java缓存起来

    Properties常用于项目中参数的配置,当项目中某段程序需要获取动态参数时,就从Properties中读取该参数,使程序是可配置的.灵活的. 有些配置参数要求立即生效,有些则未必: 一.实时性要求 ...

  10. 为树莓派添加一个强实时性前端[原创cnblogs.com/helesheng]

    树莓派是最近流行嵌入式平台,其自由的开源特性以及低廉的价格,吸引了来 自全球的大量极客和计算机大咖的关注.来自各大树莓派社区的幕后英雄,无私地在这个开源硬件平台上做了大量的工作,将其打造成了世界上通用 ...

随机推荐

  1. 1. 什么是three.js?

    Three.js是一个基于JavaScript编写的开源3D图形库,它使用WebGL技术在网页上渲染3D图形.Three.js提供了许多高级功能,如几何体.纹理.光照.阴影等,以便开发者能够快速地创建 ...

  2. ADO.NET 事务 transaction

    事务回滚:

  3. SpringBoot进阶教程(八十二)Spring Security图形验证码

    在之前的博文<SpringBoot进阶教程(八十)Spring Security>中,已经介绍了在Spring Security中如何基于formLogin认证.基于HttpBasic认证 ...

  4. web服务器静态资源下载

    1. 使用 Beego 实现静态文件下载 Beego 是一个强大的 Go Web 框架,提供了处理静态文件的功能.通过简单的配置,我们可以将本地文件夹作为静态资源目录,并为用户提供下载链接. 1.1 ...

  5. OpenFunction 成为 CNCF 沙箱项目,使 Serverless 函数与应用运行更简单

    2022 年 4 月 27 日,青云科技容器团队开源的函数即服务(FaaS: Function-as-a-Service)项目 OpenFunction 顺利通过了云原生计算基金会 CNCF 技术监督 ...

  6. 【Kernel】基于 QEMU 的 Linux 内核编译和安装

    目录 安装虚拟机系统 共享目录 编译内核 卸载内核 参考资料 本文主要记录个人做存储系统研究时,在 QEMU 环境下编译和安装 Linux 内核的过程 安装虚拟机系统 之前在 利用 RocksDB + ...

  7. AI 居然说我是牛马,还画出了我牛马的一生,我绷不住了...

    今天真是服了,AI 居然敢嘲笑我是牛马,还直接甩了张大图到我脸上. 看来我的人生在 AI 眼中就是个笑话,从 "初级牛马" 一路升级到 "资深牛马".真是谢谢你 ...

  8. 程序员如何借势AI提高自己:从高效工作到技能升级的全面指南

    又是一年1024,时光荏苒,转眼又到了这个特别的日子.坦白说,这篇文章我其实并不太想写,因为我并没有通过AI找到普适于程序员群体的高效赚钱秘籍.然而,反思过去的工作,我发现利用AI的确让我在工作中变得 ...

  9. Excel 导入解析数据 NPOIExcelHelper

    前端代码 html <link href="~/lib/bootstrap-fileinput/css/fileinput.min.css" media="all& ...

  10. Grafana如何利用Karpenter消除50%的云资源浪费?|落地案例

    原文链接: https://grafana.com/blog/2023/11/09/how-grafana-labs-switched-to-karpenter-to-reduce-costs-and ...