BPlay1.0系列(6:视频播放)

前面我们已经完成了工程的构建、ffmpeg装载以及基本控件添加,现在就开始BPlay1.0核心部分的分析:视频播放。

1、解封装

Bffmpeg::BLoadMediaFile这个方法在 BPlay1.0系列(4:Bffmpeg单例模式设计+媒体流文件检测) 提到过,当时说是用于检测媒体文件的合法性而构建的,其实其本质就是解封装媒体文件,通过解封装来判断文件是否可以成功载入。在讲解解封装之前,需要先了解这一步在整个视频播放流程的作用和地位,以及播放器在运行的时候究竟需要做哪些事。
在这里插入图片描述

上图描述了一个封装数据格式文件在播放时的完整步骤,可以看到第一步就是解封装,以MP4文件为例,它是由编码视频+编码音频合成的,解封装(Bffmpeg::BLoadMediaFile)就是将MP4拆成两部分:编码视频和编码音频,并保存相关信息到FormatContext中,同时获取到音/视频对应的索引和解码器,用于后续音视频解码。

2、音视频解码

首先在音视频主控线程里面,需要不断获取帧数据,将视频帧和音频帧分别加到各自的队列里面:

/********************************
 * void Bffmpeg::run()
 * 功能:音视频主控线程
 * *****************************/
void Bffmpeg::run()
{
    int Ret = 0;
    AVPacket pkt;
    int c = 0;
    while (ffmpegrun) {
        if (AudioQue.que.size() > 5 && VideoQue.que.size() > 5) {
            msleep(10);
            continue;
        }
        Ret = av_read_frame(FormatContext, &pkt);
        if (0 != Ret) {
            BLOG("av_read_frame fail, Ret[%d]", Ret);
            return;
        }

        if (pkt.stream_index == Audio_index) {
            /* 音频流入队列 */
            QMutexLocker locker(&AudioQue.mtx);
            AudioQue.que.push_back(pkt);
        } else if (pkt.stream_index == Video_index) {
            /* 视频流入队列 */
            QMutexLocker locker(&VideoQue.mtx);
            VideoQue.que.push_back(pkt);
        }

        msleep(1);
    }
}

接着时视频解码线程Bvideo::run,它不断从VideoQue队列里面取数据(编码),解码后送到frameque(RGB队列):

/********************************
 * void Bvideo::run()
 * 功能:视频解码线程
 * *****************************/
void Bvideo::run()
{
    while (videorun)
    {
        AVPacket pkt;
        if (Bffmpeg::GetInstance()->GetVideoQue().que.size() == 0) {
            msleep(10);
            continue;
        } else {
            /* 视频流编码数据出队列 */
            QMutexLocker Locker(&Bffmpeg::GetInstance()->GetVideoQue().mtx);
            AVPacket pkt = Bffmpeg::GetInstance()->GetVideoQue().que.front();
            Bffmpeg::GetInstance()->GetVideoQue().que.pop_front();
        }

        /* 解码 */
        AVFrame* frame = Bffmpeg::GetInstance()->Decode(pkt);
        av_packet_unref(&pkt); 
        if (frame == NULL) {
            continue;
        }
        
        /* 解码帧数据入RGB队列 */
        QMutexLocker FrameLocker(&frameque.mtx);
        frameque.frame.push_back(frame);
        
        msleep(1);
    }
    return;
}

视频解码线程了负责解码的方法实际是Bffmpeg::GetInstance()->Decode(pkt):

/********************************
 * AVFrame* Bffmpeg::Decode(AVPacket &pkt)
 * 功能:音视频解码
 * *****************************/
AVFrame* Bffmpeg::Decode(AVPacket &pkt)
{
    int Ret = 0;
    AVFrame *frame = av_frame_alloc();   /* 存在反复申请释放(转RGB后释放),后续优化 */
    
    /* 发送数据到ffmepg,放到解码队列中 */
    Ret = avcodec_send_packet(FormatContext->streams[pkt.stream_index]->codec, &pkt);
    if (0 != Ret) {
        BLOG("avcodec_send_packet fail, ret:%d", Ret);
        return NULL;
    }

    /* 从解码队列中取出frame */
    Ret = avcodec_receive_frame(FormatContext->streams[pkt.stream_index]->codec, frame);
    if (0 != Ret) {
        BLOG("avcodec_send_paavcodec_receive_framecket fail, ret:%d", Ret);
        return NULL;
    }
    
    return frame;
}

经过这几个关键接口,视频解码数据已经生成,并且通过frameque(RGB队列)转到显示模块(Bwidget)了。

3、视频显示

显示视频帧主要是通过Bwidget的paintEvent实现,通过定时器(update方法)触发画图事件,先从frame队列取出解码数据,再进行格式统一转化(RGBA32),最后就可以在画板上呈现出来了:

/********************************
 * void Bwidget::paintEvent(QPaintEvent *event)
 * 功能:绘制一帧图像
 * *****************************/
void Bwidget::paintEvent(QPaintEvent *event)
{
    if (Bvideo::GetInstance()->GetFrameque().frame.size() == 0) {
        return;
    }

    /* 解码数据出队列 */
    QMutexLocker Locker(&Bvideo::GetInstance()->GetFrameque().mtx);
    AVFrame* frame = Bvideo::GetInstance()->GetFrameque().frame.front();

    SwsContext *context = NULL;
    AVCodecContext *codec = Bffmpeg::GetInstance()->GetFormatContext()->streams[Bffmpeg::GetInstance()->GetVideoIndex()]->codec;

    /* 解码数据转RGB32 */
    context = sws_getCachedContext(context,                                         /* 转化上下文结构体 */
                                   codec->width, codec->height, codec->pix_fmt,     /* 源图像格式和宽高 */
                                   Width, Height, AV_PIX_FMT_BGRA,                  /* 目标图像格式和宽高 */
                                   SWS_BICUBIC, NULL, NULL, NULL);

    if (NULL == context) {
        BLOG("sws_getCachedContext fail");
        return;
    }

    uint8_t *data[AV_NUM_DATA_POINTERS] = { 0 };
    data[0] = (uint8_t *)Image->bits();
    int linesize[AV_NUM_DATA_POINTERS] = { 0 };
    linesize[0] = Width * 4;                /* 实际显示一行的宽度,32位4个字节 */
    sws_scale(context, frame->data,         /* 源视频解码数据 */
              frame->linesize,              /* 每行大小 */
              0,                            /* 用不到 */
              frame->height,                /* 图像高度 */
              data,                         /* 输出的每个通道数据指针 */
              linesize);                    /* 每个通道行字节数 */

    /* 释放解码数据,出队列 */
    av_frame_free(&frame);
    Bvideo::GetInstance()->GetFrameque().frame.pop_front();

    /* 绘制图像 */
    QPainter painter;
    painter.begin(this);
    painter.drawImage(QPoint(X, Y), *Image);
    painter.end();

    return;
}

此外,对于这个画板还有两个初始化的关键方法Bwidget::InitMedia(),在加载完媒体文件后调用,主要是相关信息获取、内存申请,画布规划这些:

/********************************
 * void Bwidget::InitMedia()
 * 功能:初始化媒体画布相关信息
 * *****************************/
void Bwidget::InitMedia()
{
    int WidgetWidth = width();          /* Bwidget宽 */
    int WidgetHeight = height();        /* Bwidget高 */
    int VideoIndex = Bffmpeg::GetInstance()->GetVideoIndex();
    int MediaWidth = Bffmpeg::GetInstance()->GetFormatContext()->streams[VideoIndex]->codec->width;     /* 视频宽 */
    int MediaHeight = Bffmpeg::GetInstance()->GetFormatContext()->streams[VideoIndex]->codec->height;   /* 视频高 */
    
    if (((float)WidgetWidth / (float)WidgetHeight) > ((float)MediaWidth / (float)MediaHeight)) {
        /* Bwidget宽高比大于视频,左右需要有黑边 */
        Width = (int)((float)WidgetHeight * (float)MediaWidth / (float)MediaHeight);
        Height = WidgetHeight;
        X = (WidgetWidth - Width) / 2;
        Y = 0;
    } else {
        /* Bwidget宽高比小于视频,上下需要有黑边 */
        Width = WidgetWidth;
        Height = ((float)WidgetWidth * (float)MediaHeight / (float)MediaWidth);
        X = 0;
        Y = (WidgetHeight - Height) / 2;;
    }

    if (ImageData) {
        delete ImageData;
    }
    
    if (Image) {
        delete Image;
    }

    /* 初始化视频区域 */
    ImageData = new uchar[Width * Height * 4];
    Image = new QImage(ImageData, Width, Height, QImage::Format_RGB32);
    
    return;
}

最后构建工程,效果如下:
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值