package expo.modules.bayutvideocompressor import android.graphics.SurfaceTexture import android.opengl.* import android.os.Handler import android.os.HandlerThread import android.view.Surface import java.nio.ByteBuffer import java.nio.ByteOrder import java.nio.FloatBuffer /** * OutputSurface handles the decoder→encoder surface-to-surface pipeline. * Decoded frames are rendered as an OpenGL texture, then drawn to the * encoder's input surface — achieving zero-copy GPU-accelerated transfer. */ class OutputSurface( private val encoderInputSurface: Surface, private val width: Int, private val height: Int ) : SurfaceTexture.OnFrameAvailableListener { private var eglDisplay: EGLDisplay = EGL14.EGL_NO_DISPLAY private var eglContext: EGLContext = EGL14.EGL_NO_CONTEXT private var eglSurface: EGLSurface = EGL14.EGL_NO_SURFACE private var surfaceTexture: SurfaceTexture? = null private var textureRenderer: TextureRenderer? = null var surface: Surface? = null private set private val frameSyncObject = Object() private var frameAvailable = false // Dedicated thread with looper for SurfaceTexture callbacks // This prevents the callback from depending on the main thread's availability private val callbackThread = HandlerThread("SurfaceTextureCallback").apply { start() } private val callbackHandler = Handler(callbackThread.looper) init { setupEgl() textureRenderer = TextureRenderer() textureRenderer!!.surfaceCreated() surfaceTexture = SurfaceTexture(textureRenderer!!.textureId) // Use dedicated handler thread for callbacks — critical for reliability surfaceTexture!!.setOnFrameAvailableListener(this, callbackHandler) surface = Surface(surfaceTexture) } private fun setupEgl() { eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY) if (eglDisplay == EGL14.EGL_NO_DISPLAY) throw RuntimeException("Unable to get EGL display") val version = IntArray(2) EGL14.eglInitialize(eglDisplay, version, 0, version, 1) val configAttribs = intArrayOf( EGL14.EGL_RED_SIZE, 8, EGL14.EGL_GREEN_SIZE, 8, EGL14.EGL_BLUE_SIZE, 8, EGL14.EGL_ALPHA_SIZE, 8, EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT, EGL14.EGL_NONE ) val configs = arrayOfNulls(1) val numConfigs = IntArray(1) EGL14.eglChooseConfig(eglDisplay, configAttribs, 0, configs, 0, 1, numConfigs, 0) val contextAttribs = intArrayOf( EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE ) eglContext = EGL14.eglCreateContext( eglDisplay, configs[0], EGL14.EGL_NO_CONTEXT, contextAttribs, 0 ) val surfaceAttribs = intArrayOf(EGL14.EGL_NONE) eglSurface = EGL14.eglCreateWindowSurface( eglDisplay, configs[0], encoderInputSurface, surfaceAttribs, 0 ) EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext) } fun awaitNewImage() { synchronized(frameSyncObject) { while (!frameAvailable) { frameSyncObject.wait(10_000) if (!frameAvailable) { throw RuntimeException("Timeout waiting for frame") } } frameAvailable = false } // Must be called on the thread with the EGL context surfaceTexture?.updateTexImage() } fun drawImage() { textureRenderer?.drawFrame(surfaceTexture!!) } fun setPresentationTime(nsecs: Long) { EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, nsecs) } fun swapBuffers() { EGL14.eglSwapBuffers(eglDisplay, eglSurface) } override fun onFrameAvailable(surfaceTexture: SurfaceTexture?) { synchronized(frameSyncObject) { if (frameAvailable) { // Drop duplicate frame notification return } frameAvailable = true frameSyncObject.notifyAll() } } fun release() { surface?.release() surface = null surfaceTexture?.release() surfaceTexture = null textureRenderer = null if (eglDisplay != EGL14.EGL_NO_DISPLAY) { EGL14.eglMakeCurrent(eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT) EGL14.eglDestroySurface(eglDisplay, eglSurface) EGL14.eglDestroyContext(eglDisplay, eglContext) EGL14.eglTerminate(eglDisplay) } eglDisplay = EGL14.EGL_NO_DISPLAY eglContext = EGL14.EGL_NO_CONTEXT eglSurface = EGL14.EGL_NO_SURFACE callbackThread.quitSafely() } } /** * Renders an external OES texture to the current surface using OpenGL ES 2.0. */ class TextureRenderer { private val TRIANGLE_VERTICES_DATA = floatArrayOf( // X, Y, Z, U, V -1.0f, -1.0f, 0f, 0f, 0f, 1.0f, -1.0f, 0f, 1f, 0f, -1.0f, 1.0f, 0f, 0f, 1f, 1.0f, 1.0f, 0f, 1f, 1f, ) private val VERTEX_SHADER = """ uniform mat4 uMVPMatrix; uniform mat4 uSTMatrix; attribute vec4 aPosition; attribute vec4 aTextureCoord; varying vec2 vTextureCoord; void main() { gl_Position = uMVPMatrix * aPosition; vTextureCoord = (uSTMatrix * aTextureCoord).xy; } """.trimIndent() private val FRAGMENT_SHADER = """ #extension GL_OES_EGL_image_external : require precision mediump float; varying vec2 vTextureCoord; uniform samplerExternalOES sTexture; void main() { gl_FragColor = texture2D(sTexture, vTextureCoord); } """.trimIndent() private val triangleVertices: FloatBuffer private val mvpMatrix = FloatArray(16) private val stMatrix = FloatArray(16) private var program = 0 private var muMVPMatrixHandle = 0 private var muSTMatrixHandle = 0 private var maPositionHandle = 0 private var maTextureHandle = 0 var textureId = -1 private set init { triangleVertices = ByteBuffer.allocateDirect( TRIANGLE_VERTICES_DATA.size * 4 ).order(ByteOrder.nativeOrder()).asFloatBuffer() triangleVertices.put(TRIANGLE_VERTICES_DATA).position(0) android.opengl.Matrix.setIdentityM(stMatrix, 0) } fun surfaceCreated() { program = createProgram(VERTEX_SHADER, FRAGMENT_SHADER) maPositionHandle = GLES20.glGetAttribLocation(program, "aPosition") maTextureHandle = GLES20.glGetAttribLocation(program, "aTextureCoord") muMVPMatrixHandle = GLES20.glGetUniformLocation(program, "uMVPMatrix") muSTMatrixHandle = GLES20.glGetUniformLocation(program, "uSTMatrix") val textures = IntArray(1) GLES20.glGenTextures(1, textures, 0) textureId = textures[0] GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId) GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR.toFloat()) GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR.toFloat()) GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE) GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE) android.opengl.Matrix.setIdentityM(mvpMatrix, 0) } fun drawFrame(st: SurfaceTexture) { st.getTransformMatrix(stMatrix) GLES20.glClearColor(0f, 0f, 0f, 1f) GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT or GLES20.GL_COLOR_BUFFER_BIT) GLES20.glUseProgram(program) GLES20.glActiveTexture(GLES20.GL_TEXTURE0) GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId) triangleVertices.position(0) GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, 20, triangleVertices) GLES20.glEnableVertexAttribArray(maPositionHandle) triangleVertices.position(3) GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 20, triangleVertices) GLES20.glEnableVertexAttribArray(maTextureHandle) GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mvpMatrix, 0) GLES20.glUniformMatrix4fv(muSTMatrixHandle, 1, false, stMatrix, 0) GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) GLES20.glDisableVertexAttribArray(maPositionHandle) GLES20.glDisableVertexAttribArray(maTextureHandle) } private fun createProgram(vertexSource: String, fragmentSource: String): Int { val vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource) val fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource) val program = GLES20.glCreateProgram() GLES20.glAttachShader(program, vertexShader) GLES20.glAttachShader(program, fragmentShader) GLES20.glLinkProgram(program) val linkStatus = IntArray(1) GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0) if (linkStatus[0] != GLES20.GL_TRUE) { val log = GLES20.glGetProgramInfoLog(program) GLES20.glDeleteProgram(program) throw RuntimeException("Could not link program: $log") } return program } private fun loadShader(shaderType: Int, source: String): Int { val shader = GLES20.glCreateShader(shaderType) GLES20.glShaderSource(shader, source) GLES20.glCompileShader(shader) val compiled = IntArray(1) GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0) if (compiled[0] == 0) { val log = GLES20.glGetShaderInfoLog(shader) GLES20.glDeleteShader(shader) throw RuntimeException("Could not compile shader: $log") } return shader } }