read_thread
读包线程函数,大概实现的内容总览:
1、打开输入文件,并创建输入上下文;
2、查找流信息
3、自定义开始位置
4、确定流索引
5、打开音频、视频、字幕解码器
6、循环读取包,(可选)seek处理。
7、结束退出。
创建上下文
// 创建锁,用于等待读
SDL_mutex *wait_mutex = SDL_CreateMutex();
// 创建包结构体
pkt = av_packet_alloc();
// 创建输入上下文
ic = avformat_alloc_context();
// 设置自定义中断回调
ic->interrupt_callback.callback = decode_interrupt_cb;
ic->interrupt_callback.opaque = is;
// 打开输入文件
err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
// 中断回调实现
staticintdecode_interrupt_cb(void *ctx)
{
VideoState *is = ctx;
return is->abort_request;
}设置自定义中断回调,FFmpeg底层会定期调用这个回调函数以判断是否需要退出。
我们还注意到打开输入文件前会设置一个scan_all_pmts的参数
if (!av_dict_get(format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE)) {
av_dict_set(&format_opts, "scan_all_pmts", "1", AV_DICT_DONT_OVERWRITE);
scan_all_pmts_set = 1;
}目的是使MPEG-TS demuxer扫描所有的PMT(Program Map Table),确保完整识别流中的所有节目和音视频轨道。
查找流信息
俗称“验货”,打开文件后,此时的很多参数信息并不完整或不可靠,需要做进一步探测。
err = avformat_find_stream_info(ic, opts);自定义开始位置
if (start_time != AV_NOPTS_VALUE) {
int64_t timestamp;
timestamp = start_time;
if (ic->start_time != AV_NOPTS_VALUE)
timestamp += ic->start_time;
// 跳转指定位置
ret = avformat_seek_file(ic, -1, INT64_MIN, timestamp, INT64_MAX, 0);
}如果有设置start_time参数,则从跳转指定位置开始播放。
确定流索引
for (i = 0; i < ic->nb_streams; i++) {
AVStream *st = ic->streams[i];
enumAVMediaTypetype = st->codecpar->codec_type;
st->discard = AVDISCARD_ALL;
if (type >= 0 && wanted_stream_spec[type] && st_index[type] == -1)
if (avformat_match_stream_specifier(ic, st, wanted_stream_spec[type]) > 0)
st_index[type] = i;
}首先按照用户自定义的媒体类型,确定索引,比如"v:1"。
如果没有自定义,则自动查找流:
st_index[AVMEDIA_TYPE_VIDEO] = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,..);
st_index[AVMEDIA_TYPE_AUDIO] = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,..);
st_index[AVMEDIA_TYPE_SUBTITLE] = av_find_best_stream(ic, AVMEDIA_TYPE_SUBTITLE,..);确定好索引后,还设置了下默认的窗口大小:
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]];
AVCodecParameters *codecpar = st->codecpar;
AVRational sar = av_guess_sample_aspect_ratio(ic, st, NULL);
if (codecpar->width)
set_default_window_size(codecpar->width, codecpar->height, sar);
}SAR是像素宽高比,要计算出窗口显示大小,需要转成显示宽高比DAR
//最低宽高比1:1
if (av_cmp_q(aspect_ratio, av_make_q(0, 1)) <= 0)
aspect_ratio = av_make_q(1, 1);
//转换成显示宽高比
aspect_ratio = av_mul_q(aspect_ratio, av_make_q(pic_width, pic_height));
height = scr_height;
//计算显示宽度
width = av_rescale(height, aspect_ratio.num, aspect_ratio.den) & ~1;~1是最低为取0,表示取偶数,像YUV420格式宽度必须是偶数。
打开解码器
打开解码器就还是哪些路数,创建解码器上下文;拷贝参数;查找解码器这些,但是源码中多了些参数设置:
if (stream_lowres > codec->max_lowres)
stream_lowres = codec->max_lowres;
// 解码器的低分辨率模式
avctx->lowres = stream_lowres;lowres是FFmpeg某些解码器支持的快速预览模式:牺牲画质换取解码速度,降低 CPU/内存开销。值越大,画质越差,解码速度越快。当然不能超过max_lowers。
if (fast)
avctx->flags2 |= AV_CODEC_FLAG2_FAST;fast参数表示是否使用快速非标准解码模式,也是牺牲画质换取解码速度。
// 根据CPU核心自己决定使用多少线程解码
av_dict_set(&opts, "threads", "auto", 0);
// 设置低分辨率模式
av_dict_set_int(&opts, "lowres", stream_lowres, 0);
// 解码时把输入packet的opaque复制到输出 frame
av_dict_set(&opts, "flags", "+copy_opaque", AV_DICT_MULTIKEY);然后才是打开解码器:
if ((ret = avcodec_open2(avctx, codec, &opts)) < 0){}打开解码器之后启动三个解码线程:音频解码线程、视频解码线程、字幕解码线程。
其中视频、字幕稍微简单些,音频复杂些。
看下视频、字幕的初始化:
staticintdecoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond) {
memset(d, 0, sizeof(Decoder));
// 创建包
d->pkt = av_packet_alloc();
// 记录解码器上下文
d->avctx = avctx;
// 关联包队列
d->queue = queue;
// 条件变量
d->empty_queue_cond = empty_queue_cond;
d->start_pts = AV_NOPTS_VALUE;
d->pkt_serial = -1;
return0;
}启动解码线程:
staticintdecoder_start(Decoder *d, int (*fn)(void *), constchar *thread_name, void* arg)
{
packet_queue_start(d->queue);
//启动线程
d->decoder_tid = SDL_CreateThread(fn, thread_name, arg);
return0;
}
staticvoidpacket_queue_start(PacketQueue *q)
{
SDL_LockMutex(q->mutex);
// 不中断
q->abort_request = 0;
// 序号自增
q->serial++;
SDL_UnlockMutex(q->mutex);
}复杂的音频的初始化先跳过,它涉及了滤镜使用、音频设备初始化、音频播放初始化等等,内容多得都影响了咱本篇的主要内容了。
循环读包
接下来就是进入循环读包的环节了,也是很复杂,但却是重中之重。在真正读包前又判断一堆前置条件。
首先是退出、暂停处理。
// 结束循环读包
if (is->abort_request)
break;
// 暂停读包
if (is->paused != is->last_paused) {
is->last_paused = is->paused;
if (is->paused)
is->read_pause_return = av_read_pause(ic); //暂停
else
av_read_play(ic); //恢复
}跳转处理:
// 执行跳转
ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max,is->seek_flags);
// 清空当前包队列
if (is->audio_stream >= 0)
packet_queue_flush(&is->audioq);
if (is->subtitle_stream >= 0)
packet_queue_flush(&is->subtitleq);
if (is->video_stream >= 0)
packet_queue_flush(&is->videoq);
// 重新设置时钟
if (is->seek_flags & AVSEEK_FLAG_BYTE) {
set_clock(&is->extclk, NAN, 0);
} else {
set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);
}
// 状态重置
is->seek_req = 0;
is->queue_attachments_req = 1;
is->eof = 0;
// 如果是暂停状态,则显示下一帧
if (is->paused)
step_to_next_frame(is);
}请求附加数据,比如mp3音频的专辑封面:
if (is->queue_attachments_req) {
if (is->video_st && is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC) {
// 引用附加数据
if ((ret = av_packet_ref(pkt, &is->video_st->attached_pic)) < 0)
goto fail;
// 放入视频队列以通知渲染这张图片
packet_queue_put(&is->videoq, pkt);
// 放入一个空包以通知队列结束
packet_queue_put_nullpacket(&is->videoq, pkt, is->video_stream);
}
is->queue_attachments_req = 0;
}如果包队列都满了则等待:
// 不是无限缓冲
if (infinite_buffer<1 &&
// 总大小超过最大限制
(is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
// 音频、视频、字幕包队列都满了
|| (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
SDL_LockMutex(wait_mutex);
SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
SDL_UnlockMutex(wait_mutex);
continue;
}
staticintstream_has_enough_packets(AVStream *st, int stream_id, PacketQueue *queue) {
return stream_id < 0 ||
queue->abort_request ||
// 是附加数据(比如专辑封面)
(st->disposition & AV_DISPOSITION_ATTACHED_PIC) ||
// 队列已经塞了25个包 && 总时长超过1秒
queue->nb_packets > MIN_FRAMES && (!queue->duration || av_q2d(st->time_base) * queue->duration > 1.0);
}是否需要重头再播:
//不是暂停
if (!is->paused &&
// 音频解码完成 且 音频队列为空
(!is->audio_st || (is->auddec.finished == is->audioq.serial && frame_queue_nb_remaining(&is->sampq) == 0)) &&
// 视频解码完成 且 视频队列为空
(!is->video_st || (is->viddec.finished == is->videoq.serial && frame_queue_nb_remaining(&is->pictq) == 0))) {
// 判断是否需要从头播放
if (loop != 1 && (!loop || --loop)) {
stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);
} elseif (autoexit) { //自动退出
ret = AVERROR_EOF;
goto fail;
}
}读数据包:
// 读一个包
ret = av_read_frame(ic, pkt);
// 读失败情况
if (ret < 0) {
// 只是读结束了
if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) {
// 填充空包
if (is->video_stream >= 0)
packet_queue_put_nullpacket(&is->videoq, pkt, is->video_stream);
if (is->audio_stream >= 0)
packet_queue_put_nullpacket(&is->audioq, pkt, is->audio_stream);
if (is->subtitle_stream >= 0)
packet_queue_put_nullpacket(&is->subtitleq, pkt, is->subtitle_stream);
// 修改结束标记
is->eof = 1;
}
// 真得是失败了
if (ic->pb && ic->pb->error) {
if (autoexit)
goto fail;
else
break;
}
//其它异常情况,等待10ms后再读
SDL_LockMutex(wait_mutex);
SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
SDL_UnlockMutex(wait_mutex);
continue;
} else { // 读成功
is->eof = 0;
}
stream_start_time = ic->streams[pkt->stream_index]->start_time;
pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
// 判断当前包是否在用户自定义的播放范围中 比如播放前10秒
pkt_in_play_range = duration == AV_NOPTS_VALUE ||
// (当前包时间戳 - 流开始时间戳)* 时间基 - 用户自定义的播放起始时间 < 用户自定义的播放时长
(pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *
av_q2d(ic->streams[pkt->stream_index]->time_base) -
(double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000
<= ((double)duration / 1000000);
// 包放入对应的队列
if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {
packet_queue_put(&is->audioq, pkt);
} elseif (pkt->stream_index == is->video_stream && pkt_in_play_range
&& !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
packet_queue_put(&is->videoq, pkt);
} elseif (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
packet_queue_put(&is->subtitleq, pkt);
} else {
// 无效包丢弃
av_packet_unref(pkt);
}读包结束
//资源释放
if (ic && !is->ic)
avformat_close_input(&ic);
av_packet_free(&pkt);
// 有异常 发送退出事件消息
if (ret != 0) {
SDL_Event event;
event.type = FF_QUIT_EVENT;
event.user.data1 = is;
SDL_PushEvent(&event);
}
SDL_DestroyMutex(wait_mutex);最后
read_thread函数是真长啊,就因为音频、视频、字幕的数据包都集中在这里处理滴。
夜雨聆风