学习笔记 -- 从零开始学习Android Camera2 -- (1)

本文介绍了如何使用Android的Camera2框架,从获取CameraManager到创建CameraDevice,建立CaptureSession,以及设置预览和拍照的流程。重点在于理解Camera2的基础步骤,包括设备枚举、Surface设置、捕获请求创建等,并提供了相关代码示例。
摘要由CSDN通过智能技术生成

一.仿写流程

学习一个框架,第一步学习肯定是照着代码看文档。 既然要看代码,就要看最权威的,这里我是代码是参照https://github.com/android/camera-samples android给的官方示例,结合官方文档https://developer.android.com/reference/android/hardware/camera2/package-summary来看,所以首先要先看一遍文档,然后重写一遍它里面最基础的Camera2Basic。

文档里面写了啥

第一: android.hardware.camera2是在API level 21加上的,21以下的设备不能用,Camera2是为了替换deprecated的Camera类

第二:使用Camera2的步骤
1. 首先要获取一个CameraManager对象,这个是所有操作的前提。我们可以通过这个对象来进行枚举Camera设备的操作,也可以通过CameraManager的getCameraCharacteristics(String)方法获取每个摄像机设备的信息,这些信息存放在CameraCharacteristics类中。
2. 然后我们还可以通过CameraManager获取每一个摄像头对应的CameraDevices对象,获取的方法是调用CameraManager的openCamera方法。每个CameraDevices都对应一个Andorid的Camera设备。
3. 下一步需要创建一个session,session中包含了一系列已经设置好宽高和格式的Surface,摄像机会通过这些宽高和格式来设置摄像头的输出。Surface可以从SurfaceView, SurfaceTexture , MediaCodec, MediaRecorder, Allocation, 和 ImageReader中来。 除此之外,创建session还需要构建一个包含各种摄像头参数和输出Surface的CaptureRequest。 其实每次调用拍照都会把CaptureRequest送到相机设备,而相机设备也是根据这个CaptureRequest来进行拍照,你可以在 CaptureRequest中设置白平衡,光圈大小,曝光时间等参数。
4. 最后根据需要,看是需要拍照(capture),还是摄像(repeating),拍照API的优先级比摄像的优先级高。如果调用多次,就把CaptureRequest送到相机设备中多次,调用capture就送一次。

文档不如代码看起来舒服,还是直接上代码。
代码结构如下

├── app
│   ├── build.gradle
│   └── src
│       ├── main
│       │   ├── AndroidManifest.xml
│       │   ├── java
│       │   │   └── com
│       │   │       └── example
│       │   │           └── android
│       │   │               └── camera2
│       │   │                   └── basic
│       │   │                       ├── CameraActivity.kt
│       │   │                       └── fragments
│       │   │                           ├── CameraFragment.kt
│       │   │                           ├── ImageViewerFragment.kt
│       │   │                           ├── PermissionsFragment.kt
│       │   │                           └── SelectorFragment.kt
│       │   └── res
│       │       ├── layout
│       │       │   ├── activity_camera.xml
│       │       │   └── fragment_camera.xml
│       │       ├── navigation
│       │       │   └── nav_graph.xml
└── utils
    └── src
        └── main
            ├── AndroidManifest.xml
            ├── java
            │   └── com
            │       └── example
            │           └── android
            │               └── camera
            │                   └── utils
            │                       ├── AutoFitSurfaceView.kt
            │                       ├── CameraSizes.kt
            │                       ├── ExifUtils.kt
            │                       ├── GenericListAdapter.kt
            │                       ├── OrientationLiveData.kt
            │                       ├── Yuv.kt
            │                       └── YuvToRgbConverter.kt

CameraActivity用的是nav_graph.xml来进行导航,CameraActivity本身并不含任何的逻辑和UI,基本上所有的逻辑代码都是在各个Fragment中写的。

首先是PermissionFramgment,这个Fragment没有什么,就是请求Camera权限,请求完权限后就会导航到SelectorFragment。

