一、环境介绍
操作系统: windows系统 、 win10 X64
使用的FFMPEG库版本下载地址:https://download.csdn.net/download/xiaolong1126626497/12304729
在windows下使用FFMPEG库时,为了方便程序运行,记得把库的路径加到系统的环境变量里。
完整项目源码下载地址(下载即可编译运行,不懂可以私信): https://download.csdn.net/download/xiaolong1126626497/19763637
二、程序功能介绍
代码里有两个线程:主线程进行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编码的,也可以播放,只是每加时间处理,播放会非常的快。
播放CCTV直播:rtmp://58.200.131.2:1935/livetv/cctv14
四、程序代码
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界面设计图: