Android Camera2 教程 · 第一章 · 概览

Android Camera2 教程 · 第一章 · 概览,第1张

从 Android 50 开始,Google 引入了一套全新的相机框架 Camera2(androidhardwarecamera2)并且废弃了旧的相机框架 Camera1(androidhardwareCamera)。作为一个专门从事相机应用开发的开发者来说,这一刻我等了太久了,Camera1 那寥寥无几的 API 和极差的灵活性早已不能满足日益复杂的相机功能开发。Camera2 的出现给相机应用程序带来了巨大的变革,因为它的目的是为了给应用层提供更多的相机控制权限,从而构建出更高质量的相机应用程序。本文是 Camera2 教程的开篇作,本章将介绍以下几个内容:

Camera2 的 API 模型被设计成一个 Pipeline(管道),它按顺序处理每一帧的请求并返回请求结果给客户端。下面这张来自官方的图展示了 Pipeline 的工作流程,我们会通过一个简单的例子详细解释这张图。

为了解释上面的示意图,假设我们想要同时拍摄两张不同尺寸的,并且在拍摄的过程中闪光灯必须亮起来。整个拍摄流程如下:

一个新的 CaptureRequest 会被放入一个被称作 Pending Request Queue 的队列中等待被执行,当 In-Flight Capture Queue 队列空闲的时候就会从 Pending Request Queue 获取若干个待处理的 CaptureRequest,并且根据每一个 CaptureRequest 的配置进行 Capture *** 作。最后我们从不同尺寸的 Surface 中获取数据并且还会得到一个包含了很多与本次拍照相关的信息的 CaptureResult,流程结束。

相机功能的强大与否和硬件息息相关,不同厂商对 Camera2 的支持程度也不同,所以 Camera2 定义了一个叫做 Supported Hardware Level 的重要概念,其作用是将不同设备上的 Camera2 根据功能的支持情况划分成多个不同级别以便开发者能够大概了解当前设备上 Camera2 的支持情况。截止到 Android P 为止,从低到高一共有 LEGACY、LIMITED、FULL 和 LEVEL_3 四个级别:

相机的所有 *** 作和参数配置最终都是服务于图像捕获,例如对焦是为了让某一个区域的图像更加清晰,调节曝光补偿是为了调节图像的亮度。因此,在 Camera2 里面所有的相机 *** 作和参数配置都被抽象成 Capture(捕获),所以不要简单的把 Capture 直接理解成是拍照,因为 Capture *** 作可能仅仅是为了让预览画面更清晰而进行对焦而已。如果你熟悉 Camera1,那你可能会问 setFlashMode() 在哪? setFocusMode() 在哪? takePicture() 在哪?告诉你,它们都是通过 Capture 来实现的。

Capture 从执行方式上又被细分为单次模式、多次模式和重复模式三种,我们来一一解释下:

CameraManager 是一个负责查询和建立相机连接的系统服务,它的功能不多,这里列出几个 CameraManager 的关键功能:

CameraCharacteristics 是一个只读的相机信息提供者,其内部携带大量的相机信息,包括代表相机朝向的 LENS_FACING ;判断闪光灯是否可用的 FLASH_INFO_AVAILABLE ;获取所有可用 AE 模式的 CONTROL_AE_AVAILABLE_MODES 等等。如果你对 Camera1 比较熟悉,那么 CameraCharacteristics 有点像 Camera1 的 CameraCameraInfo 或者 CameraParameters 。

CameraDevice 代表当前连接的相机设备,它的职责有以下四个:

熟悉 Camera1 的人可能会说 CameraDevice 就是 Camera1 的 Camera 类,实则不是,Camera 类几乎负责了所有相机的 *** 作,而 CameraDevice 的功能则十分的单一,就是只负责建立相机连接的事务,而更加细化的相机 *** 作则交给了稍后会介绍的 CameraCaptureSession。

Surface 是一块用于填充图像数据的内存空间,例如你可以使用 SurfaceView 的 Surface 接收每一帧预览数据用于显示预览画面,也可以使用 ImageReader 的 Surface 接收 JPEG 或 YUV 数据。每一个 Surface 都可以有自己的尺寸和数据格式,你可以从 CameraCharacteristics 获取某一个数据格式支持的尺寸列表。

CameraCaptureSession 实际上就是配置了目标 Surface 的 Pipeline 实例,我们在使用相机功能之前必须先创建 CameraCaptureSession 实例。一个 CameraDevice 一次只能开启一个 CameraCaptureSession,绝大部分的相机 *** 作都是通过向 CameraCaptureSession 提交一个 Capture 请求实现的,例如拍照、连拍、设置闪光灯模式、触摸对焦、显示预览画面等等。

CaptureRequest 是向 CameraCaptureSession 提交 Capture 请求时的信息载体,其内部包括了本次 Capture 的参数配置和接收图像数据的 Surface。CaptureRequest 可以配置的信息非常多,包括图像格式、图像分辨率、传感器控制、闪光灯控制、3A 控制等等,可以说绝大部分的相机参数都是通过 CaptureRequest 配置的。值得注意的是每一个 CaptureRequest 表示一帧画面的 *** 作,这意味着你可以精确控制每一帧的 Capture *** 作。

CaptureResult 是每一次 Capture *** 作的结果,里面包括了很多状态信息,包括闪光灯状态、对焦状态、时间戳等等。例如你可以在拍照完成的时候,通过 CaptureResult 获取本次拍照时的对焦状态和时间戳。需要注意的是,CaptureResult 并不包含任何图像数据,前面我们在介绍 Surface 的时候说了,图像数据都是从 Surface 获取的。

如果要我给出强有力的理由解释为什么要使用 Camera2,那么通过 Camera2 提供的高级特性可以构建出更加高质量的相机应用程序应该是最佳理由了。

如果你熟悉 Camera1,并且打算从 Camera1 迁移到 Camera2 的话,希望以下几个建议可以对你起到帮助:

本章到此结束,主要是介绍了 Camera2 的一些基础概念,让大家能够基本了解 Camera2 的工作流程和基础概念,并且知道使用 Camera2 能够做些什么。如果你对 Camera2 还是感到很陌生,不要紧,后续的教程会带领大家逐步深入了解 Camera2。

Google针对新的同步机制,在BBQ对象JAVA层面设计了一系列功能接口,列举功能更新较大几个接口:

提供用于下一次缓冲区要更新的事务。 BBQ 不会立即提交此事务,通过该接口将下一帧的提交控制在调用者手中,调用者可以将其用于更高级别的同步。

将传入的事务合并到 BBQ 中的下一个事务。 当具有指定帧号的下一帧可用时,将直接与含有Buffer的事务进行合并并提交。

客户端可以监听Buffer的合成状态,在 SurfaceFlinger 中已应用包含带有 framenumber 的缓冲区的事务时触发回调,通知客户端合成完成。

与之前Android版本不同的是,Surface对象的创建、Buffer size与Surface size的更新也支持直接通过BBQ进行 *** 作。

Android 12 Google将BufferQueue(简称BQ)组件从SF端移动到了客户端,BQ组件的初始化也放在BBQ的初始化中。通过类名可以看出BBQ更像是BQ的装饰者,在BQ本来功能特性的基础上添加了同步的功能。

通过官图大概了解,整个生产消费模型都在客户端,图形缓冲区的出队、入队、获取等 *** 作都在客户端完成,预示着生产着模型从远程通讯变成了本地通讯, 消费者监听器也从SF端的 ContentsChangedListener 。带来的改变就是客户端需要通过事务Transaction来向SF端提交Buffer与图层的属性。

接下来以应用显示流程为例,梳理下BBQ的初始化流程:

应用端通过方法 relayoutWindow 向WMS服务申请窗口布局,创建应用对应SurfaceControl,随后根据SurfaceControl创建BlastBufferQueue:

frameworks / base / core / java / android / view / ViewRootImpljava

BBQ主要核心逻辑的初始化都放在了Native对象的构造函数,做了以下几件事:

frameworks / native / libs / gui / BLASTBufferQueuecpp

frameworks / native / libs / gui / BLASTBufferQueuecpp

BLASTBufferItemConsumer (简称BBIC)继承自 ConsumerBase ,创建BBIC的同时,消费者模型与消费者监听器建立起了连接:

frameworks / native / libs / gui / ConsumerBasecpp

frameworks / native / libs / gui / BufferQueueConsumercpp

这一步也就让 BBIC 建立了对Buffer状态的监听。接下来看BBQ如何有选择性的监听Buffer的状态。

BBIC 拥有监听Buffer所有状态的能力,BBQ对Buffer特定状态的监听离不开 BBIC,因此,BBQ 继承了两个抽象类 ConsumerBase BufferItemConsumer ,分别针对 Buffer 消费状态与生产状态进行监听。

frameworks / native / libs / gui / BufferQueueConsumercpp

BBQ初始化完成,消费者模型建立完成,由于BBQ动态监听缓冲区的状态,如果有可消费的缓冲区,BBQ会触发缓冲区的事务提交:

通过梳理BBQ的初始化,对消费者端的大概流程有了一定的认识,接下来梳理下生产者方的代表,也就是Surface。Android 显示的的内容来源于各种绘制模块,而这些绘制模块需要与BQ建立连接,获取Buffer用以绘制,这样才能将绘制的画像通过BBQ提交给SF合成。Surface作为生产者模型与绘制模块之间桥梁,相关的流程掌握显得尤为重要。

绘制模块指的是那些图像生产者,如以使用SurfaceView、GlSurfaceView、TextureView控件为代表的Video模块、Camera模块、游戏应用等,以及使用软件绘制、硬件加速绘制为代表的普通控件。

回到创建BBQ的流程,在ViewRootImplgetOrCreateBLASTSurface方法中,创建完BBQ,紧接着会创建Surface对象,直接看Native 对象的构造函数:

首先Surface的创建会传入生产者模型 GraphicBufferProducer ,这样Surface对象拥有了 *** 作缓冲区的能力,同时在构造函数中Surface提供了一系列hook为首的函数,连接到 ANativeWindow 的函数指针,为的是给EGL模块提供对缓冲区 *** 作的入口。而hook函数会直接调用内部的本地函数,以 hook_queueBuffer 为例:

同时软件绘制不需要通过hook函数来中转,当上层通过SurfacelockCanvas方法获取画布时会直接调用本地函数函数 Surface::dequeueBuffer

Surface只是绘制的中介,还需要与绘制模块进行连接后,绘制模块才能获取缓冲区和绘制图像数据,关于绘制模块如何连接到Surface,这里不做记录。

结合第一节的关于BBQ 重点API功能介绍与BBQ的初始化流程,回过头看下这三个API功能是如何实现的。

首先看 setNextTransaction 函数,调用者通过该接口可以实现将当前帧 Buffer 的提交权利控制在自己手中,同时可以加入其他图层想要的更新,然后提交,放在同一帧生效。可以思考下,如果当前帧的控制权交给了调用者,是否会导致下一帧的紊乱呢?看下这块流程:

这里BBQ做了线程阻塞的机制,当绘制模块绘制完成下一帧,并将Buffer放回了缓冲区队列,触发BBQ的 onFrameAvailable 回调,如果调用者使用了 setNextTransaction 函数传入了自定义事务,那么就会在 onFrameAvailable 函数中阻塞住线程, 暂停执行下一帧的 processNextBufferLocked 。而唤醒线程的任务交给了 releaseBufferCallback 函数。

当前帧会执行绘制提交函数 processNextBufferLocked ,但是不会立即提交,会将事务控制在自己手中。可以看到, releaseBufferCallback 的回调函数会通过 t->setBuffer传递到SF端。

也就是说当调用者主动提交事务后,SF端合成完成后会回调该通知,唤醒线程。否则会一直阻塞等待调用者提交。

大概流程如图示:

根据BBQ相关文档提示:

该机制在同步单个帧时阻塞在 UI 线程中很好,但在尝试同步多个帧时效果不佳。 它最终会减慢渲染速度。 相反,在 RenderThread 级别处理同步以允许 UI 线程继续处理帧

因此多帧同步还是有优化空间。

将调用者传入的事务合并到 BBQ 中的下一个事务。 当具有指定帧号的下一帧可用时,将直接与含有Buffer的事务进行合并并提交。也就是说将调用者事务所包含的其他对图层属性的更新合入到BBQ的事务中,与BBQ的事务在指定帧数一同生效。这个怎么实现的呢?

这个函数会将调用者传入的事务都保存在 mPendingTransactions 集合中,当执行到下一帧的

processNextBufferLocked 函数时,将集合中的事务都合入到BBQ事务中,然后直接提交:

大概流程如图示:

客户端可以监听Buffer的合成状态,在 SurfaceFlinger 中已应用包含带有 frameNumber 的缓冲区的事务时触发回调,通知调用者合成完成。

通过 t->addTransactionCompletedCallback 将 transactionCallbackThunk 回调函数传给了SF,当合成完成会触发回调,并通知调用者状态。

根据上面流程的梳理,用一张图总结下BBQ与相关模块之间的结构关系:

如果你有兴趣为Android平台开发游戏,有很多你需要了解的东西。如果你有过游戏开发经验,那么转移到移动平台上来将不是特别困难。你主要只需学习其架构以及API就行了。如果你是一名游戏开发新手,我总结了一张列表,上面有你必需知道的东西,供你起步用。这些知识适用于很多类型的游戏,包括动作类、策略类、模拟类和益智类。Android是一个基于Java的环境。这对初学者来说是个好消息,因为相对于C,Java被广泛认为是一门更容易上手的语言,它是移动开发的规范。Google也做了一件出色的工作,它将API文档化并提供示例代码供使用。其中有个叫做APIDemos的示例几乎展示了所有API的功能。如果你熟悉Java并且用过Eclipse,要让你的第一个应用跑起来那是相当简单。如果你以前从没写过代码,在你前进路上还要学习很多,但别气馁。

获取SDK

新手上路的第一步便是获取AndroidSDK(软件开发工具包)。SDK里有一个核心类库,一个模拟器,一些工具和示例代码。我强烈建议使用Eclipse和AndroidEclipse插件。如果你玩Android的话,EclipseIDE对Java开发者来说很好用。如果这是你第一次开发Java项目,你可能会需要下载全套JDK,它里面包括签名和部署你的应用程序的一些工具。

学习应用程序架构

别急着一头扎进开发的海洋里,理解Android应用程序架构是很重要的。如果你不学一下,你设计出来的游戏在线下将很难调试。你将需要理解、Activities、Intents以及它们怎样相互联系。Google提供了很多有用的架构信息。真正重要的是要理解为什么你的游戏需要多于一个的Activity,以及什么才是设计一个有良好用户体验的游戏。要理解这些,首先要了解什么是Activity生命周期。

学习Activity生命周期

Activity生命周期由Android *** 作系统来管理。你的activity创建、恢复、暂停、销毁都受 *** 作系统的支配。正确处理这些事件是很重要的,这样应用程序才能表现良好,做用户认为正确的事。在你设计你的游戏之前了解所有这些是如何工作的是件好事,因为以后你可以为自己节省调试时间和昂贵的重新设计时间。对大多数应用来说,默认的设置将工作正常,但对于游戏,你可能需要考虑将标志打开。当设置为默认时,Android在它认为合适时会创建activity的新实例。对于游戏来说,你可能只需要一个游戏activity的实例。这对于你要怎样管理事务的状态有些影响,但对于我来说,这解决了一些资源管理的问题,应予以考虑。

主循环

根据你写的游戏的类型,你可能需要也可能不需要一个主循环。如果你的游戏不依赖于时间或者它仅仅对用户所做的加以回应,并且不做任何视觉上的改变,永远等待着用户的输入,那么你就不需要主循环。如果你写的是动作类游戏或者带有动画、定时器或任何自动 *** 作的游戏,你应该认真考虑下使用主循环。

游戏的主循环以一个特定的顺序通常尽可能多的在每秒钟内“滴答”提醒子系统运行。你的主循环需要在它自己的线程里运行,原因是Android有一个主用户界面线程,如果你不运行自己的线程,用户界面线程将会被你的游戏所阻塞,这会导致Android *** 作系统无法正常的更新任务。执行的顺序通常如下:状态,输入,人工智能,物理,动画,声音,录像。

更新状态意思是管理状态转换,例如游戏的结束、人物的选择或下一个级别。很多时候你需要在某个状态上等上几秒钟,而状态管理应该处理这种延迟,并且在时间过了之后设置成下一个状态。

输入是指用户按下的任何键、对于滚动条的移动或者用户的触摸。在处理物理之前处理这些是很重要的,因为很多时候输入会影响到物理层,因而首先处理输入将会使游戏的反应更加良好。在Android里,输入事件从主用户界面线程而来,因此你必须写代码将输入放入缓冲区,这样你的主循环可以在需要的时刻就从缓冲区里取到它。这并非难事。首先为下一个用户输入定义一个域,然后将或函数设为接到一个用户动作就放到那个域里,有这两步就够了。如果对于给定游戏的状态,这是一个合法的输入 *** 作,那么所有输入需要在那一刻做的更新 *** 作都已经定下来了,剩下来就让物理去关心怎样响应输入吧。

人工智能所做的类似于用户在决定下一个要“按”哪个按钮。学习怎样写人工智能程序超出了这篇文章的范围,但大体的意思是人工智能会按照用户的意图来按按钮。这些也有待物理去处理和响应吧。

物理可能是也可能不是真正的物理。对于动作类游戏来说,关键点是要考虑到上一次更新的时间、正在更新的当前时间、用户输入以及人工智能,并且决定它们朝着什么方向发展和是否会发生冲突。对于一个你可视化地抓取一些部件并滑动它们的游戏来说,物理就是这个游戏中滑动部件或者使之放入合适的位置的部分。对于一个小游戏来说,物理即使这个游戏中决定答案是错还是对的部分。你可能将其命名为其他东西,但每个游戏都有一个作为游戏引擎的红肉部分(译者注:可能是主体部分的意思),在这篇文章里,我把这部分称为物理。

动画并非像在游戏里放入会动的gif那样简单。你需要使得游戏能在恰当的时间画出每一帧。这并没有听起来那么困难。保留一些像isDancing、danceFrame和那样的状态域,那样动画更新便能决定是否可以切换到下一帧去了。动画更新真正做的事就那么多。真正来显示动画的变化是由录像更新来处理的。

声音更新要处理触发声音、停止声音、音量变化以及音调变化。正常情况下当写游戏的时候,声音更新会产生一些传往声音缓冲区的字节流,但是Android能够管理自己的声音,因而你的选择将是使用SoundPool或者MediaPlayer。它们都需要小心处理以免出错,但你要知道,因为一些底层实现细节,小型、低比特率的声音文件将带来最佳的性能和稳定性。

录像更新要考虑游戏的状态、角色的位置、分数、状态等等,并将一切画到屏幕上。如果使用主循环,你可能需要使用SurfaceView,并做一个“推”绘制。对于其他视图,视图本身能够调用绘制 *** 作,主循环不必处理。SurfaceView每秒产生的帧数最多,最适合于一些有动画或屏幕上有运动部件的游戏。录像更新所要做的工作是获取游戏的状态,并及时地为这个状态绘制图像。其他的自动化 *** 作最好由不同的更新任务来处理。

3D还是2D?

在开始写游戏之前,你要决定是做3D的还是2D的。2D游戏有一个低得多的学习曲线,一般更容易获得良好的性能。3D游戏需要更深入的数学技能,并且如果你不在意的话会有性能问题产生。如果你打算画比方框和圆圈更复杂的图形,还需要会使用3DStudio和Maya那样的建模工具。Android支持OpenGL用来3D编程,并且在OpenGL方面有很多很好的教程可供学习。

建立简单、高质量的方法

上手时,要确保你整个游戏不要就用一个庞大而冗长的方法。如果你遵循我上面描述的主循环模式,这将相当简单。每个你写的方法应当完成一个非常特定的任务,并且它就应该无差错地那样做。举例来说,如果你需要洗一副纸牌,你应该写一个“”的方法,并且该方法就应该只做这一件事。

这是一个适用于任何软件开发的编码实践,但对于游戏开发来说这尤为重要。在一个有状态的、实时的系统里,调试将变得非常困难。使你的方法尽量的小,一般的经验法则是每个方法有且仅有一个目的(译者注:完成且仅完成一个功能)。如果你要为一个场景用编程方式画一个背景,你可能需要一个叫做“”的方法。诸如此类的任务能够很快完成,因而你可以按照搭积木的方法来开发你的游戏,而你能够继续添加你要的功能,并且不会使得这一切难以理解。

最重要的是效率!

性能是任何游戏的主要问题。我们的目标是使得游戏的反应越快越好,看起来越流畅越好。某些方法如CanvasdrawLine比较慢。并且要将屏幕大小的位图画到主画布上,每一帧都是代价昂贵的。如何权衡对于达到最佳性能很有必要。确保管理好你的资源,使用技巧来以最少量的CPU资源完成你的任务。如果性能不好的话,即使是最好的游戏玩起来也没劲。人们一般对于游戏卡或者响应慢几乎难以容忍。

提示和技巧

看一下SDK中的示例LunarLander。它使用SurfaceView,这对于一个每秒需要处理最多帧的游戏来说是合适的。如果你要做3D,示例中有GLView可以处理3D显示的很多初始化工作。对LightRacer来说,我不得不优化把所有东西都画出来这种方法,否则帧率将会大大地降低。我只在视图初始化的时候把背景画进一个位图里一次。路径放在它们自己的位图里,随着车手的前进而更新。这两个位图在每一帧里都被画进主画布中去,车手画在顶端,到最后会有一个爆炸。这种技术使得游戏运行在一个可以玩的程度。

如果适用的话,使得你的位图的大小精确等于你打算画到屏幕上的大小,这也是个好的实践。这么做了以后就需要缩放,可以节省CPU资源。

在游戏中始终一致的位图配置(如RGBA8888)。这将会通过减少不同格式之间转换的时间来节省图形库的CPU时间。

如果你决定开发3D游戏但没有3D方面的知识,你需要挑选一两本3D游戏编程方面的书并学习线性代数。你最少要理解点积、叉积、向量、单元向量、法线、矩阵和变换。这方面我遇到的最好的书是叫《3D游戏编程和计算机图形学数学》。

声音文件要小而且低比特率。需要加载的越少,加载速度越快,游戏所需内存越少。

声音使用OGG文件,使用PNG文件。

确保释放所有媒体播放器,当Activity销毁时空出所有的资源。这能保证垃圾收集器清除了所有东西,也能保证在两次游戏开始之间没有内存泄露。

加入Android谷歌小组,寻求社区支持。这里有人可以在开发过程中给你帮助。

最重要的是,花时间测试再测试,确保每一小部分都如你所愿地工作。改善游戏是整个开发中最耗时最困难的部分。如果你匆匆将其推向市场,你很可能会使用户们失望,你会感到你的努力都白费了。你不可能使所有人都喜欢你写的东西,但你至少要尽量发布你最高质量的作品。

        本文之所以有必要编写并作记录,主要原因是因为在工作中开发出一个万能的自定义camera预览控件之后,本是一个提高效率以及提供一个强大能力的控件,但是产品并不能理解这个万能控件存在的意义,产品无法与技术设计相结合的理解使用;并且发现我们的智能业务部Camera自定义预览技术虽然是使用多年,但是我们并没有真正的形成规范,由于产品在不能够理解系统平台(Android/iOS)给产品和研发带来了什么,导致产品可能会出现在不理解系统平台以及系统知识的情况下,臆想产品所谓的形态;当产品设计脱离了系统平台所支持的技术点以及设计的初衷,就会导致回归问题的时候,出现不必要的讨论,其根结就是一点:“信息不同步,知识不同步”。

        所以,为了提高效率,就采用记录和分享的方式,尝试性推动产品、测试、研发三者对工程与架构的同步理解,更深的懂得程序架构设计意义,尝试性通过信息同步的方式,在一个统一的知识储备的平台下,共同完成一个更高效,和高品质的工程产品。(为了能够让非技术:产品设计,以及测试都能够理解,所以,使用了更多的白话解释)

        附:强大灵活的FsCameraTextureView(第一版,自适应截取)( 第二版本版本:自适应展示)

        首先,抛出几个问题,

      1)什么是摄像头支持的previewSize?

      2)什么是视频或者的pictureSize

      3)  如何获取和查看摄像头支持的PreViewSize 和PictureSize ?

      4)手机预览所见的区域SurfaceView(TextureView)与camera 的previewSize的关系是什么?

      5)为什么会设计了两种预览方式view,两种预览方式都会有什么样子的效果呢?

一,概述

通过Android Camera拍摄预览中设置setPreviewCallback实现onPreviewFrame接口,实时截取每一帧视频流数据(简单说来,就是通过设置一个接口,接收系统回调通知我们的每一帧数据)

二,知识点

    1, camera支持的格式:

    2,拍照流程

    3,camera权限

  三,Android Camera中PreviewSize、 PictureSize、 SurfaceView(TextureView)之间的关系

        1,PreviewSize:

          相机预览时候的能支持的尺寸,简单的说一下,就是预览的大小,也就是拍照前能够看到的大小。(通过Android手机相机可以试一下,这个参数设置不同,同样的焦距下,拍摄桌子上一个固定距离的东西,看到的视野会不同)

          相机的预览尺寸,不能随意的设置值,只能通过camera的parameters的getSupportedPreviewSizes方法,获取支持的预览尺寸列表,并从列表中选择一个设置在parameters中。(通俗简单的说就是,获取camera中能够支持的预览大小合集,如果你想要查看某个预览对应的尺寸,就把该尺寸设置到camera的属性中即可,则camera会返回相对应尺寸的预览数据流提供显示)。

        2,PictureSize :

指的是拍照之后,最终拍摄到的大小,也就是的质量。尺寸同样也只能从支持的列表中选取一个设置。 调用camera的takePicture方法(拍照)后,获得拍照的图像数据,注意picturesize和previewsize的宽高比也要保证一致,否则获取的会将preview时的图像裁剪成picturesize的比例。 previewsize的分辨率,只会影响预览时的分辨率,不会影响获取的分辨率,所以preview只是确定了图像的取景最大范围。最终的分辨率是由picturesize来决定。 所以,最好的设置方法,例如:previewsize为1280720,picturesize为25601440。(由于我们没有拍照业务,目前这个知识,不做深究)

        3,SurfaceView(TextureView)

          用于展示camera预览图像的view,就是将preview获得的数据,放在这个view上。所以如果preview的宽高比和SurfaceView的宽高比不一样,就会导致看到的图像拉伸变形。图像拉伸变形解决的办法:

          (1)就是在确定preview的分辨率后,重新设置SurfaceView宽高;

          (2)如果SurfaceView宽高定死,则需要获取一个比例适合SurfaceView尺寸的PreviewSize 的preview,尽量小的裁剪,然后填充在SurfaceView中。

        4,利用的显示方式,理解Preview与SurfaceView(TextureView)显示关系

          ImageView (UI上面设计的一个控件)与bitmap 的关系,比如限定死一个ImageView的大小,但是与ImageView尺寸不一致,就会有几种方案,首先选取一张长方形19201080的,ImageView就是紫色部分,无论长宽比都比ImageView要大。

适配例1:拉伸填充ScaleTypeFIT_XY :虽然被全部填充,但是整个为了适配已经扭曲,失真,缩放到控件大小,完全填充控件大小展示。

适配例2:等比例裁剪填充ScaleTypeCENTER_CROP ,因为在该模式下,会被等比缩放直到完全填充整个ImageView,并居中显示。该模式也是最常用的模式了。如图可以看到,的高度是能完全展示出来的,但是左右部分被进行了裁剪,并没有完全显示。

