【MCU系列】第一篇--OWT-Server環境搭建、除錯和VideoMix的原理剖析

語言: CN / TW / HK

簡介:

OWT是「Open WebRTC Toolkit」的簡稱,為Intel開源的影片會議「SFU+MCU」系統,借用官方git的README說明:

The media server for OWT provides an efficient video conference and streaming service that is based on WebRTC. It scales a single WebRTC stream out to many endpoints. At the same time, it enables media analytics capabilities for media streams. It features:

  • Distributed, scalable, and reliable SFU + MCU server
  • High performance VP8, VP9, H.264, and HEVC real-time transcoding on Intel® Core™ and Intel® Xeon® processors
  • Wide streaming protocols support including WebRTC, RTSP, RTMP, HLS, MPEG-DASH
  • Efficient mixing of HD video streams to save bandwidth and power on mobile devices
  • Intelligent Quality of Service (QoS) control mechanisms that adapt to different network environments
  • Customer defined media analytics plugins to perform analytics on streams from MCU
  • The usage scenarios for real-time media stream analytics including but not limited to movement/object detection

環境搭建和執行

請參考OWT-Server User GuideOWT Server快速入門忘籬大神講解OWT Server環境的搭建、除錯和分析

常見問題和注意事項

  1. node版本需使用v8.15.0。

  2. 當前的master分支會使用webrtc-m79,同步和編譯程式碼比較耗時,需使用穩定的VPN,且總共程式碼佔用17G左右,需較大的硬碟容量,建議直接使用和除錯4.3.x分支,我所使用的commit為:ec770323d387c6e63c609712481d9d2b0beebd52。

  3. 在pack打包時需確認check過程中沒有錯誤。video_agent、audio_agent、webrtc_agent依賴的動態庫都需拷貝至各自的lib目錄下,特別是如果使能了ffmpeg的fdk-aac,需手動將libfdk-aac.so.1拷貝至對應的lib目錄下。在缺乏依賴的動態庫時執行看不到明顯的錯誤提示,需檢視對應agent的執行日誌。

  4. 在打包4.3.x分支過程中,需手動將source/agent/sip/sipIn/build/Release/lib.target/目錄下的sipLib.so拷貝至上層目錄Release下。

  5. 在虛擬機器區域網192下執行init-all.sh啟動rabbitmq server中發生錯誤:

    Job for rabbitmq-server.service failed because the control process exited with error code. See "systemctl status rabbitmq-server.service" and "journalctl -xe" for details.
    複製程式碼

    檢視日誌,錯誤提示是:

    ERROR: epmd error for host 192: badarg (unknown POSIX error)
    複製程式碼

    解決方法:

    vim /etc/rabbitmq/rabbitmq-env.conf

    在檔案裡面新增這一行:[email protected],儲存

    (注意:如果rabbitmq-env.conf這個檔案沒有,開啟之後自動建立)

整體框架和程式碼分析

注:本文先聚焦媒體單元,對於信令和API後續再論。

OWT的程式結構,使用Nodejs呼叫C++程式碼,請參考忘籬大神CodeNodejs

OWT的程式碼分析,請參考Intel owt-server VideoMixer設計忘籬大神CodeVideo

VideoMix的原理剖析

將解碼後的源image的YUV資料按照Layout佈局的要求resize到目標image的大小,然後貼圖拷貝至相應的記憶體區域輸出合成後的圖片。如下圖所示[1]:左邊為目標合成image,右邊為原始image

需要先進行取樣縮放,需將下圖中間的源s1縮放成右邊的目標大小
然後貼圖,貼圖原理為:取樣後圖像的Y分量直接memcpy到合成影象的對應區域,U/V分量則需要注意:U/V水平和垂直均1/2取樣,隔行拷貝,每次拷貝取樣後w/2長度。合成後目標image的U/V資料也是隔行-且儲存也是連續的,所以UV拷貝的時候連續拷貝取樣後w/2長度然後進入合成影象下一行的U/V資料然後再拷貝,拷貝的高度為取樣後h/2高度。

上面的過程可由Google開源的libyuv的I420Scale函式來勝任,程式碼如下:

void SoftFrameGenerator::layout_regions(SoftFrameGenerator *t, rtc::scoped_refptr<webrtc::I420Buffer> compositeBuffer, const LayoutSolution &regions)
{
        uint32_t composite_width = compositeBuffer->width();
        uint32_t composite_height = compositeBuffer->height();

​
        for (LayoutSolution::const_iterator it = regions.begin(); it != regions.end(); ++it) {
          boost::shared_ptr<webrtc::VideoFrame> inputFrame = t->m_owner->getInputFrame(it->input);
          if (inputFrame == NULL) {
            continue;
          }

          rtc::scoped_refptr<webrtc::VideoFrameBuffer> inputBuffer = inputFrame->video_frame_buffer();

​
          Region region = it->region;
          uint32_t dst_x      = (uint64_t)composite_width * region.area.rect.left.numerator / region.area.rect.left.denominator;
          uint32_t dst_y      = (uint64_t)composite_height * region.area.rect.top.numerator / region.area.rect.top.denominator;
          uint32_t dst_width  = (uint64_t)composite_width * region.area.rect.width.numerator / region.area.rect.width.denominator;
          uint32_t dst_height = (uint64_t)composite_height * region.area.rect.height.numerator / region.area.rect.height.denominator;

​
          if (dst_x + dst_width > composite_width)
          dst_width = composite_width - dst_x;

​
          if (dst_y + dst_height > composite_height)
          dst_height = composite_height - dst_y;

​
          uint32_t cropped_dst_width;
          uint32_t cropped_dst_height;
          uint32_t src_x;
          uint32_t src_y;
          uint32_t src_width;
          uint32_t src_height;
          if (t->m_crop) {
            src_width   = std::min((uint32_t)inputBuffer->width(), dst_width * inputBuffer->height() / dst_height);
            src_height  = std::min((uint32_t)inputBuffer->height(), dst_height * inputBuffer->width() / dst_width);
            src_x       = (inputBuffer->width() - src_width) / 2;
            src_y       = (inputBuffer->height() - src_height) / 2;

​
            cropped_dst_width   = dst_width;
            cropped_dst_height  = dst_height;
          } else {
            src_width   = inputBuffer->width();
            src_height  = inputBuffer->height();
            src_x       = 0;
            src_y       = 0;

​
            cropped_dst_width   = std::min(dst_width, inputBuffer->width() * dst_height / inputBuffer->height());
            cropped_dst_height  = std::min(dst_height, inputBuffer->height() * dst_width / inputBuffer->width());
          }

​
          dst_x += (dst_width - cropped_dst_width) / 2;
          dst_y += (dst_height - cropped_dst_height) / 2;

​
          src_x               &= ~1;
          src_y               &= ~1;
          src_width           &= ~1;
          src_height          &= ~1;
          dst_x               &= ~1;
          dst_y               &= ~1;
          cropped_dst_width   &= ~1;
          cropped_dst_height  &= ~1;

​
          int ret = libyuv::I420Scale(
          inputBuffer->DataY() + src_y * inputBuffer->StrideY() + src_x, inputBuffer->StrideY(),
          inputBuffer->DataU() + (src_y * inputBuffer->StrideU() + src_x) / 2, inputBuffer->StrideU(),
          inputBuffer->DataV() + (src_y * inputBuffer->StrideV() + src_x) / 2, inputBuffer->StrideV(),
          src_width, src_height,
          compositeBuffer->MutableDataY() + dst_y * compositeBuffer->StrideY() + dst_x, compositeBuffer->StrideY(),
          compositeBuffer->MutableDataU() + (dst_y * compositeBuffer->StrideU() + dst_x) / 2, compositeBuffer->StrideU(),
          compositeBuffer->MutableDataV() + (dst_y * compositeBuffer->StrideV() + dst_x) / 2, compositeBuffer->StrideV(),
          cropped_dst_width, cropped_dst_height,
          libyuv::kFilterBox);
          if (ret != 0)
            ELOG_ERROR("I420Scale failed, ret %d", ret);
          }
}
複製程式碼

可以看到上面程式碼中,多次出現「stride」,那「什麼是stride」[2]呢?

當影片影象儲存在記憶體中時,記憶體緩衝區可能在每行畫素後面有額外的填充位元組,填充位元組會影響影象在記憶體中的儲存方式,但不會影響影象的顯示方式。stride是從記憶體中一行畫素開頭到記憶體中的下一行畫素間隔的位元組數,也可以叫做pitch,如果存在填充位元組,stride就會大於影象寬度,如下圖所示:

 

兩個包含相同尺寸影象的buffer可能有不同stride,所以在處理影片影象時必須考慮stride。

另外,影象在記憶體中的排列方式有兩種。「top-down」影象的首行第一個畫素先儲存在記憶體中。「bottom-up」影象的最後一行畫素先儲存在記憶體中。下圖顯示了兩者的區別:

bottom-up

影象具有負的stride,因為stride被定義為從第一行畫素到第二行畫素需要向後移動的距離。YUV影象通常是top-down,Direct3D surface則必須是top-down。而記憶體中的RGB影象則通常是bottom-up。做影片轉換時尤其需要處理stride不匹配的buffer。

綜上所述,對於影片會議MCU來說,混流伺服器的Layout佈局座標一般有如下定義:

 

對於混流服務的每路輸入流來說,需要設定相對左上角合理的座標值(left,top)「和」(right,bottom),然後就可以通過座標值決定偏移量來進行上述畫素拷貝操作。

有了上述的座標和Layout設計作為基礎,我們可以為使用者提前設定好常用的Layout佈局模版,以下佈局和圖片均摘自於Freeswitch

1up_top_left+9

2up_bottom+8
2up_middle+8
2up_top+8
3up+4
3up+9
3x3
4x4
5x5
6x6
8x8
overlaps
畫中畫
自此,影片混流合成的基本原理介紹完畢。

歡迎大家拍磚留言,分享你感興趣的話題!歡迎大家關注我個人公眾號!

Reference

[1]

YUV影象合成原理:

https://blog.csdn.net/zwz1984/article/details/50403150#comments

[2]

Image Stride:

https://docs.microsoft.com/en-us/windows/win32/medfound/image-stride

本文使用 mdnice 排版

分享到: