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
方法,该方法执行了以下操作:
// 创建对象
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);
ffplayer
的图像渲染对象在进行播放时,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 的一个过程。
SDL_CreateThreadEx(&is->_read_tid, read_thread, ffp, "ff_read")
就可以翻译为创建名称为ff_read 的线程,线程执行的方法为read_thread.
数据读取操作如上所述,是在read_thread 函数中实现的。大致的操作和我之前使用ffmpeg 解码本地视频的操作是一样的,可以从我的ffmpeg 学习解码视频获取到步骤。
简要的步骤如下:
ic = avformat_alloc_context();
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);
探测媒体类型,解封装,并给音视频流的AVStream 结构体进行赋值
err = avformat_find_stream_info(ic, opts);
循环遍历streams
流,方便分别针对音频和视频流做出解析
ijkplayer
在中间穿插了缓冲、超时等设定,但都是从AVFormatContext
的 nb_streams
数组中找到需要解码的音频和视频,并记录下标。
stream_component_open
函数内部找到解码器(使用硬解或者软解)并创建相应线程的。avcodec_find_decoder
寻找解码器avcodec_open2
初始化一个音视频解码器的AVCodecContext
该字段位于ff_ffplay.c 的3515行
ret = av_read_frame(ic, pkt);
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);
}
}
ijkplayer 在视频解码上支持软解码和硬解码,可在开始之前配置有限使用的解码方式,播放过程中不可切换。iOS 平台上硬解码使用VideoToolbox
,不过音频解码只支持软解。
在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 方法,就会有限打开硬件解码器,如果硬解打开失败,就自动切换成软解码
解码操作是使用avcodec_decode_video2
来完成的,但是在ffmpeg3.0+ 以后,该函数被标记为将要废弃,需要转而使用avcodec_receive_frame
和 avcodec_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 解析