Android Permukaan

Penafian


Artikel ini ditujukan untuk pengembang android pemula dengan sedikit pengalaman bekerja dengan video dan / atau kamera, terutama mereka yang mulai menganalisis contoh-contoh grafika dan yang merasa kesulitan, di sini kita akan melihat kode yang sama dengan deskripsi sederhana dari langkah-langkah dasar yang diilustrasikan oleh diagram negara.


Mengapa kelas Surface diberikan di header? Di android, banyak kelas memiliki kata Surface dalam namanya (Surface, SurfaceHolder, SurfaceTexture, SurfaceView, GLSurfaceView), mereka tidak terhubung oleh hierarki umum, namun, mereka dikombinasikan oleh logika tingkat rendah untuk bekerja dengan output gambar. Tampaknya masuk akal bagi saya untuk menggunakannya dalam judul untuk menekankan upaya untuk mengungkapkan pekerjaan dengan bagian khusus dari SDK ini.


Contoh penggunaan dengan API yang berbeda


Mari kita coba menulis contoh berikut: kita akan mengambil pratinjau dari kamera, menayangkan gambar animasi di atasnya, menampilkan semuanya di layar dan, jika perlu, menuliskannya ke file. Kode lengkapnya ada di https://github.com/tttzof351/AndroidSurfaceExample/


Untuk tampilan, kami akan menggunakan GLSurfaceView , untuk merekam dengan kelas MediaCodec dan EGLSurface , dan berkomunikasi dengan kamera melalui API V2 . Skema umum kira-kira sebagai berikut:



Overlay Permukaan Berganda


Permukaan sebenarnya adalah pegangan untuk area dalam memori yang perlu diisi dengan gambar. Kemungkinan besar, kami membuatnya berusaha menampilkan sesuatu di layar atau dalam file, sehingga berfungsi seperti buffer untuk beberapa "proses" yang menghasilkan data.


Untuk membuat overlay dari beberapa Surface, kita akan menggunakan OpenGL.
Untuk melakukan ini, kita akan membuat dua tekstur eksternal persegi dan dapatkan dari Surface


Dalam kode tersebut, akan terlihat seperti ini:


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) 

Koordinat XYZ


Sekarang kita perlu memahami cara membuat dan mengatur tekstur, dan untuk ini kita harus ingat bagaimana struktur koordinat disusun dalam OpenGL: pusatnya bertepatan dengan pusat adegan (jendela), dan perbatasan dinormalisasi, mis. Dari -1 hingga 1.


Dalam adegan ini, kami ingin menetapkan dua persegi panjang (pekerjaan ada di pesawat, jadi semua koordinat z diatur secara logis ke 0f) - merah kami akan menunjukkan satu di mana kami akan menempatkan pratinjau untuk kamera, dan warna biru untuk gambar animasi:


Kami menuliskan koordinat kami secara eksplisit:



 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 ) 

Koordinat UV


Apakah ini cukup? Ternyata tidak ada :(


Tekstur adalah pemetaan gambar ke area adegan dan untuk membuatnya dengan benar, Anda perlu menentukan secara tepat di mana titik-titik dalam gambar akan jatuh di dalam area ini - untuk ini, OpenGL menggunakan koordinat UV - mereka keluar dari sudut kiri bawah dan memiliki batas dari 0 ke 1 untuk masing-masing kapak.


Ini berfungsi sebagai berikut - kami akan mengatur koordinat UV untuk setiap titik di daerah kami dan akan mencari titik yang sesuai pada gambar, dengan asumsi bahwa lebar dan tingginya sama dengan 1.


Pertimbangkan sebuah contoh - kita akan mengasumsikan bahwa kamera memberi kita gambar dalam keadaan terbalik dan terpantul, dan pada saat yang sama kita ingin menunjukkan hanya bagian kanan atas, mis. Ambil 0,8 pada garis lintang dan ketinggian gambar.


Titik halus - pada tahap ini kita tidak tahu rasio aspek dari area pada layar - kita hanya memiliki kuadrat dalam koordinat relatif, yang akan mengisi seluruh adegan dan sesuai peregangan. Jika kita membuat kamera layar penuh, maka ukuran relatif kita (2 di setiap sisi) akan meregang ke 1080x1920 konvensional. Kami berasumsi bahwa kami mengatur dimensi adegan sedemikian sehingga rasio mereka akan sama dengan rasio kamera.
Mari kita lihat ke mana koordinatnya pergi - titik kanan atas area kita (1, 1, 0) harus pergi ke koordinat UV (0, 0), yang kiri bawah di (0.8f, 0.8f), dll.



Dengan demikian, kami memperoleh korespondensi XYZ dan 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 

Jika rasio aspek antara pratinjau dari kamera dan area pada layar awalnya bertepatan, maka jelas akan terus disimpan, karena dalam kasus kami, kami hanya dikalikan dengan 0,8f.
Dan apa yang akan kita makan, kita akan menetapkan nilai lebih dari 1? Bergantung pada pengaturan yang kami berikan ke OpenGL, kami akan mendapatkan poin dari beberapa bagian gambar. Dalam contoh kita, baris terakhir akan diulangi sepanjang sumbu yang sesuai dan kita akan melihat artefak dalam bentuk "garis-garis"


Intinya: jika kita ingin mengompres / memotong gambar sambil mempertahankan posisi area pada layar, maka koordinat UV adalah pilihan kita!


Tetapkan koordinat untuk tekstur kami.



 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 ) 

