
初始化播放器
// 注册编解码器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线程,探测音视频数据信息
// 初始化AVFormatContextic = avformat_alloc_context();//找到视频格式:AVInputFormatif (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中的变量赋值给AVCodecContextret = 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 中处理seekret = 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);}}
