基於 WebRTC 實現自定義編碼解析度傳送

網易雲信發表於2021-01-22

2020年如果問什麼技術領域最火?毫無疑問:音視訊。2020年遠端辦公和線上教育的強勢發展,都離不開音視訊的身影,視訊會議、線上教學、娛樂直播等都是音視訊的典型應用場景。

更加豐富的使用場景更需要我們考慮如何提供更多的可配置能力項,比如解析度、幀率、位元速率等,以實現更好的使用者體驗。本文將主要從“解析度”展開具體分享。

如何實現自定義編碼解析度

我們先來看看“解析度”的定義。解析度:是度量影像內畫素資料量多少的一個引數,是衡量一幀影像或視訊質量的關鍵指標。解析度越高,影像體積(位元組數)越大,畫質越好。對於一個YUV i420 格式、解析度 1080p 的視訊流來說,一幀影像的體積為 1920x1080x1.5x8/1024/1024≈23.73Mbit,幀率 30,則 1s 的大小是 30x23.73≈711.9Mbit。可見資料量之大,對位元速率要求之高,所以在實際傳輸過程中就需要對視訊進行壓縮編碼。因此,視訊採集裝置採集出的原始資料解析度我們稱採集解析度,實際送進編碼器的資料解析度我們就稱之為編碼解析度

視訊畫面是否清晰、比例是否合適,這些都會直接影響使用者體驗。攝像頭採集解析度的選擇是有限的,有時我們想要的解析度並不能直接通過攝像頭採集到。那麼,根據場景配置合適編碼解析度的能力就至關重要了。如何將採集到的視訊轉換成我們想要的編碼解析度去傳送?這就是我們今天的主要分享的內容。

WebRTC 是 Google 開源的,功能強大的實時音視訊專案,市面上大多開發者都是基於 WebRTC 構建實時音視訊通訊的解決方案。在 WebRTC 中各個模組都有很好的抽象解耦處理, 對我們進行二次開發非常友好。在我們構建實時音視訊通訊解決方案時,需要去了解和學習 WebRTC 的設計思想及程式碼模組,並具備二次開發和擴充套件的能力。本文我們基於 WebRTC Release 72 版本,聊聊如何實現自定義編碼解析度。

首先,我們思考下面幾個問題:

  • 視訊資料從採集到編碼傳送,其 Pipeline 是怎樣的?
  • 怎麼根據設定的編碼解析度選擇合適的採集解析度?
  • 怎麼能得到想要的編碼解析度?

本文內容也將從以上三點展開具體分享。

視訊資料的 Pipeline

首先,我們來了解一下視訊資料的 Pipeline。視訊資料由 VideoCapturer 產生,VideoCapturer 採集資料後經過 VideoAdapter 處理,然後經由 VideoSource 的 VideoBroadcaster 分發給註冊的 VideoSink ,VideoSink 即編碼器 Encoder Sink 和本地預覽 Preview Sink。

對視訊解析度來說,流程是:將想要的解析度設定給 VideoCapturer,VideoCapturer 選擇合適的解析度去採集,原始的採集解析度資料再經過 VideoAdapter 計算,不符合預期後再進行縮放裁剪得到編碼解析度的視訊資料,將資料再送進編碼器編碼後傳送。

視訊資料的 Pipeline

這裡就有兩個關鍵性問題:

  • VideoCapturer 如何選擇合適的採集解析度?
  • VideoAdapter 如何將採集解析度轉換成編碼解析度?

如何選擇合適的採集解析度

採集解析度的選擇

WebRTC 中對視訊採集抽象出一個 Base 類:videocapturer.cc,我們把抽象稱為 VideoCapturer,在 VideoCapturer 中設定引數屬性,比如視訊解析度、幀率、支援的畫素格式等,VideoCapturer 將根據設定的引數,計算出最佳的採集格式,再用這個採集格式去呼叫各個平臺的 VDM(Video Device Module,視訊硬體裝置模組)。具體的設定如下:

程式碼摘自 WebRTC 中 src/media/base/videocapturer.h

