## 一、前言说明
推流组件写了这么久,主要都是推720P/1080P/2K这种分辨率的,现在突然间来了用户需要8K,一开始心里没底,后面试了下效果,远超预期好的一逼,CPU资源占用居然是0%,你说谁看到了不震惊,毕竟分辨率摆在这里,你要想想用播放器去播放器8K的
文件,动不动就99%的占用,而且很多人电脑都根本无法播放8K,一打开就卡死。为什么推流这里可以做到这么低的资源占用呢?主要得益于之前做的一个策略处理,那就是如果源头是264/265流,直接不用解码就可以推流,通过av_read_frame取出来的avpacket包直接转发出去即可,完全不用解码,这样就不涉及到资源占用,而写入那边基本上的压力都在网络带宽,如果是写入文件则压力在磁盘写入的速度。
我在推流组件的
开发上投入了不少时间,一直以来主要处理的都是 720P、1080P、2K 这类分辨率的推流需求,对于 8K 推流,起初心里确实没什么底。但当我们实际尝试后,效果却远超预期,好到让人惊喜。最让人震惊的是,8K 推流时 CPU 资源占用居然能达到 0%。要知道,用播放器播放 8K 文件时,CPU 占用动不动就飙升到 99%,而且很多电脑根本无法承受 8K 播放的压力,一打开就会卡死。那为什么推流在 8K 分辨率下能做到这么低的资源占用呢?这主要得益于我们之前制定的一个策略处理。具体来说,如果推流的源头是 264/265 流,我们就采用直接推流的方式,不需要进行解码。通过 av_read_frame 取出来的 avpacket 包可以直接转发出去,整个过程完全不涉及解码操作,自然也就不会占用过多资源。在这种情况下,推流时的压力主要集中在网络带宽上,如果是将内容写入文件,那么压力则转移到了磁盘的写入速度上。这种策略充分利用了原始流的特性,避开了解码这个高资源消耗的环节,从而实现了 8K 推流时的高效表现。
## 二、效果图

## 三、相关代码
```cpp
#include "ffmpegsavesimple.h"
#include "ffmpegsavehelper.h"
//用法示例(保存文件/推流)
#if 0
FFmpegSaveSimple *f = new FFmpegSaveSimple(this);
f->setUrl("f:/mp4/push/1.mp4", "f:/1.mp4");
f->setUrl("f:/mp4/push/1.mp4", "rtmp://127.0.0.1/stream");
f->start();
#endif
FFmpegSaveSimple::FFmpegSaveSimple(QObject *parent) : QThread(parent)
{
stopped = false;
saveFile = false;
audioIndex = -1;
videoIndex = -1;
formatCtxIn = NULL;
formatCtxOut = NULL;
//初始化ffmpeg的库
FFmpegHelper::initLib();
}
FFmpegSaveSimple::~FFmpegSaveSimple()
{
this->stop();
this->close();
}
void FFmpegSaveSimple::run()
{
if (!this->openInput() || !this->openOutput()) {
this->close();
return;
}
int ret;
AVPacket packet;
qint64 videoCount = 0;
qint64 startTime = av_gettime();
while (!stopped) {
//读取一帧
int index = packet.stream_index;
if ((ret = av_read_frame(formatCtxIn, &packet)) < 0) {
if (ret == AVERROR_EOF || ret == AVERROR_EXIT) {
debug(0, "文件结束");
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++;
} else if (index == audioIndex) {
}
//纠正有些文件比如h264格式的没有pts
if (packet.pts == AV_NOPTS_VALUE) {
qreal fps = av_q2d(formatCtxIn->streams[videoIndex]->r_frame_rate);
FFmpegSaveHelper::rescalePacket(&packet, timeBaseIn, videoCount, fps);
}
//推流需要延时/防止
数据过大撑爆缓存
if (!saveFile && index == videoIndex) {
FFmpegHelper::delayTime(formatCtxIn, &packet, startTime);
}
//重新调整时间基准
FFmpegSaveHelper::rescalePacket(&packet, timeBaseIn, timeBaseOut);
qDebug() << TIMEMS << "发送一帧" << index << videoCount << packet.flags << packet.pts << packet.dts;
if ((ret = av_interleaved_write_frame(formatCtxOut, &packet)) < 0) {
debug(ret, "写数据包");
break;
}
av_packet_unref(&packet);
}
//写文件尾
if ((ret = av_write_trailer(formatCtxOut)) < 0) {
debug(ret, "写文件尾");
}
this->close();
}
bool FFmpegSaveSimple::openInput()
{
int ret = -1;
if ((ret = avformat_open_input(&formatCtxIn, urlIn.toUtf8().constData(), NULL, NULL)) < 0) {
debug(ret, "打开输入");
return false;
}
if ((ret = avformat_find_stream_info(formatCtxIn, NULL)) < 0) {
debug(ret, "无流信息");
return false;
}
audioIndex = av_find_best_stream(formatCtxIn, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
videoIndex = av_find_best_stream(formatCtxIn, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if (videoIndex < 0) {
debug(ret, "无视频流");
return false;
}
debug(0,
QString("打开成功: %1").arg(urlIn));
return true;
}
bool FFmpegSaveSimple::openOutput()
{
int ret = -1;
saveFile = (!urlOut.contains("://"));
const char *format = FFmpegSaveHelper::getFormat(urlOut);
QByteArray urlData = urlOut.toUtf8();
const char *url = urlData.constData();
if ((ret = avformat_alloc_output_context2(&formatCtxOut, NULL, format, url)) < 0) {
debug(ret, "创建输出");
return false;
}
//根据输入流创建输出流
for (int i = 0; i < formatCtxIn->nb_streams; i++) {
AVStream *streamIn = formatCtxIn->streams
;
AVStream *streamOut = avformat_new_stream(formatCtxOut, NULL);
if (!streamOut) {
return false;
}
if ((ret = FFmpegHelper::copyContext(streamIn, streamOut)) < 0) {
debug(ret, "复制参数");
return false;
}
}
if ((ret = avio_open(&formatCtxOut->pb, url, AVIO_FLAG_WRITE)) < 0) {
debug(ret, "打开输出");
return false;
}
if ((ret = avformat_write_header(formatCtxOut, NULL)) < 0) {
debug(ret, "写文件头");
return false;
}
debug(0, QString("开始%2: %1").arg(urlOut).arg(saveFile ? "保存" : "推流"));
return true;
}
void FFmpegSaveSimple::close()
{
stopped = false;
if (formatCtxOut) {
avformat_close_input(&formatCtxOut);
formatCtxOut = NULL;
debug(0, QString("关闭输出: %1").arg(urlOut));
}
if (formatCtxIn) {
avformat_close_input(&formatCtxIn);
formatCtxIn = NULL;
debug(0, QString("关闭输入: %1").arg(urlIn));
}
}
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();
}
```
## 四、相关地址
1. 国内站点:[https://gitee.com/feiyangqingyun](https://gitee.com/feiyangqingyun)
2. 国际站点:[https://github.com/feiyangqingyun](https://github.com/feiyangqingyun)
3. 个人作品:[https://blog.csdn.net/feiyangqingyun/article/details/97565652](https://blog.csdn.net/feiyangqingyun/article/details/97565652)
4. 文件地址:[https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g](https://pan.baidu.com/s/1d7TH_GEYl5nOecuNlWJJ7g) 提取码:01jf 文件名:bin_video_demo。
## 五、功能特点
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等)。