(Android-RTC-7)分析AndroidVideoDecoder,看webrtc如何利用shader把texture输出yuv420

最近输出的内容变少了,一是工作内容确实不少,二是生活方面。但都不应该是放弃学习的原因。废话不说本篇承接上篇内容,分析AndroidVideoDecoder & HardwareVideoEncoder

一、前提回顾

稍微回顾一下之前的内容,以解码DefaultVideoDeocderFactory,createDecoder是由HardwareVideoDecoderFactory 和 SoftwareVideoDecoderFactory各自创建出来,然后回传到PeerConnectionClient。

@Override
public @Nullable
VideoDecoder createDecoder(VideoCodecInfo codecType) {
    VideoDecoder softwareDecoder = softwareVideoDecoderFactory.createDecoder(codecType);
    final VideoDecoder hardwareDecoder = hardwareVideoDecoderFactory.createDecoder(codecType);
    if (softwareDecoder == null && platformSoftwareVideoDecoderFactory != null) {
        softwareDecoder = platformSoftwareVideoDecoderFactory.createDecoder(codecType);
    }
    if (hardwareDecoder != null && softwareDecoder != null) {
        // Both hardware and software supported, wrap it in a software fallback
        return new VideoDecoderFallback(
                /* fallback= */ softwareDecoder, /* primary= */ hardwareDecoder);
    }
    return hardwareDecoder != null ? hardwareDecoder : softwareDecoder;
}

对于HardwareVideoDecoderFactory createDecoder创建硬解器,具体如下:

@Nullable
@Override
public VideoDecoder createDecoder(VideoCodecInfo codecType) {
    VideoCodecMimeType type = VideoCodecMimeType.valueOf(codecType.getName());
    MediaCodecInfo info = findCodecForType(type);
    if (info == null) {
        return null;
    }
    CodecCapabilities capabilities = info.getCapabilitiesForType(type.mimeType());
    return new AndroidVideoDecoder(new MediaCodecWrapperFactoryImpl(), info.getName(), type,
            MediaCodecUtils.selectColorFormat(MediaCodecUtils.DECODER_COLOR_FORMATS, capabilities),
            sharedContext);
}

对是的,今天就是来看看这个AndroidVideoDecoder有什么值得去挖掘学习的。

二、AndroidVideoDecoder

看Android开源项目,建议大家善用AS窗口左下角的Structure标签,它会展现当前类的所有成员变量,方法,能給阅读者一个总体概述。

 我们直接来聊聊关键点,initDeocde -> SurfaceTextureHelper

@Override
public VideoCodecStatus initDecode(Settings settings, Callback callback) {
    this.decoderThreadChecker = new ThreadChecker();
    this.callback = callback;
    if (sharedContext != null) {
        surfaceTextureHelper = createSurfaceTextureHelper();
        surface = new Surface(surfaceTextureHelper.getSurfaceTexture());
        surfaceTextureHelper.startListening(this);
    }
    return initDecodeInternal(settings.width, settings.height);
}
private SurfaceTextureHelper(Context sharedContext, Handler handler, boolean alignTimestamps,
                             YuvConverter yuvConverter, FrameRefMonitor frameRefMonitor) {
    if (handler.getLooper().getThread() != Thread.currentThread()) {
        throw new IllegalStateException("SurfaceTextureHelper must be created on the handler thread");
    }
    this.handler = handler;
    this.timestampAligner = alignTimestamps ? new TimestampAligner() : null;
    this.yuvConverter = yuvConverter;
    this.frameRefMonitor = frameRefMonitor;
    // 1
    eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_BUFFER);
    try {
        // Both these statements have been observed to fail on rare occasions, see BUG=webrtc:5682.
        eglBase.createDummyPbufferSurface();
        eglBase.makeCurrent();
    } catch (RuntimeException e) {
        // Clean up before rethrowing the exception.
        eglBase.release();
        handler.getLooper().quit();
        throw e;
    }
    // 2
    oesTextureId = GlUtil.generateTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
    surfaceTexture = new SurfaceTexture(oesTextureId);
    setOnFrameAvailableListener(surfaceTexture, (SurfaceTexture st) -> {
        if (hasPendingTexture) {
            Logging.d(TAG, "A frame is already pending, dropping frame.");
        }
        hasPendingTexture = true;
        tryDeliverTextureFrame();
    }, handler);
}

