QT应用编程: 基于FFMPEG设计的流媒体播放器(播放rtmp视频流)

简介: QT应用编程: 基于FFMPEG设计的流媒体播放器(播放rtmp视频流)

一、环境介绍

操作系统: windows系统 、  win10 X64

使用的FFMPEG库版本下载地址:https://download.csdn.net/download/xiaolong1126626497/12304729

在windows下使用FFMPEG库时,为了方便程序运行,记得把库的路径加到系统的环境变量里。

完整项目源码下载地址(下载即可编译运行,不懂可以私信): https://download.csdn.net/download/xiaolong1126626497/19763637

image.png

image.png

二、程序功能介绍

代码里有两个线程:主线程进行UI界面显示,子线程负责拉流解码,子线程里解码视频之后,将图像数据通过信号发送给主UI界面进行刷新显示。


代码里的目前支持解码的视频编码为H264、音频是AAC,其他的编码暂时没有加入支持,如果有需求,修改增加代码即可。


代码里的视频解码流程:获取一帧H264编码的视频帧-->解码成YUV420P格式数据->转换为RGB24格式->加载到QImage里-->通过标签控件进行显示。


代码里的音频解码流程:获取一帧AAC编码的音频帧--->解码成PCM格式--->进行音频重采样转换成自己需要的PCM格式-->通过QT的音频接口输出到声卡进行播放。


打包的成品软件下载地址:https://download.csdn.net/download/xiaolong1126626497/12317449

完整项目源码下载地址(下载即可编译运行,不懂可以私信): https://download.csdn.net/download/xiaolong1126626497/19323184

三、程序运行效果

下面是播放流媒体服务器视频的效果,视频+声音是OK的。

上面的RTMP地址栏里,也可以填本地电脑上的视频路径,只要视频是H264+AAC编码的,也可以播放,只是每加时间处理,播放会非常的快。

image.png

image.png

播放CCTV直播:rtmp://58.200.131.2:1935/livetv/cctv14

image.png

image.png

四、程序代码

xxx.pro工程文件代码:

QT       += core gui
QT       += multimediawidgets
QT       += xml
QT       += multimedia
QT       += network
QT       += widgets
QT       += serialport
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
    Thread_FFMPEG_LaLiu.cpp \
    main.cpp \
    widget.cpp
HEADERS += \
    Thread_FFMPEG_LaLiu.h \
    widget.h
FORMS += \
    widget.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
win32
{
    message('运行win32版本')
    INCLUDEPATH+=C:/FFMPEG_WIN32_LIB/include
    LIBS+=C:/FFMPEG_WIN32_LIB/bin/av*
    LIBS+=C:/FFMPEG_WIN32_LIB/bin/sw*
    LIBS+=C:/FFMPEG_WIN32_LIB/bin/pos*
}
RESOURCES += \
    image.qrc
RC_ICONS=log.ico

laliu.cpp代码:

#include "Thread_FFMPEG_LaLiu.h"
#define MAX_AUDIO_FRAME_SIZE  192000
//定义拉流的线程
class Thread_FFMPEG_LaLiu thread_laliu;
class VideoAudioDecode video_audio_decode;
//线程执行起点
void Thread_FFMPEG_LaLiu::run()
{
    Audio_Out_Init();
    //判断之前是否申请了空间
    if(rgb24_data)
    {
        delete rgb24_data;
        rgb24_data=nullptr;
    }
    if(yuv420p_data)
    {
        delete yuv420p_data;
        yuv420p_data=nullptr;
    }
    LogSend("开始拉流.\n");
    //qDebug()<<"AV_CODEC_ID_H264="<<AV_CODEC_ID_H264; //27
    //qDebug()<<"AV_CODEC_ID_AAC="<<AV_CODEC_ID_AAC; //86018
    ffmpeg_rtmp_client();
}
//拉流
int Thread_FFMPEG_LaLiu::ffmpeg_rtmp_client()
{
    int video_width=0;
    int video_height=0;
    // Allocate an AVFormatContext
    AVFormatContext* format_ctx = avformat_alloc_context();
    // 打开rtsp:打开输入流并读取标题。 编解码器未打开
    const char* url =video_audio_decode.rtmp_url;// "rtmp://193.112.142.152:8888/live/abcd";
    LogSend(tr("拉流地址: %1\n").arg(url));
    int ret = -1;
    ret = avformat_open_input(&format_ctx, url, nullptr, nullptr);
    if(ret != 0)
    {
        LogSend(tr("无法打开网址: %1, return value: %2 \n").arg(url).arg(ret));
        return -1;
    }
    // 读取媒体文件的数据包以获取流信息
    ret = avformat_find_stream_info(format_ctx, nullptr);
    if(ret < 0)
    {
        LogSend(tr("无法获取流信息: %1\n").arg(ret));
        return -1;
    }
    AVCodec  *video_pCodec;
    AVCodec  *audio_pCodec;
    // audio/video stream index
    int video_stream_index = -1;
    int audio_stream_index = -1;
    LogSend(tr("视频中流的数量: %1\n").arg(format_ctx->nb_streams));
    for(int i = 0; i < format_ctx->nb_streams; ++i)
    {
        const AVStream* stream = format_ctx->streams[i];
        LogSend(tr("编码数据的类型: %1\n").arg(stream->codecpar->codec_id));
        if(stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            //判断视频流是否是H264格式
            if(stream->codecpar->codec_id!=AV_CODEC_ID_H264)
            {
                LogSend("当前视频编码格式暂时不支持. 目前只支持:H264\n");
                return 0;
            }
            //查找解码器
            video_pCodec=avcodec_find_decoder(AV_CODEC_ID_H264);
            //打开解码器
            int err = avcodec_open2(stream->codec,video_pCodec, NULL);
            if(err!=0)
            {
                  LogSend(tr("H264解码器打开失败.\n"));
                  return 0;
            }
            video_stream_index = i;
            //得到视频帧的宽高
            video_width=stream->codecpar->width;
            video_height=stream->codecpar->height;
            LogSend(tr("视频帧的尺寸(以像素为单位): (宽X高)%1x%2 像素格式: %3\n").arg(
                stream->codecpar->width).arg(stream->codecpar->height).arg(stream->codecpar->format));
        }
        else if(stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
        {
            audio_stream_index = i;
            qDebug()<<tr("音频样本格式: %1").arg(stream->codecpar->format);
            //判断音频流是否是AAC格式
            if(stream->codecpar->codec_id!=AV_CODEC_ID_AAC)
            {
                LogSend("当前音频编码格式暂时不支持. 目前只支持:AAC\n");
                return 0;
            }
            //查找解码器
            audio_pCodec=avcodec_find_decoder(AV_CODEC_ID_AAC);
            //打开解码器
            int err = avcodec_open2(stream->codec,audio_pCodec, nullptr);
            if(err!=0)
            {
                  LogSend(tr("AAC解码器打开失败.\n"));
                  return 0;
            }
        }
    }
    if (video_stream_index == -1)
    {
         LogSend("没有检测到视频流.\n");
         return -1;
    }
    if (audio_stream_index == -1)
    {
        LogSend("没有检测到音频流.\n");
    }
    //初始化解码相关的参数
    AVFrame *yuv420p_pFrame = nullptr;
    AVFrame *PCM_pFrame = nullptr;
    yuv420p_pFrame = av_frame_alloc();// 存放解码后YUV数据的缓冲区
    PCM_pFrame = av_frame_alloc();// 存放解码后PCM数据的缓冲区
    //创建packet,用于存储解码前音频的数据
    AVPacket *packet = (AVPacket *)malloc(sizeof(AVPacket));
    av_init_packet(packet);
    //设置音频转码后输出相关参数
    //采样的布局方式
    uint64_t out_channel_layout = AV_CH_LAYOUT_MONO;
    //采样个数
    int out_nb_samples = 1024;
    //采样格式
    enum AVSampleFormat  sample_fmt = AV_SAMPLE_FMT_S16;
    //采样率
    int out_sample_rate = 44100;
    //通道数
    int out_channels = av_get_channel_layout_nb_channels(out_channel_layout);
    printf("%d\n",out_channels);
    //创建buffer
    int buffer_size = av_samples_get_buffer_size(nullptr, out_channels, out_nb_samples, sample_fmt, 1);
    //注意要用av_malloc
    uint8_t *buffer = (uint8_t *)av_malloc(MAX_AUDIO_FRAME_SIZE * 2);
    int64_t in_channel_layout = av_get_default_channel_layout(format_ctx->streams[audio_stream_index]->codec->channels);
    //打开转码器
    struct SwrContext *convert_ctx = swr_alloc();
    //设置转码参数
    convert_ctx = swr_alloc_set_opts(convert_ctx, out_channel_layout, sample_fmt, out_sample_rate, \
           in_channel_layout, format_ctx->streams[audio_stream_index]->codec->sample_fmt, format_ctx->streams[audio_stream_index]->codec->sample_rate, 0, nullptr);
    //初始化转码器
    swr_init(convert_ctx);
//申请存放yuv420p数据的空间
    yuv420p_data=new unsigned char[video_width*video_height*3/2];
    //申请存放rgb24数据的空间
    rgb24_data=new unsigned char[video_width*video_height*3];
    int y_size=video_width*video_height;
    AVPacket pkt;
    int re;
    bool send_flag=1;
    while(video_audio_decode.run_flag)
    {
        //读取一帧数据
        ret=av_read_frame(format_ctx, &pkt);
        if(ret < 0)
        {
            continue;
        }
        //得到视频包
        if(pkt.stream_index == video_stream_index)
        {
            //解码视频 frame
             re = avcodec_send_packet(format_ctx->streams[video_stream_index]->codec,&pkt);//发送视频帧
             if (re != 0)
             {
                 av_packet_unref(&pkt);//不成功就释放这个pkt
                 continue;
             }
             re = avcodec_receive_frame(format_ctx->streams[video_stream_index]->codec, yuv420p_pFrame);//接受后对视频帧进行解码
             if (re != 0)
             {
                 av_packet_unref(&pkt);//不成功就释放这个pkt
                 continue;
             }
            //将YUV数据拷贝到缓冲区
            memcpy(yuv420p_data,(const void *)yuv420p_pFrame->data[0],y_size);
            memcpy(yuv420p_data+y_size,(const void *)yuv420p_pFrame->data[1],y_size/4);
            memcpy(yuv420p_data+y_size+y_size/4,(const void *)yuv420p_pFrame->data[2],y_size/4);
            //将yuv420p转为RGB24格式
            YUV420P_to_RGB24(yuv420p_data,rgb24_data,video_width,video_height);
            //加载图片数据
            QImage image(rgb24_data,video_width,video_height,QImage::Format_RGB888);
            VideoDataOutput(image); //发送信号
        }
        //得到音频包
        if(pkt.stream_index == audio_stream_index)
        {
            //解码声音
             re = avcodec_send_packet(format_ctx->streams[audio_stream_index]->codec,&pkt);//发送视频帧
             if (re != 0)
             {
                 av_packet_unref(&pkt);//不成功就释放这个pkt
                 continue;
             }
             re = avcodec_receive_frame(format_ctx->streams[audio_stream_index]->codec, PCM_pFrame);//接受后对视频帧进行解码
             if (re != 0)
             {
                 av_packet_unref(&pkt);//不成功就释放这个pkt
                 continue;
             }
             //只发送一次
             if(send_flag)
             {
                 send_flag=0;
                 //得到PCM数据的配置信息
                 LogSend(tr("nb_samples=%1\n").arg(PCM_pFrame->nb_samples)); //此帧描述的音频样本数(每通道
                 LogSend(tr("音频数据声道=%1\n").arg(PCM_pFrame->channels));      //声道数量
                 LogSend(tr("音频数据采样率=%1\n").arg(PCM_pFrame->sample_rate)); //采样率
                 LogSend(tr("channel_layout=%1\n").arg(PCM_pFrame->channel_layout)); //通道布局
             }
             //转码
            swr_convert(convert_ctx, &buffer, MAX_AUDIO_FRAME_SIZE, (const uint8_t **)PCM_pFrame->data, PCM_pFrame->nb_samples);
            //播放音频
            audio_out_streamIn->write((const char *)buffer,buffer_size);
         }
        av_packet_unref(&pkt);
    }
    avformat_free_context(format_ctx);
    avformat_close_input(&format_ctx);//释放解封装器的空间,以防空间被快速消耗完
    return 0;
}
//图像颜色转换
void Thread_FFMPEG_LaLiu::YUV420P_to_RGB24(unsigned char *data, unsigned char *rgb, int width, int height)
{
    int index = 0;
    unsigned char *ybase = data;
    unsigned char *ubase = &data[width * height];
    unsigned char *vbase = &data[width * height * 5 / 4];
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            //YYYYYYYYUUVV
            unsigned char Y = ybase[x + y * width];
            unsigned char U = ubase[y / 2 * width / 2 + (x / 2)];
            unsigned char V = vbase[y / 2 * width / 2 + (x / 2)];
            rgb[index++] = Y + 1.402 * (V - 128); //R
            rgb[index++] = Y - 0.34413 * (U - 128) - 0.71414 * (V - 128); //G
            rgb[index++] = Y + 1.772 * (U - 128); //B
        }
    }
}
//音频输出初始化
void Thread_FFMPEG_LaLiu::Audio_Out_Init()
{
    QAudioFormat auido_out_format;
    //设置录音的格式
    auido_out_format.setSampleRate(44100); //设置采样率以对赫兹采样。 以秒为单位,每秒采集多少声音数据的频率.
    auido_out_format.setChannelCount(1);   //将通道数设置为通道。
    auido_out_format.setSampleSize(16);     /*将样本大小设置为指定的sampleSize(以位为单位)通常为8或16,但是某些系统可能支持更大的样本量。*/
    auido_out_format.setCodec("audio/pcm"); //设置编码格式
    auido_out_format.setByteOrder(QAudioFormat::LittleEndian); //样本是小端字节顺序
    auido_out_format.setSampleType(QAudioFormat::SignedInt); //样本类型
    QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice());
    if(audio_out)
    {
        delete audio_out;
        audio_out=nullptr;
    }
    audio_out = new QAudioOutput(auido_out_format);
    audio_out_streamIn=audio_out->start();
    LogSend("音频输出初始化成功.\n");
}

