Qt+FFmpeg+OpenGL实现视频播放器(1)

1、了解FFmpeg库

介绍
avcodec音视频编解码核心库
avformat音视频容器格式的封装和解析
avutil核心工具库
swscal图像格式转换的模块
swresampel音频重采样
avfilter音视频滤镜库 如视频加水印、音频变声
avdevice输入输出设备库,提供设备数据的输入与输出

FFmpeg 依靠以上几个库,实现了强大的音视频编码、解码、编辑、转换、采集等能力。

2、播放流程

  • 解封装(Demuxing):就是将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。例如,FLV 格式的数据,经过解封装操作后,输出 H.264 编码的视频码流和 AAC 编码的音频码流。

  • 软硬件解码(Decode):就是将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。通过解码,将压缩编码的视频数据 H.264,MPEG2 解码成为非压缩的颜色数据,例如 YUV 等等;将压缩编码的音频数据 AAC,MP3 解码成为非压缩的音频抽样数据,例如 PCM 数据。解码分为硬编码和软编码。

  • 像素格式转换:将 YUV 数据格式转换成 RGB 数据格式。

  • 重采样:对音频重新采样。

  • dts/pts:dts 是解码的时间戳,而 pts 是显示的时间戳。pts 用于获取当前播放进度。进度条移动需要用到av_seek_frame函数。

  • 音视频同步:就是根据解封装模块处理过程中获取到的参数信息,同步解码出来的音频和视频数据,并将音视频频数据送至系统的显卡和声卡播放出来(Render)。

4、ffmpeg解码流程

注意:新版本后不用再注册所有格式和编解码器 av_register_all();

5、实战

了解了这么多,接下来开始第一步,打开一个视频文件,打印出音视频流信息。

创建一个类 VideoInfo,用于处理视频文件的打开和流信息打印。

运行环境macOS+Qt6.6+FFmpeg7.0.1+OpenGL

videoinfo.cpp

#include "videoinfo.h"
#include <QDebug>

VideoInfo::VideoInfo(QObject *parent)
    : QObject(parent), formatContext(nullptr)
{
    // 注册所有格式和编解码器
    //av_register_all();
}

VideoInfo::~VideoInfo()
{
    if (formatContext) {
        avformat_close_input(&formatContext);
    }
}

bool VideoInfo::openFile(const QString &filePath)
{
    if (avformat_open_input(&formatContext, filePath.toStdString().c_str(), nullptr, nullptr) != 0) {
        qWarning() << "Could not open file:" << filePath;
        return false;
    }

    if (avformat_find_stream_info(formatContext, nullptr) < 0) {
        qWarning() << "Could not find stream information";
        return false;
    }

    av_dump_format(formatContext, 0, filePath.toStdString().c_str(), 0);
    return true;
}

main.cpp

#include <QApplication>
#include <QPushButton>
#include <QFileDialog>
#include "videoinfo.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    VideoInfo videoInfo;
    QPushButton button("Open Video File");

    QObject::connect(&button, &QPushButton::clicked, [&]() {
        QString filePath = QFileDialog::getOpenFileName(nullptr, "Open Video File", "", "Video Files (*.mp4 *.avi *.mkv)");
        if (!filePath.isEmpty()) {
            videoInfo.openFile(filePath);
        }
    });

    button.show();

    return app.exec();
}
  • av_register_all(): 注册所有可用的文件格式和编解码器。这是 FFmpeg 初始化的一部分。在 FFmpeg 的较新版本中可能已经不需要这个调用,但为了兼容性,可以保留。

  • 打开视频文件: 使用 avformat_open_input 打开视频文件,并检查是否成功。

  • 查找流信息: 使用 avformat_find_stream_info 来解析视频文件中的流信息。

  • 打印流信息: 使用 av_dump_format 输出文件的详细信息,包括音视频流的信息。

运行程序,打开视频文件后

获得了以下信息:

  1. 文件元数据: 包含标题、艺术家、编码器、版权等信息。
  2. 视频流: 854x480 分辨率的 H.264 编码视频,24 fps。
  3. 音频流: AAC 编码的音频,立体声,采样率 48000 Hz。

这些信息表明视频文件的解析和基本信息提取功能已经实现。

