ffplay源码分析(1)

1 ffplay 简介

FFplay是使用FFmpeg库和SDL库的非常精简且可移植的开源媒体播放器。整个播放器源码才三千多行, 且性能稳定功能齐全,被广大开发者学习引用。比如向ijkplayer(哔哩哔哩)就是基于ffplay 做二次开发的。我们可以顺着如下六个函数,去阅读ffplay 源码:

1)read_thread() : 读取读取音频,视频,字幕packet 并将将其放入对应的packet_queue 中去。

2)audio_thread(): 音频解码,取音频packet_queue 内容,解码,并将解码后的数据放入音频packet_queue 中去

3)video_thread(): 视频解码,取视频packet_queue 内容,解码,并将解码后的数据放入视频packet_queue 中去

4)subtitle_thread():字幕解码,取音频packet_queue 内容,解码,并将解码后的数据放入字幕packet_queue 中去

5)event_loop(): 主线程,处理视频,字幕显示逻辑,接收并处理用户传递的命令,另外音视频同步也是在这里做的。

6)sdl_audio_callback(): auido 输出函数, 由sdl 发起callback 被动调用。ffplay 源码整体流程图如下所示:

关于ffplay 源码介绍如下几篇博客介绍的比较仔细,本文也是在参考如下几篇博客基础上写的笔记。

         ffplay源码分析(一):代码架构简述

         ffplay源码分析(二): 探讨自定义队列

         ffplay源码分析(三):read_thread线程

         ffplay源码分析(四): ffplay解码线程分析

         ffplay源码分析(五):ffplay video显示线程分析

         ffplay源码分析(六):ffplay audio输出线程分析

         ffplay源码分析(七):ffplay subtitle显示线程分析

         ffplay源码分析(八):音视频同步 -- 基础

         ffplay源码分析(九):视频同步音频

         ffplay源码分析(十):音频同步视频

         ffplay源码分析(十一):同步到外部时钟

 

2 read_thread()

read_thread() 线程分为两个阶段:1)准备阶段,打开文件,检测Stream信息,打开解码器, 二)demux 阶段,主循环读数据,解封装:读取Packet,存入PacketQueue

1)准备阶段

在read_thread() 线程中会去调用avformat_open_input() 去初始化media 的相关参数信息

    ic->interrupt_callback.callback = decode_interrupt_cb;//异常中断,及时响应用户退出命令,或播放器异常时及时退出
    ic->interrupt_callback.opaque = is;
    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;
    }
    err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
    if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
        stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
...
   if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
        ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);
...
    if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) {
        stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);

打开audio ,video,subtitle 解码器,比create 对应解码线程. 在stream_component_open() 里面会去调用decoder_start() 建立各自线程。

然后调用av_read_frame() 函数去读取packet.

ret = av_read_frame(ic, pkt);//读取packet
...
packet_queue_put(&is->audioq, pkt);//将读取的packet 放入queue

以下关于read_thread() 几个细节地方:

一) 设置帧的最大duration, 主要是避免不连续的pts,否则会出现画面静止的情况 。 这里设置的是10s, 当两帧pts 超过10s 时, 当前帧的pts 会drop 掉,采用前一帧的pts

is->max_frame_duration = (ic->iformat->flags & AVFMT_TS_DISCONT) ? 10.0 : 3600.0;

二) 设置起点去播,起播前, 直接调用avformat_seek_file()

       timestamp = start_time;
        /* add the stream 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);

三) 选择合适的流去播,比如用户指定的汉语,英语音轨等等。如果没有指定,就选第一个。

        if (type >= 0 && wanted_stream_spec[type] && st_index[type] == -1)
            if (avformat_match_stream_specifier(ic, st, wanted_stream_spec[type]) > 0)

 四) 如果用seek 请求,第一步调用avformat_seek_file() 跳到对应点,然后flush queue, 并put 一个刷新包,最后设置外部时钟

         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);
    packet_queue_put(&is->audioq, &flush_pkt);
}
if (is->subtitle_stream >= 0) {
    packet_queue_flush(&is->subtitleq);
    packet_queue_put(&is->subtitleq, &flush_pkt);
}
if (is->video_stream >= 0) {
    packet_queue_flush(&is->videoq);
    packet_queue_put(&is->videoq, &flush_pkt);
}
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);
}

三 audio_thread()

audio 解码线程完成的任务是: 音频解码,取音频packet_queue 内容,解码,并将解码后的数据放入音频packet_queue 中去 。

通过decoder_decode_frame() 去解码,得到audio data,并开启滤镜功能,对audio 进行相应的转化。

ret = configure_audio_filters(is, afilters, 1); //根据用户指令,去配置audio filters
...
ret = av_buffersrc_add_frame(is->in_audio_filter, frame); // 将frame 添加到滤镜
...
//得到经过滤镜处理的数据,注意这里有个while, 直到audio filter 里面的数据完全获取才退出
while ((ret = av_buffersink_get_frame_flags(is->out_audio_filter, frame, 0)) >= 0) 
...
frame_queue_push(&is->sampq); // 将audio 数据push 到audio frame queue

四 video_thread() 

 video 解码线程完成的任务是:视频解码,取视频packet_queue 内容,解码,并将解码后的数据放入视频packet_queue 中去。

解码是在get_video_frame()函数中通过调用decoder_decode_frame() 完成的。 当用户使能framedrop,且解码当前的pts diff 很大时就会drop 掉这一帧。

if (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) {
            if (frame->pts != AV_NOPTS_VALUE) {
                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;
                }
            }
        }

并且在video_thread() 中还有关于video filter 的动作, 当width,height,format ,帧不连续时就会启用filter 功能,调整画面输出。

最后通过queue_picture()将frame 存放到队列中去。

 ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);

五 subtitle_thread()

subtitle_thread() 线程比较简单,直接贴源码。

static int subtitle_thread(void *arg)
{
    VideoState *is = arg;
    Frame *sp;
    int got_subtitle;
    double pts;

    for (;;) {
        if (!(sp = frame_queue_peek_writable(&is->subpq)))
            return 0;

        if ((got_subtitle = decoder_decode_frame(&is->subdec, NULL, &sp->sub)) < 0)//解码,得到一帧subtitle 数据
            break;

        pts = 0;

        if (got_subtitle && sp->sub.format == 0) {
            if (sp->sub.pts != AV_NOPTS_VALUE)
                pts = sp->sub.pts / (double)AV_TIME_BASE;
            sp->pts = pts;
            sp->serial = is->subdec.pkt_serial;
            sp->width = is->subdec.avctx->width;
            sp->height = is->subdec.avctx->height;
            sp->uploaded = 0;//uploaded = 0 ,表面这帧字幕没有被显示

            /* now we can update the picture count */
            frame_queue_push(&is->subpq);//将字幕数据推到frame queue 
        } else if (got_subtitle) {
            avsubtitle_free(&sp->sub);
        }
    }
    return 0;
}

 

  • 1
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值