FFmpeg庫視頻解碼初探(軟硬件解碼)

ffmpeg

最近有工做需求用到ffmpeg,分享下。包括一些編碼的基礎知識,ffmpeg視頻解碼基礎,還有GPU解碼的部分。
屬於科普工做,並不深刻,記錄了踩過的一些坑,但願有用
飲水思源:雷霄驊(雷神) &
代碼部分參考自 同事***(打碼)代碼,謝謝大神!shell

FFmpeg是一種功能強大的經常使用的視頻/音頻處理開源框架。支持幾乎全部主流格式音視頻的編解碼,並能進行拼接等操做。緩存

基礎知識

  • 視頻格式:mp4, avi, mkv等,稱之爲封裝格式,能夠當作是一種容器。
  • 視頻流編碼格式:h264, h265等,能夠認爲是一種壓縮手段,減少文件體積。
  • 音頻流編碼格式:MP3, AAC等,音頻壓縮方式。
  • 視頻像素數據:RGB、YUV(YUV420),實際上的圖像編碼格式,包括存儲亮度和色彩數據。
  • 封裝格式和編碼格式的關係:封裝格式能夠理解爲存放編碼後音視頻信息的一種容器,不通的容器對支持的編碼格式有所不一樣。
    flv 編碼格式,侵刪
    侵刪網絡

  • 總體解碼流程:

    侵刪框架

  • h264 h264參考博客:

    侵刪ide

    • 主要由NALU結構組成
    • I幀:幀內編碼,適度壓縮,相似jpg,大約6:1;P幀:前向預測幀,大約20:1;B幀:雙向預測內插編碼幀,大約50:1.
  • YUV420: Y(亮度),U(色度),V(濃度),Y決定灰度,UV共同決定顏色
    • 因爲人類對與色彩的感知能力有限,因此一般會選擇下降顏色信息密度,即對UV份量進行壓縮

      侵刪函數

    • 基於YUV份量在存儲方式上的不一樣,又衍生出YUV420SP,YUV420P等格式工具



侵刪ui

ffmpeg基本組成

  • 術語,這些概念跟上面講的步驟實際上是能找到對用關係的:
    • ⾳音/視頻流(stream) 一路路⾳音/視頻稱爲⼀一路路流。ffmpeg⽀支持5種流:⾳音頻(a),視頻(v),字幕(s),數據(d)以及附件(t)。
    • 容器 對應上文提到的MP4,flv等,包括音頻視頻各個流。
    • 編解碼器 用於編解碼各個流
    • 解/複用 在容器中分離出流
    • 過濾器 對音/視頻進行特殊處理,好比加水印等
  • 使用:FFmpeg能夠經過它的工具,以命令行的形式使用,也支持利用接口進行編碼調用。工具調用方式,不贅述,主要分享一下lib庫的使用經歷。

ffmpeg庫的使用

  • 關鍵結構體:
    • AVIOContext(URLContext): IO類型,主要存儲協議類型及狀態
    • AVFormatContext:主要存儲視頻音頻中包含的封裝信息
    • AVInputFormat:存儲對應音視頻使用的封裝格式
    • AVStream:存儲一個視頻(或音頻)流相關的數據
    • AVCodecContext: 每一個AVStream對應一個AVCodeContext,存儲對應流的解碼信息
    • AVCodec:每一個AVCodeContext對應一個AVCodec,包含對應的解碼器
    • AVPacket:編碼後的數據
    • AVFrame:解碼後的數據

      侵刪
  • 關鍵函數:
    • av_register_all(新版本廢棄):註冊可用的編解碼器,編解複用器等等
    • avformat_alloc_context:分配一個AVFormatContext結構體
    • avio_alloc_context:I/O上下文,能夠用來定製IO操做
    • av_open_input_file:以輸入方式打開一個源文件,可使用文件名做爲參數,也可使用定製I/O
    • av_find_stream_info:獲取文件流信息
    • avcodec_find_decoder(ID)/avcodec_find_decoder_by_name:經過ID或者name查找解碼器
    • avcodec_open:使用一個給定的codec,初始化AVCodecContext用於解碼操做
    • av_read_frame:從源文件容器中讀取一個packet數據包,並非每一次讀取都是有效的,當返回操做碼>=0時,循環調用該函數進行讀取,讀出來的包須要進行解碼操做
    • avcodec_decode_video2(新版本中不推薦使用這個函數):解碼,返回frame
    • av_send_packet:向解碼器發送一個packet數據,並解碼
    • av_receive_frame:獲取解碼後的數據,receive並非每次均可以成功的。

      侵刪
  • 定製I/O,ffmpeg直接在內存中讀取視頻文件
    • 緣由:對於網絡傳輸過來的短視頻base64文件,但願可以不通過磁盤IO,直接從內存讀取。
      1. 定製IO回調,自定義緩存,IO數據源
      1. 經過av_probe_input_buffer函數探測當前視頻格式信息
      1. 將探測獲得的fmt信息註冊到AVFormatContext中,並打開源文件
      1. 後續按常規方式使用ffmpeg接口便可
  • 經過定製IO得到文件基本信息並肯定視頻流,解碼器信息的代碼示例