SelectorFragment


 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        view as RecyclerView
        view.apply {
            layoutManager = LinearLayoutManager(requireContext())

			//这里就是文档中提到的第一步,先获取CameraManager对象
            val cameraManager =
                    requireContext().getSystemService(Context.CAMERA_SERVICE) as CameraManager

			// 调用enumerateCameras方法,获取一个List<FormatItem>
            val cameraList = enumerateCameras(cameraManager)

			// 下面就是初始化RecyclerView中的一部分
            val layoutId = android.R.layout.simple_list_item_1
            adapter = GenericListAdapter(cameraList, itemLayoutId = layoutId) { view, item, _ ->
                view.findViewById<TextView>(android.R.id.text1).text = item.title
                view.setOnClickListener {
                    Navigation.findNavController(requireActivity(), R.id.fragment_container)
                    		//每个item点击之后,都会导航到CameraFragment,参数是cameraId和格式format。
                            .navigate(SelectorFragmentDirections.actionSelectorToCamera(
                                    item.cameraId, item.format))
                }
            }
        }

    }

上面来看,最重要的就是调用enumerateCameras来获取List,那么就看看enumerateCameras方法做了什么。

private fun enumerateCameras(cameraManager: CameraManager): List<FormatItem> {
            val availableCameras: MutableList<FormatItem> = mutableListOf()

            // Get list of all compatible cameras
            // 筛选符合要求的cameraId
            val cameraIds = cameraManager.cameraIdList.filter {
            	// 获取每个Camera的设备信息
                val characteristics = cameraManager.getCameraCharacteristics(it)
                // 看看这个设备是否满足Camera的最小功能集(深度摄像机不一定包含在里面)
                val capabilities = characteristics.get(
                        CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
                capabilities?.contains(
                        CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) ?: false
            }


            // Iterate over the list of cameras and return all the compatible ones
            // 下面就是把每个camera和其支持的format都分别加入到List<FormatItem>中,这里判断支持的格式就只有
            cameraIds.forEach { id ->
                val characteristics = cameraManager.getCameraCharacteristics(id)
                val orientation = lensOrientationString(
                        characteristics.get(CameraCharacteristics.LENS_FACING)!!)

                // Query the available capabilities and output formats
                val capabilities = characteristics.get(
                        CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!!
                val outputFormats = characteristics.get(
                        CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!.outputFormats

                // All cameras *must* support JPEG output so we don't need to check characteristics
                // 每个摄像机都支持JPEG输出(压缩后的格式)
                availableCameras.add(FormatItem(
                        "$orientation JPEG ($id)", id, ImageFormat.JPEG))

                // Return cameras that support RAW capability
                // 是否支持RAW(原始画质), 使用RAW_SENSOR的话,每种相机都不同
                if (capabilities.contains(
                                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_RAW) &&
                        outputFormats.contains(ImageFormat.RAW_SENSOR)) {
                    availableCameras.add(FormatItem(
                            "$orientation RAW ($id)", id, ImageFormat.RAW_SENSOR))
                }

                // Return cameras that support JPEG DEPTH capability
                // 深度相机,基本用不到
                if (capabilities.contains(
                            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT) &&
                        outputFormats.contains(ImageFormat.DEPTH_JPEG)) {
                    availableCameras.add(FormatItem(
                            "$orientation DEPTH ($id)", id, ImageFormat.DEPTH_JPEG))
                }
            }

            return availableCameras
        }

所以SelectorFragment中基本就是获取cameraId和format,然后交给CameraFragment来使用,那么看来,最重要的代码还是在CameraFragment中实现的,所以接下来看看CameraFragment是怎么实现的。

 fragmentCameraBinding.viewFinder.holder.addCallback(object : SurfaceHolder.Callback {
            override fun surfaceDestroyed(holder: SurfaceHolder) = Unit

            override fun surfaceChanged(
                    holder: SurfaceHolder,
                    format: Int,
                    width: Int,
                    height: Int) = Unit

			// 这个时候Surface已经创建了
            override fun surfaceCreated(holder: SurfaceHolder) {
                // Selects appropriate preview size and configures view finder
                
                //获取最优的预览大小,小于1080p和屏幕尺寸下最大的摄像头拍摄分辨率
                //这里就是从characteristics.getCameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP).getOutputSizes(SurfaceHolder::class.java)中获取Size数组,这个数据就是摄像头的输出分辨率
                val previewSize = getPreviewOutputSize(
                    fragmentCameraBinding.viewFinder.display,
                    characteristics,
                    SurfaceHolder::class.java
                )
                Log.d(TAG, "View finder size: ${fragmentCameraBinding.viewFinder.width} x ${fragmentCameraBinding.viewFinder.height}")
                Log.d(TAG, "Selected preview size: $previewSize")
                // 根据要得到的摄像头分辨率设置 surface分辨率
                // 这个surface要用来创建session,就是上面的第三步,然后摄像机就可以根据这个surface大小来选择拍照的分辨率。
                // 这样就能保证预览的宽高比,与摄像头一致,就可以保证预览没有进行拉伸
                fragmentCameraBinding.viewFinder.setAspectRatio(
                    previewSize.width,
                    previewSize.height
                )

                // To ensure that size is set, initialize camera in the view's thread
                view.post { initializeCamera() }
            }
        })

其实上面最关键的就是计算出想要的摄像头分辨率,然后把对应的宽高比传给View,View在进行调整,调整到和摄像头输出的宽高比一致,这样就不会造成预览画面弯曲。计算的过程需要得到对应CameraDevice的OutputSize数组。
然后通过View调用initializeCamera,初始化摄像头,这样就能保证Surface的宽高比已经改变了,摄像头也会根据这个Surface来初始化摄像头的分辨率。

initializeCamera方法是整个Demo中最关键的部分

private fun initializeCamera() = lifecycleScope.launch(Dispatchers.Main) {
        // Open the selected camera
        // 这里面实现也简单,就是我们在文档第二步做所,用cameraManager根据cameraId来创建CameraDevice
        camera = openCamera(cameraManager, args.cameraId, cameraHandler)

        // Initialize an image reader which will be used to capture still photos
        //设置输出的格式
        val size = characteristics.get(
                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
                .getOutputSizes(args.pixelFormat).maxByOrNull { it.height * it.width }!!
        imageReader = ImageReader.newInstance(
                size.width, size.height, args.pixelFormat, IMAGE_BUFFER_SIZE)

        // Creates list of Surfaces where the camera will output frames
        // 设置到Request中输出的Surface,第三步中所说
        val targets = listOf(fragmentCameraBinding.viewFinder.holder.surface, imageReader.surface)

        // Start a capture session using our open camera and list of Surfaces where frames will go
        // 创建Session,第三步中所说
        // createCaptureSession中也简单,就是调用CameraDevice对应的createCaptureSession方法
        session = createCaptureSession(camera, targets, cameraHandler)

        val captureRequest = camera.createCaptureRequest(
                CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(fragmentCameraBinding.viewFinder.holder.surface) }

        // This will keep sending the capture request as frequently as possible until the
        // session is torn down or session.stopRepeating() is called
        // 这里就是第四步,调用setRepeatingRequest获取一个持续的图片流
        session.setRepeatingRequest(captureRequest.build(), null, cameraHandler)

        // Listen to the capture button
        fragmentCameraBinding.captureButton.setOnClickListener {

            // Disable click listener to prevent multiple requests simultaneously in flight
            it.isEnabled = false

            // Perform I/O heavy operations in a different scope
            lifecycleScope.launch(Dispatchers.IO) {
                // takePhoto方法是单独拍照
                takePhoto().use { result ->
                    Log.d(TAG, "Result received: $result")

                    // Save the result to disk
                    val output = saveResult(result)
                    Log.d(TAG, "Image saved: ${output.absolutePath}")

                    // If the result is a JPEG file, update EXIF metadata with orientation info
                    if (output.extension == "jpg") {
                        val exif = ExifInterface(output.absolutePath)
                        exif.setAttribute(
                                ExifInterface.TAG_ORIENTATION, result.orientation.toString())
                        exif.saveAttributes()
                        Log.d(TAG, "EXIF metadata saved: ${output.absolutePath}")
                    }

                    // Display the photo taken to user
                    lifecycleScope.launch(Dispatchers.Main) {
                        navController.navigate(CameraFragmentDirections
                                .actionCameraToJpegViewer(output.absolutePath)
                                .setOrientation(result.orientation)
                                .setDepth(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
                                        result.format == ImageFormat.DEPTH_JPEG))
                    }
                }

                // Re-enable click listener after photo is taken
                it.post { it.isEnabled = true }
            }
        }
    }

上面的流程就走完了调用一个摄像头的全部流程,主要就是先根据CameraId,调用CameraManager中的方法创建CameraDevice对象,然后根据这个CameraDevice对象,创建一个CameraCaptureSession对象,用这个对象调用setRepeatingRequest方法。
除此之外,还有一个takePhoto方法,这个方法就是在预览摄像头的同时进行拍照用的,看一下这个方法的具体实现。

    private suspend fun takePhoto():
            CombinedCaptureResult = suspendCoroutine { cont ->

        // Flush any images left in the image reader
        @Suppress("ControlFlowWithEmptyBody")
        while (imageReader.acquireNextImage() != null) {
        }

        // Start a new image queue
        val imageQueue = ArrayBlockingQueue<Image>(IMAGE_BUFFER_SIZE)
        imageReader.setOnImageAvailableListener({ reader ->
            val image = reader.acquireNextImage()
            Log.d(TAG, "Image available in queue: ${image.timestamp}")
            imageQueue.add(image)
        }, imageReaderHandler)

		// TEMPLATE_STILL_CAPTURE会优先保证拍出来的画面质量,而不是帧数
		// 这个时候预览的Surface会有一个白色动画,所以其实也没必要保证帧数
        val captureRequest = session.device.createCaptureRequest(
                CameraDevice.TEMPLATE_STILL_CAPTURE).apply { addTarget(imageReader.surface) }
        session.capture(captureRequest.build(), object : CameraCaptureSession.CaptureCallback() {

            override fun onCaptureStarted(
                    session: CameraCaptureSession,
                    request: CaptureRequest,
                    timestamp: Long,
                    frameNumber: Long) {
                super.onCaptureStarted(session, request, timestamp, frameNumber)
                fragmentCameraBinding.viewFinder.post(animationTask)
            }

            override fun onCaptureCompleted(
                    session: CameraCaptureSession,
                    request: CaptureRequest,
                    result: TotalCaptureResult) {
                super.onCaptureCompleted(session, request, result)
                val resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP)
                Log.d(TAG, "Capture result received: $resultTimestamp")

                // Set a timeout in case image captured is dropped from the pipeline
                val exc = TimeoutException("Image dequeuing took too long")
                val timeoutRunnable = Runnable { cont.resumeWithException(exc) }
                imageReaderHandler.postDelayed(timeoutRunnable, IMAGE_CAPTURE_TIMEOUT_MILLIS)

                // Loop in the coroutine's context until an image with matching timestamp comes
                // We need to launch the coroutine context again because the callback is done in
                //  the handler provided to the `capture` method, not in our coroutine context
                @Suppress("BlockingMethodInNonBlockingContext")
                lifecycleScope.launch(cont.context) {
                    while (true) {

                        // Dequeue images while timestamps don't match
                        val image = imageQueue.take()
                        // TODO(owahltinez): b/142011420
                        // if (image.timestamp != resultTimestamp) continue
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
                                image.format != ImageFormat.DEPTH_JPEG &&
                                image.timestamp != resultTimestamp) continue
                        Log.d(TAG, "Matching image dequeued: ${image.timestamp}")

                        // Unset the image reader listener
                        imageReaderHandler.removeCallbacks(timeoutRunnable)
                        imageReader.setOnImageAvailableListener(null, null)

                        // Clear the queue of images, if there are left
                        while (imageQueue.size > 0) {
                            imageQueue.take().close()
                        }

                        // Compute EXIF orientation metadata
                        val rotation = relativeOrientation.value ?: 0
                        val mirrored = characteristics.get(CameraCharacteristics.LENS_FACING) ==
                                CameraCharacteristics.LENS_FACING_FRONT
                        val exifOrientation = computeExifOrientation(rotation, mirrored)

                        // Build the result and resume progress
                        cont.resume(CombinedCaptureResult(
                                image, result, exifOrientation, imageReader.imageFormat))

                        // There is no need to break out of the loop, this coroutine will suspend
                    }
                }
            }
        }, cameraHandler)
    }

其他的就不是很关键,从这个库我们能学到摄像机的基本应用流程。

根据上面这个库,自己搞一下整个摄像机的流程,如下

private void initCameraParam() {
		//检测相机权限
        if (!checkPermission())
            return;
        //获取CameraManager对象
        mCameraManager = (CameraManager) (getSystemService(Context.CAMERA_SERVICE));
        if (mCameraManager == null)
            return;
        try {
            mData = new ArrayList<>();
            //这里我们直接使用了第一个摄像机设备ID
            CameraCharacteristics cc = mCameraManager.getCameraCharacteristics(mCameraManager.getCameraIdList()[0]);

			// 这个View继承SurfaceView,可以根据传入的分辨率自己调整View的大小,除此之外和SurfaceView一样
            AutoFitSurfaceView autoView = findViewById(R.id.camera_surface);
            
            autoView.getHolder().addCallback(new SurfaceHolder.Callback() {
            	//需要等到Surface创建后加入到
                @Override
                public void surfaceCreated(@NonNull SurfaceHolder holder) {
					//下面这一段就是根据View的初步大小和Camera设备能输出的大小进行匹配,找到一个最合适的
                    Display display = autoView.getDisplay();
                    Point point = new Point();
                    display.getRealSize(point);

                    SmartSize renderSize = new SmartSize(point.x, point.y);
                    SmartSize maxSize = new SmartSize(1920, 1080);

                    if (renderSize.getLongSize() <= maxSize.getLongSize() && renderSize.getShortSize() <= maxSize.getShortSize())
                        maxSize = renderSize;

                    StreamConfigurationMap config = cc.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                    Size[] sizes = config.getOutputSizes(SurfaceHolder.class);
                    SmartSize resultSize = new SmartSize(0, 0);
                    for (Size size: sizes) {
                        int longSize = Math.max(size.getWidth(), size.getHeight());
                        int shortSize = Math.min(size.getWidth(), size.getHeight());
                        if (resultSize.getLongSize() <= longSize && longSize <= maxSize.getLongSize() &&
                                resultSize.getShortSize() <= shortSize && shortSize <= maxSize.getShortSize()) {
                            resultSize = new SmartSize(longSize, shortSize);
                        }
                    }
                    if (resultSize.getShortSize() != 0 && resultSize.getLongSize() != 0)
                        autoView.setAspectRatio(resultSize.getLongSize(), resultSize.getShortSize());


					// 通过view.post 保证Surface的大小已经确定修改
                    autoView.post(() -> {
                        try {
                            if (!checkPermission())
                                return;
                            //打开摄像机 还是默认使用第一个设备ID
                            mCameraManager.openCamera(mCameraManager.getCameraIdList()[0], new CameraDevice.StateCallback() {
                                @Override
                                public void onOpened(@NonNull CameraDevice camera) {
                                    mCameraDevice = camera;
									//打开之后创建Session和Request
                                    OutputConfiguration outputConfiguration = new OutputConfiguration(autoView.getHolder().getSurface());
                                    CameraCaptureSession.StateCallback callback = new CameraCaptureSession.StateCallback() {
                                        @Override
                                        public void onConfigured(@NonNull CameraCaptureSession session) {
                                           //Session创建成功的回调,在这个回调中创建Request,然后调用setRepeatingRequest获取图像流
                                            mCameraCaptureSession = session;
                                            try {
                                                CaptureRequest.Builder builder = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                                                builder.addTarget(autoView.getHolder().getSurface());
                                                mCameraCaptureSession.setRepeatingRequest(builder.build(), new CameraCaptureSession.CaptureCallback() {
                                                    @Override
                                                    public void onCaptureStarted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, long timestamp, long frameNumber) {
                                                        super.onCaptureStarted(session, request, timestamp, frameNumber);
                                                    }

                                                    @Override
                                                    public void onCaptureProgressed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureResult partialResult) {
                                                        super.onCaptureProgressed(session, request, partialResult);
                                                    }

                                                    @Override
                                                    public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
                                                        super.onCaptureCompleted(session, request, result);
                                                    }

                                                    @Override
                                                    public void onCaptureFailed(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull CaptureFailure failure) {
                                                        super.onCaptureFailed(session, request, failure);
                                                    }

                                                    @Override
                                                    public void onCaptureSequenceCompleted(@NonNull CameraCaptureSession session, int sequenceId, long frameNumber) {
                                                        super.onCaptureSequenceCompleted(session, sequenceId, frameNumber);
                                                    }

                                                    @Override
                                                    public void onCaptureSequenceAborted(@NonNull CameraCaptureSession session, int sequenceId) {
                                                        super.onCaptureSequenceAborted(session, sequenceId);
                                                    }

                                                    @Override
                                                    public void onCaptureBufferLost(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull Surface target, long frameNumber) {
                                                        super.onCaptureBufferLost(session, request, target, frameNumber);
                                                    }
                                                }, mCameraHandler);
                                            } catch (Exception e) {
                                                e.printStackTrace();
                                            }
                                        }

                                        @Override
                                        public void onConfigureFailed(@NonNull CameraCaptureSession session) {
                                            Log.e(TAG, "onConfigureFailed: ");
                                        }
                                    };
                                    //这里是主动创建Session的地方
                                    try {
                                        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
                                            SessionConfiguration configuration = new SessionConfiguration(SessionConfiguration.SESSION_REGULAR,
                                                    Collections.singletonList(outputConfiguration),
                                                    new HandlerExecutor(mCameraHandler),
                                                    callback);
                                            mCameraDevice.createCaptureSession(configuration);
                                        } else {
                                            mCameraDevice.createCaptureSession(Collections.singletonList(autoView.getHolder().getSurface()),
                                                    callback, mCameraHandler);
                                        }
                                    } catch (Exception e) {
                                        e.printStackTrace();
                                    }

                                }

                                @Override
                                public void onDisconnected(@NonNull CameraDevice camera) {
                                    Log.e(TAG, "onDisconnected: ");
                                }

                                @Override
                                public void onError(@NonNull CameraDevice camera, int error) {
                                    String msg;
                                    switch (error) {
                                        case ERROR_CAMERA_DEVICE:
                                            msg = "Fatal (device)";
                                            break;
                                        case ERROR_CAMERA_DISABLED:
                                            msg = "Device policy";
                                            break;
                                        case ERROR_CAMERA_IN_USE:
                                            msg = "Camera in use";
                                            break;
                                        case ERROR_CAMERA_SERVICE:
                                            msg = "Fatal (service)";
                                            break;
                                        case ERROR_MAX_CAMERAS_IN_USE:
                                            msg = "Maximum cameras in use";
                                            break;
                                        default:
                                            msg = "Unknown";
                                            break;
                                    }
                                    Log.e(TAG, "onError: " + msg);

                                }
                            }, mCameraHandler);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    });
                }

                @Override
                public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {


                }

                @Override
                public void surfaceDestroyed(@NonNull SurfaceHolder holder) {

                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

以上,就基本根据一个Demo写了一个摄像头从创建到预览的全部过程,复写代码是为了更好的掌握结构,所以只是注重了流程,接下来会详细学习摄像头的一些参数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值