继续创造,加速成长!这是我参与「日新方案 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 解析