一、简介

上节介绍了使用SDL播放音频,这节介绍视频显示,其解码流程跟音频差不多。

解码视频是比较耗时的,需要我们自己开个线程去解码,而音频是SDL帮我们管理了子线程去解码音频,初始化音频SDL后就开始进行播放(SDL_PauseAudio(0);)了,一播放就会调用回调函数(sdlAudioCallback),然后在去调用音频解码(decodeAudio),这个decodeAudio是被动调用,而且每次调用只解码一个。但是视频解码是主动去调用,然后解码所有东西,所有需要while循环。

二、视频解码

2.1 解码视频

void VideoPlayer::decodeVideo(){
    while (true) {
        _vMutex->lock();
        if(_vPktList->empty()){
            _vMutex->unlock();
            continue;
        }

        // 取出头部的视频包
        AVPacket pkt = _vPktList->front();
        _vPktList->pop_front();
        _vMutex->unlock();

        // 发送压缩数据到解码器
        int ret = avcodec_send_packet(_vDecodeCtx, &pkt);
        // 释放pkt
        av_packet_unref(&pkt);
        CONTINUE(avcodec_send_packet);

        while (true) {
            ret = avcodec_receive_frame(_vDecodeCtx, _vSwsInFrame);
            if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                break;
            } else BREAK(avcodec_receive_frame);

            // 像素格式的转换
            sws_scale(_vSwsCtx,
                      _vSwsInFrame->data, _vSwsInFrame->linesize,
                      0, _vDecodeCtx->height,
                      _vSwsOutFrame->data, _vSwsOutFrame->linesize);
            qDebug()<< _vSwsOutFrame->data[0];
        }
    }
}

2.2 调用解码视频方法

开启子线程调用视频解码方法:

// 初始化视频信息
int VideoPlayer::initVideoInfo() {
    int ret = initDecoder(&_vDecodeCtx,&_vStream,AVMEDIA_TYPE_VIDEO);
    RET(initDecoder);

    // 初始化像素格式转换
    ret = initSws();
    RET(initSws);

    // 开启新的线程去解码视频数据
    std::thread([this](){
        decodeVideo();
    }).detach();
    return 0;
}

三、像素格式转换

上面解码出_vSwsInFrame后是YUV数据,而显示是需要RGB的,所以这里需要进行像素格式转换。

3.1 初始化

初始化像素格式转换:

int VideoPlayer::initSws(){
    int inW = _vDecodeCtx->width;
    int inH = _vDecodeCtx->height;

    // 输出frame的参数
    _vSwsOutSpec.width = inW >> 4 << 4;// 先除以16在乘以16,保证是16的倍数
    _vSwsOutSpec.height = inH >> 4 << 4;
    _vSwsOutSpec.pixFmt = AV_PIX_FMT_RGB24;
    _vSwsOutSpec.size = av_image_get_buffer_size(
                            _vSwsOutSpec.pixFmt,
                            _vSwsOutSpec.width,
                            _vSwsOutSpec.height, 1);

    // 初始化像素格式转换的上下文
    _vSwsCtx = sws_getContext(inW,
                              inH,
                              _vDecodeCtx->pix_fmt,

                              _vSwsOutSpec.width,
                              _vSwsOutSpec.height,
                              _vSwsOutSpec.pixFmt,

                              SWS_BILINEAR, nullptr, nullptr, nullptr);
    if (!_vSwsCtx) {
        qDebug() << "sws_getContext error";
        return -1;
    }

    // 初始化像素格式转换的输入frame
    _vSwsInFrame = av_frame_alloc();
    if (!_vSwsInFrame) {
        qDebug() << "av_frame_alloc error";
        return -1;
    }

    // 初始化像素格式转换的输出frame
    _vSwsOutFrame = av_frame_alloc();
    if (!_vSwsOutFrame) {
        qDebug() << "av_frame_alloc error";
        return -1;
    }

    // _vSwsOutFrame的data[0]指向的内存空间
//    int ret = av_image_alloc(_vSwsOutFrame->data,
//                             _vSwsOutFrame->linesize,
//                             _vSwsOutSpec.width,
//                             _vSwsOutSpec.height,
//                             _vSwsOutSpec.pixFmt,
//                             1);
//    RET(av_image_alloc);

    return 0;
}