1、EglBase.createDummyPbufferSurface 创建了 Pbuffer,有关pbuffer的知识点可以看我以前的相关文章。理解pbuffer和fbo的区别,我想这个问题已经很多知识博主说得很明白,然后面试的时候会经常问pbuffer能代替fbo吗?哪些情况用pbuffer?这里給出几年前给网友解疑的回答:

pbuffer适合大规模多GPU计算的工程,在移动端fbo还真的能替代pbuffer绝大多数的使用场景。

2、SurfaceTexture.FrameAvailableListener,SurfaceTexture的工作方式很多同学都知道,我就不展开阐述了。那这里我想说啥呢,就是很多同学问到的SurfaceTexture如何高效提取帧数据?

@Override
public VideoCodecStatus initDecode(Settings settings, Callback callback) {
    ... ...
    return initDecodeInternal(settings.width, settings.height);
}
private VideoCodecStatus initDecodeInternal(int width, int height) {
    codec = mediaCodecWrapperFactory.createByCodecName(codecName);
    codec.start();
    ... ...
    outputThread = createOutputThread();
    outputThread.start();
    ... ...
    return VideoCodecStatus.OK;
}
private Thread createOutputThread() {
    return new Thread("AndroidVideoDecoder.outputThread") {
        @Override
        public void run() {
            outputThreadChecker = new ThreadChecker();
            while (running) {
                deliverDecodedFrame();
            }
            releaseCodecOnOutputThread();
        }
    };
}

这里车速有点快,请大家坐稳跟上节奏。回到AndroidVideoDecoder的 initDecoder -> initDecodeInternal -> createOutputThread。意思很明显,就是一个线程不断的提取传递解码后的数据,跟进deliverDecodedFrame()

protected void deliverDecodedFrame() {
    ... ...
    MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
    int result = codec.dequeueOutputBuffer(info, DEQUEUE_OUTPUT_BUFFER_TIMEOUT_US);
    ... ...
    if (surfaceTextureHelper != null) {
        deliverTextureFrame(result, info, rotation, decodeTimeMs);
    } else {
        deliverByteFrame(result, info, rotation, decodeTimeMs);
    }
}

private void deliverTextureFrame(final int index, final MediaCodec.BufferInfo info,
                                 final int rotation, final Integer decodeTimeMs) {
    ... ...
    synchronized (renderedTextureMetadataLock) {
        if (renderedTextureMetadata != null) {
            codec.releaseOutputBuffer(index, false);
            return; // still waiting for previous frame, drop this one.
        }
        surfaceTextureHelper.setTextureSize(width, height);
        surfaceTextureHelper.setFrameRotation(rotation);
        renderedTextureMetadata = new DecodedTextureMetadata(info.presentationTimeUs, decodeTimeMs);
        codec.releaseOutputBuffer(index, /* render= */ true);
    }
}

@Override
public void onFrame(VideoFrame frame) {
    final VideoFrame newFrame;
    final Integer decodeTimeMs;
    final long timestampNs;
    synchronized (renderedTextureMetadataLock) {
        if (renderedTextureMetadata == null) {
            throw new IllegalStateException(
                    "Rendered texture metadata was null in onTextureFrameAvailable.");
        }
        timestampNs = renderedTextureMetadata.presentationTimestampUs * 1000;
        decodeTimeMs = renderedTextureMetadata.decodeTimeMs;
        renderedTextureMetadata = null;
    }
    // Change timestamp of frame.
    final VideoFrame frameWithModifiedTimeStamp =
            new VideoFrame(frame.getBuffer(), frame.getRotation(), timestampNs);
    callback.onDecodedFrame(frameWithModifiedTimeStamp, decodeTimeMs, null /* qp */);
}

deliverTextureFrame非真的deliver Texture。关键是在 renderedTextureMetadata,看到在onFrame,把回调出来的VideoFrame替换renderedTextureMetadata的pts,最后才回调上去。那么这个onFrame是哪里的回调接口,就是SurfaceTextureHelper里面的SurfaceTexture.FrameAvailableListener抛上来的。

private void tryDeliverTextureFrame() {
    updateTexImage();

    final float[] transformMatrix = new float[16];
    surfaceTexture.getTransformMatrix(transformMatrix);
    long timestampNs = surfaceTexture.getTimestamp();

    final TextureBuffer buffer =
            new TextureBufferImpl(textureWidth, textureHeight, TextureBuffer.Type.OES, oesTextureId,
                    RendererCommon.convertMatrixToAndroidGraphicsMatrix(transformMatrix), handler,
                    yuvConverter, textureRefCountMonitor);
    
    final VideoFrame frame = new VideoFrame(buffer, frameRotation, timestampNs);
    listener.onFrame(frame);
    frame.release();
}

在tryDeliverTextureFrame函数当中,看到把 oesTextureId 封装成 TextureBuffer,再封装成VideoFrame回调給AndroidVideoDeocder,可以看到关键类 YuvConverter,显然这就是把Texture转换成yuv数据的辅助类。

接下来我就不装*了,直接看代码,挖掘有价值的知识。

// TextureBufferImpl 
@Override
public VideoFrame.I420Buffer toI420() {
    return ThreadUtils.invokeAtFrontUninterruptibly(
            toI420Handler, () -> yuvConverter.convert(this));
}
// ThreadUtils
public static <V> V invokeAtFrontUninterruptibly(
        final Handler handler, final Callable<V> callable) {
    if (handler.getLooper().getThread() == Thread.currentThread()) {
        try {
            return callable.call();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    final Result result = new Result();
    final CaughtException caughtException = new CaughtException();
    final CountDownLatch barrier = new CountDownLatch(1);
    handler.post(new Runnable() {
        @Override
        public void run() {
            try {
                result.value = callable.call();
            } catch (Exception e) {
                caughtException.e = e;
            }
            barrier.countDown();
        }
    });
    awaitUninterruptibly(barrier);

    if (caughtException.e != null) {
        final RuntimeException runtimeException = new RuntimeException(caughtException.e);
        runtimeException.setStackTrace(
                concatStackTraces(caughtException.e.getStackTrace(), runtimeException.getStackTrace()));
        throw runtimeException;
    }
    return result.value;
}

2.1、学习过Kotlin的同学,或多或少都涉及过suspend修饰词——协程,简单理解就是业务代码执行流程在同步不阻塞的情况下,代码片A执行在指定线程A,代码片B执行在指定线程B,确保业务流程。那么在Java版本有无这样的舒爽的用法?肯定是有的!版本答案就在上面,其实就是利用了Java的CountDownLatch同步计数,awaitUninterruptibly(CountDownLatch)里在调用线程上循环等待,在返回执行结果,值得同学细品,并运用到实际项目当中。

2.2、打开YuvConveter,快速定位到convert方法,方法备注已经写得明明白白/*Converts the texture buffer to I420.*/,继续分析是如何转换的,代码分两部分理解:①texture按照yuv的格式draw到fbo上;②把fbo的内容提取出来;

三、Texture Conveter YUV420

public I420Buffer convert(TextureBuffer inputTextureBuffer) {
    TextureBuffer preparedBuffer = (TextureBuffer) videoFrameDrawer.prepareBufferForViewportSize(
            inputTextureBuffer, inputTextureBuffer.getWidth(), inputTextureBuffer.getHeight());
    // We draw into a buffer laid out like
    //    +---------+
    //    |         |
    //    |  Y      |
    //    |         |
    //    |         |
    //    +----+----+
    //    | U  | V  |
    //    |    |    |
    //    +----+----+
    // In memory, we use the same stride for all of Y, U and V. The
    // U data starts at offset |height| * |stride| from the Y data,
    // and the V data starts at at offset |stride/2| from the U
    // data, with rows of U and V data alternating.
    //
    // Now, it would have made sense to allocate a pixel buffer with
    // a single byte per pixel (EGL10.EGL_COLOR_BUFFER_TYPE,
    // EGL10.EGL_LUMINANCE_BUFFER,), but that seems to be
    // unsupported by devices. So do the following hack: Allocate an
    // RGBA buffer, of width |stride|/4. To render each of these
    // large pixels, sample the texture at 4 different x coordinates
    // and store the results in the four components.
    //
    // Since the V data needs to start on a boundary of such a
    // larger pixel, it is not sufficient that |stride| is even, it
    // has to be a multiple of 8 pixels.
	// Note1
    final int frameWidth = preparedBuffer.getWidth();
    final int frameHeight = preparedBuffer.getHeight();
    final int stride = ((frameWidth + 7) / 8) * 8;
    final int uvHeight = (frameHeight + 1) / 2;
    // Total height of the combined memory layout.
    final int totalHeight = frameHeight + uvHeight;
    // Viewport width is divided by four since we are squeezing in four color bytes in each RGBA pixel.
    final int viewportWidth = stride / 4;
    // Note2
    // Produce a frame buffer starting at top-left corner, not bottom-left.
    final Matrix renderMatrix = new Matrix();
    renderMatrix.preTranslate(0.5f, 0.5f);
    renderMatrix.preScale(1f, -1f);
    renderMatrix.preTranslate(-0.5f, -0.5f);
    // Note3
    i420TextureFrameBuffer.setSize(viewportWidth, totalHeight);
    // Bind our framebuffer.
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, i420TextureFrameBuffer.getFrameBufferId());
    GlUtil.checkNoGLES2Error("glBindFramebuffer");
    // Note4
    // Draw Y.
    shaderCallbacks.setPlaneY();
    VideoFrameDrawer.drawTexture(drawer, preparedBuffer, renderMatrix, frameWidth, frameHeight,
            /* viewportX= */ 0, /* viewportY= */ 0, viewportWidth,
            /* viewportHeight= */ frameHeight);
    // Draw U.
    shaderCallbacks.setPlaneU();
    VideoFrameDrawer.drawTexture(drawer, preparedBuffer, renderMatrix, frameWidth, frameHeight,
            /* viewportX= */ 0, /* viewportY= */ frameHeight, viewportWidth / 2,
            /* viewportHeight= */ uvHeight);
    // Draw V.
    shaderCallbacks.setPlaneV();
    VideoFrameDrawer.drawTexture(drawer, preparedBuffer, renderMatrix, frameWidth, frameHeight,
            /* viewportX= */ viewportWidth / 2, /* viewportY= */ frameHeight, viewportWidth / 2,
            /* viewportHeight= */ uvHeight);

    GLES20.glReadPixels(0, 0, i420TextureFrameBuffer.getWidth(), i420TextureFrameBuffer.getHeight(),
            GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, i420ByteBuffer);

    GlUtil.checkNoGLES2Error("YuvConverter.convert");
    // Restore normal framebuffer.
    GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
	
	... ...
}

Note1:上面的代码对应第一部分,开头的部分注解主要是说明8字节内存对齐,YUV420格式的数据存储到一整块pixel(RGBA)buffer上。

Note2:之后准备了一个renderMatrix,执行了三个左乘矩阵运算,最终的renderMatrix = 单位矩阵MIdentity*T*S*-T,关于左乘和右乘的区别,请看这里面的矩阵运算。这里的操作有点类似8字节内存对齐的套路,往下看就知道了。

Note3:不要看名字是i420TextureFrameBuffer,其实它格式是为GLES20.GL_RGBA的FrameBuffer,宽度是stride / 4,即内存对齐后的frameWidth,高度是frameHeight + uvHeight。这其实就是传统意义的yuv-size = width * height * 3 / 2的大小了。

private final GlTextureFrameBuffer i420TextureFrameBuffer =
            new GlTextureFrameBuffer(GLES20.GL_RGBA);
final int frameWidth = preparedBuffer.getWidth();
final int frameHeight = preparedBuffer.getHeight();
final int stride = ((frameWidth + 7) / 8) * 8;
final int uvHeight = (frameHeight + 1) / 2;
final int viewportWidth = stride / 4;
final int totalHeight = frameHeight + uvHeight;
i420TextureFrameBuffer.setSize(viewportWidth, totalHeight);

Note4:往下走想看懂DrawY,DrawU,DrawV是什么骚操作,得先知道这里的ShaderCallbacks、GlGenericDrawer和VideoFrameDrawer,三者是如何关联工作的。

private final ShaderCallbacks shaderCallbacks = new ShaderCallbacks();
private final GlGenericDrawer drawer = new GlGenericDrawer(FRAGMENT_SHADER, shaderCallbacks);

public GlGenericDrawer(String genericFragmentSource, ShaderCallbacks shaderCallbacks) {
        this(DEFAULT_VERTEX_SHADER_STRING, genericFragmentSource, shaderCallbacks);
    }

GlGenericDrawer就是Shader着色器的封装对象,然后呢这里的代码我觉得有点绕,搞不到为啥要这样写,得慢慢的一步步分析。

Note4.1:分析GlGenericDrawer构造传入的DEFAULT_VERTEX_SHADER_STRING 和 FRAGMENT_SHADER,注意这里不是全部的shader内容,往下看流程VideoFrameDrawer.drawTexture -> GlGenericDrawer.drawOes -> prepareShader->createShader

/**
 * Draw an OES texture frame with specified texture transformation matrix. Required resources are
 * allocated at the first call to this function.
 */
@Override
public void drawOes(int oesTextureId, float[] texMatrix, int frameWidth, int frameHeight,
                    int viewportX, int viewportY, int viewportWidth, int viewportHeight) {
    prepareShader(
            ShaderType.OES, texMatrix, frameWidth, frameHeight, viewportWidth, viewportHeight);
    // Bind the texture.
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, oesTextureId);
    // Draw the texture.
    GLES20.glViewport(viewportX, viewportY, viewportWidth, viewportHeight);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    // Unbind the texture as a precaution.
    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
}

