Android AVDemo(13):視頻渲染丨音視頻工程示例
塞尚《查德布凡光禿的樹木》
這個公眾號會 路線圖 式的遍歷分享音視頻技術 : 音視頻基礎(完成) → 音視頻工具(完成) → 音視頻工程示例(進行中) → 音視頻工業實戰(準備) 。 關注一下成本不高,錯過乾貨損失不小 ↓↓↓
iOS/Android 客户端開發同學如果想要開始學習音視頻開發,最絲滑的方式是對 音視頻基礎概念知識 有一定了解後,再借助 iOS/Android 平台的音視頻能力上手去實踐音視頻的 採集 → 編碼 → 封裝 → 解封裝 → 解碼 → 渲染
過程,並藉助 音視頻工具 來分析和理解對應的音視頻數據。
在音視頻工程示例這個欄目,我們將通過拆解 流程並實現 Demo 來向大家介紹如何在 iOS/Android 平台上手音視頻開發。
這裏是 Android 第十三篇: Android 視頻渲染 Demo 。這個 Demo 裏包含以下內容:
-
1)實現一個視頻採集裝模塊;
-
2)實現一個視頻渲染模塊;
-
3)串聯視頻採集和渲染模塊,將採集的視頻數據輸入給渲染模塊進行渲染;
-
4)詳盡的代碼註釋,幫你理解代碼邏輯和原理。
在本文中,我們將詳解一下 Demo 的具體實現和源碼。讀完本文內容相信就能幫你掌握相關知識。
不過,如果你的需求是:1)直接獲得全部工程源碼;2)想進一步諮詢音視頻技術問題;3)諮詢音視頻職業發展問題。可以根據自己的需要考慮是否加入『關鍵幀的音視頻開發圈』。
1、視頻採集模塊
在這個 Demo 中,視頻採集模塊 的實現與 《Android 視頻採集 Demo》 中一樣,這裏就不再重複介紹了,其接口如下:
public interface KFIVideoCapture { ///< 視頻採集初始化。 public void setup(Context context, KFVideoCaptureConfig config, KFVideoCaptureListener listener, EGLContext eglShareContext); ///< 釋放採集實例。 public void release(); ///< 開始採集。 public void startRunning(); ///< 關閉採集。 public void stopRunning(); ///< 是否正在採集。 public boolean isRunning(); ///< 獲取 OpenGL 上下文。 public EGLContext getEGLContext(); ///< 切換攝像頭。 public void switchCamera(); }
2、視頻渲染模塊
在之前的 《Android 視頻採集 Demo》 那篇中,我們採集後的視頻數據是通過 來做預覽渲染的。這篇我們來介紹一下使用
內部管理
、
,並且使用 OpenGL 實現渲染功能。
首先,我們在 中定義渲染回調。
public interface KFRenderListener { void surfaceCreate(@NonNull Surface surface); ///< 渲染緩存創建. void surfaceChanged(@NonNull Surface surface, int width, int height); ///< 渲染緩存變更分辨率。 void surfaceDestroy(@NonNull Surface surface); ///< 渲染緩存銷燬。 }
然後,我們在 中管理
、
以及具體渲染邏輯。
public class KFRenderView extends ViewGroup { private KFGLContext mEGLContext = null; ///< OpenGL 上下文。 private KFGLFilter mFilter = null; ///< 特效渲染到指定 Surface。 private EGLContext mShareContext = null; ///< 共享上下文。 private View mRenderView = null; ///< 渲染視圖基類。 private int mSurfaceWidth = 0; ///< 渲染緩存寬。 private int mSurfaceHeight = 0; ///< 渲染緩存高。 private FloatBuffer mSquareVerticesBuffer = null; ///< 自定義頂點. private KFRenderMode mRenderMode = KFRenderMode.KFRenderModeFill; ///< 自適應模式,黑邊,比例填衝。 private boolean mSurfaceChanged = false; ///< 渲染緩存是否變更。 private Size mLastRenderSize = new Size(0,0); ///< 標記上次渲染 Size。 public enum KFRenderMode { KFRenderStretch, ///< 拉伸滿,可能變形。 KFRenderModeFit, ///< 黑邊。 KFRenderModeFill ///< 比例填充。 }; public KFRenderView(Context context, EGLContext eglContext) { super(context); mShareContext = eglContext; ///< 共享上下文。 _setupSquareVertices(); ///< 初始化頂點。 boolean isSurfaceView = false; ///< TextureView 與 SurfaceView 開關。 if (isSurfaceView) { mRenderView = new KFSurfaceView(context, mListener); } else { mRenderView = new KFTextureView(context, mListener); } this.addView(mRenderView);// 添加視圖到父視圖 } public void release() { ///< 釋放 GL 上下文、特效。 if (mEGLContext != null) { mEGLContext.bind(); if (mFilter != null){ mFilter.release(); mFilter = null; } mEGLContext.unbind(); mEGLContext.release(); mEGLContext = null; } } public void render(KFTextureFrame inputFrame) { if (inputFrame == null) { return; } ///< 輸入紋理使用自定義特效渲染到 View 的 Surface 上。 if (mEGLContext != null && mFilter != null) { boolean frameResolutionChanged = inputFrame.textureSize.getWidth() != mLastRenderSize.getWidth() || inputFrame.textureSize.getHeight() != mLastRenderSize.getHeight(); ///< 渲染緩存變更或者視圖大小變更重新設置頂點。 if (mSurfaceChanged || frameResolutionChanged) { _recalculateVertices(inputFrame.textureSize); mSurfaceChanged = false; mLastRenderSize = inputFrame.textureSize; } ///< 渲染到指定 Surface。 mEGLContext.bind(); mFilter.setSquareVerticesBuffer(mSquareVerticesBuffer); GLES20.glViewport(0, 0, mSurfaceWidth, mSurfaceHeight); mFilter.render(inputFrame); mEGLContext.swapBuffers(); mEGLContext.unbind(); } } private KFRenderListener mListener = new KFRenderListener() { @Override ///< 渲染緩存創建。 public void surfaceCreate(@NonNull Surface surface) { mEGLContext = new KFGLContext(mShareContext,surface); ///< 初始化特效。 mEGLContext.bind(); _setupFilter(); mEGLContext.unbind(); } @Override ///< 渲染緩存變更。 public void surfaceChanged(@NonNull Surface surface, int width, int height) { mSurfaceWidth = width; mSurfaceHeight = height; mSurfaceChanged = true; ///< 設置 GL 上下文 Surface。 mEGLContext.bind(); mEGLContext.setSurface(surface); mEGLContext.unbind(); } @Override public void surfaceDestroy(@NonNull Surface surface) { } }; private void _setupFilter() { ///< 初始化特效。 if (mFilter == null) { mFilter = new KFGLFilter(true, KFGLBase.defaultVertexShader,KFGLBase.defaultFragmentShader); } } private void _setupSquareVertices() { ///< 初始化頂點緩存。 final float squareVertices[] = { -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, }; ByteBuffer squareVerticesByteBuffer = ByteBuffer.allocateDirect(4 * squareVertices.length); squareVerticesByteBuffer.order(ByteOrder.nativeOrder()); mSquareVerticesBuffer = squareVerticesByteBuffer.asFloatBuffer(); mSquareVerticesBuffer.put(squareVertices); mSquareVerticesBuffer.position(0); } private void _recalculateVertices(Size inputImageSize) { ///< 按照適應模式創建頂點。 if (mSurfaceWidth == 0 || mSurfaceHeight == 0) { return; } Size renderSize = new Size(mSurfaceWidth,mSurfaceHeight); float heightScaling = 1, widthScaling = 1; Size insetSize = new Size(0,0); float inputAspectRatio = (float) inputImageSize.getWidth() / (float)inputImageSize.getHeight(); float outputAspectRatio = (float)renderSize.getWidth() / (float)renderSize.getHeight(); boolean isAutomaticHeight = inputAspectRatio <= outputAspectRatio ? false : true; if (isAutomaticHeight) { float insetSizeHeight = (float)inputImageSize.getHeight() / ((float)inputImageSize.getWidth() / (float)renderSize.getWidth()); insetSize = new Size(renderSize.getWidth(),(int)insetSizeHeight); } else { float insetSizeWidth = (float)inputImageSize.getWidth() / ((float)inputImageSize.getHeight() / (float)renderSize.getHeight()); insetSize = new Size((int)insetSizeWidth,renderSize.getHeight()); } switch (mRenderMode) { case KFRenderStretch: { widthScaling = 1; heightScaling = 1; }; break; case KFRenderModeFit: { widthScaling = (float)insetSize.getWidth() / (float)renderSize.getWidth(); heightScaling = (float)insetSize.getHeight() / (float)renderSize.getHeight(); }; break; case KFRenderModeFill: { widthScaling = (float) renderSize.getHeight() / (float)insetSize.getHeight(); heightScaling = (float)renderSize.getWidth() / (float)insetSize.getWidth(); }; break; } final float squareVertices[] = { -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, }; final float customVertices[] = { -widthScaling, -heightScaling, widthScaling, -heightScaling, -widthScaling, heightScaling, widthScaling, heightScaling, }; ByteBuffer squareVerticesByteBuffer = ByteBuffer.allocateDirect(4 * customVertices.length); squareVerticesByteBuffer.order(ByteOrder.nativeOrder()); mSquareVerticesBuffer = squareVerticesByteBuffer.asFloatBuffer(); mSquareVerticesBuffer.put(customVertices); mSquareVerticesBuffer.position(0); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { ///< 視圖變更 Size。 this.mRenderView.layout(left,top,right,bottom); } }
上面是 的實現,繼承自 ViewGroup,其中主要包含這幾個部分:
-
1)管理
、
。
-
通過
開關來控制使用
或
。
-
性能高一些,可以在自線程更新 UI 的 View,遵循雙緩衝機制,但無法像正常視圖實現動畫。
-
性能稍微差一點,重載了 Draw 方法,可以像正常視圖實現動畫。
-
2)創建 OpenGL 上下文。
-
這裏需要注意的是,我們通過
管理上下文,初始化方法需要輸入渲染視圖 Surface 作為參數,這樣就可以將繪製結果直接渲染到指定 Surface。
-
3)創建渲染特效。
-
通過
將紋理數據渲染到渲染視圖 Surface。
-
通過
控制自定義渲染比例,在方法
中實時計算頂點數據來實現。
-
4)渲染回調通知 Surface 生命週期。
-
在
的
回調中通知 Surface 創建。
-
在
的
回調中通知 Surface 變更。
-
在
的
回調中通知 Surface 銷燬。
接下來是內部渲染視圖 的實現,需要注意 Surface 是通過
方法
獲取:
public class KFSurfaceView extends SurfaceView implements SurfaceHolder.Callback { private KFRenderListener mListener = null; ///< 回調。 private SurfaceHolder mHolder = null; ///< Surface 的抽象接口。 public KFSurfaceView(Context context, KFRenderListener listener) { super(context); mListener = listener; getHolder().addCallback(this); } @Override public void surfaceCreated(@NonNull SurfaceHolder surfaceHolder) { ///< Surface 創建。 mHolder = surfaceHolder; ///< 根據 SurfaceHolder 創建 Surface。 if (mListener != null) { mListener.surfaceCreate(surfaceHolder.getSurface()); } } @Override public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int format, int width, int height) { ///< Surface 分辨率變更。 if (mListener != null) { mListener.surfaceChanged(surfaceHolder.getSurface(),width,height); } } @Override public void surfaceDestroyed(@NonNull SurfaceHolder surfaceHolder) { ///< Surface 銷燬。 if (mListener != null) { mListener.surfaceDestroy(surfaceHolder.getSurface()); } } }
接下來是內部渲染視圖 的實現,需要注意 Surface 是通過
創建生成:
public class KFTextureView extends TextureView implements TextureView.SurfaceTextureListener { private KFRenderListener mListener = null; ///< 回調。 private Surface mSurface = null; ///< 渲染緩存。 private SurfaceTexture mSurfaceTexture = null; ///< 紋理緩存。 public KFTextureView(Context context, KFRenderListener listener) { super(context); this.setSurfaceTextureListener(this); mListener = listener; } @Override public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surfaceTexture, int width, int height) { ///< 紋理緩存創建。 mSurfaceTexture = surfaceTexture; ///< 根據 SurfaceTexture 創建 Surface。 mSurface = new Surface(surfaceTexture); if (mListener != null) { ///< 創建時候回調一次分辨率變更,對其 SurfaceView 接口。 mListener.surfaceCreate(mSurface); mListener.surfaceChanged(mSurface,width,height); } } @Override public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surfaceTexture, int width, int height) { ///< 紋理緩存變更分辨率。 if (mListener != null) { mListener.surfaceChanged(mSurface,width,height); } } @Override public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surfaceTexture) { } @Override public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surfaceTexture) { ///< 紋理緩存銷燬。 if (mListener != null) { mListener.surfaceDestroy(mSurface); } if (mSurface != null) { mSurface.release(); mSurface = null; } return false; } }
更具體細節見上述代碼及其註釋。
3、採集視頻數據並渲染
我們在一個 中來實現對採集的視頻數據進行渲染播放。
public class MainActivity extends AppCompatActivity { private KFIVideoCapture mCapture; ///< 相機採集。 private KFVideoCaptureConfig mCaptureConfig; ///< 相機採集配置。 private KFRenderView mRenderView; ///< 渲染視圖。 private KFGLContext mGLContext; ///< OpenGL 上下文。 @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ///< 檢測採集相關權限。 if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions((Activity) this, new String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); } ///< OpenGL 上下文。 mGLContext = new KFGLContext(null); ///< 渲染視圖。 mRenderView = new KFRenderView(this,mGLContext.getContext()); WindowManager windowManager = (WindowManager)this.getSystemService(this.WINDOW_SERVICE); Rect outRect = new Rect(); windowManager.getDefaultDisplay().getRectSize(outRect); FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(outRect.width(), outRect.height()); addContentView(mRenderView,params); ///< 採集配置:攝像頭方向、分辨率、幀率。 mCaptureConfig = new KFVideoCaptureConfig(); mCaptureConfig.cameraFacing = LENS_FACING_FRONT; mCaptureConfig.resolution = new Size(720,1280); mCaptureConfig.fps = 30; boolean useCamera2 = false; if (useCamera2) { mCapture = new KFVideoCaptureV2(); } else { mCapture = new KFVideoCaptureV1(); } mCapture.setup(this,mCaptureConfig,mVideoCaptureListener,mGLContext.getContext()); mCapture.startRunning(); } private KFVideoCaptureListener mVideoCaptureListener = new KFVideoCaptureListener() { @Override ///< 相機打開回調。 public void cameraOnOpened(){} @Override ///< 相機關閉回調。 public void cameraOnClosed() { } @Override ///< 相機出錯回調。 public void cameraOnError(int error,String errorMsg) { } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Override ///< 相機數據回調。 public void onFrameAvailable(KFFrame frame) { mRenderView.render((KFTextureFrame) frame); } }; }
上面是 的實現,主要分為以下幾個部分:
-
1)創建 OpenGL 上下文。
-
創建上下文
,這樣好處是採集與預覽可以共享,提高擴展性。
-
2)創建採集實例。
-
這裏需要注意的是,我們通過開關
選擇
或
。
-
參數配置
,可自定義攝像頭方向、幀率、分辨率。
-
3)採集數據回調
,將數據輸入給渲染視圖進行預覽。
更具體細節見上述代碼及其註釋。
- 完 -
謝謝看完全文,也點一下『贊』和 『在看』吧 ↓
- 一文看完 WWDC 2022 音視頻相關的更新要點丨音視頻工程示例
- 一看就懂的 OpenGL 基礎概念丨音視頻基礎
- 視頻轉碼後有色差要如何處理呢?丨有問有答
- WWDC 2022 音視頻相關 Session 概覽(EDR 相關)丨音視頻工程示例
- 音視頻知識圖譜 2022.06
- Android AVDemo(13):視頻渲染丨音視頻工程示例
- 想在自己的視頻平台支持 HDR 需要做哪些工作?丨有問有答
- Android AVDemo(11):視頻轉封裝,從 MP4 到 MP4丨音視頻工程示例
- 音視頻面試題集錦 2022.05
- Android AVDemo(6):音頻渲染,免費獲得源碼丨音視頻工程示例
- Android AVDemo(4):音頻解封裝,從 MP4 中解封裝出 AAC丨音視頻工程示例
- 如何根據 NALU 裸流數據來判斷其是 H.264 還是 H.265 編碼?丨有問有答
- 音視頻知識圖譜 2022.04
- Android AVDemo(2):音頻編碼,採集 PCM 數據編碼為 AAC丨音視頻工程示例
- 音視頻面試題集錦 2022.04
- Android AVDemo(1):音頻採集,免費獲取全部源碼丨音視頻工程示例
- iOS 視頻處理框架及重點 API 合集丨音視頻工程示例
- iOS AVDemo(13):視頻渲染,用 Metal 渲染丨音視頻工程示例
- 如何像抖音直播一樣,從 App 直播間到桌面畫中畫實現畫面無縫切換?丨有問有答
- 如何在視頻採集流水線中增加濾鏡處理節點?丨有問有答