前言
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造成的性能问题,以及数据量变大的问题。
后面,我们会继续分享有关渲染、录制和多屏渲染相关的文章。
本篇就到这里,希望对大家有所帮助!