VideoCapturer.h
bool GetBestCaptureFormat(const VideoFormat& desired, VideoFormat* best_format);//內部遍歷裝置支援的所有采集格式呼叫GetFormatDistance()計算出每個格式的distance,選出distance最小的那個格式
int64_t GetFormatDistance(const VideoFormat& desired, const VideoFormat& supported);//根據演算法計算出裝置支援的格式與我們想要的採集格式的差距,distance為0即剛好滿足我們的設定
void SetSupportedFormats(const std::vector<VideoFormat>& formats);//設定採集裝置支援的格式fps,resolution,NV12, I420,MJPEG等       

根據設定的引數,有時 GetBestCaptureFormat() 並不能得到比較符合我們設定的採集格式,因為不同的裝置採集能力不同,iOS、Android、PC、Mac 原生的攝像採集和外接 USB 攝像採集對解析度的支援是不同的,尤其外接 USB 攝像採集能力參差不齊。因此,我們需要對 GetFormatDistance() 稍作調整以滿足我們的需求,下面我們就來聊聊具體應該如何進行程式碼調整以滿足需求。

選擇策略原始碼分析

我們先分析一下 GetFormatDistance() 的原始碼,摘取部分程式碼:

程式碼摘自 WebRTC 中 src/media/base/videocapturer.cc

// Get the distance between the supported and desired formats.
int64_t VideoCapturer::GetFormatDistance(const VideoFormat& desired,
                                         const VideoFormat& supported) {
  //....省略部分程式碼
  // Check resolution and fps.
  int desired_width = desired.width;//編碼解析度寬
  int desired_height = desired.height;//編碼解析度高
  int64_t delta_w = supported.width - desired_width;//寬的差
  
  float supported_fps = VideoFormat::IntervalToFpsFloat(supported.interval);//採集裝置支援的幀率
  float delta_fps = supported_fps - VideoFormat::IntervalToFpsFloat(desired.interval);//幀率差
  int64_t aspect_h = desired_width
                         ? supported.width * desired_height / desired_width
                         : desired_height;//計算出設定的寬高比的高,採集裝置的解析度支援一般寬>高
  int64_t delta_h = supported.height - aspect_h;//高的差
  int64_t delta_fourcc;//設定的支援畫素格式優先順序,比如優先設定了NV12,同樣解析度和幀率的情況優先使用NV12格式採集
  
  //....省略部分降級策略程式碼,主要針對裝置支援的解析度和幀率不滿足設定後的降級策略
  
  int64_t distance = 0;
  distance |=
      (delta_w << 28) | (delta_h << 16) | (delta_fps << 8) | delta_fourcc;

  return distance;
}

Distance 組成

我們主要關注 Distance 這個引數。Distance 是 WebRTC 中的概念,它是設定的採集格式與裝置支援的採集格式按照一定演算法策略計算出的差值,差值越小代表裝置支援的採集格式與設定想要的格式越接近,為 0 即剛好匹配。

Distance 由四部分組成 delta_w,delta_h,delta_fps,delta_fourcc,其中 delta_w(解析度寬) 權重最重,delta_h(解析度高) 其次,delta_fps(幀率) 再次,delta_fourcc(畫素格式) 最後。這樣導致的問題是寬的比重太高, 高的比重太低,無法匹配到比較精確支援的解析度。

Example:

以 iPhone xs Max 800x800 fps:10 為例,我們摘取部分採集格式的 distance, 原生的 GetFormatDistance() 的演算法是不滿足需求的,想要的是 800x800,可以從下圖看出結果 Best 是960x540,不符合預期:

Supported NV12 192x144x10 distance 489635708928
Supported NV12 352x288x10 distance 360789835776
Supported NV12 480x360x10 distance 257721630720
Supported NV12 640x480x10 distance 128880476160
Supported NV12 960x540x10 distance 43032248320
Supported NV12 1024x768x10 distance 60179873792
Supported NV12 1280x720x10 distance 128959119360
Supported NV12 1440x1080x10 distance 171869470720
Supported NV12 1920x1080x10 distance 300812861440
Supported NV12 1920x1440x10 distance 300742082560
Supported NV12 3088x2316x10 distance 614332104704
Best NV12 960x540x10 distance 43032248320

選擇策略調整

為了獲取我們想要的解析度,按照我們分析,需要明確調整 GetFormatDisctance() 的演算法,將解析度的權重調整為最高,幀率其次,在沒有指定畫素格式的情況下,畫素格式最後,那麼修改情況如下:

