雷神simplest_ffmpeg_player解析(一)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/asd501823206/article/details/96189328

写在前面

学习雷神的博客,向雷神致敬~

看了雷神的小学期视频课,在Github上下载了simplest_ffmpeg_player的代码,为代码加上了注释,作为留存。

2019.07.16

视频中的前置知识点
simple_ffmpeg_decoder.cpp注释
simple_ffmpeg_decoder_pure.cpp注释


链接及参考资料

《基于 FFmpeg + SDL 的视频播放器的制作》课程的视频
FFmpeg Documentation
FFmpeg源代码简单分析


知识点

封装、编码格式

在这里插入图片描述
在这里插入图片描述

FFmpeg解码流程及数据结构

在这里插入图片描述

FFmpeg数据结构简介

AVFormatContext:封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装格式相关信息
AVInputFormat:每种封装格式对应一个该结构体
AVStream:视频文件每个视频(音频)流对应一个该结构体
AVCodecContext:编码器上下文结构体,保存了视频(音频)编解码相关信息
AVCodec:每种视频(音频)编解码器对应一个该结构体
AVPacket:存储一帧压缩编码数据
AVFrame:存储一帧解码后像素(采样)数据

AVFormatContext

  • iformat:输入视频的AVIputFormat
  • nb_streams:输入视频的AVStream个数
  • streams:输入视频的AVStream[]数组
  • duration:输入视频的时长(以微秒为单位)
  • bit_rate:输入视频的码率

AVIputFormat

  • name:封装格式名称
  • long_name:封装格式的长名称
  • extensions:封装格式的扩展名
  • id:封装格式ID
  • 一些封装格式处理的接口函数

AVStream

  • id:序号
  • codec:该流对应的AVCodecContext
  • time_base:该流的时基
  • r_frame_rate:该流的帧率

AVCodecContext

  • codec:编解码器的AVCodec
  • width,height:图像的宽高(只针对视频)
  • pix_fmt:像素格式(只针对视频)
  • sample_rate:采样率(只针对音频)
  • channels:声道数(只针对音频)
  • sample_fmt:采样格式(只针对音频)

AVCodec

  • name:编解码器名称
  • long_name:编解码器长名称
  • type:编解码器类型
  • id:编解码器ID
  • 一些编解码的接口函数

1.simplest_ffmpeg_decoder.cpp
/**
 * 最简单的基于FFmpeg的视频解码器
 * Simplest FFmpeg Decoder
 *
 * 雷霄骅 Lei Xiaohua
 * [email protected]
 * 中国传媒大学/数字电视技术
 * Communication University of China / Digital TV Technology
 * http://blog.csdn.net/leixiaohua1020
 *
 *
 * 本程序实现了视频文件解码为YUV数据。它使用了libavcodec和
 * libavformat。是最简单的FFmpeg视频解码方面的教程。
 * 通过学习本例子可以了解FFmpeg的解码流程。
 * This software is a simplest decoder based on FFmpeg.
 * It decodes video to YUV pixel data.
 * It uses libavcodec and libavformat.
 * Suitable for beginner of FFmpeg.
 *
 */



#include <stdio.h>

#define __STDC_CONSTANT_MACROS

#ifdef _WIN32
//Windows
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#ifdef __cplusplus
};
#endif
#endif

/**
 * 将视频解封装、解码,转换为yuv格式
 **/
