Android Camera系列(一):SurfaceView+Camera

news2024/9/21 17:48:58

心行慈善,何需努力看经—《西游记》

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

Alt

一. Camera操作

Android系统存在这么多年,google更新了不少API。光是对摄像头的操作目前就有3中API:

  • android.hardware.Camera:最早用来自定义Camera的API
  • android.hardware.camera2.*:Android5.0之后推荐使用的API,对Camera操作更灵活,功能更丰富
  • CameraX:对Camera2的封装,API更简单

由于Android版本众多,考虑兼容性,本文我们还是对android.hardware.Camera进行讲解,操作Camera具体需要哪些步骤呢?

  1. 设置Camera权限,Android6.0以上请动态申请
  2. 打开相机,Camera.open()
  3. 开始预览,startPreview
  4. 停止预览,stopPreview
  5. 关闭相机,release()

1. 打开相机

申请Camera权限,Android6.0以上记得在打开相机前进行动态申请

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />
    ...
</manifest>

我们对Camera进行了简单的封装,无论是Camera还是Camera2我们预览视图都可以是SurfaceView、TextureView、GLSurfaceView,所以我们将Camera操作和预览视图进行分离,让预览View不在依赖具体的某种Camera实现

定义接口类ICameraManager:

/**
 * Camera和Camera2通用接口
 *
 * @author xiaozhi
 * @since 2024/8/15
 */
public interface ICameraManager {

    /**
     * 打开Camera
     */
    void openCamera();

    /**
     * 关闭释放Camera
     */
    void releaseCamera();

    /**
     * 开启预览
     *
     * @param surfaceHolder
     */
    void startPreview(SurfaceHolder surfaceHolder);

    /**
     * 开启预览
     *
     * @param surfaceTexture
     */
    void startPreview(SurfaceTexture surfaceTexture);

    /**
     * 停止预览
     */
    void stopPreview();

    ...
}

CameraManager实现ICameraManager,打开摄像头

    /**
     * 打开Camera
     */
    @Override
    public synchronized void openCamera() {
        Logs.i(TAG, "Camera open #" + mCameraId);
        if (mCamera == null) {
            if (mCameraId >= Camera.getNumberOfCameras()) {
                onOpenError(CAMERA_ERROR_NO_ID, "No camera.");
                return;
            }
            try {
                mCamera = Camera.open(mCameraId);
                Camera.getCameraInfo(mCameraId, mCameraInfo);
                mCamera.setErrorCallback(errorCallback);
                initCamera();
                onOpen();
                mOrientationEventListener.enable();
            } catch (Exception e) {
                onOpenError(CAMERA_ERROR_OPEN, e.getMessage());
            }
        }
    }

打开摄像头之后我们可以设置摄像头的一些基本参数,如预览尺寸,拍照尺寸等

    /**
     * 配置Camera参数
     */
    private void initCamera() {
        if (mCamera != null) {
            mParameters = mCamera.getParameters();
            if (mDisplayOrientation == -1) {
                setCameraDisplayOrientation(mContext, mCameraId, mCamera);
            }
            // 设置预览方向
            mCamera.setDisplayOrientation(mDisplayOrientation);
            // 设置拍照方向
            mParameters.setRotation(mOrientation);

            // 如果摄像头不支持这些参数都会出错的,所以设置的时候一定要判断是否支持
            List<String> supportedFlashModes = mParameters.getSupportedFlashModes();
            if (supportedFlashModes != null && supportedFlashModes.contains(Parameters.FLASH_MODE_OFF)) {
                mParameters.setFlashMode(Parameters.FLASH_MODE_OFF); // 设置闪光模式
            }
            List<String> supportedFocusModes = mParameters.getSupportedFocusModes();
            if (supportedFocusModes != null && supportedFocusModes.contains(Parameters.FOCUS_MODE_AUTO)) {
                mParameters.setFocusMode(Parameters.FOCUS_MODE_AUTO); // 设置聚焦模式
            }
            mParameters.setPreviewFormat(ImageFormat.NV21); // 设置预览图片格式
            mParameters.setPictureFormat(ImageFormat.JPEG); // 设置拍照图片格式

            Camera.Size previewSize = getSuitableSize(mParameters.getSupportedPreviewSizes());
            mPreviewWidth = previewSize.width;
            mPreviewHeight = previewSize.height;
            mPreviewSize = new Size(mPreviewWidth, mPreviewHeight);
            mParameters.setPreviewSize(mPreviewWidth, mPreviewHeight);
            Logs.d(TAG, "previewWidth: " + mPreviewWidth + ", previewHeight: " + mPreviewHeight);

            Camera.Size pictureSize = mParameters.getPictureSize();
            mParameters.setPictureSize(pictureSize.width, pictureSize.height);
            Logs.d(TAG, "pictureWidth: " + pictureSize.width + ", pictureHeight: " + pictureSize.height);

            mCamera.setParameters(mParameters);
            isSupportZoom = mParameters.isSmoothZoomSupported();
        }
    }

