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造成的性能问题,以及数据量变大的问题。

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

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

相关推荐
hxjhnct1 天前
Vue 自定义滑块组件
前端·javascript·vue.js
华仔啊1 天前
JavaScript 中如何正确判断 null 和 undefined?
前端·javascript
weibkreuz1 天前
函数柯里化@11
前端·javascript·react.js
king王一帅1 天前
Incremark 0.3.0 发布:双引擎架构 + 完整插件生态,AI 流式渲染的终极方案
前端·人工智能·开源
转转技术团队1 天前
HLS 流媒体技术:畅享高清视频,忘却 MP4 卡顿的烦恼!
前端
程序员的程1 天前
我做了一个前端股票行情 SDK:stock-sdk(浏览器和 Node 都能跑)
前端·npm·github
KlayPeter1 天前
前端数据存储全解析:localStorage、sessionStorage 与 Cookie
开发语言·前端·javascript·vue.js·缓存·前端框架
沉默-_-1 天前
从小程序前端到Spring后端:新手上路必须理清的核心概念图
java·前端·后端·spring·微信小程序
裴嘉靖1 天前
前端获取二进制文件并预览的完整指南
前端·pdf