在人工智能和短视频应用如火如荼的今天,手机卫士团队去年在人工智能相机和视频产品方向进行了创新实验。完成了多种先进技术的积累,包括:
人工智能中的人脸检测,人脸跟踪,手势识别,智能美颜,人体分割等;
Android视频处理的硬编硬解;
OpenGL高级绘制渲染;
自研的3D渲染引擎;
图形图像处理;
iOS11 ARKit编程框架。
手机卫士资深技术人员@老鱼分6篇技术文章对Android视频处理的硬编硬解部分进行详细介绍。
在Android中播放视频很简单,可以使用MediaPlayer+SurfaceView或者VideoView设置一个视频文件路径就可以实现播放了。但是如果想对音视频再进行处理,比如视频播放过程中增加水印,或者对视频进行转码,就需要对视频进行编解码处理了。那么Android上视频编解码一般怎么做的呢?
其实正常视频编解码都是分为两种:软解码和硬解码。Android上软解码的代表:ffmpeg,非常成熟和好用的软解码第三方库。硬解码:MediaCodec,Android自带的硬解码库。本篇主要了解下MediaCodec。
从API 16(Android 4.1)开始,Android提供了MediaCodec类以便开发者更加灵活的处理音视频的编解码,MediaCodec类可以访问底层媒体编解码器框架(StageFright或openMAX),即编码器/解码器组件。这是Android low-level多媒体支持基础设施的一部分(通常与MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface, and AudioTrack.一起使用))。
上图工作流的核心是缓冲区(ByteBuffer),MediaCodec编解码都是通过缓冲区进行数据处理的。简单的说就是:使用者从编解码器中请求一个空的缓冲区(dequeueInputBuffer),然后把数据填充进去,再给回到编解码器(queueInputBuffer),这是输入过程。编解码器收到数据后进行处理。使用者从编解码器中请求输出流并把数据读取出来(dequeueOutputBuffer),读取完数据之后释放掉缓冲区(releaseOutputBuffer),以便下一帧数据的处理,这是输出过程。
编解码器可以处理三种类型的数据:压缩数据(即为经过H264. H265等编码的视频数据或AAC等编码的音频数据)、原始音频数据、原始视频数据。三种类型的数据均可以利用ByteBuffers进行处理,但是对于原始视频数据应提供一个Surface以提高编解码器的性能。Surface直接使用本地视频数据缓存(native video buffers),而没有映射或复制数据到ByteBuffers,因此,这种方式会更加高效。在使用Surface的时候,通常不能直接访问原始视频数据,但是可以使用ImageReader类来访问非安全的解码(原始)视频帧。这仍然比使用ByteBuffers更加高效,因为一些本地缓存(native buffer)可以被映射到 direct ByteBuffers。当使用ByteBuffer模式,你可以利用Image类和getInput/OutputImage(int)方法来访问到原始视频数据帧。
输入缓存(对于解码器)和输出缓存(对编码器)中包含由多媒体格式类型决定的压缩数据。对于视频类型是单个压缩的视频帧。对于音频数据通常是单个可访问单元(一个编码的音频片段,通常包含几毫秒的遵循特定格式类型的音频数据),但这种要求也不是十分严格,一个缓存内可能包含多个可访问的音频单元。在这两种情况下,缓存不会在任意的字节边界上开始或结束,而是在帧或可访问单元的边界上开始或结束。
原始的音频数据缓存包含完整的PCM(脉冲编码调制)音频数据帧,这是每一个通道按照通道顺序的一个样本。每一个样本是一个按照本机字节顺序的16位带符号整数(16-bit signed integer in native byte order)。
short[] getSamplesForChannel(MediaCodec codec, int bufferId, int channelIx) {
ByteBuffer outputBuffer = codec.getOutputBuffer(bufferId);
MediaFormat format = codec.getOutputFormat(bufferId);
ShortBuffer samples = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer();
int numChannels = formet.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
if (channelIx < 0 || channelIx >= numChannels) {
return null;
}
short[] res = new short[samples.remaining() / numChannels];
for (int i = 0; i < res.length; ++i) {
res[i] = samples.get(i * numChannels + channelIx);
}
return res;
}
在ByteBuffer模式下,视频缓存(video buffers)根据它们的颜色格式(color format)进行展现。你可以通过调用getCodecInfo().getCapabilitiesForType(…).colorFormats方法获得编解码器支持的颜色格式数组。视频编解码器可以支持三种类型的颜色格式:
native raw video format:这种格式通过COLOR_FormatSurface标记,并可以与输入或输出Surface一起使用。
flexible YUV buffers(例如:COLOR_FormatYUV420Flexible):利用一个输入或输出Surface,或在在ByteBuffer模式下,可以通过调用getInput/OutputImage(int)方法使用这些格式。
specific formats:通常只在ByteBuffer模式下被支持。有些颜色格式是特定供应商指定的。其他的一些被定义在 MediaCodecInfo.CodecCapabilities中。这些颜色格式同 flexible format相似,你仍然可以使用 getInput/OutputImage(int)方法(API 21)。
从Android 5.1(API 21)开始,所有的视频编解码器都支持灵活的YUV4:2:0缓存(flexible YUV420 buffers)。
在编解码器的生命周期内有三种理论状态:停止态-Stopped、执行态-Executing、释放态-Released,停止状态(Stopped)包括了三种子状态:未初始化(Uninitialized)、配置(Configured)、错误(Error)。执行状态(Executing)在概念上会经历三种子状态:刷新(Flushed)、运行(Running)、流结束(End-of-Stream)。
当你使用任意一种工厂方法(factory methods)创建了一个编解码器,此时编解码器处于未初始化状态(Uninitialized)。首先,你需要使用configure(…)方法对编解码器进行配置,这将使编解码器转为配置状态(Configured)。然后调用start()方法使其转入执行状态(Executing)。在这种状态下你可以通过上述的缓存队列操作处理数据。
执行状态(Executing)包含三个子状态: 刷新(Flushed)、运行( Running) 以及流结束(End-of-Stream)。在调用start()方法后编解码器立即进入刷新子状态(Flushed),此时编解码器会拥有所有的缓存。一旦第一个输入缓存(input buffer)被移出队列,编解码器就转入运行子状态(Running),编解码器的大部分生命周期会在此状态下度过。当你将一个带有end-of-stream 标记的输入缓存入队列时,编解码器将转入流结束子状态(End-of-Stream)。在这种状态下,编解码器不再接收新的输入缓存,但它仍然产生输出缓存(output buffers)直到end-of- stream标记到达输出端。你可以在执行状态(Executing)下的任何时候通过调用flush()方法使编解码器重新返回到刷新子状态(Flushed)。
通过调用stop()方法使编解码器返回到未初始化状态(Uninitialized),此时这个编解码器可以再次重新配置 。当你使用完编解码器后,你必须调用release()方法释放其资源。
在极少情况下编解码器会遇到错误并进入错误状态(Error)。这个错误可能是在队列操作时返回一个错误的值或者有时候产生了一个异常导致的。通过调用 -
reset()方法使编解码器再次可用。你可以在任何状态调用reset()方法使编解码器返回到未初始化状态(Uninitialized)。否则,调用 release()方法进入最终的Released状态。