2.开始预览

    /**
     * 使用Surfaceview开启预览
     *
     * @param holder
     */
    @Override
    public synchronized void startPreview(SurfaceHolder holder) {
        Logs.i(TAG, "startPreview...");
        if (isPreviewing) {
            return;
        }
        if (mCamera != null) {
            try {
                mCamera.setPreviewDisplay(holder);
                if (!mPreviewBufferCallbacks.isEmpty()) {
                    mCamera.addCallbackBuffer(new byte[mPreviewWidth * mPreviewHeight * 3 / 2]);
                    mCamera.setPreviewCallbackWithBuffer(mPreviewCallback);
                }
                mCamera.startPreview();
                onPreview(mPreviewWidth, mPreviewHeight);
            } catch (Exception e) {
                onPreviewError(CAMERA_ERROR_PREVIEW, e.getMessage());
            }
        }
    }

3. 停止预览

    /**
     * 关闭预览
     */
    @Override
    public synchronized void stopPreview() {
        Logs.v(TAG, "stopPreview.");
        if (isPreviewing && null != mCamera) {
            try {
                mCamera.setPreviewCallback(null);
                mCamera.stopPreview();
                mPreviewBufferCallbacks.clear();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        isPreviewing = false;
    }

4. 关闭相机

    /**
     * 停止预览,释放Camera
     */
    @Override
    public synchronized void releaseCamera() {
        Logs.v(TAG, "releaseCamera.");
        if (null != mCamera) {
            stopPreview();
            try {
                mCamera.release();
                mCamera = null;
                mCameraBytes = null;
                mDisplayOrientation = -1;
            } catch (Exception e) {
            }
            onClose();
        }
    }

二.SurfaceView使用

我们要预览Camera数据必须要使用一个视图承接,SurfaceView是最常用的,也是Camera最初的标配

SurfaceView的特点:在自己独立的线程中绘制,内部使用双缓冲机制,画面更流畅。相比于 TextureView,它内存占用低,绘制更及时,耗时也更低,但不支持动画和截图。

Camera预览需要将SurfaceHolder传递给Camera然后开启预览如何获取SurfaceHoler?

  1. 自定义CameraSurfaceView继承SurfaceView
  2. 实现SurfaceHolder.Callback接口,并在CameraSurfaceView初始化时设置回调
  3. 实现自定义CameraCallback接口,监听Camera状态
  4. 一定要实现onResumeonPause接口,并在对应的Activity生命周期中调用。这是所有使用Camera的bug的源头
/**
 * 摄像头预览SurfaceView
 *
 * @author xiaozhi
 * @since 2024/8/22
 */
public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback, CameraCallback {

    private static final String TAG = CameraSurfaceView.class.getSimpleName();
    SurfaceHolder mSurfaceHolder;
    private Context mContext;
    private Handler mHandler;

    private boolean hasSurface; // 是否存在摄像头显示层
    private CameraManager mCameraManager;
    private int mRatioWidth = 0;
    private int mRatioHeight = 0;
    private int mSurfaceWidth;
    private int mSurfaceHeight;

    public CameraSurfaceView(Context context) {
        super(context);
        init(context);
    }

    public CameraSurfaceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public CameraSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        mContext = context;
        mHandler = new Handler(context.getMainLooper());
        mSurfaceHolder = getHolder();
        mSurfaceHolder.setFormat(PixelFormat.TRANSPARENT);//translucent半透明 transparent透明
        mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        mSurfaceHolder.addCallback(this);
        mCameraManager = new CameraManager(context);
        mCameraManager.setCameraCallback(this);
    }

    public CameraManager getCameraManager() {
        return mCameraManager;
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Logs.i(TAG, "surfaceCreated..." + hasSurface);
        if (!hasSurface && holder != null) {
            hasSurface = true;
            openCamera();
        }
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        Logs.i(TAG, "surfaceChanged [" + width + ", " + height + "]");
        mSurfaceWidth = width;
        mSurfaceHeight = height;
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        Logs.v(TAG, "surfaceDestroyed.");
        closeCamera();
        hasSurface = false;
    }

    public SurfaceHolder getSurfaceHolder() {
        return mSurfaceHolder;
    }

    public void onResume() {
        if (hasSurface) {
            // 当activity暂停,但是并未停止的时候,surface仍然存在,所以 surfaceCreated()
            // 并不会调用,需要在此处初始化摄像头
            openCamera();
        }
    }

    public void onPause() {
        closeCamera();
    }

    /**
     * 打开摄像头
     */
    private void openCamera() {
        if (mSurfaceHolder == null) {
            Logs.e(TAG, "SurfaceHolder is null.");
            return;
        }
        if (mCameraManager.isOpen()) {
            Logs.w(TAG, "Camera is opened!");
            return;
        }
        mCameraManager.openCamera();
    }

    /**
     * 关闭摄像头
     */
    private void closeCamera() {
        mCameraManager.releaseCamera();
    }

    private String getString(int resId) {
        return getResources().getString(resId);
    }

    private void setAspectRatio(int width, int height) {
        if (width < 0 || height < 0) {
            throw new IllegalArgumentException("Size cannot be negative.");
        }
        mRatioWidth = width;
        mRatioHeight = height;
        requestLayout();
    }

    @Override
    protected 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);
            }
        }
    }

    @Override
    public void onOpen() {
        mCameraManager.startPreview(getSurfaceHolder());
    }

    @Override
    public void onOpenError(int error, String msg) {
    }

    @Override
    public void onPreview(int previewWidth, int previewHeight) {
        if (mSurfaceWidth > mSurfaceHeight) {
            setAspectRatio(previewWidth, previewHeight);
        } else {
            setAspectRatio(previewHeight, previewWidth);
        }
    }

    @Override
    public void onPreviewError(int error, String msg) {
    }

    @Override
    public void onClose() {
    }
}