Shader


Memiliki koordinat XYZ dan UV statis sangat tidak nyaman - misalnya, kita mungkin ingin memindahkan dan menskalakan tekstur kita dengan gerakan. Untuk mengubahnya, kita akan membuat dua matriks untuk setiap tekstur: MVPMatrix dan TexMatrix untuk masing-masing untuk koordinat XYZ dan UV.


Setiap OpenGL2 harus mengandung shader untuk menampilkan sesuatu di layar. Tentu saja, ini bukan topik yang dapat diungkapkan dalam satu paragraf, namun, dalam kasus kami, mereka akan sepele, dan karena itu Anda dapat dengan cepat memahami apa yang mereka lakukan, tanpa banyak pengetahuan tentang materi.


Pertama-tama, ada dua shader - vertex dan fragmen.


Yang pertama (vertex) akan memproses simpul kita, yaitu, cukup gandakan koordinat XYZ / UV kita dengan matriks yang sesuai dan isi dengan variabel OpenGL gl_Position yang persis bertanggung jawab atas posisi akhir dari tekstur kita di layar.


Yang kedua (fragmen) harus mengisi gl_FragColor dengan piksel gambar.


Total yang kita miliki: variabel di dalam vertex shader kita harus mengisi kolom dengan data kita, yaitu:


  • MVPMatrix -> uMVPMatrix
  • TexMatrix -> uTexMatrix
  • koordinat XYZ vertex kami -> aPosisi
  • Koordinat UV -> aTextureCoord

