视频转码三方库

在研讨端上视频转码紧缩时,发现网上大部分的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视频相同,相似下面的作用:

MediaCodec对HDR视频进行转码压缩
MediaCodec对HDR视频进行转码压缩

比较了LightCompressor和其他库的完成区别后,发现LightCompressor在设置输出视频的mediaformat时,额定设置了Color_StandardColor_TransferColor_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视频便是正确的么?

MediaCodec对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_StandardColor_TransferColor_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提取成果:

MediaCodec对HDR视频进行转码压缩

根本每帧里,都会携带这个信息。

而在运用RGBA1010102YUVP10进行转码后输出的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