ijkplayer源码解析系列3--解码流程

还没有看过前置知识的同学可以先从下面的链接点过去:

音视频 ijkplayer 源码解析系列1--播放器介绍

音视频 ijkplayer源码解析系列2--如何解码图像

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);
}

其核心流程就是:

  1. jni_get_media_player获取播放器IjkMediaPlayer实例,这个IjkMediaPlayer是在java层播放器初始化的时候调用jni_set_media_player的流程中初始刷的,这里我们就先不展开了,我们先知道jni_get_media_player拿到的就是jni层播放器实例即可。
  2. 执行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;
}

从代码中我们可以发现,它会先前置判断:

  1. 会先检查播放器IjkMediaPlayer实例是否为空

  2. 然后执行MPST_RET_IF_EQ,检查播放器状态到了那一步。那么我们就不得不提ijkplayer的播放器状态。其播放器流转状态为

    cpp 复制代码
    1. 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_INITIALIZEDMP_STATE_STOPPED的时候会继续走初始化逻辑,否则就直接退出当前函数。

  3. 检查播放器视频源是否为空

做完前置判断以后,表示可以开始做播放器的状态流转了

  1. 首先先改变播放器状态为 ijkmp_change_state_l(mp, MP_STATE_ASYNC_PREPARING);
  2. 然后更新消息队列 msg_queue_start(&mp->ffplayer->msg_queue);
  3. 再之后增加当前播放器的计数 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_threadvideo_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(;;)循环,循环的去执行视频的解码流程:

  1. get_video_frame(ffp, frame)主要就是利用avcodec_send_packetavcodec_receive_frame解码和解封装的流程,将解码后的数据放到frame里面

  2. convert_image执行图片尺寸转换

  3. queue_picture主要是把frame里面的数据更新到ffp->is->pictq队列里,并通知等待线程

    1. 拿到pictq队列汇总空白framevp = frame_queue_peek_writable(&is->pictq),从ffp->is->pictq的frame队列中拿出队列中收到空白frame

    2. 填充vp->bmpSDL_VoutFillFrameYUVOverlay(vp->bmp, src_frame)执行到ijksdl_vout_overlay_ffmpeg#func_fill_frame,此时会有两种可能

      1. vp->bmpsrc_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]);
      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));
          }
        }
    3. pictq队列指针和数据量更新,并通知等待线程更新了。

相关推荐
dvlinker1 天前
【音视频开发】使用支持硬件加速的D3D11绘图遇到的绘图失败与绘图崩溃问题的记录与总结
音视频开发·c/c++·视频播放·d3d11·d3d11绘图模式
音视频牛哥7 天前
Android平台GB28181实时回传流程和技术实现
音视频开发·视频编码·直播
音视频牛哥8 天前
RTMP、RTSP直播播放器的低延迟设计探讨
音视频开发·视频编码·直播
音视频牛哥12 天前
电脑共享同屏的几种方法分享
音视频开发·视频编码·直播
x007xyz2 个月前
前端纯手工绘制音频波形图
前端·音视频开发·canvas
音视频牛哥2 个月前
Android摄像头采集选Camera1还是Camera2?
音视频开发·视频编码·直播
九酒2 个月前
【harmonyOS NEXT 下的前端开发者】WAV音频编码实现
前端·harmonyos·音视频开发
音视频牛哥2 个月前
结合GB/T28181规范探讨Android平台设备接入模块心跳实现
音视频开发·视频编码·直播
哔哩哔哩技术2 个月前
自研点直播转码核心
音视频开发