适配例3 :  ScaleTypeCENTER_INSIDE,此模式,用以完全展示内容为目的。将被等比缩放到能够完整展示在ImageView中并居中,如果大小,小于控件尺寸,那么就直接居中展示该

            适配ImageView方式还有很多,就不一一列举,这三种已经足够重要,为什么讲解camera预览,却穿插了的适配,其实可以这么理解,camera的preview就是由多张组成,不断的像帧动画一样变化,而SurfaceView就是一个载体,相当于ImageView,业务中定死了SurfaceView的大小之后,被动的承载你选择的previewSize,来展示camera的Preview,你可以选择类似于前面三种例子来理解preview的填充,以下会举例说明preview的填充策略选择有哪几种方式,我们会采用哪种方式:

        1)拉伸填充,自适应view,不可取,比如:手机的SurfaceView是整个手机的屏幕尺寸(全屏填充),或者任意尺寸比例的surfaceView,使用这种方式,就如同(适配例1)的方式,导致视频扭曲,拉伸。

        2)等比例裁剪填充,目前我们项目中,采用的就是这种方式,并且提供给很多三方使用,已经成为一种独立,并且稳定的技术实现自定义view,简单说一下视频的适配策略方式,SurfaceView随便由业务方,自定义宽度大小,比如业务方选择了19001000的SurfaceView, 我们的适配过程是:(1)从PreviewSize列表中选取最接近SurfaceView尺寸的PreviewSize(假设该摄像头,只支持19201080,和320640),19201080最接近,所以被获取;(此处展示一下蹩脚的英文Try to find an size match aspect ratio and size,尝试找到纵横比与view大小比适中的一个尺寸)(2)等比例裁剪填充到SurfaceView,首先我们设计的逻辑是,先选取一个缩放比例,假设等比例1920的按照SurfaceView的宽度等比例缩小到1900,而为了不让Preview失真,则高度1080等比例缩小的值是106875(等比例方程式,这里就不重复初中的知识,请自行计算),所以被压缩成为19001068这个尺寸,依旧保证完整,并且不失真。(3)将等比例缩减的,19001068进行显示在19001000的SurfaceView中,就会有一种效果类似(适配例2),宽度全部展示,高度被裁剪。(如同  适配例2中左右部分裁剪一样的道理)                       

          3)完全展示camera内容的缩放填充(类似适配例3),我们打开任意一部手机的camera,预览图像都没有全屏幕展示,类似拍照功能,所见即所得,PreviewSize是多少,就显示什么样子的比例尺寸,以及最后生产的照片比例就是多少,我们的自定义view,也可以随意设置大小,此模式下,用以完全展示camera内容为目的。Preview将被等比缩放到能够完整展示在SurfaceView中并居中,但是可能会有部分位置无法填充(类似适配例3显示效果)。

(该方式只是进行了技术储备,由于没有业务场景设计,所以没有使用,目前只是储备了这样的自定义控件)

四,灵活的自定义TextureView预览控件       

        FsCameraTextureView(第一版,自适应截取):等比例裁剪填充,方式(适配方式2),采用前面说的适配方式2,而产出的一种自定义view,2019年5月产出至今,在金融APP,以及商城的app中使用,经过逐步优化,和多版本检验,目前该控件,拥有以下特点:  1)稳定:目前各个使用场景,均无逻辑崩溃,内存泄漏,线程等任意问题; 2)灵活:随意设置预览view的尺寸大小,自适应任意业务设计;不仅仅满足刷脸业务,并且满足任意相机预览业务方使用; 3)提高效率,减轻工作量:使用简单, *** 作步骤简洁,接入只需要两步;减轻接入端,或者想要使用相机预览的业务的工作量,不需要重复造车,并且安全稳定。

      输出的业务方有(经不完全统计):(目前业务为保密进行公网保密处理)1)创新科技业务部-区块链部门 2)泰国人脸识别业务SDK3)S DBank 人脸业务4)核验身份z业务5)HTBank 人脸业务 6)云,商业平台部门

      FsAllPreviewCameraTextureView(技术储备版,全预览模式显示):完全展示camera内容的缩放填充,采用前面说的(适配方式3)适合拍照相关的业务使用,优点同样是,外部业务随意改变view大小,可以自适应view,由于目前没有业务方使用,暂时做储备,不深入讲解。

如果可以控件开源成功,后期,我将开源这两个控件,让更多的使用方使用,我们也希望共同技术进步,提高工程产出的使用能力。

预计下一次分享内容是(临时命名)

1)人脸核验内存和线程爆表到泄漏为零

2)分享七年前参于的Scrum(如何提高岗位间效率所定制的敏捷开发过程)

本文参考:

>

SurfaceView是View的继承结构中一个比较特殊的子类,它的作用是提供一个第二线程来完成图形的绘制。因此应用程序不需要等待View的图形绘制,第二线程会异步完成图形的绘制。

SurfaceView实现的步骤:

继续SurfaceView并实现SurfaceHolderCallback接口,该接口提供了SurfaceView创建、属性发生变化、销毁的时间点,那么你可以在适当的时间点完成具体的工作。

在SurfaceView初始化的时候调用SurfaceViewgetHolder()方法获取一个SurfaceHolder,SurfaceHolder用于管理SurfaceView的工作过程。为了让SurfaceHolder起作用,必须为SurfaceHolder添加回调方法(即第一步实现的SurfaceHolderCallback):

[java] view plaincopyprint

SurfaceHolderaddCallBack(SurfaceHolderCallback);

在SurfaceView内创建第二线程的内部类(暂命名为SecondThread),它的主要任务是完成Canvas的图形绘制。为了能让SecondThread获得Canvas实例,必须给SecondThread传递在步骤二中获得的SurfaceHolder。现在就可以通过SurfaceHolderlockCanvas()方法得到Canvas实例,并在Canvas上绘制图形。当图形绘制完成后,必须马上调用SurfaceHolderunlockCanvasAndPost()为Canvas解锁,使其他线程可以使用该画布。

有几个注意点:

每一次通过SurfaceHolder获取的Canvas都会保持上一次绘制的状态。如果需要重新绘制图形,可以通过调用CanvasdrawColor()或CanvasdrawBitmap()来擦除上次遗留的图形。

并不一定只用第二线程来绘制图形,也可以开启第三,第四个线程来绘制图形。

注意线程安全。

不需要像View一样,调用invalidate()方法来指示图形的刷新。

SurfaceView的一个范例:

[java] view plaincopyprint

package comsin90lzcandroidsample;

import javautilArrayList;

import javautilCollections;

import javautilList;

import androidcontentContext;

import androidgraphicsCanvas;

import androidgraphicsColor;

import androidgraphicsPaint;

import androidutilAttributeSet;

import androidutilLog;

import androidviewKeyEvent;

import androidviewSurfaceHolder;

import androidviewSurfaceView;

public class CanvasView extends SurfaceView implements SurfaceHolderCallback {

public static class Point {

private float x;

private float y;

public Point(float x, float y) {

thisx = x;

thisy = y;

}

public float getX() {

return x;

}

public void setX(float x) {

thisx = x;

}

public float getY() {

return y;

}

public void setY(float y) {

thisy = y;

}

public Point nextPoint(Orien o) {

float tempX = x;

float tempY = y;

switch (o) {

case UP:

tempY = y - LINE_LENGTH;

break;

case DOWN:

tempY = y + LINE_LENGTH;

break;

case LEFT:

tempX = x - LINE_LENGTH;

break;

case RIGHT:

tempX = x + LINE_LENGTH;

break;

case UNKNOWN:

break;

}

return new Point(tempX, tempY);

}

}

enum Orien {

UP, LEFT, DOWN, RIGHT, UNKNOWN

}

public static class DrawThread extends Thread {

private List<Point> points = Collections

synchronizedList(new ArrayList<Point>());

private boolean mRun;

private Paint mPaint;

private Orien curOrien;

public synchronized void setRun(boolean run) {

thismRun = run;

notifyAll();

}

public synchronized boolean getRun() {

while (!mRun) {

try {

wait();

} catch (InterruptedException e) {

eprintStackTrace();

}

}

return mRun;

}

//当按上下左右键时,生成相应的点坐标

private synchronized boolean doKeyDown(int KeyCode, KeyEvent event) {

synchronized (holder) {

Point p = null;

switch (KeyCode) {

case KeyEventKEYCODE_DPAD_UP:

if (curOrien != OrienDOWN) {

curOrien = OrienUP;

p = curPointnextPoint(curOrien);

}

break;

case KeyEventKEYCODE_DPAD_DOWN:

if (curOrien != OrienUP) {

curOrien = OrienDOWN;

p = curPointnextPoint(curOrien);

}

break;

case KeyEventKEYCODE_DPAD_LEFT:

if (curOrien != OrienRIGHT) {

curOrien = OrienLEFT;

p = curPointnextPoint(curOrien);

}

break;

case KeyEventKEYCODE_DPAD_RIGHT:

if (curOrien != OrienLEFT) {

curOrien = OrienRIGHT;

p = curPointnextPoint(curOrien);

}

break;

default:

curOrien = OrienUNKNOWN;

}

if (p != null) {

curPoint = p;

pointsadd(p);

setRun(true);

}

Logi(LOG_TAG, curOrientoString());

}

return true;

}

//当释放按键时,停止绘图

private synchronized boolean doKeyUp(int KeyCode, KeyEvent event) {

synchronized (holder) {

setRun(false);

curOrien = OrienUNKNOWN;

}

return true;

}

SurfaceHolder holder;

private Point curPoint;

public DrawThread(SurfaceHolder holder) {

thisholder = holder;

mPaint = new Paint();

mPaintsetColor(ColorGREEN);

curPoint = new Point(50, 50);

pointsadd(curPoint);

}

public void resetPoint() {

}

private void doDraw(Canvas canvas) {

for (int i = 0; i + 1 < pointssize(); i += 1) {

Point lp = pointsget(i);

Point np = pointsget(i + 1);

canvasdrawLine(lpgetX(), lpgetY(), npgetX(), npgetY(),

mPaint);

}

}

@Override

public void run() {

Canvas canvas = null;

while (getRun()) {

try {

canvas = holderlockCanvas();

synchronized (holder) {

doDraw(canvas);

}

} finally {

holderunlockCanvasAndPost(canvas);

setRun(false);

}

}

}

}

private DrawThread thread;

public static final String LOG_TAG = "CanvasView";

private static final int LINE_LENGTH = 30;

public CanvasView(Context context) {

super(context);

}

public CanvasView(Context context, AttributeSet attrs) {

super(context, attrs);

//SurfaceView由SurfaceHolder管理

SurfaceHolder holder = getHolder();

holderaddCallback(this);

thread = new DrawThread(holder);

threadstart();

}

@Override

public boolean onKeyDown(int keyCode, KeyEvent event) {

return threaddoKeyDown(keyCode, event);

}

@Override

public boolean onKeyUp(int keyCode, KeyEvent event) {

return threaddoKeyUp(keyCode, event);

}

@Override

public void surfaceChanged(SurfaceHolder holder, int format, int width,

int height) {

Logi(LOG_TAG, "surfaceChanged");

threadresetPoint();

threadsetRun(true);

}

@Override

public void surfaceCreated(SurfaceHolder holder) {

Logi(LOG_TAG, "surfaceCreated");

threadresetPoint();

threadsetRun(true);

}

@Override

public void surfaceDestroyed(SurfaceHolder holder) {

Logi(LOG_TAG, "surfaceDestroyed");

threadsetRun(false);

}

}

[TOC]

​ Activity的View hierachy的树形结构,最顶层的DecorView,也就是根结点视图,在SurfaceFlinger中有对应的Layer。

