avcodec_receive_frame始终返回EAGAIN

今天我们研究一个问题:

avcodec_receive_frame()始终返回EAGAIN

根本的解决方案还需要深入debug,但是这个函数很太复杂,需要些时间和耐心;

目前在不考虑编解码性能的情况下,能work around的方法只有一个,那就是取消硬解码,使用libx264进行软解码:

  • 删除所有有关mediacodec相关的选项  

  • 增加如下编译选项:

--enable-libx264 \
    --enable-encoder=libx264 \
    --enable-parser=h264 \
    --enable-encoder=h264 \
    --enable-decoder=h264 \
    --enable-muxer=h264 \
    --enable-demuxer=h264 \
  • 代码方面,不可以再使用这句,这样是会去找硬解码的:

dec = avcodec_find_decoder_by_name("h264_mediacodec")

而要使用下面这句,去使用软解码:

AVCodec *dec = avcodec_find_decoder(stream->codecpar->codec_id);

其实上面的codec_id就是AV_CODEC_ID_H264

但是规避问题不是一个好程序员,就像你炒股亏钱了还要一直扳本直到把所有的钱都套进去了才甘心一样,做程序就要这种死磕到底的精神,不吃饭不睡觉通宵达旦也要找出根本原因,掉头发秃顶肩周炎腰椎间盘突出也在所不惜,你做到了吗,你没有!

关于EAGAIN这个问题,网上查到的更多的是说要循环调用avcodec_send_packet来进行喂数据,特里同学当然是这么做的:

while (1) {    
    ret = avcodec_send_packet(stream->decCtx, pkt_in);


    if (ret < 0) {
        if (ret == AVERROR(EAGAIN)) {
            av_packet_unref(pkt_in);
            continue;
        }
        av_log(NULL, AV_LOG_ERROR, "Error while sending a packet to the decoder\n");
        break;
     }


     while (ret >= 0) {
         ret = avcodec_receive_frame(stream->decCtx, stream->decFrame);
         ... //代码省略
     }
     ... //代码省略
}

但是这样做了也还是一直返回EAGAIN,它不是前面几帧返回EAGAIN,是所有的帧都EAGAIN,整个mp4文件H264码流发完了还是EAGAIN,所以我的这个问题另有蹊跷,头痛啊!

怀疑是AVCodec或者AVCodecContext的问题:

AVCodec
                      soft        hard
capabilities         0x3022     0x60020                         
priv_data_size       53304      112                               
decode               pointer    NULL
receive_frame        NULL       pointer     
caps_interal         0x53       0x4                             
bsfs                 NULL       h264_mp4toannexb                


AVCodecContext
                      soft        hard
ticks_per_frame       2             1

经过调试发现,上面这些参数,在使用软硬解码时的值是不一样的,于是我就试着将硬解码时的值改为软解码时的值会不会有用呢,于是我在avcodec_open2正式调用之前做了对应的修改,重新运行,发现并没什么卵用,问题依旧!

咋整?为了解决这个问题,厚着脸皮在群里在公众号留言在github的issue留言,后来有个抖音公司里的音视频大佬回了我,也是下面这个公众号的作者,大家有兴趣可以关注一下:

他叫我把ffmpeg的打印打开试试,于是我就去网上查,因为ffmpeg默认的打印是printf输出的,printf的输出在android里面是看不到的,需要将打印重新定位到logcat才行,修改如下:

#include <android/log.h>


#define LOG_TAG "MeidaOperationNative"
#define JLOG_I(...) ((void)__android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__))
#define JLOG_E(...) ((void)__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__))


static void my_logoutput(void *ptr, int level, const char *fmt, va_list vl)
{
    va_list vl2;
    char *line = malloc(128);
    static int print_prefix = 1;
    va_copy(vl2, vl);
    av_log_format_line(ptr, level, fmt, vl2, line, 128, &print_prefix);
    va_end(vl2);
    line[127] = '\0';
    JLOG_E("%s", line);
    free(line);
}


av_log_set_level(AV_LOG_INFO);
av_log_set_callback(my_logoutput);

这样处理之后,ffmpeg中所有用 av_log(NULL, AV_LOG_TRACE, "")打印的信息都会输出到logcat中。

于是我在logcat中看到了如下的出错打印:

No output buffer available, try again later

总算有点蛛丝马迹了,感谢大佬的指点啊!这个是ffmpeg中打印出来的,于是我到ffmpeg的源码目录中grep一把,找到了地方,在下面这个文件中的第861行:

ffmpeg-4.4/libavcodec/mediacodecdec_common.c


