今天我们研究一个问题:
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才对,瞎猜的啊)有知道的大佬请务必指点指点。
当我们像无头苍蝇一样无助的时候,这也算一种解决方案吧,你说呢?!