Haftungsausschluss
Dieser Artikel ist für Anfänger gedacht, die mit Android wenig Erfahrung im Umgang mit Videos und / oder Kameras haben, insbesondere für diejenigen, die mit der Analyse von Grafika- Beispielen begonnen haben und diese als schwierig empfanden. Hier sehen wir uns ähnlichen Code mit einer vereinfachten Beschreibung der grundlegenden Schritte an, die durch Zustandsdiagramme veranschaulicht werden.
Warum wird die Surface-Klasse im Header gerendert? In Android haben viele Klassen das Wort Surface in ihrem Namen (Surface, SurfaceHolder, SurfaceTexture, SurfaceView, GLSurfaceView), sie sind nicht durch eine gemeinsame Hierarchie verbunden, sie werden jedoch durch eine Logik auf niedriger Ebene für die Arbeit mit der Bildausgabe kombiniert. Es erschien mir vernünftig, es im Titel zu verwenden, um einen Versuch hervorzuheben, die Arbeit mit diesem bestimmten Teil des SDK offenzulegen.
Beispielanwendung mit unterschiedlicher API
Versuchen wir folgendes Beispiel zu schreiben: Wir nehmen eine Vorschau von der Kamera, überlagern eine animierte Zeichnung, zeigen alles auf dem Bildschirm an und schreiben, falls erforderlich, in eine Datei. Der vollständige Code wird https://github.com/tttzof351/AndroidSurfaceExample/ liegen.
Für die Ausgabe auf den Bildschirmen verwenden wir GLSurfaceView , um mit den Klassen MediaCodec und EGLSurface aufzuzeichnen und über API V2 mit der Kamera zu kommunizieren. Das allgemeine Schema ist ungefähr wie folgt:

Mehrere Oberflächenüberlagerungen
Die Oberfläche ist eigentlich ein Anfasser für den Bereich im Speicher, der mit dem Bild gefüllt werden muss. Höchstwahrscheinlich versucht es, etwas auf dem Bildschirm oder in einer Datei anzuzeigen, sodass es wie ein Puffer für einen „Prozess“ funktioniert, der Daten erzeugt.
Um ein Overlay aus mehreren Surface zu erstellen, verwenden wir OpenGL.
Zu diesem Zweck werden zwei quadratische externe Texturen erstellt und daraus eine Oberfläche erstellt
Im Code sieht es ungefähr so aus:
OpenGLExtarnalTexture.kt
val textures = IntArray(1) GLES20.glGenTextures(1, textures, 0) val textureId = textures[0]
XYZ-Koordinaten
Jetzt müssen wir verstehen, wie man Texturen erstellt und anordnet. Dazu müssen wir uns merken, wie das Koordinatengitter in OpenGL angeordnet ist: Sein Mittelpunkt fällt mit dem Mittelpunkt der Szene (Fenster) zusammen, und die Ränder werden normalisiert, d. H. Von -1 bis 1.
In dieser Szene möchten wir zwei Rechtecke setzen (die Arbeit befindet sich in der Ebene, sodass alle z-Koordinaten logisch auf 0f gesetzt sind) - in Rot geben wir diejenige an, in der wir die Vorschau für die Kamera platzieren, und in Blau für das animierte Zeichenobjekt:
Wir schreiben unsere Koordinaten explizit auf:

fullscreenTexture = floatArrayOf(
UV-Koordinaten
Reicht das Es stellt sich heraus, dass nein :(
Eine Textur ist eine Abbildung eines Bildes auf einen Szenenbereich. Um dies korrekt zu machen, müssen Sie genau angeben, wo die Punkte im Bild in diesen Bereich fallen. OpenGL verwendet dazu UV- Koordinaten. Sie kommen aus der unteren linken Ecke und haben Ränder von 0 bis 1 Achsen.
Es funktioniert wie folgt: Wir setzen UV- Koordinaten für jeden Scheitelpunkt unserer Fläche und suchen nach den entsprechenden Punkten im Bild, vorausgesetzt, dass dort Breite und Höhe gleich 1 sind.
Betrachten Sie ein Beispiel - wir gehen davon aus, dass die Kamera das Bild in einem invertierten und reflektierten Zustand liefert, und gleichzeitig möchten wir nur den rechten oberen Teil anzeigen, d. H. 0,8 in Breite und Höhe des Bildes.
Der subtile Punkt - zu diesem Zeitpunkt kennen wir das Seitenverhältnis des Bereichs auf dem Bildschirm nicht - besteht nur aus einem Quadrat in relativen Koordinaten, das die gesamte Szene ausfüllt und entsprechend gedehnt wird. Wenn wir eine Vollbildkamera herstellen würden, würden sich unsere relativen Größen (2 auf jeder Seite) auf die herkömmlichen 1080x1920 erstrecken. Wir gehen davon aus, dass wir die Dimensionen der Szene so einstellen, dass ihr Verhältnis dem Verhältnis der Kamera entspricht.
Mal sehen, wohin die Koordinaten gehen - der obere rechte Punkt unserer Fläche (1, 1, 0) sollte zur UV-Koordinate (0, 0) gehen, der untere linke in (0.8f, 0.8f) usw.

So erhalten wir die Entsprechung von XYZ und UV:
Wenn das Seitenverhältnis zwischen der Vorschau von der Kamera und dem Bereich auf dem Bildschirm anfänglich übereinstimmt, wird es offensichtlich weiterhin gespeichert, da wir in unserem Fall nur mit 0,8f multipliziert haben.
Und was werden wir essen, wir werden Werte größer als 1 festlegen? Abhängig von den Einstellungen, die wir an OpenGL übergeben haben, erhalten wir Punkte für einen Teil des Bildes. In unserem Beispiel wird die letzte Zeile entlang der entsprechenden Achse wiederholt, und es werden Artefakte in Form von "Streifen" angezeigt.
Fazit: Wenn wir das Bild unter Beibehaltung der Position des Bereichs auf dem Bildschirm komprimieren / ausschneiden möchten, sind die UV-Koordinaten unsere Wahl!
Stellen Sie die Koordinaten für unsere Texturen ein.

fullscreenTexture = floatArrayOf(
Shader
Statische XYZ- und UV-Koordinaten sind nicht sehr praktisch. Wir möchten beispielsweise unsere Texturen mit Gesten verschieben und skalieren. Um sie zu transformieren, erstellen wir zwei Matrizen für jede Textur: MVPMatrix und TexMatrix für XYZ- bzw. UV-Koordinaten.
Jedes OpenGL2 muss Shader enthalten, um etwas auf dem Bildschirm anzuzeigen. Natürlich ist dies kein Thema, das in einem Absatz offengelegt werden kann, in unserem Fall sind sie jedoch trivial, und daher können Sie schnell verstehen, was sie tun, ohne viel Wissen über das Material.
Zunächst gibt es zwei Shader - Vertex und Fragment.
Der erste (Vertex) verarbeitet unsere Vertices, dh multipliziert einfach unsere XYZ / UV-Koordinaten mit den entsprechenden Matrizen und fügt die OpenGL-Variable gl_Position ein, die genau für die endgültige Position unserer Textur auf dem Bildschirm verantwortlich ist.
Das zweite (Fragment) sollte gl_FragColor mit Bildpixeln füllen.
Insgesamt haben wir: die Variablen im Vertex-Shader, die wir in die Felder mit unseren Daten eintragen müssen, nämlich:
- MVPMatrix -> uMVPMatrix
- TexMatrix -> uTexMatrix
- unsere XYZ-Eckpunktkoordinaten -> aPosition
- UV-Koordinaten -> aTextureCoord
vTextureCoord - wird benötigt, um Daten vom Vertex-Shader an den Fragment-Shader weiterzuleiten
Im Fragment-Shader nehmen wir die konvertierten UV-Koordinaten und verwenden sie, um die Bildpixel im Texturbereich anzuzeigen.
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); } """
Als Referenz geben wir den Unterschied zwischen den Typen an:
- uniform - Eine Variable dieses Typs behält Werte bei wiederholten Aufrufen bei. Wir verwenden einen Shader, der nacheinander für zwei Texturen aufgerufen wird, sodass wir ihn bei jedem Rendern überschreiben
- Attribut - Daten dieses Typs werden aus dem Vertex-Puffer gelesen und müssen bei jedem Rendern geladen werden
- Variieren - Wird benötigt, um Daten vom Vertex-Shader zum Fragment zu übertragen
Wie übergebe ich Parameter an einen Shader? Dazu müssen Sie zuerst die ID (den Zeiger) der Variablen abrufen:
val aPositionHandle = GLES20.glGetAttribLocation(programId, "aPosition")
Für diese ID müssen Sie nun die Daten laden:
Direkte Zeichnung
Nachdem wir unsere Shader mit allen Daten gefüllt haben, sollten wir die Textur bitten, das Bild zu aktualisieren und OpenGL, um unsere Eckpunkte zu zeichnen:
fun updateFrame(...) { ... surfaceTexture.updateTexImage() GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) }
In unserem Beispiel werden wir die Arbeit mit der OpenGL-Szene in zwei Klassen aufteilen - direkt Szenen und Texturen:
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 {
StateMachine / State Machine / State Machine
Alle APIs, die wir in unserem Beispiel verwenden möchten, sind grundsätzlich asynchron (naja, vielleicht mit Ausnahme des animierten Drawable). Wir werden solche Aufrufe in separate StateMachines einbinden, einen Ansatz, bei dem die Systemzustände explizit ausgeschrieben werden und Übergänge zwischen ihnen durch das Senden von Ereignissen auftreten.
Schauen wir uns ein einfaches Beispiel an, wie dies aussehen wird. Nehmen wir an, wir haben diesen Code:
imageView.setOnClickListener { loadImage { bitmap -> imageView.setBitmap(bitmap) } }
Im Allgemeinen ist alles in Ordnung - schön und kompakt, aber wir werden versuchen, es folgendermaßen umzuschreiben:
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() }
Einerseits stellte sich viel mehr heraus, dennoch erschienen einige implizite, aber nützliche Eigenschaften: Wiederholtes Drücken führt jetzt nicht zu unnötigen loadImage- Starts , obwohl dies bei einem solchen Volume nicht offensichtlich ist, aber wir haben den verschachtelten Rückruf beseitigt, den wir später verwenden werden und der Schreibstil der Übergangsmethode ermöglicht es Ihnen, ein Übergangsdiagramm zu erstellen, das den Code eins zu eins wiederholt, d. h. in unserem Fall:

Grau kennzeichnet Übergänge, die nicht explizit ausgeschrieben sind. Oft werden sie protokolliert oder lösen eine Ausnahme aus, da dies ein Anzeichen für einen Fehler ist. Wir werden es vorerst schaffen, es einfach zu ignorieren und in Zukunft nicht mehr auf die Diagramme zu verweisen.
Erstellen Sie die Basisschnittstellen für StateMachine:
interface Action interface State interface StateMachine<S : State, A : Action> { var state: S fun transition(action: A) fun send(action: A) }
GLSurfaceView
Der einfachste Weg, etwas mit OpenGL in Android anzuzeigen, ist die GLSurfaceView-Klasse - sie erstellt automatisch einen neuen Stream zum Zeichnen, der mit den GLSurfaceView :: onResume / onPause-Methoden gestartet / angehalten wird.
Der Einfachheit halber werden wir unsere Ansicht auf ein Verhältnis von 16: 9 einstellen.
Der Rendervorgang selbst wird in einen separaten Rückruf verschoben - GLSurfaceView.Renderer.
Wenn wir es in StateMachine einpacken, bekommen wir so etwas:
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, ...))
Zeichnen wir ein Übergangsdiagramm:

Jetzt versucht unser Code, etwas auf dem Bildschirm anzuzeigen, aber im Moment funktioniert es nur schlecht - wir werden nichts anderes als einen schwarzen Bildschirm sehen. Es ist nicht schwer zu erraten, dass jetzt nichts in unsere Oberfläche gelangt, da wir noch keine Bildquellen implementiert haben. Lass uns das beheben - erstens erstelle ein 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) } ... }
Jetzt können wir den Abschnitt in GLSurfaceMachine ergänzen, indem wir canvasDrawable auf canvas rendern, wodurch die Oberfläche der entsprechenden Textur bereitgestellt wird:
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() }
Danach sehen wir etwas wie:

Kamera-API V2
Das grüne Rechteck macht sicherlich Spaß und ist faszinierend, aber es ist an der Zeit, die Vorschau von der Kamera auf die verbleibende Oberfläche zu bringen.
Lassen Sie uns die Schritte für die Arbeit mit der Kamera aufschreiben:
- Wir warten auf Erlaubnis. Wir werden diesen Zustand WaitingStart haben
- Wir bekommen die Kamera-Manager-Instanz, wir finden die logische ID (normalerweise gibt es zwei davon - für die Rückseite und die Vorderseite, und die logische ist, weil die Kamera auf modernen Geräten aus vielen Sensoren bestehen kann) der gewünschten Kamera, wählen die entsprechende Größe, öffnen die Kamera, wir bekommen 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) {
- Bei geöffneter Kamera wenden wir uns an eine Oberfläche, um das Bild anzuzeigen. WaitingSurface Status
- Nachdem wir cameraDevice, Surface haben, müssen wir eine Sitzung eröffnen, damit die Kamera endlich mit der Datenübertragung beginnt. Wartesitzungsstatus
cameraDevice.createCaptureSession( arrayListOf(surface), object : CameraCaptureSession.StateCallback() { override fun onConfigured(session: CameraCaptureSession) { send(CameraAction.SessionReady(session)) } }, handler )
- Jetzt können wir die Vorschau erfassen. StartingPreview Status
val request = cameraDevice.createCaptureRequest( CameraDevice.TEMPLATE_PREVIEW ).apply { addTarget(surface) } session.setRepeatingRequest( request.build(), object : CameraCaptureSession.CaptureCallback() {...} handler )
Wir veranschaulichen unser aktuelles Schema:
CameraMachine.kt


MediaCodec ist eine Klasse für die Arbeit mit Systemcodecs auf niedriger Ebene. Im Allgemeinen besteht die API aus einer Reihe von Eingabe- / Ausgabepuffern (das klingt leider einfacher als damit zu arbeiten), in denen Daten (roh oder codiert, abhängig von der Betriebsart des Codierers / Decodierers) abgelegt werden Am Ausgang erhalten wir das Ergebnis.
Obwohl ByteBuffer normalerweise als Puffer fungiert, können Sie Surface für die Arbeit mit Video verwenden, wodurch MediaCodec :: createInputSurface an uns zurückgegeben wird. Darauf sollten wir die aufzuzeichnenden Frames zeichnen (bei diesem Ansatz verspricht uns die Dokumentation eine schnellere Codierung durch die Verwendung von GPU )
Nun müssen wir lernen, wie wir die vorhandene Oberfläche zeichnen, die wir in GLSurfaceMachine auf Surface von MediaCodec erstellt haben. Es ist wichtig, sich daran zu erinnern, dass Surface ein Objekt ist, das einen Konsumenten erzeugt, und im Allgemeinen ist es unmöglich, etwas daraus zu lesen, d. H. Es gibt keine bedingte Methode getBitmap / readImage / ...
Wir werden wie folgt vorgehen: Auf der Grundlage des vorhandenen GL-Kontexts werden wir einen neuen erstellen, der ein gemeinsames Gedächtnis hat, und daher können wir ihn verwenden, um die ID-Shniks der Texturen, die wir zuvor dort erstellt haben, wiederzuverwenden. Mit dem neuen GL-Kontext und Surface von MediaCodec erstellen wir dann ein EGLSurface - einen Off-Screen-Puffer, auf dem wir auch unsere OpenGLScene-Klasse erstellen können. Dann versuchen wir bei jedem Frame-Rendering, den Frame parallel in die Datei zu schreiben.
EGL bedeutet die Schnittstelle der Interaktion der OpenGL-API mit dem Fenstersubsystem der Plattform, wir werden die Arbeit damit von grafika stehlen. Ich werde den Förderer (EncoderHelper) mit MediaCodec auch nicht direkt beschreiben, sondern nur das endgültige Interaktionsschema zwischen unseren Komponenten angeben:
EncoderMachine.kt
EncoderHelper.kt

Das ergebnis:
- Das Arbeiten mit Videos erfordert mindestens Grundkenntnisse in OpenGL
- Die Android Media-API ist recht einfach, was Flexibilität bietet, aber manchmal zwingt sie Sie dazu, etwas mehr Code zu schreiben, als Sie möchten
- Asynchrone APIs können in StateMachines eingeschlossen werden