查看原文
其他

FFmpeg连载3-视频解码

思想觉悟 思想觉悟 2023-04-10

导读

在前面我们介绍了FFmpeg的解封装,并且实现了提取视频文件中的音频流和视频流单独输出,使用ffplay播放验证, 今天我们使用FFmpeg解码视频流,将视频解码为YUV并输出到文件,然后使用ffplay播放YUV图像。

关于YUV的相关知识,之前笔者也有过一些笔记,但是写得比较简单,大家可以网上找找更加详细的资料:
音视频基础知识-YUV图像

关于使用FFmpeg进行视频解码的文章,之前也写过类似的文章《Android使用ffmpeg解码视频为YUV》
但是在这篇文章中有一个错误的点就是写入的YUV的方法不是通用的,对于一些视频解码出来的YUV,按照文章中的方法写入可能会有播放花屏,甚至无法播放的情况。对于这点如果有误人子弟的话, 笔者深感抱歉,在这里说明一下,笔者发表的这些博客仅作为笔记或交流需要,不具备权威性,观点总结仅限于自己的理解,不保证所有的准确性哈。。。

AVFrame介绍

相对于解封装而言,视频解码时我们需要用到一个新的结构体AVFrame。

AVFrame可以说是一个与AVPacket相对应的结构体,既然AVPacket表示的是音视频包解码前或编码后的数据,那么AVFrame就是音视频包解码后或编码前的原始数据包。

AVFrame内部包含了一个视频帧或音频帧所持续播放的时间,播放的时机等时间信息,同时还包含了采样率,采样格式、图片格式、帧类型等相关信息。

在FFmpeg中我们使用av_frame_alloc()分配一个AVFrame,使用av_frame_free释放一个AVFrame,使用函数av_frame_get_buffer为AVFrame内部分配数据缓冲区。

视频解码

回顾之前的解封装的一张图:

视频的解码阶段就是发生在函数av_read_frame之后,如果读取到的资源包是视频类型的则送进解码器进行解码。

我们来看看另外一张图,这张图主要介绍了解码视频过程中用到的一些结构体的功能:

下面简单介绍一下视频解码的两个重要操作步骤:

1、配置解码器

配置解码器这个步骤又可以拆分为四个小步骤:

a、查找解码器 b、分配解码器上下文 c、按照视频流信息配置解码参数到解码器上下文 d、打开解码器

这四个小步骤对于的FFmpeg的API分别是:

// 查找解码器
avcodec_find_decoder 或 avcodec_find_decoder
// 分配解码器上下文
avcodec_alloc_context3
//按照视频流信息配置解码参数到解码器上下文
avcodec_parameters_to_context
//打开解码器
avcodec_open2

2、发送解码包及获取解码YUV数据帧

解码阶段主要用到的两个关键API是avcodec_send_packetavcodec_receive_frame其中avcodec_send_packet表示发送一个视频数据包到解码器,然后使用avcodec_receive_frame接收 解码数据帧(也就是YUV数据)。avcodec_send_packetavcodec_receive_frame并不是一一对应的调用关系,而是一个avcodec_send_packet的调用,可能会对应n个avcodec_receive_frame函数的 调用。因为解码器内部是有缓存和参考帧的,并不是每送进去一个数据包就能解码出一帧数据,可能出现送进去几个数据包,但是暂时没有数据帧解码输出的情况,也可能会出现某个时间点送进去一个数据包,然后会输出n个数据帧的情况。

主要代码如下:

VideoDecoder.h

#include <string>

class VideoDecoder {

public:
    VideoDecoder();

    ~VideoDecoder();

    void decode_video(std::string media_path, std::string yuv_path);
};

以下是实现文件:

VideoDecoder.cpp
#include "VideoDecoder.h"
#include <iostream>
extern "C"{
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
#include <libavutil/log.h>
}

VideoDecoder::VideoDecoder() {

}

VideoDecoder::~VideoDecoder() {

}

