Android Camera系列(三):GLSurfaceView+Camera

2024-09-03 21:20

本文主要是介绍Android Camera系列(三):GLSurfaceView+Camera,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

人类的悲欢并不相通—鲁迅

本系列主要讲述Android开发中Camera的相关操作、预览方式、视频录制等,项目结构代码耦合性低,旨在帮助大家能从中有所收获(方便copy :) ),对于个人来说也是一个总结的好机会

Alt

本章我们来讲解GLSurfaceView进行Camera预览,基于第一篇Android Camera系列(一):SurfaceView+Camera的成果,我们已经对Camera进行了封装,CameraManager拿来直接使用就好

一.GLSurfaceView使用

GLSurfaceView实际上就是继承了SurfaceView,并在其内部封装了EGL环境管理和渲染线程,使得我们可以直接使用opengl的API接口对图像进行变换等操作,如:黑白滤镜、美颜等各种复杂的滤镜效果

  1. 自定义CameraGLSurfaceView继承GLSurfaceView
  2. 实现SurfaceTexture.OnFrameAvailableListener接口,并在onFrameAvailable回调中请求每一帧数据进行渲染,也就是说Camera的预览需要我们自己绘制完成
  3. GLSurfaceView提供了绘制接口Renderer,我们需要定义CameraSurfaceRenderer实现该接口,并在GLSurfaceView初始化时设置自定义的渲染类。在onSurfaceCreated回调中创建外部纹理SurfaceTexture,并设置OnFrameAvailableListener监听Camera数据回调
  4. 实现自定义CameraCallback接口,监听Camera状态
  5. 一定要实现onResumeonPause接口,并在对应的Activity生命周期中调用。这是所有使用Camera的bug的源头