laliu.h代码

#ifndef THREAD_FFMPEG_LALIU_H
#define THREAD_FFMPEG_LALIU_H
#include <QAbstractVideoSurface>
#include <QVideoProbe>
#include <QThread>
#include <QApplication>
#include <QDebug>
#include <QObject>
#include <QMutex>
#include <QMutexLocker>
#include <QWaitCondition>
#include <QQueue>
#include <QCamera>
#include <QPen>
#include <QPainter>
#include <QRgb>
#include <QAudio>     //这五个是QT处理音频的库
#include <QAudioFormat>
#include <QAudioInput>
#include <QAudioOutput>
#include <QIODevice>
#include <QPlainTextEdit>
#include <QScrollBar>
//声明引用C的头文件
extern "C"
{
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #include <math.h>
    #include <libavutil/avassert.h>
    #include <libavutil/channel_layout.h>
    #include <libavutil/opt.h>
    #include <libavutil/mathematics.h>
    #include <libavutil/timestamp.h>
    #include <libavformat/avformat.h>
    #include <libswscale/swscale.h>
    #include <libswresample/swresample.h>
    #include "libavfilter/avfilter.h"
    #include "libavutil/avassert.h"
    #include "libavutil/channel_layout.h"
    #include "libavutil/common.h"
    #include "libavutil/opt.h"
}
//视频音频解码线程
class Thread_FFMPEG_LaLiu: public QThread
{
    Q_OBJECT
public:
      unsigned char *yuv420p_data;
      unsigned char *rgb24_data;
      QAudioOutput *audio_out;
      QIODevice* audio_out_streamIn;
      Thread_FFMPEG_LaLiu()
      {
          rgb24_data=nullptr;yuv420p_data=nullptr;
          audio_out=nullptr;
          audio_out_streamIn=nullptr;
      }
      int ffmpeg_rtmp_client();
      void Audio_Out_Init();
      void YUV420P_to_RGB24(unsigned char *data, unsigned char *rgb, int width, int height);
protected:
    void run();
signals:
    void LogSend(QString text);
    void VideoDataOutput(QImage); //输出信号
};
//解码拉流时的一些全局参数
class VideoAudioDecode
{
public:
    char rtmp_url[1024];
    bool run_flag; //1表示运行 0表示停止
};
extern class Thread_FFMPEG_LaLiu thread_laliu;
extern class VideoAudioDecode video_audio_decode;
#endif // THREAD_FFMPEG_LALIU_H

