ExoPlayer MediaCodec视频解码Buffer模式GPU渲染加速

前言

ExoPlayer是一款扩展性很强的播放器,通过扩展我们可以内接很多解码框架,如ffmpeg、av1等,同样,我们可以做一些功能强化,如最大帧率限制、Buffer模式渲染等。

实际上,在之前的文章《ExoPlayer MediaCodec视频解码Buffer模式支持》我们实现了Buffer模式,本来在写完那篇文章之后,觉得没必要再进一步了,因为目前而言,MediaCodec的Surface模式基本上覆盖了很多场景,其次Buffer模式当时开发的初衷是特殊兜底和测试用;然而,在部分厂商的设备上,Surface模式表现出了不稳定,总是播放失败,当然,神奇的是,这个设备正好跑Exo-FFmpeg能完全解码并流畅渲染,然而测试反馈画质不太满意,有些模糊,原因是Exo-FFmpeg渲染的视频中,我们使用的ANativeWindow渲染的,这个渲染逻辑内部可能使用了其他ColorSpace色彩矩阵,而我们的资源ColorSpace普遍普遍是COLORSPACE_BT709。

经过沟通,我们决定使用Exo MediaCodec Buffer模式渲染,测试反馈这个模式也播放的不错。

显然,我们需要继续优化,提升渲染效率,在GPU上实现Nv21和Nv12转I420,并且动态获取ColorSpace。

在这之前,你可能需要简单了解下色彩空间,具体而言就是色相、亮度、饱和度相关色彩模型。

方案

本篇会优化渲染能力,不再像之前的文章中,通过CPU方式实现实现Nv21、NV12、YUV422等格式转I420,那么,意味着我们需要把原始数据传输到GPU,实现GPU上YUV编码对齐,当然,需要重新规划Shader脚本。

glsl脚本实现

那么,这里我们需要告诉shader,传输进来的数据类型,这里定义u_format_type变量,同时还有告诉shader色彩空间colorFormatMatrix矩阵(rgb 3x3)。

java 复制代码
precision highp float;

// Uniforms (由 Java 代码传入的控制参数)
uniform int u_format_type;      // 0: Planar (I420), 1: NV12, 2: NV21
uniform sampler2D s_texture_y;  // 纹理单元 0 (Y 分量)
uniform sampler2D s_texture_u;  // 纹理单元 1 (U 分量 或 UV/VU 交错分量)
uniform sampler2D s_texture_v;  // 纹理单元 2 (V 分量,仅用于 Planar 格式)

// Varying (由 Vertex Shader 传来的插值后的纹理坐标)
varying vec2 v_texCoord;
//颜色空间
uniform mat3 colorFormatMatrix;

void main() {
    // 1. 采样 Y (亮度) 分量
    float Y = texture2D(s_texture_y, v_texCoord).r;
    float U, V;

    // 2. 根据格式类型 u_format_type 采样 U 和 V 分量
    if (u_format_type == 0 || u_format_type == 3) {
        // Planar (I420/YV12): 从两个独立的纹理中采样 U 和 V
        U = texture2D(s_texture_u, v_texCoord).r;
        V = texture2D(s_texture_v, v_texCoord).r;
    } else {
        // Semi-Planar (NV12/NV21/NV16): 从一个共享纹理中采样 UV 或 VU
        vec4 uv_data = texture2D(s_texture_u, v_texCoord); // s_texture_u 被用于 UV/VU

        if (u_format_type == 1) {
            // NV12 (UVUV...) -> U=R, V=G
            U = uv_data.r;
            V = uv_data.a;
        } else if(u_format_type == 2){
            // NV21 (VUVU...) -> V=R, U=G
            V = uv_data.r;
            U = uv_data.a;
        }else if(u_format_type == 4){
            // NV12 或 NV16 - 逻辑完全一样!
            // texture_u 实际上存的是 UVUV...
             U = uv_data.r;
             V = uv_data.a;
        }
    }

    // 3. YUV 到 RGB 转换 (BT.601 标准)
    // 移除所有的 'f' 后缀

    // 调整 YUV 范围到中心点
    Y = 1.1643 * (Y - 0.0625);
    U = U - 0.5;
    V = V - 0.5;

    // YUV 转换矩阵应用于调整后的 Y, U, V (BT.601)
 //  float R = Y + 1.596 * V;
  // float G = Y - 0.391 * U - 0.813 * V;
  // float B = Y + 2.018 * U;

   vec3 yuv;
   yuv.x = Y;
   yuv.y = U;
   yuv.z = V;

    // 4. 输出最终颜色
   // gl_FragColor = vec4(R, G, B, 1.0);
    gl_FragColor = vec4(colorFormatMatrix * yuv, 1.0);
}

