元のアドレスから転載: https://segmentfault.com/a/1190000042611851
以前、ffplay のデバッグ環境の統合、ffplay の全体的なアーキテクチャ、ffplay の読み取りスレッド、およびその他の関連コンテンツを紹介しましたが、今日は ffplay デコード スレッドのワークフローを紹介します。
ビデオのデコードとオーディオのデコードのプロセスはほぼ同じであるため、この記事では主にビデオのデコード スレッドの内容を紹介し、字幕のデコードについては無視します。
この写真から始めましょう:
図からわかるように、デコード スレッドの主な作業は、デコードするリソース パケットをキューから取り出し、それをデコーダに送信し、最後にデコードされたデータ フレームをフレーム キューに入れて待機することです。 SDL でプレイしてください。
デコード処理
デコード スレッドは、ストリームが開かれるとき、つまり関数 stream_component_open で作成され、ビデオ デコード スレッドの動作関数は video_thread です。
以下は video_thread 関数の内容です フィルター関連の処理を取り除くと非常に効率化された関数であることがわかります for ループ内の get_video_frame 関数で 1 フレームの画像データを取得した後、データの pts を調整しますフレームを変換し、データ フレームをフレーム キューに入れます。
/**
* 视频解码
* @param arg
* @return
*/
static int video_thread(void *arg)
{
VideoState *is = arg;
// 分配frame
AVFrame *frame = av_frame_alloc();
double pts;
double duration;
int ret;
AVRational tb = is->video_st->time_base;
AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
#if CONFIG_AVFILTER
AVFilterGraph *graph = NULL;
AVFilterContext *filt_out = NULL, *filt_in = NULL;
int last_w = 0;
int last_h = 0;
enum AVPixelFormat last_format = -2;
int last_serial = -1;
int last_vfilter_idx = 0;
#endif
if (!frame)
return AVERROR(ENOMEM);
for (;;) {
// 获取一帧视频图像,返回负值会退出线程,什么时候会返回负值?问管家VideoState
ret = get_video_frame(is, frame);
if (ret < 0)
goto the_end;
if (!ret)
continue;
#if CONFIG_AVFILTER
if ( last_w != frame->width
|| last_h != frame->height
|| last_format != frame->format
|| last_serial != is->viddec.pkt_serial
|| last_vfilter_idx != is->vfilter_idx) {
av_log(NULL, AV_LOG_DEBUG,
"Video frame changed from size:%dx%d format:%s serial:%d to size:%dx%d format:%s serial:%d\n",
last_w, last_h,
(const char *)av_x_if_null(av_get_pix_fmt_name(last_format), "none"), last_serial,
frame->width, frame->height,
(const char *)av_x_if_null(av_get_pix_fmt_name(frame->format), "none"), is->viddec.pkt_serial);
avfilter_graph_free(&graph);
graph = avfilter_graph_alloc();
if (!graph) {
ret = AVERROR(ENOMEM);
goto the_end;
}
graph->nb_threads = filter_nbthreads;
if ((ret = configure_video_filters(graph, is, vfilters_list ? vfilters_list[is->vfilter_idx] : NULL, frame)) < 0) {
SDL_Event event;
event.type = FF_QUIT_EVENT;
event.user.data1 = is;
SDL_PushEvent(&event);
goto the_end;
}
filt_in = is->in_video_filter;
filt_out = is->out_video_filter;
last_w = frame->width;
last_h = frame->height;
last_format = frame->format;
last_serial = is->viddec.pkt_serial;
last_vfilter_idx = is->vfilter_idx;
frame_rate = av_buffersink_get_frame_rate(filt_out);
}
ret = av_buffersrc_add_frame(filt_in, frame);
if (ret < 0)
goto the_end;
while (ret >= 0) {
is->frame_last_returned_time = av_gettime_relative() / 1000000.0;
ret = av_buffersink_get_frame_flags(filt_out, frame, 0);
if (ret < 0) {
if (ret == AVERROR_EOF)
is->viddec.finished = is->viddec.pkt_serial;
ret = 0;
break;
}
is->frame_last_filter_delay = av_gettime_relative() / 1000000.0 - is->frame_last_returned_time;
if (fabs(is->frame_last_filter_delay) > AV_NOSYNC_THRESHOLD / 10.0)
is->frame_last_filter_delay = 0;
tb = av_buffersink_get_time_base(filt_out);
#endif
// 计算持续播放时间
duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){
frame_rate.den, frame_rate.num}) : 0);
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
// 将解码得到的帧放进队列
ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
av_frame_unref(frame);
#if CONFIG_AVFILTER
if (is->videoq.serial != is->viddec.pkt_serial)
break;
}
#endif
if (ret < 0)
goto the_end;
}
the_end:
#if CONFIG_AVFILTER
avfilter_graph_free(&graph);
#endif
av_frame_free(&frame);
return 0;
}
get_video_frame 関数の動作を見てみましょう。
/**
* 获取一帧图像
* @param is
* @param frame
* @return
*/
static int get_video_frame(VideoState *is, AVFrame *frame)
{
int got_picture;
// 获取解码数据
if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0)
return -1;
// 分析是否需要丢帧,例如解码出来的图片已经比真正播放的音频都慢了,那就要丢帧了
if (got_picture) {
double dpts = NAN;
if (frame->pts != AV_NOPTS_VALUE)
// 计算pts
dpts = av_q2d(is->video_st->time_base) * frame->pts;
frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);
// 同步时钟不以视频为基准时
if (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {
if (frame->pts != AV_NOPTS_VALUE) {
// 理论上如果需要连续接上播放的话 dpts + diff = get_master_clock(is)
// 所以可以算出diff 注意绝对值
double diff = dpts - get_master_clock(is);
if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD &&
diff - is->frame_last_filter_delay < 0 &&
is->viddec.pkt_serial == is->vidclk.serial &&
is->videoq.nb_packets) {
is->frame_drops_early++;
av_frame_unref(frame);
got_picture = 0;
}
}
}
}
return got_picture;
}
この関数は内部で関数 decoder_decode_frame を呼び出してデコードされたフレームを取得し、同期されたクロックに基づいてフレーム損失処理が必要かどうかを判断します。次に、関数 decoder_decode_frame を見てみましょう。
static int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub) {
int ret = AVERROR(EAGAIN);
for (;;) {
// 解码器的序列需要和解码包队列的序列一致
if (d->queue->serial == d->pkt_serial) {
do {
// 请求退出了则返回-1
if (d->queue->abort_request)
return -1;
switch (d->avctx->codec_type) {
case AVMEDIA_TYPE_VIDEO:
// 获取1帧解码数据
ret = avcodec_receive_frame(d->avctx, frame);
if (ret >= 0) {
// 更新pts为AVPacket的pts
if (decoder_reorder_pts == -1) {
frame->pts = frame->best_effort_timestamp;
} else if (!decoder_reorder_pts) {
frame->pts = frame->pkt_dts;
}
}
break;
case AVMEDIA_TYPE_AUDIO:
ret = avcodec_receive_frame(d->avctx, frame);
if (ret >= 0) {
AVRational tb = (AVRational){
1, frame->sample_rate};
if (frame->pts != AV_NOPTS_VALUE)
frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, 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;
}
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->packet_pending = 0;
} else {
int old_serial = d->pkt_serial;
if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)
return -1;
if (old_serial != d->pkt_serial) {
// 刷新解码器
avcodec_flush_buffers(d->avctx);
d->finished = 0;
d->next_pts = d->start_pts;
d->next_pts_tb = d->start_pts_tb;
}
}
if (d->queue->serial == d->pkt_serial)
break;
av_packet_unref(d->pkt);
} while (1);
if (d->avctx->codec_type == AVMEDIA_TYPE_SUBTITLE) {
int got_frame = 0;
ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, d->pkt);
if (ret < 0) {
ret = AVERROR(EAGAIN);
} else {
if (got_frame && !d->pkt->data) {
d->packet_pending = 1;
}
ret = got_frame ? 0 : (d->pkt->data ? AVERROR(EAGAIN) : AVERROR_EOF);
}
av_packet_unref(d->pkt);
} else {
// 数据送进解码器失败,遇到EAGAIN怎么办?
if (avcodec_send_packet(d->avctx, d->pkt) == AVERROR(EAGAIN)) {
av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
d->packet_pending = 1;
} else {
av_packet_unref(d->pkt);
}
}
}
}
関数 decoder_decode_frame がデコードの真のコアであることがわかり、この関数を呼び出すことでビデオ、オーディオ、字幕がすべてデコードされていることがわかります。この関数の主な仕事は、for ループでデータ パケットを継続的に取得し、FFmpeg の API を呼び出してデコードすることです。
1. まず、デコード対象のキューの再生シーケンスがデコーダーの再生シーケンスと一致しているかどうかを判断します。
if (d->queue->serial == d->pkt_serial)
一貫性がある場合は、FFmpeg 関数 avcodec_receive_frame を通じてデータ フレームを取得し、データ フレーム pts を更新します。
ここで質問がありますが、デコード プロセスは最初に avcodec_send_packet を実行し、次に avcodec_receive_frame を実行してデータ フレームを取得すると前に言いませんでしたか? ここで avcodec_receive_frame から直接データ フレームを取得するにはどうすればよいですか?
これは、関数 decoder_decode_frame が常に呼び出される for ループであるためです。最初に avcodec_receive_frame を呼び出すことで、デコーダが内部で大量のデータをバッファリングし、デコードされたパケットをデコーダに送信できなくなることを防ぐことができます。
2. キューにデコードするデータがあるかどうかを確認します。ない場合は、読み取りスレッドを起動します。
if (d->queue->nb_packets == 0)
// 队列空了,唤醒读取线程,赶紧读取数据
SDL_CondSignal(d->empty_queue_cond);
3. デコーダへの送信に失敗したパケットがあるかどうかを確認し、存在する場合は、最初にそれらのパケットを処理します。
if (d->packet_pending) {
// 处理有缓冲的数据
d->packet_pending = 0;
}
このパケットは、avcodec_send_packet を呼び出すと AVERROR (EAGAIN) が返されるとキャッシュされます。
4. キャッシュされたパケットがない場合は、キューから packet_queue_get を取得します。
int old_serial = d->pkt_serial;
if (packet_queue_get(d->queue, d->pkt, 1, &d->pkt_serial) < 0)
return -1;
if (old_serial != d->pkt_serial) {
// 刷新解码器
avcodec_flush_buffers(d->avctx);
d->finished = 0;
d->next_pts = d->start_pts;
d->next_pts_tb = d->start_pts_tb;
}
5. パケットをデコーダに送信し、戻り値が AVERROR (EAGAIN) の場合は、パケットをキャッシュし、次回の処理のために最初にデコーダに送信します。
同時に、データをデコーダにスムーズに送信できるように、最初のステップで avcodec_receive_frame が最初に呼び出される理由もここで説明できます。
// 数据送进解码器失败,遇到EAGAIN怎么办?
if (avcodec_send_packet(d->avctx, d->pkt) == AVERROR(EAGAIN)) {
av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
d->packet_pending = 1;
} else {
av_packet_unref(d->pkt);
}
>>> 音视频开发 视频教程: https://ke.qq.com/course/3202131?flowToken=1031864
>>> 音视频开发学习资料、教学视频,免费分享有需要的可以自行添加学习交流群 739729163 领取