widget.h代码:

#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include "Thread_FFMPEG_LaLiu.h"
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
//主线程
class Widget : public QWidget
{
    Q_OBJECT
public:
    Widget(QWidget *parent = nullptr);
    ~Widget();
    void SetStyle(const QString &qssFile);
    void Log_Text_Display(QPlainTextEdit *plainTextEdit_log,QString text);
private slots:
    void Log_Display(QString text);
     void VideoDataDisplay(QImage image);
     void on_pushButton_start_clicked();
private:
    Ui::Widget *ui;
};
#endif // WIDGET_H

widget.cpp代码:

#include "widget.h"
#include "ui_widget.h"
/*
 * 设置QT界面的样式
*/
void Widget::SetStyle(const QString &qssFile) {
    QFile file(qssFile);
    if (file.open(QFile::ReadOnly)) {
        QString qss = QLatin1String(file.readAll());
        qApp->setStyleSheet(qss);
        QString PaletteColor = qss.mid(20,7);
        qApp->setPalette(QPalette(QColor(PaletteColor)));
        file.close();
    }
    else
    {
        qApp->setStyleSheet("");
    }
}
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    /*基本设置*/
    this->SetStyle(":/images/blue.css");     //设置样式表
    this->setWindowIcon(QIcon(":/log.ico")); //设置图标
    this->setWindowTitle("RTMP拉流客户端");
    //设置默认的拉流地址
    ui->lineEdit_rtmp_url->setText("rtmp://193.112.142.152:8888/live/abcd");
    //连接拉流线程的图像输出信号
    connect(&thread_laliu,SIGNAL(VideoDataOutput(QImage )),this,SLOT(VideoDataDisplay(QImage )));
    //连接拉流线程的日志信息
    connect(&thread_laliu,SIGNAL(LogSend(QString)),this,SLOT(Log_Display(QString)));
}
Widget::~Widget()
{
    delete ui;
}
//视频刷新显示
void Widget::VideoDataDisplay(QImage image)
{
    QPixmap my_pixmap;
    my_pixmap.convertFromImage(image);
    ui->label_ImageDisplay->setPixmap(my_pixmap);
}
/*日志显示*/
void Widget::Log_Text_Display(QPlainTextEdit *plainTextEdit_log,QString text)
{
    plainTextEdit_log->insertPlainText(text);
    //移动滚动条到底部
    QScrollBar *scrollbar = plainTextEdit_log->verticalScrollBar();
    if(scrollbar)
    {
        scrollbar->setSliderPosition(scrollbar->maximum());
    }
}
//日志显示
void Widget::Log_Display(QString text)
{
    Log_Text_Display(ui->plainTextEdit_log,text);
}
//开始拉流
void Widget::on_pushButton_start_clicked()
{
    video_audio_decode.run_flag=1; //运行标志
    strncpy(video_audio_decode.rtmp_url,ui->lineEdit_rtmp_url->text().toLocal8Bit().data(),sizeof(video_audio_decode.rtmp_url));
    //开始运行线程
    thread_laliu.start();
}