当然,上面提到了色彩空间,我们在纹理传输时,会提供3x3的矩阵

java 复制代码
private static final float[] kColorConversion601 = {
        1.164f, 1.164f, 1.164f,
        0.0f, -0.392f, 2.017f,
        1.596f, -0.813f, 0.0f,
};

private static final float[] kColorConversion709 = {
        1.164f, 1.164f, 1.164f,
        0.0f, -0.213f, 2.112f,
        1.793f, -0.533f, 0.0f,
};

private static final float[] kColorConversion2020 = {
        1.168f, 1.168f, 1.168f,
        0.0f, -0.188f, 2.148f,
        1.683f, -0.652f, 0.0f,
};

GLYUVSurfaceView实现

传输原理

有了脚本,我们只需要按部就班实现GLSurfaceView,但纹理上传,我们需要了解下OPEN GL上传的数据类型,单一类型的纹理,都是通过GLES20.GL_LUMINANCE实现上传,而对于UVUV或者UVUV交错的类型,使用GLES20.GL_LUMINANCE(明亮度,可以作为单一类型如Y、U、V、R、G、B传输)显然在Shader无法准确提取出U和V分量,那怎么办呢?

实际上,Open GL ES 提供了一个数据类型GLES20.GL_LUMINANCE_ALPHA,和GLES20.GL_LUMINANCE相比GLES20.GL_LUMINANCE_ALPHA是每2个字节作为一组,而前者是单一字节,如果使用GLES20.GL_LUMINANCE会出现网格现象(Y分离正常,UV分量处理不当出现画面网格),那么GLES20.GL_LUMINANCE_ALPHA正好可以交错实现UVUVUV...或者VUVUVU...这样的传输,而且在GL中能够直接提取U分量和V分量。

具体实现

下面是GLSurfaceView核心逻辑,我们会实现YUV420(NV21、NV12、PLANAR)、YUV422(交错、PLANAR)的Buffer数据直接传输至GPU,然后在GPU上进行格式转换

java 复制代码
/**
 * 核心:计算布局、处理 Stride(零拷贝失败时)并上传纹理。
 */
/**
 * 核心:计算布局、处理 Stride 并上传纹理。
 */
