视频转码三方库
在研讨端上视频转码紧缩时,发现网上大部分的Demo或转码库是基于FFmpeg
的,只要少部分的是选用Android的MediaCodec Api
进行硬件转码的。
LightCompressor
LiTr
android-transcoder
Transcoder
上述几个库的完成原理,其实都是参考的Android Encode cts。运用一个自行准备的EGL Surface
,将编码器解码原视频后的每帧画面,制作到EGL Surface上,然后将这个EGL Surface作为编码器的输入源,然后再运用MediaMuxer
混合音频、视频轨道,输出最终的视频文件。
运用HDR视频对上面几个库进行测验,发现只要LightCompressor
可以在转码后,依然是HDR视频。HDR视频在通过其他几个库的转码后,就变成了SDR,且色彩呈现了泛白的状况。就和在不支撑HDR作用的设备上播放HDR视频相同,相似下面的作用:
比较了LightCompressor
和其他库的完成区别后,发现LightCompressor
在设置输出视频的mediaformat时,额定设置了Color_Standard
,Color_Transfer
,Color_Range
。
if (Build.VERSION.SDK_INT > 23) {
getColorStandard(inputFormat)?.let {
setInteger(MediaFormat.KEY_COLOR_STANDARD, it)
}
getColorTransfer(inputFormat)?.let {
setInteger(MediaFormat.KEY_COLOR_TRANSFER, it)
}
getColorRange(inputFormat)?.let {
setInteger(MediaFormat.KEY_COLOR_RANGE, it)
}
}
那么LightCompressor
转码出来的HDR视频便是正确的么?
可以看到,原本10bit位深的HDR,变成了8bit位深。
如何正确处理HDR视频
首先一点,便是需求在支撑HDR视频录制的设备上才能考虑将HDR视频转码输出成HDR视频
将HDR转成色彩正确的SDR视频
已然运用了EGL Surface
作为数据中转,那么其实可以在OpenGL中进行正确的色彩转化,将BT.2020转成BT.709或BT.601,再进行制作。这个的原理和在不支撑HDR设备上播放HDR视频是相同的。可以参看信念着了火的HDR转SDR系列文章,以及关键帧Keyframe的如何正确将 HDR 视频转换成 SDR 视频。
保持HDR视频格式
已然LightCompressor
中设置Color_Standard
、Color_Transfer
、Color_Range
后能输出半成品HDR了,阐明这三个参数便是必要的,那么就需求对不正确的地方进行研讨处理。
8bit、10bit位深
变成8bit是由于在创立EGL Surface
时,InputSurface
中色彩空间运用的是RGB888。
// Configure EGL for recordable and OpenGL ES 2.0. We want enough RGB bits
// to minimize artifacts from possible YUV conversion.
int[] attribList = {
EGL14.EGL_RED_SIZE, 8,
EGL14.EGL_GREEN_SIZE, 8,
EGL14.EGL_BLUE_SIZE, 8,
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGLExt.EGL_RECORDABLE_ANDROID, 1,
EGL14.EGL_NONE
};
int[] numConfigs = new int[1];
if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, mConfigs, 0, mConfigs.length,
numConfigs, 0)) {
throw new RuntimeException("unable to find RGB888+recordable ES2 EGL config");
}
所以在纹路层面上,就现已不是广色域了。
RGBA1010102
假如去检查最新的Android 13中的cts,可以发现里边现已支撑了highBitDepth,便是把EGL环境装备成RGBA1010102
。
// Configure EGL for recordable and OpenGL ES 2.0. We want enough RGB bits
// to minimize artifacts from possible YUV conversion.
int eglColorSize = useHighBitDepth ? 10: 8;
int eglAlphaSize = useHighBitDepth ? 2: 0;
int recordable = useHighBitDepth ? 0: 1;
int[] attribList = {
EGL14.EGL_RED_SIZE, eglColorSize,
EGL14.EGL_GREEN_SIZE, eglColorSize,
EGL14.EGL_BLUE_SIZE, eglColorSize,
EGL14.EGL_ALPHA_SIZE, eglAlphaSize,
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGLExt.EGL_RECORDABLE_ANDROID, recordable,
EGL14.EGL_NONE
};
这样是否真的可行?通过简略测验,很多设备都是不支撑RGBA1010102+Recordable装备的,只要那些可以拍照杜比视界的小米设备可以支撑。在这些设备上,用上述装备,就能使得转码输出的HDR视频的位深为10bit。
YUVP10
实际上,Android阵营中,HDR视频的主要生产者,是华为设备,于2022年下半年推出的Vivid HDR
视频。像除华为外的其他设备,并没有在相机中默认启用HDR录制,而华为自2022年下半年mate50系列开始,就直接默认启用了Vivid HDR的录制,这就使得华为设备录制的HDR视频量超过了Android总HDR视频量的50%。
而关于Vivid HDR视频,在华为设备上是无法运用RGBA1010102
的,会使得编码器在编码时报错。那么就需求运用YUVP10
来处理Vivid HDR视频。
EGL的装备如下:
public static final String EGL_YUV_EXT_NAME = "EGL_EXT_yuv_surface";
public static final int EGL_YUV_BUFFER_EXT = 0x3300;
public static final int EGL_YUV_ORDER_EXT = 0x3301;
public static final int EGL_YUV_ORDER_YUV_EXT = 0x3302;
public static final int EGL_YUV_NUMBER_OF_PLANES_EXT = 0x3311;
public static final int EGL_YUV_SUBSAMPLE_EXT = 0x3312;
public static final int EGL_YUV_DEPTH_RANGE_EXT = 0x3317;
public static final int EGL_YUV_CSC_STANDARD_EXT = 0x330A;
public static final int EGL_YUV_PLANE_BPP_EXT = 0x331A;
public static final int EGL_YUV_SUBSAMPLE_4_2_0_EXT = 0x3313;
public static final int EGL_YUV_DEPTH_RANGE_LIMITED_EXT = 0x3318;
public static final int EGL_YUV_DEPTH_RANGE_FULL_EXT = 0x3319;
public static final int EGL_YUV_CSC_STANDARD_601_EXT = 0x330B;
public static final int EGL_YUV_CSC_STANDARD_709_EXT = 0x330C;
public static final int EGL_YUV_CSC_STANDARD_2020_EXT = 0x330D;
public static final int EGL_YUV_PLANE_BPP_0_EXT = 0x331B;
public static final int EGL_YUV_PLANE_BPP_8_EXT = 0x331C;
public static final int EGL_YUV_PLANE_BPP_10_EXT = 0x331D;
int[] attribList = {
EGL14.EGL_SURFACE_TYPE, EGL14.EGL_WINDOW_BIT,
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL14.EGL_COLOR_BUFFER_TYPE, GLUtils.EGL_YUV_BUFFER_EXT,
GLUtils.EGL_YUV_ORDER_EXT, GLUtils.EGL_YUV_ORDER_YUV_EXT,
GLUtils.EGL_YUV_NUMBER_OF_PLANES_EXT, 2,
GLUtils.EGL_YUV_SUBSAMPLE_EXT, GLUtils.EGL_YUV_SUBSAMPLE_4_2_0_EXT,
GLUtils.EGL_YUV_DEPTH_RANGE_EXT, GLUtils.EGL_YUV_DEPTH_RANGE_LIMITED_EXT,
GLUtils.EGL_YUV_CSC_STANDARD_EXT, GLUtils.EGL_YUV_CSC_STANDARD_2020_EXT,
GLUtils.EGL_YUV_PLANE_BPP_EXT, GLUtils.EGL_YUV_PLANE_BPP_10_EXT,
EGL14.EGL_NONE
};
运用YUVP10
,也就意味着,OpenGL需求处理的纹路,也得是YUV的。因而在cts
中可以找到的,用于编码器输出制作帧的TextureRender
类,里边的shader glsl
也需求修改。
原先的shader
是面向rgba
纹路的:
#extension GL_OES_EGL_image_external : require
precision mediump float;
varying vec2 vTextureCoord;
uniform samplerExternalOES sTexture;
void main() {
gl_FragColor = texture2D(sTexture, vTextureCoord);
}
这儿需求改成YUV纹路
:
#version 300 es
#extension GL_EXT_YUV_target : require
#extension GL_OES_EGL_image_external : require
#extension GL_OES_EGL_image_external_essl3 : require
precision highp float;
uniform __samplerExternal2DY2YEXT sTexture;
in vec2 vTextureCoord;
layout (yuv) out vec4 color;
void main()
{
color = texture(sTexture, vTextureCoord);
}
需求注意的是,上面fragment
着色器处理YUV
纹路,需求用到ES 3
版本,处理RGBA
纹路用的是ES 2
版本,因而vertex
着色器也需求对应换成ES 3
的完成。
运用YUV
纹路后,就可以将Vivid HDR转码输出10bit的HDR视频了。
理论上而言,在设备支撑的状况下运用YUV
纹路,应该是可以处理所有的HDR视频的,并不局限于Vivid HDR视频。
HDR元数据
HDR视频除了色彩空间和一般视频有不同外,最重要的便是HDR视频会额定携带HDR元数据,里边携带了亮度信息,用于对视频局部进行亮度调整。
HDR元数据包括静态元数据和HDR 10+有的动态元数据。
静态元数据
静态元数据的设置很简略,直接从MediaFormat中提取即可。
if (inputFormat.containsKey(MediaFormat.KEY_HDR_STATIC_INFO)) {
outputFormat.setByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO,
inputFormat.getByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO));
}
动态元数据
运用ffprobe
命令,可以提取帧里边的动态元数据:
ffprobe -show_frames -select_streams v video.mp4 -print_format json
杜比视界及Vivid HDR提取成果:
根本每帧里,都会携带这个信息。
而在运用RGBA1010102
或YUVP10
进行转码后输出的10bit HDR
视频中,并不能提取到这些信息。阐明HDR元数据丢失了,没有被写进转码输出的视频中。
那这时,就需求我们手动提取HDR元数据,然后手动丢个编码器了。
提取HDR动态元数据
在解码器解码HDR视频时,在onOutputBufferAvailable
回调中,是会把每帧中的额定信息带出来的(假如有的话),HDR元数据就在其中。
@Override
public void onOutputBufferAvailable(@NonNull MediaCodec codec, int index,
@NonNull MediaCodec.BufferInfo info) {
byte[] hdr10Info = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
MediaFormat format = codec.getOutputFormat();
//这个便是HDR元数据
ByteBuffer hdrByteBuffer = format.getByteBuffer(MediaFormat.KEY_HDR10_PLUS_INFO);
if (hdrByteBuffer != null) {
int limit = hdrByteBuffer.limit();
if (limit > 0) {
hdr10Info = new byte[limit];
hdrByteBuffer.get(hdr10Info);
}
}
}
}
向编码器写入动态元数据
提取了HDR元数据后,只需求将元数据手动设置给编码器即可。
//hdr10+的元数据需求手动写给编码器
Bundle codecParameters = new Bundle();
codecParameters.putByteArray(MediaCodec.PARAMETER_KEY_HDR10_PLUS_INFO, hdr10Info);
if (mEncoder != null) {
mEncoder.setParameters(codecParameters);
}
帧和元数据对应
需求注意的是,假如对解码器和编码器选用异步形式,那么向编码器设置元数据的机遇,就需求额定处理。由于假如解码器非常快的解析了2帧元数据,并设置给了编码器,这时编码器只吐出了一帧,那么就会呈现把原视频第二帧的元数据设置给了第一帧的状况。因而需求做手动的同步处理,解码器解一帧,编码器编一帧,再解码器解一帧,保证元数据的对应联系。
完成后,重新运用ffprobe
命令验证一下元数据即可。
END
依照上面的方法处理,就可以转码HDR视频后依然输出HDR视频了。
Git项目代码:MediaCodecExample