【FFmpeg进阶】2、FFplay播放器实战及源码剖析

一、播放器原理

        播放一个媒体文件一般需要 5 个基本模块,按层级顺序:
                1、文件读取模块(Source)
                2、解复用模块(Demuxer)
                3、音视频解码模块(Decoder)
                4、色彩空间转换模块(Color Space Converter)
                5、音视频渲染模块(Render)
1、 文件读取模块( Source 的作用是为下级解复用模块( Demuxer )以包的形式源源不断的提供数据流, 对于下一级的 Demuxer 来说,本地文件和网络数据是一样的。在 ffmpeg 框架中,文件读取模块可分为 3 层:
        协议层: pipe tcp udp http 等这些具体的本地文件或网络协议
        抽象层:URLContext 结构来统一表示底层具体的本地文件或网络协议
        接口层用:AVIOContext 结构来扩展 URLProtocol 结构成内部有缓冲机制的广泛意义上的文件,并且仅仅由最上层用 AVIOContext 对模块外提供服务,实现读媒体文件功能。
2、 解复用模块( Demuxer :作用是识别文件类型,媒体类型,分离出音频、视频、字幕原始数据流,打上时间戳信息后传给下级的视频频解码模块(Decoder)。可以简单的分为两层,底层是 AVIContext,TCPContext,UDPContext 等等,这些具体媒体的解复用结构和相关的基础程序,上层是 AVInputFormat 结构和相关的程序。上下层之间由AVInputFormat 相对应的AVFormatContext 结构的 priv_data 字段关联 AVIContext 或TCPContext 或 UDPContext 等等具体的文件格式。AVInputFormat 和具体的音视频编码算法格式由 AVFormatContext 结构的streams 字段关联媒体格式,streams 相当于Demuxer 的 output pin, 解复用模块分离音视频裸数据通过 streams 传递给下级音视频解码器。
3、 视频频解码模块( Decoder 的作用就是解码数据包,并且把同步时钟信息传递下去。
4、 色彩空间转换模块( Color Space Converter 颜色空间转换过滤器的作用是把 视频解码器解码出来的数据转换成当前显示系统支持的颜色格式
5、 音视频渲染模块( Render 的作用就是在适当的时间渲染相应的媒体,对视频媒体就是直接显示图像,对音频就是播放声音。

二、main()函数解析

1、FFplay的主要流程

调用如下函数:

        av_register_all () :注册所有编码器和解码器。
        show_banner () :打印输出 FFmpeg 版本信息(编译时间,编译选项,类库信息等)。
        parse_options () :解析输入的命令。
        SDL_Init () SDL 初始化。
        stream_open () :打开输入媒体。
        event_loop () :处理各种消息,不停地循环下去。

2、FFplay代码的总体结构

1)parse_options()

        parse_options() 解析全部输入选项。 即将输入命令“ffplay -f h264 test.264” 中的 “-f” 这样的命令解析出来。 需要注意的是 ,FFplay ( ffplay.c) 的 parse_options() FFmpeg ( ffmpeg.c) 中 的 parse_options()实际上是一样的。
2)SDL_Init()
        SDL_Init()用于初始化 SDL
        FFplay 中视频的显示和声音的播放都用到了 SDL
3)stream_open()
        stream_open()的作用是 打开输入的媒体
        这个函数还是比较复杂的,包含了 FFplay 中各种线程的创建。
stream_open()调用了如下函数:
         packet_queue_init():初始化各个 PacketQueue(视频/音频/字幕)
         read_thread() :读取媒体信息线程。
read_thread() 调用了如下函数:
        avformat_open_input():打开媒体。
        avformat_find_stream_info():获得媒体信息。
        av_dump_format():输出媒体信息到控制台。
         stream_component_open() :分别打开视频/音频/字幕解码线程
         refresh_thread() :视频刷新线程。
        av_read_frame():获取一帧压缩编码数据(即一个 AVPacket)。
        packet_queue_put():根据压缩编码数据类型的不同(视频/音频/字幕),放到不同的 PacketQueue 中。
4)refresh_thread()
refresh_thread()调用了如下函数:
        SDL_PushEvent(FF_REFRESH_EVENT):发送 FF_REFRESH_EVENT 的 SDL_Event
        av_usleep():每两次发送之间,间隔一段时间。
5)stream_component_open()
stream_component_open()用于打开视频/音频/字幕解码的线程。 其函数调用关系如下所示。
        avcodec_find_decoder():获得解码器。
        avcodec_open2():打开解码器。
         audio_open() :打开音频解码。
        SDL_PauseAudio():SDL 中播放音频的函数。
         video_thread() :创建视频解码线程。
         subtitle_thread() :创建字幕解码线程。
        packet_queue_start():初始化 PacketQueue。
6)audio_open()
audio_open()调用了如下函数
        SDL_OpenAudio():SDL 中打开音频设备的函数。
        注意它是根据 SDL_AudioSpec 参数打开音频设备。
        SDL_AudioSpec 中的 callback 字段指定了音频播放的回调函数 sdl_audio_callback()。当音频设备需要更多数据的时候,会调用该回调函数。因此该函数是会被反复调用的。   