private void uploadYuvTextures(ByteBuffer fullBuffer, int format, int width, int height, int stride) {
    YuvFormat formatType = getFormatType(format);

    final int yStride = stride;
    final int yWidth = width;
    final int yHeight = height;

    // --- 1. 区分 420 和 422 ---
    boolean is422 = (formatType == YuvFormat.PLANAR_422 || formatType == YuvFormat.NV16);

    // --- 2. 计算色度(Chroma)平面的尺寸 ---
    // 宽度:无论是 420 还是 422,UV 宽度通常都是 Y 的一半
    final int cWidth = (width + 1) / 2;

    // 高度:420 是 Y 的一半,422 与 Y 相同
    final int cHeight = is422 ? height : (height + 1) / 2;

    // Stride:Planar 模式下通常是 Y stride 的一半
    final int cStride = stride / 2;

    int uOffset, vOffset;

    // --- 3. 根据格式计算偏移量 ---
    switch (formatType) {
        case PLANAR:
        case PLANAR_422: // 逻辑相同,只是 cHeight 变了
            uOffset = yStride * height;
            vOffset = uOffset + cStride * cHeight;
            break;
        case NV12:
        case NV16: // 逻辑相同,NV16 的 UV 平面高度是 NV12 的两倍
            uOffset = yStride * height;
            vOffset = -1; // 不使用
            break;
        case NV21:
            vOffset = yStride * height;
            uOffset = -1; // 不使用
            break;
        default:
            return;
    }

    // --- 4. 上传 Y 纹理 (Texture Unit 0) ---
    fullBuffer.position(0);
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[0]);
    uploadPlane(fullBuffer, yStride, yWidth, yHeight, GLES20.GL_LUMINANCE);

    try {
        GlUtil.checkGlError();
    } catch (GlUtil.GlException e) {
        // 错误处理...
    }

    // --- 5. 上传 U/V 或 UV 纹理 ---
    if (formatType == YuvFormat.PLANAR || formatType == YuvFormat.PLANAR_422) {
        // Planar: U (T1) 和 V (T2) 分开

        // 上传 U
        fullBuffer.position(uOffset);
        GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[1]);
        uploadPlane(fullBuffer, cStride, cWidth, cHeight, GLES20.GL_LUMINANCE);

        // 上传 V
        fullBuffer.position(vOffset);
        GLES20.glActiveTexture(GLES20.GL_TEXTURE2);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[2]);
        uploadPlane(fullBuffer, cStride, cWidth, cHeight, GLES20.GL_LUMINANCE);

    } else {
        // Semi-Planar (NV12/NV21/NV16): UV 交错 (T1)

        // 对于 NV12/NV16,UV 数据交错存放,宽度看似是 cWidth,但每个像素有2个字节(U和V)
        // 在 GL_LUMINANCE_ALPHA 格式下,纹理宽度 = cWidth,纹理高度 = cHeight
        // 内存中的行宽 (Stride) 通常等于 Y 的 Stride
        int uvStride = yStride;
        int uvPlaneWidth = cWidth;

        int uvOffset = (formatType == YuvFormat.NV12 || formatType == YuvFormat.NV16) ? uOffset : vOffset;
        fullBuffer.position(uvOffset);

        GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[1]);

        // 注意:这里使用 GL_LUMINANCE_ALPHA,一个纹理像素包含 (Luminance, Alpha) 即 (U, V)
        uploadPlane(fullBuffer, uvStride, uvPlaneWidth, cHeight, GLES20.GL_LUMINANCE_ALPHA);

        // 禁用第三个纹理单元
        GLES20.glActiveTexture(GLES20.GL_TEXTURE2);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
    }

    currentFormat = formatType;
}

当然,uploadPlane的源码同样重要,此部份决定画面是否会倾斜、黑屏、或者花屏,下面是完整源码,不过在实际的实现中,我们要考虑planeStride大于数据行的问题,对空行数据进行填充,防止错位。

java 复制代码
/**
 * 【关键函数】辅助函数:上传单个平面纹理,支持 GLES 3.0 零拷贝和 GLES 2.0 回退。
 */
private void uploadPlane(ByteBuffer buffer, int planeStride, int planeWidth, int planeHeight, int glFormat) {
    GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
    ByteBuffer localBuffer = buffer.duplicate();
    int bytesPerPixel = (glFormat == GLES20.GL_LUMINANCE_ALPHA) ? 2 : 1;
    int actualDataBytesPerRow = planeWidth * bytesPerPixel;

    if (planeStride > actualDataBytesPerRow) {
        if (glVersion >= 3) {
            GLES30.glPixelStorei(GLES30.GL_UNPACK_ROW_LENGTH, planeStride);

            GLES20.glTexImage2D(
                    GLES20.GL_TEXTURE_2D, 0, glFormat,
                    planeWidth, planeHeight, 0,
                    glFormat, GLES20.GL_UNSIGNED_BYTE, localBuffer.slice());

            GLES30.glPixelStorei(GLES30.GL_UNPACK_ROW_LENGTH, 0);

            buffer.position(buffer.position() + planeStride * planeHeight);

        } else {
            int paddingBytes = planeStride - actualDataBytesPerRow;

            ByteBuffer cleanBuffer = ByteBuffer.allocateDirect(planeWidth * planeHeight * bytesPerPixel)
                    .order(ByteOrder.nativeOrder());

            for (int i = 0; i < planeHeight; i++) {
                localBuffer.limit(localBuffer.position() + actualDataBytesPerRow);
                cleanBuffer.put(localBuffer.slice());
                localBuffer.position(localBuffer.position() + paddingBytes);
            }

            cleanBuffer.flip();

            GLES20.glTexImage2D(
                    GLES20.GL_TEXTURE_2D, 0, glFormat,
                    planeWidth, planeHeight, 0,
                    glFormat, GLES20.GL_UNSIGNED_BYTE, cleanBuffer);

            // 推进原始 buffer 的 position
            buffer.position(buffer.position() + planeStride * planeHeight);
        }

    } else {
       //stride等于width的情况,完美,直接上传
        localBuffer.limit(localBuffer.position() + planeStride * planeHeight);
        buffer.position(buffer.position() + planeStride * planeHeight);

        GLES20.glTexImage2D(
                GLES20.GL_TEXTURE_2D, 0, glFormat,
                planeWidth, planeHeight, 0,
                glFormat, GLES20.GL_UNSIGNED_BYTE, localBuffer.slice());
    }
}

