WebRTC 移動端的視頻畫面旋轉問題

最近遇到一個比較有意思的問題,這裏記錄一下,免得日後忘記細節。

事情的起因是因爲我們的技術團隊做了一個合流的功能,就是把來自各種設備的多個視頻流進行解碼、按照一定佈局(可以簡單理解爲畫中畫)重新構造新的視頻幀數據再編碼,最後轉推CDN。這個合流沒什麼可說的,是一種常見的處理多流的手段。那麼遇到了什麼問題呢?來自移動端設備視頻畫面的方向問題。

大家知道,Android設備的取景器正向一般情況下是音量鍵那一側,而非自然正向(豎着拿手機的正上方)。即取景器正向採集進來的畫面,是需要旋轉90°或者270°(前後置)才能使捕捉的畫面按照自然正向顯示。這部分知識大家可以自行上網搜索相關資料,例如:

Android 手機Camera Orientation問題的深入研究
Android Camera 踩坑

那麼,爲什麼我們通過WebRTC native SDK開發的Android應用程序,視頻畫面到達桌面端Chrome瀏覽器、或者其他移動端設備,顯示的是正確的呢?是不是WebRTC已經把採集畫面,加上設備顯示方向信息,對原始幀做了旋轉後再編碼發送的呢?

其實不然,在Android端,採集到的視頻幀並沒有旋轉,之所以在桌面端Chrome顯示正常,是因爲有一個方向信息隱藏在RTP的擴展字段裏,它的名字是:

urn:3gpp:video-orientation

請參考:https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml
如果你曾經分析過SDP信息,你會發現,各個端其實都有這項信息,例如下面是桌面端Chrome的:
在這裏插入圖片描述
所以真相是,接收端會根據存在RTP擴展字段裏的這項信息來對解碼後的圖像進行旋轉,以保證正確的顯示方向,而不是在編碼前就旋轉了畫面。

OK,回到文章一開始我提到的合流,我們遇到的問題。那就是合流那一方,在解碼視頻流時,並沒有理會RTP擴展字段中的方向,直接對發來的視頻幀進行了合屏,導致的結果就是,來自移動端的視頻畫面和Chrome中顯示的不一致。例如,如果Android設備沒有鎖定方向,且豎着拿手機(音量鍵向右),Chrome中顯示的是自然正向,而合屏顯示的是向右:
在這裏插入圖片描述
OK,那麼原因很清楚了,怎麼解決?有兩種方法:

方法一:修改合流端,即從RTP擴展字段中讀取video-orientation來對視頻幀進行旋轉,以保持和Chrome及其他設備相同的顯示方向

方法二:修改推流端,去掉video-orientation,直接對取景器採集到的視頻畫面進行旋轉,然後再編碼發送,接收端不需要理解video-orientation,直接解碼顯示(或解碼後合屏再編碼)就可以了。

兩種方法都各有利弊,具體還要看應用場景。我們技術團隊經過幾次討論溝通,最終選擇了方法一。

到這裏事情還沒有結束,雖然選擇了方法一,但我還是偷偷花了點時間驗證了一下方法二,看看在推流端旋轉後產生的結果是怎樣的。

通過閱讀WebRTC的代碼,找到了WebRTC已經實現的對視頻幀的旋轉代碼,位於:

\media\base\adapted_video_track_source.cc

摘抄代碼片段如下(注,這裏的代碼基於 WebRTC M73分支,因WebRTC代碼演變較快,可能未來不一定在這裏了):

void AdaptedVideoTrackSource::OnFrame(const webrtc::VideoFrame& frame) {
  rtc::scoped_refptr<webrtc::VideoFrameBuffer> buffer(
      frame.video_frame_buffer());
  /* Note that this is a "best effort" approach to
     wants.rotation_applied; apply_rotation_ can change from false to
     true between the check of apply_rotation() and the call to
     broadcaster_.OnFrame(), in which case we generate a frame with
     pending rotation despite some sink with wants.rotation_applied ==
     true was just added. The VideoBroadcaster enforces
     synchronization for us in this case, by not passing the frame on
     to sinks which don't want it. */
  if (apply_rotation() && frame.rotation() != webrtc::kVideoRotation_0 &&
      buffer->type() == webrtc::VideoFrameBuffer::Type::kI420) {
    /* Apply pending rotation. */
    webrtc::VideoFrame rotated_frame =
        webrtc::VideoFrame::Builder()
            .set_video_frame_buffer(webrtc::I420Buffer::Rotate(
                *buffer->GetI420(), frame.rotation()))
            .set_rotation(webrtc::kVideoRotation_0)
            .set_timestamp_us(frame.timestamp_us())
            .set_id(frame.id())
            .build();
    broadcaster_.OnFrame(rotated_frame);
  } else {
    broadcaster_.OnFrame(frame);
  }
}

代碼很簡單,如果滿足以下三個條件,就對視頻幀進行旋轉:

apply_rotation() 是真
frame.rotation() 不是 0
buffer->type() 是 i420

根據appliy_rotation()的實現順藤摸瓜,它的值來自於 VideoSendStreamImpl 的構造函數,代碼位於:

video\video_send_stream_impl.cc

摘抄賦值語句如下:

// Only request rotation at the source when we positively know that the remote
// side doesn't support the rotation extension. This allows us to prepare the
// encoder in the expectation that rotation is supported - which is the common
// case.
bool rotation_applied =
    std::find_if(config_->rtp.extensions.begin(),
                 config_->rtp.extensions.end(),
                 [](const RtpExtension& extension) {
                   return extension.uri == RtpExtension::kVideoRotationUri;
                 }) == config_->rtp.extensions.end();

註釋說明了,只有你確定遠端不支持rotation extension的情況下,再對source進行旋轉請求。這正是我們上面提到的情況。

OK,如果想讓rotation_applied爲true,那麼從find_if語句看到,在config_->rtp.extensions中就不能存在RtpExtension::kVideoRotationUri (這個常量值就是上面提到的 urn:3gpp:video-orientation)。我在這裏加了日誌,打印出來看到 rtp.extensions包含以下幾項信息:

{uri: http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time, id: 3},
{uri: http://www.webrtc.org/experiments/rtp-hdrext/playout-delay, id: 6},
{uri: urn:3gpp:video-orientation, id: 4}

顯然,rtp.extensions中含有urn:3gpp:video-orientation,那麼 rotation_applied 就是 false,繼而 apply_rotation() 返回的就是false,就不會執行旋轉。

第二個條件;frame.rotation() 不能是0,這個沒有什麼好解釋的,如果設備方向未鎖定,橫屏處於取景器正向,不用做旋轉,大家都相安無事,顯示正常。

第三個條件,buffer->type()必須是i420。這個參考以下代碼:

sdk\android\src\jni\android_video_track_source.cc

代碼片段摘抄如下:

void AndroidVideoTrackSource::OnFrameCaptured(
    JNIEnv* jni,
    int width,
    int height,
    int64_t timestamp_ns,
    VideoRotation rotation,
    const JavaRef<jobject>& j_video_frame_buffer) {
    // 略過若干語句
    
    // AdaptedVideoTrackSource handles applying rotation for I420 frames.
 	if (apply_rotation() && rotation != kVideoRotation_0) {
    	buffer = buffer->ToI420();
    }
  }

可以看到,很顯然,我們還是隻需要保證rtp extension中沒有video-orientation即可讓 appliy_rotation()爲true,buffer就會被轉爲 i420。

OK,讓WebRTC幫助我們完成視頻幀旋轉,其實歸根結底就是做一件事:去掉urn:3gpp:video-orientation

因爲我只是做實驗,所以用簡單粗暴的方法,直接從以下位置拿掉了它:

media\engine\webrtc_video_engine.cc

RtpCapabilities WebRtcVideoEngine::GetCapabilities() const {
  RtpCapabilities capabilities;
  capabilities.header_extensions.push_back(
      webrtc::RtpExtension(webrtc::RtpExtension::kTimestampOffsetUri,
                           webrtc::RtpExtension::kTimestampOffsetDefaultId));
  capabilities.header_extensions.push_back(
      webrtc::RtpExtension(webrtc::RtpExtension::kAbsSendTimeUri,
                           webrtc::RtpExtension::kAbsSendTimeDefaultId));
  // 我註釋掉了以下代碼  
  // capabilities.header_extensions.push_back(
  //     webrtc::RtpExtension(webrtc::RtpExtension::kVideoRotationUri,
  //                          webrtc::RtpExtension::kVideoRotationDefaultId));
  // 省略若干代碼
  return capabilities;
}

最後,編譯WebRTC Android native SDK,放到工程裏跑一下看下效果吧。

通過上面的方法,可以同時解決各個拉流端,包括合流端不支持解析video rotation信息的情況。

不過,在我的實驗中,最後還是遺留了一個問題。如下圖:
在這裏插入圖片描述
在H.264編碼格式情況下,旋轉視頻畫面後再去執行硬編碼,在拉流端得到的圖像色彩失真了。這個問題只在部分Android手機上發生(例如華爲榮耀、Mate20 Pro),但在小米某旗艦機上是正常的。另外,如果把視頻編碼改爲VP8,則在任何一款手機上旋轉後,都沒有這個問題(VP8是軟編碼)。所以我推斷這個應該是和芯片類型有關係,華爲芯片是海思,小米是高通。通過現象猜測色彩空間可能沒有解析對,這個就需要花時間去看編解碼相關代碼了。

因爲只是實驗性質,加上上面提到的,我們技術團隊準備從合流端統一解決,所以這個畫面色彩失真的問題就暫時不打算投入時間繼續研究了。如果有知道原因的朋友,歡迎告訴我。

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