ffplay源码分析__stream_open函数(二)

前言

FFplay是 FFmpeg 官方提供的一个播放器的实现,支持快进快退,逐帧播放,滤镜等。本文是在VS2017的Debug x86模式下调试运行的,ffplay.c移植到VS2017工程可以参考
ffplay移植vs2017https://mp.csdn.net/mp_blog/creation/editor/143884981

stream_open函数主要是做了些初始化工作,并创建了读文件线程read_thread。先分析函数中出现的几个结构体。

一、数据结构

1、VideoState结构体

是ffplay的全局管理器,记录各种数据状态(时钟,队列,解码器等)。先看VideoState的部分字段

1)read_tid :read_thread线程ID,由SDL_CreateThread()返回

2)  AVInputFormat *iformat:媒体文件容器

3)Clock audclk:音频时钟,Clock vidclk:视频时钟,Clock extclk:外部时钟

4)FrameQueue pictq:视频帧队列,存放解码后的视频数据

     FrameQueue subpq:字幕帧队列,存放解码后的字幕数据

     FrameQueue sampq:音频帧队列,存放解码后的音频数据

5)Decoder auddec:音频解码器,存放音频解码器相关的参数
     Decoder viddec:视频解码器
     Decoder subdec:字幕解码器

6)av_sync_type:时钟同步方式,默认以音频时钟为主

7)last_video_stream:代表最后一个视频流,另外两个 last_audio_stream, last_subtitle_stream一样

8)char* filename:音视频文件名或者网络地址url。

9)width, height, xleft, ytop:分别代表SDL播放器窗口的宽,高,左偏移和上偏移。

10)PacketQueue videoq:视频AVPacket 队列

PacketQueue audioq:音频AVPacket 队列

PacketQueue subtitleq:字幕AVPacket 队列

11)continue_read_thread:条件变量,当解码队列为空或视频跳转时发送信号给read_thread,通知read_thread立即读取数据

2、MyAVPacketList结构体

typedef struct MyAVPacketList {
    AVPacket *pkt;
    int serial;
} MyAVPacketList;

MyAVPacketList结构体包含了AVPacket *pkt和 int serial两个字段,增加serial字段是为了给AVPacket添加序列号标识,跳转的时候serial字段会变。

3、PacketQueue结构体

typedef struct PacketQueue {
    AVFifoBuffer *pkt_list; //ffmpeg实现的先进先出的队列
    int nb_packets; // 队列中AVPacket数量
    int size; //队列占用内存大小
    int64_t duration; // 队列所有数据包的持续时间
    int abort_request; //是否结束
    int serial; //队列序列号
    SDL_mutex *mutex; //队列锁
    SDL_cond *cond;//队列条件变量
} PacketQueue;

PacketQueue 是解码前的 Packet 队列,用于保存解封装后的数据。初始化 PacketQueue 时,会对 VideoState 中的 videoq(视频包队列)、audio(音频包队列)、subtitleq(字幕包队列)依次调用packet_queue_init函数进行初始化。不同于 FrameQueue , PacketQueue 采用链表的方式实现队列。由于解码前的包大小不可控,无法明确缓冲区的最大容量,如果使用环形缓冲区,容易触发缓冲区扩容,需要移动缓冲区内的数据。因此,使用链表实现队列更加合适。其中AVFifoBuffer是ffmpeg实现的一个先进先出的队列,队列中存放的每个元素是MyAVPacketList结构体。

4、Frame结构体

/* Common struct for handling all types of decoded data and allocated render buffers. */
typedef struct Frame {
    AVFrame *frame; //AVFrame的指针
    AVSubtitle sub; //字幕指针
    int serial; //Frame的序列号
    double pts;  //显示时间戳         /* presentation timestamp for the frame */
    double duration;//AVFrame的持续时长 /* estimated duration of the frame */
    int64_t pos; //在文件的字节位置   /* byte position of the frame in the input file */
    int width;//视频宽度
    int height;//视频高度
    int format;//AVFrame格式
    AVRational sar;//缩放比
    int uploaded;//是否上传到纹理
    int flip_v;
} Frame;

对AVFrame的又一次封装,补充AVFrame的相关信息(比如序列号,宽高,显示时刻等) 

5、FrameQueue结构体