以上是Shader和纹理传输的核心逻辑,当然,GL仅仅实现传输是不够的,因为我们还需要实现顶点坐标(世界坐标系)和着色器坐标的绑定。

接下来实现一下onDrawFrame

java 复制代码
@Override
public void onDrawFrame(GL10 gl) {
    // ... (onDrawFrame 逻辑不变) ...
    @Nullable
    VideoDecoderOutputBuffer pendingOutputBuffer =
            pendingOutputBufferReference.getAndSet(/* newValue= */ null);
    if (pendingOutputBuffer == null && renderedOutputBuffer == null) {
        return;
    }
    if (pendingOutputBuffer != null) {
        if (renderedOutputBuffer != null) {
            renderedOutputBuffer.release();
        }
        renderedOutputBuffer = pendingOutputBuffer;
    }

    final VideoDecoderOutputBuffer outputBuffer = checkNotNull(renderedOutputBuffer);
    ByteBuffer buffer = outputBuffer.data;
    int format = outputBuffer.decoderPrivate;
    int frameWidth = outputBuffer.width;
    int frameHeight = outputBuffer.height;
    int stride = outputBuffer.yuvStrides[0];

    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    if (buffer != null && frameWidth > 0 && frameHeight > 0) {
       //处理颜色空间(主要是画面饱和度)
        float[] colorConversion = kColorConversion709;
        switch (outputBuffer.colorspace) {
            case VideoDecoderOutputBuffer.COLORSPACE_BT601:
                colorConversion = kColorConversion601;
                break;
            case VideoDecoderOutputBuffer.COLORSPACE_BT2020:
                colorConversion = kColorConversion2020;
                break;
            case VideoDecoderOutputBuffer.COLORSPACE_BT709:
            default:
                // Do nothing.
                break;
        }
        GLES20.glUniformMatrix3fv(
                colorFormatMatrix,
                /* color= */ 1,
                /* transpose= */ false,
                colorConversion,
                /* offset= */ 0);

        uploadYuvTextures(buffer, format, frameWidth, frameHeight, stride);
    }

    if (frameWidth > 0 && frameHeight > 0) {
        GLES20.glUseProgram(programHandle);

        GLES20.glUniform1i(formatTypeUniform, currentFormat.getType());

        int positionHandle = GLES20.glGetAttribLocation(programHandle, "a_position");
        GLES20.glEnableVertexAttribArray(positionHandle);
        GLES20.glVertexAttribPointer(positionHandle, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);

        int texCoordHandle = GLES20.glGetAttribLocation(programHandle, "a_texCoord");
        GLES20.glEnableVertexAttribArray(texCoordHandle);
        GLES20.glVertexAttribPointer(texCoordHandle, 2, GLES20.GL_FLOAT, false, 0, textureBuffer);

        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

        GLES20.glDisableVertexAttribArray(positionHandle);
        GLES20.glDisableVertexAttribArray(texCoordHandle);
    }
}

上面就是GLVideoSurfaceView的核心逻辑了,这里提一下ColorSpace(颜色空间),颜色空间默认情况都BT709,用于决定画面的饱和度和色彩鲜艳程度,处理不当容易画面失真,下面我们也会讲到。

