首页 > 其他分享 >33_音视频播放器_画面显示

33_音视频播放器_画面显示

时间:2022-11-08 18:00:23浏览次数:85  
标签:播放器 33 解码 音视频 vSwsOutFrame 像素 vSwsOutSpec data image

目录

一、简介

上节介绍了使用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();

   ......
}

源码链接

标签:播放器,33,解码,音视频,vSwsOutFrame,像素,vSwsOutSpec,data,image
From: https://www.cnblogs.com/zuojie/p/16870625.html

相关文章

  • 32_音视频播放器_SDL播放
    目录一、简介二、音频重采样2.1引入头文件2.2定义重采样相关属性2.3初始化重采样2.4重采样三、SDL播放四、停止功能五、处理读完音频包的情况六、实现调节音量七、实现......
  • 视频融合平台EasyCVR继承播放器,但是无法播放该如何解决?
    EasyCVR视频融合云平台基于云边端一体化架构,兼容性高、拓展性强,可支持多类型设备、多协议方式接入,包括国标GB/T28181、RTMP、RTSP/Onvif协议,以及厂家的私有协议,如:海康Ehome......
  • 30_音视频播放器_解封装
    目录一、简介二、读出文件三、初始化3.1读取文件3.2初始化音频信息和视频信息3.3初始化解码器四、实现视频时长一、简介我们使用QT+ffmpeg实现一个播放器,这里我们主要......
  • 【操作说明】全能型H.265播放器如何使用?
    本播放器集成了公司业务的接口,包含了实播,回放,云台控制和回放速度控制,截图和全屏功能可以根据type直接初始化接口地址如果是第三方业务对接,也可以单独配置接口地址正确使用......
  • Computer Vision_33_SIFT:Improving Bag-of-Features for Large Scale Image Search—
    此部分是计算机视觉部分,主要侧重在底层特征提取,视频分析,跟踪,目标检测和识别方面等方面。对于自己不太熟悉的领域比如摄像机标定和立体视觉,仅仅列出上google上引用次数比较多......
  • 音视频开发进阶|第六讲:色彩和色彩空间·下篇
    在前两篇推文中,我们了解了色彩空间、像素、图像和视频之间的组成关系,并且比较详细的学习了色彩空间RGB、YUV的采样&存储格式。今天,我们基于这些内容,再补充一些重要的关联......
  • 融云 CDN 播放器 2.0 版本正式上线
    近期,融云自研CDN播放插件2.0版本正式上线。在原有版本支持融云内置CDN直播流播放的基础上,开放了相关API,满足开发者的多种业务需求。关注【融云全球互联网通信云】了......
  • 垃圾蜘蛛UA屏蔽封禁python|SM-G900P|OPPO A33|DataForSeoBot|omgili|SemrushBot|Adsbo
    (python|Python|SM-G900P|OPPOA33|DataForSeoBot|omgili|SemrushBot|Adsbot|AhrefsBot|mj12bot|MJ12|WebMeUp|serpstatbot|leiki|opensiteexplorer|hubspot)列表里面有两......
  • js获取上传音视频的时长
    js获取上传音视频的时长 获取上传视频路径,将该路径放入video标签,获取视频时长 方式一:隐藏一个音频标签,播放获取。<videostyle="display:none;"controls="controls......
  • 我把分布式音乐播放器适配了Stage模型
     OpenAtomOpenHarmony(以下简称“OpenHarmony”)应用开发自API8及其更早版本一直使用的是FA模型进行开发。FA模型是FeatureAbility的缩写,它和PA(ParticleAbility)两种类型......