之前参考 Google 官方 native codec demo 实现了一个 Android 机上硬解码的功能,期望能改善手机的 CPU 使用率,提高应用性能。但是后来同事报出说在 华为 Mate10 手机上解码失败,由于当时我手边没有 Mate10,只有 P30,而在 P30 、小米8 和 小米 MIX 2S 上测试均未复现。因此一直没处理,最近再次提上日程,本想给华为提 case,写了个硬解码 Demo,竟然解决了这一概率性失败的问题。

代码实现

代码实现完全参考 Google 官方 native codec demo,具体工程可从我的 GitHub 上下载 NdkMediaCodecDemo。

原始问题

原始问题在于使用 NDK MediaCodec 硬解码 h264/mp4 视频时,概率性地从开始就解码失败,具体表现为:在 P30、小米8、小米 Mix2S 手机上几乎不复现该问题,一直都能解码成功。而在 mate10 上几乎没有能成功解码的时候,一直返回 EAGAIN,从 log 里看,和正常解码时的 log 相比,有以下异常内容:

12-15 19:00:11.554 21696 21956 I ACodec  : [OMX.hisi.video.decoder.avc] ExecutingState flushing now (codec owns 1/5 input, 8/8 output).
12-15 19:00:12.269  1000 21960 E VIDEO   : VIDEO-[InquireSliceProperty]:[11002]sliceheader first part dec err

此外,正常解码的 log 中还多出了一些 log:

12-15 19:00:40.720  1000 21999 W HiDecoder: VIDEO-[EraseOutputBufferRecord]:[696]invalid buffer, shareFd:30
12-15 19:00:40.720  1000 21999 W HiDecoder: VIDEO-[EraseOutputBufferRecord]:[696]invalid buffer, shareFd:45
12-15 19:00:40.747  1000 21995 I VIDEO   : VIDEO-[H264_ReportColorAspectsInfo]:[454]syntax value : range 0 primaries 0 matrix 2 transfer 0
12-15 19:00:40.747  1000 21999 I ComponentImp: VIDEO-[EventProcess]:[333]event type (0)
12-15 19:00:40.747  1000 21999 I ColorParams: VIDEO-[PrintColorAspects]:[104]Range 2 Primaries 0 MatrixCoeffs 0 Transfer 0
12-15 19:00:40.748  1000  1188 I OMXParms: VIDEO-[GetParameter]:[257]index(0x2000001) ParamPortDefinition
12-15 19:00:40.749  1000  1188 I OMXParms: VIDEO-[GetParameter]:[257]index(0x7f000006) ??
12-15 19:00:40.749  1000  1188 I OMXParms: VIDEO-[GetConfig]:[310]index(0x700000f) ConfigCommonOutputCrop
12-15 19:00:40.749  1000  1188 D OMXParms: VIDEO-[GetImageCropInfo]:[924]GetImageCropInfo: left 0, top 0, width 720, height 1080
12-15 19:00:40.749  1000  1188 I OMXParms: VIDEO-[SetConfig]:[336]index(0x7f00000b) ??
12-15 19:00:40.749  1000  1188 I OMXParms: VIDEO-[GetConfig]:[310]index(0x7f00000b) ??
12-15 19:00:40.749  1000  1188 I ColorParams: VIDEO-[PrintColorAspects]:[104]Range 2 Primaries 0 MatrixCoeffs 0 Transfer 0
12-15 19:00:40.749 21696 21992 I ACodec  : [OMX.hisi.video.decoder.avc] got color aspects (R:2(Limited), P:0(Unspecified), M:0(Unspecified), T:0(Unspecified)) err=0(NO_ERROR)
12-15 19:00:40.766 21696 21696 D DECODE_DEMO: Try again later. tryagaincnt = 0.
12-15 19:00:40.767 21696 21696 D DECODE_DEMO: read time = 1, samplesize = 37888
12-15 19:00:40.778  1000 21999 I HiDecoder: VIDEO-[UpdateDecodeParam]:[1546]need report image info change:0, need realloc pmv buf:1
12-15 19:00:40.778  1000 21999 I HiDecoder: VIDEO-[UpdateDecodeParam]:[1551]update decode params: bitDepth:8=>8, dispWidth:720=>720, dispHeight:1080=>1080
12-15 19:00:40.779  1000 21999 I HiDecoder: VIDEO-[UpdateDecodeParam]:[1554]update decode params: decWidth:720=>720, decHeight:1080=>1088
12-15 19:00:40.779  1000 21999 I HiDecoder: VIDEO-[DealWithImageInfoChange]:[1698]pmv size:195840, mFrameBufferList size():8
12-15 19:00:40.779  1000 21999 I HiDecoder: VIDEO-[IsNeedDisableHFBC]:[1997]output buffer is not native buffer, not support HFBC
12-15 19:00:40.779  1000 21999 I HiDecoder: VIDEO-[SetHfbcMode]:[2090]disable hfbc
12-15 19:00:40.779  1000 21999 I HiDecoder: VIDEO-[UpdateOutputBufCnt]:[2122]update output buffer num: min count(9), max count(13)

