• 【音视频笔记】Mediacodec+Muxer生成mp4,浏览器无法播放问题处理


    背景

    最近在测试视频录制功能时发现,AudioRecord + MediaCodec + MediaMuxer生成的MP4,PC浏览器无法播放 ,但是Android、Windows、Mac的播放器应用都能正常播放。虽然不禁想吐槽浏览器视频组件的容错性差,但我也意识生成的文件格式肯定也是有问题的。

    然后尝试了合成MP4视频时,只保留视频通道,不要音频,发现拖到浏览器中可以正常播放。使用ffprobe检查有问题的MP4文件,有如下错误输出:

    [aac @ 0x7f95c9c0e7c0] Input buffer exhausted before END element found
    
    • 1

    至此,基本确定问题出现在生成的音频数据上。

    解决过程

    由于此前个人音视频开发经验不足,MediaCodec、MediaMuxer编码和合成视频的相关代码参考了一些开源项目及博客。
    但由于开发周期紧急,没有足够的时间来仔细研究和排查,当时就采用了一种曲线救国的方案。

    曲线修复方案

    能想到这个方案也比较偶然。当时查阅了一些资料和博客,用到了ffmpegffprobe工具对问题视频进行分析。

    在尝试了使用ffmpeg工具对问题视频进行转换后,意外地发现,虽然命令也会报错[aac @ 0x7f95c9c0e7c0] Input buffer exhausted before END element found,但是,问题视频经过fmpeg转换后,生成的新视频,用ffprobe命令查看是没有错误输出的,也可以正常播放!也就是说,ffmpeg在处理转换有问题的音频时,会自动跳过那些有问题的数据。

    由此,想到了一个比较曲折的方案:先用AudioRecord + MediaCodec + MediaMuxer生成MP4,然后使用ffmpeg命令对生成的视频进行一点无关紧要的转换(重点是让它处理掉有问题的数据),然后就能得到一个格式正确的音频数据,然后用MediaExtractor提取出原MP4中的视频数据,最后用MediaMuxer合成最终格式正确的mp4文件。
    因为是音频有问题,所以实践中我就使用了如下命令来转换:

    ffmpeg -i input.mp4 -vn -ab 96k out.m4a
    
    • 1

    -vn参数指定不要视频数据,-ab 96k将音频码率转为96k。

    现在,只需要裁剪、交叉编译一个满足以上需求的arm版本的ffmpeg可执行程序就好了。关于如何裁剪和编译ffmpeg,网上音视频相关的技术文章一大把,就不赘述细节了。

    这里记录一下我反复测试编译配置参数后,能输出较小体积(约2.6MB)arm版ffmpeg可执行命令的编译脚本,方便以后查看。因为我只需要处理音频,所以这个配置编译出的ffmpeg只能解码MP4和aac,并且只支持输出m4a音频。

    #!/bin/sh
    
    # NDK路径,根据电脑环境配置情况调整
    NDK_HOME="/Users/shenyong/Library/Android/sdk/ndk/21.4.7075529"
    TOOLCHAIN="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64"
    SYSROOT="$TOOLCHAIN/sysroot"
    
    # 默认使用arm编译配置
    API=29
    ARCH=arm
    CPU=armv7-a
    TOOL_CPU_NAME=armv7a
    # CROSS_PREFIX, CC and CXX for arm
    CROSS_PREFIX="$TOOLCHAIN/bin/arm-linux-androideabi-"
    CC="$TOOLCHAIN/bin/$TOOL_CPU_NAME-linux-androideabi$API-clang"
    CXX="$TOOLCHAIN/bin/$TOOL_CPU_NAME-linux-androideabi$API-clang++"
    OUTPUT_DIR="./android/$CPU"
    OPTIMIZE_CFLAGS="-march=$CPU"
    
    function config_arm64() {
      ARCH=arm64
      CPU=armv8-a
      TOOL_CPU_NAME=aarch64
      # CROSS_PREFIX, CC and CXX for arm64
      CROSS_PREFIX="$TOOLCHAIN/bin/$TOOL_CPU_NAME-linux-android-"
      CC="$TOOLCHAIN/bin/$TOOL_CPU_NAME-linux-android$API-clang"
      CXX="$TOOLCHAIN/bin/$TOOL_CPU_NAME-linux-android$API-clang++"
      OUTPUT_DIR="./android/$CPU"
      OPTIMIZE_CFLAGS="-march=$CPU"
    
      #libmediandk.so路径
      MEDIA_NDK_LIB=$TOOLCHAIN/sysroot/usr/lib/aarch64-linux-android/$API
      ADD_MEDIA_NDK_SO="--extra-ldflags=-L$MEDIA_NDK_LIB --extra-libs=-lmediandk "
    }
    
    # 如果需要编译arm64版本,将以下行取消注释即可
    config_arm64
    
    #清除之前的编译配置及输出
    make distclean
    
    ./configure \
    --prefix=$OUTPUT_DIR \
    --target-os=android \
    --arch=$ARCH \
    --cpu=$CPU \
    --enable-cross-compile \
    --cross-prefix=$CROSS_PREFIX \
    --sysroot=$SYSROOT \
    --cc=$CC \
    --cxx=$CXX \
    --extra-cflags="-Os -fpic $OPTIMIZE_CFLAGS " \
    --disable-shared \
    --enable-static \
    --enable-neon \
    --disable-asm \
    --disable-gpl \
    --disable-postproc \
    --enable-ffmpeg \
    --disable-ffplay \
    --disable-ffprobe \
    --disable-avdevice \
    --disable-doc \
    --disable-symver \
    --disable-protocols \
    --enable-protocol=file \
    --disable-network \
    --disable-jni \
    --disable-mediacodec \
    --disable-hwaccels \
    --disable-encoders \
    --enable-encoder=aac \
    --disable-decoders \
    --enable-decoder=aac \
    --enable-decoder=mpeg4 \
    --disable-muxers \
    --enable-muxer=ipod \
    --disable-demuxers \
    --enable-demuxer=aac \
    --enable-demuxer=mpegvideo \
    --enable-demuxer=mov \
    --disable-parsers \
    --enable-parser=aac \
    --enable-parser=mpeg4video \
    --enable-parser=mpegaudio \
    --disable-filters \
    --disable-bsfs \
    --enable-bsf=aac_adtstoasc
    
    make clean
    make -j12
    make install
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92

    解决问题根源

    既然自己分析找不到问题根源,就看看别人正常工作的代码有什么不一样吧,于是开始在GitHub上找相似功能的开源库。在运行AudioVideoRecordingSample这个演示库后,发现别人生成的视频和音频,用ffprobe命令检查格式都是正确的。

    仔细分析对比后,终于找到了问题点。网上各种博客的示例代码中,都是在dequeueOutputBuffer()返回的输出buffer下标大于0时,就直接写入Muxer,关键部分类似这样:

    int outputBufferIndex = mAudioCodec.dequeueOutputBuffer(bufferInfo, 0);
    if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
        // 将mMediaCodec的指定的格式的数据轨道,设置到mMediaMuxer上
        mAudioTrackIndex = mMediaMuxer.addTrack(mAudioCodec.getOutputFormat());
    	// ...
    } else {
        while (outputBufferIndex >= 0) {
            // 获取数据
            ByteBuffer outBuffer = mAudioCodec.getOutputBuffers()[outputBufferIndex];
            audioPts = (System.nanoTime() - startNanoTime) / 1000;
            bufferInfo.presentationTimeUs = audioPts;
            // 编码数据写入muxer
            mMediaMuxer.writeSampleData(mAudioTrackIndex, outBuffer, bufferInfo);
            // 释放 outBuffer
            mAudioCodec.releaseOutputBuffer(outputBufferIndex, false);
    
            outputBufferIndex = mAudioCodec.dequeueOutputBuffer(bufferInfo, 0);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    但是,我发现AudioVideoRecordingSample这个库在获取的outputBufferIndex >= 0时,还有一个关键的处理:

    // ...
    if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
        // You shoud set output format to muxer here when you target Android4.3 or less
        // but MediaCodec#getOutputFormat can not call here(because INFO_OUTPUT_FORMAT_CHANGED don't come yet)
        // therefor we should expand and prepare output format from buffer data.
        // This sample is for API>=18(>=Android 4.3), just ignore this flag here
        if (DEBUG) Log.d(TAG, "drain:BUFFER_FLAG_CODEC_CONFIG");
        mBufferInfo.size = 0;
    }
    if (mBufferInfo.size != 0) {
        // ...
        muxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo);
    }
    /// ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    就是判断当前的mBufferInfo有BUFFER_FLAG_CODEC_CONFIG这个标志时,把size置为0了,所以这一次回调的数据,是没有写入muxer的。于是赶紧看了一眼BUFFER_FLAG_CODEC_CONFIG的官方文档:

        /**
         * This indicated that the buffer marked as such contains codec
         * initialization / codec specific data instead of media data.
         */
        public static final int BUFFER_FLAG_CODEC_CONFIG = 2;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这才恍然大悟!当BufferInfo有这个标志的时候,buffer包含编解码器初始化或编解码器特定的数据而不是媒体数据!

    于是在自己的代码中也上这个判断处理,生成的视频文件再用ffprobe查看,也能正常输出信息,没有报错了。关键代码如下:

    while (true) {
        try {
            // 返回有效数据填充的输出缓冲区的索引
            int outputBufferIndex = mAudioCodec.dequeueOutputBuffer(bufferInfo, 0);
            if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // 将mMediaCodec的指定的格式的数据轨道,设置到mMediaMuxer上
                mAudioTrackIndex = mMediaMuxer.addTrack(mAudioCodec.getOutputFormat());
            } else {
                while (outputBufferIndex >= 0) {
                    // 获取数据
                    ByteBuffer outBuffer = mAudioCodec.getOutputBuffers()[outputBufferIndex];
                    // 修改音频的 pts,基准时间戳
                    audioPts = (System.nanoTime() - startNanoTime) / 1000;
                    bufferInfo.presentationTimeUs = audioPts;
                    if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                        Log.w(TAG, "audio BUFFER_FLAG_CODEC_CONFIG bufferInfo.size: " + bufferInfo.size);
                        // 配置回调,不是有效的媒体数据,不写入。如果写入了,会导致mp4文件有错误数据帧,
                        // 容错性不够好的播放器(比如pc浏览器)可能无法正常播放视频。
                          bufferInfo.size = 0;
                    }
                    // 写入音频数据
                    if (bufferInfo.size > 0) {
                        mMediaMuxer.writeSampleData(mAudioTrackIndex, outBuffer, bufferInfo);
                    }
                    if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                        Log.w(TAG, "audio BUFFER_FLAG_END_OF_STREAM bufferInfo.size: " + bufferInfo.size);
                    }
                    // 释放 outBuffer
                    mAudioCodec.releaseOutputBuffer(outputBufferIndex, false);
                    if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                        Log.w(TAG, "audio got BUFFER_FLAG_END_OF_STREAM flag. audioPts: "
                                + bufferInfo.presentationTimeUs + "bufferInfo.size: " + bufferInfo.size);
                        if (shouldExit) {
                            onDestroy();
                            return;
                        }
                    }
                    outputBufferIndex = mAudioCodec.dequeueOutputBuffer(bufferInfo, 0);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    这样一来,使用Mediacodec+Muxer就能生成格式正确的mp4视频文件了,无需其他处理,效率大大提高。

    从打印日志来看,带这个标志的一般就是第一个输出的buffer,并且数据量很少:

    2023-09-21 10:16:36.664 BaseVid...corder  W  audio BUFFER_FLAG_CODEC_CONFIG bufferInfo.size: 2
    2023-09-21 10:16:36.675 BaseVid...corder  W  video BUFFER_FLAG_CODEC_CONFIG bufferInfo.size: 30
    
    • 1
    • 2

    最后经过测试验证,也确实是这样的:
    只要bufferInfo有BUFFER_FLAG_CODEC_CONFIG标志时,把buffer数据写入muxer了,用ffprobe查看生成的视频文件,就一定会有[aac @ 0x7f95c9c0e7c0] Input buffer exhausted before END element found这个错误输入;反之不写入就是正常的。

  • 相关阅读:
    node.js - 上传文件至阿里云oss
    Python文件操作篇
    数据结构:3.3.4遍历应用例子
    【Flink】第二节 windows下运行
    计算机毕业设计ssm基于ssm的高校党建平台f80yq系统+程序+源码+lw+远程部署
    我的这个c++程序到底是怎么了?(相关搜索:for循环|主线程)
    解决Devops部署Java应用中文乱码
    深入理解Linux内核——内存管理(4)——伙伴系统(1)
    【独立站运营】做社交媒体营销的两大关键点
    WHAT - React 学习系列(一)
  • 原文地址:https://blog.csdn.net/China_Style/article/details/133036623