// Visible for testing.
GlShader createShader(ShaderType shaderType) {
    return new GlShader(
            vertexShader, createFragmentShaderString(genericFragmentSource, shaderType));
}
static String createFragmentShaderString(String genericFragmentSource, ShaderType shaderType) {
    final StringBuilder stringBuilder = new StringBuilder();
    if (shaderType == ShaderType.OES) {
        stringBuilder.append("#extension GL_OES_EGL_image_external : require\n");
    }
    stringBuilder.append("precision mediump float;\n");
    stringBuilder.append("varying vec2 tc;\n");

    if (shaderType == ShaderType.YUV) {
        stringBuilder.append("uniform sampler2D y_tex;\n");
        stringBuilder.append("uniform sampler2D u_tex;\n");
        stringBuilder.append("uniform sampler2D v_tex;\n");

        // Add separate function for sampling texture.
        // yuv_to_rgb_mat is inverse of the matrix defined in YuvConverter.
        stringBuilder.append("vec4 sample(vec2 p) {\n");
        stringBuilder.append("  float y = texture2D(y_tex, p).r * 1.16438;\n");
        stringBuilder.append("  float u = texture2D(u_tex, p).r;\n");
        stringBuilder.append("  float v = texture2D(v_tex, p).r;\n");
        stringBuilder.append("  return vec4(y + 1.59603 * v - 0.874202,\n");
        stringBuilder.append("    y - 0.391762 * u - 0.812968 * v + 0.531668,\n");
        stringBuilder.append("    y + 2.01723 * u - 1.08563, 1);\n");
        stringBuilder.append("}\n");
        stringBuilder.append(genericFragmentSource);
    } else {
        final String samplerName = shaderType == ShaderType.OES ? "samplerExternalOES" : "sampler2D";
        stringBuilder.append("uniform ").append(samplerName).append(" tex;\n");

        // Update the sampling function in-place.
        stringBuilder.append(genericFragmentSource.replace("sample(", "texture2D(tex, "));
    }

    return stringBuilder.toString();
}