struct  buffer_data {
    uint8_t *ptr_;
    size_t size_;
  };
typedef buffer_data BufferData;
    int VideoParseFFmpeg::read_packet(void *opaque, uint8_t *buf, int buf_size) {
    //opaque用戶自定義指針
    struct buffer_data *bd = (struct buffer_data *) opaque;
    buf_size = FFMIN(buf_size, bd->size_);

    if (!buf_size)
      return AVERROR_EOF;

    memcpy(buf, bd->ptr_, buf_size);
    bd->ptr_ += buf_size;
    bd->size_ -= buf_size;

    return buf_size;
}
int LoadContent(const std::string &video_content){
  int ret = 0;
  //分配緩存空間
  video_size_ = video_content.size();
  avio_ctx_buffer_size_ = video_size_+AV_INPUT_BUFFER_PADDING_SIZE;
  avio_ctx_buffer_ = (uint8_t *)av_malloc(avio_ctx_buffer_size_);
  
  //bd爲自定義結構,指向內存中的視頻文件
  bd_.ptr_ = (uint8_t *)video_content.c_str();
  bd_.size_ = video_content.size();

  input_ctx_ = avformat_alloc_context();
  //自定義io
  avio_ctx_ = avio_alloc_context(avio_ctx_buffer_,
                                     avio_ctx_buffer_size_,
                                     0,
                                     &bd_,
                                     &read_packet, //自定義讀取回調
                                     NULL,
                                     NULL);
  AVInputFormat *in_fmt{NULL};
//視頻格式探測
  if((ret = av_probe_input_buffer(avio_ctx_, &in_fmt_, "", NULL, 0, 0)) < 0) {
        LOGGER_WARN(Log::GetLog(), "fail to prob input, err [{}]", AVERROR(ret));
    return -1;
  }
  //註冊iocontext
  input_ctx_->pb = avio_ctx_;

  /* open the input file */
  if ((ret = avformat_open_input(&input_ctx_, "", in_fmt_, NULL)) != 0) {
        LOGGER_WARN(Log::GetLog(), "fail to open input, err [{}]", AVERROR(ret));
    return -1;
  }
//  if ((ret = avformat_open_input(&input_ctx_, "./smoke.mp4", NULL, NULL)) != 0) {
//    LOGGER_WARN(Log::GetLog(), "fail to open input, err [{}]", AVERROR(ret));
//    return -1;
//  }
//獲取流信息
  if ((ret = avformat_find_stream_info(input_ctx_, NULL)) < 0) {
        LOGGER_WARN(Log::GetLog(), "fail to find input stream information, err[{}]", AVERROR(ret));
    return -1;
  }

  /* find the video stream information */
  //找到視頻流,獲取其對應的decoder
  if ((ret = av_find_best_stream(input_ctx_, AVMEDIA_TYPE_VIDEO, -1, -1, &decoder_, 0)) < 0) {
        LOGGER_WARN(Log::GetLog(), "fail to find a video stream from input, err[{}]", ret);
    return -1;
  }
  video_stream_idx_ = ret;
//獲取decoder_context,把decoder註冊進去
  if (!(decoder_ctx_ = avcodec_alloc_context3(decoder_))) {
        LOGGER_WARN(Log::GetLog(), "fail to alloc avcodec context");
    return -1;
  }
  video_stream_ = input_ctx_->streams[video_stream_idx_];
  
  //新版本再也不將音視頻流信息直接保存到streams[video_stream_idx_]中,而是存放在AVCodecParammeters中(涉及format,width,height,codec_type等),該函數提供了轉換
  if ((ret = avcodec_parameters_to_context(decoder_ctx_, video_stream_->codecpar)) < 0){
        LOGGER_WARN(Log::GetLog(), "fail to convert parameters to context, err [{}]", ret);
    return -1;
  }
    //獲取幀率等基本信息
  if(video_stream_->avg_frame_rate.den != 0) {
    fps_ = video_stream_->avg_frame_rate.num / video_stream_->avg_frame_rate.den;
  }
  video_length_sec_ = input_ctx_->duration/AV_TIME_BASE;
//YUV420p等
  pix_fmt_ = (AVPixelFormat)video_stream_->codecpar->format;
//硬解碼部分
  if (hw_enable_ && is_hw_support_fmt(pix_fmt_)) {
      for (int i = 0;; i++)
      {
        const AVCodecHWConfig *config = avcodec_get_hw_config(decoder_, i);
        if (!config) {
            LOGGER_WARN(Log::GetLog(), "decoder [{}] does not support device type [{}]", decoder_->name, av_hwdevice_get_type_name(hw_type_));
          return -1;
        }
        if (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX &&
          config->device_type == hw_type_) {
          hw_pix_fmt_ = config->pix_fmt;
          break;
        }
      }
    decoder_ctx_->pix_fmt = hw_pix_fmt_;

      if ((ret = hw_decoder_init(decoder_ctx_, hw_type_)) < 0) {
          LOGGER_WARN(Log::GetLog(), "fail to init hw decoder, err [{}]", ret);
      return -1;
      }
    }

  if ((ret = avcodec_open2(decoder_ctx_, decoder_, NULL)) < 0) {
        LOGGER_WARN(Log::GetLog(), "fail to open decodec, err[{}]", ret);
    return -1;
  }
  }
  • 踩坑記錄:
    • 因爲某些mp4文件的moov文件被放置在文件尾部,須要設置較大的緩存空間纔可以順利解析該文件,不然會在 av_find_best_stream時報找不到流信息。
    • 自定義緩存buffer使用完畢後必須主動收回,不然會形成內存泄漏;該buffer在使用過程當中,ffmpeg可能根據須要主動從新分配,致使buffer位置大小改變,此時該內存依舊須要外部手動釋放,固然的不能使用源buffer指針。
  • 視頻解碼
while (true) {
      if ((av_read_frame(input_ctx_, &packet_)) < 0){
        break;
      }

      if (video_stream_idx_ == packet_.stream_index) {
        //std::shared_ptr<cv::Mat> p_frame = nullptr;
        decode_write(decoder_ctx_, &packet_, &buffer, frames);
        //frames.push_back(p_frame);
      }
    }

    /* flush the decoder */
    packet_.data = NULL;
    packet_.size = 0;
    //std::shared_ptr<cv::Mat> p_frame = nullptr;
    //cv::Mat *p_frame = NULL;
    decode_write(decoder_ctx_, &packet_, &buffer, frames);
    
    
    ====================================================
   
    //code block in decode_write
    ret = avcodec_send_packet(avctx, packet);
    if (ret < 0) {
        LOGGER_WARN(Log::GetLog(), "error during decodeing, err[{}]", AVERROR(ret));
    return ret;
  }

  while (true)
  {
    auto clear = [&frame, &sw_frame, this]{
      if (frame != NULL)
        av_frame_free(&frame);
      if (sw_frame != NULL)
        av_frame_free(&sw_frame);
      av_packet_unref(&packet_);
    };
    if (!(frame = av_frame_alloc()) || !(sw_frame = av_frame_alloc()))
    {
          LOGGER_WARN(Log::GetLog(), "cant alloc frame, err[{}]", AVERROR(ENOMEM));
      clear();
      return 0;
    }

    ret = avcodec_receive_frame(avctx, frame);
    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
      clear();
      return 0;
    }
    else if (ret < 0) {
          LOGGER_WARN(Log::GetLog(), "error while decoding, err[{}]", AVERROR(ret));
      clear();
      return ret;
    }
    ...
}
  • 視頻解碼的坑:
    • read_frame/send_packet/receive_frame幾個函數都有可能出現暫時的不成功(ret>0),多是由於數據還沒喲準備好,此時不能判斷爲錯誤,須要繼續嘗試。
    • send_packet和receive_frame並非一一對應的,大多數狀況下解碼較慢。因此可能當全部packet都已經發送,可是還有不少解碼完的數據並無經過receive_frame收到,此時須要經過一次flush連續將緩存中解碼完的frame都取出來。
  • 硬件解碼:
    • ffmpeg hw accelerate官網介紹
    • 許多平臺支持對部分視頻處理的工做提供硬件加速能力,包括編碼、解碼、過濾等操做。一般咱們會使用到一些API來進行編解碼,這些API對不一樣硬件的支持各不相同,而ffmpeg對這些API的支持程度也有所不一樣。
    • 一般咱們使用NVENC/NVDEC(原名NVDIA)API,在NIVIDIA設備上進行編解碼。
    • 默認的ffmpeg並無開啓硬件解碼的選項,須要咱們從新編譯ffmpeg庫開啓。
    ./configure --prefix=./ --bindir=bin/ffmpeg --incdir=include/ffmpeg --libdir=lib64/ffmpeg --disable-x86asm --arch=x86_64 --optflags='-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic' --extra-ldflags='-Wl,-z,relro' --enable-libx264 --enable-libx265 --enable-avfilter --enable-pthreads --enable-shared --enable-gpl --disable-debug --enable-cuda --enable-cuvid --enable-nvenc --enable-nonfree --enable-libnpp --extra-cflags=-I/usr/local/cuda-8.0/include --extra-ldflags=-L/usr/local/cuda-8.0/lib64
    • 編譯時要預先安裝cuda庫,而後使用--extra-cflags=-I/usr/local/cuda-8.0/include --extra-ldflags=-L/usr/local/cuda-8.0/lib64選項指定cuda庫的版本,cuda8,cuda10在要注意區分
    • 整個編譯過程當中可能會有各類庫缺失的問題,查文檔安裝便可。
    • ffmpeg編譯選項
    • 附一個編譯錯誤解決
      • ERROR: cuda requested, but not all dependencies are satisfied: ffnvcodec
        參考解決
  • 硬件解碼代碼塊
