天天看點

FFMPEG開發快速入坑——視訊轉換處理

作者:音視訊流媒體技術

本章節重點講解FFMPEG中對于視訊圖像格式轉換的處理。

一、視訊格式轉換的基本API

視訊幀圖像的格式轉換、縮放等處理,主要使用 libswscale庫中的API函數完成的

  1. sws_getContext() 根據要輸入輸出圖像的 寬高和 像素格式 建立轉換器
  2. sws_scale() 根據輸入圖像資料進行實際的轉換操作,結果輸出到輸出緩沖區上
  3. sws_freeContext()釋放轉換器

這幾個API函數的功能比較明确,關鍵是參數的設定,特别是sws_scale()幾個參數的設定,具體的參數值依賴于相應的視訊幀圖像格式。

二、常用的視訊幀圖像格式

  1. YUV420P格式

在YUV420P的存儲格式中,每4個像素點占用4個Y、1個U、1個V,Y分量、U分量、V分量的資料分别單獨存放,對應AVFrame結構體中字段

typedef struct AVFrame {
  ......
  //
  // 視訊幀圖像資料 或者 音頻幀PCM資料, 根據不同的格式有不同的存放方式
  // 對于視訊幀:RGB/RGBA 格式時 data[0] 中一次存放每個像素的RGB/RGBA資料
  //            YUV420 格式時 data[0]存放Y資料;  data[1]存放U資料; data[2]存放V資料
  // 對于音頻幀: data[0]存放左聲道資料;  data[1]存放右聲道資料
  //
  uint8_t *data[AV_NUM_DATA_POINTERS];  
  
  //
  // 行位元組跨度, 相當于stride
  // 對于視訊幀: 上下兩行同一列像素相差的位元組數,例如:對于RGBA通常是(width*4), 但是有時FFMPEG内部會有擴充, 可能會比這個值大
  // 對于音頻幀: 單個通道中所有采樣占用的位元組數
  //
  int linesize[AV_NUM_DATA_POINTERS];
  
  int format;         // 對于視訊幀是圖像格式; 對于音頻幀是采樣格式  
  int64_t pts;        // 目前資料幀的時間戳  
  int width, height;  // 僅用于視訊幀, 寬度高度
  int key_frame;      // 僅用于視訊, 目前是否是I幀
  ......
}
           
FFMPEG開發快速入坑——視訊轉換處理

以一張 720*1280的視訊幀圖像為例,AVFrame中的各個字段值:

uint8_t* yuvData[4]  = {nullptr};
  int      linesize[4] = {0};
  av_image_alloc(yuvData, linesize, 720, 1280, AV_PIX_FMT_YUV420P, 1); // 配置設定記憶體空間
  // 将 Y & U & V 資料分别填充到 yuvData[0] yuvData[1] yuvData[2] 中

  AVFrame* pFrame = av_frame_alloc();
  pFrame->format  = AV_PIX_FMT_YUV420P;
  pFrame->width   = 720;
  pFrame->height  = 1280;
  pFrame->data[0] = yuvData[0];
  pFrame->data[1] = yuvData[1];
  pFrame->data[2] = yuvData[2];
  pFrame->linesize[0] = linesize[0];  // Y資料行長位元組數, 720
  pFrame->linesize[1] = linesize[1];  // U資料行長位元組數, 360
  pFrame->linesize[2] = linesize[1];  // V資料行長位元組數, 360
 ......
           

相關學習資料推薦,點選下方連結免費報名,先碼住不迷路~】

【免費分享】音視訊學習資料包、大廠面試題、技術視訊和學習路線圖,資料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以點選加群免費領取~

FFMPEG開發快速入坑——視訊轉換處理

這裡的linesize[]的值需要特别注意一下,這個地方可能會讓很多開發者疑惑:

既然linesize[]表示一個通道中一行像素資料的位元組數,那麼直接根據通道類型計算就可以了(例如:對于Y通道來說,圖像寬度720,那麼一行像素的Y資料就固定是720個位元組),為什麼要單獨使用一個字段表示? 。

具體原因:這裡會涉及到一個記憶體管理的處理,如果AVFrame的緩沖區是開發者自己建立的,上面的lineSize[]的确可以自己計算。 但是在FFMPEG内部編解碼器處理時需要處理圖像邊界等問題,很多時候為了友善優化處理,通常内部建立的圖像緩沖區要比輸出的原始圖像要大一些,而輸出的圖像内容隻是出于緩沖區中的一部分,此時要輸出圖像要麼建立一個新的緩沖區做一次圖像拷貝,要麼直接将這個緩沖區輸出,避免一次拷貝,FFMPEG就是采用後面一種方式少了一次拷貝。

例如:.mp4檔案中視訊幀原始圖像大小是 720*1280,解碼器内部實際為了解碼優化,實際建立的圖像緩沖區可能是768*1328大小,在調用 avcodec_receive_frame()擷取到的解碼後的視訊幀資料 pFrame中 data[0] 位址是通過pCacheBuffer直接位址偏移得到,同樣 linesize[0] = 768; linsize[1] = 384; linesize[2] = 384; 這樣通過data[0] 找到下一行第一個像素的Y資料就是 (data[0]+linsize[0]) 而不是直接 (data[0]+720)了

FFMPEG開發快速入坑——視訊轉換處理

2. RGBA格式

RGBA圖像格式在大部分作業系統的GUI Framework和控件中比較常用,在RGBA的存儲格式中,每個像素點分别使用R、G、B、A四個通道交錯存放。在AVFrame結構體中僅需要用到data[0]字段來指定資料存儲區域,linesize[0]通常為 (width*4)

FFMPEG開發快速入坑——視訊轉換處理

RGBA圖像格式的記憶體布局

3. 其他視訊圖像格式

在FFMPEG中視訊幀格式主要分為 YUV 和 RGB/RGBA 兩大系列類型,在這兩大類型中又根據不同存儲和記憶體布局有更多的細分,詳細可以參考FFMPEG相關的說明文檔。

在這兩種類型中主要注意的是:

如果格式後面帶'P'的表示是 planar存儲模式,通常不同的通道資料分别存儲在data[0], data[1], data[2], ..... 中

其他的格式是packed存儲模式,即:各個通道資料是交錯存儲在一起,此時資料通路隻能通過data[0]來進行偏移計算, data[1], data[2],..... 等都是空的

三、轉換示例代碼

直接使用 libswscale庫進行轉換

//
// 視訊幀格式轉換
//
int32_t VideoConvert(
  const AVFrame*  pInFrame,           // 輸入視訊幀  
  AVPixelFormat   eOutFormat,         // 輸出視訊格式
  int32_t         nOutWidth,          // 輸出視訊寬度
  int32_t         nOutHeight,         // 輸出視訊高度
  AVFrame**       ppOutFrame    )     // 輸出視訊幀
{
  struct SwsContext* pSwsCtx = nullptr;
  AVFrame*           pOutFrame = nullptr;

  // 建立格式轉換器, 指定縮放算法,轉換過程中不增加任何濾鏡特效處理
  pSwsCtx = sws_getContext(pInFrame->width, pInFrame->height, (AVPixelFormat)pInFrame->format,
                            nOutWidth, nOutHeight, eOutFormat,
                            SWS_BICUBIC,  nullptr,  nullptr, nullptr);
  if (pSwsCtx == nullptr)
  {
    LOGE("<VideoConvert> [ERROR] fail to sws_getContext()\n");
    return -1;
  }


  // 建立輸出視訊幀對象以及配置設定相應的緩沖區
  uint8_t* data[4]     = {nullptr};
  int      linesize[4] = {0};
  int res = av_image_alloc(data, linesize, nOutWidth, nOutHeight, eOutFormat, 1);
  if (res < 0)
  {
    LOGE("<VideoConvert> [ERROR] fail to av_image_alloc(), res=%d\n", res);
    sws_freeContext(pSwsCtx);
    return -2;
  }
  pOutFrame                = av_frame_alloc();
  pOutFrame->format        = eOutFormat;
  pOutFrame->width         = nOutWidth;
  pOutFrame->height        = nOutHeight;
  pOutFrame->data[0]       = data[0];
  pOutFrame->data[1]       = data[1];
  pOutFrame->data[2]       = data[2];
  pOutFrame->data[3]       = data[3];
  pOutFrame->linesize[0]   = linesize[0];
  pOutFrame->linesize[1]   = linesize[1];
  pOutFrame->linesize[2]   = linesize[2];
  pOutFrame->linesize[3]   = linesize[3];

  // 進行格式轉換處理
  res = sws_scale(pSwsCtx,
                  static_cast<const uint8_t* const*>(pInFrame->data),
                  pInFrame->linesize,
                  0,
                  pOutFrame->height,
                  pOutFrame->data,
                  pOutFrame->linesize);
  if (res < 0)
  {
    LOGE("<VideoConvert> [ERROR] fail to sws_scale(), res=%d\n", res);
    sws_freeContext(pSwsCtx);
    av_frame_free(&pOutFrame);
    return -3;
  }

  (*ppOutFrame) = pOutFrame;
  sws_freeContext(pSwsCtx);  // 釋放轉換器
  return 0;
}
           

四、其他格式轉換庫

這裡 libswscale 是FFMPEG本身自帶的圖像格式轉換和縮放庫,但實際項目中這個庫的性能不太高,如果要考慮高性能的圖像格式轉換,可以考慮使用 libyuv 這個開源庫。

文章系列目錄

華叔-視覺魔術師:FFMPEG開發快速入坑——緒論62 贊同 · 11 評論文章

原文 https://zhuanlan.zhihu.com/p/345687433

繼續閱讀