代码在createFragmentShaderString中追加了一些内容,所以完整的shader内容如下所示:

/* DEFAULT_VERTEX_SHADER_STRING */
varying vec2 tc;
attribute vec4 in_pos;
attribute vec4 in_tc;
uniform mat4 tex_mat;
void main() {
  gl_Position = in_pos;
  tc = (tex_mat * in_tc).xy;
}


/* FRAGMENT_SHADER */

#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 tc;
uniform samplerExternalOES tex;
// Difference in texture coordinate corresponding to one
// sub-pixel in the x direction.
uniform vec2 xUnit;
// Color conversion coefficients, including constant term
uniform vec4 coeffs;

void main() {
  gl_FragColor.r = coeffs.a + dot(coeffs.rgb,
      texture2D(tex, tc - 1.5 * xUnit).rgb);
  gl_FragColor.g = coeffs.a + dot(coeffs.rgb,
      texture2D(tex, tc - 0.5 * xUnit).rgb);
  gl_FragColor.b = coeffs.a + dot(coeffs.rgb,
      texture2D(tex, tc + 0.5 * xUnit).rgb);
  gl_FragColor.a = coeffs.a + dot(coeffs.rgb,
      texture2D(tex, tc + 1.5 * xUnit).rgb);
}

Note4.2:shader准备好之后,看绘制流程,既是DrawY,DrawU,DrawV,先看ShaderCallbacks代码,继续分析。