​ 对于具有SurfaceView的窗口来说,每一个SurfaceView在SurfaceFlinger服务中还对应有一个独立的Layer,用来单独描述它的绘图表面,以区别于它的宿主窗口的绘图表面。

​ 在WMS和SurfaceFlinger中,它与宿主窗口是分离的。这样的好处是对这个Surface的渲染可以放到单独线程去做。这对于一些游戏、视频等性能相关的应用非常有益,因为它不会影响主线程对事件的响应。但它也有缺点,因为这个Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,一些View中的特性也无法使用。

优点:

​ 可以在一个独立的线程中进行绘制,不会影响主线程。

​ 使用双缓冲机制,播放视频时画面更流畅。

缺点:

​ Surface不在View hierachy中,显示也不受View的属性控制,所以不能进行平移,缩放等变换。

​ 在40(API level 14)中引入。和SurfaceView不同,不会在WMS中单独创建窗口,而是作为View hierachy中的一个普通View,因此可以和其它普通View一样进行移动,旋转,缩放,动画等变化。TextureView必须在硬件加速的窗口中。

优点:

​ 支持移动、旋转、缩放等动画,支持截图

缺点:

​ 必须在硬件加速的窗口中使用,占用内存比SurfaceView高(因为开启了硬件加速),可能有1〜3帧延迟。

​ 如果说Surface是画布(画框), SurfaceTexture则是一幅画。可以使用 new Surface(SurfaceTexture) 创建一个Surface。SurfaceTexture并不直接显示图像,而是转为一个外部纹理(图像),用于图像的二次处理。

​ 视频码率就是数据传输时单位时间传送的数据位数,一般我们用的单位是kbps即千位每秒。通俗一点的理解就是取样率,单位时间内取样率越大,精度就越高,处理出来的文件就越接近原始文件。码率和质量成正比,但是文件体积也和码率成正比,即 码率越高越清晰,视频文件越大 。码率率超过一定数值,对图像的质量没有多大影响,因为 原始图像只有那么清晰,再高码率也不会变的比原图更清晰 。

​ 每秒的帧数表示图形处理器处理场时每秒钟能够更新的次数,一般使用fps(Frames per Second)表示。因此帧率越高,画面越流畅。比如Android理论上是16ms一张图像,即60fps。

​ ANativeWindow代表的是本地窗口,可以看成NDK提供Native版本的Surface。通过 ANativeWindow_fromSurface 获得ANativeWindow指针, ANativeWindow_release 进行释放。类似Java,可以对它进行lock、unlockAndPost以及通过 ANativeWindow_Buffer 进行图像数据的修改。

在NDK中使用ANativeWindow编译时需要链接NDK中的 libandroidso 库

有时候会用到颜色拾取器这样的东西来查看屏幕上的颜色值,一直是用Pixolor这个软件来看颜色的;很方便,点哪里显示哪里,也没有延迟,以为是什么黑科技;我注意到一个细节,如果只是切换屏幕,颜色拾取器不会更新,只有移动拾取器才更新选中;可以确定是截屏来实现的了,那就简单了,截屏获取像素点的颜色值就好了

网上看了一下,截屏大概分为保存View为图像和调用录屏服务两种办法,录屏是比较好的办法,可以在APP外截屏,所以简单的封装了一下

Step 1 Add the JitPack repository to your build file

Step 2 Add the dependency

主要分为两步,第一步是开启录屏;第二步就可以直接获取截屏,返回Bitmap

截图的过程录屏是开启的,录屏开启就可以进行截屏, *** 作完需要关闭录屏

截屏过程很快,效果很好

如果是APP外截屏则开启悬浮窗服务,可以通过 *** 作悬浮窗进行截屏

参考文章: Android 截屏方式整理 、 Android录屏(50+)

1初始化一个 MediaProjectionManager

2创建并启动 Intent

3在 onActivityResult 中拿到MediaProjection

4设置VirtualDisplay将图像和展示的View关联起来。一般来说我们会将图像展示到SurfaceView,这里为了为了便于拿到截图,我们使用ImageReader,他内置有SurfaceView。

5通过ImageReader拿到截图

6注意截屏之后要及时关闭VirtualDisplay ,因为VirtualDisplay 是十分消耗内存和电量的。

录屏和截屏差不多,只是截屏的时候只是设置了一个ImageReader去获取图像数据,而录屏是设置一个 SurfaceView 去接收内容,获取视频流,然后通过 MediaCodec 来实现视频的硬编码,然后保存为视频文件

初始化录屏的大小和码率

开始录屏,设置输出文件

停止录屏

参考文章: Android视频录制--屏幕录制

1在 AndroidManifest 中添加权限,Android 60 加入的动态权限申请,如果应用的 targetSdkVersion 是 23,申请敏感权限还需要动态申请

中间 *** 作和截屏一样,就是准备开启录屏

5创建虚拟屏幕,这一步就是通过 MediaProject 录制屏幕的关键所在, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR 参数是指创建屏幕镜像,所以我们实际录制内容的是屏幕镜像,但内容和实际屏幕是一样的,并且这里我们把 VirtualDisplay 的渲染目标 Surface 设置为 MediaRecorder 的 getSurface ,后面我就可以通过 MediaRecorder 将屏幕内容录制下来,并且存成 video 文件

6录制屏幕数据,这里利用 MediaRecord 将屏幕内容保存下来

以上就是关于Android Camera2 教程 · 第一章 · 概览全部的内容,包括:Android Camera2 教程 · 第一章 · 概览、BlastBufferQueue 原理解读、基于android系统的手机游戏的开发等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/web/10073383.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2023-05-05
下一篇2023-05-05

发表评论

登录后才能评论

评论列表(0条)

    保存