int64_t VideoCapturer::GetFormatDistance(const VideoFormat& desired,
const VideoFormat& supported) {
 //....省略部分程式碼
  // Check resolution and fps.
int desired_width = desired.width; //編碼解析度寬
int desired_height = desired.height; //編碼解析度高
  int64_t delta_w = supported.width - desired_width;
  int64_t delta_h = supported.height - desired_height;
  int64_t delta_fps = supported.framerate() - desired.framerate();
  distance = std::abs(delta_w) + std::abs(delta_h);
  //....省略降級策略, 比如設定了1080p,但是攝像採集裝置最高支援720p,需要降級
  distance = (distance << 16 | std::abs(delta_fps) << 8 | delta_fourcc);
return distance;
}

修改後的 Distance 組成

修改後:Distance 由三部分組成解析度 (delta_w+delta_h),幀率 delta_fps,畫素 delta_fourcc,其中 (delta_w+delta_h) 比重最高,delta_fps 其次,delta_fourcc 最後。

Example:

還是以 iPhone xs Max 800x800 fps:10 為例,我們摘取部分採集格式的 Distance, GetFormatDistance() 修改後, 我們想要的是 800x800, 選擇的 Best 是1440x1080, 我們可以通過縮放裁剪得到 800x800, 符合預期(對解析度要求不是特別精確的情況下,可以調整降級策略,選擇1024x768):

Supported NV12 192x144x10 distance 828375040
Supported NV12 352x288x10 distance 629145600
Supported NV12 480x360x10 distance 498073600
Supported NV12 640x480x10 distance 314572800
Supported NV12 960x540x10 distance 275251200
Supported NV12 1024x768x10 distance 167772160
Supported NV12 1280x720x10 distance 367001600
Supported NV12 1440x1080x10 distance 60293120
Supported NV12 1920x1080x10 distance 91750400
Supported NV12 1920x1440x10 distance 115343360
Supported NV12 3088x2316x10 distance 249298944
Best NV12 1440x1080x10 distance 60293120

如何實現採集解析度到編碼解析度

視訊資料採集完成後,會經過 VideoAdapter (WebRTC中的抽象) 處理再分發到對應的 Sink (WebRTC中的抽象)。我們在 VideoAdapter 中稍作調整以計算出縮放裁剪所需的引數,再把視訊資料用 LibYUV 縮放再裁剪到編碼解析度(為了儘可能保留多的畫面影像資訊,先用縮放處理,寬高比不一致時再裁剪多餘的畫素資訊)。這裡我們重點分析兩個問題:

  • 還是選用上面的例子,我們想要的解析度為 800x800 ,但是我們得到的最佳採集解析度為 1440x1080,那麼,如何從 1440x1080 採集解析度得到設定的編碼解析度 800x800 呢?
  • 在視訊資料從 VideoCapture 流到 VideoSink 的過程中會經過 VideoAdapter 的處理,VideoAdapter 具體做了哪些事呢?

下面我們就這兩個問題展開具體的分析,我們先了解一下 VideoAdapter 是什麼。

VideoAdapter 介紹

WebRTC 中對 VideoAdapter 是這樣描述的:

VideoAdapter adapts an input video frame to an output frame based on the specified input and output formats. The adaptation includes dropping frames to reduce frame rate and scaling frames.VideoAdapter is
thread safe.

我們可以理解為:VideoAdapter 是資料輸入輸出控制的模組,可以對幀率、解析度做對應的幀率控制和解析度降級。在 VQC(Video Quality Control)視訊質量控制模組裡,通過對 VideoAdapter 的配置,可以做到在低頻寬、高 CPU 情況下對幀率進行動態降幀,對解析度進行動態縮放,以保證視訊的流暢性,從而提高使用者體驗。

摘自 src/media/base/videoadapter.h

VideoAdapter.h
bool AdaptFrameResolution(int in_width,
int in_height,
                            int64_t in_timestamp_ns,
int* cropped_width,
int* cropped_height,
int* out_width,
int* out_height);
void OnOutputFormatRequest(
const absl::optional<std::pair<int, int>>& target_aspect_ratio,
const absl::optional<int>& max_pixel_count,
const absl::optional<int>& max_fps);
void OnOutputFormatRequest(const absl::optional<VideoFormat>& format);

VideoAdapter 原始碼分析

