一.仿写流程
学习一个框架,第一步学习肯定是照着代码看文档。 既然要看代码,就要看最权威的,这里我是代码是参照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)
}
其他的就不是很关键,从这个库我们能学到摄像机的基本应用流程。
根据上面这个库,自己搞一下整个摄像机的流程,如下文章来源:https://www.toymoban.com/news/detail-437971.html
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写了一个摄像头从创建到预览的全部过程,复写代码是为了更好的掌握结构,所以只是注重了流程,接下来会详细学习摄像头的一些参数。文章来源地址https://www.toymoban.com/news/detail-437971.html
到了这里,关于学习笔记 -- 从零开始学习Android Camera2 -- (1)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!