还没有看过前置知识的同学可以先从下面的链接点过去:
1、概述
我们在之前的音视频 ijkplayer源码解析系列2--如何解码图像中讲解了基础的解码图像的流程,接下来我们从ijkplayer源码的角度,解析下ijkplayer播放器是如何解码视频文件的。
ijkplayer解封装和解码的主要流程其实和我们在音视频 ijkplayer源码解析系列2--如何解码图像里写的流程是比较相似的,它比较优雅的地方其实是将音频、视频和字幕的解码流程放到三个线程中处理,解码出来的三种类型数据(音频、视频、字幕)做好音画同步问题以后,进行播放。流程我们可以参考下面的流程图,当然这里的流程图是省略的很多细节的,在后续的更新中会不断写清楚。
本章我们就先讲解下ijkplayer的解码部分,至于渲染的部分交给下一章。那我们就结合上面的这张图开始解析代码。
2、流分类和解码准备工作
我们从android调用入口的位置开始分析,android可调用的jni方法名为:_prepareAsync
arduino
{ "_prepareAsync", "()V", (void *) IjkMediaPlayer_prepareAsync },
如上述的代码其在jni层对应的方法名为IjkMediaPlayer_prepareAsync
scss
static void
IjkMediaPlayer_prepareAsync(JNIEnv *env, jobject thiz)
{
MPTRACE("%s\n", __func__);
int retval = 0;
IjkMediaPlayer *mp = jni_get_media_player(env, thiz);
JNI_CHECK_GOTO(mp, env, "java/lang/IllegalStateException", "mpjni: prepareAsync: null mp", LABEL_RETURN);
retval = ijkmp_prepare_async(mp);
IJK_CHECK_MPRET_GOTO(retval, env, LABEL_RETURN);
LABEL_RETURN:
ijkmp_dec_ref_p(&mp);
}
其核心流程就是:
- jni_get_media_player获取播放器IjkMediaPlayer实例,这个IjkMediaPlayer是在java层播放器初始化的时候调用
jni_set_media_player
的流程中初始刷的,这里我们就先不展开了,我们先知道jni_get_media_player拿到的就是jni层播放器实例即可。 - 执行
ijkmp_prepare_async
,进入c层继续执行初始化,代码如下
cpp
static int ijkmp_prepare_async_l(IjkMediaPlayer *mp)
{
assert(mp);
MPST_RET_IF_EQ(mp->mp_state, MP_STATE_IDLE);
// MPST_RET_IF_EQ(mp->mp_state, MP_STATE_INITIALIZED);
MPST_RET_IF_EQ(mp->mp_state, MP_STATE_ASYNC_PREPARING);
MPST_RET_IF_EQ(mp->mp_state, MP_STATE_PREPARED);
MPST_RET_IF_EQ(mp->mp_state, MP_STATE_STARTED);
MPST_RET_IF_EQ(mp->mp_state, MP_STATE_PAUSED);
MPST_RET_IF_EQ(mp->mp_state, MP_STATE_COMPLETED);
// MPST_RET_IF_EQ(mp->mp_state, MP_STATE_STOPPED);
MPST_RET_IF_EQ(mp->mp_state, MP_STATE_ERROR);
MPST_RET_IF_EQ(mp->mp_state, MP_STATE_END);
assert(mp->data_source);
ijkmp_change_state_l(mp, MP_STATE_ASYNC_PREPARING);
msg_queue_start(&mp->ffplayer->msg_queue);
// released in msg_loop
ijkmp_inc_ref(mp);
mp->msg_thread = SDL_CreateThreadEx(&mp->_msg_thread, ijkmp_msg_loop, mp, "ff_msg_loop");
// msg_thread is detached inside msg_loop
// TODO: 9 release weak_thiz if pthread_create() failed;
int retval = ffp_prepare_async_l(mp->ffplayer, mp->data_source);
if (retval < 0) {
ijkmp_change_state_l(mp, MP_STATE_ERROR);
return retval;
}
return 0;
}
从代码中我们可以发现,它会先前置判断:
-
会先检查播放器IjkMediaPlayer实例是否为空
-
然后执行
MPST_RET_IF_EQ
,检查播放器状态到了那一步。那么我们就不得不提ijkplayer的播放器状态。其播放器流转状态为cpp1. MP_STATE_IDLE(空闲):这是播放器对象刚创建时的状态 2. MP_STATE_INITIALIZED(初始化):当播放器设置了数据源,例如一个媒体文件或者一个网络流媒体地址后,播放器会进入这个状态 3. MP_STATE_ASYNC_PREPARING(异步准备):当调用了ijkmp_prepare_async_l方法以后,播放器会进入这个状态。这个状态下,播放器会在这个后台线程中准备媒体数据 4. MP_STATE_PREPARED(准备完成):当媒体数据准备完成后,播放器会进入这个状态。在这个状态下,可以调用start方法开始播放 5. MP_STATE_STARTED(开始):当调用start方法后,播放器会进入这个状态,并开始播放媒体 6. MP_STATE_PAUSED(暂停):当调用pause方法后,播放器会进入这个状态。在这个状态下,可以调用start方法恢复播放 7. MP_STATE_COMPLETED(完成):当媒体播放完成后,播放器会进入这个状态 8. MP_STATE_STOPPED(停止):当调用了stop方法后,播放器会进入这个状态 9. MP_STATE_ERROR(错误):当播放器发生错误时,会进入这个状态 10. MP_STATE_END(结束):当播放器被释放后,会进入这个状态
我们刚刚
ijkmp_prepare_async
函数中就会前置判断播放器的状态是不是MP_STATE_INITIALIZED,那么他是怎么判断的呢。它是借助MPST_RET_IF_EQ
cpp#define MPST_RET_IF_EQ_INT(real, expected, errcode) \ do { \ if ((real) == (expected)) return (errcode); \ } while(0) #define MPST_RET_IF_EQ(real, expected) \ MPST_RET_IF_EQ_INT(real, expected, EIJK_INVALID_STATE)
对上MPST_RET_IF_EQ的代码实现,它不是定义了函数,而是使用了
define
的方式,define是用于创建符号常量,比如#define a b
其实就是被b重命名成a。所以使用MPST_RET_IF_EQ
函数的所有位置在经过预处理以后都会变成do{}while的函数,因此如果状态不满条件,就会退出当前函数,而不是退出MPST_RET_IF_EQ
,因为他本身也不是函数。所以
ijkmp_prepare_async
函数这段多次执行MPST_RET_IF_EQ
的逻辑就是:只有状态是MP_STATE_INITIALIZED
和MP_STATE_STOPPED
的时候会继续走初始化逻辑,否则就直接退出当前函数。 -
检查播放器视频源是否为空
做完前置判断以后,表示可以开始做播放器的状态流转了
- 首先先改变播放器状态为
ijkmp_change_state_l(mp, MP_STATE_ASYNC_PREPARING);
- 然后更新消息队列
msg_queue_start(&mp->ffplayer->msg_queue);
- 再之后增加当前播放器的计数
ijkmp_inc_ref(mp);
之后就开始 int retval = ffp_prepare_async_l(mp->ffplayer, mp->data_source);
执行异步初始化了,函数入参分别是播放器实例和视频源。ffp_prepare_async_l
函数里包含了一些其他初始化逻辑,此处我们先省略,主要将解码的关键部分,也是就是 VideoState *is = stream_open(ffp, file_name, NULL);
,其函数如下:
cpp
static VideoState *stream_open(FFPlayer *ffp, const char *filename, AVInputFormat *iformat)
{
VideoState *is;
is = av_mallocz(sizeof(VideoState));
if (!is)
return NULL;
...
is->video_refresh_tid = SDL_CreateThreadEx(&is->_video_refresh_tid, video_refresh_thread, ffp, "ff_vout");
is->read_tid = SDL_CreateThreadEx(&is->_read_tid, read_thread, ffp, "ff_read");
...
return is;
}
对照stream_open
的函数实现,我们可以发现,主要就是创建两个线程:read_thread
和video_refresh_thread
两个线程。
read_thread
线程内所做的事情其实分离流&创建流处理线程,主体流程其实可以参考音视频 ijkplayer源码解析系列2--如何解码图像的中的解封装流程,即
cpp
static int read_thread(void *arg)
{
...
// 创建解码上下文
ic = avformat_alloc_context();
...
// 分离出多个AVStream,即:音频、视频和字幕
err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);
...
// 下面三个主要是找到音频、视频、字幕流的index
if (!ffp->video_disable)
st_index[AVMEDIA_TYPE_VIDEO] =
av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,
st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);
if (!ffp->audio_disable)
st_index[AVMEDIA_TYPE_AUDIO] =
av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,
st_index[AVMEDIA_TYPE_AUDIO],
st_index[AVMEDIA_TYPE_VIDEO],
NULL, 0);
if (!ffp->video_disable && !ffp->subtitle_disable)
st_index[AVMEDIA_TYPE_SUBTITLE] =
av_find_best_stream(ic, AVMEDIA_TYPE_SUBTITLE,
st_index[AVMEDIA_TYPE_SUBTITLE],
(st_index[AVMEDIA_TYPE_AUDIO] >= 0 ?
st_index[AVMEDIA_TYPE_AUDIO] :
st_index[AVMEDIA_TYPE_VIDEO]),
NULL, 0);
...
// 下面三个if主要就是创建三个线程分别处理音频、视频、字幕流
/* open the streams */
if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
stream_component_open(ffp, st_index[AVMEDIA_TYPE_AUDIO]);
} else {
ffp->av_sync_type = AV_SYNC_VIDEO_MASTER;
is->av_sync_type = ffp->av_sync_type;
}
ret = -1;
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
ret = stream_component_open(ffp, st_index[AVMEDIA_TYPE_VIDEO]);
}
if (is->show_mode == SHOW_MODE_NONE)
is->show_mode = ret >= 0 ? SHOW_MODE_VIDEO : SHOW_MODE_RDFT;
if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) {
stream_component_open(ffp, st_index[AVMEDIA_TYPE_SUBTITLE]);
}
...
}
从上述代码中我们也可以发现核心便是解封装、分离流和stream_component_open
创建三个解码线程
cpp
static int stream_component_open(FFPlayer *ffp, int stream_index)
{
switch (avctx->codec_type) {
case AVMEDIA_TYPE_AUDIO:
...
// 创建音频解码线程
if ((ret = decoder_start(&is->auddec, audio_thread, ffp, "ff_audio_dec")) < 0)
goto out;
...
case AVMEDIA_TYPE_VIDEO:
...
// 创建视频解码线程
if ((ret = decoder_start(&is->viddec, video_thread, ffp, "ff_video_dec")) < 0)
goto out;
...
case AVMEDIA_TYPE_SUBTITLE:
if ((ret = decoder_start(&is->subdec, subtitle_thread, ffp, "ff_subtitle_dec")) < 0)
goto out;
break;
default:
break;
}
}
总结下上述流程可以形成这么个表格,帮助大家理解:
switch (avctx->codec_type) | 线程对应工作 | ||
---|---|---|---|
read_thread帧读取线程 | stream_component_open音频、视频、字幕 | 音频流 | 具体工作流程见下面的【audio_thread音频流流程】 |
视频流 | 1. decoder_init:在ffp->is->viddec内保存解码器上下文、队列 2. ffp->node_vdec = ffpipeline_open_video_decoder:关键执行ffpipenode_ffplay_vdec#ffpipenode_create_video_decoder_from_ffplay a. 创建IJKFF_Pipenode实例 b. 赋值func_destroy和func_run_sync c. av_freep释放编码器信息(video_codec_info) 3. decoder_start创建video_thread线程,具体工作流程见下面的【video_thread视频流流程】 | ||
字幕流 | 具体工作流程见下面的【subtitle_thread字幕流流程】 |
我们还是以视频流的解码线程为例进行分析,剩下两个流的处理流程也是相似的,细节的话,我们后续的文章中不断填坑。
3、视频解码线程分析
这一part的内容主要是decoder_start(&is->viddec, video_thread, ffp, "ff_video_dec")
中创建video_thread线程对应的内容:
cpp
static int video_thread(void *arg)
{
FFPlayer *ffp = (FFPlayer *)arg;
int ret = 0;
if (ffp->node_vdec) {
ret = ffpipenode_run_sync(ffp->node_vdec);
}
return ret;
}
前置封装了很多层,我们快进到核心流程:
cpp
ffpipenode_run_sync(ffp->node_vdec);
-> ff_ffpipenode#node->func_run_sync(node);
-> ffpipenode_ffplay_vdec#func_run_sync
-> ff_ffplay#ffp_video_thread
-> ff_ffplay#ffplay_video_thread
然后我们就来到了ff_ffplay#ffplay_video_thread
,其实现的也就是解封装和解码的流程,这里面逻辑其实相对负责,还包含了滤镜部分和帧同步相关的处理,为了降低理解成本,我们先把滤镜部分的代码忽略掉,简单剖析下这部分的代码:
cpp
static int ffplay_video_thread(void *arg)
{
FFPlayer *ffp = arg;
VideoState *is = ffp->is;
AVFrame *frame = av_frame_alloc();
...
ffp_notify_msg2(ffp, FFP_MSG_VIDEO_ROTATION_CHANGED, ffp_get_video_rotate_degrees(ffp));
if (!frame) {
return AVERROR(ENOMEM);
}
for (;;) {
ret = get_video_frame(ffp, frame);
if (ret < 0)
goto the_end;
if (!ret)
continue;
if (ffp->get_frame_mode) {
while (retry_convert_image <= MAX_RETRY_CONVERT_IMAGE) {
ret = convert_image(ffp, frame, (int64_t)pts, frame->width, frame->height);
if (!ret) {
convert_frame_count++;
break;
}
retry_convert_image++;
av_log(NULL, AV_LOG_ERROR, "convert image error retry_convert_image = %d\n", retry_convert_image);
}
retry_convert_image = 0;
if (ret || ffp->get_img_info->count <= 0) {
if (ret) {
av_log(NULL, AV_LOG_ERROR, "convert image abort ret = %d\n", ret);
ffp_notify_msg3(ffp, FFP_MSG_GET_IMG_STATE, 0, ret);
} else {
av_log(NULL, AV_LOG_INFO, "convert image complete convert_frame_count = %d\n", convert_frame_count);
}
goto the_end;
}
av_frame_unref(frame);
continue;
}
ret = queue_picture(ffp, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
av_frame_unref(frame);
the_end:
av_log(NULL, AV_LOG_INFO, "convert image convert_frame_count = %d\n", convert_frame_count);
av_frame_free(&frame);
return 0;
}
对照上述代码,其实就是一个for(;;)循环,循环的去执行视频的解码流程:
-
get_video_frame(ffp, frame)
主要就是利用avcodec_send_packet
和avcodec_receive_frame
解码和解封装的流程,将解码后的数据放到frame里面 -
convert_image
执行图片尺寸转换 -
queue_picture
主要是把frame里面的数据更新到ffp->is->pictq
队列里,并通知等待线程-
拿到
pictq
队列汇总空白frame
:vp = frame_queue_peek_writable(&is->pictq)
,从ffp->is->pictq
的frame队列中拿出队列中收到空白frame -
填充
vp->bmp
:SDL_VoutFillFrameYUVOverlay(vp->bmp, src_frame)
执行到ijksdl_vout_overlay_ffmpeg#func_fill_frame
,此时会有两种可能-
vp->bmp
和src_frame
的格式一样&&格式为AV_PIX_FMT_YUV420P|AV_PIX_FMT_YUV444P10LE
,保存src_frame
的引用,并保存对应的参数cpp// 将frame数据引用放到opaque->linked_frame里 av_frame_ref(opaque->linked_frame, frame); // opaque->linked_frame的dat和linesize保存到overlay的成员变量里pixels和pitches overlay_fill(overlay, opaque->linked_frame, opaque->planes); if (need_swap_uv) FFSWAP(Uint8*, overlay->pixels[1], overlay->pixels[2]);
-
其他:创建或拿到AVFrame,然后转换
src_frame
的数据做格式转换,然后存在vp->bmp
里cpp// managed frame AVFrame* managed_frame = opaque_obtain_managed_frame_buffer(opaque); if (!managed_frame) { ALOGE("OOM in opaque_obtain_managed_frame_buffer"); return -1; } overlay_fill(overlay, opaque->managed_frame, opaque->planes); // setup frame managed for (int i = 0; i < overlay->planes; ++i) { swscale_dst_pic.data[i] = overlay->pixels[i]; swscale_dst_pic.linesize[i] = overlay->pitches[i]; } if (need_swap_uv) FFSWAP(Uint8*, swscale_dst_pic.data[1], swscale_dst_pic.data[2]); if (use_linked_frame) { // do nothing } else if (ijk_image_convert(frame->width, frame->height, dst_format, swscale_dst_pic.data, swscale_dst_pic.linesize, frame->format, (const uint8_t**) frame->data, frame->linesize)) { opaque->img_convert_ctx = sws_getCachedContext(opaque->img_convert_ctx, frame->width, frame->height, frame->format, frame->width, frame->height, dst_format, opaque->sws_flags, NULL, NULL, NULL); if (opaque->img_convert_ctx == NULL) { ALOGE("sws_getCachedContext failed"); return -1; } sws_scale(opaque->img_convert_ctx, (const uint8_t**) frame->data, frame->linesize, 0, frame->height, swscale_dst_pic.data, swscale_dst_pic.linesize); if (!opaque->no_neon_warned) { opaque->no_neon_warned = 1; ALOGE("non-neon image convert %s -> %s", av_get_pix_fmt_name(frame->format), av_get_pix_fmt_name(dst_format)); } }
-
-
pictq
队列指针和数据量更新,并通知等待线程更新了。
-