初始化播放器
// 注册编解码器
avcodec_register_all();
// 初始化libavformat,注册所有的复用器,解复用器和协议。
av_register_all();
// 初始化openssl库
avformat_network_init();
// 设置日志回调
av_log_set_callback(ffp_log_callback_brief);
播放准备
从ijkplayer的ffp_prepare_async_l
函数开始:
首先会打开音频输出初始化,安卓上就是配置AudioTrack的Jni反射函数,这里还没有创建AudioTrack,只是配置好函数指针,指向ijksdl_aout_android_audiotrack.c
文件里面定义的AudioTrack反射函数。
初始化帧队列
在函数stream_open
中。
初始化帧队列,帧队列在ijk中是结构体FrameQueue
,有三个帧队列,分别是图片帧队列、字幕帧队列、音频帧队列。他们存放的都是解码后的数据。
队列内部是采用数组实现的,他们的大小分别是:
图片帧队列长度:3
音频帧度列长度:9
字幕帧队列长度:16
保存在帧队列中的数据结构来自ffmpeg,是AVFrame结构体,每个结构体都预先分配好了内存空间。采用函数av_frame_alloc
for (i = 0; i < f->max_size; i++)
if (!(f->queue[i].frame = av_frame_alloc()))
return AVERROR(ENOMEM);
初始化包队列
包队列中保存的ffmpeg结构体是AVPacket,表示未解码的压缩数据,即将送入到解码器解码,采用链表的结构来保存。
包队列也是分为3个:视频包队列,音频包队列,字幕包队列。
进入IO线程,探测音视频数据信息
// 初始化AVFormatContext
ic = avformat_alloc_context();
//找到视频格式:AVInputFormat
if (ffp->iformat_name)
is->iformat = av_find_input_format(ffp->iformat_name);
// 打开输入流并读取音视频header信息
err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts);
// 遍历当前流资源中的所有流,所有的音视频流都找到对应的流信息
avformat_find_stream_info(ic, opts);
// 遍历所有的流,并找到第一个h264视频流,并选中那个视频流。
// 找到最佳的流,把音频、视频、字幕流的轨道位置都记录下来。
av_find_best_stream();
初始化解码器
对音频、视频、字幕轨道分别都调用stream_component_open
。
// 解码器上下文
AVCodecContext *avctx;
// 解码器
AVCodec *codec = NULL;
avctx = avcodec_alloc_context3(NULL);
// 将AVCodecParameters中的变量赋值给AVCodecContext
ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);
// 设置时间基准
av_codec_set_pkt_timebase(avctx, ic->streams[stream_index]->time_base);
// 找到解码器
codec = avcodec_find_decoder(avctx->codec_id);
// 如果有强制解码器,就指定强制的
if (forced_codec_name)
codec = avcodec_find_decoder_by_name(forced_codec_name);
// 把解码器的id赋值给解码器上下文
avctx->codec_id = codec->id;
// 根据解码器类型,配置解码器信息
switch(avctx->codec_type){
case AVMEDIA_TYPE_AUDIO:
// 初始化音频av_filter
// 打开音频输出器,Android平台上是jni反射到AudioTrack对象。
// 解码器初始化 调用:decoder_init
// 启动音频解码器线程
if ((ret = decoder_start(&is->auddec, audio_thread, ffp, "ff_audio_dec")) < 0)
goto out;
break;
case AVMEDIA_TYPE_VIDEO:
// 解码器初始化 调用:decoder_init
// 打开视频解码器,这一步ijkplayer封装做了一个抽象封装,这个封装后面再单独抽成一篇文章来分析。
// 启动视频解码器线程
break;
case AVMEDIA_TYPE_SUBTITLE:
// 解码器初始化 调用:decoder_init
// 启动字幕解码器线程
break;
}
// 解码器初始化
static void decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond) {
memset(d, 0, sizeof(Decoder));
d->avctx = avctx;
d->queue = queue;
d->empty_queue_cond = empty_queue_cond;
d->start_pts = AV_NOPTS_VALUE;
d->first_frame_decoded_time = SDL_GetTickHR();
d->first_frame_decoded = 0;
SDL_ProfilerReset(&d->decode_profiler, -1);
}
启动视频解码器线程这块再多说一下,如果是软解,线程执行的函数是ff_ffplay.c文件的ffplay_video_thread
函数,如果是硬解,执行的是ffpipenode_android_mediacodec_vdec.c文件的func_run_sync
函数。
准备完成
此时播放器准备完成,如果有Ijk的start-on-prepared
开关,则会直接开始播放,不需要调用start。默认是关闭的。
那么此时执行如下代码段:
ffp->prepared = true;
//发送PREPARED回调
ffp_notify_msg1(ffp, FFP_MSG_PREPARED);
if (!ffp->render_wait_start && !ffp->start_on_prepared) {
while (is->pause_req && !is->abort_request) {
SDL_Delay(20);// 经过测试,只要设置uri,然后不调用start,就会一直卡在这里。
}
}
即不断地在这个IO线程中休眠。SDL_Delay在内部调用的是nanosleep
函数,和Java的Thread.sleep的含义一样。
开始播放
下载包数据
开始播放改变的是VideoState结构体的int pause_req
变量,然后在多个地方都会检查这个变量,表示是否播放暂停了。这里我们接着正常第一次播放的流程来看,那么还是衔接上面的流程。
在上面发送了PREPARED回调告诉上层播放器底部已经准备好可以开始播放后,io线程就进入了while循环并每隔20毫秒就检查一下这个pause_req
的标志位,看看上层是否开始播放了。当开始播放之后:
static int read_thread(void *arg)
{
// ...
for(;;){
// 我们进入了一个for循环,这里是io线程不断拉流的主体逻辑。
//如果是seek 请求
if (is->seek_req) {
//ffmepg 中处理seek
ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);
// 清空音频、视频、字幕包队列。
packet_queue_flush(&is->audioq);
packet_queue_put(&is->audioq, &flush_pkt);
}
// 如果队列满了,不再读取,wait 10 ms,并continue进入下一次循环。
// 可能的原因是,包数据没有被解码器消费掉,一般是视频暂停了,或者视频解码过慢。
// 处理播放结束
// 调用ffmpeg函数读取包数据到pkt里面。
ret = av_read_frame(ic, pkt);
// 判断pkt的stream类型,然后放到对应的音视频包队列里面。
packet_queue_put(&is->audioq, pkt);
}
}