乐于分享
好东西不私藏

认真读读ffplay源码(三):读包

认真读读ffplay源码(三):读包

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(01)) <= 0)
    aspect_ratio = av_make_q(11);
//转换成显示宽高比
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, 0sizeof(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 : 000);
    } 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函数是真长啊,就因为音频、视频、字幕的数据包都集中在这里处理滴。