public class CameraGLSurfaceView extends GLSurfaceView implements SurfaceTexture.OnFrameAvailableListener, CameraCallback {private static final String TAG = CameraGLSurfaceView.class.getSimpleName();private Context mContext;private SurfaceTexture mSurfaceTexture;private CameraHandler mCameraHandler;private boolean hasSurface; // 是否存在摄像头显示层private CameraManager mCameraManager;private int mRatioWidth = 0;private int mRatioHeight = 0;private int mGLSurfaceWidth;private int mGLSurfaceHeight;private CameraSurfaceRenderer mRenderer;public CameraGLSurfaceView(Context context) {super(context);init(context);}public CameraGLSurfaceView(Context context, AttributeSet attrs) {super(context, attrs);init(context);}private void init(Context context) {mContext = context;mCameraHandler = new CameraHandler(this);mCameraManager = new CameraManager(context);mCameraManager.setCameraCallback(this);setEGLContextClientVersion(2);mRenderer = new CameraSurfaceRenderer(mCameraHandler);setRenderer(mRenderer);setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);}public SurfaceTexture getSurfaceTexture() {return mSurfaceTexture;}private void setAspectRatio(int width, int height) {if (width < 0 || height < 0) {throw new IllegalArgumentException("Size cannot be negative.");}mRatioWidth = width;mRatioHeight = height;requestLayout();}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int width = MeasureSpec.getSize(widthMeasureSpec);int height = MeasureSpec.getSize(heightMeasureSpec);if (0 == mRatioWidth || 0 == mRatioHeight) {setMeasuredDimension(width, height);} else {if (width < height * mRatioWidth / mRatioHeight) {setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);} else {setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);}}}@Overridepublic void onFrameAvailable(SurfaceTexture surfaceTexture) {requestRender();}/*** Connects the SurfaceTexture to the Camera preview output, and starts the preview.*/private void handleSetSurfaceTexture(SurfaceTexture st) {Logs.i(TAG, "handleSetSurfaceTexture.");mSurfaceTexture = st;hasSurface = true;mSurfaceTexture.setOnFrameAvailableListener(this);openCamera();}/**** @param width* @param height*/private void handleSurfaceChanged(int width, int height) {Logs.i(TAG, "handleSurfaceChanged.");mGLSurfaceWidth = width;mGLSurfaceHeight = height;setAspectRatio();}/*** 打开摄像头并预览*/public void onResume() {super.onResume();if (hasSurface) {// 当activity暂停,但是并未停止的时候,surface仍然存在,所以 surfaceCreated()// 并不会调用,需要在此处初始化摄像头openCamera();}}/*** 停止预览并关闭摄像头*/public void onPause() {super.onPause();closeCamera();}public void onDestroy() {mCameraHandler.invalidateHandler();}/*** 打开摄像头*/private void openCamera() {if (mSurfaceTexture == null) {Logs.e(TAG, "mSurfaceTexture is null.");return;}if (mCameraManager.isOpen()) {Logs.w(TAG, "Camera is opened!");return;}mCameraManager.openCamera();if (mCameraManager.isOpen()) {mCameraManager.startPreview(mSurfaceTexture);}}private void closeCamera() {mCameraManager.releaseCamera();queueEvent(() -> mRenderer.notifyPausing());mSurfaceTexture = null;}@Overridepublic void onOpen() {}@Overridepublic void onOpenError(int error, String msg) {}@Overridepublic void onPreview(int previewWidth, int previewHeight) {Logs.i(TAG, "onPreview " + previewWidth + " " + previewHeight);queueEvent(() -> mRenderer.setCameraPreviewSize(previewWidth, previewHeight));setAspectRatio();}@Overridepublic void onPreviewError(int error, String msg) {}@Overridepublic void onClose() {}private void setAspectRatio() {int previewWidth = mCameraManager.getPreviewWidth();int previewHeight = mCameraManager.getPreviewHeight();if (mGLSurfaceWidth > mGLSurfaceHeight) {setAspectRatio(previewWidth, previewHeight);} else {setAspectRatio(previewHeight, previewWidth);}}/*** Handles camera operation requests from other threads.  Necessary because the Camera* must only be accessed from one thread.* <p>* The object is created on the UI thread, and all handlers run there.  Messages are* sent from other threads, using sendMessage().*/static class CameraHandler extends Handler {public static final int MSG_SET_SURFACE_TEXTURE = 0;public static final int MSG_SURFACE_CHANGED = 1;private WeakReference<CameraGLSurfaceView> mWeakGLSurfaceView;public CameraHandler(CameraGLSurfaceView view) {mWeakGLSurfaceView = new WeakReference<>(view);}/*** Drop the reference to the activity.  Useful as a paranoid measure to ensure that* attempts to access a stale Activity through a handler are caught.*/public void invalidateHandler() {mWeakGLSurfaceView.clear();}@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);int what = msg.what;CameraGLSurfaceView view = mWeakGLSurfaceView.get();if (view == null) {return;}switch (what) {case MSG_SET_SURFACE_TEXTURE:view.handleSetSurfaceTexture((SurfaceTexture) msg.obj);break;case MSG_SURFACE_CHANGED:view.handleSurfaceChanged(msg.arg1, msg.arg2);break;default:throw new RuntimeException("unknown msg " + what);}}}/*** Renderer object for our GLSurfaceView.* <p>* Do not call any methods here directly from another thread -- use the* GLSurfaceView#queueEvent() call.*/static class CameraSurfaceRenderer implements GLSurfaceView.Renderer {private CameraGLSurfaceView.CameraHandler mCameraHandler;private final float[] mSTMatrix = new float[16];private FullFrameRect mFullScreen;// width/height of the incoming camera preview framesprivate boolean mIncomingSizeUpdated;private int mIncomingWidth;private int mIncomingHeight;private int mTextureId = -1;private SurfaceTexture mSurfaceTexture;public CameraSurfaceRenderer(CameraGLSurfaceView.CameraHandler cameraHandler) {mCameraHandler = cameraHandler;mTextureId = -1;mIncomingSizeUpdated = false;mIncomingWidth = mIncomingHeight = -1;}/*** Notifies the renderer thread that the activity is pausing.* <p>* For best results, call this *after* disabling Camera preview.*/public void notifyPausing() {if (mSurfaceTexture != null) {Logs.d(TAG, "renderer pausing -- releasing SurfaceTexture");mSurfaceTexture.release();mSurfaceTexture = null;}if (mFullScreen != null) {mFullScreen.release(false);     // assume the GLSurfaceView EGL context is aboutmFullScreen = null;             //  to be destroyed}mIncomingWidth = mIncomingHeight = -1;}@Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {Logs.i(TAG, "onSurfaceCreated. " + Thread.currentThread().getName());// Set up the texture blitter that will be used for on-screen display.  This// is *not* applied to the recording, because that uses a separate shader.mFullScreen = new FullFrameRect(new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT));mTextureId = mFullScreen.createTextureObject();// Create a SurfaceTexture, with an external texture, in this EGL context.  We don't// have a Looper in this thread -- GLSurfaceView doesn't create one -- so the frame// available messages will arrive on the main thread.mSurfaceTexture = new SurfaceTexture(mTextureId);mCameraHandler.sendMessage(mCameraHandler.obtainMessage(CameraHandler.MSG_SET_SURFACE_TEXTURE, mSurfaceTexture));}@Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {gl.glViewport(0, 0, width, height);mCameraHandler.sendMessage(mCameraHandler.obtainMessage(CameraHandler.MSG_SURFACE_CHANGED, width, height));}@Overridepublic void onDrawFrame(GL10 gl) {if (mSurfaceTexture == null) return;mSurfaceTexture.updateTexImage();if (mIncomingWidth <= 0 || mIncomingHeight <= 0) {return;}if (mIncomingSizeUpdated) {mFullScreen.getProgram().setTexSize(mIncomingWidth, mIncomingHeight);mIncomingSizeUpdated = false;}mSurfaceTexture.getTransformMatrix(mSTMatrix);mFullScreen.drawFrame(mTextureId, mSTMatrix);}public void setCameraPreviewSize(int width, int height) {mIncomingWidth = width;mIncomingHeight = height;mIncomingSizeUpdated = true;}}
}

