全网首发/Qt结合ffmpeg实现rist推拉流/可信赖的互联网流媒体协议/跨平台支持各个系统

一、前言说明

现在音视频时代发展真快,各种协议层出不穷,一个是满足现在的需求,一个是为了满足新的需求,之前搞过rtmp、rtsp、srt、udp推拉流,现在又新出了个rist,乍一看还以为是rtsp的堂弟,其实不搭边的,RIST的全称是"Reliable Internet Stream Transport"(可信赖的互联网流媒体协议)​,它是一种专注于高效、安全传输音视频流的开放标准协议。RIST协议凭借其低延迟、高可靠性和广播支持能力,已成为流媒体传输领域的重要选择。​对于需要大规模分发或复杂网络环境的应用(如直播、远程制作),RIST是更优解;而对于简单的点对点传输,SRT可能更轻量化​。未来,随着5G和边缘计算的普及,RIST在实时交互场景中的潜力将进一步释放。

毫无疑问,作为宇宙第一音视频轮子的ffmpeg,支持rist也是必然的,大概从ffmpeg6开始就支持,用的是开源的rist库,不过支持很不友好,只能说偶尔可用,能够理解,毕竟这玩意才是刚出来的新鲜玩意,个人推荐用ffmpeg8,要稳定很多,但是偶尔还是会崩溃,可能还需要经过一段时间的磨合。现在ffmpeg飙版本很厉害,基本上一两年就一个大的版本,有些API接口还不兼容,导致网上很多文章提供的方法都行不通。用ffmpeg做rist的推拉流,和之前rtsp的推拉流完全一致,只不过封装容器和udp一样是mpegts,拉流的时候,地址前面要加个@符号,原因未知,可能这是协议规定要求吧,也有可能是ffmpeg内部的约定,比如推流地址rist://192.168.0.110:9001,拉流地址就是rist://@192.168.0.110:9001,如果不这样填,拉流不成功。

二、效果图


三、相关地址

  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_demo。

四、功能特点

  1. 同时支持多种解码内核,包括qmedia内核(Qt4/Qt5/Qt6)、ffmpeg内核(ffmpeg2/ffmpeg3/ffmpeg4/ffmpeg5/ffmpeg6)、vlc内核(vlc2/vlc3)、mpv内核(mpv1/mp2)、mdk内核、海康sdk、easyplayer内核等。
  2. 非常完善的多重基类设计,新增一种解码内核只需要实现极少的代码量,就可以应用整套机制,极易拓展。
  3. 同时支持多种画面显示策略,自动调整(原始分辨率小于显示控件尺寸则按照原始分辨率大小显示,否则等比缩放)、等比缩放(永远等比缩放)、拉伸填充(永远拉伸填充)。所有内核和所有视频显示模式下都支持三种画面显示策略。
  4. 同时支持多种视频显示模式,句柄模式(传入控件句柄交给对方绘制控制)、绘制模式(回调拿到数据后转成QImage用QPainter绘制)、GPU模式(回调拿到数据后转成yuv用QOpenglWidget绘制)。
  5. 支持多种硬件加速类型,ffmpeg可选dxva2、d3d11va等,vlc可选any、dxva2、d3d11va,mpv可选auto、dxva2、d3d11va,mdk可选dxva2、d3d11va、cuda、mft等。不同的系统环境有不同的类型选择,比如linux系统有vaapi、vdpau,macos系统有videotoolbox。
  6. 解码线程和显示窗体分离,可指定任意解码内核挂载到任意显示窗体,动态切换。
  7. 支持共享解码线程,默认开启并且自动处理,当识别到相同的视频地址,共享一个解码线程,在网络视频环境中可以大大节约网络流量以及对方设备的推流压力。国内顶尖视频厂商均采用此策略。这样只要拉一路视频流就可以共享到几十个几百个通道展示。
  8. 自动识别视频旋转角度并绘制,比如手机上拍摄的视频一般是旋转了90度的,播放的时候要自动旋转处理,不然默认是倒着的。
  9. 自动识别视频流播放过程中分辨率的变化,在视频控件上自动调整尺寸。比如摄像机可以在使用过程中动态配置分辨率,当分辨率改动后对应视频控件也要做出同步反应。
  10. 音视频文件无感知自动切换循环播放,不会出现切换期间黑屏等肉眼可见的切换痕迹。
  11. 视频控件同时支持任意解码内核、任意画面显示策略、任意视频显示模式。
  12. 视频控件悬浮条同时支持句柄、绘制、GPU三种模式,非绝对坐标移来移去。
  13. 本地摄像头设备支持指定设备名称、分辨率、帧率进行播放。
  14. 本地桌面采集支持设定采集区域、偏移值、指定桌面索引、帧率、多个桌面同时采集等。还支持指定窗口标题采集固定窗口。
  15. 录像文件同时支持打开的视频文件、本地摄像头、本地桌面、网络视频流等。
  16. 瞬间响应打开和关闭,无论是打开不存在的视频或者网络流,探测设备是否存在,读取中的超时等待,收到关闭指令立即中断之前的操作并响应。
  17. 支持打开各种图片文件,支持本地音视频文件拖曳播放。
  18. 视频流通信方式可选tcp/udp,有些设备可能只提供了某一种协议通信比如tcp,需要指定该种协议方式打开。
  19. 可设置连接超时时间(视频流探测用的超时时间)、读取超时时间(采集过程中的超时时间)。
  20. 支持逐帧播放,提供上一帧/下一帧函数接口,可以逐帧查阅采集到的图像。
  21. 音频文件自动提取专辑信息比如标题、艺术家、专辑、专辑封面,自动显示专辑封面。
  22. 视频响应极低延迟0.2s左右,极速响应打开视频流0.5s左右,专门做了优化处理。
  23. 支持H264/H265编码(现在越来越多的监控摄像头是H265视频流格式)生成视频文件,内部自动识别切换编码格式。
  24. 支持用户信息中包含特殊字符(比如用户信息中包含+#@等字符)的视频流播放,内置解析转义处理。
  25. 支持滤镜,各种水印及图形效果,支持多个水印和图像,可以将OSD标签信息和各种图形信息写入到MP4文件。
  26. 支持视频流中的各种音频格式,AAC、PCM、G.726、G.711A、G.711Mu、G.711ulaw、G.711alaw、MP2L2等都支持,推荐选择AAC兼容性跨平台性最好。
  27. 内核ffmpeg采用纯qt+ffmpeg解码,非sdl等第三方绘制播放依赖,gpu绘制采用qopenglwidget,音频播放采用qaudiooutput。
  28. 内核ffmpeg和内核mdk支持安卓,其中mdk支持安卓硬解码,性能非常凶残。
  29. 可以切换音视频轨道,也就是节目通道,可能ts文件带了多个音视频节目流,可以分别设置要播放哪一个,可以播放前设置好和播放过程中动态设置。
  30. 可以设置视频旋转角度,可以播放前设置好和播放过程中动态改变。
  31. 视频控件悬浮条自带开始和停止录像切换、声音静音切换、抓拍截图、关闭视频等功能。
  32. 音频组件支持声音波形值数据解析,可以根据该值绘制波形曲线和柱状声音条,默认提供了声音振幅信号。
  33. 标签和图形信息支持三种绘制方式,绘制到遮罩层、绘制到图片、源头绘制(对应信息可以存储到文件)。
  34. 通过传入一个url地址,该地址可以带上通信协议、分辨率、帧率等信息,无需其他设置。
  35. 保存视频到文件支持三种策略,自动处理、仅限文件、全部转码,转码策略支持自动识别、转264、转265,编码保存支持指定分辨率缩放或者等比例缩放。比如对保存文件体积有要求可以指定缩放后再存储。
  36. 支持加密保存文件和解密播放文件,可以指定秘钥文本。
  37. 提供的监控布局类支持64通道同时显示,还支持各种异型布局,比如13通道,手机上6行2列布局。各种布局可以自由定义。
  38. 支持电子放大,在悬浮条切换到电子放大模式,在画面上选择需要放大的区域,选取完毕后自动放大,再次切换放大模式可以复位。
  39. 各组件中极其详细的打印信息提示,尤其是报错信息提示,封装的统一打印格式。针对现场复杂的设备环境测试极其方便有用,相当于精确定位到具体哪个通道哪个步骤出错。
  40. 同时提供了简单示例、视频播放器、多画面视频监控、监控回放、逐帧播放、多屏渲染等单独窗体示例,专门演示对应功能如何使用。
  41. 监控回放可选不同厂家类型、回放时间段、用户信息、指定通道。支持切换回放进度。
  42. 可以从声卡设备下拉框选择声卡播放声音,提供对应的切换声卡函数接口。
  43. 支持编译到手机app使用,提供了专门的手机app布局界面,可以作为手机上的视频监控使用。
  44. 代码框架和结构优化到最优,性能强悍,注释详细,持续迭代更新升级。
  45. 源码支持windows、linux、mac、android等,支持各种国产linux系统,包括但不限于统信UOS/中标麒麟/银河麒麟等。还支持嵌入式linux。
  46. 源码支持Qt4、Qt5、Qt6,兼容所有版本。

五、相关代码

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

SaveMode AbstractSaveThread::getSaveMode(const QString &url)
{
    SaveMode saveMode = SaveMode_File;
    if (url.startsWith("rtmp://") || url.startsWith("rtmps://")) {
        saveMode = SaveMode_Rtmp;
    } else if (url.startsWith("rtsp://") || url.startsWith("rtsps://")) {
        saveMode = SaveMode_Rtsp;
    } else if (url.startsWith("srt://")) {
        saveMode = SaveMode_Srt;
    } else if (url.startsWith("udp://")) {
        saveMode = SaveMode_Udp;
    } else if (url.startsWith("tcp://")) {
        saveMode = SaveMode_Tcp;
    } else if (url.startsWith("rtp://")) {
        saveMode = SaveMode_Rtp;
    } else if (url.startsWith("rist://")) {
        saveMode = SaveMode_Rist;
    }

    return saveMode;
}

QString AbstractSaveThread::getSaveFormat(const QString &url, const QString &audioCodecName)
{
    QString format = "mp4";
    if (url.startsWith("rtmp://") || url.startsWith("rtmps://")) {
        format = "flv";
    } else if (url.startsWith("rtsp://") || url.startsWith("rtsps://")) {
        format = "rtsp";
    } else if (url.startsWith("srt://") || url.startsWith("udp://") || url.startsWith("rist://")) {
        format = "mpegts";
    } else if (url.startsWith("rtp://")) {
        format = "rtp_mpegts";
    } else if (audioCodecName != "aac") {
        format = "mov";
    }

    return format;
}

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[i];
        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();
}
相关推荐
kkoral27 分钟前
OpenCV 与 FFmpeg 的关系
opencv·ffmpeg
kkoral27 分钟前
如何在 Python 中使用 OpenCV 调用 FFmpeg 的特定功能?
python·opencv·ffmpeg
一然明月37 分钟前
Qt QML 锚定(Anchors)全解析
java·数据库·qt
一只爱学习的小鱼儿1 小时前
使用QT编写粒子显示热力图效果
开发语言·qt
大树学长1 小时前
【QT开发】Redis通信相关(一)
redis·qt
笨笨马甲1 小时前
Qt 人脸识别
开发语言·qt
山上三树2 小时前
Qt QObject介绍
开发语言·qt
山上三树2 小时前
QObject、QWidget、Widget三者的关系
qt
坚定学代码2 小时前
qt c++ 局域网聊天小工具
c++·qt·个人开发
山栀shanzhi3 小时前
【FFmpeg】音视频MP4封装格式转封装MOV
ffmpeg·音视频