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; // 输入指定格式 指向demuxerint 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 masterdouble audio_clock; // 当前音频帧的PTS+当前帧Duint 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;// endAVStream *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_sizeint audio_write_buf_size;int audio_volume; // 音量int muted; // =1静音,=0则正常struct AudioParams audio_src; // 音频frame的参数
#if CONFIG_AVFILTERstruct AudioParams audio_filter_src;
#endifstruct AudioParams audio_tgt; // SDL支持的音频参数,重采样转换:audio_src->audio_tgtstruct SwrContext *swr_ctx; // 音频重采样contextint 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 discontinuitystruct 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_AVFILTERint vfilter_idx;AVFilterContext *in_video_filter; // the first filter in the video chainAVFilterContext *out_video_filter; // the last filter in the video chainAVFilterContext *in_audio_filter; // the first filter in the audio chainAVFilterContext *out_audio_filter; // the last filter in the audio chainAVFilterGraph *agraph; // audio filter graph
#endif// 保留最近的相应audio、video、subtitle流的steam indexint 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_serialint *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_cond可以按pthread_cond_t理解)} PacketQueue;
函数api
PacketQueue 操作提供以下⽅法:
packet_queue_init:初始化⽤于初始各个字段的值,并创建mutex和cond
packet_queue_destroy:销毁过程负责清理mutex和cond:
packet_queue_start:启动队列,会插入一个flush_pkt(并触发q.serial+1),它是⼀个特殊的packet,主要⽤来作为⾮连续的两端数据的“分界”标记:packet_queue_abort:中⽌队列 设置退出标志位并唤醒其他线程:/**
2 * @brief packet_queue_get 从队头移出数据并返回,线程安全。
3 * @param q 队列
4 * @param pkt 输出参数,即MyAVPacketList.pkt
5 * @param block 传入0阻塞等待 1非阻塞
6 * @param serial 输出参数,即MyAVPacketList.serial
7 * @return <0: aborted; =0: no packet; >0: has packet
8 */
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial)static int packet_queue_put(PacketQueue *q, AVPacket *pkt)
往队列中放⼊⼀个节点,如果插⼊失败,则需要释放pkt,底层调用packet_queue_put_privatestatic int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
主要是在堆构造MyAVPacketList初始化加入队列以及设置q的相关参数。并发出加入队列信号packet_queue_put_nullpacket:存⼊⼀个空节点,放⼊空包意味着流的结束,⼀般在媒体数据读取完成的时候放⼊空包。放⼊
空包,⽬的是为了冲刷解码器,将编码器⾥⾯所有frame都读取出来:
packet_queue_flush:清除队列内所有的节点,包括节点对应的AVPacket。主要用在 退出和seek“洗地”
MyAVPacketList的内存是完全由PacketQueue维护的,在put的时候malloc,在get的时候free。
AVPacket分两块:
⼀部分是AVPacket结构体的内存,这部分从MyAVPacketList的定义可以看出是和MyAVPacketList共存亡的。
另⼀部分是AVPacket字段指向的内存,这部分⼀般通过 av_packet_unref 函数释放。⼀般情况下,是在get后由调⽤者负责⽤ av_packet_unref 函数释放。特殊的情况是当碰到
packet_queue_flush 或put失败时,这时需要队列⾃⼰处理。
serial的变化过程:
小总结
PacketQueue设计思路:
-
设计⼀个多线程安全的队列
-
引⼊serial的概念,区别前后数据包是否连续,主要应⽤于seek操作。
-
设计了两类特殊的packet——flush_pkt和nullpkt(类似⽤于多线程编程的事件模型——往队列中放⼊ flush事件、放⼊null事件),我们在⾳频输出、视频输出、播放控制等模块时也会继续对flush_pkt和 nullpkt的作⽤展开分析。
Frame 和 FrameQueue队列
1.FrameQueue是⼀个环形缓冲区(ring buffer),是⽤数组(这样设计为了快速访问)实现的⼀个FIFO。
2.ffplay中创建了三个frame_queue:⾳频frame_queue,视频frame_queue,字幕frame_queue。每⼀个frame_queue⼀个写端⼀个读端,写端位于解码线程,读端位于播放线程。
3.FrameQueue的设计⽐如PacketQueue复杂,引⼊了读取节点但节点不出队列的操作、读取下⼀节点也不出队列等等的操作
/* 处理所有类型的解码数据和分配的渲染缓冲区的通用结构. */
typedef struct Frame {AVFrame *frame; // 指向数据帧AVSubtitle sub; // ⽤于字幕int serial; // 播放序列,在seek的操作时serial会变化double pts; // 时间戳,单位为秒double duration; // 该帧持续时间,单位为秒int64_t pos; // 该帧在输⼊⽂件中的字节位置int width; // 图像宽度int height;// 图像⾼读int format;// 对于图像为(enum AVPixelFormat),// 对于声⾳则为(enum AVSampleFormat)AVRational sar; // 图像的宽⾼⽐,如果未知或未指定则为0/1 图像的宽⾼⽐(16:9,4:3...),该值来
⾃AVFrame结构体的sample_aspect_ratio变量)。int uploaded; // ⽤来记录该帧是否已经显示过?int flip_v; // =1则旋转180, = 0则正常播放
} Frame;typedef struct FrameQueue {Frame queue[FRAME_QUEUE_SIZE]; // FRAME_QUEUE_SIZE 最⼤size, 数字太⼤时会占⽤⼤量的内存,需要注意该值的设置 实际分配的时候视
频为3,⾳频为9,字幕为16int rindex; // 读索引。待播放时读取此帧进⾏播
放,播放后此帧成为上⼀帧int windex; // 写索引int size; // 当前总帧数int max_size; // 可存储最⼤帧数int keep_last; // = 1说明要在队列⾥⾯保持最后⼀
帧的数据不释放,只在销毁队列的时候才将其真正释放int rindex_shown; // 初始化为0,配合keep_last=1
使⽤SDL_mutex *mutex; // 互斥量SDL_cond *cond; // 条件变量PacketQueue *pktq; // 数据包缓冲队列
} FrameQueue;
函数api
frame_queue_unref_item:释放Frame⾥⾯的AVFrame和 AVSubtitlestatic int frame_queue_init(FrameQueue *f, PacketQueue *pktq, int ma
x_size, int keep_last) 初始化队列,设置f基本参数.此函数不会分配frame.data的内存。
frame_queue_destory:销毁队列 会释放frame,并解除frame.data引用 frame_queue_signal:发送唤醒信号frame_queue_peek_last:获取上⼀Frame
frame_queue_peek:获取当前Frame,调⽤之前先调⽤frame_queue_nb_remaining确保有frame可读
frame_queue_peek_next:获取当前Frame的下⼀Frame,调⽤之前先调⽤frame_queue_nb_remaining确保⾄少有2 Frame在队列frame_queue_peek_writable:获取⼀个可写Frame,可以以阻塞或⾮阻塞⽅式进⾏
frame_queue_peek_readable:获取⼀个可读Frame,可以以阻塞或⾮阻塞⽅式进⾏frame_queue_push:更新写索引,此时Frame才真正⼊队列,队列节点Frame个数加1
frame_queue_next:更新读索引,此时Frame才真正出队列,队列节点Frame个数减1,内部调⽤frame_queue_unref_item是否对应的AVFrame和AVSubtitleframe_queue_nb_remaining:获取队列Frame节点个数
frame_queue_last_pos:获取最近播放Frame对应数据在媒体⽂件的位置,主要在seek时使⽤
关于引⼊了是否保留已显示的最后⼀帧的keep_last机制。
在启⽤keep_last机制后,rindex_shown值总是为1,rindex_shown确保了最后播放的⼀帧总保留在队列 中。
static int frame_queue_nb_remaining(FrameQueue *f)
{return f->size - f->rindex_shown;
}static void frame_queue_next(FrameQueue *f)
{if (f->keep_last && !f->rindex_shown) { //开启保留机制f->rindex_shown = 1; // 第⼀次调⽤置为1return;}frame_queue_unref_item(&f->queue[f->rindex]);if (++f->rindex == f->max_size)f->rindex = 0;SDL_LockMutex(f->mutex);f->size--;SDL_CondSignal(f->cond);SDL_UnlockMutex(f->mutex);
}
AudioParams ⾳频参数
typedef struct AudioParams {int freq; // 采样率int channels; // 通道数int64_t channel_layout; // 通道布局,⽐如2.1声道,5.1声道等enum AVSampleFormat fmt; // ⾳频采样格式,⽐如AV_SAMPLE_FM
T_S16表示为有符号16bit深度,交错排列模式。int frame_size; // ⼀个采样单元占⽤的字节数(⽐如2通道时,则左右通道各采样⼀次合成⼀个采样单元)int bytes_per_sec; // ⼀秒时间的字节数,⽐如采样率48Khz,2
channel,16bit,则⼀秒48000*2*16/8=192000
} AudioParams;
Decoder解码器封装
typedef struct Decoder {AVPacket pkt;PacketQueue *queue; // 数据包队列AVCodecContext *avctx;// 解码器上下⽂int pkt_serial; // 包序列int finished; // =0,解码器处于⼯作状态;=⾮0,解码器处于空闲状态int packet_pending; // =0,解码器处于异常状态,需要考虑重置解码器;=1,解码器处于正常状态SDL_cond *empty_queue_cond;// 检查到packet队列空时发送 signal缓存read_thread读取数据int64_t start_pts; // 初始化时是stream的start timeAVRational start_pts_tb;// 初始化时是stream的time_baseint64_t next_pts; // 记录最近⼀次解码后的frame的pts,当解出来的部分帧没有有效的pts时则使⽤next_pts进⾏推算AVRational next_pts_tb; //next_pts单位SDL_Thread *decoder_tid; // 线程句柄
} Decoder;
ffplay解决方案
数据读取线程
- seek怎么做
- -ss 起始播放
seek的指定的位置开始播放 avformat_seek_file
然后执行packet_queue_flush操作 清空队列数据。
- -ss 起始播放
- 数据播放完毕和码流数据读取完毕
-
数据播放完毕指pktqueue和framequeue这两个队列都没有了数据,即视频或音频等媒体文件已经完全播放结束
码流读取完毕指编码数据已经完全被读取,这可能发生在播放过程中,也可能在文件传输或下载过程中。在某些情况下,如果媒体文件被设计为循环播放,那么“码流数据读取完毕”可能并不意味着播放结束,因为播放器可能会重新开始读取码流数据。 -
5 检测pkt队列是否已经有足够数据
缓存队列满,则等待
1.非无限缓冲区
2. audioq,videoq,subtitleq三个PacketQueue的总字节数达到了MAX_QUEUE_SIZE(15M)
或 ⾳频、视频、字幕流都已有可用的包
可用的:
1. 流已打开(stream_id > 0)
2. 非退出请求(queue->abort_request)
3. 非配置了AV_DISPOSITION_ATTACHED_PIC
4. packet队列内包个数⼤于MIN_FRAMES(>25),并满⾜PacketQueue总时⻓为0或总时⻓超过1s 队列有数据并是有效时长
思考推理:总数据⼤⼩
每个packet队列的情况。
-
- 循环播放
- 8 检测数据是否读取完毕
static int loop = 1; // 设置循环次数 控制
当loop为循环播放时,会重新seek播放。
- 8 检测数据是否读取完毕
- 指定播放位置
- 9 检测是否在播放范围内 -ss -t
主要对播放位置进行dur检测,是否合规。
- 9 检测是否在播放范围内 -ss -t
音视频解码线程
- flush_pkt的作⽤
- 主要作用清空pkt队列缓存,实现音视频同步seek移动,当然切换音轨也需要加入flush_pkt。
- Decoder的packet_pending和pkt的作⽤
- packet_pending它用来标记当前是否有待解码的数据包。
当解码器收到一个新的数据包时,会将 packet_pending 设置为 1。
当解码器完成对当前数据包的解码工作后,会将 packet_pending 设置为 0。
防止avcodec_send_packet队满未及时处理pkt,需要暂存起来,重pending pkt加入队列处理。
- packet_pending它用来标记当前是否有待解码的数据包。
- 解码流程:avcodec_receive_frame-> packet_queue_get-> avcodec_send_packet
- 接收-》过滤-》发送 这样设计主要为了保证接收前 队列是空的情况。
音视频重采样
⾳频重采样在 audio_decode_frame() 中实现, audio_decode_frame() 就是从⾳频frame队列中 取出⼀个frame,按指定格式经过重采样后输出(解码不是在该函数进⾏)。 先进行音频重采样设置随后进行重采样补偿,为了播放起来平滑。
1 audio_decode_frame ffplay.c 2682 0x40ddaa 取出一帧
2 sdl_audio_callback ffplay.c 2852 0x40e53f 音频输出回调
3 ?? 0x6c743e0d
/// @brief 对处理后的音频帧进行解码、转换(如果需要)和存储在is->audio_buf中,其大小由返回值指定值
/// @param is
/// @return 返回一帧大小
static int audio_decode_frame(VideoState *is)
{
...// 若队列头部可读,则由af指向可读帧if (!(af = frame_queue_peek_readable(&is->sampq)))return -1;frame_queue_next(&is->sampq);} while (af->serial != is->audioq.serial);// 根据frame中指定的音频参数获取缓冲区的大小 af->frame->channels * af->frame->nb_samples * 2data_size = av_samples_get_buffer_size(NULL,af->frame->channels,af->frame->nb_samples,af->frame->format, 1);// 获取声道布局dec_channel_layout =(af->frame->channel_layout &&af->frame->channels == av_get_channel_layout_nb_channels(af->frame->channel_layout)) ?af->frame->channel_layout : av_get_default_channel_layout(af->frame->channels);// 获取样本数校正值:若同步时钟是音频,则不调整样本数;否则根据同步需要调整样本数 不同步时加快或放慢播放音频,通过动态采样点实现wanted_nb_samples = synchronize_audio(is, af->frame->nb_samples);// is->audio_tgt是SDL可接受的音频帧数,是audio_open()中取得的参数// 在audio_open()函数中又有"is->audio_src = is->audio_tgt""// 此处表示:如果frame中的音频参数 == is->audio_src == is->audio_tgt,// 那音频重采样的过程就免了(因此时is->swr_ctr是NULL)// 否则使用frame(源)和is->audio_tgt(目标)中的音频参数来设置is->swr_ctx,// 并使用frame中的音频参数来赋值is->audio_srcif (af->frame->format != is->audio_src.fmt || // 采样格式dec_channel_layout != is->audio_src.channel_layout || // 通道布局af->frame->sample_rate != is->audio_src.freq || // 采样率// 第4个条件, 要改变样本数量, 那就是需要初始化重采样(wanted_nb_samples != af->frame->nb_samples && !is->swr_ctx) // samples不同且swr_ctx没有初始化) {swr_free(&is->swr_ctx);is->swr_ctx = swr_alloc_set_opts(NULL,is->audio_tgt.channel_layout, // 目标输出is->audio_tgt.fmt,is->audio_tgt.freq,dec_channel_layout, // 数据源af->frame->format,af->frame->sample_rate,0, NULL);if (!is->swr_ctx || swr_init(is->swr_ctx) < 0) {av_log(NULL, AV_LOG_ERROR,"Cannot create sample rate converter for conversion of %d Hz %s %d channels to %d Hz %s %d channels!\n",af->frame->sample_rate, av_get_sample_fmt_name(af->frame->format), af->frame->channels,is->audio_tgt.freq, av_get_sample_fmt_name(is->audio_tgt.fmt), is->audio_tgt.channels);swr_free(&is->swr_ctx);return -1;}is->audio_src.channel_layout = dec_channel_layout;is->audio_src.channels = af->frame->channels;is->audio_src.freq = af->frame->sample_rate;is->audio_src.fmt = af->frame->format;}if (is->swr_ctx) {// 重采样输入参数1:输入音频样本数是af->frame->nb_samples// 重采样输入参数2:输入音频缓冲区const uint8_t **in = (const uint8_t **)af->frame->extended_data; // data[0] data[1]// 重采样输出参数1:输出音频缓冲区尺寸uint8_t **out = &is->audio_buf1; //真正分配缓存audio_buf1,指向是用audio_buf// 重采样输出参数2:输出音频缓冲区 计算输出采样点int out_count = (int64_t)wanted_nb_samples * is->audio_tgt.freq / af->frame->sample_rate+ 256;int out_size = av_samples_get_buffer_size(NULL, is->audio_tgt.channels,out_count, is->audio_tgt.fmt, 0);int len2;if (out_size < 0) {av_log(NULL, AV_LOG_ERROR, "av_samples_get_buffer_size() failed\n");return -1;}// 如果frame中的样本数经过校正,则条件成立if (wanted_nb_samples != af->frame->nb_samples) {//设置补偿参数/*** 音频重采样通常需要使用一些滤波算法,例如最邻近插值、线性插值等。* 这些算法在计算中会产生一些误差,导致重采样后的音频数据与原始数据之间存在时间差。* int swr_set_compensation(struct SwrContext *s, int sample_delta, int compensation_distance);* s: SWR 上下文指针。sample_delta: 需要增加或减少的采样数。正值表示增加采样数,负值表示减少采样数。compensation_distance: 需要补偿的音频帧数。通常设置为 1 即可。return 成功时返回 0。失败时返回负值错误码。*/if (swr_set_compensation(is->swr_ctx,(wanted_nb_samples - af->frame->nb_samples) * is->audio_tgt.freq / af->frame->sample_rate,wanted_nb_samples * is->audio_tgt.freq / af->frame->sample_rate) < 0) {av_log(NULL, AV_LOG_ERROR, "swr_set_compensation() failed\n");return -1;}}//动态扩容av_fast_malloc(&is->audio_buf1, &is->audio_buf1_size, out_size);if (!is->audio_buf1)return AVERROR(ENOMEM);// 音频重采样:返回值是重采样后得到的音频数据中单个声道的样本数 重采样后的采样点len2 = swr_convert(is->swr_ctx, out, out_count, in, af->frame->nb_samples);if (len2 < 0) {av_log(NULL, AV_LOG_ERROR, "swr_convert() failed\n");return -1;}if (len2 == out_count) {av_log(NULL, AV_LOG_WARNING, "audio buffer is probably too small\n");if (swr_init(is->swr_ctx) < 0)swr_free(&is->swr_ctx);}// 重采样返回的一帧音频数据大小(以字节为单位)is->audio_buf = is->audio_buf1;//重新计算一帧所占大小resampled_data_size = len2 * is->audio_tgt.channels * av_get_bytes_per_sample(is->audio_tgt.fmt);} else {// 未经重采样,则将指针指向frame中的音频数据is->audio_buf = af->frame->data[0]; // s16交错模式data[0], fltp data[0] data[1]resampled_data_size = data_size;}
...return resampled_data_size;
}
视频重采样
api介绍
创建视频图像缩放上下文的函数。
struct SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat,int dstW, int dstH, enum AVPixelFormat dstFormat,int flags, SwsFilter *srcFilter, SwsFilter *dstFilter,const double *param);
第⼀参数可以传NULL,默认会开辟⼀块新的空间。
srcW,srcH, srcFormat, 原始数据的宽⾼和原始像素格式(YUV420)
dstW,dstH,dstFormat; ⽬标宽,⽬标⾼,⽬标的像素格式(这⾥的宽⾼可能是⼿机屏幕分辨率,
RGBA8888),这⾥不仅仅包含了尺⼨的转换和像素格式的转换
flag 提供了⼀系列的算法,快速线性,差值,矩阵,不同的算法性能也不同,快速线性算法性能相对较
⾼。只针对尺⼨的变换。
#define SWS_FAST_BILINEAR 1
#define SWS_BILINEAR 2
#define SWS_BICUBIC 4
#define SWS_X 8
#define SWS_POINT 0x10
#define SWS_AREA 0x20
#define SWS_BICUBLIN 0x40
后⾯还有两个参数是做过滤器⽤的,⼀般⽤不到,传NULL,最后⼀个参数是跟flag算法相关,也可以
传NULL。返回值:
成功返回一个新创建的 SwsContext 对象。
失败返回 NULL。用于创建和管理视频图像缩放上下文的函数。它与 sws_getContext() 函数的作用类似,但它能够缓存已经创建的 SwsContext 对象,以避免重复创建。
struct SwsContext *sws_getCachedContext(struct SwsContext *context,int srcW, int srcH, enum AVPixelFormat srcFormat,int dstW, int dstH, enum AVPixelFormat dstFormat,int flags, SwsFilter *srcFilter,SwsFilter *dstFilter, const double *param);
struct SwsContext *context: 可选的现有 SwsContext 对象。如果传入 NULL,函数将创建一个新的对象。
其他参数与 sws_getContext() 函数相同。
返回值:
返回一个 SwsContext 对象。如果传入的 context 参数不为 NULL,则返回该对象;否则返回一个新创建的 SwsContext 对象。用于执行视频图像缩放操作的函数。它主要用于将一个源图像缩放或转换到目标图像的指定大小和像素格式。
int sws_scale(struct SwsContext *c, const uint8_t *const src[],const int srcStride[], int srcSliceY, int srcSliceH,uint8_t *dst[], const int dstStride[]);
参数1 转换格式的上下⽂。
参数2 输⼊图像的每个颜⾊通道的数据指针。avframe.data数组,因为不同像素的存储格式不同,所以srcSlice[]维数也有可能不同。YUV420P为例,它是planar格式
使⽤FFmpeg解码后存储在AVFrame的data[]数组中时:data[0]——-Y分量 data[1]——-U分量 data[2]——-V分量
linesize[]数组中保存的是对应通道的数据宽度:linesize[0]——-Y分量的宽度,linesize[1]——-U分量的宽度 linesize[2]——-V分量的宽度
⽽RGB24,它是packed格式 只有⼀维即data[0]
3.参数const int srcStride[] 输⼊图像的每个颜⾊通道的跨度,对应的是解码后的AVFrame中的linesize[]数组。4.参数int srcSliceY, int srcSliceH,定义在输⼊图像上处理区域,srcSliceY是起始位置,srcSliceH是处理多少⾏。如果srcSliceY=0,srcSliceH=height,表示⼀次性处理完整个图像。这种设置是为了多线程
并⾏,例如可以创建两个线程,第⼀个线程处理 [0, h/2-1]⾏,第⼆个线程处理 [h/2, h-1]⾏。并⾏处理
加快速度。
5.参数uint8_t *const dst[], const int dstStride[]定义输出图像信息(输出的每个颜⾊通道数据指针,每个颜⾊通道⾏字节数)返回值:
成功返回目标图像的高度。
失败返回负错误码。void sws_freeContext(struct SwsContext *swsContext);
释放SwsContext。
源码使用
/// @brief 动态图像显示,根据frame中的像素格式与SDL⽀持的像素格式的匹配,将图像数据输出到 tex
/// @param tex
/// @param frame
/// @param img_convert_ctx
/// @return
static int upload_texture(SDL_Texture **tex, AVFrame *frame, struct SwsContext **img_convert_ctx) {int ret = 0;Uint32 sdl_pix_fmt;SDL_BlendMode sdl_blendmode;//像素格式转换get_sdl_pix_fmt_and_blendmode(frame->format, &sdl_pix_fmt, &sdl_blendmode);//由于格式变动 需要重新分配内存 参数tex实际是&is->vid_texture,此处根据得到的SDL像素格式,为&is->vid_textureif (realloc_texture(tex, sdl_pix_fmt == SDL_PIXELFORMAT_UNKNOWN ? SDL_PIXELFORMAT_ARGB8888 : sdl_pix_fmt, frame->width, frame->height, sdl_blendmode, 0) < 0)return -1;switch (sdl_pix_fmt) {// frame格式是SDL不⽀持的格式,则需要进⾏图像格式转换,转换为⽬标格式AV_PIX_FMT_BGRA,对应SDL_PIXELFORMAT_BGRA32case SDL_PIXELFORMAT_UNKNOWN:*img_convert_ctx = sws_getCachedContext(*img_convert_ctx,frame->width, frame->height, frame->format, frame->width, frame->height,AV_PIX_FMT_BGRA, sws_flags, NULL, NULL, NULL);if (*img_convert_ctx != NULL) {uint8_t *pixels[4];int pitch[4];if (!SDL_LockTexture(*tex, NULL, (void **)pixels, pitch)) {sws_scale(*img_convert_ctx, (const uint8_t * const *)frame->data, frame->linesize,0, frame->height, pixels, pitch);SDL_UnlockTexture(*tex);}} else {av_log(NULL, AV_LOG_FATAL, "Cannot initialize the conversion context\n");ret = -1;}break;// frame格式对应SDL_PIXELFORMAT_IYUV,不⽤进⾏图像格式转换,调⽤SDL_UpdateYUVTexture()更新SDL texturecase SDL_PIXELFORMAT_IYUV:if (frame->linesize[0] > 0 && frame->linesize[1] > 0 && frame->linesize[2] > 0) {ret = SDL_UpdateYUVTexture(*tex, NULL, frame->data[0], frame->linesize[0],frame->data[1], frame->linesize[1],frame->data[2], frame->linesize[2]);} else if (frame->linesize[0] < 0 && frame->linesize[1] < 0 && frame->linesize[2] < 0) {ret = SDL_UpdateYUVTexture(*tex, NULL, frame->data[0] + frame->linesize[0] * (frame->height - 1), -frame->linesize[0],frame->data[1] + frame->linesize[1] * (AV_CEIL_RSHIFT(frame->height, 1) - 1), -frame->linesize[1],frame->data[2] + frame->linesize[2] * (AV_CEIL_RSHIFT(frame->height, 1) - 1), -frame->linesize[2]);} else {av_log(NULL, AV_LOG_ERROR, "Mixed negative and positive linesizes are not supported.\n");return -1;}break;// frame格式对应其他SDL像素格式,不⽤进⾏图像格式转换,调⽤SDL_UpdateTexture()更新SDL texturedefault:if (frame->linesize[0] < 0) {ret = SDL_UpdateTexture(*tex, NULL, frame->data[0] + frame->linesize[0] * (frame->height - 1), -frame->linesize[0]);} else {ret = SDL_UpdateTexture(*tex, NULL, frame->data[0], frame->linesize[0]);}break;}return ret;
}
ffmpeg中的sws_scale算法
1 #define SWS_FAST_BILINEAR 1 2 #define SWS_BILINEAR 2 3 #define SWS_BICUBIC 4 4 #define SWS_X 8 5 #define SWS_POINT 0x10 6 #define SWS_AREA 0x20 7 #define SWS_BICUBLIN 0x40 8 #define SWS_GAUSS 0x80 9 #define SWS_SINC 0x100 10 #define SWS_LANCZOS 0x200 11 #define SWS_SPLINE 0x400
查阅文章,直接总结一下:如果对图像的缩放,要追求⾼效,⽐如说是视频图像的处理,在不明确是放⼤还是缩⼩时,直接使⽤SWS_FAST_BILINEAR算法即可。如果明确是要缩放并显示,建议使⽤Point算法。
FFmpeg使⽤不同sws_scale()缩放算法的命令示例(bilinear,bicubic,neighbor):
ffmpeg -s 480x272 -pix_fmt yuv420p -i src01_480x272.yuv -s 1280x720 -
sws_flags bilinear -pix_fmt yuv420p src01_bilinear_1280x720.yuvffmpeg -s 480x272 -pix_fmt yuv420p -i src01_480x272.yuv -s 1280x720 -
sws_flags bicubic -pix_fmt yuv420p src01_bicubic_1280x720.yuvffmpeg -s 480x272 -pix_fmt yuv420p -i src01_480x272.yuv -s 1280x720 -
sws_flags neighbor -pix_fmt yuv420p src01_neighbor_1280x720.yuv
音频输出逻辑
- 如何实现音频输出?
-
打开SDL⾳频设备,设置参数 wanted_spec.callback = sdl_audio_callback;
设置一大堆参数,涉及音频重采样等等 启动SDL⾳频设备播放 SDL_PauseAudioDevice(audio_dev, 0); //播放
SDL⾳频回调函数读取数据,从FrameQueue读取frame填充回调函数提供的buffer空间。在哪调用:
1 audio_open ffplay.c 2741 0x40e7cb
2 stream_component_open ffplay.c 2927 0x40f089
3 read_thread ffplay.c 3216 0x40fe84
4 SDL_LogCritical 0x6c7bb439
5.stream_open
6.main
-
2.音量调整逻辑
初始化音量 stream_open
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;
is->muted = 0; //禁音
is->av_sync_type = av_sync_type;填充时处理
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);// 3.调整音量/* 如果处于mute状态则直接使用stream填0数据, 暂停时is->audio_buf = NULL */if (!is->muted && is->audio_buf)SDL_MixAudioFormat(stream, (uint8_t *)is->audio_buf + is->audio_buf_index,AUDIO_S16SYS, len1, is->audio_volume);
}
视频输出
1,如何按照媒体文件默认尺寸输出 如何自定义设置媒体播放尺寸??
static void set_default_window_size(int width, int height, AVRational sar)
{SDL_Rect rect;// -x screen_width -y screen_heightint max_width = screen_width ? screen_width : INT_MAX; // 确定是否指定窗口最大宽度int max_height = screen_height ? screen_height : INT_MAX; // 确定是否指定窗口最大高度if (max_width == INT_MAX && max_height == INT_MAX)max_height = height; // 没有指定最大高度时则使用视频的高度calculate_display_rect(&rect, 0, 0, max_width, max_height, width, height, sar);default_width = rect.w;default_height = rect.h;
}
2.视频输出逻辑
1 upload_texture ffplay.c 1059 0x408f96 根据frame中的像素格式与SDL⽀持的像素格式的匹配,将图像数据输出到 tex
2 video_image_display ffplay.c 1195 0x409797 取出当前需要播放帧,使用sdl渲染显示
3 video_display ffplay.c 1549 0x40a8a2 显示一帧图像 设置纹理 渲染器 窗口
4 video_refresh ffplay.c 1948 0x40b8bc 显示一帧图像,重点是音视频同步规则。lastvp vp nextvp 检查这三帧的合法性
5 refresh_loop_wait_event ffplay.c 3787 0x411217 注册输入设备监听事件,不断取出事件队列处理。也就是不断更新视频的帧
6 event_loop ffplay.c 3834 0x41143c
7 main ffplay.c 4339 0x412b09video_refresh逻辑如下
总结
本文章对ffplay进行全面分析,特别适合新手阅读ffplay源码。从ffplay数据结构再到模块拆分,对ffplay各个模块分析从数据读取,到解码,重采样最后视频输出。
ffplay分析更多相关内容
ffplay播放器 暂停、逐帧、音量、快进快退seek功能分析-CSDN博客
ffplay音视频同步分析-CSDN博客
深入理解音视频pts,dts,time_base以及时间数学公式-CSDN博客
源代码分析分享
jbjnb/ffpmeg-ffplay (gitee.com)
学习资料分享
0voice · GitHub