1.Camera操作时机

  • surfaceCreated回调中打开Camera,在surfaceDestroyed中关闭摄像头
    ,这基本是所有Camera操作的常识,我们代码中也展示了同样的方式。
  • 但这还不够完美,我们必须将Camera的操作和Activity的生命周期绑定,在onResume中也打开一次摄像头,在onPause中关闭一次摄像头,确保SurfaceHolder不可用以及Activity不在前台时正确关闭Camera

2.SurfaceView大小计算时机

在操作摄像头之前我们并不知道预览的尺寸,只能设置一个我们想要的尺寸,最终预览尺寸只能等到openCamera之后,CameraCallback中提供了回调接口onPreview在此我们可以设置SurfaceView的大小比例来适配Camera预览尺寸,避免预览页面拉升或压缩。

三.最后

本文介绍了Camera+SurfaceView的基本操作及关键代码,但是你如果看github中代码会发现和文中出入很大。其原因在于文章我想用一种简单的方式让没有做过自定义Camera的人也能明白。而github中的项目我已经进行了多次重构、抽象。其中包括对Camera的抽象,定义ICameraManager接口。对预览的View的重构,定义BaseCameraView接口,以及预览视图的众多超类。

lib-camera库包结构如下:

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

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

github地址:https://github.com/xiaozhi003/AndroidCamera

参考:

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2098996.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Next】2. 项目构建