760 int ff_mediacodec_dec_receive(AVCodecContext *avctx, MediaCodecDecContext *s,
761                               AVFrame *frame, bool wait)
762 {
787     index = ff_AMediaCodec_dequeueOutputBuffer(codec, &info, output_dequeue_timeout_us);
788     if (index >= 0) {
            //代码省略
826     } else if (ff_AMediaCodec_infoOutputFormatChanged(codec, index)) {
            //代码省略
853     } else if (ff_AMediaCodec_infoOutputBuffersChanged(codec, index)) {
854         ff_AMediaCodec_cleanOutputBuffers(codec);
855     } else if (ff_AMediaCodec_infoTryAgainLater(codec, index)) {
856         if (s->draining) {
857             av_log(avctx, AV_LOG_ERROR, "Failed to dequeue output buffer within %" PRIi64 "ms "
858                                         "while draining remaining frames, output will probably lack frames\n",
859                                         output_dequeue_timeout_us / 1000);
860         } else {
861             av_log(avctx, AV_LOG_TRACE, "No output buffer available, try again later\n");
862         }
863     } else {
864         av_log(avctx, AV_LOG_ERROR, "Failed to dequeue output buffer (status=%zd)\n", index);
865         return AVERROR_EXTERNAL;
866     }

也就是说787行的函数调用ff_AMediaCodec_dequeueOutputBuffer返回是<0的,通过打断点进入到该函数里面:

ssize_t ff_AMediaCodec_dequeueOutputBuffer(FFAMediaCodec* codec, FFAMediaCodecBufferInfo *info, int64_t timeoutUs)
{
    int ret = 0;
    JNIEnv *env = NULL;
    JNI_GET_ENV_OR_RETURN(env, codec, AVERROR_EXTERNAL);


    ret = (*env)->CallIntMethod(env, codec->object, codec->jfields.dequeue_output_buffer_id, codec->buffer_info, timeoutUs);
    if (ff_jni_exception_check(env, 1, codec) < 0) {
        return AVERROR_EXTERNAL;
    }
    //代码省略
}

发现每次第7行CallIntMethod的调用都是返回-1,这是native对java层接口的访问,也就是说压根获取不到outputbufferid,跟踪发现其中的参数timeoutUs总为0,其他参数也都有值,貌似都正常,而CallIntMethod无法step into进去,所以也看不到它调用的java的接口实现。至此,貌似就断了,走不下去了。

其实它这里调用的正是我们在java层经常用的MediaCodec的dequeueOutputBuffer()接口:

libavcodec/mediacodec_wrapper.c 中有如下方法映射:


 { "android/media/MediaCodec", "dequeueOutputBuffer", 
 "(Landroid/media/MediaCodec$BufferInfo;J)I", 
 FF_JNI_METHOD, offsetof(struct JNIAMediaCodecFields,
  dequeue_output_buffer_id), 1 },
实际java代码调用是类这样:
int outBuffId = mMediaDeCodec.dequeueOutputBuffer(mBuffInfo, 3000);

java层的这个接口的最后一个时间参数给0或者3000都没所谓啊,我还特意试了的,没有影响,都能正常获取buffer的id。

求助无门,后来想着是不是还有一些ffmpeg的打印没用输出到logcat啊,我把打印都保存到文件试试噶:

static void my_logoutput(void *ptr, int level, const char *fmt, va_list vl) {
    FILE *fp = fopen("/storage/emulated/0/Android/av_log.txt", "w+");
    if (fp) {
        vfprintf(fp, fmt, vl);
        fflush(fp);
        fclose(fp);
    }
}

不改不知道一改吓一跳,这样修改之后,居然硬解码成功了,之前的错误没有了,也就是说加了这个log保存文件的功能后,能正常获取到outputbufferid了。

按理说我这里的my_logoutput实现和获取buffer id应该没有关系才对啊,要说有关系,无非就是这里的打开文件写文件关闭文件多耗了点时间,对了就是时间,再回头想想那个timeoutUs参数,如果我把这个参数改为非0,比如改为8000会怎么样,于是我直接把ffmpeg拖出来修改:

ffmpeg4.4/libavcodec/mediacodecdec_common.c


    #define INPUT_DEQUEUE_TIMEOUT_US 8000
    #define OUTPUT_DEQUEUE_TIMEOUT_US 8000
    #define OUTPUT_DEQUEUE_BLOCK_TIMEOUT_US 1000000
    //此处省略几百行代码
    int64_t output_dequeue_timeout_us = OUTPUT_DEQUEUE_TIMEOUT_US;


    if (s->draining && s->eos) {
        return AVERROR_EOF;
    }


    if (s->draining) {
        /* If the codec is flushing or need to be flushed, block for a fair
         * amount of time to ensure we got a frame */
        output_dequeue_timeout_us = OUTPUT_DEQUEUE_BLOCK_TIMEOUT_US;
    } else if (s->output_buffer_count == 0 || !wait) {
        /* If the codec hasn't produced any frames, do not block so we
         * can push data to it as fast as possible, and get the first
         * frame */
        output_dequeue_timeout_us = 0;
    }
    
    //特里同学hack
    output_dequeue_timeout_us = OUTPUT_DEQUEUE_TIMEOUT_US;


    index = ff_AMediaCodec_dequeueOutputBuffer(codec, &info, output_dequeue_timeout_us);

25行处是我添加的代码,给它个8ms反应时间(如果是0应该是立即返回),再重新编译后拷贝so到androidstudio那边,把my_logoutput注释掉,重新运行,发现硬编解码正常,问题解决了。

现在想想有点觉得不可思议,我一个ffmpeg新手就这么把ffmpeg的源代码给改了,为了自己的目的把大名鼎鼎的ffmpeg给hack了,我始终不太相信这就是root cause,肯定是我哪里没搞对才让timeoutUs这个值成了0,才导致了获取outputbufferid始终为-1,(比如13行的draining应该为1才对,瞎猜的啊)有知道的大佬请务必指点指点。

当我们像无头苍蝇一样无助的时候,这也算一种解决方案吧,你说呢?!

74539d553778aa37ed0f8a90139583a1.png

技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。

1a3e7df7b1297ddeaafde7b065efb803.png

私信领取相关资料

推荐阅读:

音视频开发工作经验分享 || 视频版

OpenGL ES 学习资源分享

开通专辑 | 细数那些年写过的技术文章专辑

NDK 学习进阶免费视频来了

你想要的音视频开发资料库来了

推荐几个堪称教科书级别的 Android 音视频入门项目

觉得不错,点个在看呗~

09dd30a02a63cba761c7795b5356dad4.gif

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值