最近在做Android端的ffmpeg硬解码开发,用的ffmpeg4.2.1,发现网上相关资料比较少,大部分都是直接调用Android的MediaCodec进行硬解码的开发,用ffmpeg的比较少。在尝试使用ffmpeg自带的硬解码examples进行改造的过程中也发现有一些问题,一直阻塞着,前后花了两周时间,最终在公司大佬的指点下才调通了这一块。这里简单记录一下。
首先是解码函数,这里要注意硬解码使用的是 avcodec_find_decoder_by_name("h264_mediacodec")。解码最后的时候发了一个空包给解码器,空包的意思就是告诉解码器,数据发完了,可以结束了。
JNIEXPORT jint JNICALL Java_com_example_myapplication_NativeUtils_decode (JNIEnv *env, jobject obj, jstring input_jstr, jstring output_jstr) { AVFormatContext *pFormatCtx = NULL; int ret, videoIndex, err_code; AVCodecContext *pCodecCtx = NULL; AVCodec *pCodec = NULL; AVPacket *packet; FILE *fp_yuv; clock_t time_start, time_finish; double time_duration; AVStream *video = NULL; AVFrame *frame = NULL, *sw_frame = NULL; receive_cnt = 0; send_cnt = 0; char *input_str = (*env)->GetStringUTFChars(env, input_jstr, NULL); char *output_str = (*env)->GetStringUTFChars(env, output_jstr, NULL); LOGI("----input:%s----\n", input_str); LOGI("----output:%s----\n", output_str); av_log_set_callback(logcat_log); pFormatCtx = avformat_alloc_context(); if (err_code = avformat_open_input(&pFormatCtx, input_str, NULL, NULL) != 0) { LOGE("Couldn't open input stream--- %s: %d\n---", input_str, err_code); return -1; } if (avformat_find_stream_info(pFormatCtx, NULL) < 0) { LOGE("Couldn't find stream information.\n"); return -1; } ret = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, &pCodec, 0); if (ret < 0) { LOGE("Cannot find a video stream in the input file\n"); return -1; } videoIndex = ret; //pCodec= avcodec_find_decoder(AV_CODEC_ID_H264); pCodec = avcodec_find_decoder_by_name("h264_mediacodec"); if (!(pCodecCtx = avcodec_alloc_context3(pCodec))) return AVERROR(ENOMEM); video = pFormatCtx->streams[videoIndex]; if (avcodec_parameters_to_context(pCodecCtx, video->codecpar) < 0) return -1; if ((ret = avcodec_open2(pCodecCtx, pCodec, NULL)) < 0) { LOGE("----Failed to open codec for stream :%d----\n", ret); return -1; } packet = (AVPacket *) av_malloc(sizeof(AVPacket)); fp_yuv = fopen(output_str, "wb+"); if (fp_yuv == NULL) { LOGE("----Cannot open output file----\n"); } if (!(frame = av_frame_alloc()) || !(sw_frame = av_frame_alloc())) { LOGD("----frame init error----\n"); return -1; } time_start = clock(); int read_frame_ret = 0; int read_frame_eof = 0; while (1) { if (!read_frame_eof) { read_frame_ret = av_read_frame(pFormatCtx, packet); if (read_frame_ret == AVERROR_EOF) { read_frame_eof = 1; packet->data = NULL; packet->size = 0; packet->stream_index = videoIndex; } else if (read_frame_ret < 0) { av_packet_unref(packet); LOGE("----av_read_frame end by error, read_frame_ret:0x%x----\n", read_frame_ret); break; } } if (packet && (videoIndex == packet->stream_index)) { ret = decode_write(pCodecCtx, packet, fp_yuv, frame, sw_frame); if (ret == AVERROR_EOF) { av_packet_unref(packet); break; } } av_packet_unref(packet); } LOGD("----av_read_frame end, read_frame_ret:0x%x----\n", read_frame_ret); fclose(fp_yuv); time_finish = clock(); time_duration = (double) (time_finish - time_start); LOGD("--------time:%fus\n", time_duration); av_frame_free(&frame); av_frame_free(&sw_frame); avcodec_close(pCodecCtx); avformat_close_input(&pFormatCtx); av_buffer_unref(&hw_device_ctx); LOGD("----Decode End----\n"); return 0; }
以下是解码的核心代码,这里最主要的是要理解avcodec_send_packet方法和avcodec_receive_frame方法是异步的。此外AVERROR(EAGAIN)即-11代表过一会再尝试,解码过程中报这个是正常的。
int decode_write(AVCodecContext *avctx, AVPacket *packet, FILE *output_file, AVFrame *frame, AVFrame *sw_frame) { int size; int tempRet = 0; int sendRet = 0; int receiveRet = 0; int again = 0; int readEnd = 0; uint8_t *buffer = NULL; sendRet = avcodec_send_packet(avctx, packet); if (sendRet < 0) { LOGD("----avcodec_send_packet error, send_cnt:%d, ret:0x%x----\n", send_cnt, sendRet); if (sendRet == AVERROR(EAGAIN)) { again = 1; } } else { send_cnt++; } LOGD("----avcodec_send_packet successful, send_cnt:%d, ret:0x%x----\n", send_cnt, sendRet); while (1) { receiveRet = avcodec_receive_frame(avctx, frame); if ((receiveRet == AVERROR(EAGAIN)) || receiveRet == AVERROR_EOF) { if (buffer) { av_freep(&buffer); } LOGD("----avcodec_receive_frame error1, receive_cnt:%d, ret:0x%x----\n", receive_cnt, receiveRet); readEnd = 1; break; } else if (receiveRet < 0) { LOGD("----avcodec_receive_frame error2, receive_cnt:%d ret:%d, 0x%x----\n", receive_cnt, receiveRet); goto fail; } if (!readEnd) { AVFrame *tmp_frame; tmp_frame = frame; receive_cnt++; LOGD("----avcodec_receive_frame successful, receive_cnt:%d ret:0x%x----\n", receive_cnt, receiveRet); size = av_image_get_buffer_size(tmp_frame->format, tmp_frame->width, tmp_frame->height, 1); buffer = av_malloc(size); if (!buffer) { LOGE("Can not alloc buffer\n"); tempRet = AVERROR(ENOMEM); goto fail; } tempRet = av_image_copy_to_buffer(buffer, size, (const uint8_t *const *) tmp_frame->data, (const int *) tmp_frame->linesize, tmp_frame->format, tmp_frame->width, tmp_frame->height, 1); if (tempRet < 0) { LOGE("Can not copy image to buffer\n"); goto fail; } if ((tempRet = fwrite(buffer, 1, size, output_file)) < 0) { LOGE("----fwrite buffer error!----\n"); goto fail; } else { LOGD("----fwrite buffer successful!----\n"); } } fail: av_freep(&buffer); if (tempRet < 0) return tempRet; } if (again) { return decode_write(avctx, packet, output_file, frame, sw_frame); } else if (readEnd) { return receiveRet; } }
最后不要忘记实现jni.h里的方法,这是一个固定的方法名,类似android的生命周期,加载jni的时候会自己调用 ,ffmpeg需要靠这个来调用Android的MediaCodec。
jint JNI_OnLoad(JavaVM *vm, void *reserved) { LOGI("ffmpeg JNI_OnLoad"); av_jni_set_java_vm(vm, reserved); return JNI_VERSION_1_6; }