解码线程
ffplay的解码线程独⽴于数据读线程,并且每种类型的流(AVStream)都有其各⾃的解码线程,如:
- video_thread⽤于解码video stream;
- audio_thread⽤于解码audio stream;
- subtitle_thread⽤于解码subtitle stream。
为⽅便阅读,先列⼀张表格,梳理各个变量、函数名称
类型 | PacketQueue | FrameQueue | vidck | 解码线程 |
---|---|---|---|---|
视频 | videoq | pictq | vidcllk | video_thread |
⾳频 | audioq | sampq | audclk | audio_thread |
字幕 | subtitleq | subpq | ⽆ | subtitle_thread |
其中PacketQueue⽤于存放从read_thread取到的各⾃播放时间内的AVPacket。FrameQueue⽤于存放 各⾃解码后的AVFrame。Clock⽤于同步⾳视频。解码线程负责将PacketQueue数据解码为AVFrame, 并存⼊FrameQueue。
对于不同流,其解码过程⼤同⼩异。
typedef struct Decoder {
AVPacket pkt;
PacketQueue *queue; // 数据包队列
AVCodecContext *avctx; // 解码器上下⽂
int pkt_serial; // 包序列
int finished; // =0,解码器处于⼯作状态;=⾮0,解码器处于空闲状态
int packet_pending; // =0,解码器处于异常状态,需要考虑重置解码器;=1,解码器处于正常状 态
SDL_cond *empty_queue_cond; // 检查到packet队列空时发送 signal缓存read_thread读取数据
int64_t start_pts; // 初始化时是stream的start time
AVRational start_pts_tb; // 初始化时是stream的time_base
int64_t next_pts; // 记录最近⼀次解码后的frame的pts,当解出来的部分帧没有有效的pts 时则使⽤next_pts进⾏推算
AVRational next_pts_tb; // next_pts的单位
SDL_Thread *decoder_tid; // 线程句柄
} Decoder;
解码器相关的函数
(decoder我们ffplay⾃定义,重新封装的。 avcodec才是ffmpeg的提供的)
- 初始化解码器
void decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond); - 启动解码器
int decoder_start(Decoder *d, int (*fn)(void *), const char thread_name, void arg) - 解帧
int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub); - 终⽌解码器
void decoder_abort(Decoder *d, FrameQueue *fq); - 销毁解码器
void decoder_destroy(Decoder *d);
使用方法
- 启动解码线程
- decoder_init()
- decoder_start()
- 解码线程具体流程
- decoder_decode_frame()
- 退出解码线程
- decoder_abort()
- decoder_destroy()
视频解码线程
数据来源:从read_thread线程⽽来
数据处理:在video_thread进⾏解码,具体调⽤get_video_frame
数据出⼝:在video_refresh读取frame进⾏显示
video_thread()
我们先看video_thead,对于滤镜部分(CONFIG_AVFILTER定义部分),这⾥不做分析 ,简化后的代码 如下:
static int video_thread(void *arg)
{
VideoState *is = arg;
AVFrame *frame = av_frame_alloc(); // 分配解码帧
double pts; // pts
double duration; // 帧持续时间
int ret;
// 1 获取stream timebase
AVRational tb = is->video_st->time_base;
// 2 获取帧率,以便计算每帧picture的duration
AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
if (!frame)
return AVERROR(ENOMEM);
for (;;) {
// 循环取出视频解码的帧数据
// 3 解码获取⼀帧视频画⾯
ret = get_video_frame(is, frame);
if (ret < 0)
goto the_end;//解码结束, 什么时候会结束
if (!ret) //没有解码得到画⾯, 什么情况下会得不到解后的帧
continue;
// 4 计算帧持续时间和换算pts值为秒
// 1/帧率 = duration 单位秒, 没有帧率时则设置为0, 有帧率帧计算出帧间隔
duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){
frame_rate.den, frame_rate.num}) : 0);
// 根据AVStream timebase计算出pts值, 单位为秒
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
// 5 将解码后的视频帧插⼊队列
ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
// 6 释放frame对应的数据
av_frame_unref(frame);// 正常情况下frame对应的buf以被av_frame_m ove_ref
if (ret < 0) // 返回值⼩于0则退出线程
goto the_end;
}
the_end:
av_frame_free(&frame);// 释放frame
return 0;
}
在该流程中,当调⽤函数返回值⼩于<0时则退出线程。 线程的总体流程很清晰:
- 获取stream timebase,以便将frame的pts转成秒为单位
- 获取帧率,以便计算每帧picture的duration
- 获取解码后的视频帧,具体调⽤get_video_frame()实现
- 计算帧持续时间和换算pts值为秒
- 将解码后的视频帧插⼊队列,具体调⽤queue_picture()实现
- 释放frame对应的数据
get_video_frame()
主要流程:
- 调⽤ decoder_decode_frame 解码并获取解码后的视频帧;
- 分析如果获取到帧是否需要drop掉(逻辑就是如果刚解出来就落后主时钟,那就没有必要放⼊Frame队 列,再拿去播放,但是也是有⼀定的条件的,⻅下⾯分析)
static int get_video_frame(VideoState *is, AVFrame *frame)
{
int got_picture;
// 1. 获取解码后的视频帧
if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0)
return -1;// 返回-1意味着要退出解码线程, 所以要分析decoder_decode_ frame什么情况下返回-1
if (got_picture) {
// 2. 分析获取到的该帧是否要drop掉
// 2. 分析获取到的该帧是否要drop掉, 该机制的⽬的是在放⼊帧队列前先drop掉过时 的视频帧
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 // 允许drop帧
|| (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {