ffplay播放器研究分析

ffplay研究分析意义

ffplay.c是FFmpeg源码⾃带的播放器,调⽤FFmpeg和SDL API实现⼀个⾮常有⽤的播放器。 例如哔哩哔哩著名开源项⽬ijkplayer也是基于ffplay.c进⾏⼆次开发

ffplay实现了播放器的主体功能,掌握其原理对于我们独⽴开发播放器⾮常有帮助。

FFplay框架分析

内容涉及:

1. 队列设计与管理

  • Packet队列设计
    • 线程安全(支持互斥、等待、唤醒)
    • 缓存数据大小
    • 缓存包数
    • 队列播放可持续时间
    • 进队列/出队列操作
  • Frame队列设计
    • 线程安全(支持互斥、等待、唤醒)
    • 缓存帧数
    • 支持读取数据而不移除队列中的元素
    • 进队列/出队列操作

2. 音视频同步

  • 音频同步
  • 视频同步
  • 外部时钟同步

3. 音频处理

  • 音量调节
  • 静音
  • 重采样

4. 视频处理

  • 图像格式转换(如YUV到RGB)
  • 图像缩放(如从1280*720到800*480)

5. 播放器控制

  • 播放
  • 暂停
  • 停止
  • 快进/快退
  • 逐帧播放
  • 静音

数据结构分析

VideoState 播放器封装

typedef struct VideoState {
    SDL_Thread *read_tid;		// 读线程句柄
    AVInputFormat *iformat;		// 输入指定格式 指向demuxer
    int abort_request;			// =1时请求退出播放
    int force_refresh;			// =1时需要刷新画⾯,请求⽴即刷新画⾯的意思
    int paused;		// =1时暂停,=0时播放
    int last_paused;	// 暂存“暂停”/“播放”状态
    int queue_attachments_req;	
    int seek_req;		// 标识1次seek请求
    int seek_flags;		// seek标志,诸如AVSEEK_FLAG_BYTE等
    int64_t seek_pos;	// 请求seek的目标位置(当前位置+增量)
    int64_t seek_rel;	// 本次seek的位置增量
    int read_pause_return;
    AVFormatContext *ic;	// iformat的上下文
    int realtime;		// =1为实时流

    Clock audclk;	// 音频时钟
    Clock vidclk;	// 视频时钟
    Clock extclk;	// 外部时钟

    FrameQueue pictq;	// 视频Frame队列
    FrameQueue subpq;	// 字幕Frame队列
    FrameQueue sampq;	// 采样Frame队列

    Decoder auddec;	// 音频解码器
    Decoder viddec;	// 视频解码器
    Decoder subdec;	// 字幕解码器

    int audio_stream;	// 音频流索引

    int av_sync_type;	// 音视频同步类型, 默认audio master

    double audio_clock; // 当前音频帧的PTS+当前帧Du
    int audio_clock_serial;	// 播放序列,seek可改变此值


	 // 以下4个参数 非audio master同步方式使用
    double audio_diff_cum; /* used for AV difference average computation */
    double audio_diff_avg_coef;
    double audio_diff_threshold;
    int audio_diff_avg_count;
	// end

	
    AVStream *audio_st;		// 音频流
    PacketQueue audioq;			// 音频packet队列
    int audio_hw_buf_size;	// SDL音频缓冲区的大小(字节为单位)
    // 指向待播放的1帧音频数据,指向的数据区将被拷贝SDL音频缓冲区。若经过重采样则指向audio_buf1,否则指向frame中的音频
    uint8_t *audio_buf;		// 指向需要重采样的数据
    uint8_t *audio_buf1;	// 指向重采样后的数据
    unsigned int audio_buf_size; /* 待播放的1帧音频数据(audio_buf指向)的大小 in bytes */
    unsigned int audio_buf1_size;	// 申请到的音频缓冲区audio_buf1的实际尺寸
    
    int audio_buf_index; /* 更新拷贝位置 当前音频帧中已拷入SDL音频缓冲区 in bytes */
	// 当前音频帧中尚未拷入SDL音频缓冲区的数据量:
	// audio_buf_size = audio_buf_index + audio_write_buf_size
    int audio_write_buf_size;
    int audio_volume;	// 音量
    int muted;		// =1静音,=0则正常
    struct AudioParams audio_src;	// 音频frame的参数
#if CONFIG_AVFILTER
    struct AudioParams audio_filter_src;
#endif
    struct AudioParams audio_tgt;	// SDL支持的音频参数,重采样转换:audio_src->audio_tgt
    struct SwrContext *swr_ctx;		// 音频重采样context
    int frame_drops_early;	// 丢弃视频packet计数
    int frame_drops_late;	// 丢弃视频frame计数

    enum ShowMode {
        SHOW_MODE_NONE = -1, SHOW_MODE_VIDEO = 0, SHOW_MODE_WAVES, SHOW_MODE_RDFT, SHOW_MODE_NB
    } show_mode;

	
	// 音频波形显示使用
    int16_t sample_array[SAMPLE_ARRAY_SIZE];
    int sample_array_index;
    int last_i_start;
    RDFTContext *rdft;
    int rdft_bits;
    FFTSample *rdft_data;
    int xpos;
    double last_vis_time;
    SDL_Texture *vis_texture;
    SDL_Texture *sub_texture; // 字幕显示
    SDL_Texture *vid_texture; // 视频显示

    int subtitle_stream;	// 字幕流索引
    AVStream *subtitle_st;	// 字幕流
    PacketQueue subtitleq;	// 字幕packet队列

    double frame_timer;	// 记录最后一帧播放的时刻
    double frame_last_returned_time;
    double frame_last_filter_delay;

	
    int video_stream;	// 视频流索引
    AVStream *video_st;	// 视频流
    PacketQueue videoq;	// 视频队列
    double max_frame_duration;      // ⼀帧最⼤间隔. maximum duration of a frame - above this, we consider the jump a timestamp discontinuity
    struct SwsContext *img_convert_ctx;  // 视频尺寸格式变换
    struct SwsContext *sub_convert_ctx;	// 字幕尺寸格式变换
    int eof;	// 是否读取结束

    char *filename;	// 文件名
    int width, height, xleft, ytop;	 // 宽、高,x起始坐标,y起始坐标
    int step;		// =1 步进播放模式, =0 其他模式

#if CONFIG_AVFILTER
    int vfilter_idx;
    AVFilterContext *in_video_filter;   // the first filter in the video chain
    AVFilterContext *out_video_filter;  // the last filter in the video chain
    AVFilterContext *in_audio_filter;   // the first filter in the audio chain
    AVFilterContext *out_audio_filter;  // the last filter in the audio chain
    AVFilterGraph *agraph;              // audio filter graph
#endif
	// 保留最近的相应audio、video、subtitle流的steam index
    int last_video_stream, last_audio_stream, last_subtitle_stream;
	// 当读取数据队列满了后进入休眠时,可以通过该condition唤醒读线程
    SDL_cond *continue_read_thread;
} VideoState;

