zoukankan      html  css  js  c++  java
  • ffplay源码分析3-代码框架

    ffplay是FFmpeg工程自带的简单播放器,使用FFmpeg提供的解码器和SDL库进行视频播放。本文基于FFmpeg工程4.1版本进行分析,其中ffplay源码清单如下:
    https://github.com/FFmpeg/FFmpeg/blob/n4.1/fftools/ffplay.c

    在尝试分析源码前,可先阅读如下参考文章作为铺垫:
    [1]. 雷霄骅,视音频编解码技术零基础学习方法
    [2]. 视频编解码基础概念
    [3]. 色彩空间与像素格式
    [4]. 音频参数解析
    [5]. FFmpeg基础概念

    “ffplay源码分析”系列文章如下:
    [1]. ffplay源码分析1-概述
    [2]. ffplay源码分析2-数据结构
    [3]. ffplay源码分析3-代码框架
    [4]. ffplay源码分析4-音视频同步
    [5]. ffplay源码分析5-图像格式转换
    [6]. ffplay源码分析6-音频重采样
    [7]. ffplay源码分析7-播放控制

    3. 代码框架

    本节简单梳理ffplay.c代码框架。一些关键问题及细节问题在后续章节探讨。

    3.1 流程图

    ffplay流程图

    3.2 主线程

    主线程主要实现三项功能:视频播放(音视频同步)、字幕播放、SDL消息处理。

    主线程在进行一些必要的初始化工作、创建解复用线程后,即进入event_loop()主循环,处理视频播放和SDL消息事件:

    main() -->
    static void event_loop(VideoState *cur_stream)
    {
        SDL_Event event;
        ......
    
        for (;;) {
            // SDL event队列为空,则在while循环中播放视频帧。否则从队列头部取一个event,退出当前函数,在上级函数中处理event
            refresh_loop_wait_event(cur_stream, &event);
            // SDL事件处理
            switch (event.type) {
            case SDL_KEYDOWN:
                switch (event.key.keysym.sym) {
                case SDLK_f:            // f键:强制刷新
                    ......
                    break;
                case SDLK_p:            // p键
                case SDLK_SPACE:        // 空格键:暂停
                    ......
                case SDLK_s:            // s键:逐帧播放
                    ......
                    break;
                ......
            ......
            }
        }
    }
    

    3.2.1 视频播放

    主要代码在refresh_loop_wait_event()函数中,如下:

    static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
        double remaining_time = 0.0;
        SDL_PumpEvents();
        while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) {
            if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) {
                SDL_ShowCursor(0);
                cursor_hidden = 1;
            }
            if (remaining_time > 0.0)
                av_usleep((int64_t)(remaining_time * 1000000.0));
            remaining_time = REFRESH_RATE;
            if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
                // 立即显示当前帧,或延时remaining_time后再显示
                video_refresh(is, &remaining_time);
            SDL_PumpEvents();
        }
    }
    

    while()语句表示如果SDL event队列为空,则在while循环中播放视频帧;否则从队列头部取一个event,退出当前函数,在上级函数中处理event。
    refresh_loop_wait_event()中调用了非常关键的函数video_refresh()video_refresh()函数实现音视频的同步及视频帧的显示,是ffplay.c中最核心函数之一,在“4.3节 视频同步到音频”中详细分析。

    3.2.2 SDL消息处理

    处理各种SDL消息,比如暂停、强制刷新等按键事件。比较简单。

    main() -->
    static void event_loop(VideoState *cur_stream)
    {
        SDL_Event event;
        ......
    
        for (;;) {
            // SDL event队列为空,则在while循环中播放视频帧。否则从队列头部取一个event,退出当前函数,在上级函数中处理event
            refresh_loop_wait_event(cur_stream, &event);
            // SDL事件处理
            switch (event.type) {
            case SDL_KEYDOWN:
                switch (event.key.keysym.sym) {
                case SDLK_f:            // f键:强制刷新
                    ......
                    break;
                case SDLK_p:            // p键
                case SDLK_SPACE:        // 空格键:暂停
                    ......
                    break;
                ......
            ......
            }
        }
    }
    

    3.3 解复用线程

    解复用线程读取视频文件,将取到的packet根据类型(音频、视频、字幕)存入不同是packet队列中。
    为节省篇幅,如下源码中非关键内容的源码使用“......”替代。代码流程参考注释。

    /* this thread gets the stream from the disk or the network */
    static int read_thread(void *arg)
    {
        VideoState *is = arg;
        AVFormatContext *ic = NULL;
        int st_index[AVMEDIA_TYPE_NB];
        ......
    
        ......
        
        // 中断回调机制。为底层I/O层提供一个处理接口,比如中止IO操作。
        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;
        }
        // 1. 构建AVFormatContext
        // 1.1 打开视频文件:读取文件头,将文件格式信息存储在"fmt context"中
        err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);
        
        ......
    
        if (find_stream_info) {
            ......
            // 1.2 搜索流信息:读取一段视频文件数据,尝试解码,将取到的流信息填入ic->streams
            //     ic->streams是一个指针数组,数组大小是ic->nb_streams
            err = avformat_find_stream_info(ic, opts);
            ......
        }
    
        ......
    
        // 2. 查找用于解码处理的流
        // 2.1 将对应的stream_index存入st_index[]数组
        if (!video_disable)
            st_index[AVMEDIA_TYPE_VIDEO] =          // 视频流
                av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO,
                                    st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0);
        if (!audio_disable)
            st_index[AVMEDIA_TYPE_AUDIO] =          // 音频流
                av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO,
                                    st_index[AVMEDIA_TYPE_AUDIO],
                                    st_index[AVMEDIA_TYPE_VIDEO],
                                    NULL, 0);
        if (!video_disable && !subtitle_disable)
            st_index[AVMEDIA_TYPE_SUBTITLE] =       // 字幕流
                av_find_best_stream(ic, AVMEDIA_TYPE_SUBTITLE,
                                    st_index[AVMEDIA_TYPE_SUBTITLE],
                                    (st_index[AVMEDIA_TYPE_AUDIO] >= 0 ?
                                     st_index[AVMEDIA_TYPE_AUDIO] :
                                     st_index[AVMEDIA_TYPE_VIDEO]),
                                    NULL, 0);
    
        is->show_mode = show_mode;
        // 2.2 从待处理流中获取相关参数,设置显示窗口的宽度、高度及宽高比
        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);
        }
    
        // 3. 创建对应流的解码线程
        /* open the streams */
        if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {
            // 3.1 创建音频解码线程
            stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);
        }
    
        ret = -1;
        if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {
            // 3.2 创建视频解码线程
            ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);
        }
        if (is->show_mode == SHOW_MODE_NONE)
            is->show_mode = ret >= 0 ? SHOW_MODE_VIDEO : SHOW_MODE_RDFT;
    
        if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) {
            // 3.3 创建字幕解码线程
            stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);
        }
    
        ......
    
        // 4. 解复用处理
        for (;;) {
            // 停止
            ......
            
            // 暂停/继续
            ......
            
            // seek操作
            ......
    
            ......
            
            // 4.1 从输入文件中读取一个packet
            ret = av_read_frame(ic, pkt);
            if (ret < 0) {
                if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) {
                    // 输入文件已读完,则往packet队列中发送NULL packet,以冲洗(flush)解码器,否则解码器中缓存的帧取不出来
                    if (is->video_stream >= 0)
                        packet_queue_put_nullpacket(&is->videoq, is->video_stream);
                    if (is->audio_stream >= 0)
                        packet_queue_put_nullpacket(&is->audioq, is->audio_stream);
                    if (is->subtitle_stream >= 0)
                        packet_queue_put_nullpacket(&is->subtitleq, is->subtitle_stream);
                    is->eof = 1;
                }
                if (ic->pb && ic->pb->error)    // 出错则退出当前线程
                    break;
                SDL_LockMutex(wait_mutex);
                SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
                SDL_UnlockMutex(wait_mutex);
                continue;
            } else {
                is->eof = 0;
            }
            // 4.2 判断当前packet是否在播放范围内,是则入列,否则丢弃
            /* check if packet is in play range specified by user, then queue, otherwise discard */
            stream_start_time = ic->streams[pkt->stream_index]->start_time; // 第一个显示帧的pts
            pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;
            // 简化一下"||"后那个长长的表达式:
            // [pkt_pts]  - [stream_start_time] - [start_time]                       <= [duration]
            // [当前帧pts] - [第一帧pts]         - [当前播放序列第一帧(seek起始点)pts] <= [duration]
            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);
            // 4.3 根据当前packet类型(音频、视频、字幕),将其存入对应的packet队列
            if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {
                packet_queue_put(&is->audioq, pkt);
            } else if (pkt->stream_index == is->video_stream && pkt_in_play_range
                       && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
                packet_queue_put(&is->videoq, pkt);
            } else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {
                packet_queue_put(&is->subtitleq, pkt);
            } else {
                av_packet_unref(pkt);
            }
        }
    
        ret = 0;
     fail:
        ......
        return 0;
    }
    

    解复用线程实现如下功能:
    [1]. 创建音频、视频、字幕解码线程
    [2]. 从输入文件读取packet,根据packet类型(音频、视频、字幕)将这放入不同packet队列

    3.4 视频解码线程

    视频解码线程从视频packet队列中取数据,解码后存入视频frame队列。

    3.4.1 video_thread()

    视频解码线程将解码后的帧放入frame队列中。为节省篇幅,如下源码中删除了滤镜filter相关代码。

    // 视频解码线程:从视频packet_queue中取数据,解码后放入视频frame_queue
    static int video_thread(void *arg)
    {
        VideoState *is = arg;
        AVFrame *frame = av_frame_alloc();
        double pts;
        double duration;
        int ret;
        AVRational tb = is->video_st->time_base;
        AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
    
        if (!frame) {
            return AVERROR(ENOMEM);
        }
    
        for (;;) {
            ret = get_video_frame(is, frame);
            if (ret < 0)
                goto the_end;
            if (!ret)
                continue;
            
            // 当前帧播放时长
            duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
            // 当前帧显示时间戳
            pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
            // 将当前帧压入frame_queue
            ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
            av_frame_unref(frame);
    
            if (ret < 0)
                goto the_end;
        }
    the_end:
        av_frame_free(&frame);
        return 0;
    }
    

    3.4.2 get_video_frame()

    从packet队列中取一个packet解码得到一个frame,并判断是否要根据framedrop机制丢弃失去同步的视频帧。参考源码中注释:

    static int get_video_frame(VideoState *is, AVFrame *frame)
    {
        int got_picture;
    
        if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0)
            return -1;
    
        if (got_picture) {
            double dpts = NAN;
    
            if (frame->pts != AV_NOPTS_VALUE)
                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);
    
            // ffplay文档中对"-framedrop"选项的说明: 
            //   Drop video frames if video is out of sync.Enabled by default if the master clock is not set to video.
            //   Use this option to enable frame dropping for all master clock sources, use - noframedrop to disable it.
            // "-framedrop"选项用于设置当视频帧失去同步时,是否丢弃视频帧。"-framedrop"选项以bool方式改变变量framedrop值。
            // 音视频同步方式有三种:A同步到视频,B同步到音频,C同步到外部时钟。
            // 1) 当命令行不带"-framedrop"选项或"-noframedrop"时,framedrop值为默认值-1,若同步方式是"同步到视频"
            //    则不丢弃失去同步的视频帧,否则将丢弃失去同步的视频帧。
            // 2) 当命令行带"-framedrop"选项时,framedrop值为1,无论何种同步方式,均丢弃失去同步的视频帧。
            // 3) 当命令行带"-noframedrop"选项时,framedrop值为0,无论何种同步方式,均不丢弃失去同步的视频帧。
            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;
                    }
                }
            }
        }
    
        return got_picture;
    }
    

    ffplay中framedrop处理有两种,一处是此处解码后得到的frame尚未存入frame队列前,以is->frame_drops_early++为标记;另一处是frame队列中读取frame进行显示的时候,以is->frame_drops_late++为标记。
    本处framedrop操作涉及的变量is->frame_last_filter_delay属于滤镜filter操作相关,ffplay中默认是关闭滤镜的,本文不考虑滤镜相关操作。

    3.4.3 decoder_decode_frame()

    这个函数是很核心的一个函数,可以解码视频帧和音频帧。视频解码线程中,视频帧实际的解码操作就在此函数中进行。分析过程参考3.2节。

    3.5 音频解码线程

    音频解码线程从音频packet队列中取数据,解码后存入音频frame队列

    3.5.1 打开音频设备

    音频设备的打开实际是在解复用线程中实现的。解复用线程中先打开音频设备(设定音频回调函数供SDL音频播放线程回调),然后再创建音频解码线程。调用链如下:

    main() -->
    stream_open() -->
    read_thread() -->
    stream_component_open() -->
        audio_open(is, channel_layout, nb_channels, sample_rate, &is->audio_tgt);
        decoder_start(&is->auddec, audio_thread, is);
    

    audio_open()函数填入期望的音频参数,打开音频设备后,将实际的音频参数存入输出参数is->audio_tgt中,后面音频播放线程用会用到此参数。
    音频格式的各参数与重采样强相关,audio_open()的详细实现在后面第5节讲述。

    3.5.2 audio_thread()

    从音频packet_queue中取数据,解码后放入音频frame_queue:

    // 音频解码线程:从音频packet_queue中取数据,解码后放入音频frame_queue
    static int audio_thread(void *arg)
    {
        VideoState *is = arg;
        AVFrame *frame = av_frame_alloc();
        Frame *af;
        int got_frame = 0;
        AVRational tb;
        int ret = 0;
    
        if (!frame)
            return AVERROR(ENOMEM);
    
        do {
            if ((got_frame = decoder_decode_frame(&is->auddec, frame, NULL)) < 0)
                goto the_end;
    
            if (got_frame) {
                    tb = (AVRational){1, frame->sample_rate};
    
                    if (!(af = frame_queue_peek_writable(&is->sampq)))
                        goto the_end;
    
                    af->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
                    af->pos = frame->pkt_pos;
                    af->serial = is->auddec.pkt_serial;
                    // 当前帧包含的(单个声道)采样数/采样率就是当前帧的播放时长
                    af->duration = av_q2d((AVRational){frame->nb_samples, frame->sample_rate});
    
                    // 将frame数据拷入af->frame,af->frame指向音频frame队列尾部
                    av_frame_move_ref(af->frame, frame);
                    // 更新音频frame队列大小及写指针
                    frame_queue_push(&is->sampq);
            }
        } while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF);
     the_end:
        av_frame_free(&frame);
        return ret;
    }
    

    3.5.3 decoder_decode_frame()

    此函数既可以解码音频帧,也可以解码视频帧,函数分析参考3.2节。

    3.6 音频播放线程

    音频播放线程是SDL内建的线程,通过回调的方式调用用户提供的回调函数。
    回调函数在SDL_OpenAudio()时指定。
    暂停/继续回调过程由SDL_PauseAudio()控制。

    3.6.1 sdl_audio_callback()

    音频回调函数如下:

    // 音频处理回调函数。读队列获取音频包,解码,播放
    // 此函数被SDL按需调用,此函数不在用户主线程中,因此数据需要保护
    // param[in]  opaque 用户在注册回调函数时指定的参数
    // param[out] stream 音频数据缓冲区地址,将解码后的音频数据填入此缓冲区
    // param[out] len    音频数据缓冲区大小,单位字节
    // 回调函数返回后,stream指向的音频缓冲区将变为无效
    // 双声道采样点的顺序为LRLRLR
    /* prepare a new audio buffer */
    static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
    {
        VideoState *is = opaque;
        int audio_size, len1;
    
        audio_callback_time = av_gettime_relative();
    
        while (len > 0) {   // 输入参数len等于is->audio_hw_buf_size,是audio_open()中申请到的SDL音频缓冲区大小
            if (is->audio_buf_index >= is->audio_buf_size) {
               // 1. 从音频frame队列中取出一个frame,转换为音频设备支持的格式,返回值是重采样音频帧的大小
               audio_size = audio_decode_frame(is);
               if (audio_size < 0) {
                    /* if error, just output silence */
                   is->audio_buf = NULL;
                   is->audio_buf_size = SDL_AUDIO_MIN_BUFFER_SIZE / is->audio_tgt.frame_size * is->audio_tgt.frame_size;
               } else {
                   if (is->show_mode != SHOW_MODE_VIDEO)
                       update_sample_display(is, (int16_t *)is->audio_buf, audio_size);
                   is->audio_buf_size = audio_size;
               }
               is->audio_buf_index = 0;
            }
            // 引入is->audio_buf_index的作用:防止一帧音频数据大小超过SDL音频缓冲区大小,这样一帧数据需要经过多次拷贝
            // 用is->audio_buf_index标识重采样帧中已拷入SDL音频缓冲区的数据位置索引,len1表示本次拷贝的数据量
            len1 = is->audio_buf_size - is->audio_buf_index;
            if (len1 > len)
                len1 = len;
            // 2. 将转换后的音频数据拷贝到音频缓冲区stream中,之后的播放就是音频设备驱动程序的工作了
            if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
                memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
            else {
                memset(stream, 0, len1);
                if (!is->muted && is->audio_buf)
                    SDL_MixAudioFormat(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, AUDIO_S16SYS, len1, is->audio_volume);
            }
            len -= len1;
            stream += len1;
            is->audio_buf_index += len1;
        }
        // is->audio_write_buf_size是本帧中尚未拷入SDL音频缓冲区的数据量
        is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;
        /* Let's assume the audio driver that is used by SDL has two periods. */
        // 3. 更新时钟
        if (!isnan(is->audio_clock)) {
            // 更新音频时钟,更新时刻:每次往声卡缓冲区拷入数据后
            // 前面audio_decode_frame中更新的is->audio_clock是以音频帧为单位,所以此处第二个参数要减去未拷贝数据量占用的时间
            set_clock_at(&is->audclk, is->audio_clock - (double)(2 * is->audio_hw_buf_size + is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec, is->audio_clock_serial, audio_callback_time / 1000000.0);
            // 使用音频时钟更新外部时钟
            sync_clock_to_slave(&is->extclk, &is->audclk);
        }
    }
    

    3.6.2 audio_decode_frame()

    audio_decode_frame()主要是进行音频重采样,从音频frame队列中取出一个frame,此frame的格式是输入文件中的音频格式,音频设备不一定支持这些参数,所以要将frame转换为音频设备支持的格式。
    audio_decode_frame()的实现在后面第5节讲述。

    3.7 字幕解码线程

    实现细节略。以后有机会研究字幕时,再作补充。

  • 相关阅读:
    阿里云的一道面试题:写一个爬取文档树和通过输入关键字检索爬取的内容的demo
    linux配置SVN,添加用户,配置用户组的各个权限教程
    logback的使用和配置|logback比log4j的优点|logback是一个更好的log4j
    [已解决]mysql查询一周内的数据,解决一周的起始日期是从星期日(星期天|周日|周天)开始的问题
    MySql-----InnoDB记录存储结构-----1
    Mysql----字符集和比较规则
    Mysql-----启动和配置文件-----2(未完,待续)
    MySql----前言有点用----1
    Java高并发--------并行模式和算法(需要看更多的东西,才能总结)---------5
    Java高并发------锁优化及注意事项--------4
  • 原文地址:https://www.cnblogs.com/leisure_chn/p/10301831.html
Copyright © 2011-2022 走看看