//配置解碼器
if (hw_enable_ && is_hw_support_fmt(pix_fmt_)) {
    for (int i = 0;; i++)
    {
    //獲取支持該decoder的hw 配置型
      const AVCodecHWConfig *config = avcodec_get_hw_config(decoder_, i);
      if (!config) {
            LOGGER_WARN(Log::GetLog(), "decoder [{}] does not support device type [{}]", decoder_->name, av_hwdevice_get_type_name(hw_type_));
        return -1;
      }
      
      //AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX使用hw_device_ctx API
      //hw_type_支持的硬件類型(cuda)
      if (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX &&
          config->device_type == hw_type_) {
        hw_pix_fmt_ = config->pix_fmt;
        break;
      }
    }
    //decoder_ctx_->get_format = &get_hw_format;
    decoder_ctx_->pix_fmt = hw_pix_fmt_;

    if ((ret = hw_decoder_init(decoder_ctx_, hw_type_)) < 0) {
          LOGGER_WARN(Log::GetLog(), "fail to init hw decoder, err [{}]", ret);
      return -1;
    }
  }
  ret = avcodec_open2(decoder_ctx_, decoder_, NULL))
  ...
  
  int VideoParseFFmpeg::hw_decoder_init(AVCodecContext *ctx, const enum AVHWDeviceType type) 
  {
  int err = 0;
  if ((err = av_hwdevice_ctx_create(&hw_device_ctx_, type,NULL, NULL, 0)) < 0)
  {
        LOGGER_WARN(Log::GetLog(), "fail to create specified HW device, err[{}]", AVERROR(err));
    char buf[1024] = { 0 };
    av_strerror(err, buf, 1024);
    return err;
  }
  //註冊硬解碼上下文
  ctx->hw_device_ctx = av_buffer_ref(hw_device_ctx_);

  return err;
}

//解碼
//receive_frame之後
if (frame->format == hw_pix_fmt_ &&
    hw_enable_ &&
    is_hw_support_fmt(pix_fmt_)) {
  /* retrieve data from GPU to CPU */
  if ((ret = av_hwframe_transfer_data(sw_frame, frame, 0)) < 0) {
    LOGGER_WARN(Log::GetLog(), "error transferring the data to system memory, err[{}]", ret);
    clear();
    return ret;
  }
  tmp_frame = sw_frame;
} else {
  tmp_frame = frame;
}
p_mat_out.push_back(avFrame2Mat(tmp_frame,
                                avctx,
                                (AVPixelFormat) tmp_frame->format));
clear();
  • 硬解碼踩坑:
    • CUDA只支持YUV420和YUV444格式圖片的解碼,不支持YUV422 。此時程序會直接在avcodec_send_packet函數core出,cuda庫顯示錯誤。應該有相關接口能夠直接判斷,可是我還沒找到。
    • 對於不支持的格式,依舊須要使用軟件解碼。也能夠提早轉成420再解碼,對信息損失敏感的話,仍是用軟解碼好了。
    • 在將不一樣格式轉換到RGB時須要使用到 ffmpeg的sws_scale格式轉換接口,注意部分格式命名新版ffmpeg已經不支持,須要在進一步轉換,參考FFmpeg deprecated pixel format used,接口使用不復雜,再也不贅述
  • 格式轉換接口的坑:
    在ffmpeg 4.1.4庫使用過程當中發現,舊版本中
    avpicture_get_size
    avpicture_fill
    兩個函數已經被廢棄,網上常見教程依然使用這兩個函數,新版本使用這兩個函數轉換圖片會失真
    應該使用如下函數替代之:
    av_image_get_buffer_size
    av_image_fill_arrays

---(end)---this

相關文章
相關標籤/搜索