1.Camera操作时机

与SurfaceView和TextureView不同,GLSurfaceView中并没有地方获取SurfaceTexture的地方,虽然Renderer接口有onSurfaceCreated回调但是并没有SurfaceTexture,而是要求我们自己创建外部纹理ID用于Camera预览数据的回调。至于这个外部纹理如何又显示到GLSurfaceView上的,这个章节就不先介绍了,后续我们进行完整的opengl环境搭建再进一步讨论。

Renderer中的所有回调接口都是运行在独立的线程中的,这也就是为什么我们要单独定义个类,而不是让CameraGLSurfaceView直接实现该接口,让他和别的接口方法隔离

GLSurfaceViewsetRenderer接口源码可以看到会启动一个GLThread线程,Renderer接口都是运行在该线程中

    public void setRenderer(Renderer renderer) {...mRenderer = renderer;mGLThread = new GLThread(mThisWeakRef);mGLThread.start();}

因此我们要把创建好的外部纹理通过Handler传递给UI线程,UI线程在获取到SurfaceTexture后打开摄像头,记得在onResume中也同样打开一次摄像头

        @Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {mFullScreen = new FullFrameRect(new Texture2dProgram(Texture2dProgram.ProgramType.TEXTURE_EXT));// 创建外部纹理IDmTextureId = mFullScreen.createTextureObject();// 在此EGL上下文中创建具有外部纹理的SurfaceTexturemSurfaceTexture = new SurfaceTexture(mTextureId);// 将SurfaceTexture传递给UI线程mCameraHandler.sendMessage(mCameraHandler.obtainMessage(CameraHandler.MSG_SET_SURFACE_TEXTURE, mSurfaceTexture));}

我们重写surfaceDestroyed,在该回调和onPause中关闭摄像头

注意surfaceDestroyedSurfaceHolder.Callback的方法,该方法是运行在UI线程中的

2. GLSurfaceView计算大小

  1. 和SurfaceView和TextureView一样,我们在onPreview回调中设置TextureView的大小和比例
  2. onSurfaceChanged回调中设置GL画布大小,偶发预览变形大多是没有在此调用glViewport方法导致
        @Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {gl.glViewport(0, 0, width, height);mCameraHandler.sendMessage(mCameraHandler.obtainMessage(CameraHandler.MSG_SURFACE_CHANGED, width, height));}

二.最后

本文介绍了Camera+GLSurfaceView的基本操作及关键代码。本章内容也不是很多,介绍了如何用GLSurfaceView预览Camera数据的流程。我们没有对opengl的API进行过多的讲解,以及Renderer接口是如何将数据渲染到GLSurfaceView中的。在后续的章节我会讲解opengl在Android中的使用,希望聪明的你在回看该章会有种醍醐灌顶的感觉吧。

lib-camera库包结构如下:

说明
cameracamera相关操作功能包,包括Camera和Camera2。以及各种预览视图
encoderMediaCdoec录制视频相关,包括对ByteBuffer和Surface的录制
glesopengles操作相关
permission权限相关
util工具类

每个包都可独立使用做到最低的耦合,方便白嫖

github地址:https://github.com/xiaozhi003/AndroidCamera,https://gitee.com/xiaozhi003/android-camera

参考:

  1. https://github.com/afei-cn/CameraDemo
  2. https://github.com/saki4510t/UVCCamera
  3. https://github.com/google/grafika

这篇关于Android Camera系列(三):GLSurfaceView+Camera的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1134076

相关文章

Android Kotlin 高阶函数详解及其在协程中的应用小结

《AndroidKotlin高阶函数详解及其在协程中的应用小结》高阶函数是Kotlin中的一个重要特性,它能够将函数作为一等公民(First-ClassCitizen),使得代码更加简洁、灵活和可... 目录1. 引言2. 什么是高阶函数?3. 高阶函数的基础用法3.1 传递函数作为参数3.2 Lambda

Android自定义Scrollbar的两种实现方式

《Android自定义Scrollbar的两种实现方式》本文介绍两种实现自定义滚动条的方法,分别通过ItemDecoration方案和独立View方案实现滚动条定制化,文章通过代码示例讲解的非常详细,... 目录方案一:ItemDecoration实现(推荐用于RecyclerView)实现原理完整代码实现

Android App安装列表获取方法(实践方案)

《AndroidApp安装列表获取方法(实践方案)》文章介绍了Android11及以上版本获取应用列表的方案调整,包括权限配置、白名单配置和action配置三种方式,并提供了相应的Java和Kotl... 目录前言实现方案         方案概述一、 androidManifest 三种配置方式

Android WebView无法加载H5页面的常见问题和解决方法

《AndroidWebView无法加载H5页面的常见问题和解决方法》AndroidWebView是一种视图组件,使得Android应用能够显示网页内容,它基于Chromium,具备现代浏览器的许多功... 目录1. WebView 简介2. 常见问题3. 网络权限设置4. 启用 JavaScript5. D

Android如何获取当前CPU频率和占用率

《Android如何获取当前CPU频率和占用率》最近在优化App的性能,需要获取当前CPU视频频率和占用率,所以本文小编就来和大家总结一下如何在Android中获取当前CPU频率和占用率吧... 最近在优化 App 的性能,需要获取当前 CPU视频频率和占用率,通过查询资料,大致思路如下:目前没有标准的

Android开发中gradle下载缓慢的问题级解决方法

《Android开发中gradle下载缓慢的问题级解决方法》本文介绍了解决Android开发中Gradle下载缓慢问题的几种方法,本文给大家介绍的非常详细,感兴趣的朋友跟随小编一起看看吧... 目录一、网络环境优化二、Gradle版本与配置优化三、其他优化措施针对android开发中Gradle下载缓慢的问

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

Android里面的Service种类以及启动方式

《Android里面的Service种类以及启动方式》Android中的Service分为前台服务和后台服务,前台服务需要亮身份牌并显示通知,后台服务则有启动方式选择,包括startService和b... 目录一句话总结:一、Service 的两种类型:1. 前台服务(必须亮身份牌)2. 后台服务(偷偷干

Android kotlin语言实现删除文件的解决方案

《Androidkotlin语言实现删除文件的解决方案》:本文主要介绍Androidkotlin语言实现删除文件的解决方案,在项目开发过程中,尤其是需要跨平台协作的项目,那么删除用户指定的文件的... 目录一、前言二、适用环境三、模板内容1.权限申请2.Activity中的模板一、前言在项目开发过程中,尤

Android数据库Room的实际使用过程总结

《Android数据库Room的实际使用过程总结》这篇文章主要给大家介绍了关于Android数据库Room的实际使用过程,详细介绍了如何创建实体类、数据访问对象(DAO)和数据库抽象类,需要的朋友可以... 目录前言一、Room的基本使用1.项目配置2.创建实体类(Entity)3.创建数据访问对象(DAO