Isenção de responsabilidade
Este artigo é destinado a desenvolvedores iniciantes de Android com pouca experiência em trabalhar com vídeo e / ou câmera, especialmente aqueles que começaram a analisar exemplos de grafika e os acharam difíceis, aqui veremos código semelhante com uma descrição simplificada das etapas básicas ilustradas pelos diagramas de estado.
Por que a classe Surface é renderizada no cabeçalho? No android, muitas classes têm a palavra Surface em seu nome (Surface, SurfaceHolder, SurfaceTexture, SurfaceView, GLSurfaceView), elas não são conectadas por uma hierarquia comum; no entanto, são combinadas por uma lógica de baixo nível para trabalhar com saída de imagem. Pareceu-me razoável usá-lo no título para enfatizar uma tentativa de divulgar trabalho com essa parte específica do SDK.
Exemplo de uso com API diferente
Vamos tentar escrever o seguinte exemplo: tiraremos uma prévia da câmera, sobreporemos um desenho animado, exibiremos tudo na tela e, se necessário, gravaremos em um arquivo. O código completo será https://github.com/tttzof351/AndroidSurfaceExample/
Para saída para as telas, usaremos o GLSurfaceView , para gravar com as classes MediaCodec e EGLSurface , e nos comunicar com a câmera via API V2 . O esquema geral é aproximadamente o seguinte:

Sobreposições de superfície múltiplas
A superfície é realmente uma alça para a área na memória que precisa ser preenchida com a imagem. Provavelmente, tentamos exibir algo na tela ou em um arquivo, para que funcione como um buffer para algum "processo" que produz dados.
Para criar uma sobreposição de vários Surface, usaremos o OpenGL.
Para fazer isso, criaremos duas texturas externas quadradas e obteremos delas Surface
No código, será algo parecido com isto:
OpenGLExtarnalTexture.kt
val textures = IntArray(1) GLES20.glGenTextures(1, textures, 0) val textureId = textures[0]
Coordenadas XYZ
Agora precisamos entender como criar e organizar as texturas, e para isso teremos que lembrar como a grade de coordenadas é estruturada no OpenGL: seu centro coincide com o centro da cena (janela) e as bordas são normalizadas, ou seja, de -1 a 1.
Nesta cena, queremos definir dois retângulos (o trabalho está no plano, para que todas as coordenadas z sejam logicamente definidas como 0f) - em vermelho, indicaremos aquele em que colocaremos a visualização da câmera e em azul para o desenho animado:
Anotamos nossas coordenadas explicitamente:

fullscreenTexture = floatArrayOf(
Coordenadas UV
Isso é suficiente? Acontece que não :(
Uma textura é um mapeamento de uma imagem para uma área de cena e, para torná-la correta, você precisa especificar exatamente onde os pontos da imagem se encaixam nessa área - para isso, o OpenGL usa coordenadas UV - eles saem do canto inferior esquerdo e têm bordas de 0 a 1 para cada eixos.
Funciona da seguinte forma: definiremos as coordenadas UV para cada vértice de nossa área e procuraremos os pontos correspondentes na imagem, assumindo que a largura e a altura sejam iguais a 1.
Considere um exemplo: assumiremos que a câmera nos fornece a imagem em um estado invertido e refletido e, ao mesmo tempo, queremos mostrar apenas a parte superior direita, ou seja, obter 0,8 de latitude e altura da imagem.
O ponto sutil - neste estágio, não sabemos a proporção da área na tela - só temos um quadrado em coordenadas relativas, que preencherão toda a cena e, consequentemente, se estenderão. Se fizéssemos uma câmera de tela cheia, nossos tamanhos relativos (2 de cada lado) se estenderiam para o 1080x1920 convencional. Assumimos que definimos as dimensões da cena de forma que sua proporção seja igual à proporção da câmera.
Vamos ver para onde vão as coordenadas - o ponto superior direito da nossa área (1, 1, 0) deve ir para a coordenada UV (0, 0), a parte inferior esquerda em (0.8f, 0.8f), etc.

Assim, obtemos a correspondência de XYZ e UV:
Se a proporção entre a visualização da câmera e a área na tela coincidir inicialmente, ela obviamente continuará sendo salva, porque, no nosso caso, apenas multiplicamos por 0,8f.
E o que vamos comer, vamos definir valores maiores que 1? Dependendo das configurações que passamos para o OpenGL, obteremos pontos de alguma parte da imagem. Em nosso exemplo, a última linha será repetida ao longo do eixo correspondente e veremos artefatos na forma de "listras"
Conclusão: se queremos compactar / cortar a imagem, mantendo a posição da área na tela, as coordenadas UV são a nossa escolha!
Defina as coordenadas para as nossas texturas.

fullscreenTexture = floatArrayOf(
Shaders
Ter coordenadas estáticas XYZ e UV não é muito conveniente - por exemplo, podemos querer mover e dimensionar nossas texturas com gestos. Para transformá-los, criaremos duas matrizes para cada textura: MVPMatrix e TexMatrix for para coordenadas XYZ e UV, respectivamente.
Cada OpenGL2 deve conter shaders para exibir algo na tela. Obviamente, esse não é um tópico que possa ser divulgado em um parágrafo; no entanto, no nosso caso, eles serão triviais e, portanto, você poderá entender rapidamente que eles são o que estão fazendo, sem muito conhecimento do material.
Primeiro de tudo, existem dois shaders - vértice e fragmento.
O primeiro (vértice) processará nossos vértices, ou seja, basta multiplicar nossas coordenadas XYZ / UV com suas matrizes correspondentes e preencher a variável OpenGL gl_Position, que é exatamente responsável pela posição final de nossa textura na tela.
O segundo (fragmento) deve preencher gl_FragColor com pixels da imagem.
Total que temos: as variáveis dentro do vertex shader, devemos preencher os campos com nossos dados, a saber:
- MVPMatrix -> uMVPMatrix
- TexMatrix -> uTexMatrix
- nossas coordenadas de vértice XYZ -> aPosition
- Coordenadas UV -> aTextureCoord
vTextureCoord - necessário para encaminhar dados do shader de vértice para o shader de fragmento
No shader de fragmento, pegamos as coordenadas UV convertidas e as usamos para exibir os pixels da imagem na área de textura.
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); } """
Para referência, indicamos a diferença entre os tipos:
- uniforme - uma variável desse tipo retém valores durante chamadas repetidas, usamos um sombreador que é chamado seqüencialmente para duas texturas, portanto, ainda o substituiremos a cada renderização
- attribute - dados deste tipo são lidos do buffer de vértice, precisam ser carregados a cada renderização
- variando - necessário para transferir dados do shader de vértice para o fragmento
Como passar parâmetros para um shader? Para fazer isso, você primeiro precisa obter o id (ponteiro) da variável:
val aPositionHandle = GLES20.glGetAttribLocation(programId, "aPosition")
Agora, para esse ID, você precisa carregar os dados:
Desenho direto
Depois de preenchermos os shaders com todos os dados, devemos solicitar a textura para atualizar a imagem e o OpenGL para desenhar nossos vértices:
fun updateFrame(...) { ... surfaceTexture.updateTexImage() GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) }
Em nosso exemplo, dividiremos o trabalho com a cena OpenGL em duas classes - diretamente cenas e texturas:
OpenGLExternalTexture.kt
class OpenGLExternalTexture(verticesData: FloatArray, ...) { val surfaceTexture: SurfaceTexture val surface: Surface init {
OpenGLScene.kt
class OpenGLScene( sceneWidth: Int, sceneHeight: Int, ... ) { val fullscreenTexture = OpenGLExternalTexture(...) val smallTexture = OpenGLExternalTexture(..) val aPositionHandle: Int ... init {
Máquina de estado / Máquina de estado / Máquina de estado
Todas as APIs que pretendemos usar em nosso exemplo são basicamente assíncronas (bem, talvez com exceção do Drawable animado). Envolvemos essas chamadas em StateMachines separados, uma abordagem em que o estado do sistema é explicitamente gravado e as transições entre elas ocorrem através do envio de eventos.
Vejamos um exemplo simples de como isso ficará, suponha que tenhamos este código:
imageView.setOnClickListener { loadImage { bitmap -> imageView.setBitmap(bitmap) } }
Em geral, está tudo bem - bonito e compacto, mas tentaremos reescrevê-lo da seguinte maneira:
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() }
Por um lado, descobriu-se muito mais, no entanto, várias propriedades implícitas, mas úteis, apareceram: pressionar repetidamente agora não leva a um carregamento desnecessário de imagImage, embora não seja óbvio com esse volume, mas nos livramos da chamada de retorno de chamada aninhada, que usaremos posteriormente , e o estilo de escrita do método de transição permite criar um diagrama de transição que repete o código individualmente, ou seja, no nosso caso:

Cinza indica transições que não são explicitamente gravadas. Geralmente, eles são registrados ou gerados uma exceção, considerando-o um sinal de erro. Por enquanto, conseguiremos simplesmente ignorá-lo e, no futuro, não apontaremos para os diagramas.
Crie as interfaces base para StateMachine:
interface Action interface State interface StateMachine<S : State, A : Action> { var state: S fun transition(action: A) fun send(action: A) }
GLSurfaceView
A maneira mais fácil de exibir algo usando o OpenGL no Android é a classe GLSurfaceView - ela cria automaticamente um novo fluxo de desenho, que inicia / pausa usando os métodos GLSurfaceView :: onResume / onPause.
Por uma questão de simplicidade, definiremos nossa visão para uma proporção de 16: 9
O próprio processo de renderização é movido para um retorno de chamada separado - GLSurfaceView.Renderer.
Embrulhando-o no StateMachine, temos algo parecido com isto:
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, ...))
Vamos desenhar um diagrama de transição:

Agora, nosso código está tentando exibir algo na tela, embora, por enquanto, ele o faça mal - não veremos nada além de uma tela preta. Não é difícil adivinhar o fato é que nada está entrando em nossa superfície agora porque ainda não implementamos fontes de imagem. Vamos corrigir isso - primeiro, crie um 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) } ... }
Agora podemos complementar a seção no GLSurfaceMachine renderizando canvasDrawable na tela que fornece a superfície da textura correspondente:
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() }
Após o que veremos algo como:

API da câmera V2
O retângulo verde é certamente divertido e intrigante, mas é hora de tentar trazer a visualização da câmera para a superfície restante.
Vamos escrever as etapas para trabalhar com a câmera:
- Estamos aguardando permissão. Teremos esse estado WaitingStart
- Obtemos a instância do gerenciador de câmera, encontramos o ID lógico (geralmente existem dois deles - para trás e frente, e o lógico é porque a câmera pode consistir em muitos sensores em dispositivos modernos) da câmera desejada, selecione o tamanho apropriado, abra a câmera, obteremos o cameraDevice. Status WaitingOpen
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) {
- Tendo uma câmera aberta, nos voltamos para solicitar uma superfície para exibir a imagem. WaitingSurface Status
- Agora que temos o cameraDevice, Surface, precisamos abrir uma sessão para que a câmera finalmente comece a enviar dados. Status WaitingSession
cameraDevice.createCaptureSession( arrayListOf(surface), object : CameraCaptureSession.StateCallback() { override fun onConfigured(session: CameraCaptureSession) { send(CameraAction.SessionReady(session)) } }, handler )
- Agora podemos capturar a visualização. Status da visualização inicial
val request = cameraDevice.createCaptureRequest( CameraDevice.TEMPLATE_PREVIEW ).apply { addTarget(surface) } session.setRepeatingRequest( request.build(), object : CameraCaptureSession.CaptureCallback() {...} handler )
Ilustramos nosso esquema atual:
CameraMachine.kt


MediaCodec é uma classe para trabalho de baixo nível com codecs de sistema, em geral, sua API é um conjunto de buffers de entrada / saída (parece, infelizmente, mais fácil do que trabalhar com ele) no qual os dados (brutos ou codificados dependem do modo de operação do codificador / decodificador) são colocados, e na saída, obtemos o resultado.
Apesar do ByteBuffer geralmente atuar como buffers, você pode usar o Surface para trabalhar com vídeo, que retornará o MediaCodec :: createInputSurface para nós, nele devemos desenhar os quadros que queremos gravar (com essa abordagem, a documentação nos promete uma codificação mais rápida através do uso de gpu )
Bem, agora precisamos aprender a desenhar a superfície existente que criamos no GLSurfaceMachine on Surface da MediaCodec. É importante lembrar: Surface é um objeto que cria um consumidor e, em geral, é impossível ler algo dele, ou seja, não há método condicional getBitmap / readImage / ...
Vamos proceder da seguinte maneira: com base no contexto GL existente, criaremos um novo que terá uma memória comum e, portanto, podemos usá-lo para reutilizar os id-shniks das texturas que criamos anteriormente lá. Em seguida, usando o novo contexto GL e Surface do MediaCodec, criaremos um EGLSurface - um buffer fora da tela no qual também podemos criar nossa classe OpenGLScene. Então, a cada renderização de quadro, tentaremos escrever o quadro no arquivo em paralelo.
EGL significa a interface de interação da API OpenGL com o subsistema de janelas da plataforma; roubaremos o trabalho da grafika. Também não descreverei o transportador (EncoderHelper) diretamente com o MediaCodec; darei apenas o esquema final de interação entre nossos componentes:
EncoderMachine.kt
EncoderHelper.kt

O resultado:
- Trabalhar com vídeo requer pelo menos habilidades básicas em OpenGL
- A API do Android Media é de nível bastante baixo, o que oferece flexibilidade, mas às vezes obriga a escrever um pouco mais de código do que você gostaria
- APIs assíncronas podem ser agrupadas em StateMachines