本篇会有很多源代码,请注意阅读每行代码上面的注释。
先简单介绍IjkPlayer中AvPacket是如何获取的???
在ff_ffplay.c的read_thread线程,会循环的调用av_read_frame函数,不停的从流中获得AvPacket数据。并且read_thread线程还会创建4个子线程:audio_thread、video_thread、subtitle_thread、aout_thread,而前三个线程的功能:分别将音频、视频、字幕三个类型的AvPacket数据解码为Frame(帧数据)。所以本篇内容将聊这三个线程之一:audio_thread。(三个线程流程都一样,举一反三)
如果想知道read_thread线程如何创建4个子线程的请看:Android ---- Ijkplayer阅读native层源码之IjkMediaPlayer_prepareAsync
audio_thread:
static int audio_thread(void *arg)
{//循环do {// 将音频队列的存储个数、大小、和音频能播放多长时间这些信息存入,用于UI展示 // FFPlayer.stat.audio_cacheffp_audio_statistic_l(ffp);// 注意:// 将缓存队列中一个AVPacket数据解码为Frame数据,解码出来的最新一帧存放在frame中。解码成功返回 1if ((got_frame = decoder_decode_frame(ffp, &is->auddec, frame, NULL)) < 0)// 解码一帧成功if (got_frame) {tb = (AVRational){1, frame->sample_rate};// 如果开启了音视频精准同步校验,拖动进度条后,第一帧数据才会去校验,默认关闭if (ffp->enable_accurate_seek && is->audio_accurate_seek_req && !is->seek_req) {// 获得该帧的显示时间frame_pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);now = av_gettime_relative() / 1000;if (!isnan(frame_pts)) {// 计算该帧播放时间samples_duration = (double) frame->nb_samples / frame->sample_rate;// 计算播放完该帧的实际时间audio_clock = frame_pts + samples_duration;//is->accurate_seek_aframe_pts = audio_clock * 1000 * 1000;// 拿到当前播放的进度条值----播放时间audio_seek_pos = is->seek_pos;// 计算解码音频帧的显示完成时间与此时的进度条对应时间值相减,即显示完该帧后,进度条需要前进多少时间deviation = llabs((int64_t)(audio_clock * 1000 * 1000) - is->seek_pos);// 如果当前音频帧慢于播放的进度或者大于了能接受的最大偏差,即解码速度太慢或者太快,进入if ((audio_clock * 1000 * 1000 < is->seek_pos ) || deviation > MAX_DEVIATION) {if (is->drop_aframe_count == 0) {SDL_LockMutex(is->accurate_seek_mutex);// 如果还未开始播放if (is->accurate_seek_start_time <= 0 && (is->video_stream < 0 || is->video_accurate_seek_req)) {is->accurate_seek_start_time = now;}SDL_UnlockMutex(is->accurate_seek_mutex);}// 将该帧丢弃,统计丢弃帧总数加1is->drop_aframe_count++;// 如果拖动了进度条,进入while (is->video_accurate_seek_req && !is->abort_request) {// 拿到最新解码的视频帧的显示完成时间int64_t vpts = is->accurate_seek_vframe_pts;// 音视频解码帧显示时间的差值deviation2 = vpts - audio_clock * 1000 * 1000;// 计算最新解码的视频帧显示完成时间与此时的进度条对应时间值相减,即显示完该帧后,进度条需要前进多少时间deviation3 = vpts - is->seek_pos;// 上面的大前提:音频解码速度比进度条慢或者太快// 如果视频比音频快但是视频比进度条慢,// 即进度条>视频>音频if (deviation2 > -100 * 1000 && deviation3 < 0) {break;} else {// 睡眠20毫秒// 进度条>音频太慢>视频太慢// 音频太快>进度条>视频太慢// 音频太快>视频太快>进度条// 视频太快>音频太快>进度条 感觉这种情况有问题// 视频太快>进度条>音频太慢 感觉这种情况有问题av_usleep(20 * 1000);}now = av_gettime_relative() / 1000;if ((now - is->accurate_seek_start_time) > ffp->accurate_seek_timeout) {break;}}} else {// 进度条<音频<MAX_DEVIATION// 前面才赋值,肯定相等if (audio_seek_pos == is->seek_pos) {is->drop_aframe_count = 0;SDL_LockMutex(is->accurate_seek_mutex);is->audio_accurate_seek_req = 0;SDL_CondSignal(is->video_accurate_seek_cond);if (audio_seek_pos == is->seek_pos && is->video_accurate_seek_req && !is->abort_request) {SDL_CondWaitTimeout(is->audio_accurate_seek_cond, is->accurate_seek_mutex, ffp->accurate_seek_timeout);} else {ffp_notify_msg2(ffp, FFP_MSG_ACCURATE_SEEK_COMPLETE, (int)(audio_clock * 1000));}// 存在此时用户拖动了进度条,导致is->seek_pos变化if (audio_seek_pos != is->seek_pos && !is->abort_request) {is->audio_accurate_seek_req = 1;SDL_UnlockMutex(is->accurate_seek_mutex);av_frame_unref(frame);continue;}SDL_UnlockMutex(is->accurate_seek_mutex);}}} else {audio_accurate_seek_fail = 1;}is->accurate_seek_start_time = 0;audio_accurate_seek_fail = 0;}// 从缓存队列中获得一个存储对象(Frame),并将其给afif (!(af = frame_queue_peek_writable(&is->sampq)))goto the_end;// 初始化存储对象af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);af->pos = frame->pkt_pos;af->serial = is->auddec.pkt_serial;af->duration = av_q2d((AVRational){frame->nb_samples, frame->sample_rate});//将刚解码的一帧数据frame 拷贝到af->frame(存储对象)中,并清空frameav_frame_move_ref(af->frame, frame);// 发起一个有新缓存数据信息号。frame_queue_push(&is->sampq);}} while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF);}
上面代码先调用decoder_decode_frame函数去完成解码一个AvPacket数据,然后处理解码成功后的一帧数据。具体看上面的注释
解码成功后的处理:
1. 如果开启了音视频同步校验,拖动进度条后,解码成功的音频首帧会和视频的首帧进行同步。默认该功能是关闭的
2. 将解码成功的帧存入缓存队列(is->sampq)中
decoder_decode_frame:
// 将解码一个AVPacket数据解码为一帧Frame数据,解码的一帧放入变量frame中,并返回 1
static int decoder_decode_frame(FFPlayer *ffp, Decoder *d, AVFrame *frame, AVSubtitle *sub) {int ret = AVERROR(EAGAIN);// 死循环,for (;;) {AVPacket pkt;if (d->queue->serial == d->pkt_serial) {// 通过avcodec_receive_frame拿到上一个AVPacket解码出来的一帧数据do {if (d->queue->abort_request)return -1;switch (d->avctx->codec_type) {case AVMEDIA_TYPE_VIDEO:省略。。。。。case AVMEDIA_TYPE_AUDIO:// FFmpeg基本操作:从解码器中拿到解码成功后的一帧数据。(上一个AvPacket的数据)。=0表示成功ret = avcodec_receive_frame(d->avctx, frame);// 解码成功,按照系统时间基,重新计算该帧的播放时间if (ret >= 0) {AVRational tb = (AVRational){1, frame->sample_rate};// 如果该帧有pts,则转换其时间基if (frame->pts != AV_NOPTS_VALUE)frame->pts = av_rescale_q(frame->pts, av_codec_get_pkt_timebase(d->avctx), tb);else if (d->next_pts != AV_NOPTS_VALUE)frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb);if (frame->pts != AV_NOPTS_VALUE) {// 预测下一帧的播放时间,并设置给解码器d->next_pts = frame->pts + frame->nb_samples;d->next_pts_tb = tb;}}break;default:break;}// 如果解码器更新if (ret == AVERROR_EOF) {d->finished = d->pkt_serial;avcodec_flush_buffers(d->avctx);return 0;}// 解码成功if (ret >= 0)return 1;} while (ret != AVERROR(EAGAIN));}do {// 如果需要解码的队列中没有数据,即缓存为空if (d->queue->nb_packets == 0)// 发送一个空信号SDL_CondSignal(d->empty_queue_cond);// 是否有正在解码的数据。if (d->packet_pending) {// 将d->pkt拷贝到pkt中,清空d->pktav_packet_move_ref(&pkt, &d->pkt);d->packet_pending = 0;} else {// 获得一个AVPacket数据,并放入pkt中,用于解码if (packet_queue_get_or_buffering(ffp, d->queue, &pkt, &d->pkt_serial, &d->finished) < 0)return -1;}} while (d->queue->serial != d->pkt_serial);// 如果缓存队列被刷新了----被清空了if (pkt.data == flush_pkt.data) {avcodec_flush_buffers(d->avctx);d->finished = 0;d->next_pts = d->start_pts;d->next_pts_tb = d->start_pts_tb;} else {// 如果是字幕数据if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {// 省略。。。。} else {// FFmpeg基本操作:将一帧的AVPacket数据包放入解码器解码if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) {d->packet_pending = 1;// 将pkt拷贝到d->pkt,然后清空pktav_packet_move_ref(&d->pkt, &pkt);}}av_packet_unref(&pkt);}}
}
上面的核心就是调用FFmpeg 提供的两个方法:avcodec_send_packet、avcodec_receive_frame。
一个将AvPacket数据送到解码器,一个从解码器拿到解码成功后的一帧数据。是不是很简单。那就在简单叙述下上面代码的功能:首先先调用avcodec_receive_frame去检查解码器中是否还有解码成功的数据没取完(因为一个音频的AvPacket可能解码出多个帧数据),如果有,取出一帧,然后返回。如果没有,调用packet_queue_get_or_buffering获得一个缓存的AvPacket 数据,然后通过avcodec_send_packet函数将其传送给解码器解码。