美文网首页
iOS上 H265+G.711A/AAC录像的坑

iOS上 H265+G.711A/AAC录像的坑

作者: FingerStyle | 来源:发表于2023-06-14 19:04 被阅读0次

    前段时间做公司的摄像头项目,视频用的是h265,音频原先用的是G.711A,后面改成了AAC。这里的录像功能指的是把用户直播看到的内容录制到系统相册里面,实现方式是通过ffmpeg把视频流数据加上一定的配置(相当于写了文件头)写入到iOS的相册。由于iOS相册的读取条件不透明,经常会有些不兼容的情况,又没有任何提示,因此遇到了不少的坑,这里记下来方便后来人避坑。

    一、录像无法保存到相册

    问题表现:录下来的视频用ffplay、VLC这些都能播放,但是无法保存到iOS相册,用quicktime也提示不兼容。 通过ffplay打印的信息看不出任何问题,视频内容也是正常的。
    根本原因:视频编码器的extraData没有赋值,导致头部信息不对,苹果相册无法识别。
    解决过程:一开始我猜测是文件头写的不对,我是用mp4作为容器来封装音视频数据的,因此专门去查看了mp4的格式说明(不熟悉的朋友可以看看这个文章,写的挺详细https://zhuanlan.zhihu.com/p/543684404?utm_id=0),对比文件头的内容,发现并没有什么问题。
    我在网上搜了下iOS相册兼容的问题,发现有人提到iOS貌似对Annex-B支持的不太好,而需要用hvcc, 而嵌入式这边给过来的视频是Annex-B格式的(头部带有SPS/PPS/VPS等信息,用0x000001分开),那会不会是iOS相册不支持Annex-B视频的录制呢?我去社区里问了一圈,没有得到肯定的答复,然后又尝试用ffmpeg把视频帧从Annex-B 转为hvcc,这个过程中还发现了filter这种东西,虽然最后没用上,但也算是增长了知识。ffmpeg 是有annexTohvcc的代码的,但是没有在头文件公开出来,因此还需要拷贝源文件,修改方法名重新编译才能使用。费了大半天的工夫弄好之后,发现还是不行,转换后的视频调用write_frame方法的时候就报错了,看来这条路是走不通了。

    于是我又跟嵌入式讨论,嵌入式那边也做了录像,他们录制的视频文件是可以用quicktime播放的,而且也是用ffmpeg写的,因此我跟他们对比了一下代码,发现内容也差不多,没有看出明显的区别,然后我又打开HexFriend对视频的内容进行对比。发现嵌入式那边录制的视频文件基本上每隔一段内容都会有类似0x000001....... 的内容,大概有20多个字节是重复的吧,而我这边是没有这么长的重复字节的,我就怀疑这是他们在每一帧前面加了些内容。 我拿着这个再问去他们,他们说这些就是SPS/PPS/VPS的内容,如果没有这个视频播放不了,发给我的视频数据里面也有这个内容,但是同样的内容,为什么客户端写进去就无法兼容iOS相册呢?然后我再仔细对比了一下代码,发现他们多了一段逻辑,是对avcodecContext的extraData 进行赋值,而我这边却没有,他们说这个就是SPS/PPS/VPS的内容,那会不会是extraData没赋值导致的问题呢?
    带着疑问,我尝试从视频头部截取这部分内容,根据已知的规则,SPS/PPS/VPS每一段都是用0x000001来开头的,因此我借鉴了一下ffmpeg里面读取hevc视频头的源码,做了一些改动

    int findHevcExtraDataEnd (const uint8_t *buf, int buf_size){
        int64_t state64 = -1;
        for (int i = 0; i < buf_size; i++) {
            int nut;
            
            state64 = (state64 << 8) | buf[i];
            
            if (((state64 >> 3 * 8) & 0xFFFFFF) != START_CODE)
                continue;
            
            nut = (state64 >> (2 * 8 + 1)) & 0x3F;
            if (nut == NAL_IDR_N_LP || nut == NAL_IDR_W_RADL ) {
                return i-6;
            }
        }
        return -1;
    }
    

    通过findHevcExtraDataEnd 这个方法可以从h265的数据里面抽取出SPS/PPS/VPS。然后再把这个填充到extraData里面,果然写到相册就不报错了。视频也能播放了,只是还有些其他的问题。

    二、视频前面一小段是黑的

    问题表现:视频前面一小段是黑的,黑的时间不规律,有时长达几秒,有时很短,但是有声音
    根本原因:录制的时候没有从关键帧开始录,导致前面的P帧、B帧都解析不了。
    解决过程: 这个问题其实相对比较好解,分析了一小段视频之后,就找到问题所在了。解决方案就是在录制前做一下判断,是否有拿到I帧,如果没有,则不进行录像。

    三、没有声音(G.711A)

    问题表现: 相册里面只有视频,无法播放声音,在ffplay和VLC上也无法播放。此时音频用的是g.711A格式,容器格式是mp4
    根本原因:ffmpeg不支持将g.711A封装到mp4,需要改源码或者将文件扩展名改为.mov
    解决过程:这个问题发现的时候,我第一时间就在google上搜索,得到了一些线索,因此解决起来不算麻烦,不过网上是h264+g.711a时出现问题,但本质上都一样。

    四、声音只有前半段

    问题表现: 只有前半段有声音,后半段没有。
    根本原因: 音视频都写到一个流里了。
    解决过程: 这个问题的解决过程还是走了一点弯路,一开始没有仔细去看代码,而是从音频的参数和PTS着手,怀疑是参数或者PTS设置的不对。因为PTS是控制播放速度的,所以一开始就是调整音频包的PTS,缩小到原来的一半,发现问题没有得到任何改善。 然后又去修改每帧的比特数和声道数。 发现比特数改了没有什么效果,把单声道改为双声道之后倒是能时间正常了,但是声音却失真了,杂音也变得更大。这是什么原因呢? 我用google 搜索、问chatGPT都没有得到满意的结果,于是就想会不会是数据写的有问题。于是又打开HexFriend,对比嵌入式那边录制的正常的音频和我录制的音频,发现有问题的音频的stsz单数sample 有值,双数为0,而正常的stsz 是每个sample都有值,而且看sample数量也发现有问题的音频sample数比正常的少了差不多一半。这是不是说明只录了一半的数据呢?带着这个疑惑我又去重新看我写的代码,才发现原本是分开应该是视频包写到视频流,音频包写到音频流的代码,变成了视频和音频都写到了音频流里。这才导致音频只有一半的长度。

    ========================================================
    原本录像的问题到这里就差不都结束了,但后面音频改为了AAC,又引发了新的问题

    五、没有声音(AAC)

    问题表现: 相册录进去的视频没有声音,但是同样的文件用ffplay 和VLC能正常播放。
    根本原因:音频数据没有加ADTS头
    解决过程: 由于云端上传的限制,我们更换了音频格式到AAC,但嵌入式那边麦克风只支持8K和16K采样率,于是我们又换了编码器(AAC-FDK)。因为改动比较大,一开始我是怀疑音频的参数设置的不对或者是编码器问题。我对比了嵌入式那边的设置,改了几个参数之后发现没效果,又怀疑是苹果不支持16K的采样率。 于是我先用ffmpeg解复用,再用苹果的audioConverter来解码aac,发现解码的音频是能正常播放的,因此断定问题出在封装上,而不是音频本身。
    有前面的经验作为参考,我觉得extraData 应该是有问题。我问嵌入式那边是否有设置extraData,他们说没有,于是我找他们拿了一段视频对比,看了下mp4 头的box, 没有找到明显差异。于是我又去对比音频内容,发现写进去的音频内容都有fff1开头,而且extraData是2个字节,跟正常的adts头部长度不一致(7个字节),而他们录进去的视频没有fff1,我怀疑是头部少了些内容。嵌入式那边跟我说这个不需要adts头也能播,我看了下他们确实没加头,这就很奇怪了。 我让他们把adts头加在音频前面看看,但他们说传给云端的音频不能加adts,否则传不上去。于是我只能再去对比代码,但反复对比仍然找不到问题所在,这时他们跟我说之前发给我的视频是另一个摄像头录的,跟我连的摄像头不是同一个,不排除是摄像头的问题。然后我让他们重新在我连的摄像头上烧录程序,重新录制,发现居然录出来的视频也有问题,然后我就怀疑是摄像头本身有问题了。
    到这里我以为问题基本确定了,但后面他们查到一个规律,发现没有adts头,有一定的概率音频写进去播放不了,但加了adts头,就一定能播放。 但他们碍于云端限制又不能加头,于是我只能想办法自己加上去了。 音频跟视频不同,视频每一个I帧的头部都是一样的,而adts头部跟文件长度有关,之前的g.711A每一帧长度是固定的,但AAC每一帧长度不固定,所以没法直接从数据头部截取,只能自己拼接了。还好这个代码网上有现成的,直接抄过来就行。加上了ADTS头后我再试了试,还是不行,这又是怎么回事???
    我只能再检查extraData ,发现一个很奇怪的现象,就是刚创建avcodeContext的时候,extraData 是空的,但录了一帧之后,发现extraData变成2个字节了,明显不是正常的,但是这是从哪来的呢?我一时半会找不到头绪,于是只能尝试在写入包之前清掉extraData,发现这样也还是有问题, 又尝试在avcodec_parameters_to_context 之前清除extraData,这下总算是正常了。

    在这个问题的解决过程中,我还尝试了使用AVAssetWriter和AVAssetExporter来封装,发现都有这样那样的问题,要不就时间戳设置的不对,导致白屏,要不就导出失败,最后发现还是ffmpeg好用。

    总结:

    ffmpeg在音视频处理方面还是很强大的,虽然有些瑕疵,像不支持g.711a这样的音频写到mp4,然后extraData有些莫名其秒的数据之类的,但总体上还是比较好用,设计的也很合理,源码也有不少直接学习参考。对于新手来说,学习ffmpeg可以得到很多收获。 而苹果自带的框架也很强大,但是不开源,错误提示也不够清晰,对于新手不够友好。最关键的是,ffmpeg是跨平台的,代码可以多端使用,因此还是可以投入大量时间去学习研究的。作为端上的开发同学来说,两者都得会,特别是对于系统相册的特性,平时也要多一些了解,积累这方面的经验,否则出了还真是难以排查。

    相关文章

      网友评论

          本文标题:iOS上 H265+G.711A/AAC录像的坑

          本文链接:https://www.haomeiwen.com/subject/feckydtx.html