Clock 时钟封装

typedef struct Clock {
    double pts;           /* 时钟基础, 当前帧(待播放)显示时间戳,播放后,当前帧变成上⼀帧 */
    double pts_drift;     /* 当前pts与当前系统时钟的差值, audio、video对于该值是独⽴的 */
    // 当前时钟(如视频时钟)最后⼀次更新时间,也可称当前时钟时间
    double last_updated; // 最后⼀次更新的系统时钟
    double speed;	// 时钟速度控制,⽤于控制播放速度
    
   // 播放序列,所谓播放序列就是⼀段连续的播放动作,⼀个seek操作会启动⼀段新的播放序列 
    int serial;           /* clock is based on a packet with this serial */
    int paused;	// = 1 说明是暂停状态
     // 指向packet_serial
    int *queue_serial;    /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;

MyAVPacketList和PacketQueue队列

注意:
⾳频、视频、字幕流都有⾃⼰独⽴的PacketQueue。
这⾥也看到了serial字段,MyAVPacketList的serial字段的赋值来⾃PacketQueue的serial,每个PacketQueue的serial是独⽴的。

typedef struct MyAVPacketList {
    AVPacket pkt; //解封装后的数据
    struct MyAVPacketList *next; //下⼀个节点
    int serial; //播放序列

} MyAVPacketList;

serial字段主要⽤于标记当前节点的播放序列号,
ffplay中多处⽤到serial的概念,主要⽤来区分是否连续数据,每做⼀次seek,该serial都会做+1的递增,以区分不同的播放序列。类似二维数组比如PacketQueue.ser=1 则MyAVPacketList.ser=2 ... MyAVPacketList.ser=4
PacketQueue.ser=2 则MyAVPacketList.ser=2 ... MyAVPacketList.ser=4


该结构体内定义了“队列”⾃身的属性
typedef struct PacketQueue {
    MyAVPacketList *first_pkt, *last_pkt;// 队⾸,队尾指针
    int nb_packets;// 包数量,也就是队列元素数量
    int size;// 队列所有元素的数据⼤⼩总和

    int64_t duration;// 队列所有元素的数据播放持续时间
    int abort_request;// ⽤户退出请求标志
    int serial;// 播放序列号,和MyAVPacketList的serial作⽤相同
    SDL_mutex *mutex;// ⽤于维持PacketQueue的多线程安全(SDL_mutex可以按pthread_mutex_t理解)
    SDL_cond *cond;// ⽤于读、写线程相互通知(SDL_c
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值