打开 Next.js 的官方文档&#xff1a;https://nextjs.org/docs/getting-started/installation&#xff08;国内文档不够新&#xff09; Next.js 版本 14.2 &#xff0c; Node.js 的版本要求必须 > 18.18。 Next 有两种开发模式&#xff0c;下面讲新的 APP Router。 创建项…

VS2022 C++ 控制台中文乱码解决方案

最近写读文件的代码时&#xff0c;遇到了VS控制台读取中文时出现乱码&#xff0c;看了网上主要有两种方法 &#xff1a; 1、在VS菜单栏里点击"文件"选项&#xff0c;然后选择"高级保存选项"来设置文件格式&#xff0c;但是我的文件选项里没有"高级保存…

COT报告:美国期货市场持仓情况分析

市场情绪的晴雨表 COT报告揭示了美国期货市场中不同参与者的持仓情况&#xff0c;尤其是净多头和净空头头寸。这份报告发布时会有约三天的滞后&#xff0c;因此提供的是过往数据。通常&#xff0c;该报告在星期五发布&#xff0c;反映的是截至前一星期二的数据&#xff0c;因此…

蓝牙地址BD Addr烧录

关于蓝牙地址,有很多文章有介绍,主要要知道下面一个图: 蓝牙设备地址(或BD_ADDR)是制造商分配给每个蓝牙设备的唯一48位标识符。蓝牙地址通常显示为以十六进制书写的6个字节,用冒号分隔(例如-00:11:22:FF:EE)。蓝牙地址的上半部分(最重要的24位)被称为组织唯一标识符…

MASM32+ HTML JavaScript,好搭档

哪个编程工具让你的工作效率翻倍&#xff1f; 在日益繁忙的工作环境中&#xff0c;选择合适的编程工具已成为提升开发者工作效率的关键。不同的工具能够帮助我们简化代码编写、自动化任务、提升调试速度&#xff0c;甚至让团队协作更加顺畅。那么&#xff0c;哪款编程工具让你…

「Python数据分析」Pandas进阶,使用merge()函数合并数据

在使用python语言进行数据分析的过程中&#xff0c;我们的数据&#xff0c;有很大一部分是结构化数据&#xff0c;也就是比较整齐的数据。 这里&#xff0c;我不展开讲什么是结构化数据&#xff0c;因为这个范围太过于庞大。但是&#xff0c;有一个知识点&#xff0c;必须要讲…

前端代码注释风格 - CSS篇

本文基于《阿里巴巴CSS编程规约》、stylelint rules进行编写&#xff0c;涉及预编译语言&#xff08;Sass、Less&#xff09;的编码风格和最佳实践。 1.1 编码风格 空格的使用 选择器和{之间保留一个空格。.selector-disabled { 在使用逗号分隔的属性中&#xff0c;逗号后保…

Python检测和识别车牌-python经典练手项目

车牌检测与识别技术用途广泛&#xff0c;可以用于道路系统、无票停车场、车辆门禁等。这项技术结合了计算机视觉和人工智能。 本文将使用Python创建一个车牌检测和识别程序。该程序对输入图像进行处理&#xff0c;检测和识别车牌&#xff0c;最后显示车牌字符&#xff0c;作为…

专利复现_基于ngboost和SHAP值可解释预测方法

大家好&#xff0c;我是重庆未来之智的Toby老师&#xff0c;最近看到一篇专利&#xff0c;名称是《基于NGBoost和SHAP值的可解释地震动参数概率密度分布预测方法》。该专利申请工日是2021年3月2日。 专利复现 我看了这专利申请文案后&#xff0c;文章整体布局和文字内容结构不错…

c++修炼之路之C++11

目录 一&#xff1a;使用列表初始化 二&#xff1a;decltype和nullptr 三&#xff1a;右值引用和移动语义 四&#xff1a;新的类功能 五&#xff1a;可变参数模板 六&#xff1a;lambda表达式 七&#xff1a;包装器 1.function包装器 2.bind包装器 接下来的日子会顺…

《深度学习》OpenCV 图像轮廓检测、轮廓处理及代码演示