Exo VideoRender适配

既然渲染部分实现,那么接下来该实现这部分,在之前的那篇文章中,我们用CPU做了大量的工作,不过缺点显而易见。

作为1帧1920x1080的数据,算法复杂度为O(w*h+width),同时一帧数据占比2.79MB,在CPU中运算,内存和CPU负载很高。

YUV Buffer数据处理

为了保持调试的需要,我们也会保留CPU渲染的方式,同时新增GPU的方式,当然,我们使用的GLVideoSurfaceView基本保持一个,同时decoderPrivate用来表示ColorSpace,不过需要注意的是yuvI420ToGpu函数中, decoderOutputBuffer.decoderPrivate强制设置为CodecCapabilities.COLOR_FormatYUV420Planar,因为CPU转换后的就是YUV420Planar结构,如果这个错误设置,必然导致灰白图像中绿条闪烁。

java 复制代码
private void onDrainOutputBuffer(MediaCodecAdapter codec,ByteBuffer decodeOutputBuffer, int index, long presentationTimeUs) {
  if(isGPUTransform){
    rawYuvToGpu(codec, decodeOutputBuffer, presentationTimeUs);
  }else {
    yuvI420ToGpu(codec, decodeOutputBuffer, presentationTimeUs);
  }
}

private void rawYuvToGpu(MediaCodecAdapter codec, ByteBuffer decodeOutputBuffer, long presentationTimeUs) {
  if(decodeOutputBuffer != null){
    MediaFormat outputFormat = codec.getOutputFormat();
    int colorFormat = outputFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT);
    int width = outputFormat.getInteger(MediaFormat.KEY_WIDTH);
    int height = outputFormat.getInteger(MediaFormat.KEY_HEIGHT);

    int alignWidth = width;
    int alignHeight = height;
    boolean isDebug = false;

    int stride = outputFormat.getInteger(MediaFormat.KEY_STRIDE);
    int sliceHeight = outputFormat.getInteger(MediaFormat.KEY_SLICE_HEIGHT);
    if (stride > 0 && sliceHeight > 0) {
      alignWidth = stride;
      alignHeight = sliceHeight;
    }

    int decodeOutputBufferSize  = decodeOutputBuffer.limit();
    ByteBufferHolder finalBufferHolder = byteBufferPool.obtain(decodeOutputBufferSize);
    finalBufferHolder.put(decodeOutputBuffer);
    finalBufferHolder.position(0);

    VideoDecoderOutputBufferWrapper decoderOutputBuffer = new VideoDecoderOutputBufferWrapper(new DecoderOutputBuffer.Owner<VideoDecoderOutputBuffer>() {
      @Override
      public void releaseOutputBuffer(VideoDecoderOutputBuffer outputBuffer) {
        if(outputBuffer instanceof VideoDecoderOutputBufferWrapper){
          byteBufferPool.recycle(((VideoDecoderOutputBufferWrapper) outputBuffer).bufferHolder);
        }
      }
    });
    decoderOutputBuffer.init(presentationTimeUs,C.VIDEO_OUTPUT_MODE_YUV,null);
    decoderOutputBuffer.bufferHolder = finalBufferHolder;
    decoderOutputBuffer.data = finalBufferHolder.getBuffer();
    decoderOutputBuffer.decoderPrivate = colorFormat;
    decoderOutputBuffer.width = alignWidth;
    decoderOutputBuffer.height = alignHeight;
    decoderOutputBuffer.colorspace = MediaCodecColorSpaceExtractor.extractColorStandard(outputFormat);
    decoderOutputBuffer.yuvStrides = new int[]{stride,-1,-1};

    VideoDecoderOutputBufferRenderer bufferRenderer = videoDecoderOutputBufferRenderer;
    if(bufferRenderer != null){
      bufferRenderer.setOutputBuffer(decoderOutputBuffer);
    }
  }
}
private void yuvI420ToGpu(MediaCodecAdapter codec, ByteBuffer decodeOutputBuffer, long presentationTimeUs) {
  if(decodeOutputBuffer != null){
    MediaFormat outputFormat = codec.getOutputFormat();
    int colorFormat = outputFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT);
    int width = outputFormat.getInteger(MediaFormat.KEY_WIDTH);
    int height = outputFormat.getInteger(MediaFormat.KEY_HEIGHT);

    int alignWidth = width;
    int alignHeight = height;

    int stride = outputFormat.getInteger(MediaFormat.KEY_STRIDE);
    int sliceHeight = outputFormat.getInteger(MediaFormat.KEY_SLICE_HEIGHT);
    if (stride > 0 && sliceHeight > 0) {
      alignWidth = stride;
      alignHeight = sliceHeight;
    }

  //  alignWidth = alignTo16(alignWidth); 对齐逻辑存在风险
   // alignHeight = alignTo16(alignHeight); 对齐逻辑存在风险

    int decodeOutputBufferSize = 0;
    ByteBufferHolder finalBufferHolder = null;

    switch (colorFormat){
      case CodecCapabilities.COLOR_FormatYUV420Flexible:
      case CodecCapabilities.COLOR_FormatYUV420Planar:
      case CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
        //android 这里是I420格式
        decodeOutputBufferSize = decodeOutputBuffer.limit();
        finalBufferHolder = byteBufferPool.obtain(decodeOutputBufferSize);
        finalBufferHolder.put(decodeOutputBuffer);
        finalBufferHolder.position(0);
        break;
      case CodecCapabilities.COLOR_FormatYUV420SemiPlanar: {
        decodeOutputBufferSize = alignWidth * alignHeight * 3 / 2;
        ByteBufferHolder bufferHolder = byteBufferPool.obtain(decodeOutputBufferSize);
        YuvTools.yuvNv12ToYuv420P(decodeOutputBuffer, bufferHolder.getBuffer(), alignWidth, alignHeight);
        bufferHolder.position(0);
        finalBufferHolder = bufferHolder;
      }
        break;
      case CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar: {
        decodeOutputBufferSize = alignWidth * alignHeight * 3 / 2;
        ByteBufferHolder bufferHolder = byteBufferPool.obtain(decodeOutputBufferSize);
        YuvTools.yuvNv21ToYuv420P(decodeOutputBuffer, bufferHolder.getBuffer(), alignWidth, alignHeight);
        bufferHolder.position(0);
        finalBufferHolder = bufferHolder;
      }
        break;
      default:
        throw new IllegalStateException("the Color-Format is not supported : " + colorFormat);
    }
    VideoDecoderOutputBufferWrapper decoderOutputBuffer = new VideoDecoderOutputBufferWrapper(new DecoderOutputBuffer.Owner<VideoDecoderOutputBuffer>() {
      @Override
      public void releaseOutputBuffer(VideoDecoderOutputBuffer outputBuffer) {
        if(outputBuffer instanceof VideoDecoderOutputBufferWrapper){
          byteBufferPool.recycle(((VideoDecoderOutputBufferWrapper) outputBuffer).bufferHolder);
        }
      }
    });
    decoderOutputBuffer.init(presentationTimeUs,C.VIDEO_OUTPUT_MODE_YUV,null);
    boolean isDebug = false;


    decoderOutputBuffer.init(presentationTimeUs,C.VIDEO_OUTPUT_MODE_YUV,null);
    decoderOutputBuffer.bufferHolder = finalBufferHolder;
    decoderOutputBuffer.data = finalBufferHolder.getBuffer();
    decoderOutputBuffer.decoderPrivate = CodecCapabilities.COLOR_FormatYUV420Planar;
    decoderOutputBuffer.width = alignWidth;
    decoderOutputBuffer.height = alignHeight;
    decoderOutputBuffer.colorspace = MediaCodecColorSpaceExtractor.extractColorStandard(outputFormat);
    decoderOutputBuffer.yuvStrides = new int[]{stride,stride/2,stride/2};

    if (isDebug) {
      finalBufferHolder.position(0);
      int limit = finalBufferHolder.limit();
      Buffer buffer = bufferPool.obtain(limit);
      finalBufferHolder.get(buffer.getBuffer(), 0, limit);
      buffer.setEffectiveSize(limit);
      Bitmap bitmap = YuvTools.toBitmap(buffer.getBuffer(), width, height);
      Log.d(TAG, "Bitmap = " + bitmap);
      buffer.recycle();
      finalBufferHolder.position(0);
    }

  //  yuvDataBuffer.recycle();
    VideoDecoderOutputBufferRenderer bufferRenderer = videoDecoderOutputBufferRenderer;
    if(bufferRenderer != null){
      bufferRenderer.setOutputBuffer(decoderOutputBuffer);
    }
    decodeOutputBuffer = null;
  }
}