在像素转换的时候是有要求的,最好是16的倍数,否则其他分辨率的不能播放,所以我们需要在输出参数中控制宽高的大小

3.2 像素格式转换

然后在视频解码方法中进行像素格式的转换

// 像素格式的转换
sws_scale(_vSwsCtx,
          _vSwsInFrame->data, _vSwsInFrame->linesize,
          0, _vDecodeCtx->height,
          _vSwsOutFrame->data, _vSwsOutFrame->linesize);
qDebug()<< _vSwsOutFrame->data[0];          

这里的_vSwsOutFrame->datadata[0]就是像素格式转换后的RGB数据。现在通过运行打印data[0],可以发现它是空的。

这里跟音频的重采样里的data[0]道理是一样,需要我们手动的给_vSwsOutFrame->data[0]创建一块内存区域,这块内存区域需要多大呢,因为视频解码avcodec_receive_frame解码出来的就是一帧大小,所以这个_vSwsOutFrame->data[0]指向的内存空间只需要一帧大小就可以了。

// _vSwsOutFrame的data[0]指向的内存空间
int ret = av_image_alloc(_vSwsOutFrame->data,
                         _vSwsOutFrame->linesize,
                         _vSwsOutSpec.width,
                         _vSwsOutSpec.height,
                         _vSwsOutSpec.pixFmt,
                         1);
RET(av_image_alloc);

我们在调用avcodec_receive_frame_vSwsInFrame_vSwsInFrame.data[0]在其内部已经给创建好内存区域了,而且每次调用此方法其内部都会先自动销毁_vSwsInFrame.data[0],然后在给其分配空间,所以不需要我们手动创建和销毁data[0]
如果avcodec_receive_frame是最有一次调用,不会再有下一次调用了,那么最后一次内部分配的data[0]的空间是不是一直没有释放呢?其实是不会的,因为我们这个程序最后一次调用_vSwsInFrame是有值的,而且它的ret还是返回的是成功状态,那么它就会继续执行while循环,当再次执行时,因为已经没有数据量,ret返回的状态就会满足if条件if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF),最后就会退出while循环。

四、显示画面

上面完成像素格式的转换后,就需要通过发送信号出去给到VideoWidget进行显示。

4.1 定义信号

videoplayer.h中定义信号

void frameDecoded(VideoPlayer *player,
                      uint8_t *data,
                      VideoSwsSpec &spec);

videoplayer_video.cpp的视频解码方法里最后进行发送信号

4.2 发送信号

// 发出信号
emit frameDecoded(this,
                  _vSwsOutFrame->data[0],
                  _vSwsOutSpec);

这里是不能直接发送_vSwsOutSpec类型的数据的,需要我们注册此类型的数据。

// mainwindow.cpp的构造方法里

// 注册信号的参数类型,保证能够发出信号
qRegisterMetaType<VideoPlayer::VideoSwsSpec>("VideoSwsSpec&");

4.3 定义槽函数

videowidget.h中定义槽函数:

public slots:
    void onPlayerFrameDecoded(VideoPlayer *player,
                                  uint8_t *data,
                                  VideoPlayer::VideoSwsSpec &spec);

4.4 注册监听信号

mainwindow.cpp中注册监听信号:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow) {
    ......
    connect(_player, &VideoPlayer::frameDecoded,
            ui->videoWidget, &VideoWidget::onPlayerFrameDecoded);
    ......
}

4.5 画面显示

实现videowidget.cpp里的方法:

#include "videowidget.h"
#include <QDebug>
#include <QPainter>

VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
    // 设置背景色
    setAttribute(Qt::WA_StyledBackground);
    setStyleSheet("background: black");
}

VideoWidget::~VideoWidget() {
    if (_image) {
        delete _image;
        _image = nullptr;
    }
}