sdl_audio_callback()调用了如下函数
        audio_decode_frame():解码音频数据。
        update_sample_display():当不显示视频图像,而是显示音频波形的时候,调用此函数。
audio_decode_frame()调用了如下函数
        packet_queue_get():获取音频压缩编码数据(一个 AVPacket)。
        avcodec_decode_audio4():解码音频压缩编码数据(得到一个 AVFrame)。
        swr_init():初始化 libswresample 中的 SwrContext。libswresample 用于音频采样采样数据(PCM)的转换。
        swr_convert():转换音频采样率到适合系统播放的格式。
        swr_free():释放 SwrContext。
7)video_thread()

video_thread()调用了如下函数
        avcodec_alloc_frame():初始化一个 AVFrame。
         get_video_frame() 获取一个存储解码后数据的 AVFrame。
         queue_picture():
get_video_frame() 调用了如下函数     
        packet_queue_get():获取视频压缩编码数据(一个 AVPacket)。
        avcodec_decode_video2():解码视频压缩编码数据(得到一个 AVFrame)。
queue_picture() 调用了如下函数
        SDL_LockYUVOverlay():锁定一个 SDL_Overlay。
        sws_getCachedContext():初始化 libswscale 中的 SwsContext。Libswscale 用于图像的 Raw 格式数据(YUV,RGB)之间的转换。注意 sws_getCachedContext()和 sws_getContext()
功能是一致的。
        sws_scale():转换图像数据到适合系统播放的格式。
        SDL_UnlockYUVOverlay():解锁一个 SDL_Overlay。
8)subtitle_thread()
subtitle_thread()调用了如下函数
        packet_queue_get():获取字幕压缩编码数据(一个 AVPacket)。
        avcodec_decode_subtitle2():解码字幕压缩编码数据。
9)event_loop()
        FFplay 再打开媒体之后,便会进入 event_loop()函数,永远不停的循环下去。
该函数用于接收并处理各种各样的消息。
有点像 Windows 的消息循环机制。
PS:该循环确实是无止尽的,其形 式为如下
SDL_Event event;
for (;;) {
        SDL_WaitEvent(&event);
                switch (event.type) {
                case SDLK_ESCAPE:
                case SDLK_q:
                        do_exit(cur_stream);
                        break;
                case SDLK_f:
                …
                …
                }
}
event_loop()函数调用关系:
        根据 event_loop() SDL_WaitEvent() 接收到的 SDL_Event 类型的不同,会调用不同的函
数进行处理(从编程的角度来说就是一个 switch() 语法)。
仅仅列举了几个例子:
        SDLK_ESCAPE(按下 “ESC” 键): do_exit() 。退出程序。
        SDLK_f(按下 “f” 键): toggle_full_screen() 。切换全屏显示。
        SDLK_SPACE(按下 空格 键): toggle_pause() 。切换 暂停
        SDLK_DOWN(按下鼠标键): stream_seek() 。跳转到指定的时间点播放。
        SDL_VIDEORESIZE(窗口大小发生变化): SDL_SetVideoMode() 。重新设置宽高。
        FF_REFRESH_EVENT(视频刷新事件(自定义事件)): video_refresh() 。刷新视频。
10)do_exit()
        下面分析一下 do_exit() 函数。该函数用于退出程序。
函数的调用关系如下所示。
        do_exit() 函数调用了以下函数
        stream_close() :关闭打开的媒体。
        SDL_Quit() :关闭 SDL
11)stream_close()
stream_close()函数调用了以下函数
        packet_queue_destroy() :释放 PacketQueue
        SDL_FreeYUVOverlay() :释放 SDL_Overlay
        sws_freeContext() :释放 SwsContext
12)video_refresh()
下面重点分析 video_refresh() 函数。
        该函数用于 将图像显示到显示器上。
video_refresh()函数调用了以下函数
        video_display() :显示像素数据到屏幕上。
        show_status :这算不上是一个函数,但是是一个独立的功能模块,因此列了出来。该部分打印输出播放的状态至屏幕上。如下图所示。
video_display()函数调用了以下函数
        video_open() :初始化的时候调用,打开播放窗口。
        video_audio_display() :显示音频波形图(或者频谱图)的时候调用。里面包含了不少画
图操作。
        video_image_display() :显示视频画面的时候调用。
video_open()函数调用了以下函数
        SDL_SetVideoMode() :设置 SDL_Surface (即 SDL 最基础的黑色的框)的大小等信息。
        SDL_WM_SetCaption() :设置 SDL_Surface 对应窗口的标题文字。
13)音视频同步
        视频帧的播放时间其实依赖 pts 字段的,音频和视频都有自己单独的 pts。
        但 pts 究竟是如何生成的呢 ,假如音视频不同步时,pts 是否需要动态调整,以保证音
视频的同步?
        下面先来分析,如何控制视频帧的显示时间的:
static void video_refresh(void *opaque){ 
    //根据索引获取当前需要显示的 
    VideoPicture VideoPicture *vp = &is->pictq[is->pictq_rindex]; 
    if (is->paused) 
        goto display; //只有在 paused 的情况下,才播放图像 
        // 将当前帧的 pts 减去上一帧的 pts,得到中间时间差 
        last_duration = vp->pts - is->frame_last_pts; 
        //检查差值是否在合理范围内,因为两个连续帧 pts 的时间差,不应该太大或太小 
        if (last_duration > 0 && last_duration < 10.0) { 
            /* if duration of the last frame was sane, update last_duration in video state */ 
            is->frame_last_duration = last_duration; 
        }
        //既然要音视频同步,肯定要以视频或音频为参考标准,然后控制延时来保证音视频的同步, 
        //这个函数就做这个事情了,下面会有分析,具体是如何做到的。 
        delay = compute_target_delay(is->frame_last_duration, is); 
        //获取当前时间 
        time= av_gettime()/1000000.0; 
        //假如当前时间小于 frame_timer + delay,也就是这帧改显示的时间超前,还没到,就直接返回 
        if (time < is->frame_timer + delay) 
            return; 
        //根据音频时钟,只要需要延时,即 delay 大于 0,就需要更新累加到 frame_timer 当中。 
        if (delay > 0) 
            //更新 frame_timer,frame_time 是 delay 的累加值 
            is->frame_timer += delay * FFMAX(1, floor((time-is->frame_timer) / delay));
    SDL_LockMutex(is->pictq_mutex); 
    //更新 is 当中当前帧的 pts,比如 video_current_pts、video_current_pos 等变量
    update_video_pts(is, vp->pts, vp->pos); 
    SDL_UnlockMutex(is->pictq_mutex); 
display: 
    /* display picture */
    if (!display_disable) 
        video_display(is); 
}
        函数 compute_target_delay 根据音频的时钟信号,重新计算了延时,从而达到了根据
音频 来调整视频的显示时间,从而实现音视频同步的效果。
static double compute_target_delay(double delay, VideoState *is) { 
    double sync_threshold, diff; 
    //因为音频是采样数据,有固定的采用周期并且依赖于主系统时钟,要调整音频的延时播放较难控制。 所以实际场合中视频同步音频相比音频同步视频实现起来更容易。 
    if (((is->av_sync_type == AV_SYNC_AUDIO_MASTER && is->audio_st) || 
        is->av_sync_type == AV_SYNC_EXTERNAL_CLOCK)) { 
        //获取当前视频帧播放的时间,与系统主时钟时间相减得到差值 
        diff = get_video_clock(is) - get_master_clock(is); 
        sync_threshold = FFMAX(AV_SYNC_THRESHOLD, delay); 
        //假如当前帧的播放时间,也就是 pts,滞后于主时钟 
        if (fabs(diff) < AV_NOSYNC_THRESHOLD) { 
            if (diff <= -sync_threshold) 
                delay = 0; 
        //假如当前帧的播放时间,也就是 pts,超前于主时钟,那就需要加大延时 
        else if (diff >= sync_threshold) 
            delay = 2 * delay; 
        } 
    }
    return delay; 
}
14)如何控制视频的播放和暂停?
static void stream_toggle_pause(VideoState *is) { if (is->paused) { 
    //由于 frame_timer 记下来视频从开始播放到当前帧播放的时间,所以暂停后,必须要将暂停的时间( is->video_current_pts_drift - is->video_current_pts)一起累加起来,并加上 drift 时间。
    is->frame_timer += av_gettime() / 1000000.0 + is->video_current_pts_drift - is->video_current_pts; 
    if (is->read_pause_return != AVERROR(ENOSYS)) {
        //并更新 video_current_pts 
        is->video_current_pts = is->video_current_pts_drift + av_gettime() / 1000000.0; 
    } 
    //drift 其实就是当前帧的 pts 和当前时间的时间差 
    is->video_current_pts_drift = is->video_current_pts - av_gettime() / 1000000.0;  
    }
    //paused 取反,paused 标志位也会控制到图像帧的展示,按一次空格键实现暂停,再按一次就实现播 放了。
    is->paused = !is->paused; 
}
特别说明:paused 标志位控制着视频是否播放,当需要继续播放的时候,一定要重新更新当前所需要播放 帧的 pts 时间,因为这里面要加上已经暂停的时间。
15)逐帧播放是如何做的?
在视频解码线程中,不断通过 stream_toggle_paused ,控制对视频的暂停和显示,从而实现逐帧播放:
static void step_to_next_frame(VideoState *is) { 
    //逐帧播放时,一定要先继续播放,然后再设置 step 变量,控制逐帧播放 
    if (is->paused) 
        stream_toggle_pause(is);//会不断将 paused 进行取反 
    is->step = 1; 
}
其原理就是不断的播放,然后暂停,从而实现逐帧播放: 
static int video_thread(void *arg) { 
    if (is->step) 
        stream_toggle_pause(is);
         …………………… 
    if (is->paused) 
        goto display;//显示视频 
    } 
}

三、效果展示

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值