Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2015 The WebRTC project authors. All Rights Reserved. |
| 3 | * |
| 4 | * Use of this source code is governed by a BSD-style license |
| 5 | * that can be found in the LICENSE file in the root of the source |
| 6 | * tree. An additional intellectual property rights grant can be found |
| 7 | * in the file PATENTS. All contributing project authors may |
| 8 | * be found in the AUTHORS file in the root of the source tree. |
| 9 | */ |
| 10 | |
| 11 | package org.webrtc; |
| 12 | |
| 13 | import android.opengl.GLES11Ext; |
| 14 | import android.opengl.GLES20; |
| 15 | import java.nio.ByteBuffer; |
| 16 | import java.nio.FloatBuffer; |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 17 | import org.webrtc.VideoFrame.I420Buffer; |
| 18 | import org.webrtc.VideoFrame.TextureBuffer; |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 19 | |
| 20 | /** |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 21 | * Class for converting OES textures to a YUV ByteBuffer. It should be constructed on a thread with |
| 22 | * an active EGL context, and only be used from that thread. |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 23 | */ |
Sami Kalliomäki | 6bf70d2 | 2017-10-17 09:22:23 +0200 | [diff] [blame] | 24 | public class YuvConverter { |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 25 | // Vertex coordinates in Normalized Device Coordinates, i.e. |
| 26 | // (-1, -1) is bottom-left and (1, 1) is top-right. |
| 27 | private static final FloatBuffer DEVICE_RECTANGLE = GlUtil.createFloatBuffer(new float[] { |
| 28 | -1.0f, -1.0f, // Bottom left. |
| 29 | 1.0f, -1.0f, // Bottom right. |
| 30 | -1.0f, 1.0f, // Top left. |
| 31 | 1.0f, 1.0f, // Top right. |
| 32 | }); |
| 33 | |
| 34 | // Texture coordinates - (0, 0) is bottom-left and (1, 1) is top-right. |
| 35 | private static final FloatBuffer TEXTURE_RECTANGLE = GlUtil.createFloatBuffer(new float[] { |
| 36 | 0.0f, 0.0f, // Bottom left. |
| 37 | 1.0f, 0.0f, // Bottom right. |
| 38 | 0.0f, 1.0f, // Top left. |
| 39 | 1.0f, 1.0f // Top right. |
| 40 | }); |
| 41 | |
| 42 | // clang-format off |
| 43 | private static final String VERTEX_SHADER = |
| 44 | "varying vec2 interp_tc;\n" |
| 45 | + "attribute vec4 in_pos;\n" |
| 46 | + "attribute vec4 in_tc;\n" |
| 47 | + "\n" |
| 48 | + "uniform mat4 texMatrix;\n" |
| 49 | + "\n" |
| 50 | + "void main() {\n" |
| 51 | + " gl_Position = in_pos;\n" |
| 52 | + " interp_tc = (texMatrix * in_tc).xy;\n" |
| 53 | + "}\n"; |
| 54 | |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 55 | private static final String OES_FRAGMENT_SHADER = |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 56 | "#extension GL_OES_EGL_image_external : require\n" |
| 57 | + "precision mediump float;\n" |
| 58 | + "varying vec2 interp_tc;\n" |
| 59 | + "\n" |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 60 | + "uniform samplerExternalOES tex;\n" |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 61 | // Difference in texture coordinate corresponding to one |
| 62 | // sub-pixel in the x direction. |
| 63 | + "uniform vec2 xUnit;\n" |
| 64 | // Color conversion coefficients, including constant term |
| 65 | + "uniform vec4 coeffs;\n" |
| 66 | + "\n" |
| 67 | + "void main() {\n" |
| 68 | // Since the alpha read from the texture is always 1, this could |
| 69 | // be written as a mat4 x vec4 multiply. However, that seems to |
| 70 | // give a worse framerate, possibly because the additional |
| 71 | // multiplies by 1.0 consume resources. TODO(nisse): Could also |
| 72 | // try to do it as a vec3 x mat3x4, followed by an add in of a |
| 73 | // constant vector. |
| 74 | + " gl_FragColor.r = coeffs.a + dot(coeffs.rgb,\n" |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 75 | + " texture2D(tex, interp_tc - 1.5 * xUnit).rgb);\n" |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 76 | + " gl_FragColor.g = coeffs.a + dot(coeffs.rgb,\n" |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 77 | + " texture2D(tex, interp_tc - 0.5 * xUnit).rgb);\n" |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 78 | + " gl_FragColor.b = coeffs.a + dot(coeffs.rgb,\n" |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 79 | + " texture2D(tex, interp_tc + 0.5 * xUnit).rgb);\n" |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 80 | + " gl_FragColor.a = coeffs.a + dot(coeffs.rgb,\n" |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 81 | + " texture2D(tex, interp_tc + 1.5 * xUnit).rgb);\n" |
| 82 | + "}\n"; |
| 83 | |
| 84 | private static final String RGB_FRAGMENT_SHADER = |
| 85 | "precision mediump float;\n" |
| 86 | + "varying vec2 interp_tc;\n" |
| 87 | + "\n" |
| 88 | + "uniform sample2D tex;\n" |
| 89 | // Difference in texture coordinate corresponding to one |
| 90 | // sub-pixel in the x direction. |
| 91 | + "uniform vec2 xUnit;\n" |
| 92 | // Color conversion coefficients, including constant term |
| 93 | + "uniform vec4 coeffs;\n" |
| 94 | + "\n" |
| 95 | + "void main() {\n" |
| 96 | // Since the alpha read from the texture is always 1, this could |
| 97 | // be written as a mat4 x vec4 multiply. However, that seems to |
| 98 | // give a worse framerate, possibly because the additional |
| 99 | // multiplies by 1.0 consume resources. TODO(nisse): Could also |
| 100 | // try to do it as a vec3 x mat3x4, followed by an add in of a |
| 101 | // constant vector. |
| 102 | + " gl_FragColor.r = coeffs.a + dot(coeffs.rgb,\n" |
| 103 | + " texture2D(tex, interp_tc - 1.5 * xUnit).rgb);\n" |
| 104 | + " gl_FragColor.g = coeffs.a + dot(coeffs.rgb,\n" |
| 105 | + " texture2D(tex, interp_tc - 0.5 * xUnit).rgb);\n" |
| 106 | + " gl_FragColor.b = coeffs.a + dot(coeffs.rgb,\n" |
| 107 | + " texture2D(tex, interp_tc + 0.5 * xUnit).rgb);\n" |
| 108 | + " gl_FragColor.a = coeffs.a + dot(coeffs.rgb,\n" |
| 109 | + " texture2D(tex, interp_tc + 1.5 * xUnit).rgb);\n" |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 110 | + "}\n"; |
| 111 | // clang-format on |
| 112 | |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 113 | private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker(); |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 114 | private final GlTextureFrameBuffer textureFrameBuffer; |
| 115 | private TextureBuffer.Type shaderTextureType; |
| 116 | private GlShader shader; |
| 117 | private int texMatrixLoc; |
| 118 | private int xUnitLoc; |
| 119 | private int coeffsLoc; |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 120 | private boolean released = false; |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 121 | |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 122 | /** |
| 123 | * This class should be constructed on a thread that has an active EGL context. |
| 124 | */ |
| 125 | public YuvConverter() { |
| 126 | threadChecker.checkIsOnValidThread(); |
sakal | 2fcd2dd | 2017-01-18 03:21:10 -0800 | [diff] [blame] | 127 | textureFrameBuffer = new GlTextureFrameBuffer(GLES20.GL_RGBA); |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 128 | } |
| 129 | |
| 130 | /** Converts the texture buffer to I420. */ |
| 131 | public I420Buffer convert(TextureBuffer textureBuffer) { |
| 132 | final int width = textureBuffer.getWidth(); |
| 133 | final int height = textureBuffer.getHeight(); |
| 134 | |
| 135 | // SurfaceTextureHelper requires a stride that is divisible by 8. Round width up. |
| 136 | // See SurfaceTextureHelper for details on the size and format. |
| 137 | final int stride = ((width + 7) / 8) * 8; |
| 138 | final int uvHeight = (height + 1) / 2; |
| 139 | // Due to the layout used by SurfaceTextureHelper, vPos + stride * uvHeight would overrun the |
| 140 | // buffer. Add one row at the bottom to compensate for this. There will never be data in the |
| 141 | // extra row, but now other code does not have to deal with v stride * v height exceeding the |
| 142 | // buffer's capacity. |
| 143 | final int size = stride * (height + uvHeight + 1); |
| 144 | ByteBuffer buffer = JniCommon.allocateNativeByteBuffer(size); |
| 145 | convert(buffer, width, height, stride, textureBuffer.getTextureId(), |
| 146 | RendererCommon.convertMatrixFromAndroidGraphicsMatrix(textureBuffer.getTransformMatrix()), |
| 147 | textureBuffer.getType()); |
| 148 | |
| 149 | final int yPos = 0; |
| 150 | final int uPos = yPos + stride * height; |
| 151 | // Rows of U and V alternate in the buffer, so V data starts after the first row of U. |
| 152 | final int vPos = uPos + stride / 2; |
| 153 | |
| 154 | buffer.position(yPos); |
| 155 | buffer.limit(yPos + stride * height); |
| 156 | ByteBuffer dataY = buffer.slice(); |
| 157 | |
| 158 | buffer.position(uPos); |
| 159 | buffer.limit(uPos + stride * uvHeight); |
| 160 | ByteBuffer dataU = buffer.slice(); |
| 161 | |
| 162 | buffer.position(vPos); |
| 163 | buffer.limit(vPos + stride * uvHeight); |
| 164 | ByteBuffer dataV = buffer.slice(); |
| 165 | |
| 166 | // SurfaceTextureHelper uses the same stride for Y, U, and V data. |
| 167 | return JavaI420Buffer.wrap(width, height, dataY, stride, dataU, stride, dataV, stride, |
| 168 | () -> { JniCommon.freeNativeByteBuffer(buffer); }); |
| 169 | } |
| 170 | |
| 171 | /** Deprecated, use convert(TextureBuffer). */ |
| 172 | @Deprecated |
| 173 | void convert(ByteBuffer buf, int width, int height, int stride, int srcTextureId, |
| 174 | float[] transformMatrix) { |
| 175 | convert(buf, width, height, stride, srcTextureId, transformMatrix, TextureBuffer.Type.OES); |
| 176 | } |
| 177 | |
| 178 | private void initShader(TextureBuffer.Type textureType) { |
| 179 | if (shader != null) { |
| 180 | shader.release(); |
| 181 | } |
| 182 | |
| 183 | final String fragmentShader; |
| 184 | switch (textureType) { |
| 185 | case OES: |
| 186 | fragmentShader = OES_FRAGMENT_SHADER; |
| 187 | break; |
| 188 | case RGB: |
| 189 | fragmentShader = RGB_FRAGMENT_SHADER; |
| 190 | break; |
| 191 | default: |
| 192 | throw new IllegalArgumentException("Unsupported texture type."); |
| 193 | } |
| 194 | |
| 195 | shaderTextureType = textureType; |
| 196 | shader = new GlShader(VERTEX_SHADER, fragmentShader); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 197 | shader.useProgram(); |
| 198 | texMatrixLoc = shader.getUniformLocation("texMatrix"); |
| 199 | xUnitLoc = shader.getUniformLocation("xUnit"); |
| 200 | coeffsLoc = shader.getUniformLocation("coeffs"); |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 201 | GLES20.glUniform1i(shader.getUniformLocation("tex"), 0); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 202 | GlUtil.checkNoGLES2Error("Initialize fragment shader uniform values."); |
| 203 | // Initialize vertex shader attributes. |
| 204 | shader.setVertexAttribArray("in_pos", 2, DEVICE_RECTANGLE); |
| 205 | // If the width is not a multiple of 4 pixels, the texture |
| 206 | // will be scaled up slightly and clipped at the right border. |
| 207 | shader.setVertexAttribArray("in_tc", 2, TEXTURE_RECTANGLE); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 208 | } |
| 209 | |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 210 | private void convert(ByteBuffer buf, int width, int height, int stride, int srcTextureId, |
| 211 | float[] transformMatrix, TextureBuffer.Type textureType) { |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 212 | threadChecker.checkIsOnValidThread(); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 213 | if (released) { |
| 214 | throw new IllegalStateException("YuvConverter.convert called on released object"); |
| 215 | } |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 216 | if (textureType != shaderTextureType) { |
| 217 | initShader(textureType); |
| 218 | } |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 219 | |
| 220 | // We draw into a buffer laid out like |
| 221 | // |
| 222 | // +---------+ |
| 223 | // | | |
| 224 | // | Y | |
| 225 | // | | |
| 226 | // | | |
| 227 | // +----+----+ |
| 228 | // | U | V | |
| 229 | // | | | |
| 230 | // +----+----+ |
| 231 | // |
| 232 | // In memory, we use the same stride for all of Y, U and V. The |
| 233 | // U data starts at offset |height| * |stride| from the Y data, |
| 234 | // and the V data starts at at offset |stride/2| from the U |
| 235 | // data, with rows of U and V data alternating. |
| 236 | // |
| 237 | // Now, it would have made sense to allocate a pixel buffer with |
| 238 | // a single byte per pixel (EGL10.EGL_COLOR_BUFFER_TYPE, |
| 239 | // EGL10.EGL_LUMINANCE_BUFFER,), but that seems to be |
| 240 | // unsupported by devices. So do the following hack: Allocate an |
| 241 | // RGBA buffer, of width |stride|/4. To render each of these |
| 242 | // large pixels, sample the texture at 4 different x coordinates |
| 243 | // and store the results in the four components. |
| 244 | // |
| 245 | // Since the V data needs to start on a boundary of such a |
| 246 | // larger pixel, it is not sufficient that |stride| is even, it |
| 247 | // has to be a multiple of 8 pixels. |
| 248 | |
| 249 | if (stride % 8 != 0) { |
| 250 | throw new IllegalArgumentException("Invalid stride, must be a multiple of 8"); |
| 251 | } |
| 252 | if (stride < width) { |
| 253 | throw new IllegalArgumentException("Invalid stride, must >= width"); |
| 254 | } |
| 255 | |
| 256 | int y_width = (width + 3) / 4; |
| 257 | int uv_width = (width + 7) / 8; |
| 258 | int uv_height = (height + 1) / 2; |
| 259 | int total_height = height + uv_height; |
| 260 | int size = stride * total_height; |
| 261 | |
| 262 | if (buf.capacity() < size) { |
| 263 | throw new IllegalArgumentException("YuvConverter.convert called with too small buffer"); |
| 264 | } |
| 265 | // Produce a frame buffer starting at top-left corner, not |
| 266 | // bottom-left. |
| 267 | transformMatrix = |
| 268 | RendererCommon.multiplyMatrices(transformMatrix, RendererCommon.verticalFlipMatrix()); |
| 269 | |
sakal | 2fcd2dd | 2017-01-18 03:21:10 -0800 | [diff] [blame] | 270 | final int frameBufferWidth = stride / 4; |
| 271 | final int frameBufferHeight = total_height; |
| 272 | textureFrameBuffer.setSize(frameBufferWidth, frameBufferHeight); |
| 273 | |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 274 | // Bind our framebuffer. |
sakal | 2fcd2dd | 2017-01-18 03:21:10 -0800 | [diff] [blame] | 275 | GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, textureFrameBuffer.getFrameBufferId()); |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 276 | GlUtil.checkNoGLES2Error("glBindFramebuffer"); |
| 277 | |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 278 | GLES20.glActiveTexture(GLES20.GL_TEXTURE0); |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 279 | GLES20.glBindTexture(textureType.getGlTarget(), srcTextureId); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 280 | GLES20.glUniformMatrix4fv(texMatrixLoc, 1, false, transformMatrix, 0); |
| 281 | |
| 282 | // Draw Y |
| 283 | GLES20.glViewport(0, 0, y_width, height); |
| 284 | // Matrix * (1;0;0;0) / width. Note that opengl uses column major order. |
| 285 | GLES20.glUniform2f(xUnitLoc, transformMatrix[0] / width, transformMatrix[1] / width); |
| 286 | // Y'UV444 to RGB888, see |
| 287 | // https://en.wikipedia.org/wiki/YUV#Y.27UV444_to_RGB888_conversion. |
| 288 | // We use the ITU-R coefficients for U and V */ |
| 289 | GLES20.glUniform4f(coeffsLoc, 0.299f, 0.587f, 0.114f, 0.0f); |
| 290 | GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); |
| 291 | |
| 292 | // Draw U |
| 293 | GLES20.glViewport(0, height, uv_width, uv_height); |
| 294 | // Matrix * (1;0;0;0) / (width / 2). Note that opengl uses column major order. |
| 295 | GLES20.glUniform2f( |
| 296 | xUnitLoc, 2.0f * transformMatrix[0] / width, 2.0f * transformMatrix[1] / width); |
| 297 | GLES20.glUniform4f(coeffsLoc, -0.169f, -0.331f, 0.499f, 0.5f); |
| 298 | GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); |
| 299 | |
| 300 | // Draw V |
| 301 | GLES20.glViewport(stride / 8, height, uv_width, uv_height); |
| 302 | GLES20.glUniform4f(coeffsLoc, 0.499f, -0.418f, -0.0813f, 0.5f); |
| 303 | GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); |
| 304 | |
| 305 | GLES20.glReadPixels( |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 306 | 0, 0, frameBufferWidth, frameBufferHeight, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buf); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 307 | |
| 308 | GlUtil.checkNoGLES2Error("YuvConverter.convert"); |
| 309 | |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 310 | // Restore normal framebuffer. |
| 311 | GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); |
| 312 | GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); |
| 313 | |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 314 | // Unbind texture. Reportedly needed on some devices to get |
| 315 | // the texture updated from the camera. |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 316 | GLES20.glBindTexture(textureType.getGlTarget(), 0); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 317 | } |
| 318 | |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 319 | public void release() { |
| 320 | threadChecker.checkIsOnValidThread(); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 321 | released = true; |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 322 | if (shader != null) { |
| 323 | shader.release(); |
| 324 | } |
sakal | 2fcd2dd | 2017-01-18 03:21:10 -0800 | [diff] [blame] | 325 | textureFrameBuffer.release(); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 326 | } |
| 327 | } |