使用FFmpeg 解码视频数据

548 阅读6分钟

开发环境

  • FFmpeg 3.4 window 动态库
  • window
  • Visual Studio 2022

用的开发环境可以在前面的文章中找的哦,依然是在前篇博客里面工程里面增加代码,接着 使用FFmpeg 做音频数据重采样 文章继续开发

开发过程

1. 在FileDecode的AVOpenFile方法里面添加代码

int FileDecode::AVOpenFile(std::string filename)
{

#ifdef WRITE_DECODED_PCM_FILE
    outdecodedfile = fopen("decode.pcm", "wb");
    if (!outdecodedfile) {
        std::cout << "open out put file failed";
    }
#endif

#ifdef WRITE_DECODED_YUV_FILE
    outdecodedYUVfile = fopen("decoded_video.yuv", "wb");
    if (!outdecodedYUVfile) {
        std::cout << "open out put YUV file failed";
    }
#endif

	int openInputResult = avformat_open_input(&formatCtx, filename.c_str(), NULL, NULL);
    if (openInputResult != 0) {
        std::cout << "open input failed" << std::endl;
        return -1;
    }

    if (avformat_find_stream_info(formatCtx, NULL) < 0) {
        std::cout << "find stram info faild" << std::endl;
        return -1;
    }

    av_dump_format(formatCtx, 0, filename.c_str(), 0);

    audioStream = av_find_best_stream(formatCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    if (audioStream < 0) {
        std::cout << "av find best audio stream failed" << std::endl;
        return -1;
    }

    videoStream = av_find_best_stream(formatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if (videoStream < 0) {
        std::cout << "av find best video stream failed" << std::endl;
        return -1;
    }

    return 0;
}
  • 增加了 outdecodedYUVfile 文件的创建,用来存放解码后的视频源数据
  • videoStream 视频流解析

2. 在FileDecode的创建 OpenVideoDecode方法

方法的作用查找和打开一个视频解码器

int FileDecode::OpenVideoDecode() {
    videoCodecCtx = formatCtx->streams[videoStream]->codec;

    AVCodec* codec = avcodec_find_decoder(videoCodecCtx->codec_id);
    if (codec == NULL) {
        std::cout << "cannot find video codec id: " << videoCodecCtx->codec_id << std::endl;
        return -1;
    }

    // Open codec
    AVDictionary* dict = NULL;
    int codecOpenResult = avcodec_open2(videoCodecCtx, codec, &dict);
    if (codecOpenResult < 0) {
        std::cout << "open video decode faild" << std::endl;
        return -1;
    }

    return 0;
}

videoCodecCtx里面存储了解码器的上下文,后面会用到里面的信息

3. 在FileDecode的Decode() 方法里面增加对视频包的处理

int FileDecode::Decode()
{
    
    AVPacket avpkt;
    av_init_packet(&avpkt);
    do {
        
       
        if (av_read_frame(formatCtx, &avpkt) < 0) {

            //没有读到数据,说明结束了
            return 0;
        }
        if (avpkt.stream_index == audioStream)
        {
            //std::cout << "read one audio frame" << std::endl;
            DecodeAudio(&avpkt);
            av_packet_unref(&avpkt);
        }
        else if(avpkt.stream_index == videoStream) {
            DecodeVideo(&avpkt);
            av_packet_unref(&avpkt);
            continue;
        }
    } while (avpkt.data == NULL);
}

DecodeVideo 就是对视频的流解码处理

int FileDecode::DecodeVideo(AVPacket* originalPacket)
{
    int ret = avcodec_send_packet(videoCodecCtx, originalPacket);
    if (ret < 0)
    {
        return -1;
    }
    AVFrame* frame = av_frame_alloc();
    ret = avcodec_receive_frame(videoCodecCtx, frame);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
        return -2;
    }
    else if (ret < 0) {
        std::cout << "error decoding";
        return -1;
    }
#ifdef  WRITE_DECODED_YUV_FILE


    enum AVPixelFormat pix_fmt = videoCodecCtx->pix_fmt;
    bool isPlanar = is_planar_yuv(pix_fmt);

    int wrapy = frame->linesize[0];
    int wrapu = frame->linesize[1];
    int wrapv = frame->linesize[2];
    int xsize = frame->width;
    int ysize = frame->height;

    if (pix_fmt == AV_PIX_FMT_YUV420P) {
        fwrite(frame->data[0], 1, wrapy * ysize, outdecodedYUVfile); // Y
        fwrite(frame->data[2], 1, wrapv * ysize / 2, outdecodedYUVfile); // V
        fwrite(frame->data[1], 1, wrapu * ysize / 2, outdecodedYUVfile); // U
    }


#endif //  WRITE_DECODED_PCM_FILE

    av_frame_free(&frame);
   
}

同样的,解码过程是由 avcodec_send_packet 和 avcodec_receive_frame 完成。送入AVPacket ,得到AVFrame 解码后的数据,然后将Frame里面的yuv数据写入文件。

宏定义判断条件里面的代码,就是用来把AVFrame* 里面解码出来的数据包写入文件,这里会涉及一个知识点,yuv,我们的文件是h264视频编码,也是目前主流的视频编码格式。它解码出来的视频原始数据格式是yuv,yuv有比较多的格式。这次我们只重点将一个,后面可以专题来讲着个知识。

yuv 讲解

YUV格式有两大类:planarpacked

  • 对于planarYUV格式,先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V就好比 YYYYYYYYYYYYYYYYYYYYYUUUUUUUUUUUUUUUVVVVVVVVVVV,存储planar格式的数据的时候,比如AVFrame->data 里面有数组长度为3,AVFrame->data[0],AVFrame->data[1],AVFrame->data[2]分别指向buf里面存储的y,u,v的起点。说到道理大家明白了和音频一样,AVframe->data 是个指针指向作用,实际是一个以一维数组存储所有数据
  • 对于packedYUV格式,每个像素点的Y,U,V是连续交叉存储的,比如 YYYYUV YYYYUV。所以 AVFrame->data[0] 就指向了全部数据,frame->linesize[0] 也是全部数据的长度。这种情况 frame->data[i] 都等于 AVFrame->data[0]。 frame->linesize[i] 都等于 frame->linesize[0],可以通过这个来判断yuv平面类型
int is_planar_yuv(const AVFrame *frame) {
    if (!frame)
        return -1; // Invalid frame

    // Check if linesize array contains different values
    for (int i = 1; i < AV_NUM_DATA_POINTERS; i++) {
        if (frame->linesize[i] != frame->linesize[0])
            return 1; // Planar layout
    }

    // Check if data pointers are different
    for (int i = 1; i < AV_NUM_DATA_POINTERS; i++) {
        if (frame->data[i] != frame->data[0])
            return 1; // Planar layout
    }

    return 0; // Non-planar layout
}

在我的代码里面,弄得更加简单一点,常用的就那几种,直接写上就可以了,严格的话,就用上面的

bool FileDecode::is_planar_yuv(enum AVPixelFormat pix_fmt) {
   
    // Check if the pixel format corresponds to planar layout
    switch (pix_fmt) {
    case AV_PIX_FMT_YUV420P:
    case AV_PIX_FMT_YUV422P:
    case AV_PIX_FMT_YUV444P:
        // Add more planar pixel formats here if needed
        return 1; // Planar layout
    default:
        return 0; // Non-planar layout
    }
}

sample.jpg

  • YUV 4:4:4采样,每一个Y对应一组UV分量。
  • YUV 4:2:2采样,每两个Y共用一组UV分量。
  • YUV 4:2:0采样,每四个Y共用一组UV分量

在我这儿测试例子里面,我用的文件是 AV_PIX_FMT_YUV420P 的,是planar layout 格式的。 所以就从frame->data的三个指针位置去取

通过上面表述,我们就可以计算一个帧视频数据有多少个字节,加入是yuv420格式的,960*400分辨率的。那么它的y占用960×400个字节,u占用 960×400/4个, v占用 960×400/4。一张yuv图一共占用960×400×3/2个字节。

回到上面工程源码:

    int wrapy = frame->linesize[0];
    int wrapu = frame->linesize[1];
    int wrapv = frame->linesize[2];
    int xsize = frame->width;
    int ysize = frame->height;
    
     if (pix_fmt == AV_PIX_FMT_YUV420P) {
        fwrite(frame->data[0], 1, wrapy * ysize, outdecodedYUVfile); // Y
        fwrite(frame->data[2], 1, wrapv * ysize / 2, outdecodedYUVfile); // V
        fwrite(frame->data[1], 1, wrapu * ysize / 2, outdecodedYUVfile); // U
    }

xszie 就是宽度 960,ysize 就是400。那wrapy是什么呢,这里涉及一个字节对齐。

1714052072515.jpg

在这个图里面,有效数据宽高是960*400,但是为了计算机存储,宽度多了4个字节,后边部分,其实是冗余数据。那么这个wrapy就是960,xsize是964,同理高度也可以有这样的值。所有在拷贝数据的时候,我们只拷贝960部分的。

不知是否发现AVFrame->data 拷贝到文件数据最终格式就是 YYYYUV YYYYUV YYYYUV。 这其实就是packed 格式的。

4.运行验证

执行程序后,会生成一个 decoded_video.yuv 文件,这个文件是很大的,这也证明了为什么视频数据编码压缩。我用下面这个工具来验证播放。

1714052524210.jpg

工具->参数设置,可以设置yuv的格式,分辨率可以自定义,按照实际情况填写

image.png

5. 补充

注意要关闭和释放相关参数

void FileDecode::Close()
{
    if (swrResample) {
        swrResample->Close();
    }

#ifdef  WRITE_DECODED_PCM_FILE
    fclose(outdecodedfile);
#endif //  WRITE_DECODED_PCM_FILE

#ifdef  WRITE_DECODED_YUV_FILE
    fclose(outdecodedYUVfile);
#endif //  WRITE_DECODED_PCM_FILE



  
    if (audioCodecCtx) {
        avcodec_close(audioCodecCtx); // 注意这里要用关闭,不要用下面free,会不够彻底,导致avformat_close_input崩溃
        //avcodec_free_context(&audioCodecCtx);
    }
    if (videoCodecCtx) {
        avcodec_close(videoCodecCtx);
        //avcodec_free_context(&videoCodecCtx);
    }

    if (formatCtx) {
        avformat_close_input(&formatCtx);
    }
    
}

这里之前的的有个bug,avcodec_close 替代 avcodec_free_context,才可以正常关闭解码器,不然后面释放format的时候会报错

6.总结

视频解码写文件流程和音频类似,难点在于yuv格式的理解,这也是音视频知识的一个重要点。需要的话,可以自行去仔细学,以后有机会,我也会详细讲解一次

7. 其他: