Android车机录制视频报错,竟是编码器的锅 ?

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.createByCodecName("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.avcOMX.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封装了Camera1Camera2,内部做了很多功能的封装,API使用起来相对比较简单。
接下来我们来探寻下CameraView的源码,以下关于CameraView的源码解析都是基于CameraView 2.7.2版本

4.1 初始化MediaCodec

先来看一下MediaCodec是什么时候被初始化的。

可以发现初始化MediaCodecVideoMediaEncoder.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用来选定最终的编码器

  • 首先会去匹配mineType,相同的才会被添加到待选列表
  • 如果modemode == 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)的时候,没有创建两次DeviceEncodersBUG,但是由于系统提供的video/avc的最优先序列是OMX.qcom.video.encoder.avc,所以App依旧崩溃。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

氦客

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值