基於 WebRTC 實現自定義編碼分辨率發送

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 的研發工作。之前從事在線教育領域的音視頻工作,對構建實時音視頻解決方案和使用場景有一定的理解,喜歡鑽研和解決複雜技術問題。

更多技術乾貨,歡迎關注【網易智企技術+】微信公衆號

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