以上主要是适配ExoPlayer代码,通过上面的逻辑,我们完整实现了MediaCodec->Buffer(I420、NV21、NV12、YUV422)->GPU的核心流程。

色彩空间提取

其实,这里我们注意到MediaCodecColorSpaceExtractor,这个负责提取色彩空间

java 复制代码
// 假设这是一个解码循环中的辅助类
public class MediaCodecColorSpaceExtractor {

    private static final String TAG = "ColorSpaceExtractor";

    /**
     * 在 MediaCodec 处于解码状态时,从输出格式中提取颜色空间信息。
     *
     * @param outputFormat 已启动的 MediaFormat 实例
     * @return 颜色标准常量 (例如 MediaFormat.COLOR_STANDARD_BT709),如果不支持或未找到,则返回 -1
     */
    public static int extractColorStandard(MediaFormat outputFormat) {
        try {
            if (outputFormat == null) {
                return -1;
            }
            if (outputFormat.containsKey(MediaFormat.KEY_COLOR_STANDARD)) {
                int colorStandard = outputFormat.getInteger(MediaFormat.KEY_COLOR_STANDARD);
                return getColorSpaceStandard(colorStandard);
            } else {
                return -1;
            }
        } catch (IllegalStateException e) {
            return -1;
        }
    }


