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 直播間到桌面畫中畫實現畫面無縫切換?丨有問有答
- 如何在視訊採集流水線中增加濾鏡處理節點?丨有問有答