最近遇到一個比較有意思的問題,這裏記錄一下,免得日後忘記細節。
事情的起因是因爲我們的技術團隊做了一個合流的功能,就是把來自各種設備的多個視頻流進行解碼、按照一定佈局(可以簡單理解爲畫中畫)重新構造新的視頻幀數據再編碼,最後轉推CDN。這個合流沒什麼可說的,是一種常見的處理多流的手段。那麼遇到了什麼問題呢?來自移動端設備視頻畫面的方向問題。
大家知道,Android設備的取景器正向一般情況下是音量鍵那一側,而非自然正向(豎着拿手機的正上方)。即取景器正向採集進來的畫面,是需要旋轉90°或者270°(前後置)才能使捕捉的畫面按照自然正向顯示。這部分知識大家可以自行上網搜索相關資料,例如:
那麼,爲什麼我們通過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是軟編碼)。所以我推斷這個應該是和芯片類型有關係,華爲芯片是海思,小米是高通。通過現象猜測色彩空間可能沒有解析對,這個就需要花時間去看編解碼相關代碼了。
因爲只是實驗性質,加上上面提到的,我們技術團隊準備從合流端統一解決,所以這個畫面色彩失真的問題就暫時不打算投入時間繼續研究了。如果有知道原因的朋友,歡迎告訴我。