VideoAdapter 中根據設定的 desried_format,呼叫 AdaptFrameResolution(),可以計算出採集解析度到編碼解析度應該縮放和裁剪的 cropped_width, cropped_height, out_width, out_height 引數, WebRTC 原生的 adaptFrameResolution 是根據計算畫素面積計算縮放引數,而不能得到精確的寬&高:

摘自src/media/base/videoadapter.cc

bool VideoAdapter::AdaptFrameResolution(int in_width,
int in_height,
                                        int64_t in_timestamp_ns,
int* cropped_width,
int* cropped_height,
int* out_width,
int* out_height) {
//.....省略部分程式碼
// Calculate how the input should be cropped.
if (!target_aspect_ratio || target_aspect_ratio->first <= 0 ||
        target_aspect_ratio->second <= 0) {
      *cropped_width = in_width;
      *cropped_height = in_height;
    } else {
const float requested_aspect =
          target_aspect_ratio->first /
static_cast<float>(target_aspect_ratio->second);
      *cropped_width =
          std::min(in_width, static_cast<int>(in_height * requested_aspect));
      *cropped_height =
          std::min(in_height, static_cast<int>(in_width / requested_aspect));
    }
const Fraction scale;//vqc 縮放係數 ....省略程式碼
    // Calculate final output size.
    *out_width = *cropped_width / scale.denominator * scale.numerator;
    *out_height = *cropped_height / scale.denominator * scale.numerator;
 }

Example:

以 iPhone xs Max 800x800 fps:10 為例,設定編碼解析度為 800x800,採集解析度是 1440x1080,根據原生的演算法,計算得到的新的解析度為 720x720, 不符合預期。

VideoAdapter 調整

VideoAdapter 是 VQC(視訊質量控制模組)中對視訊質量做調整的重要部分,VQC 之所以可以完成幀率控制、解析度縮放等操作,主要依賴於 VideoAdapter,因此修改需要考慮對 VQC 的影響。

為了能精確獲得想要的解析度,且不影響 VQC 模組對解析度的控制,我們對 AdaptFrameResolution() 做以下調整:

bool VideoAdapter::AdaptFrameResolution(int in_width,
int in_height,
                                        int64_t in_timestamp_ns,
int* cropped_width,
int* cropped_height,
int* out_width,
int* out_height) {
  //....省略部分程式碼
bool in_more =
        (static_cast<float>(in_width) / static_cast<float>(in_height)) >=
        (static_cast<float>(desired_width_) /
static_cast<float>(desired_height_));
if (in_more) {
        *cropped_height = in_height;
        *cropped_width = *cropped_height * desired_width_ / desired_height_;
    } else {
      *cropped_width = in_width;
      *cropped_height = *cropped_width * desired_height_ / desired_width_;
    }
    *out_width = desired_width_;
    *out_height = desired_height_;
    //....省略部分程式碼
return true;
}

Example:

同樣以 iPhone xs Max 800x800 fps:10 為例,設定編碼解析度為 800x800,採集解析度是 1440x1080,根據調整後的演算法,計算得到的編碼解析度為 800x800, 符合預期。

總結

本文主要介紹了基於 WebRTC 如何實現編碼解析度的配置。當我們要對視訊編碼解析度進行修改時,就需要去了解視訊資料的採集、傳遞、處理、編碼等整個流程,這裡也再對今天分享幾個關鍵步驟進行歸納,當我們要實現自定義編碼解析度傳送時:

  • 首先,設定想要的編碼解析度;
  • 修改 VideoCapturer.cc,根據編碼解析度選擇合適的採集解析度;
  • 修改 VideoAdapter.cc,計算出採集解析度縮放和裁剪到編碼解析度所需的引數;
  • 根據縮放和裁剪引數使用 libyuv 將原始資料縮放裁剪到編碼解析度;
  • 再將新資料送進編碼器編碼併傳送;
  • 最後,Done。

同理我們也可以依據這種思路去做一些其他的調整。以上就是本文的全部介紹,我們也會持續分享更多音視訊相關的技術實現,也歡迎留言與我們交流相關的技術。

5G 時代已經來臨,音視訊的應用領域會越來越寬泛,一切大有可為。

作者介紹

何敬敬,網易雲信客戶端音視訊工程師,負責雲信音視訊跨平臺 SDK 的研發工作。之前從事線上教育領域的音視訊工作,對構建實時音視訊解決方案和使用場景有一定的理解,喜歡鑽研和解決複雜技術問題。

相關文章