typedef struct FrameQueue {
    Frame queue[FRAME_QUEUE_SIZE]; //Frame数组
    int rindex;//队列读索引
    int windex;//队列写索引
    int size;//队列大小
    int max_size;//队列最大值
    int keep_last;//是否保留最后一帧,只有视频保留最后一帧
    int rindex_shown;//配合keep_last用
    SDL_mutex *mutex;//队列锁
    SDL_cond *cond;//队列条件变量
    PacketQueue *pktq;//对应的PacketQueue
} FrameQueue;

FrameQueue 是解码后的 Frame 队列, Frame 是解码后的数据,例如视频解码后是 YUV 或 RGB 数据,音频解码后是 PCM 数据。初始化 FrameQueue 时,会对 VideoState 中的 pictq(视频帧队列)、subpq(字幕帧队列)、sampq(音频帧队列)依次调用frame_queue_init()函数进行初始化。FrameQueue 内部是通过数组实现了一个先进先出的环形缓冲区,windex 是写指针,被解码线程使用;rindex 是读指针,被播放线程使用。使用环形缓冲区的好处是,缓冲区内的元素被移除后,其它元素不需要移动位置,适用于事先知道缓冲区最大容量的场景。 

6、Clock结构体

typedef struct Clock {
    double pts; //时钟校时的时刻  /* clock base */
    double pts_drift; //时钟的偏移值 /* clock base minus time at which we updated the clock */
    double last_updated;//最近一次更新的系统时间
    double speed;//播放速度
    int serial;//时钟序列号 /* clock is based on a packet with this serial */
    int paused;//是否暂停状态
    int *queue_serial; //指向对应PacketQueue的序列号   /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

Clock 是时钟,在音视频同步阶段,有三种同步方法:视频同步到音频,音频同步到视频,以及音频和视频同步到外部时钟。ffplay默认采用视频同步到音频的策略。初始化 Clock 时,会对 VideoState 中的 vidclk(视频时钟)、audclk(音频时钟)、extclk(外部时钟)依次调用init_clock函数进行初始化。 

 pts_drift是为了计算时钟校时后,再次读取时钟时,经过了多长时间用的。

二、流程图

三、源码分析

1、VideoState初始化

    VideoState *is;

    is = av_mallocz(sizeof(VideoState));
    if (!is)
        return NULL;
    is->last_video_stream = is->video_stream = -1;
    is->last_audio_stream = is->audio_stream = -1;
    is->last_subtitle_stream = is->subtitle_stream = -1;
    is->filename = av_strdup(filename);
    if (!is->filename)
        goto fail;
    is->iformat = iformat;
    is->ytop    = 0;
    is->xleft   = 0;

VideoState 是 ffplay 中最大的结构体,所有的视频信息都定义在其中。初始化 VideoState 时,先定义 VideoState 结构体指针类型的局部变量is,分配堆内存。然后初始化结构体中的变量,例如视频流、音频流、字幕流的索引ID初始为-1,初始化了filename及媒体容器,以及初始化SDL窗口的显示位置。

av_mallocz函数除了申请内存外,还把申请的内存置为零。

2、FrameQueue初始化

调用frame_queue_init函数初始化音频,视频,字幕队列

    /* start video display 第四个参数是keep_last*/
    if (frame_queue_init(&is->pictq, &is->videoq, VIDEO_PICTURE_QUEUE_SIZE, 1) < 0)
        goto fail;
    if (frame_queue_init(&is->subpq, &is->subtitleq, SUBPICTURE_QUEUE_SIZE, 0) < 0)
        goto fail;
    if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0)
        goto fail;

第一个参数类型是FrameQueue,第二个参数类型是PacketQueue,第三个参数是对应FrameQueue中Frame数组的大小(对于视频是3,对于字幕是16,音频是9),第四个参数代表是否保留最后一帧,只有视频FrameQueue才保留最后一帧。

frame_queue_init函数内部代码如下:

static int frame_queue_init(FrameQueue *f, PacketQueue *pktq, int max_size, int keep_last)
{
    int i;
    memset(f, 0, sizeof(FrameQueue));
    if (!(f->mutex = SDL_CreateMutex())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    if (!(f->cond = SDL_CreateCond())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    f->pktq = pktq;
    f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE);
    f->keep_last = !!keep_last;
    for (i = 0; i < f->max_size; i++)
        if (!(f->queue[i].frame = av_frame_alloc()))
            return AVERROR(ENOMEM);
    return 0;
}

frame_queue_init函数创建了锁和条件变量,同时绑定对应的PacketQueue,并对Frame数组做了初始化。初始化时,只初始化了参数对应max_size大小的数组空间。

f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE);

