整章目录:Android------- IjkPlayer 源码学习目录
本篇会有很多源代码,请注意阅读每行代码上面的注释。
本篇介绍的主要内容为上图红框圈起部分:
先简单介绍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。(三个线程流程都一样,举一反三)
补充视频解码:Android --- IjkPlayer 阅读native层源码之将AvPacket解码为一帧视频(八)补充
如果想知道read_thread线程如何创建4个子线程的请看:Android ---- Ijkplayer阅读native层源码之IjkMediaPlayer_prepareAsync
audio_thread:
static int audio_thread(void *arg)
{
//循环
do {
// 将音频队列的存储个数、大小、和音频能播放多长时间这些信息存入,用于UI展示
// FFPlayer.stat.audio_cache
ffp_audio_statistic_l(ffp);
// 注意:
// 将缓存队列中一个AVPacket数据解码为Frame数据,解码出来的最新一帧存放在frame中。解码成功返回 1
if ((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);
}
// 将该帧丢弃,统计丢弃帧总数加1
is->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),并将其给af
if (!(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(存储对象)中,并清空frame
av_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. 如果开启了音视频同步校验,拖动进度条后,解码成功的音频首帧会和视频的首帧进行同步。默认该功能是关闭的
将解码成功的帧存入缓存队列(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->pkt
av_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,然后清空pkt
av_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函数将其传送给解码器解码。