做 Android 音视频离不开 MediaCodec。实际上不局限于平台看问题,不管是 Linux、Mac OS、iOS、Android 还是 Window 这些系统,只要涉及到音视频编解码,无非要么软编解码、要么硬编解码,软编解码通常会使用瑞士军刀“FFmpeg”,主要靠 cpu 的算力去实现。硬编解码就要调用系统开放的 API 去使用硬件编解码。从效率上看,调用硬件编解码是最快的,通常会使用专用的硬件去处理编解码。尤其在嵌入式设备上通常 cpu 性能有限,想要达到满足要求的编解码需求,不用硬件编解码也是不现实的,从方案角度来考虑使用硬编解码是最合理的。当然如果我们的目标是形形色色的设备,比如开发一款视频播放器,那么首先要尝试硬解,在无法硬解的提前下应该使用软解。但如果我们开发的是特定硬件设备上的软件,那么硬编解码是首选,这会少走很多弯路,如果方案一开始选择了软编解码,很可能由于需求的变更,使得软件可扩展性变差,比如从 720p 解码变更到 1080p 解码,1080p 消耗 cpu 算力加大,这对软解码就是一种挑战,很可能软解码不能满足要求了,看吧,还得上硬解码。但硬编解码是受到硬件具体限制的,硬件不支持程序员无能为力。不过通常来看,使用硬编解码会释放 cpu 算力,对整个应用场景体验是正向的。
在 Android 平台上硬编解码 H.264 码流,需要调用 MediaCodec 这套 API。但由于不同的硬件搭载 Android OS,这就导致编解码能力千差万别,具体编解码支持程度如何,取决于硬件实现。就拿 rk3399 这颗瑞芯微的芯片来看,编解码能力如下:
Video Decoder
- MMU embedded
- Real-time video decoder of MPEG-1, MPEG-2, MPEG-4, H.263, H.264, H.265, VC-1, VP9, VP8, MVC
- H.264/AVC,Base/Main/High/High10 profile @ level 5.1; up to 4Kx2K @ 30fps
- H.265/HEVC, Main/Main10 profile @ level 5.1 High-tier; up to 4Kx2K @ 60fps
- VP9, profile 0, up to 4Kx2K @ 60fps
- MPEG-1, ISO/IEC 11172-2, up to 1080P @ 60fps
- MPEG-2, ISO/IEC 13818-2, SP@ML, MP@HL, up to 1080P @ 60fps
- MPEG-4, ISO/IEC 14496-2, SP@L0-3, ASP@L0-5, up to 1080P @ 60fps
- VC-1, SP@ML, MP@HL, AP@L0-3, up to 1080P @ 60fps
- MVC is supported based on H.264 or H.265, up to 1080P @ 60fps
- Supports frame timeout interrupt, frame finish interrupt and bit stream error interrupt
- Error detection and concealment support for all video formats
- Output data format YUV420 semi-planar, YUV400(monochrome), YUV422 is supported by H.264
- For MPEG-4, GMC (global motion compensation) not supported
- For VC-1, up-scaling and range mapping are supported in image post-processor
- For MPEG-4 SP/H.263, using a modified H.264 in-loop filter to implement deblocking filter in post-processor unit
Video Encoder
- Support video encoder for H.264 UP to HP@level4.1, MVC and VP8
- MMU Embedded
- Only support I and P slices, not B slices
- Support error resilience based on constrained intra prediction and slices
- Input data format:
YCbCr 4:2:0 planar
YCbCr 4:2:0 semi-planar
YCbYCr 4:2:2
CbYCrY 4:2:2 interleaved
RGB444 and BGR444
RGB555 and BGR555
RGB565 and BGR565
RGB888 and BRG888
RGB101010 and BRG101010- Image size is from 96x96 to 1920x1080(Full HD)
- Maximum frame rate is up to 1920x1080@30FPS
就拿编解码 H.264 来看,解码最大支持 4Kx2K @ 30fps,编码最大支持 1920x1080@30FPS。
MediaCodec 这套 API 使用重点在于理解下面的两张图,一张为状态转移图,另一张为送入 buffer 和输出 buffer 内部如何运作。当然这两张图都是官方出品,这里只是搬过来温习一下。
一、硬解码
先上一段典型的硬解码代码。上代码前先回顾几个问题:
- 硬解码图像花屏了;
- 图像有点卡,不流畅;
- 停止硬解码后再次启用视频无输出;
- 硬解码断流了。
以上这些问题都是我实际项目中遇到的,以 NALU 单元仅为 I 帧 和 P 帧,不包含 B 帧为基础。
- 硬解码图像花屏了
首先要保证送到硬解码内部的 NALU 单元保持连贯,如果没有送关键帧,只是再送一些 P 帧,连续丢失了关键帧就会造成图像花屏的现象,所以解决这个问题思路有两个,先去分析前端释放 NALU 单元是否存在丢帧的现象,如果前端没有丢帧,就要考虑送到 MediaCodec 内部前是不是有丢失现象。NALU 单元保持连贯送入 MediaCodec 内部就不会出现花屏了。
- 图像有点卡,不流畅
图像卡顿不流畅,必然是硬解码处理不过来造成的,这就要考虑丢帧方案,关键帧不能丢,关键帧后面的 P 帧可以丢掉。丢帧方案一般只在偶尔极端情况下使用,如果一直频繁丢帧说明硬解码消费帧能力不足,硬件受限造成的图像卡顿,只能升级硬件解决。
- 停止硬解码后再次启用,视频无输出
一定要注意送到 MediaCodec 内部的 Surface 是不是被其他组件占用了,比如同时解码多路视频,停止一个 MediaCodec 对象之后,Surface 被另外一个占用,这个当然就没法再次启用,而且如果重新 configure 会报错。
- 硬解码断流了
断流问题可能是 NALU 源头本身没给数据过来了,还有可能是解码期间发出了错误停掉了。数据源头出了问题就要解决源头问题,如果是解码期间抛出异常,就要捕获到异常再次启用解码。
另外,MediaCodec configure(…)、start() 和 stop() 这些函数都有可能抛出异常,所以一定要注意捕获异常、处理异常,防止把程序直接挂掉。比如 MediaCodec 解码器低概率 stop 抛出异常 IllegalStateException。
下面是代码片段总体流程要点:
- 准备一个 LinkedBlockingQueue 队列来暂存 NALU 单元;
- 所有 MediaCodec API 使用都要围绕 try-catch 进行异常捕获;
- 极端情况丢帧应对;
- 解码期间抛出异常重启解码流程。
注意:下面这段代码没有考虑码流结束后刷出剩余的帧,如果剩余的帧是你的需求中需要的,则需要考虑。
public class Packet {
private long pts;
private byte[] data;
private int size;
private boolean isKeyFrame;
public Packet(long pts, byte[] data, int size, boolean isKeyFrame) {
this.pts = pts;
this.data = data;
this.size = size;
this.isKeyFrame = isKeyFrame;
}
public long getPts() {
return pts;
}
public void setPts(long pts) {
this.pts = pts;
}
public byte[] getData() {
return data;
}
public void setData(byte[] data) {
this.data = data;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public boolean isKeyFrame() {
return isKeyFrame;
}
public void setKeyFrame(boolean keyFrame) {
isKeyFrame = keyFrame;
}
}
public class H264Decoder {
private static final String TAG = "H264Decoder";
private final static String MIME_TYPE = "video/avc"; // H.264 Advanced Video
private MediaCodec mDecoderMediaCodec;
private SendDecodeThread mSendDecodeThread;
private final LinkedBlockingQueue<Packet> mNALUQueue = new LinkedBlockingQueue<>(10);
private int mFps;
private Surface mSurface;
private DecodeCallback mDecodeCallback;
private int mWidth;
private int mHeight;
private int mInitMediaCodecTryTimes = 0;
public H264Decoder(int width, int height, int fps, Surface surface) {
mWidth = width;
mHeight = height;
mFps = fps;
mSurface = surface;
initMediaCodec(width, height, surface);
}
private void initMediaCodec(int width, int height, Surface surface) {
try {
mDecoderMediaCodec = MediaCodec.createDecoderByType(MIME_TYPE);
//创建配置
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
mediaFormat.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline);
mediaFormat.setInteger("level", MediaCodecInfo.CodecProfileLevel.AVCLevel4); // Level 4
mDecoderMediaCodec.configure(mediaFormat, surface, null, 0);
} catch (Exception e) {
e.printStackTrace();
//创建解码失败
Log.e(TAG, "init MediaCodec fail.");
// 重新尝试六次
if (mInitMediaCodecTryTimes < 6) {
mInitMediaCodecTryTimes++;
try {
Thread.sleep(20);
} catch (InterruptedException ie) {
ie.printStackTrace();
}
initMediaCodec(mWidth, mHeight, mSurface);
}
}
}
public void setDecodeCallback(DecodeCallback decodeCallback) {
mDecodeCallback = decodeCallback;
}
/**
* 开始解码
*/
public void start() {
if (mSendDecodeThread != null && mSendDecodeThread.isRunning()) {
Log.e(TAG, "SendDecodeThread is already running");
return;
}
try {
mDecoderMediaCodec.start();
mSendDecodeThread = new SendDecodeThread();
mSendDecodeThread.setRunning(true);
mSendDecodeThread.start();
Log.d(TAG, "START");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 结束解码
*/
public void stop() {
if (mSendDecodeThread != null && mSendDecodeThread.isRunning()) {
mSendDecodeThread.setRunning(false);
mSendDecodeThread.interrupt();
try {
mSendDecodeThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
mSendDecodeThread = null;
}
mNALUQueue.clear();
if (mDecoderMediaCodec != null) {
try {
mDecoderMediaCodec.stop();
mDecoderMediaCodec.release();
mDecoderMediaCodec = null;
Log.d(TAG, "STOP");
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 送入数据
*
* @param packet
*/
public void putData(Packet packet) {
try {
mNALUQueue.offer(packet);
} catch (Exception e) {
e.printStackTrace();
}
}
private class SendDecodeThread extends Thread {
private volatile boolean isRunning = true;
private boolean isNeedDiscardFrame = false;
@Override
public void run() {
if (null != mDecodeCallback) {
mDecodeCallback.init();
}
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
while (isRunning) {
Packet packet;
//Log.d(TAG, "NALUQueue size=" + mNALUQueue.size());
if (mNALUQueue.size() > 6) {
packet = mNALUQueue.poll();
while (null != packet && !packet.isKeyFrame()) {
packet = mNALUQueue.poll();
}
// 队列中的 packet 被移除完,还未找到关键帧,剩余添加到队列的 packet 要丢掉
isNeedDiscardFrame = (null == packet);
} else {
if (isNeedDiscardFrame) {
packet = mNALUQueue.poll();
while (null != packet && !packet.isKeyFrame()) {
packet = mNALUQueue.poll();
}
isNeedDiscardFrame = (null == packet);
} else {
packet = mNALUQueue.poll();
isNeedDiscardFrame = false;
}
}
if (null == packet) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
try {
//long start = System.currentTimeMillis();
int inIndex = mDecoderMediaCodec.dequeueInputBuffer(-1);// microseconds
//Log.d(TAG, "inIndex:" + inIndex);
if (inIndex >= 0) {
ByteBuffer inputBuffer = mDecoderMediaCodec.getInputBuffer(inIndex);
inputBuffer.put(packet.getData(), 0, packet.getSize());
//填充数据后通知 MediaCodec 查询 inIndex 索引的这个 buffer
mDecoderMediaCodec.queueInputBuffer(inIndex, 0, packet.getSize(), System.currentTimeMillis() * 1000, 0);
}
int outIndex;
if ((outIndex = mDecoderMediaCodec.dequeueOutputBuffer(info, 0)) >= 0) {
//Log.d(TAG, "outIndex:" + outIndex + " flag=" + info.flags);
if (null != mDecodeCallback) {
// 实际输出为 NV12 格式
//byte[] data = new byte[info.size];
//outBuffer.get(data);
ByteBuffer outBuffer = mDecoderMediaCodec.getOutputBuffer(outIndex);
if (null == mSurface) {
mDecoderMediaCodec.releaseOutputBuffer(outIndex, false);
mDecodeCallback.onDataArrived(info.presentationTimeUs, outBuffer, outBuffer.capacity());
} else {
mDecoderMediaCodec.releaseOutputBuffer(outIndex, true);
mDecodeCallback.onDataArrived(-1, null, -1);
}
}
//long end = System.currentTimeMillis();
//Log.d(TAG, "SendDecodeThread: useTime=" + (end - start));
/*long sleepTime = 1000 / mFps - (end - start);
Thread.sleep(sleepTime > 0 && sleepTime < 1000 / mFps ? sleepTime : 0);*/
}
} catch (Exception e) {
Log.d(TAG, "SendDecodeThread: An error occurred.");
// 发生错误重启再试
H264Decoder.this.stop();
initMediaCodec(mWidth, mHeight, mSurface);
H264Decoder.this.start();
}
}
if (null != mDecodeCallback) {
mDecodeCallback.deinit();
}
}
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean running) {
isRunning = running;
}
}
public interface DecodeCallback {
public void init();
public void deinit();
public void onDataArrived(long pts, ByteBuffer data, int dataSize);
}
}
二、硬编码
在 rk3399 android 10 平台上,无法直接将 nv12 数据编码为 H.264,如果送入 NV12 进行编码,会出现编码后的数据全是关键帧却没有 P 帧输出,而且大量的 YUV 帧送入编码会吃性能,YUV 数据对内存占用较大。所以一般场景下我们会使用 Surface 将图像数据送入编码器进行编码,只需要在 Surface 上进行绘制即可。当然这个 Surface 是由编码器创建出来的,而绘制通常会搭配 OpenGL ES API 去处理。
注意:下面这段代码没有考虑码流结束后刷出剩余的帧,这是因为此处不考虑对需求影响不大,所以没写相关代码,如果需要将所有帧刷出,则应该考虑。
public class H264Encoder {
private static final String TAG = "H264Encoder";
private final static String MIME_TYPE = "video/avc"; // H.264 Advanced Video
private MediaCodec mMediaCodec;
private Surface mInputSurface;
private int mFps;
private EncodeCallback mEncodeCallback;
private byte[] mBuffer;
private SendEncodeThread mSendEncodeThread;
private int mWidth;
private int mHeight;
private int mInitMediaCodecTryTimes = 0;
public H264Encoder(int width, int height, int fps) {
mWidth = width;
mHeight = height;
mFps = fps;
mBuffer = new byte[width * height * 3 / 2];
initMediaCodec(width, height, fps);
}
private void initMediaCodec(int width, int height, int fps) {
try {
mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE);
//创建配置
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, width, height);
//设置解码预期的帧速率
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, (int) (width * height * fps * 0.3));//Biterate = Width * Height * FrameRate * Factor
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
mediaFormat.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline);
mediaFormat.setInteger("level", MediaCodecInfo.CodecProfileLevel.AVCLevel42); // Level 4
//配置绑定 mediaFormat
Log.d(TAG, "width=" + width + " height=" + height + " fps=" + fps);
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mInputSurface = mMediaCodec.createInputSurface();
} catch (Exception e) {
e.printStackTrace();
//创建编码失败
Log.e(TAG, "init MediaCodec fail.");
// 重新尝试六次
if (mInitMediaCodecTryTimes < 6) {
mInitMediaCodecTryTimes++;
try {
Thread.sleep(20);
} catch (InterruptedException ie) {
ie.printStackTrace();
}
initMediaCodec(mWidth, mHeight, mFps);
}
}
}
/**
* 获取 MediaCodec Input Surface
*
* @return
*/
public Surface getInputSurface() {
return mInputSurface;
}
/**
* 开始编码
*/
public void start() {
if (mSendEncodeThread != null && mSendEncodeThread.isRunning()) {
Log.e(TAG, "SendDecodeThread is already running");
return;
}
try {
mMediaCodec.start();
mSendEncodeThread = new SendEncodeThread();
mSendEncodeThread.setRunning(true);
mSendEncodeThread.start();
Log.d(TAG, "START");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 结束编码
*/
public void stop() {
if (mSendEncodeThread != null && mSendEncodeThread.isRunning()) {
mSendEncodeThread.setRunning(false);
mSendEncodeThread.interrupt();
try {
mSendEncodeThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (mMediaCodec != null) {
try {
mMediaCodec.stop();
mMediaCodec.release();
mMediaCodec = null;
Log.d(TAG, "STOP");
} catch (Exception e) {
e.printStackTrace();
}
}
}
private class SendEncodeThread extends Thread {
private volatile boolean isRunning = true;
@Override
public void run() {
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
while (isRunning) {
try {
long start = System.currentTimeMillis();
int outIndex = mMediaCodec.dequeueOutputBuffer(info, 0);
//Log.d(TAG, "outIndex:" + outIndex + " flag=" + info.flags);
if (outIndex >= 0) {
ByteBuffer outBuffer = mMediaCodec.getOutputBuffer(outIndex);
outBuffer.get(mBuffer, 0, info.size);
mMediaCodec.releaseOutputBuffer(outIndex, false);
if (info.flags == MediaCodec.BUFFER_FLAG_CODEC_CONFIG) {
//Log.d(TAG, "SendEncodeThread: BUFFER_FLAG_CODEC_CONFIG");
} else if (info.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME) {
//Log.d(TAG, "SendEncodeThread: BUFFER_FLAG_KEY_FRAME");
} else {
//Log.d(TAG, "SendEncodeThread: P Frame or other.");
}
if (null != mEncodeCallback) {
mEncodeCallback.onDataArrived(info.flags, info.presentationTimeUs, mBuffer, info.size);
}
}
long end = System.currentTimeMillis();
long sleepTime = 1000 / mFps - (end - start);
Thread.sleep(sleepTime > 0 && sleepTime < 1000 / mFps ? sleepTime : 0);
//Log.d(TAG, "SendEncodeThread: sleepTime=" + sleepTime);
} catch (InterruptedException e) {
Log.d(TAG, "SendEncodeThread: break sleep.");
} catch (Exception e) {
Log.d(TAG, "SendEncodeThread: An error occurred.");
// 发生错误重启再试
H264Encoder.this.stop();
initMediaCodec(mWidth, mHeight, mFps);
H264Encoder.this.start();
}
}
}
public boolean isRunning() {
return isRunning;
}
public void setRunning(boolean running) {
isRunning = running;
}
}
public void setEncodeCallback(EncodeCallback encodeCallback) {
this.mEncodeCallback = encodeCallback;
}
public interface EncodeCallback {
public void onDataArrived(int frameType, long pts, byte[] data, int dataSize);
}
}
运行这段编码器代码后,的确可以正常编码 H.264,但会出现播放视频每隔一定间隔(大约 1 秒)画面闪烁一下(类似突然某一帧码率降低造成),经过分析生成的 H.264 数据,发现存在同样的问题,如此就能定位到编码器输出的数据本身存在问题造成的,而非打包成 MP4 等格式的封装代码引入的。需要自行编译瑞芯微官方的开源最新 mpp 库(生成 libmpp.so 和 libvpu.so)进行解决。
正常编码图像:
出现闪烁帧:
解决方案需要同时编译 mpp 库和重新编译 Android 10 OS 代码。
简单的验证方法为 adb push libmpp.so 和 libvpu.so 这两个库到设备 vendor/lib 和 vendor/lib64 目录下。
系统源码内这两个 so 需要替换下面的位置:
vendor/rockchip/common/vpu/lib/libmpp/arm64/mpp_dev/libmpp.so
vendor/rockchip/common/vpu/lib/libmpp/arm64/mpp_svp/libmpp.so
vendor/rockchip/common/vpu/lib/libmpp/arm/mpp_dev/libmpp.so
vendor/rockchip/common/vpu/lib/libmpp/arm/mpp_svp/libmpp.so
vendor/rockchip/common/vpu/lib/libvpu/arm64/mpp_dev/libvpu.so
vendor/rockchip/common/vpu/lib/libvpu/arm64/mpp_svp/libvpu.so
vendor/rockchip/common/vpu/lib/libvpu/arm/mpp_dev/libvpu.so
vendor/rockchip/common/vpu/lib/libvpu/arm/mpp_svp/libvpu.so
mpp 库编译方法
mpp 库下载地址:https://github.com/rockchip-linux/mpp,编译时需要安装 cmake,版本要求 2.8.12 以上,我编译时采用的版本为 cmake version 3.20.0。
- 手动修改 build/android/arm 目录和 build/android/aarch64 目录下的 make-Android.bash 脚本的 ANDROID_NDK 变量路径,比如我修改为 /home/snake/Android/android-ndk-r15b。
- 进入 build/android/arm 运行 make-Android.bash 脚本生成编译用的 Makefile,运行 make -j8 进行编译,然后取编译好的 32 位版本:build/android/arm/mpp/legacy/libvpu.so 和 build/android/arm/mpp/libmpp.so。
- 进入 build/android/aarch64 运行 make-Android.bash 脚本生成编译用的 Makefile,运行 make -j8 进行编译,然后取编译好的 64 位版本:build/android/aarch64/mpp/legacy/libvpu.so 和 build/android/aarch64/mpp/libmpp.so。
关于编码最后再说一点,虽然 rk3399 官方称编码最大支持 1920x1080@30FPS,实际上这应该是编码一路 1080p 的最大帧率,实践时同时编码两路 1080p 帧率下降到 15 帧左右,也就是说随着编码路数增加,貌似帧率成比例降低。