1. 现象描述
工作中有一个项目,需要调用相机录制视频并保存到本地。
在
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
封装了
Camera1
和
Camera2
,内部做了很多功能的封装,
API
使用起来相对比较简单。
接下来我们来探寻下
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
依旧崩溃。