vTextureCoord - diperlukan untuk meneruskan data dari vertex shader ke fragment shader
Di fragmen shader, kami mengambil koordinat UV yang dikonversi dan menggunakannya untuk menampilkan piksel gambar di area tekstur.


 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); } """ 

Untuk referensi, kami menunjukkan perbedaan antara jenis:


  • seragam - variabel jenis ini akan mempertahankan nilai selama panggilan berulang, kami menggunakan satu shader yang disebut berurutan untuk dua tekstur, jadi kami masih akan menimpanya dengan setiap rendering
  • atribut - data jenis ini dibaca dari vertex buffer, perlu dimuat di setiap rendering
  • bervariasi - diperlukan untuk mentransfer data dari vertex shader ke fragmen

Bagaimana cara melewatkan parameter ke shader? Untuk melakukan ini, Anda harus terlebih dahulu mendapatkan id (pointer) dari variabel:


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

Sekarang untuk id ini Anda perlu memuat data:


 //      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 ) 

Gambar langsung


Setelah kami mengisi shader kami dengan semua data, kami harus meminta tekstur untuk memperbarui gambar, dan OpenGL untuk menggambar simpul kami:


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

Dalam contoh kami, kami akan membagi pekerjaan dengan adegan OpenGL menjadi dua kelas - adegan dan tekstur langsung:


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 / State Machine / State Machine


Semua API yang ingin kami gunakan dalam contoh kami pada dasarnya asinkron (well, mungkin dengan pengecualian Drawable animasi). Kami akan membungkus panggilan semacam itu di StateMachines yang terpisah, suatu pendekatan di mana status sistem secara eksplisit ditulis, dan transisi di antara mereka terjadi melalui pengiriman peristiwa.


Mari kita lihat contoh sederhana bagaimana ini akan terlihat, misalkan kita memiliki kode ini:


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

Secara umum, semuanya baik - baik dan indah, tetapi kami akan mencoba menulis ulang dengan cara berikut:


 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() } 

Di satu sisi, ternyata jauh lebih banyak , namun, beberapa properti implisit tetapi berguna muncul: pengulangan berulang sekarang tidak mengarah pada beban yang tidak perlu dimulai , meskipun tidak jelas dengan volume seperti itu, tapi kami menyingkirkan panggilan balik bersarang, yang akan kita gunakan nanti , dan gaya penulisan metode transisi memungkinkan Anda untuk membangun diagram transisi yang mengulang kode satu-satu, yaitu dalam kasus kami:



Gray menunjukkan transisi yang tidak ditulis secara eksplisit. Seringkali mereka dicatat atau dilempar pengecualian, menganggapnya sebagai tanda kesalahan. Untuk saat ini, kami akan berhasil mengabaikannya dan di masa depan kami tidak akan menunjuk ke diagram.


Buat antarmuka dasar untuk StateMachine:


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

GLSurfaceView


Cara termudah untuk menampilkan sesuatu menggunakan OpenGL di android adalah kelas GLSurfaceView - secara otomatis membuat aliran baru untuk menggambar, yang dimulai / dihentikan menggunakan metode GLSurfaceView :: onResume / onPause.


Untuk kesederhanaan, kami akan menetapkan pandangan kami ke rasio 16: 9.


Proses rendering itu sendiri dipindahkan ke callback terpisah - GLSurfaceView.Renderer.
Membungkusnya di StateMachine, kita mendapatkan sesuatu seperti ini:


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, ...)) 

Mari menggambar diagram transisi:



Sekarang kode kami mencoba untuk menampilkan sesuatu di layar, meskipun untuk saat ini ia melakukannya dengan buruk - kami tidak akan melihat apa pun selain layar hitam. Tidak sulit untuk menebak faktanya adalah bahwa tidak ada yang masuk ke Surface kami sekarang karena kami belum mengimplementasikan sumber gambar. Mari kita perbaiki ini - pertama, buat 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) } ... } 

Sekarang kita dapat melengkapi bagian dalam GLSurfaceMachine dengan merender canvasDrawable pada kanvas yang menyediakan permukaan tekstur yang sesuai:


 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() } 

Setelah itu kita akan melihat sesuatu seperti:



API Kamera V2


Kotak hijau tentu saja menyenangkan dan menarik, tetapi inilah saatnya untuk mencoba membawa pratinjau dari kamera ke permukaan yang tersisa.


Mari kita menuliskan langkah-langkah untuk bekerja dengan kamera:


  • Kami sedang menunggu izin. Kami akan memiliki status ini WaitingStart
  • Kami mendapatkan contoh manajer kamera, kami menemukan id logis (biasanya ada dua dari mereka - untuk bagian belakang dan depan, dan yang logis adalah karena kamera dapat terdiri dari banyak sensor pada perangkat modern) dari kamera yang diinginkan, pilih ukuran yang sesuai, buka kamera, kami dapatkan 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) { //Success open camera ... } }) } 

  • Memiliki kamera terbuka, kita beralih untuk mengajukan Permukaan untuk menampilkan gambar. Status Menunggu Permukaan
  • Sekarang kita memiliki CameraDevice, Surface, kita perlu membuka sesi sehingga kamera akhirnya mulai mengirimkan data. Status Tunggu Sesi

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

  • Sekarang kita dapat menangkap pratinjau. Status MulaiPreview

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

Kami menggambarkan skema kami saat ini:


CameraMachine.kt




Mediacodec


MediaCodec adalah kelas untuk pekerjaan tingkat rendah dengan codec sistem, secara umum, API-nya adalah sekumpulan buffer input / output (terdengar, sayangnya, lebih mudah daripada bekerja dengannya) ke dalam mana data (mentah atau dikodekan tergantung pada mode operasi encoder / decoder) ditempatkan, dan pada output kita mendapatkan hasilnya.


Terlepas dari kenyataan bahwa ByteBuffer biasanya bertindak sebagai buffer, Anda dapat menggunakan Surface untuk bekerja dengan video, yang akan mengembalikan MediaCodec :: createInputSurface kepada kami, di atasnya kami harus menggambar bingkai yang ingin kami rekam (dengan pendekatan ini, dokumentasi menjanjikan kami pengkodean lebih cepat melalui penggunaan gpu )


Nah, sekarang kita perlu belajar cara menggambar Surface yang ada yang kita buat di GLSurfaceMachine on Surface dari MediaCodec. Penting untuk diingat: Permukaan adalah objek yang menciptakan konsumen, dan secara umum tidak mungkin untuk membaca sesuatu darinya, mis. Tidak ada metode bersyarat getBitmap / readImage / ...


Kami akan melanjutkan sebagai berikut: berdasarkan konteks GL yang ada, kami akan membuat yang baru yang akan memiliki memori yang sama dengannya, dan oleh karena itu kami dapat menggunakannya untuk menggunakan id-shniks dari tekstur yang kami buat sebelumnya di sana. Kemudian menggunakan konteks GL baru dan Permukaan dari MediaCodec, kita akan membuat EGLSurface - penyangga di luar layar tempat kita juga dapat membuat kelas OpenGLScene kita. Kemudian, pada setiap frame rendering, kita akan mencoba menulis frame ke file secara paralel.


EGL berarti antarmuka interaksi API OpenGL dengan subsistem jendela platform, kami akan mencuri pekerjaan dari grafika. Saya tidak akan menggambarkan conveyor (EncoderHelper) dengan MediaCodec secara langsung, saya hanya akan memberikan skema terakhir interaksi antara komponen-komponen kami:


EncoderMachine.kt
EncoderHelper.kt



Hasilnya:


  • Bekerja dengan video membutuhkan setidaknya keterampilan dasar dalam OpenGL
  • Android Media API adalah level yang cukup rendah, yang memberikan fleksibilitas, tetapi terkadang membuat Anda menulis kode sedikit lebih banyak dari yang Anda inginkan
  • API Asinkron dapat dibungkus dalam StateMachines

Source: https://habr.com/ru/post/id480878/


All Articles