1. 现象描述
工作中有一个项目,使用的是CameraView这个第三方相机库来调用相机。
CameraView封装了Camera1和Camera2,内部做了很多功能的封装,API使用起来相对比较简单。
在App中接入后,手机上能够正常录制视频,看上去没有任何问题。
但当我满怀欣喜地将App安装到车机上,却发现在车机上录制视频就会报下图的这个错误 : java.lang.illegalStateException: at MediaCodec.native_dequeueOutputBuffer

这是为什么呢 ? 下文就以来探寻下该问题的原因。
2. MediaCodec 相关 API 说明
首先来介绍下相关的MediaCodec API
2.1 获取所有支持的编码器
MediaCodecInfo[] array = new MediaCodecList(MediaCodecList.REGULAR_CODECS).getCodecInfos();
车机上获取到的编码器列表,可以看到,支持video/avc的硬编码为OMX.qcom.video.encoder.avc,软编码为OMX.google.h264.encoder
codecName:OMX.google.aac.encoder supportedTypes:audio/mp4a-latm
codecName:OMX.google.amrnb.encoder supportedTypes:audio/3gpp
codecName:OMX.google.amrwb.encoder supportedTypes:audio/amr-wb
codecName:OMX.google.flac.encoder supportedTypes:audio/flac
codecName:OMX.qcom.video.encoder.avc supportedTypes:video/avc
codecName:OMX.google.h264.encoder supportedTypes:video/avc
codecName:OMX.qcom.video.encoder.h263sw supportedTypes:video/3gpp
codecName:OMX.google.h263.encoder supportedTypes:video/3gpp
codecName:OMX.qcom.video.encoder.hevc supportedTypes:video/hevc
codecName:OMX.qcom.video.encoder.mpeg4sw supportedTypes:video/mp4v-es
codecName:OMX.google.mpeg4.encoder supportedTypes:video/mp4v-es
codecName:OMX.qcom.video.encoder.vp8 supportedTypes:video/x-vnd.on2.vp8
codecName:OMX.google.vp8.encoder supportedTypes:video/x-vnd.on2.vp8
codecName:OMX.google.vp9.encoder supportedTypes:video/x-vnd.on2.vp9
MediaCodecList.REGULAR_CODECS表示只获取标准、稳定的编解码器。使用这个枚举值可以确保所获取的编解码器是按照 Android 平台的标准和稳定程度进行排序的,这在一定程度上可以避免音视频开发中的坑。MediaCodecList.ALL_CODECS表示获取所有可用的编解码器,包括可能不标准、不稳定的编解码器。使用这个枚举值可以获取到更多的编解码器信息,但可能会存在一些不稳定的因素。
2.2 通过指定编码器创建MediaCodec
通过这种方式,会去创建一个指定编码器的MediaCodec
MediaCodec codec = MediaCodec.createEncoderByType("OMX.google.h264.encoder")
2.3 通过指定Mine创建MediaCodec
通过这种方式,会创建一个该mimeType类别下的该系统最推荐的一个编码器。
MediaCodec codec = MediaCodec.createEncoderByType("video/avc");
3. 选择哪个编码器呢 ?
到这里,大家可能会疑惑,这么多编码器到底要选用哪个呢 ?
首先来看miniType,视频录制我们选择video/avc就好,也就是h264。
车机中video/avc的编码器有OMX.qcom.video.encoder.avc和OMX.google.h264.encoder。
到这里,我们再来了解下硬编码和软编码。
3.1 硬编码和软编码
- 软解码,就是利用CPU的计算能力来解码,如果CPU不是很强的情况,解码速度会比较慢,手机也会出现发热现象,但是由于使用统一的算法,兼容性会很好。
- 硬解码,是利用手机上专门的解码芯片来加速解码。通常硬解码速度会快很多,但是由于硬解码由各个厂家实现,质量参差不齐,容易出现兼容性问题。
| 优点 | 缺点 | |
|---|---|---|
| 软编码 | 1.兼容性强,对系统版本要求低,出错情况少 2.解码方面,软解码的色彩一般比硬解码柔和 3.编码的可操作空间比较大,自由度高 | 1.CPU消耗较大 2.机器容易发热 3.功耗较高 |
| 硬解码 | 功耗低,执行效率高 | 1.不同型号的芯片对编解码的实现不同,并不能保证解码效果与其他机型一样或不出错 2.可控性差,依赖底层编解码实现 3.不易升级和维护 |
3.2 如何判断硬编码和软编码
一般情况下,omx.google.开头和c2.android.开头的编码器,都是软编码。
除此之外,所有不是omx.和c2.开头的编码器,也可将其归类为软编码。
其他的都可以认为是隐编码,具体判断代码如下所示。
boolean isHardwareEncoder(@NonNull String encoder) {
encoder = encoder.toLowerCase();
boolean isSoftwareEncoder = encoder.startsWith("omx.google.")
|| encoder.startsWith("c2.android.")
|| (!encoder.startsWith("omx.") && !encoder.startsWith("c2."));
return !isSoftwareEncoder;
}
3.3 解决录制崩溃
到这里,我们就知道可以使用软解码器,也就是OMX.google.h264.encoder来降低兼容性问题。
的确,我们尝试使用OMX.google.h264.encoder这个软编码,在车机上就可以正常录制视频了。
但是,这到底是是为什么呢 ?
4. 第三方库CameraView源码解析
接下来我们来探寻下CameraView的源码,以下关于CameraView的源码解析都是基于CameraView 2.7.2版本
4.1 初始化MediaCodec
先来看一下MediaCodec是什么时候被初始化的。
可以发现初始化MediaCodec在VideoMediaEncoder.java这个类的onPrepare方法
如果mConfig.encoder不为NULL 就通过createByCodecName创建MediaCodec,这种方式会精确地创建某个编码器。
否则就通过createEncoderByType创建MediaCodec,系统会根据传入的mineType创建一个最推荐的MediaCodec。
if (mConfig.encoder != null) {
mMediaCodec = MediaCodec.createByCodecName(mConfig.encoder);
} else {
mMediaCodec = MediaCodec.createEncoderByType(mConfig.mimeType); //mineType默认为video/avc
}
接着来看一下mConfig.encoder是在哪里被赋值的
追踪到SnapshotVideoRecorder#onRendererFrame()里有这么一行
videoConfig.encoder = deviceEncoders.getVideoEncoder();
而getVideoEncoder就是调用的mVideoEncoder.getName()
public String getVideoEncoder() {
if (mVideoEncoder != null) {
return mVideoEncoder.getName();
} else {
return null;
}
}
4.2 mVideoEncoder什么时候被赋值
那这个mVideoEncoder是从哪来的呢 ?
可以发现在DeviceEncoders构造方法里mVideoEncoder被赋值
List<MediaCodecInfo> encoders = getDeviceEncoders();
mVideoEncoder = findDeviceEncoder(encoders, videoType, mode, videoOffset);
4.3 findDeviceEncoder参数解析
看一下参数一 encoders,通过getDeviceEncoders用来获取所有支持的编码器
//调用MediaCodecList.getCodecInfos(),获取所有支持的编码器
List<MediaCodecInfo> getDeviceEncoders() {
ArrayList<MediaCodecInfo> results = new ArrayList<>();
MediaCodecInfo[] array = new MediaCodecList(MediaCodecList.REGULAR_CODECS).getCodecInfos();
for (MediaCodecInfo info : array) {
if (info.isEncoder()) results.add(info);
}
return results;
}
参数二videoType默认情况下就是video/avc
参数三有两个可选项
-
MODE_RESPECT_ORDER: 看字面意思是,尊重系统提供的顺序 -
MODE_PREFER_HARDWARE: 看字面意思是,更倾向于硬编码
参数四videoOffset,默认情况下是0,只有外层的try-catch出现异常,videoOffset++去尝试下一个解码器。
4.4 findDeviceEncoder内部实现
接着来看findDeviceEncoder用来选定最终的编码器
- 首先会去匹配
nimeType,相同的才会被添加到待选列表 - 如果
mode是mode == MODE_PREFER_HARDWARE,待选列表会按照硬编码优先的规则进行排序 - 最后,从待选列表里根据
videoOffset这个索引取出对应的编码器
//选定最终的编码器
MediaCodecInfo findDeviceEncoder(@NonNull List<MediaCodecInfo> encoders,
@NonNull String mimeType,
int mode,
int offset) {
ArrayList<MediaCodecInfo> results = new ArrayList<>();
for (MediaCodecInfo encoder : encoders) {
String[] types = encoder.getSupportedTypes();
for (String type : types) {
if (type.equalsIgnoreCase(mimeType)) {
results.add(encoder);
break;
}
}
}
LOG.i("findDeviceEncoder -", "type:", mimeType, "encoders:", results.size());
if (mode == MODE_PREFER_HARDWARE) {
Collections.sort(results, new Comparator<MediaCodecInfo>() {
@Override
public int compare(MediaCodecInfo o1, MediaCodecInfo o2) {
boolean hw1 = isHardwareEncoder(o1.getName());
boolean hw2 = isHardwareEncoder(o2.getName());
return Boolean.compare(hw2, hw1);
}
});
}
if (results.size() < offset + 1) {
// This should not be a VideoException or AudioException - we want the process
// to crash here.
throw new RuntimeException("No encoders for type:" + mimeType);
}
return results.get(offset);
}
到这里,解析器的获取顺序就讲清楚了,那么,VideoMediaEncoder什么时候被初始化呢 ?
4.5 VideoMediaEncoder什么时候被初始化
一共有两个地方被调用
SnapshotVideoRecorder构造函数 -> 具体调用方法为 :takeVideo -> onTakeVideo -> 创建SnapshotVideoRecorder并调用其start()FullVideoRecorder构造函数 -> 具体调用方法为 :takeVideoSnapshot -> onTakeVideoSnapshot -> 创建FullVideoRecorder并调用其start()
4.6 CameraView的Bug
在SnapshotVideoRecorder中创建了两次DeviceEncoders,最终生效的是DeviceEncoders.MODE_PREFER_HARDWARE(优先选择硬编码)的DeviceEncoders,导致调用takeVideoSnapshot(带滤镜录制视频)优先会使用硬编码。
而在FullVideoRecorder中是只创建了一次DeviceEncoders的,最终生效的就是DeviceEncoders.MODE_RESPECT_ORDER(按照系统提供的顺序),通过takeVideo进行调用(不带滤镜录制视频)。

同时,由于OMX.qcom.video.encoder.avc这个硬编码有问题,导致选取合适的视频分辨率这个功能也出现了问题,选择的分辨率是width=192px,height=108px,而实际的应该是1920*1080。


5. 小结
到这里,我们就明白,为什么在该车机上录制视频就直接报错了。
因为车机系统本身不支持OMX.qcom.video.encoder.avc这个硬编码,但是又在支持列表里返回了OMX.qcom.video.encoder.avc,且将其放置在video/avc的最优先序列,这是最重要最根本的原因。
其次CameraView这个库在录制带滤镜视频(takeVideoSnapshot)的时候,由于有BUG创建了两次DeviceEncoders,导致最终选取的是OMX.qcom.video.encoder.avc这个硬编码,从而导致App直接崩溃了。
而录制不带滤镜视频(takeVideo)的时候,没有创建两次DeviceEncoders的BUG,但是由于系统提供的video/avc的最优先序列是OMX.qcom.video.encoder.avc,所以App依旧崩溃。



