目录 一、图像轮廓检测 1、边缘检测和轮廓检测 2、常用的图像轮廓检测方法包括&#xff1a; 1&#xff09;基于梯度的方法 2&#xff09;基于边缘检测器的方法 3&#xff09;基于阈值的方法 3、查找轮廓的函数 4、轮廓的绘制 5、轮廓特征 1&#xff09;轮廓面积 2&a…

呵,老板不过如此,SQL还是得看我

2018年7月&#xff0c;大三暑假进行时&#xff0c;时间过得飞快&#xff0c;我到这边实习都已经一个月了。 我在没工作之前&#xff0c;我老是觉得生产项目的代码跟我平时自学练的会有很大的区别。 以为生产项目代码啥的都会规范很多&#xff0c;比如在接口上会做很多安全性的…

自己开发完整项目一、登录功能-05(动态权限控制)

一、上节回顾 在上一节中&#xff0c;我们介绍了如何通过数据库查询用户的权限&#xff0c;并对方法级别的接口使用注解的方式进行权限控制&#xff0c;之后通过用户携带的tocken进行解析权限&#xff0c;判断是否可以访问。 具体步骤&#xff1a; 1.在查询用户信息的时候将用户…

神经网络中激活函数介绍、优缺点分析

本文主要介绍神经网络中的常用的激活函数 激活函数是神经网络中用于引入非线性模型&#xff0c;提升模型泛化能力的函数 非线性激活函数至关重要&#xff0c;它可以让神经网络学习复杂特征、提供模型复杂度 1、激活函数定义 激活函数是神经网络模型中的一种非线性函数&#xf…

教学能力知识

第一章课程理论知识 一、课程理念 二、课程目标 1.核心素养 2.课程总目标 三、教学建议 四、教学环节 第二章教学实施 第一节导入新课类 二.导入方法 第二节教学方法类 教学方法的选择依据 第三节教法实施原则类 设计意图 第四节设计意图类 1.教学目标 2.教学重难点 3.教学…

【安当产品应用案例100集】014-使用安当TDE实现达梦数据库实例文件的透明加密存储

随着数据安全重要性的不断提升&#xff0c;数据库文件的落盘加密已成为数据保护的一项基本要求。达梦数据库作为一款高性能的国产数据库管理系统&#xff0c;为用户提供了一种高效、安全的数据存储解决方案。本文将详细介绍如何利用安当KSP密钥管理平台及TDE透明加密组件来实现…

[数据集][目标检测]灭火器检测数据集VOC+YOLO格式3255张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;3255 标注数量(xml文件个数)&#xff1a;3255 标注数量(txt文件个数)&#xff1a;3255 标注…

c++多线程下崩溃一例分析 ACTIONABLE_HEAP_CORRUPTION heap failure block not busy DOUBLE

之前的三个代码接口使用了同一把锁&#xff0c;共享资源的访问是有序执行的没有问题。最近改成各个接口使用单独的锁&#xff0c;结果漏掉了共享资源的保护&#xff0c;于是出现了崩溃。最近与这个崩溃做斗争并定位找到的原因&#xff0c;成功复现了。这里总结下&#xff0c;后…

[YM]课设-C#-WebApi-Vue-员工管理系统 (六)前后端交互

Http状态码&#xff1a; 终于也是到了前端 上文提到http状态码 这里详细说一下 1xx 表示临时响应并需要请求者继续执行操作 2xx 成功&#xff0c;操作被成功接收并处理 3xx 表示要完成请求&#xff0c;需要进一步操作。 通常&#xff0c;这些状态代码用来重定向 4…

LiveQing视频点播流媒体RTMP推流服务用户手册-分屏展示:单分屏、四分屏、九分屏、十六分屏、轮巡播放、分组管理、记录加载

LiveQing视频点播流媒体RTMP推流服务用户手册-分屏展示:单分屏、四分屏、九分屏、十六分屏、轮巡播放、分组管理、记录加载 1、分屏展示1.1、分组管理1.1.1、新建分组1.1.2、选择资源1.1.3、编辑分组1.1.4、删除资源 1.2、多分屏1.2.1、选择资源1.2.2、单分屏1.2.3、四分屏1.2.…