下一步解码视频帧并将其渲染到窗口中,拓展现有的VideoInfo 类来处理视频解码,并显示第一帧图像。

#include "videoinfo.h"
#include <QDebug>

VideoInfo::VideoInfo(QObject *parent)
    : QObject(parent), formatContext(nullptr), codecContext(nullptr), frame(nullptr),
      packet(nullptr), swsContext(nullptr), videoStreamIndex(-1)
{
    av_register_all(); // FFmpeg 7.0.1已经不需要手动注册所有编解码器和格式
    avformat_network_init(); // 如果需要支持网络流,这个初始化是必须的
}

VideoInfo::~VideoInfo()
{
    if (frame) av_frame_free(&frame);
    if (packet) av_packet_free(&packet);
    if (codecContext) avcodec_free_context(&codecContext);
    if (formatContext) avformat_close_input(&formatContext);
    if (swsContext) sws_freeContext(swsContext);
}

bool VideoInfo::openFile(const QString &filePath)
{
    if (avformat_open_input(&formatContext, filePath.toStdString().c_str(), nullptr, nullptr) != 0) {
        qWarning() << "Could not open file:" << filePath;
        return false;
    }

    if (avformat_find_stream_info(formatContext, nullptr) < 0) {
        qWarning() << "Could not find stream information";
        return false;
    }

    av_dump_format(formatContext, 0, filePath.toStdString().c_str(), 0);

    // 查找视频流
    videoStreamIndex = -1;
    for (unsigned int i = 0; i < formatContext->nb_streams; i++) {
        if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoStreamIndex = i;
            break;
        }
    }

    if (videoStreamIndex == -1) {
        qWarning() << "Could not find video stream";
        return false;
    }

    // 查找解码器
    AVCodecParameters *codecParameters = formatContext->streams[videoStreamIndex]->codecpar;
    AVCodec *codec = avcodec_find_decoder(codecParameters->codec_id);
    if (!codec) {
        qWarning() << "Unsupported codec!";
        return false;
    }

    codecContext = avcodec_alloc_context3(codec);
    if (!codecContext) {
        qWarning() << "Could not allocate codec context";
        return false;
    }

    if (avcodec_parameters_to_context(codecContext, codecParameters) < 0) {
        qWarning() << "Could not copy codec parameters to codec context";
        return false;
    }

    if (avcodec_open2(codecContext, codec, nullptr) < 0) {
        qWarning() << "Could not open codec";
        return false;
    }

    frame = av_frame_alloc();
    packet = av_packet_alloc();

    // 初始化SwsContext用于图像转换
    swsContext = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt,
                                codecContext->width, codecContext->height, AV_PIX_FMT_RGB24,
                                SWS_BILINEAR, nullptr, nullptr, nullptr);

    return true;
}

QImage VideoInfo::getFrame()
{
    while (av_read_frame(formatContext, packet) >= 0) {
        if (packet->stream_index == videoStreamIndex) {
            int response = avcodec_send_packet(codecContext, packet);
            if (response >= 0) {
                response = avcodec_receive_frame(codecContext, frame);
                if (response >= 0) {
                    // 使用 FFmpeg 7.0.1 API: av_image_get_buffer_size 和 av_image_fill_arrays
                    int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, codecContext->width, codecContext->height, 1);
                    uint8_t *buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t));
                    
                    uint8_t *dst_data[4];
                    int dst_linesize[4];
                    av_image_fill_arrays(dst_data, dst_linesize, buffer, AV_PIX_FMT_RGB24, codecContext->width, codecContext->height, 1);

                    sws_scale(swsContext, frame->data, frame->linesize, 0, codecContext->height, dst_data, dst_linesize);

                    QImage image(dst_data[0], codecContext->width, codecContext->height, QImage::Format_RGB888);
                    av_free(buffer); // 释放内存

                    // 这里缩放图像到适合显示的尺寸
                    return image.scaled(800, 600, Qt::KeepAspectRatio); // 假设目标尺寸为800x600
                }
            }
        }
        av_packet_unref(packet);
    }
    return QImage();
}

然后在主窗口代码中添加一个QLabel来显示解码的帧。运行结果如图:

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值