void VideoDecoder::decode_video(std::string media_path, std::string yuv_path) {

    AVFormatContext *avFormatContext = nullptr;
    AVCodecContext *avCodecContext = nullptr;

    avFormatContext = avformat_alloc_context();
    avformat_open_input(&avFormatContext,media_path.c_str(), nullptr,nullptr);

    av_dump_format(avFormatContext,0,media_path.c_str(),0);

    int video_index = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_VIDEO,-1,-1, nullptr,0);
    if(video_index < 0){
        std::cout << "没有找到视频" << std::endl;
    }

    const AVCodec *avCodec = avcodec_find_decoder(avFormatContext->streams[video_index]->codecpar->codec_id);
    avCodecContext = avcodec_alloc_context3(avCodec);
    avcodec_parameters_to_context(avCodecContext,avFormatContext->streams[video_index]->codecpar);
    int ret = avcodec_open2(avCodecContext,avCodec, nullptr);
    if(ret < 0){
        std::cout << "解码器打开失败" << std::endl;
    }

    FILE *yuv_file = fopen(yuv_path.c_str(),"wb");

    AVPacket *avPacket = av_packet_alloc();
    AVFrame *avFrame = av_frame_alloc();
    while (true){
        ret = av_read_frame(avFormatContext,avPacket);
        if(ret < 0){
            std::cout << "文件读取完毕" << std::endl;
            break;
        } else if(video_index == avPacket->stream_index){
            ret = avcodec_send_packet(avCodecContext,avPacket);
            if(ret < 0){
                std::cout << "视频发送解码失败:" << av_err2str(ret) << std::endl;
            }
            while (true){
               ret = avcodec_receive_frame(avCodecContext,avFrame);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                    std::cout << "avcodec_receive_frame:" << av_err2str(ret) << std::endl;
                    break;
                } else if (ret < 0) {
                    std::cout << "视频解码失败:" << std::endl;
                    return;
                } else{
                    std::cout << "写入YUV文件avFrame->linesize[0]:"  << avFrame->linesize[0]  << "avFrame->width:" << avFrame->width << std::endl;
                    std::cout << "avFrame->format:"  << avFrame->format << std::endl;
                    // 播放 ffplay -i YUV文件路径 -pixel_format yuv420p -framerate 25 -video_size 640x480
                    // frame->linesize[1]  对齐的问题
                    // 正确写法  linesize[]代表每行的字节数量,所以每行的偏移是linesize[]
                    // 成员data是个指针数组,每个成员所指向的就是yuv三个分量的实体数据了,成员linesize是指对应于每一行的大小,为什么需要这个变量,是因为在YUV格式和RGB格式时,每行的大小不一定等于图像的宽度
                    //
                    for(int j=0; j<avFrame->height; j++)
                        fwrite(avFrame->data[0] + j * avFrame->linesize[0], 1, avFrame->width, yuv_file);
                    for(int j=0; j<avFrame->height/2; j++)
                        fwrite(avFrame->data[1] + j * avFrame->linesize[1], 1, avFrame->width/2, yuv_file);
                    for(int j=0; j<avFrame->height/2; j++)
                        fwrite(avFrame->data[2] + j * avFrame->linesize[2], 1, avFrame->width/2, yuv_file);

                    // 错误写法 用source.200kbps.766x322_10s.h264测试时可以看出该种方法是错误的
                    // 如果frame.width == avFrame->linesize[0] 则可以用这种方式写入
                    //  写入y分量
//        fwrite(avFrame->data[0], 1, avFrame->width * avFrame->height,  yuv_file);//Y
//        // 写入u分量
//        fwrite(avFrame->data[1], 1, (avFrame->width) *(avFrame->height)/4,yuv_file);//U:宽高均是Y的一半
//        //  写入v分量
//        fwrite(avFrame->data[2], 1, (avFrame->width) *(avFrame->height)/4,yuv_file);//V:宽高均是Y的一半
                }
            }
        }
        av_packet_unref(avPacket);
    }

    fflush(yuv_file);
    av_packet_free(&avPacket);
    av_frame_free(&avFrame);

    if(nullptr != yuv_file) {
        fclose(yuv_file);
        yuv_file = nullptr;
    }
}

对于解码出来的YUV输出文件,我们可以使用ffplay命令来进行播放:

// 其中 640x480 需要替换成自己解码的视频的真实宽高
ffplay -i YUV文件路径 -pixel_format yuv420p -framerate 25 -video_size 640x480

针对导读中提到的YUV非通用写法,笔者在代码中做了简单的注释。更多的资料可以查询关于FFmpeg内存对齐的问题,例如针对图像来说并不是像素对齐而是字节对齐的。

注意

在上面的例子中,视频文件读取完毕之后并没有对编码器内部进行数据冲刷,可能会导致视频的最后几帧丢失的情况,更加规范的写法应该在文件读取完毕之后再次调用函数avcodec_send_packet但是需要传入空的视频数据包, 然后通过循环调用avcodec_receive_frame将解码器中缓存的数据帧全部获取出来。

还有别忘了释放资源。。。

系列推荐

推荐阅读

FFmpeg连载1-开发环境搭建
FFmpeg连载2-分离视频和音频

关注我,一起进步,人生不止coding!!!


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存