继续创造,加速成长!这是我参与「日新方案 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
办法,该办法履行了以下操作:
- 创立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);
- 创立上面已实例化的
ffplayer
的图像烘托目标 - 创立平台相关的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 学习解码视频获取到过程。
扼要的过程如下:
- 创立上下文结构体
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
数组中找到需求解码的音频和视频,并记载下标。
- 在ijkplayer中,是经过
stream_component_open
函数内部找到解码器(运用硬解或许软解)并创立相应线程的。
-
avcodec_find_decoder
寻找解码器 -
avcodec_open2
初始化一个音视频解码器的AVCodecContext
- 读取媒体数据,得到的是音视频别离的解码前数据
该字段坐落ff_ffplay.c 的3515行
ret = av_read_frame(ic, pkt);
- 将音视频数据别离送入相应的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_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 解析