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 | |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 13 | import android.graphics.Matrix; |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 14 | import android.opengl.GLES20; |
Fabian Bergmark | 1bb36d2 | 2021-06-17 11:53:24 +0200 | [diff] [blame] | 15 | import android.opengl.GLException; |
Byoungchan Lee | 02334e0 | 2021-08-14 11:41:59 +0900 | [diff] [blame] | 16 | import androidx.annotation.Nullable; |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 17 | import java.nio.ByteBuffer; |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 18 | import org.webrtc.VideoFrame.I420Buffer; |
| 19 | import org.webrtc.VideoFrame.TextureBuffer; |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 20 | |
| 21 | /** |
Magnus Jedvert | 1d270f8 | 2018-04-16 16:28:29 +0200 | [diff] [blame] | 22 | * Class for converting OES textures to a YUV ByteBuffer. It can be constructed on any thread, but |
| 23 | * should only be operated from a single thread with an active EGL context. |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 24 | */ |
Fabian Bergmark | 1bb36d2 | 2021-06-17 11:53:24 +0200 | [diff] [blame] | 25 | public final class YuvConverter { |
| 26 | private static final String TAG = "YuvConverter"; |
| 27 | |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 28 | private static final String FRAGMENT_SHADER = |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 29 | // Difference in texture coordinate corresponding to one |
| 30 | // sub-pixel in the x direction. |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 31 | "uniform vec2 xUnit;\n" |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 32 | // Color conversion coefficients, including constant term |
| 33 | + "uniform vec4 coeffs;\n" |
| 34 | + "\n" |
| 35 | + "void main() {\n" |
| 36 | // Since the alpha read from the texture is always 1, this could |
| 37 | // be written as a mat4 x vec4 multiply. However, that seems to |
| 38 | // give a worse framerate, possibly because the additional |
Niels Möller | b5b159d | 2022-07-05 09:59:27 +0200 | [diff] [blame] | 39 | // multiplies by 1.0 consume resources. |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 40 | + " gl_FragColor.r = coeffs.a + dot(coeffs.rgb,\n" |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 41 | + " sample(tc - 1.5 * xUnit).rgb);\n" |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 42 | + " gl_FragColor.g = coeffs.a + dot(coeffs.rgb,\n" |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 43 | + " sample(tc - 0.5 * xUnit).rgb);\n" |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 44 | + " gl_FragColor.b = coeffs.a + dot(coeffs.rgb,\n" |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 45 | + " sample(tc + 0.5 * xUnit).rgb);\n" |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 46 | + " gl_FragColor.a = coeffs.a + dot(coeffs.rgb,\n" |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 47 | + " sample(tc + 1.5 * xUnit).rgb);\n" |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 48 | + "}\n"; |
| 49 | |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 50 | private static class ShaderCallbacks implements GlGenericDrawer.ShaderCallbacks { |
Sami Kalliomäki | 8ccddff | 2018-09-05 11:43:38 +0200 | [diff] [blame] | 51 | // Y'UV444 to RGB888, see https://en.wikipedia.org/wiki/YUV#Y%E2%80%B2UV444_to_RGB888_conversion |
| 52 | // We use the ITU-R BT.601 coefficients for Y, U and V. |
| 53 | // The values in Wikipedia are inaccurate, the accurate values derived from the spec are: |
| 54 | // Y = 0.299 * R + 0.587 * G + 0.114 * B |
| 55 | // U = -0.168736 * R - 0.331264 * G + 0.5 * B + 0.5 |
| 56 | // V = 0.5 * R - 0.418688 * G - 0.0813124 * B + 0.5 |
| 57 | // To map the Y-values to range [16-235] and U- and V-values to range [16-240], the matrix has |
| 58 | // been multiplied with matrix: |
| 59 | // {{219 / 255, 0, 0, 16 / 255}, |
| 60 | // {0, 224 / 255, 0, 16 / 255}, |
| 61 | // {0, 0, 224 / 255, 16 / 255}, |
| 62 | // {0, 0, 0, 1}} |
| 63 | private static final float[] yCoeffs = |
| 64 | new float[] {0.256788f, 0.504129f, 0.0979059f, 0.0627451f}; |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 65 | private static final float[] uCoeffs = |
Sami Kalliomäki | 8ccddff | 2018-09-05 11:43:38 +0200 | [diff] [blame] | 66 | new float[] {-0.148223f, -0.290993f, 0.439216f, 0.501961f}; |
| 67 | private static final float[] vCoeffs = |
| 68 | new float[] {0.439216f, -0.367788f, -0.0714274f, 0.501961f}; |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 69 | |
| 70 | private int xUnitLoc; |
| 71 | private int coeffsLoc; |
| 72 | |
| 73 | private float[] coeffs; |
| 74 | private float stepSize; |
| 75 | |
| 76 | public void setPlaneY() { |
| 77 | coeffs = yCoeffs; |
| 78 | stepSize = 1.0f; |
| 79 | } |
| 80 | |
| 81 | public void setPlaneU() { |
| 82 | coeffs = uCoeffs; |
| 83 | stepSize = 2.0f; |
| 84 | } |
| 85 | |
| 86 | public void setPlaneV() { |
| 87 | coeffs = vCoeffs; |
| 88 | stepSize = 2.0f; |
| 89 | } |
| 90 | |
| 91 | @Override |
| 92 | public void onNewShader(GlShader shader) { |
| 93 | xUnitLoc = shader.getUniformLocation("xUnit"); |
| 94 | coeffsLoc = shader.getUniformLocation("coeffs"); |
| 95 | } |
| 96 | |
| 97 | @Override |
| 98 | public void onPrepareShader(GlShader shader, float[] texMatrix, int frameWidth, int frameHeight, |
| 99 | int viewportWidth, int viewportHeight) { |
| 100 | GLES20.glUniform4fv(coeffsLoc, /* count= */ 1, coeffs, /* offset= */ 0); |
| 101 | // Matrix * (1;0;0;0) / (width / stepSize). Note that OpenGL uses column major order. |
| 102 | GLES20.glUniform2f( |
| 103 | xUnitLoc, stepSize * texMatrix[0] / frameWidth, stepSize * texMatrix[1] / frameWidth); |
| 104 | } |
| 105 | } |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 106 | |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 107 | private final ThreadUtils.ThreadChecker threadChecker = new ThreadUtils.ThreadChecker(); |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 108 | private final GlTextureFrameBuffer i420TextureFrameBuffer = |
| 109 | new GlTextureFrameBuffer(GLES20.GL_RGBA); |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 110 | private final ShaderCallbacks shaderCallbacks = new ShaderCallbacks(); |
| 111 | private final GlGenericDrawer drawer = new GlGenericDrawer(FRAGMENT_SHADER, shaderCallbacks); |
Åsa Persson | f2889bb | 2019-02-25 16:20:01 +0100 | [diff] [blame] | 112 | private final VideoFrameDrawer videoFrameDrawer; |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 113 | |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 114 | /** |
| 115 | * This class should be constructed on a thread that has an active EGL context. |
| 116 | */ |
| 117 | public YuvConverter() { |
Åsa Persson | f2889bb | 2019-02-25 16:20:01 +0100 | [diff] [blame] | 118 | this(new VideoFrameDrawer()); |
| 119 | } |
| 120 | |
| 121 | public YuvConverter(VideoFrameDrawer videoFrameDrawer) { |
| 122 | this.videoFrameDrawer = videoFrameDrawer; |
Magnus Jedvert | 1d270f8 | 2018-04-16 16:28:29 +0200 | [diff] [blame] | 123 | threadChecker.detachThread(); |
Sami Kalliomäki | cb98b11 | 2017-10-16 11:20:26 +0200 | [diff] [blame] | 124 | } |
| 125 | |
| 126 | /** Converts the texture buffer to I420. */ |
Fabian Bergmark | 1bb36d2 | 2021-06-17 11:53:24 +0200 | [diff] [blame] | 127 | @Nullable |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 128 | public I420Buffer convert(TextureBuffer inputTextureBuffer) { |
Fabian Bergmark | 1bb36d2 | 2021-06-17 11:53:24 +0200 | [diff] [blame] | 129 | try { |
| 130 | return convertInternal(inputTextureBuffer); |
| 131 | } catch (GLException e) { |
| 132 | Logging.w(TAG, "Failed to convert TextureBuffer", e); |
| 133 | } |
| 134 | return null; |
| 135 | } |
Åsa Persson | f2889bb | 2019-02-25 16:20:01 +0100 | [diff] [blame] | 136 | |
Fabian Bergmark | 1bb36d2 | 2021-06-17 11:53:24 +0200 | [diff] [blame] | 137 | private I420Buffer convertInternal(TextureBuffer inputTextureBuffer) { |
Åsa Persson | f2889bb | 2019-02-25 16:20:01 +0100 | [diff] [blame] | 138 | TextureBuffer preparedBuffer = (TextureBuffer) videoFrameDrawer.prepareBufferForViewportSize( |
| 139 | inputTextureBuffer, inputTextureBuffer.getWidth(), inputTextureBuffer.getHeight()); |
| 140 | |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 141 | // We draw into a buffer laid out like |
| 142 | // |
| 143 | // +---------+ |
| 144 | // | | |
| 145 | // | Y | |
| 146 | // | | |
| 147 | // | | |
| 148 | // +----+----+ |
| 149 | // | U | V | |
| 150 | // | | | |
| 151 | // +----+----+ |
| 152 | // |
| 153 | // In memory, we use the same stride for all of Y, U and V. The |
Artem Titov | d7ac581 | 2021-07-27 12:23:39 +0200 | [diff] [blame] | 154 | // U data starts at offset `height` * `stride` from the Y data, |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 155 | // and the V data starts at at offset |stride/2| from the U |
| 156 | // data, with rows of U and V data alternating. |
| 157 | // |
| 158 | // Now, it would have made sense to allocate a pixel buffer with |
| 159 | // a single byte per pixel (EGL10.EGL_COLOR_BUFFER_TYPE, |
| 160 | // EGL10.EGL_LUMINANCE_BUFFER,), but that seems to be |
| 161 | // unsupported by devices. So do the following hack: Allocate an |
Artem Titov | d7ac581 | 2021-07-27 12:23:39 +0200 | [diff] [blame] | 162 | // RGBA buffer, of width `stride`/4. To render each of these |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 163 | // large pixels, sample the texture at 4 different x coordinates |
| 164 | // and store the results in the four components. |
| 165 | // |
| 166 | // Since the V data needs to start on a boundary of such a |
Artem Titov | d7ac581 | 2021-07-27 12:23:39 +0200 | [diff] [blame] | 167 | // larger pixel, it is not sufficient that `stride` is even, it |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 168 | // has to be a multiple of 8 pixels. |
Åsa Persson | f2889bb | 2019-02-25 16:20:01 +0100 | [diff] [blame] | 169 | final int frameWidth = preparedBuffer.getWidth(); |
| 170 | final int frameHeight = preparedBuffer.getHeight(); |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 171 | final int stride = ((frameWidth + 7) / 8) * 8; |
| 172 | final int uvHeight = (frameHeight + 1) / 2; |
| 173 | // Total height of the combined memory layout. |
| 174 | final int totalHeight = frameHeight + uvHeight; |
| 175 | final ByteBuffer i420ByteBuffer = JniCommon.nativeAllocateByteBuffer(stride * totalHeight); |
| 176 | // Viewport width is divided by four since we are squeezing in four color bytes in each RGBA |
| 177 | // pixel. |
| 178 | final int viewportWidth = stride / 4; |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 179 | |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 180 | // Produce a frame buffer starting at top-left corner, not bottom-left. |
| 181 | final Matrix renderMatrix = new Matrix(); |
| 182 | renderMatrix.preTranslate(0.5f, 0.5f); |
| 183 | renderMatrix.preScale(1f, -1f); |
| 184 | renderMatrix.preTranslate(-0.5f, -0.5f); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 185 | |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 186 | i420TextureFrameBuffer.setSize(viewportWidth, totalHeight); |
sakal | 2fcd2dd | 2017-01-18 03:21:10 -0800 | [diff] [blame] | 187 | |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 188 | // Bind our framebuffer. |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 189 | GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, i420TextureFrameBuffer.getFrameBufferId()); |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 190 | GlUtil.checkNoGLES2Error("glBindFramebuffer"); |
| 191 | |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 192 | // Draw Y. |
| 193 | shaderCallbacks.setPlaneY(); |
Åsa Persson | f2889bb | 2019-02-25 16:20:01 +0100 | [diff] [blame] | 194 | VideoFrameDrawer.drawTexture(drawer, preparedBuffer, renderMatrix, frameWidth, frameHeight, |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 195 | /* viewportX= */ 0, /* viewportY= */ 0, viewportWidth, |
| 196 | /* viewportHeight= */ frameHeight); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 197 | |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 198 | // Draw U. |
| 199 | shaderCallbacks.setPlaneU(); |
Åsa Persson | f2889bb | 2019-02-25 16:20:01 +0100 | [diff] [blame] | 200 | VideoFrameDrawer.drawTexture(drawer, preparedBuffer, renderMatrix, frameWidth, frameHeight, |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 201 | /* viewportX= */ 0, /* viewportY= */ frameHeight, viewportWidth / 2, |
| 202 | /* viewportHeight= */ uvHeight); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 203 | |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 204 | // Draw V. |
| 205 | shaderCallbacks.setPlaneV(); |
Åsa Persson | f2889bb | 2019-02-25 16:20:01 +0100 | [diff] [blame] | 206 | VideoFrameDrawer.drawTexture(drawer, preparedBuffer, renderMatrix, frameWidth, frameHeight, |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 207 | /* viewportX= */ viewportWidth / 2, /* viewportY= */ frameHeight, viewportWidth / 2, |
| 208 | /* viewportHeight= */ uvHeight); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 209 | |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 210 | GLES20.glReadPixels(0, 0, i420TextureFrameBuffer.getWidth(), i420TextureFrameBuffer.getHeight(), |
| 211 | GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, i420ByteBuffer); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 212 | |
| 213 | GlUtil.checkNoGLES2Error("YuvConverter.convert"); |
| 214 | |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 215 | // Restore normal framebuffer. |
| 216 | GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 217 | |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 218 | // Prepare Y, U, and V ByteBuffer slices. |
| 219 | final int yPos = 0; |
| 220 | final int uPos = yPos + stride * frameHeight; |
| 221 | // Rows of U and V alternate in the buffer, so V data starts after the first row of U. |
| 222 | final int vPos = uPos + stride / 2; |
| 223 | |
| 224 | i420ByteBuffer.position(yPos); |
| 225 | i420ByteBuffer.limit(yPos + stride * frameHeight); |
| 226 | final ByteBuffer dataY = i420ByteBuffer.slice(); |
| 227 | |
| 228 | i420ByteBuffer.position(uPos); |
| 229 | // The last row does not have padding. |
| 230 | final int uvSize = stride * (uvHeight - 1) + stride / 2; |
| 231 | i420ByteBuffer.limit(uPos + uvSize); |
| 232 | final ByteBuffer dataU = i420ByteBuffer.slice(); |
| 233 | |
| 234 | i420ByteBuffer.position(vPos); |
| 235 | i420ByteBuffer.limit(vPos + uvSize); |
| 236 | final ByteBuffer dataV = i420ByteBuffer.slice(); |
| 237 | |
Åsa Persson | f2889bb | 2019-02-25 16:20:01 +0100 | [diff] [blame] | 238 | preparedBuffer.release(); |
| 239 | |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 240 | return JavaI420Buffer.wrap(frameWidth, frameHeight, dataY, stride, dataU, stride, dataV, stride, |
| 241 | () -> { JniCommon.nativeFreeByteBuffer(i420ByteBuffer); }); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 242 | } |
| 243 | |
magjed | 1cb4823 | 2016-10-20 03:19:16 -0700 | [diff] [blame] | 244 | public void release() { |
| 245 | threadChecker.checkIsOnValidThread(); |
Magnus Jedvert | 65c61dc | 2018-06-15 09:33:20 +0200 | [diff] [blame] | 246 | drawer.release(); |
| 247 | i420TextureFrameBuffer.release(); |
Åsa Persson | f2889bb | 2019-02-25 16:20:01 +0100 | [diff] [blame] | 248 | videoFrameDrawer.release(); |
Magnus Jedvert | 7b87530 | 2018-08-09 13:51:42 +0200 | [diff] [blame] | 249 | // Allow this class to be reused. |
| 250 | threadChecker.detachThread(); |
Magnus Jedvert | 577bc19 | 2016-10-19 15:29:02 +0200 | [diff] [blame] | 251 | } |
| 252 | } |