安卓系统 地表

免责声明


本文面向没有经验的初学者android开发人员,尤其是那些开始分析grafika示例但发现它们很困难的初学者 ,这里我们将看一下类似的代码,并通过状态图简化基本步骤的描述。


为什么在标题中呈现Surface类? 在android中,许多类的名称(Surface,SurfaceHolder,SurfaceTexture,SurfaceView,GLSurfaceView)都带有Surface一词,它们没有通过通用的层次结构进行连接,但是它们通过低级逻辑进行组合以处理图像输出。 在我看来,在标题中使用它来强调尝试公开使用SDK的此特定部分的工作是合理的。


不同API的用法示例


让我们尝试编写以下示例: 我们将从摄像机进行预览,在其上叠加一个动画可绘制对象,将其全部显示在屏幕上,并在必要时写入文件。 完整的代码将位于https://github.com/tttzof351/AndroidSurfaceExample/


为了输出到屏幕,我们将使用GLSurfaceView进行MediaCodecEGLSurface类的记录,并通过API V2与摄像机进行通信。 总体方案大致如下:



多个表面覆盖


表面实际上是内存中需要填充图像的区域的句柄。 最有可能的是,我们试图在屏幕上或文件中显示某些内容,因此它就像某些生成数据的“进程”的缓冲区一样工作。


要从多个Surface创建覆盖,我们将使用OpenGL。
为此,我们将创建两个正方形的外部纹理并从中获取Surface


在代码中,它将如下所示:


OpenGLExtarnalTexture.kt


val textures = IntArray(1) GLES20.glGenTextures(1, textures, 0) val textureId = textures[0] //    val textureWidth = ... val textureHeight = ... //  val surfaceTexture = SurfaceTexture(textureId) surfaceTexture.setDefaultBufferSize(textureWidth, textureHeight) //, surface  ""    val surface = Surface(surfaceTexture) 

XYZ坐标


现在我们需要了解如何创建和排列纹理,为此,我们必须记住在OpenGL中坐标网格的结构方式:其中心与场景的中心(窗口)重合,并且边界被规范化,即从-1到1。


在此场景中,我们要设置两个矩形(工作在平面上,因此所有z坐标在逻辑上都设置为0f)-红色表示要放置相机预览的矩形,蓝色表示动画可绘制对象的蓝色:


我们明确写下我们的坐标:



 fullscreenTexture = floatArrayOf( // X, Y, Z -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, ) smallTexture = floatArrayOf( // X, Y, Z 0.3f, 0.3f, 0.0f, 0.8f, 0.3f, 0.0f, 0.3f, 0.8f, 0.0f, 0.8f, 0.8f, 0.0f ) 

紫外线坐标


这样够了吗 原来没有:(


纹理是图片到场景区域的映射,为了使其正确显示,您需要准确指定图片中的点将落在该区域内的位置-为此,OpenGL使用UV坐标-它们来自左下角,并且每个边界的边界为0到1轴。


它的工作原理如下:我们将为区域的每个顶点设置UV坐标,并假设图像的宽度和高度等于1,然后在图像中寻找相应的点。


考虑一个例子-我们假设相机以反转和反射状态向我们提供图像,同时我们只想显示右上部分,即图像的纬度和高度为0.8。


微妙的一点-在此阶段,我们不知道屏幕上区域的纵横比-我们在相对坐标中只有一个正方形,它将填满整个场景并相应地拉伸。 如果我们要制作全屏相机,那么我们的相对尺寸(每侧2个)将扩展到传统的1080x1920。 我们假设我们设置场景的尺寸,以使它们的比例等于摄像机的比例。
让我们看看坐标的位置-我们区域(1,1,0)的右上角应该指向UV坐标(0,0),左下角为(0.8f,0.8f),依此类推。



因此,我们获得了XYZ和UV的对应关系:


 // X, Y, Z, U, V -1.0f, -1.0f, 0.0f, 0.8f, 0.8f, 1.0f, -1.0f, 0.0f, 0.8f, 0.0f, -1.0f, 1.0f, 0.0f, 0.0f, 0.8f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f 

如果摄像机预览与屏幕上的区域之间的宽高比最初是一致的,那么显然它将继续保存,因为在我们的例子中,我们只是乘以0.8f。
我们将吃什么,我们将值设置为大于1? 根据传递给OpenGL的设置,我们将获得图像某些部分的点。 在我们的示例中,最后一行将沿着相应的轴重复,我们将看到“条纹”形式的工件


底线:如果我们想在保持屏幕上区域位置的同时压缩/剪切图像,那么UV坐标就是我们的选择!


设置纹理的坐标。



 fullscreenTexture = floatArrayOf( // X, Y, Z, U, V -1.0f, -1.0f, 0.0f, 1f, 0f, 1.0f, -1.0f, 0.0f, 0f, 0f, -1.0f, 1.0f, 0.0f, 1f, 1f, 1.0f, 1.0f, 0.0f, 0f, 1f ) smallTexture = floatArrayOf( // X, Y, Z, U, V 0.3f, 0.3f, 0.0f, 0f, 0f, 0.8f, 0.3f, 0.0f, 1f, 0f, 0.3f, 0.8f, 0.0f, 0f, 1f, 0.8f, 0.8f, 0.0f, 1f, 1f ) 

着色器


具有静态XYZ和UV坐标不是很方便-例如,我们可能想使用手势来移动和缩放纹理。 要对其进行转换,我们将为每个纹理创建两个矩阵:分别用于XYZ和UV坐标的MVPMatrixTexMatrix


每个OpenGL2必须包含着色器,以便在屏幕上显示某些内容。 当然,这不是一个可以在一个段落中公开的主题,但是,在我们的案例中,它们将是微不足道的,因此,您无需了解太多材料就可以快速了解它们正在做什么。


首先,有两个着色器-顶点和片段。


第一个(顶点)将处理我们的顶点,即,将我们的XYZ / UV坐标乘以它们相应的矩阵,然后填充OpenGL变量gl_Position ,该变量完全负责纹理在屏幕上的最终位置。


第二个(片段)应使用图像像素填充gl_FragColor


我们共有:顶点着色器中的变量,我们必须用数据填充字段,即:


  • MVPMatrix-> uMVPMatrix
  • TexMatrix-> uTexMatrix
  • 我们的XYZ顶点坐标-> aPosition
  • UV坐标-> aTextureCoord

vTextureCoord-需要将数据从顶点着色器转发到片段着色器
在片段着色器中,我们获取转换后的UV坐标,并使用它们在纹理区域中显示图像像素。


 val vertexShader = """ uniform mat4 uMVPMatrix; uniform mat4 uTexMatrix; attribute vec4 aPosition; attribute vec4 aTextureCoord; varying vec2 vTextureCoord; void main() { gl_Position = uMVPMatrix * aPosition; vTextureCoord = (uTexMatrix * aTextureCoord).xy; } """ val fragmentShader = """ #extension GL_OES_EGL_image_external : require precision mediump float; varying vec2 vTextureCoord; uniform samplerExternalOES sTexture; void main() { gl_FragColor = texture2D(sTexture, vTextureCoord); } """ 

作为参考,我们指出了两种类型之间的区别:


  • 统一的-这种类型的变量将在重复调用期间保留值,我们使用一个着色器,该着色器被依次调用两个纹理,因此我们仍将覆盖每个渲染
  • 属性-从顶点缓冲区读取此类型的数据,需要在每次渲染时将其加载
  • 变化-将数据从顶点着色器传输到片段所需

如何将参数传递给着色器? 为此,您首先需要获取变量的ID(指针):


 val aPositionHandle = GLES20.glGetAttribLocation(programId, "aPosition") 

现在,对于此ID,您需要加载数据:


 //      floatbuffer val verticesBuffer = ByteBuffer.allocateDirect( fullscreenTexture.size * FLOAT_SIZE_BYTES ).order( ByteOrder.nativeOrder() ).asFloatBuffer() verticesBuffer.put(fullscreenTexture).position(0) /*    -  XYZ   0 .       id      ,     ,               - 5  - XYZUV,  4 - -   float */ verticesBuffer.position(0) GLES20.glVertexAttribPointer( aPositionHandle, 3, GLES20.GL_FLOAT, false, 5 * 4, verticesBuffer ) 

直接绘图


在将所有数据填充到着色器之后,我们应该要求纹理更新图像,并要求OpenGL绘制顶点:


 fun updateFrame(...) { ... surfaceTexture.updateTexImage() GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) } 

在我们的示例中,我们将把OpenGL场景的工作分为两类-直接是场景和纹理:


OpenGLExternalTexture.kt


 class OpenGLExternalTexture(verticesData: FloatArray, ...) { val surfaceTexture: SurfaceTexture val surface: Surface init { // ,    . } ... fun updateFrame(aPositionHandle: Int, ...) {...} // ,   fun release() {...} //   } 

OpenGLScene.kt


 class OpenGLScene( sceneWidth: Int, sceneHeight: Int, ... ) { val fullscreenTexture = OpenGLExternalTexture(...) val smallTexture = OpenGLExternalTexture(..) val aPositionHandle: Int ... init { // ,        . } fun updateFrame() { ... fullscreenTexture.updateFrame(aPositionHandle, ...) smallTexture.updateFrame(aPositionHandle, ...) } fun release() { fullscreenTexture.release() smallTexture.release() } } 

StateMachine /状态机/状态机


我们打算在示例中使用的所有API基本上都是异步的(嗯,也许是动画Drawable除外)。 我们将这些调用包装在单独的StateMachines中,该方法是显式写出系统状态,并通过发送事件进行状态之间的转换。


让我们看一个简单的示例,假设我们有以下代码:


 imageView.setOnClickListener { loadImage { bitmap -> imageView.setBitmap(bitmap) } } 

总的来说,一切都很好-漂亮而紧凑,但是我们将尝试通过以下方式重写它:


 val uiMachine = UIMachine() imageView.setOnClickListener { uiMachine.send(Click(imageView)) } class UIMachine { var state: State = WaitClick() fun send(action: Action) = transition(action) private fun transition(action: Action) { val state = this.state when { state is WaitingClick && action is Click -> { this.state = WaitBitmap(imageView = action.imageView) loadImage { send(BitmapIsReady(bitmap = it)) } } state is WaitingBitmap && action is BitmapIsReady -> { this.state = WaitClick state.imageView.setImageBitmap(action.bitmap) } } } } sealed class State { object WaitingClick : State() class WaitingBitmap(val imageView: ImageView): State() } sealed class Action { class Click(val imageView: ImageView): Action() class BitmapIsReady(val bitmap: Bitmap): Action() } 

一方面,结果却是很多 ,尽管如此,还是出现了一些隐式但有用的属性:现在重复按下并不会导致不必要的loadImage starts 尽管对于如此大的卷来说这并不明显,但是我们摆脱了嵌套的回调调用,稍后我们将使用它,以及transition方法的书写风格允许您构建一个过渡图,该图一对一地重复代码,即在我们的示例中:



灰色表示未明确写出的过渡。 通常,它们被记录或引发异常,认为这是错误的迹象。 现在,我们将设法忽略它,并且在将来,我们将不再指向这些图。


创建StateMachine的基本接口:


 interface Action interface State interface StateMachine<S : State, A : Action> { var state: S fun transition(action: A) fun send(action: A) } 

GLSurfaceView


在Android中使用OpenGL显示内容的最简单方法是GLSurfaceView类-它会自动创建一个新的绘图流,并使用GLSurfaceView :: onResume / onPause方法开始/暂停。


为简单起见,我们将视图设置为16:9的比例。


呈现过程本身已移至单独的回调-GLSurfaceView.Renderer。
将其包装在StateMachine中,我们得到如下内容:


GLSurfaceMachine.kt


 class GLSurfaceMachine: StateMachine<GLSurfaceState, GLSurfaceAction> { override var state: GLSurfaceState = WaitCreate() override fun send(action: GLSurfaceAction) = transition(action) override fun transition(action: GLSurfaceAction) { val state = this.state when { state is WaitCreate && action is Create -> { this.state = WaitSurfaceReady(...) this.state.glSurfaceView?.setRenderer(object :Renderer { override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) send(SurfaceReady(width, height, gl)) } override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) { } override fun onDrawFrame(gl: GL10?) { send(Draw) } }) } state is WaitSurfaceReady && action is SurfaceReady -> { val openGLScene = OpenGLScene(width, height) this.state = DrawingAvailable(openGLScene, ...) } state is DrawingAvailable && action is Draw -> { state.openGLScene.updateFrame() } state !is WaitCreate && action is Stop -> { state.uiHolder.glSurfaceView?.onPause() state.uiHolder.openGLScene?.release() this.state = WaitSurfaceReady() } state is WaitSurfaceReady && action is Start -> { state.uiHolder.glSurfaceView?.onResume() } } } } ... val glSurfaceMachine = GLSurfaceMachine() val glSurfaceView = findViewById(R.id.gl_view) glSurfaceView.layoutParams.width = width glSurfaceView.layoutParams.height = ((16f/9f) * width).toInt() glSurfaceMachine.send(GLSurfaceAction.Create(glSurfaceView, ...)) 

让我们画一个过渡图:



现在,我们的代码正在尝试在屏幕上显示某些内容,尽管目前它的效果很差-除了黑屏,我们看不到其他任何内容。 不难猜测,由于我们尚未实现图像源,因此现在没有任何东西进入Surface。 解决这个问题-首先,创建一个CanvasDrawable:


CanvasDrawable.kt


 class CanvasDrawable : Drawable() { private val backgroundPaint = Paint().apply { ... } private val circlePaint = Paint().apply { ... } override fun draw(canvas: Canvas) { canvas.drawRect(bounds, backgroundPaint) val width = bounds.width() val height = bounds.height() val posX = ... val posY = ... canvas.drawCircle(posX, posY, 0.1f * width, circlePaint) } ... } 

现在,我们可以通过在画布上渲染canvasDrawable来补充GLS​​urfaceMachine中的部分,该画布提供相应纹理的表面:


 state is DrawingAvailable && action is Draw -> { val canvasDrawable = state.canvasDrawable val smallTexture = state.openGLScene.smallTexture val bounds = canvasDrawable.bounds val canvas = smallSurface.lockCanvas(bounds) canvasDrawable.draw(canvas) smallSurface.unlockCanvasAndPost(canvas) state.openGLScene.updateFrame() } 

之后,我们将看到类似以下内容的内容:



相机API V2


绿色矩形肯定很有趣且引人入胜,但是现在是时候尝试将预览从摄像机移到其余表面了。


让我们写下使用相机的步骤:


  • 我们正在等待许可。 我们将具有此状态WaitingStart
  • 我们获得了相机管理器实例,我们找到了所需相机的逻辑ID(通常有两个-后面和前面,逻辑上是因为相机可以由现代设备上的许多传感器组成),选择合适的大小,打开相机,我们得到了cameraDevice。 状态等待中

 val manager = getSystemService(Context.CAMERA_SERVICE) as CameraManager var resultCameraId: String? = null var resultSize: Size? = null for (cameraId in manager.cameraIdList) { val chars = manager.getCameraCharacteristics(cameraId) val facing = chars.get(CameraCharacteristics.LENS_FACING) ?: -1 if (facing == LENS_FACING_BACK) { val confMap = chars.get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP ) val sizes = confMap?.getOutputSizes(SurfaceTexture::class.java) resultSize = findSize(sizes) resultCameraId = cameraId break } } resultCameraId?.let { cameraId -> manager.openCamera(cameraId, object : CameraDevice.StateCallback() { override fun onOpened(camera: CameraDevice) { //Success open camera ... } }) } 

  • 打开相机后,我们转向申请Surface来显示图像。 等待表面状态
  • 现在我们有了cameraDevice,Surface,我们需要打开一个会话,以便摄像机最终开始传输数据。 等待会话状态

 cameraDevice.createCaptureSession( arrayListOf(surface), object : CameraCaptureSession.StateCallback() { override fun onConfigured(session: CameraCaptureSession) { send(CameraAction.SessionReady(session)) } }, handler ) 

  • 现在我们可以捕获预览。 开始预览状态

 val request = cameraDevice.createCaptureRequest( CameraDevice.TEMPLATE_PREVIEW ).apply { addTarget(surface) } session.setRepeatingRequest( request.build(), object : CameraCaptureSession.CaptureCallback() {...} handler ) 

我们说明一下当前的方案:


CameraMachine.kt




媒体编解码器


MediaCodec是用于与系统编解码器进行低级工作的类,通常,其API是一组输入/输出缓冲区(不幸的是,听起来比使用它更容易),数据(原始或编码取决于编码器/解码器的操作模式)放入其中,并且在输出中,我们得到结果。


尽管ByteBuffer通常充当缓冲区,但是您可以使用Surface来处理视频,这将向我们返回MediaCodec :: createInputSurface,在其上我们应该绘制要记录的帧(通过这种方法,文档承诺通过使用gpu可以更快地进行编码)


好了,现在我们需要学习如何在MediaCodec的Surface上绘制我们在GLSurfaceMachine中创建的现有Surface。 请务必记住:Surface是一个创建使用者的对象,通常无法从其中读取某些内容,即没有条件方法getBitmap / readImage / ...


我们将按照以下步骤进行操作:在现有的GL上下文的基础上,我们将创建一个新的具有通用内存的内存,因此我们可以使用它来重用我们先前在此处创建的纹理的id-shniks。 然后,使用MediaCodec中的新GL上下文和Surface,我们将创建EGLSurface-屏幕外缓冲区,我们还可以在该缓冲区上创建我们的OpenGLScene类。 然后,在每个帧渲染时,我们将尝试将帧并行写入文件。


EGL表示OpenGL API与平台的窗口子系统的交互接口,我们将从grafika那里窃取它的工作。 我也不会直接用MediaCodec描述传送带(EncoderHelper),仅给出组件之间交互的最终方案:


EncoderMachine.kt
EncoderHelper.kt



结果:


  • 处理视频至少需要具备OpenGL的基本技能
  • Android Media API的底层级别很低,因此具有一定的灵活性,但有时它会迫使您编写比自己想要的代码更多的代码
  • 异步API可以包装在StateMachines中

Source: https://habr.com/ru/post/zh-CN480878/


All Articles