private static class ShaderCallbacks implements GlGenericDrawer.ShaderCallbacks {
    // Y'UV444 to RGB888, see https://en.wikipedia.org/wiki/YUV#Y%E2%80%B2UV444_to_RGB888_conversion
    // We use the ITU-R BT.601 coefficients for Y, U and V.
    // The values in Wikipedia are inaccurate, the accurate values derived from the spec are:
    // Y = 0.299 * R + 0.587 * G + 0.114 * B
    // U = -0.168736 * R - 0.331264 * G + 0.5 * B + 0.5
    // V = 0.5 * R - 0.418688 * G - 0.0813124 * B + 0.5
    // To map the Y-values to range [16-235] and U- and V-values to range [16-240], the matrix has
    // been multiplied with matrix:
    // {
   
   {219 / 255, 0, 0, 16 / 255},
    // {0, 224 / 255, 0, 16 / 255},
    // {0, 0, 224 / 255, 16 / 255},
    // {0, 0, 0, 1}}
    private static final float[] yCoeffs =
            new float[]{0.256788f, 0.504129f, 0.0979059f, 0.0627451f};
    private static final float[] uCoeffs =
            new float[]{-0.148223f, -0.290993f, 0.439216f, 0.501961f};
    private static final float[] vCoeffs =
            new float[]{0.439216f, -0.367788f, -0.0714274f, 0.501961f};

    public void setPlaneY() {
        coeffs = yCoeffs;
        stepSize = 1.0f;
    }

    public void setPlaneU() {
        coeffs = uCoeffs;
        stepSize = 2.0f;
    }

    public void setPlaneV() {
        coeffs = vCoeffs;
        stepSize = 2.0f;
    }

    @Override
    public void onNewShader(GlShader shader) {
        xUnitLoc = shader.getUniformLocation("xUnit");
        coeffsLoc = shader.getUniformLocation("coeffs");
    }
    @Override
    public void onPrepareShader(GlShader shader, float[] texMatrix, int frameWidth, int frameHeight,
                                int viewportWidth, int viewportHeight) {
        GLES20.glUniform4fv(coeffsLoc, /* count= */ 1, coeffs, /* offset= */ 0);
        // Matrix * (1;0;0;0) / (width / stepSize). Note that OpenGL uses column major order.
        GLES20.glUniform2f(
                xUnitLoc, stepSize * texMatrix[0] / frameWidth, stepSize * texMatrix[1] / frameWidth);
    }
}

看备注可以了解大致思路:Wiki上的推导说是有误的,正确的RGB转YUV的公式如备注所示,

Y = 0.299 * R + 0.587 * G + 0.114 * B
U = -0.168736 * R - 0.331264 * G + 0.5 * B + 0.5
V = 0.5 * R - 0.418688 * G - 0.0813124 * B + 0.5

To map the Y-values to range [16-235] and U- and V-values to range [16-240], the matrix has been multiplied with matrix:
{
   
   {219 / 255, 0, 0, 16 / 255},
{0, 224 / 255, 0, 16 / 255},
{0, 0, 224 / 255, 16 / 255},
{0, 0, 0, 1}}