还有keep_last两次取反,把非零值转成1代表true,因为C语言没有布尔型。

f->keep_last = !!keep_last;

3、PacketQueue初始化

packet_queue_init函数内部,通过av_fifo_alloc创建了AVFifoBuffer,AVFifoBuffer是FFmpeg 项目中的 fifo 的实现 ,fifo 的全称是 first in first out (先进先出),这是一个环形的buffer内存管理器,可以认为是一个环形队列,而且能动态的扩展容量。每一个元素是一个MyAVPacketList结构体元素。

初始化了锁和条件变量。

q->abort_request = 1先初始化为1,后面会初始化为0。abort_request代表是否中止请求。

4、创建条件变量continue_read_thread

if (!(is->continue_read_thread = SDL_CreateCond())) {
    av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
    goto fail;
}

continue_read_thread在解码线程中会用到,当解码时PacketQueue无数据可解码了,会发送信号给read_thread线程立即解码。另外跳转时也会用到,也是通知read_thread线程立即读取数据以便跳转。

5、初始化时钟

调用init_clock函数初始化三个时钟

init_clock(&is->vidclk, &is->videoq.serial);
init_clock(&is->audclk, &is->audioq.serial);
init_clock(&is->extclk, &is->extclk.serial);
is->audio_clock_serial = -1;
static void init_clock(Clock *c, int *queue_serial)
{
    c->speed = 1.0;
    c->paused = 0;
    c->queue_serial = queue_serial;
    set_clock(c, NAN, -1);
}

在init_clock的内部函数中,时钟的queue_serial是个指针,指向PacketQueue的serial字段。

static void set_clock(Clock *c, double pts, int serial)
{
    double time = av_gettime_relative() / 1000000.0;
    set_clock_at(c, pts, serial, time); //初始时 pts为NAN,serial为-1
}

在set_clock内部函数中,av_gettime_relative()函数是个相对时间,是从开机到现在的微秒数(1微妙=100 0000秒),所以time变量的单位是秒,set_clock又调用了set_clock_at函数,

//初始时 pts为NAN serial为-1
static void set_clock_at(Clock *c, double pts, int serial, double time)
{
	c->pts = pts;
    c->last_updated = time;
    c->pts_drift = c->pts - time;
    c->serial = serial;
}

在set_clock_at()的内部函数中,此时pts初始为NAN,last_update初始为系统时间,参数serial初始为-1,queue_serial指向对应的PacketQueue的serial字段。

6、初始化音频相关

if (startup_volume < 0)
    av_log(NULL, AV_LOG_WARNING, "-volume=%d < 0, setting to 0\n", startup_volume);
if (startup_volume > 100)
    av_log(NULL, AV_LOG_WARNING, "-volume=%d > 100, setting to 100\n", startup_volume);
startup_volume = av_clip(startup_volume, 0, 100);
startup_volume = av_clip(SDL_MIX_MAXVOLUME * startup_volume / 100, 0, SDL_MIX_MAXVOLUME);
is->audio_volume = startup_volume;

 

先限制音量范围在 0~100 之间,然后再根据 SDL 的音量范围作进一步限制。

startup_volume是个全局静态变量,初始值为100,通过第一次av_clip剪裁后是100,第二次调用av_clip函数后是128,然后赋值给is->audio_volume字段。av_clip(a,b,c)函数的作用是,给定数值区间[b,c],如果a在区间左边,返回最小值b,如果a在区间右边,返回最大值c,如果a在区间中,直接返回a。

is->muted = 0;

is->muted = 0;  是否混音初始为0

is->av_sync_type = av_sync_type;

av_sync_type是静态全局变量,代表时钟同步方式,默认是以音频时钟为主,其他时钟都向音频时钟同步

7、创建read_thread线程

is->read_tid = SDL_CreateThread(read_thread, "read_thread", is);
if (!is->read_tid) {
    av_log(NULL, AV_LOG_FATAL, "SDL_CreateThread(): %s\n", SDL_GetError());
fail:    
    stream_close(is);
    return NULL;
}

通过SDL_CreateThread创建read_thread线程,并返回线程ID。如果线程ID返回0,代表创建线程失败,并用stream_close函数关闭read_thread线程。read_thread线程详见链接:

read_thread线程分析(三)-CSDN博客

8、返回值

将局部变量is作为函数返回值返回,用于处理各种 SDL 事件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值