ui界面设计图:

image.png



image.png

目录
相关文章
|
3月前
|
网络协议 容器
【qt】 TCP编程小项目
【qt】 TCP编程小项目
61 0
|
6月前
|
开发框架 Linux API
Qt:构建高效且用户友好的跨平台应用
Qt:构建高效且用户友好的跨平台应用
|
6月前
|
开发框架 网络协议 数据库
Qt:构建跨平台应用的强大框架
Qt:构建跨平台应用的强大框架
|
1月前
|
XML 开发工具 Android开发
FFmpeg开发笔记(五十六)使用Media3的Exoplayer播放网络视频
ExoPlayer最初是为了解决Android早期MediaPlayer控件对网络视频兼容性差的问题而推出的。现在,Android官方已将其升级并纳入Jetpack的Media3库,使其成为音视频操作的统一引擎。新版ExoPlayer支持多种协议,解决了设备和系统碎片化问题,可在整个Android生态中一致运行。通过修改`build.gradle`文件、布局文件及Activity代码,并添加必要的权限,即可集成并使用ExoPlayer进行网络视频播放。具体步骤包括引入依赖库、配置播放界面、编写播放逻辑以及添加互联网访问权限。
127 1
FFmpeg开发笔记(五十六)使用Media3的Exoplayer播放网络视频
|
3月前
|
API 开发工具 C语言
C语言与图形界面:利用GTK+、Qt等库创建GUI应用。
C语言与图形界面:利用GTK+、Qt等库创建GUI应用。
177 0
关于Qt的pri模块化编程详解
关于Qt的pri模块化编程详解
|
3月前
|
JavaScript Java Go
【Qt】Qt编程注意事项
【Qt】Qt编程注意事项
|
6月前
FFmpeg开发笔记(十八)FFmpeg兼容各种音频格式的播放
《FFmpeg开发实战》一书中,第10章示例程序playaudio.c原本仅支持mp3和aac音频播放。为支持ogg、amr、wma等非固定帧率音频,需进行三处修改:1)当frame_size为0时,将输出采样数量设为512;2)遍历音频帧时,计算实际采样位数以确定播放数据大小;3)在SDL音频回调函数中,确保每次发送len字节数据。改进后的代码在chapter10/playaudio2.c,可编译运行播放ring.ogg测试,成功则显示日志并播放铃声。
115 1
FFmpeg开发笔记(十八)FFmpeg兼容各种音频格式的播放
|
6月前
|
Windows 安全 C++
Qt字符串类应用与常用基本数据类型
Qt字符串类应用与常用基本数据类型
|
6月前
|
编解码 计算机视觉 索引
使用ffmpeg MP4转 m3u8并播放 实测!!
使用ffmpeg MP4转 m3u8并播放 实测!!
313 1