所以认为是华为手机 ROM 级的问题,就想像之前给高通提 case 一样,给华为提个问题单。

由于华为问题单需要上传出现问题的应用安装包,而当我问及同事能否将公司用的 demo 上传时却得到了否定的答案。没办法,就只能把其中的解码部分抽取出来写一个简单的硬解码 Demo 然后上传了。

意外发现

在重写纯硬解码 Demo 时意外地发现,当我在 DecodeDemo::Decode() 中只完成了解码功能时,mate10 的表现竟然异常地稳定,从未出现过解码失败的情况。

这让我十分怀疑问题的原因在于 Seek 这个过程。由于在 Demo 中,我们不能确定用户从何处开始播放,所以,会有一个默认 seek 到 0 的过程(这显然不是个好的设计思路),而 seek 除了要调用 AMediaExtractor_seekTo 把 MediaExtractor Seek 到合适的位置外,还需要调用 AMediaCodec_flush 将 MediaCodec 中的缓冲区清掉。

果然,当我把 seek 过程添加到 DecodeDemo::Decode() 方法中后,mate 10 开始出现概率性解码失败了。然而,seek 是不得不用的一个功能,那问题出现在哪儿呢?刚刚我们提到过Seek 功能很简单,主要代码也就只有两行,如下:

ret = AMediaCodec_flush(codec_);
    if (ret != AMEDIA_OK) {
        AVLOGE("Error when flush codec. return %d.", ret);
        return false;
    }
    ret = AMediaExtractor_seekTo(extractor_, 0, AMEDIAEXTRACTOR_SEEK_PREVIOUS_SYNC);
    if (ret != AMEDIA_OK) {
        AVLOGE("Error when seek to. return %d.", ret);
        return false;
    }

而且,这两个函数调用的结果都是 AMEDIA_OK,没有问题。

从出错时的 log 上看,sliceheader first part dec err 似乎是说送入的数据不完整,难道是 AMediaExtractor_seekTo 调用出了问题?然而这个调用不可避免。好在,只有两行代码,就分别屏蔽一下试试呗。

结果令人兴奋,当我把 AMediaCodec_flush(codec_) 这行及其后四行代码注释掉,mate 10 又恢复了稳定成功解码的状态,从未出错。显然,问题出在 AMediaCodec_flush(codec_) 这一句。

假想分析

我做了个大胆的假想:如果 MediaCodec 的缓冲区中没有内容的时候又调用了 flush 的话,会导致缓冲区中的数据出错。

因为 seek 不得不用,而未曾播放过时 MediaCodec 的缓冲区中其实没有内容,因此,我对代码做了些优化,用一个 bool 变量 everplay 来标记是否曾经向 MediaCodec 的缓冲区中压入过数据。

于是有了 NdkMediaCodecDemo 项目中 DecodeDemo::Seek(int sec) 这个方法,以及 DecodeDemo::Play(int sec) 方法中对 everplay_ 的置 true:

bool DecodeDemo::Seek(int sec) {
    AVLOGD("PPPPPPPP::Seek.");
    if (everplay_) {
        everplay_ = false;
        int ret = AMediaCodec_flush(codec_);
        if (ret != AMEDIA_OK) {
            AVLOGE("Error when flush codec. return %d.", ret);
            return false;
        }
    }
    int ret = AMediaExtractor_seekTo(extractor_, sec * 1000 * 1000, AMEDIAEXTRACTOR_SEEK_PREVIOUS_SYNC);
    if (ret != AMEDIA_OK) {
        AVLOGE("Error when seek to. return %d.", ret);
        return false;
    }
    AVLOGD("PPPPPPPP::Seek finish.");
    return true;
}

bool DecodeDemo::Play(int sec) {
    AVLOGD("PPPPPPPP::Play.");
	everplay_ = true;
	...
}

这样一来,便不再有解码失败的问题了。这大概也印证了我的假想,至少,可以说,在未曾向 MediaCodec 中 queue 过数据之前,不能调用 AMediaCodec_flush()。

同时,给 Demo 增加了播放到一半、或者末尾之前的位置,然后 seek 到开始或中间位置的功能,反复测试,也没有问题。

HarmonyOS 2.0.0 系统才有

问题解决了,但是为什么在 小米 系列手机上没有这个问题呢?

其实在实际解决这个问题的过程中,我是先只实现了一个带 seek 的解码 Demo,然后拿各种手机对这个 Demo 进行了测试。这个 Demo 包括 四个 Button,分别是 AUTO,INIT,SEEK,PLAY。我在 AUTO 中联合调用了后面三个 Button 所调用的接口,即 return Init() && Seek() && Play();。当时还没加 everplay_ 这个标记来规避在未向 MediaCodec 压入数据前调用 AMediaCodec_flush()。然后拿各种机型做了些测试,结果如下:

android 硬解码 延时 手机如何设置硬解码_android

其中 AP 的意思是按 AUTO 键自动播放。而 ISP 意思是依次按 INIT SEEK PLAY 键手动完成这个过程。从上述结果来看,即使芯片不够好,但是是 Android 系统的,都能成功解码不出现问题。而所有 HarmonyOS 系统的手机,都会存在问题。 Vivo X9s 因没有找到账号密码没法成功安装 Demo 测试。

所以,从上述测试结果来看,这个在未向 MediaCodec 压入数据前调用 AMediaCodec_flush 导致解码失败的问题应该是 HarmonyOS 系统特有的问题。

额外收获

在实现解码 Demo 过程中,还遇到过一个很意外的 crash,后来发现在业务代码中也有偶现的 crash 也是这个原因导致的。

DecodeDemo::Init() 方法中有一段曾经是这么写的:

int track_cnt = AMediaExtractor_getTrackCount(extractor_);
    const char *info;
    for (int i = 0; i < track_cnt; i++) {
        format_ = AMediaExtractor_getTrackFormat(extractor_, i);
        AMediaFormat_getString(format_, AMEDIAFORMAT_KEY_MIME, &info);
        string str(info);
        if (str.find("video") == 0) {
            AMediaExtractor_selectTrack(extractor_, i);
            video_track_indx_ = i;
            break;
        }
        AMediaFormat_delete(format_);
        format_ = nullptr;
        delete info;
    }

因为 bool AMediaFormat_getString(AMediaFormat *, const char *name, const char * *out) 这个函数要求最后的 out 是个 const 的,所以我声明了 const char *info,后来证明 const char *info = nullptr 的写法也对,这个需要关注的是 const 的位置,可以搜索一下 const int * const a = nullptr,有不少相关博文讲解这个原理。

这一段很好理解,看看输入视频文件有几个数据流,然后依次判断哪个是视频流,但是如果输入视频文件的第一个流,亦即 0 轨道,不是视频流(轨道)的话。for 循环中当 i = 1 时,将在 AMediaFormat_getString() 这一句发生段错误。后来发现,把 delete info; 这行删掉后就不会有这个问题了。

因此,可能这个 const char* info 指向的空间是由 MediaCodec 内部管理(负责申请、释放)的,所以,当我在外面释放了这个指针之后,反而导致了段错误。所以,这就有个问题了,是否有这样一个约定俗成:即所有传入指针地址的参数,都是由接口内部来管理内存的呢?