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

  • Post author:
  • Post category:其他




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

依旧崩溃。



版权声明:本文为EthanCo原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。