    private static int getColorSpaceStandard(int standard) {
        switch (standard) {
            case MediaFormat.COLOR_STANDARD_BT709:
             //   VideoLogUtil.d(TAG, "BT.709 (HD/Standard)");
                return VideoDecoderOutputBuffer.COLORSPACE_BT709;
            case MediaFormat.COLOR_STANDARD_BT601_NTSC:
             //   VideoLogUtil.d(TAG, "BT.601 NTSC (SD)");
                return VideoDecoderOutputBuffer.COLORSPACE_BT601;
            case MediaFormat.COLOR_STANDARD_BT601_PAL:
               // VideoLogUtil.d(TAG, "BT.601 PAL (SD)");
                return VideoDecoderOutputBuffer.COLORSPACE_BT601;
            case MediaFormat.COLOR_STANDARD_BT2020:
              //  VideoLogUtil.d(TAG, "BT.2020 (UHD/HDR)");
                return VideoDecoderOutputBuffer.COLORSPACE_BT2020;
            default:
               // VideoLogUtil.d(TAG, "Unknown/Default (" + standard + ")");
                ;
        }
        return -1;
    }


    private String getColorRangeName(int range) {
        switch (range) {
            case MediaFormat.COLOR_RANGE_FULL:
                return "Full (0-255)";
            case MediaFormat.COLOR_RANGE_LIMITED:
                return "Limited (16-235)";
            default:
                return "Unknown Range";
        }
    }

    private String getColorTransferName(int transfer) {
        switch (transfer) {
            case MediaFormat.COLOR_TRANSFER_SDR_VIDEO:
                return "SDR (Standard Dynamic Range)";
            case MediaFormat.COLOR_TRANSFER_HLG:
                return "HLG (Hybrid Log-Gamma) - HDR";
            case MediaFormat.COLOR_TRANSFER_ST2084:
                return "PQ (Perceptual Quantizer) - HDR";
            default:
                return "Unknown Transfer";
        }
    }
}

总结

以上就是完整代码,本篇的主要内容是通过MediaCodecBuffer模式,避免CPU到GPU之前需要CPU转YUV I420造成的性能问题,以及数据量变大的问题。

后面,我们会继续分享有关渲染、录制和多屏渲染相关的文章。

本篇就到这里,希望对大家有所帮助!

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax