iOS ijkplayer 源码学习

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第15天

iOS ijkplayer 源码学习

ijkplayer 在iOS 中的调用主要是通过其IJKFFMoviePlayerController 控制器来完成,其中设置SDLView等界面适配可见具体的参数设定。本文章主要是用于将自己所学习到的知识进行一个总结,加深自身的印象。ijkplayer 默认使用的是软解码操作,也就是用ffmpeg 调用GPU进行解码,如果需要使用系统自带的硬解码操作,则需要进行单独的配置。

一、初始化流程

- (id)initWithContentURL:(NSURL *)aUrl withOptions:(IJKFFOptions *)options 是上层调用ijkplayer 的一个入口,我们从这里开始解析。

实现音视频的播放,主要通过的是ijkMediaPlayer类进行,ijkMediaPlayer是一个结构体

struct IjkMediaPlayer {
    // 使用ijkmp_create 创建player 后的一个计时器,创建一个player。该数值+1
    volatile int ref_count;
    // 线程锁,用于保护编解码线程
    pthread_mutex_t mutex;
    // ffmpeg 底层的播放类
    FFPlayer *ffplayer;
    // msg_loop是用于ijkplayer底层往app调用者通知各种事件的一个函数。以便于业务层根据事件做各种调整
    int (*msg_loop)(void*);
    // 记录创建消息循环ijkmp_msg_loop 函数的线程
    SDL_Thread *msg_thread;
    /*从SDL_CreateThreadEx(&mp->_msg_thread,ijkmp_msg_loop, mp,"ff_msg_loop")可以看到,其实上面的msg_thread是指向填充数据过后的_msg_thread实体。SDL_Thread里面的数据来源于SDL_CreateThreadEx函数传入。*/
    SDL_Thread _msg_thread;
    // 播放状态ijkmp_change_state_l 函数专门用来改变mp_state 状态值 
    int mp_state;
    // 存储上层传入的url
    char *data_source;
 
    void *weak_thiz;

    int restart;
    int restart_from_beginning;
    int seek_req;
    // 记录ijkmp_seek_to 要拖动到第几毫秒值
    long seek_msec;
};
复制代码

上述OC 方法主要调用了ijkplayer_ios中的ijkmp_ios_create方法,该方法执行了以下操作:

  1. 创建ijkMediaPlayer对象
// 创建对象
 IjkMediaPlayer *mp = (IjkMediaPlayer *) mallocz(sizeof(IjkMediaPlayer));
    ......
    // 创建ffplayer 对象
    mp->ffplayer = ffp_create();
    // 指定消息处理函数
    mp->msg_loop = msg_loop;
    // 指定了解码方式
    mp->ffplayer->pipeline = ffpipeline_create_from_ios(mp->ffplayer);
复制代码
  1. 创建上面已实例化的ffplayer 的图像渲染对象
  2. 创建平台相关的IJKFF_Pipeline对象,包括视频解码以及音频输出部分

二、核心代码

在进行播放时,ijkplayer 会调用ijkmp_prepare_async_l,其中的关键性代码是ffp_prepare_async_l 方法。 在此方法中打印了一些ffplayer 的参数,然后调用了stream_open进行解码操作。

static VideoState *stream_open(FFPlayer *ffp, const char *filename, AVInputFormat *iformat)
{  
    ......           
    /* start video display */
    if (frame_queue_init(&is->pictq, &is->videoq, ffp->pictq_size, 1) < 0)
        goto fail;
    if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0)
        goto fail;

    if (packet_queue_init(&is->videoq) < 0 ||
        packet_queue_init(&is->audioq) < 0 )
        goto fail;

    ......
    
    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");
    
    ......
}
复制代码

从代码中可得,其实stream_open 就是一个将AVStream ->AVPacket->AVFrame 的一个过程。

  • 创建存放video/audio解码前数据的videoq/audioq
  • 创建存放video/audio解码后数据的pictq/sampq
  • 创建读数据线程read_thread
  • 创建视频渲染线程video_refresh_thread

SDL_CreateThreadEx(&is->_read_tid, read_thread, ffp, "ff_read") 就可以翻译为创建名称为ff_read 的线程,线程执行的方法为read_thread.

2.1 数据读取

数据读取操作如上所述,是在read_thread 函数中实现的。大致的操作和我之前使用ffmpeg 解码本地视频的操作是一样的,可以从我的ffmpeg 学习解码视频获取到步骤。 简要的步骤如下:

  1. 创建上下文结构体
ic = avformat_alloc_context();
复制代码
  1. 设置中断函数,如果出错就退出
ic->interrupt_callback.callback = decode_interrupt_cb;
ic->interrupt_callback.opaque = is;
复制代码

3.打开文件、探测协议类型,如果是网络文件则创建网络连接

// 先通过名称去找到AVInputFormat 文件输入类型
is->iformat = av_find_input_format(ffp->iformat_name);
// 如果上述文件找不到,相当于第三个入参为null,调用下面的方法也能找到,只是如果第三个入参指明,打开文件探测协议类型就更加快捷一些
err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);
复制代码
  1. 探测媒体类型,解封装,并给音视频流的AVStream 结构体进行赋值 err = avformat_find_stream_info(ic, opts);

  2. 循环遍历streams流,方便分别针对音频和视频流做出解析

  ijkplayer 在中间穿插了缓冲、超时等设定,但都是从AVFormatContextnb_streams 数组中找到需要解码的音频和视频,并记录下标。

  1. 在ijkplayer中,是通过stream_component_open函数内部找到解码器(使用硬解或者软解)并创建相应线程的。
  • avcodec_find_decoder 寻找解码器
  • avcodec_open2 初始化一个音视频解码器的AVCodecContext
  1. 读取媒体数据,得到的是音视频分离的解码前数据

该字段位于ff_ffplay.c 的3515行

ret = av_read_frame(ic, pkt);
复制代码
  1. 将音视频数据分别送入相应的queue中,在送入之前会检查数据包是不是在播放的范围之内,然后进行排队送入,下面是送入的具体代码
if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {
        packet_queue_put(&is->audioq, pkt);
    } else if (pkt->stream_index == is->video_stream && pkt_in_play_range
               && !(is->video_st && (is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC))) {
        packet_queue_put(&is->videoq, pkt);
    } else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
        packet_queue_put(&is->subtitleq, pkt);
    } else {
        av_packet_unref(pkt);
    }
}
复制代码
2.2 音视频解码

  ijkplayer 在视频解码上支持软解码和硬解码,可在开始之前配置有限使用的解码方式,播放过程中不可切换。iOS 平台上硬解码使用VideoToolbox,不过音频解码只支持软解。

2.2.1 视频解码方式的选择

在ijkplayer 中使用软解码和硬解码调用方法在上文已经说了位于stream_component_open中:

static int stream_component_open(FFPlayer *ffp, int stream_index)
{
    ......
    codec = avcodec_find_decoder(avctx->codec_id);
    ......
    if ((ret = avcodec_open2(avctx, codec, &opts)) < 0) {
        goto fail;
    }
    // 如果是视频帧,就执行下面操作
    case AVMEDIA_TYPE_VIDEO:
        ......
        ?/ 初始化
        decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
        // 
        ffp->node_vdec = ffpipeline_open_video_decoder(ffp->pipeline, ffp);
        if (!ffp->node_vdec)
            goto fail;
            // 开始解码
        if ((ret = decoder_start(&is->viddec, video_thread, ffp, "ff_video_dec")) < 0)
            goto out;       
    ......
}
复制代码

首先会打开ffmpeg 的解码器,然后通过ffpipeline_open_video_decoder创建IJKFF_Pipenode.其内部就是如果配置了ffp 的videotoolbox 方法,就会有限打开硬件解码器,如果硬解打开失败,就自动切换成软解码

2.2.2 音视频解码

解码操作是使用avcodec_decode_video2来完成的,但是在ffmpeg3.0+ 以后,该函数被标记为将要废弃,需要转而使用avcodec_receive_frameavcodec_send_packet 来进行操作。这个需要注意,同一些旧版本的解析文章是有差异的,当前新的ijkplayer 使用的正是receive_frame 方法。

在ijkplayer 中,视频的解码队列是video_thread ,audio 的解码线程为audio_thread. 其调用位于ff_ffplayer.c 中的stream_component_open方法中通过调用decode_start 方法开始的。

  • 视频解码线程
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;
}
复制代码

ffpipenode_run_sync 调用的是IJKFF_Pipenode对象中的func_run_sync 这个取决于播放前配置的软硬解。 具体的代码调用也位于Strem_component_open 内的ffp->node_vdec = ffpipeline_open_video_decoder(ffp->pipeline, ffp);方法。我画了一个流程图来帮助大家理解。 如图:

[外链图片转存失败(img-r800nRVR-1567842284110)(evernotecid://44BEC8F1-2D61-4D72-8317-0554A2C29B3C/appyinxiangcom/14389767/ENResource/p36)]

  • 音频解码线程 音频解码线程也是走的decoder_decode_frame
static int audio_thread(void *arg)
{
do {
        ffp_audio_statistic_l(ffp);
        if ((got_frame = decoder_decode_frame(ffp, &is->auddec, frame, NULL)) < 0)
            goto the_end;
            
   }
}
复制代码

文章中存在的引用出处:

金山视频云ijkplayer实现 ijkMediaPlayer 解析

猜你喜欢

转载自juejin.im/post/7157304300903858190