雷神simplest_ffmpeg_player解析(一)

寫在前面

學習雷神的博客,向雷神致敬~

看了雷神的小學期視頻課,在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;
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章