Camera 和 MediaCodec 對視頻旋轉(zhuǎn)角度的處理
Surface 和 GLConsumer 對視頻旋轉(zhuǎn)角度的處理
最近遇到一個(gè)有趣的問題:通過MediaCodec解碼帶旋轉(zhuǎn)角度的視頻時(shí),如果Output Surface是TextureView或者SurfaceView提供的,那么屏幕上的視頻幀可以正常展示(處理了旋轉(zhuǎn)角度);如果Surface是由SurfaceTexture創(chuàng)建而來,那么通過SurfaceTexture獲取的OES紋理,是未處理旋轉(zhuǎn)角度的,需要自己兼容下角度問題。
此外,我也測試了Camera,首先通過Camera.setDisplayOrientation設(shè)置順時(shí)針旋轉(zhuǎn)角度,然后設(shè)置Output Surface接收Camera預(yù)覽視頻幀。不出意外,也得到了同樣的結(jié)論。
視頻旋轉(zhuǎn)角度的處理
既然都是通過Surface接收視頻幀,那為什么會存在這種差異那?MediaCodec和Camera生產(chǎn)視頻幀,并且展示到屏幕上的流程如下所示:
首先,
MediaCodec和Camera會通過native_window_set_buffers_transform函數(shù)向Surface(ANativeWindow)設(shè)置Transform Flag,保存在Surface.mTransform變量。接著,生產(chǎn)端
MediaCodec和Camera通過Surface持有的BufferQueueProducer向BufferQueue入隊(duì)BufferItem時(shí),會把Surface.mTransform賦值給BufferItem.mTransform。然后,消費(fèi)端
GLConsumer通過BufferQueueComsumer獲取BufferItem后,根據(jù)BufferItem.mTransform,還原出一個(gè)4*4的紋理變換矩陣,保存在GLConsumer.mCurrentTransformMatrix變量中,GLConsumer的使用者可以通過GLConsumer.getTransformMatrix方法獲取這個(gè)紋理變換矩陣。最后,
GLConsumer的使用者負(fù)責(zé)把紋理變換矩陣作用到紋理上,以正確展示視頻幀。
GLConsumer負(fù)責(zé)把BufferItem.mGraphicBuffer轉(zhuǎn)換為紋理,并且根據(jù)BufferItem.mTransform計(jì)算出紋理變換矩陣,此時(shí)的紋理是原始狀態(tài)(未應(yīng)用紋理矩陣)。獲取紋理變換矩陣,并且應(yīng)用到紋理,是GLConsumer(紋理)使用方的責(zé)任。
不管Surface是由TextureView提供還是通過SurfaceTexture創(chuàng)建,上述前3步都是相同的流程。導(dǎo)致上述差異的根本原因是第4步:GLConsumer的使用者是否把紋理變換矩陣應(yīng)用到紋理
當(dāng)Surface由TextureView提供時(shí),GLConsumer的使用者是DeferredLayerUpdater,它獲取紋理變換矩陣后,會填充到Layer.texTransform變量(frameworks\base\libs\hwui\Layer.h),后續(xù)在硬件加速的異步渲染線程中,OpenGLRenderer渲染DrawLayerOp(基于上面的Layer創(chuàng)建而來)時(shí),會應(yīng)用該紋理變換矩陣。
當(dāng)Surface由SurfaceView提供時(shí),Surface是獨(dú)立窗口,GLConsumer的使用者是frameworks/native/services/surfaceflinger/Layer,它獲取紋理變換矩陣后,會填充到Layer.mTexture.mTextureMatrix中,后續(xù)SurfaceFlinger合成Layer時(shí),會應(yīng)用該紋理變換矩陣。
可見,當(dāng)Output Surface由TextureView或者SurfaceView提供時(shí),GLConsumer的使用者主動獲取并使用了紋理變換矩陣,所以我們在屏幕上看到的視頻幀才是正常的。
而當(dāng)Output Surface由我們基于OES紋理ID創(chuàng)建的SurfaceTexture創(chuàng)建而來時(shí),GLConsumer(紋理)的使用者是業(yè)務(wù)方,所以需要業(yè)務(wù)方通過SurfaceTexture.getTransformMatrix主動獲取紋理變換矩陣,并把它應(yīng)用到OES紋理上。即:我們拿到的OES紋理本就是原始紋理,需要應(yīng)用紋理變換矩陣后,才能正常展示。
業(yè)務(wù)方要怎么使用紋理變換矩陣那?
首先,通過
SurfaceTexture.getTransformMatrix獲取紋理變換矩陣,該矩陣是列優(yōu)先次序存儲,可以直接通過glUniformMatrix4fv函數(shù)上傳到頂點(diǎn)著色器。在頂點(diǎn)著色器中,使用紋理變換矩陣左乘紋理坐標(biāo),并把最新的紋理坐標(biāo)傳遞給片元著色器。
在片元著色器中,正常使用紋理坐標(biāo)從紋理中取色就可以了(視頻幀正確展示)。
視頻旋轉(zhuǎn)角度的流轉(zhuǎn)流程
我們看下旋轉(zhuǎn)角度在MediaCodec和Camera中的流轉(zhuǎn)流程。
MediaCodec
通過MediaCodec解碼視頻時(shí),需要通過MediaFormat設(shè)置解碼參數(shù),例如:SPS、PPS等。當(dāng)視頻存在旋轉(zhuǎn)角度時(shí),需要通過MediaFormat.KEY_ROTATION配置旋轉(zhuǎn)角度,表示視頻幀需要順時(shí)針旋轉(zhuǎn)多少度,才能正確展示。但是要注意的是:只有當(dāng)MediaCodec直接解碼到Surface時(shí),旋轉(zhuǎn)角度才有效。
旋轉(zhuǎn)角度在MediaCodec中的流轉(zhuǎn)流程如下所示:
MediaCodec::configure配置解碼器,MediaFormat作為參數(shù)。MediaCodec發(fā)送
kWhatConfigure消息,在自有線程配置解碼器。調(diào)用
ACodec->initiateConfigureComponent(MediaFormat為參數(shù))配置ACodec。ACodec發(fā)送
kWhatConfigureComponent消息在自有線程配置ACodec。接著是
ACodec::LoadedState::onConfigureComponent方法。然后是
ACodec.configureCodec方法,負(fù)責(zé)通過MediaFormat配置ACodec,其中從format中取出了"rotation-degrees",保存在ACodec.mRotationDegrees變量中。最后,
ACodec.setupNativeWindowSizeFormatAndUsage中調(diào)用全局函數(shù)setNativeWindowSizeFormatAndUsage為Surface(ANativeWindow)設(shè)置transform flag。核心代碼在setNativeWindowSizeFormatAndUsage函數(shù)中:
// 根據(jù)旋轉(zhuǎn)角度獲得transform flag
int transform = 0;
if ((rotation % 90) == 0) {
switch ((rotation / 90) & 3) {
case 1: transform = HAL_TRANSFORM_ROT_90; break;
case 2: transform = HAL_TRANSFORM_ROT_180; break;
case 3: transform = HAL_TRANSFORM_ROT_270; break;
default: transform = 0; break;
}
}
// 為Surface(ANativeWindow)設(shè)置transform
err = native_window_set_buffers_transform(nativeWindow, transform);
native_window_set_buffers_transform函數(shù)會觸發(fā)ANativeWindow子類Surface的perform方法處理NATIVE_WINDOW_SET_BUFFERS_TRANSFORM消息,接著是dispatchSetBuffersTransform -> setBuffersTransform,最后把transform保存在了Surface.mTransform變量中。
至此,旋轉(zhuǎn)角度已經(jīng)設(shè)置到了Surface.mTransform,后面就是Surface生產(chǎn)圖像數(shù)據(jù)時(shí)負(fù)責(zé)使用它了。
Surface內(nèi)部通過BufferQueueProducer入隊(duì)圖像數(shù)據(jù)時(shí),會把Surface.mTransform賦值給BufferItem.mTransform,BufferQueue的元素就是BufferItem。
然后,消費(fèi)端GLConsumer通過BufferQueueComsumer獲取BufferItem后,根據(jù)BufferItem.mTransform,還原出一個(gè)4*4的紋理變換矩陣,保存在GLConsumer.mCurrentTransformMatrix變量中,GLConsumer的使用者可以調(diào)用GLConsumer.getTransformMatrix方法獲取這個(gè)紋理變換矩陣,然后在使用對應(yīng)紋理時(shí),把該矩陣應(yīng)用到紋理上。
Camera
旋轉(zhuǎn)角度在Camera中的流轉(zhuǎn)流程如下所示:
Camera.setDisplayOrientation設(shè)置預(yù)覽顯示時(shí)的順時(shí)針旋轉(zhuǎn)角度。對應(yīng)Native方法android_hardware_Camera_setDisplayOrientation。
接著走到
Camera.sendCommand方法,這里的Camera是客戶端,會通過Binder調(diào)用到服務(wù)端CameraClient::sendCommand。接著走到CameraClient::sendCommand設(shè)置orientation。
接著繼續(xù)走到CameraHardwareInterface.setPreviewTransform
然后通過
native_window_set_buffers_transform為Camera的Preview Window(Surface)設(shè)置旋轉(zhuǎn)角度。最后就是Surface負(fù)責(zé)使用
Surface.mTransform了,這部分流程與MediaCodec類似。
應(yīng)該說MediaCodec和Camera設(shè)置旋轉(zhuǎn)角度的流程不同,但是最終都是把旋轉(zhuǎn)角度設(shè)置到Surface.mTransform,后面就是Surface和GLConsumer的事情了。
總結(jié)
不管是MediaCodec還是Camera,都是Surface的圖像數(shù)據(jù)來源,只不過這個(gè)源圖像數(shù)據(jù),可能有一定的旋轉(zhuǎn)角度或者鏡像。這種情況下,MediaCodec和Camera就會通過native_window_set_buffers_transform為Surface設(shè)置Transform Flag,保存在Surface.mTransform變量中,表示生成的圖像數(shù)據(jù),必須經(jīng)過Transform變換之后才能正常顯示??捎玫腡ransform Flag如下所示:
// Transform Flag
typedef enum android_transform {
// 水平鏡像
HAL_TRANSFORM_FLIP_H = 0x01,
// 垂直鏡像
HAL_TRANSFORM_FLIP_V = 0x02,
// 順時(shí)針旋轉(zhuǎn)90度
HAL_TRANSFORM_ROT_90 = 0x04,
// 順時(shí)針旋轉(zhuǎn)180度
HAL_TRANSFORM_ROT_180 = 0x03,
// 順時(shí)針旋轉(zhuǎn)270度
HAL_TRANSFORM_ROT_270 = 0x07,
// don't use. see system/window.h
HAL_TRANSFORM_RESERVED = 0x08,
} android_transform_t;
生產(chǎn)端Surface通過BufferQueueProducer添加圖像數(shù)據(jù)時(shí),會把Surface.mTransform賦值給BufferItem.mTransform,然后入隊(duì)到BufferQueue。
消費(fèi)端GLConsumer通過BufferQueueComsumer獲取BufferItem后,根據(jù)BufferItem.mTransform Flag,還原出一個(gè)4*4的紋理變換矩陣,保存在GLConsumer.mCurrentTransformMatrix變量中,GLConsumer的使用者調(diào)用GLConsumer.getTransformMatrix獲取這個(gè)紋理變換矩陣,然后作用到對應(yīng)紋理上,就可以正確展示視頻幀了。
原文鏈接: https://juejin.cn/post/6844904195535929351
-- END --
進(jìn)技術(shù)交流群,掃碼添加我的微信:Byte-Flow
獲取相關(guān)資料和源碼
推薦:
全網(wǎng)最全的 Android 音視頻和 OpenGL ES 干貨,都在這了
面試官:如何利用 Shader 實(shí)現(xiàn) RGBA 到 NV21 圖像格式轉(zhuǎn)換?