接着说了一个转换关系,其实所表达的内容是,YUV的 Full range 转 TV range的过程。至于什么是Full range 和 TV range。这里简单介绍,详细请到这里。(这又可以引出说说BT601、BT709、BT2020的区别

YUV 有多种表现形式

  除了色彩空间, 还需要注意YUV 的多种表现形式, 比如:

  YUV : YUV是一种模拟型号, Y∈ [0,1]   U,V∈[-0.5,0.5] 

  YCbCr :也叫YCC或者Y'CbCr    YCbCr 是数字信号, 它包含两种形式, 分别为TV range 和 full range, TV range 主要是广播电视采用的标准, full range 主要是pc 端采用的标准, 所以full range 有时也叫 pc range

TV range 的各个分量的范围为: YUV  Y∈[16,235]   Cb∈[16-240]   Cr∈[16-240] 

Full range 的各个分量的范围均为: 0-255   

  我们平时接触到的绝大多数都是 YCbCr (tv range) , ffmpeg 解码出来的数据绝大多数也是这个, 虽然ffmpeg 里面将它的格式描述成YUV420P , 实际上它是YCbCr420p tv range

  YUV转tv range:   Y' = 219.0*Y + 16 ;  Cb = U * 224.0 + 128;  Cr = V * 224.0 + 128;   

最终yCoeffs、uCoeffs、vCoeffs(矩阵非齐次运算) = 

最终结果 yCoeffs = {0.299, 0.587, 0.114} * (219 / 255) = {0.256788f, 0.504129f, 0.0979059f, 0.0627451f}   uCoeffs、vCoeffs同理。

Note4.3:解析完coffs,再说说这个xUnit(setpSize),单纯从shader的内容上,能在第一时间能理解这个xUnit的作用是很难的。结合之前的renderMatrix 加以理解:

// Produce a frame buffer starting at top-left corner, not bottom-left.
        final Matrix renderMatrix = new Matrix();
        renderMatrix.preTranslate(0.5f, 0.5f);
        renderMatrix.preScale(1f, -1f);
        renderMatrix.preTranslate(-0.5f, -0.5f);

shaderCallbacks.setPlaneY();
VideoFrameDrawer.drawTexture(drawer, preparedBuffer, renderMatrix, frameWidth, frameHeight,
                /* viewportX= */ 0, /* viewportY= */ 0, viewportWidth,
                /* viewportHeight= */ frameHeight);

public static void drawTexture(RendererCommon.GlDrawer drawer, VideoFrame.TextureBuffer buffer,
                               Matrix renderMatrix, int frameWidth, int frameHeight, int viewportX, int viewportY,
                               int viewportWidth, int viewportHeight) {
    Matrix finalMatrix = new Matrix(buffer.getTransformMatrix());
    finalMatrix.preConcat(renderMatrix);
    float[] finalGlMatrix = RendererCommon.convertMatrixFromAndroidGraphicsMatrix(finalMatrix);
    switch (buffer.getType()) {
        case OES:
            drawer.drawOes(buffer.getTextureId(), finalGlMatrix, frameWidth, frameHeight, viewportX,
                    viewportY, viewportWidth, viewportHeight);
            break;
        case RGB:
            drawer.drawRgb(buffer.getTextureId(), finalGlMatrix, frameWidth, frameHeight, viewportX,
                    viewportY, viewportWidth, viewportHeight);
            break;
        default:
            throw new RuntimeException("Unknown texture type.");
    }
}

// GlGenericDrawer.drawOes.prepareShader会触发ShaderCallbacks.onPrepareShader
@Override
public void onPrepareShader(GlShader shader, float[] texMatrix, int frameWidth, int frameHeight,
                            int viewportWidth, int viewportHeight) {
    GLES20.glUniform4fv(coeffsLoc, /* count= */ 1, coeffs, /* offset= */ 0);
    // Matrix * (1;0;0;0) / (width / stepSize). Note that OpenGL uses column major order.
    GLES20.glUniform2f(
            xUnitLoc, stepSize * texMatrix[0] / frameWidth, stepSize * texMatrix[1] / frameWidth);
}

代码流程脉络如上:显然这个setpSize与renderMartix有莫大的关系,其中renderMartix有一句备注// Produce a frame buffer starting at top-left corner, not bottom-left. 意思是“从左上角而不是左下角开始生成帧缓冲区”,再结合GlGenericDrawer传入的最原始的纹理坐标FULL_RECTANGLE_TEXTURE_BUFFER

// Texture coordinates - (0, 0) is bottom-left and (1, 1) is top-right.
private static final FloatBuffer FULL_RECTANGLE_TEXTURE_BUFFER =
        GlUtil.createFloatBuffer(new float[]{
                0.0f, 0.0f, // Bottom left.
                1.0f, 0.0f, // Bottom right.
                0.0f, 1.0f, // Top left.
                1.0f, 1.0f, // Top right.
        });

由于篇幅的关系,(涉及矩阵乘法原理请到这里)这里直接给出运算过程和结果:

//preTranslate的translate矩阵具体值,通过Androidrexf.com查找到的源码
static float[] setTranslate(float[] dest, float dx, float dy) {
    dest[0] = 1;
    dest[1] = 0;
    dest[2] = dx;
    dest[3] = 0;
    dest[4] = 1;
    dest[5] = dy;
    dest[6] = 0;
    dest[7] = 0;
    dest[8] = 1;
    return dest;
}
//preScale的scale矩阵具体值,通过Androidrexf.com查找到的源码
static float[] getScale(float sx, float sy) {
    return new float[] { sx, 0, 0, 0, sy, 0, 0, 0, 1 };
}

0.0f, 0.0f, 0.0                             1, 0, 0.5    0, 0, 0
1.0f, 0.0f, 0.0 preTranslate(0.5f, 0.5f) 即 0, 1, 0.5 =  1, 0, 0.5
0.0f, 1.0f, 0.0                             0, 0, 1      0,1, 0.5
1.0f, 1.0f, 0.0                                          1, 1, 1

0, 0, 0                       1, 0,  0   0, 0, 0
1, 0, 0.5 preScale(1f, -1f)即 0, -1, 0 = 1, 0, 0.5
0,1, 0.5                     0, 0,  1   0, -1,0.5 
1, 1, 1                                  1, -1, 1 

0, 0, 0                                              0, 0, 0
1, 0, 0.5 preTranslate(-0.5f, -0.5f)    1, 0, -0.5   1, 0, 0
0, -1,0.5                           即  0, 1, -0.5 = 0, -1, 1
1, -1, 1                                0, 0, 1      1, -1, 1

经过renderMatrix的转换后,纹理座标确确实实的向下翻转了。

再然后,DrawY的setpSize = 1;DrawU的setpSize = 2;DrawV的setpSize = 2;而且采集大小是正方形的(都是除frameWidth),结合YUV 4:2:0 采样理论可知(如下图其中,Y 分量用叉表示,UV 分量用圆圈表示。)其实xUnit/setpSize就是采样步阀。

 然后还有一个问题要注意,为啥shader上输出rgba四个采样点,纹理采样分别是 (tc - 1.5 * xUnit)  (tc - 0.5 * xUnit)  (tc + 0.5 * xUnit)  (tc + 1.5 * xUnit) 并不是(tc - 2 * xUnit) (tc - 1 * xUnit) (tc + 1 * xUnit)  (tc + 2 * xUnit)?这是取每个纹素的中心颜色值作为采样色值。

好了,重点都解读完了。剩下的需要关注的提几点:drawU和drawV时候的viewport和窗口大小的确定;JavaI420Buffer的封装使用;再聊一点就是这里没有结合双pbo缓冲readpixel的(不理解说啥的同学自行百度,很多教学文章。)在技巧上巧妙的把rgba转成yuv420的draw,属于GPGPU的思想,大家可以细细体会。

下一章发掘HardwareVideoEncoder有用的知识点,之后就深入PeerConnectionFactory.createPeerConnection。

猜你喜欢

转载自blog.csdn.net/a360940265a/article/details/121532966