void VideoWidget::onPlayerFrameDecoded(VideoPlayer *player,
                                       uint8_t *data,
                                       VideoPlayer::VideoSwsSpec &spec) {
    // 释放之前的图片
    if (_image) {
        delete _image;
        _image = nullptr;
    }

    // 创建新的图片
    if (data != nullptr) {
        _image = new QImage((uchar *) data,
                            spec.width, spec.height,
                            QImage::Format_RGB888);

        // 计算最终的尺寸
        // 组件的尺寸
        int w = width();
        int h = height();

        // 计算rect
        int dx = 0;
        int dy = 0;
        int dw = spec.width;
        int dh = spec.height;

        // 计算目标尺寸
        if (dw > w || dh > h) { // 缩放
            if (dw * h > w * dh) { // 视频的宽高比 > 播放器的宽高比
                dh = w * dh / dw;
                dw = w;
            } else {
                dw = h * dw / dh;
                dh = h;
            }
        }

        // 居中
        dx = (w - dw) >> 1;
        dy = (h - dh) >> 1;

        _rect = QRect(dx, dy, dw, dh);
    }

    update();//触发paintEvent方法
}

void VideoWidget::paintEvent(QPaintEvent *event) {
    if (!_image) return;

    // 将图片绘制到当前组件上
    QPainter(this).drawImage(_rect, *_image);
}

这里的计算最终的尺寸我们可以参考之前介绍的《24_用Qt和FFmpeg实现简单的YUV播放器

此时我们运行播放的时候,可以发现视频画面非常的快速的播放完了,此时我们先使用SDL_Delay(33);控制播放速度,后面在进行音视频同步的时候在处理。

五、释放资源

void VideoPlayer::freeVideo(){
    clearVideoPktList();
    avcodec_free_context(&_vDecodeCtx);
    av_frame_free(&_vSwsInFrame);
    if (_vSwsOutFrame) {
        av_freep(&_vSwsOutFrame->data[0]);
        av_frame_free(&_vSwsOutFrame);
    }
    sws_freeContext(_vSwsCtx);
    _vSwsCtx = nullptr;
    _vStream = nullptr;
}

我们实现释放资源后,再去运行后点击停止会出现内存错误。
这是因为我们在像素格式转换后,将_vSwsOutFrame->data[0]数据直接发送给onPlayerFrameDecoded方法的uint8_t *data,这里就会牵扯到多线程同时访问一块内存区域(橡树转换sws_scale是在子线程,渲染时在主线程)

解决办法就是把_vSwsOutFrame->data[0]指向的RGB数据拷贝到另外一个内存空间

// 像素格式的转换
sws_scale(_vSwsCtx,
          _vSwsInFrame->data, _vSwsInFrame->linesize,
          0, _vDecodeCtx->height,
          _vSwsOutFrame->data, _vSwsOutFrame->linesize);

uint8_t *data = (uint8_t *)av_malloc(_vSwsOutSpec.size);
 memcpy(data, _vSwsOutFrame->data[0], _vSwsOutSpec.size);
// 发出信号
emit frameDecoded(this,data,_vSwsOutSpec);
void VideoWidget::freeImage() {
    if (_image) {
        av_free(_image->bits());
        delete _image;
        _image = nullptr;
    }
}

六、细节处理

6.1 非音频、视频流补充av_packet_unref

非音频、视频流补充

6.2 很快读完了所有数据包,数据包过多

我们通过while循环只要不是停止状态就拼命的读av_read_frame,读到了就往里面塞数据包(addAudioPkt(pkt)addVideoPkt(pkt))。实际上就会发现这个段代码不管音视频多大很快就会读完,这样如果音视频非常大,一下载入到内存中就会有问题,所以需要做一下限制

#define AUDIO_MAX_PKT_SIZE 1000
#define VIDEO_MAX_PKT_SIZE 500

// 从输入文件中读取数据
AVPacket pkt;
while (_state != Stopped) {
   if (_vPktList->size() >= VIDEO_MAX_PKT_SIZE ||
           _aPktList->size() >= AUDIO_MAX_PKT_SIZE) {
        SDL_Delay(10);
       continue;
   }

   qDebug()<< _vPktList->size()<< _aPktList->size();

   ......
}

源码链接

原文地址:http://www.cnblogs.com/zuojie/p/16870625.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性