先放demo源碼地址:https://github.com/xxxzhou/aoce 06_mediaplayer
效果圖:
主要幾個點:
- 用ffmpeg打開rtmp流。
- 使用vulkan Compute shader處理yuv420P/yuv422P數據格式成rgba.
- 初始化android surface為vulkan的交換鏈,把如上結果復制到交換鏈上顯示。
- 如果是opengles surface,如何不通過CPU直接把數據從vulkan復制到opengles里。
這個demo主要是為了驗證用vulkan做GPGPU處理后,能輸出到vulkan/opengles紋理,后續有可能的話,明年下半年業余時間在這項目用vulkan Compute shader移植GPUImage學習下,設計的運行平台是window/android,你可以在window平台用vscode(安裝相應cmake/C++插件)運行查看調試過程,也可以用android studio打開里面的android文件夾,運行06_mediaplayer查看對應如上效果。
集成ffmpeg
window平台自己編譯后放入aoce/thirdparty/ffmpeg/x64中,android 我直接用的這位同學編譯好的https://github.com/xufuji456/FFmpegAndroid,你也可以在https://github.com/xxxzhou/aoce_thirdparty下載我整理好的。
如下目錄放置就好。
cmake里填寫好相關window/android平台邏輯以后,你可以選擇在window端直接用vscode,也可以用MVS打開cmake生成的sln文件,用android studio打開android文件夾,自動Sync gradle引用cmake並編譯好相關C++項目。
相應ffmpeg播放器FMediaPlayer的實現我參照了android下的MediaPlayer的接口設計,主要是prepare這個接口讓我意識到我在oeip不合理的地方,按照android下的MediaPlayer的接口重新實現了下ffmpeg里播放實現,現在只是簡單走了下流程,后期還需要設計各個狀態的轉化。
用vulkan的Compute shader做GPGPU計算
這個是我在 Vulkan在Android使用Compute shader 就准備做的事,設想是用有序無環圖來構建GPU計算流程,這個demo也算是簡單驗證下這個流程,后續會驗證設計的多輸入輸出部分,如下構建。
// 生成一張執行圖 vkGraph = AoceManager::Get().getPipeGraphFactory(gpuType)->createGraph(); auto *layerFactory = AoceManager::Get().getLayerFactory(gpuType); inputLayer = layerFactory->crateInput(); outputLayer = layerFactory->createOutput(); // 輸出GPU數據 outputLayer->updateParamet({false, true}); yuv2rgbLayer = layerFactory->createYUV2RGBA(); // 生成圖 vkGraph->addNode(inputLayer)->addNode(yuv2rgbLayer)->addNode(outputLayer);
其中yuv2rgbLayer現在完成了nv12/yuv420P/yuy422P/yuyvI這些常見格式的解析,貼一段yuv420P的代碼,CS代碼需要配合線程組的划分來看,大家可以在github查看完整流程,YUV轉換的代碼主要注意盡量不要多個線程同時讀或者寫某個地址就好。
void yuv420p(){ ivec2 uv = ivec2(gl_GlobalInvocationID.xy); ivec2 size = imageSize(outTex); if(uv.x >= size.x/2 || uv.y >= size.y/2){ return; } ivec2 nuv = u12u2(u22u1(uv.xy, size.x/2), size.x); ivec2 uindex = nuv + ivec2(0,size.y); ivec2 vindex = nuv + ivec2(0,size.y*5/4); float y1 = imageLoad(inTex,ivec2(uv.x*2,uv.y*2)).r; float y2 = imageLoad(inTex,ivec2(uv.x*2+1,uv.y*2)).r; float y3 = imageLoad(inTex,ivec2(uv.x*2,uv.y*2+1)).r; float y4 = imageLoad(inTex,ivec2(uv.x*2+1,uv.y*2+1)).r; float u = imageLoad(inTex,uindex.xy).r -0.5f; float v = imageLoad(inTex,vindex.xy).r -0.5f; vec4 rgba1 = yuv2Rgb(y1,u,v,1.f); vec4 rgba2 = yuv2Rgb(y2,u,v,1.f); vec4 rgba3 = yuv2Rgb(y3,u,v,1.f); vec4 rgba4 = yuv2Rgb(y4,u,v,1.f); imageStore(outTex, ivec2(uv.x*2,uv.y*2),rgba1); imageStore(outTex, ivec2(uv.x*2+1,uv.y*2),rgba2); imageStore(outTex, ivec2(uv.x*2,uv.y*2+1),rgba3); imageStore(outTex, ivec2(uv.x*2+1,uv.y*2+1),rgba4); }
用vulkan顯示
Vulkan在Android使用Compute shader 里面,我使用native window完成vulkan展示與顯示,但是很多時候,窗口並不是特定的,這個項目的設計目標之一也是無窗口使用Vulkan的Compute shader完成圖像處理,后續高效支持對接android UI/UE4/Unity3d/window UI SDK都方便。
在這,我們需要查看運行結果,在這我們換種方式,不用native window,不然方便的android UI都不能用了,在native window的實現方式中,我們知道vulkan 交換鏈只需要ANativeWindow,相應的UI消息循環顯示圖像處理結果過程我們並不需要,通過查看 android圖形框架指明了surface對應的C++類就是ANativeWindow,這樣我們可以直接使用surface來完成vulkan 圖像呈現工程,相關實現可以看vulkan模塊下的vulkanwindow里的initsurface實現。
其中這個demo的window/andorid刷新部分有些不同,window利用本身主線程的窗口空閑時間,把ffmpeg解碼線程上的數據經vulkan處理后復制到交換鏈當前顯示圖像中,而android本身的Surface沒有找到GLSurfaceView.Renderer類似的onDrawFrame時機,所以在android vulkan中,處理與復制呈現給交換鏈全在ffmpeg解碼線程上,不過android 下面的opengles顯示又和window一樣,主線程直接復制呈現,而ffmpeg解碼線程用vulkan處理。
opengles顯示
本來我想着用vulkan處理了,就能放棄opengl相關,但是至少現在還不現實,android端本身圖像APP,以及對應android上的UE4/Unity3D,opengl es可能是更成熟的選擇。
如何把vulkan的計算結果直接復制給opengles,通過文檔 android文檔AHardwareBuffer 其中有句話AHardwareBuffers可以綁定到EGL/OpenGL和Vulkan原語,通過相應提示用法VK_ANDROID_external_memory_android_hardware_buffer擴展/eglGetNativeClientBufferANDROID,我們繼續搜索,查找到 google vulkantest 這里有段源碼,詳細說明了如何把vulkan里的vkImage綁定到AHardwareBuffer上,opengles部分根據eglGetNativeClientBufferANDROID查找到如何把AHardwareBuffer綁定到對應的opengles紋理上,這樣我們通過vkImage-AHardwareBuffer-opengl texture的綁定,對應其封裝在aoce_vulkan模塊的hardwareImage類中。
AHardwareBuffer后續還可以繼續挖,android原生平台提供的多媒體相關的SDK如攝像機,MediaPlay等都有AHardwareBuffer的身影,后續可以嘗試直接把相關綁定到vulkan加快運算。
本人android開發並不熟悉,歡迎大家指正其中錯誤的地方。