int main(int argc, char* argv[])
{
    // 封装格式上下文的结构体,也是统领全局的结构体,保存了视频文件封装格式的相关信息
   AVFormatContext    *pFormatCtx;

   // 视频流在文件中的位置
   int             i, videoindex;

   // 编码器上下文结构体,保存了视频(音频)编解码相关信息
   AVCodecContext *pCodecCtx;

   // 每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体
   AVCodec          *pCodec;

   // 存储一帧解码后像素(采样)数据
   AVFrame    *pFrame,*pFrameYUV;

   //
   unsigned char *out_buffer;

   // 存储一帧压缩编码数据
   AVPacket *packet;

   // width×height,用于计算YUV数据分布
   int y_size;

   // 视频是否解码成功的返回
   int ret, got_picture;

   // libswsscale 上下文
   struct SwsContext *img_convert_ctx;

    // 输入文件
   char filepath[]="Titanic.mkv";

    // 输出文件
   FILE *fp_yuv=fopen("output.yuv","wb+");  

    // 注册复用器,编码器等(参考FFmpeg解码流程图)
   av_register_all();
   avformat_network_init();
   pFormatCtx = avformat_alloc_context();

    // 打开多媒体数据并且获得一些相关的信息(参考FFmpeg解码流程图)
   if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0){
      printf("Couldn't open input stream.\n");
      return -1;
   }

   // 读取一部分视音频数据并且获得一些相关的信息(参考FFmpeg解码流程图)
   if(avformat_find_stream_info(pFormatCtx,NULL)<0){
      printf("Couldn't find stream information.\n");
      return -1;
   }

   // 每个视频文件中有多个流(视频流、音频流、字幕流等,而且可有多个),循环遍历找到视频流
   // 判断方式:AVFormatContext->AVStream->AVCodecContext->codec_type是否为AVMEDIA_TYPE_VIDEO
   videoindex=-1;
   for(i=0; i<pFormatCtx->nb_streams; i++) 
      if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
         videoindex=i;
         break;
      }

   // 如果没有视频流,返回
   if(videoindex==-1){
      printf("Didn't find a video stream.\n");
      return -1;
   }

    // 保存视频流中的AVCodecContext
   pCodecCtx=pFormatCtx->streams[videoindex]->codec;

   // 用于查找FFmpeg的解码器(参考FFmpeg解码流程图)
   pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
   if(pCodec==NULL){
      printf("Codec not found.\n");
      return -1;
   }

   // (参考FFmpeg解码流程图)
   if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){
      printf("Could not open codec.\n");
      return -1;
   }

   // 创建一个AVFrame,用来存放解码后的一帧的数据
   pFrame=av_frame_alloc();
   pFrameYUV=av_frame_alloc();

    // av_image_get_buffer_size:返回使用给定参数存储图像所需的数据量的字节大小
   out_buffer=(unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P,  pCodecCtx->width, pCodecCtx->height,1));

   // 根据指定的图像参数和提供的数组设置数据指针和线条(data pointers and linesizes)
   av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize,out_buffer,
      AV_PIX_FMT_YUV420P,pCodecCtx->width, pCodecCtx->height,1);

   
   // 创建一个AVPacket,用来存放下面循环获取到的未解码帧
   packet=(AVPacket *)av_malloc(sizeof(AVPacket));

   //Output Info-----------------------------
   printf("--------------- File Information ----------------\n");
   av_dump_format(pFormatCtx,0,filepath,0);
   printf("-------------------------------------------------\n");

    // sws_getContext():初始化一个SwsContext
   img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, 
      pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); 

    // 循环读取帧数据
   while(av_read_frame(pFormatCtx, packet)>=0){
       // 取出视频流,
      if(packet->stream_index==videoindex){
          // 解码一帧视频数据:输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
         ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
         if(ret < 0){
            printf("Decode Error.\n");
            return -1;
         }

         if(got_picture){
             // sws_scale():处理图像数据,用于转换像素
            sws_scale(img_convert_ctx, (const unsigned char* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, 
               pFrameYUV->data, pFrameYUV->linesize);

                // 根据YUV数据格式,分离Y、U、V数据
                // 如果视频帧的宽和高分别为w和h,那么一帧YUV420P像素数据一共占用w*h*3/2 Byte的数据
                // 其中前w*h Byte存储Y,接着的w*h*1/4 Byte存储U,最后w*h*1/4 Byte存储V
            y_size=pCodecCtx->width*pCodecCtx->height;  
            fwrite(pFrameYUV->data[0],1,y_size,fp_yuv);    //Y 
            fwrite(pFrameYUV->data[1],1,y_size/4,fp_yuv);  //U
            fwrite(pFrameYUV->data[2],1,y_size/4,fp_yuv);  //V
            printf("Succeed to decode 1 frame!\n");

         }
      }
      av_free_packet(packet);
   }

   //flush decoder
   //FIX: Flush Frames remained in Codec
   while (1) {
       // 解码一帧视频数据:输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
      ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
      if (ret < 0)
         break;
      if (!got_picture)
         break;

      // // sws_scale():处理图像数据,用于转换像素
      sws_scale(img_convert_ctx, (const unsigned char* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, 
         pFrameYUV->data, pFrameYUV->linesize);

      int y_size=pCodecCtx->width*pCodecCtx->height;  
      fwrite(pFrameYUV->data[0],1,y_size,fp_yuv);    //Y 
      fwrite(pFrameYUV->data[1],1,y_size/4,fp_yuv);  //U
      fwrite(pFrameYUV->data[2],1,y_size/4,fp_yuv);  //V

      printf("Flush Decoder: Succeed to decode 1 frame!\n");
   }

    // sws_freeContext():释放一个SwsContext
   sws_freeContext(img_convert_ctx);

    // close and free
    fclose(fp_yuv);

   av_frame_free(&pFrameYUV);
   av_frame_free(&pFrame);
   avcodec_close(pCodecCtx);
   avformat_close_input(&pFormatCtx);

   return 0;
}



2.simplest_ffmpeg_decoder_pure
/**
 * 最简单的基于FFmpeg的视频解码器(纯净版)
 * Simplest FFmpeg Decoder Pure
 *
 * 雷霄骅 Lei Xiaohua
 * [email protected]
 * 中国传媒大学/数字电视技术
 * Communication University of China / Digital TV Technology
 * http://blog.csdn.net/leixiaohua1020
 *
 *
 * 本程序实现了视频码流(支持HEVC,H.264,MPEG2等)解码为YUV数据。
 * 它仅仅使用了libavcodec(而没有使用libavformat)。
 * 是最简单的FFmpeg视频解码方面的教程。
 * 通过学习本例子可以了解FFmpeg的解码流程。
 * This software is a simplest decoder based on FFmpeg.
 * It decode bitstreams to YUV pixel data.
 * It just use libavcodec (do not contains libavformat).
 * Suitable for beginner of FFmpeg.
 */

#include <stdio.h>

#define __STDC_CONSTANT_MACROS

#ifdef _WIN32
//Windows
extern "C"
{
#include "libavcodec/avcodec.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavcodec/avcodec.h>
#ifdef __cplusplus
};
#endif
#endif


//test different codec
#define TEST_H264  1
#define TEST_HEVC  0

int main(int argc, char* argv[])
{
    // 每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体
   AVCodec *pCodec;

    // 编码器上下文结构体,保存了视频(音频)编解码相关信息
    AVCodecContext *pCodecCtx= NULL;

    // 保存了当前帧的信息,包括offset、dts、pts、宽高等
   AVCodecParserContext *pCodecParserCtx=NULL;

    FILE *fp_in;
   FILE *fp_out;

   // 存储一帧解码后像素(采样)数据
    AVFrame    *pFrame;
   
   const int in_buffer_size=4096;

   // FF_INPUT_BUFFER_PADDING_SIZE:在输入比特流的末尾用于解码的额外分配字节的所需数量。
    // 这主要是因为一些优化的比特流读取器一次读取32位或64位并且可以读取结束。
    // 注意:如果附加字节的前23位不为0,则损坏的MPEG比特流可能导致过度读取和段错误。
   unsigned char in_buffer[in_buffer_size + FF_INPUT_BUFFER_PADDING_SIZE]={0};
   unsigned char *cur_ptr;
   int cur_size;

   // 存储一帧压缩编码数据
    AVPacket packet;

    // 视频是否解码成功的返回
   int ret, got_picture;

    // hevc h264 m2v -> yuv
#if TEST_HEVC
   enum AVCodecID codec_id=AV_CODEC_ID_HEVC;
   char filepath_in[]="bigbuckbunny_480x272.hevc";
#elif TEST_H264
   AVCodecID codec_id=AV_CODEC_ID_H264;
   char filepath_in[]="bigbuckbunny_480x272.h264";
#else
   AVCodecID codec_id=AV_CODEC_ID_MPEG2VIDEO;
   char filepath_in[]="bigbuckbunny_480x272.m2v";
#endif

   char filepath_out[]="bigbuckbunny_480x272.yuv";
   int first_time=1;


   //av_log_set_level(AV_LOG_DEBUG);

   // 注册复用器,编码器等(参考FFmpeg解码流程图)
   avcodec_register_all();

    // 用于查找FFmpeg的解码器(参考FFmpeg解码流程图)
    pCodec = avcodec_find_decoder(codec_id);
    if (!pCodec) {
        printf("Codec not found\n");
        return -1;
    }

    // 创建AVCodecContext结构体
    pCodecCtx = avcodec_alloc_context3(pCodec);
    if (!pCodecCtx){
        printf("Could not allocate video codec context\n");
        return -1;
    }

    // 初始化AVCodecParserContext。其参数是codec_id,所以同时只能解析一种
    // AVCodecParser用于解析输入的数据流并把它们分成一帧一帧的压缩编码数据。
    // 比较形象的说法就是把长长的一段连续的数据“切割”成一段段的数据。
    // 核心函数是av_parser_parse2()
   pCodecParserCtx=av_parser_init(codec_id);
   if (!pCodecParserCtx){
      printf("Could not allocate video parser context\n");
      return -1;
   }

    //if(pCodec->capabilities&CODEC_CAP_TRUNCATED)
    //    pCodecCtx->flags|= CODEC_FLAG_TRUNCATED; 

    // 使用给定的AVCodec初始化AVCodecContext;
    // 在使用这个函数之前需要使用avcodec_alloc_context3()分配的context
    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
        printf("Could not open codec\n");
        return -1;
    }
   //Input File
    fp_in = fopen(filepath_in, "rb");
    if (!fp_in) {
        printf("Could not open input stream\n");
        return -1;
    }
   //Output File
   fp_out = fopen(filepath_out, "wb");
   if (!fp_out) {
      printf("Could not open output YUV file\n");
      return -1;
   }

    pFrame = av_frame_alloc();
    // 把packet的参数设为默认值,要求packet的内存已经分配好了
   av_init_packet(&packet);

   while (1) {
        // 获取视频文件的总长度
        cur_size = fread(in_buffer, 1, in_buffer_size, fp_in);
        if (cur_size == 0)
            break;
        cur_ptr=in_buffer;

        while (cur_size>0){
            /**
              * 解析数据获得一个Packet, 从输入的数据流中分离出一帧一帧的压缩编码数据
              * Parse a packet.
              *
              * @param s             parser context.
              * @param avctx         codec context.
              * @param poutbuf       set to pointer to parsed buffer or NULL if not yet finished.
              * @param poutbuf_size  set to size of parsed buffer or zero if not yet finished.
              * @param buf           input buffer.
              * @param buf_size      input length, to signal EOF, this should be 0 (so that the last frame can be output).
              * @param pts           input presentation timestamp.
              * @param dts           input decoding timestamp.
              * @param pos           input byte position in stream.
              * @return the number of bytes of the input bitstream used.
              *
              * Example:
              * @code
              *   while(in_len){
              *       len = av_parser_parse2(myparser, AVCodecContext, &data, &size,
              *                                        in_data, in_len,
              *                                        pts, dts, pos);
              *       in_data += len;
              *       in_len  -= len;
              *
              *       if(size)
              *          decode_frame(data, size);
              *   }
              * @endcode
              *
              * 其中poutbuf指向解析后输出的压缩编码数据帧,buf指向输入的压缩编码数据。
              * 如果函数执行完后输出数据为空(poutbuf_size为0),则代表解析还没有完成,还需要再次调用av_parser_parse2()解析一部分数据才可以得到解析后的数据帧。
              * 当函数执行完后输出数据不为空的时候,代表解析完成,可以将poutbuf中的这帧数据取出来做后续处理。
              */
         int len = av_parser_parse2(
            pCodecParserCtx, pCodecCtx,
            &packet.data, &packet.size,
            cur_ptr , cur_size ,
            AV_NOPTS_VALUE, AV_NOPTS_VALUE, AV_NOPTS_VALUE);

         cur_ptr += len;
         cur_size -= len;

            // 如果函数执行完后输出数据为空(poutbuf_size为0),则代表解析还没有完成,还需要再次调用av_parser_parse2()解析一部分数据才可以得到解析后的数据帧。
         if(packet.size==0)
            continue;

         //Some Info from AVCodecParserContext
         printf("[Packet]Size:%6d\t",packet.size);
         switch(pCodecParserCtx->pict_type){
            case AV_PICTURE_TYPE_I: printf("Type:I\t");break;
            case AV_PICTURE_TYPE_P: printf("Type:P\t");break;
            case AV_PICTURE_TYPE_B: printf("Type:B\t");break;
            default: printf("Type:Other\t");break;
         }
         printf("Number:%4d\n",pCodecParserCtx->output_picture_number);

            // 解码一帧视频数据:输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
         ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, &packet);
         if (ret < 0) {
            printf("Decode Error.\n");
            return ret;
         }
         if (got_picture) {
            if(first_time){
               printf("\nCodec Full Name:%s\n",pCodecCtx->codec->long_name);
               printf("width:%d\nheight:%d\n\n",pCodecCtx->width,pCodecCtx->height);
               first_time=0;
            }
            //Y, U, V
            for(int i=0;i<pFrame->height;i++){
               fwrite(pFrame->data[0]+pFrame->linesize[0]*i,1,pFrame->width,fp_out);
            }
            for(int i=0;i<pFrame->height/2;i++){
               fwrite(pFrame->data[1]+pFrame->linesize[1]*i,1,pFrame->width/2,fp_out);
            }
            for(int i=0;i<pFrame->height/2;i++){
               fwrite(pFrame->data[2]+pFrame->linesize[2]*i,1,pFrame->width/2,fp_out);
            }

            printf("Succeed to decode 1 frame!\n");
         }
      }

    }

   //Flush Decoder
    packet.data = NULL;
    packet.size = 0;
   while(1){
       // 解码一帧视频数据:输入一个压缩编码的结构体AVPacket,输出一个解码后的结构体AVFrame
      ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, &packet);
      if (ret < 0) {
         printf("Decode Error.\n");
         return ret;
      }
      if (!got_picture){
         break;
      }else {
         //Y, U, V
         for(int i=0;i<pFrame->height;i++){
            fwrite(pFrame->data[0]+pFrame->linesize[0]*i,1,pFrame->width,fp_out);
         }
         for(int i=0;i<pFrame->height/2;i++){
            fwrite(pFrame->data[1]+pFrame->linesize[1]*i,1,pFrame->width/2,fp_out);
         }
         for(int i=0;i<pFrame->height/2;i++){
            fwrite(pFrame->data[2]+pFrame->linesize[2]*i,1,pFrame->width/2,fp_out);
         }

         printf("Flush Decoder: Succeed to decode 1 frame!\n");
      }
   }

    // close and free
    fclose(fp_in);
   fclose(fp_out);


   av_parser_close(pCodecParserCtx);

   av_frame_free(&pFrame);
   avcodec_close(pCodecCtx);
   av_free(pCodecCtx);

   return 0;
}


猜你喜欢

转载自blog.csdn.net/asd501823206/article/details/96189328