1.框架分析
ffplay.c是FFmpeg源码⾃带的播放器,调⽤FFmpeg和SDL API实现⼀个⾮常有⽤的播放器。例如哔哩哔哩著名开源项⽬ijkplayer也是基于ffplay.c进⾏⼆次开发。ffplay实现了播放器的主体功能,掌握其原理对于我们独⽴开发播放器⾮常有帮助。看看整体的框架,如下图:
从整体上,分这几个大的模块,数据读取,AVpacket缓存队列,音频解码,视频解码,AVframe缓存队列,声音和视频输出,音视频同步等模块组成。
首先进入stream_open,stream_open主要的工作是创建音视频解码前和后的数据缓存队列,初始化时钟,包括音频,视频,外部时钟等,初始化数据读取线程read_thread。源码如下:
static VideoState *stream_open(const char *filename, AVInputFormat *iformat){ VideoState *is; is = av_mallocz(sizeof(VideoState)); if (!is) return NULL; is->filename = av_strdup(filename); if (!is->filename) goto fail; is->iformat = iformat; is->ytop = 0; is->xleft = 0; /* start video display */ //初始化帧队列 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;//初始化packet队列 if (packet_queue_init(&is->videoq) < 0 || packet_queue_init(&is->audioq) < 0 || packet_queue_init(&is->subtitleq) < 0) goto fail; if (!(is->continue_read_thread = SDL_CreateCond())) { av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s", SDL_GetError()); goto fail; }//初始化视频时钟 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; if (startup_volume < 0) av_log(NULL, AV_LOG_WARNING, "-volume=%d < 0, setting to 0", startup_volume); if (startup_volume > 100) av_log(NULL, AV_LOG_WARNING, "-volume=%d > 100, setting to 100", 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; //初始化数据读取线程 is->read_tid = SDL_CreateThread(read_thread, "read_thread", is); if (!is->read_tid) { av_log(NULL, AV_LOG_FATAL, "SDL_CreateThread(): %s", SDL_GetError());fail: stream_close(is); return NULL; } return is;}
看看数据读取线程read_thread做了什么?
大致就是一个解封转过程,然后把解封装的packet放到packet队列。分别调用avformat_alloc_context(分配解封装上下文),avforamt_open_input(打开文件或网络流,内存数据),avformat_find_stream_info(找到相关流信息),stream_component_open(打开音视频流),av_read_frame(解封转),packet_queue_put(放入到帧队列)。源码如下,详细请看注释:
typedef struct MyAVPacketList { AVPacket pkt; struct MyAVPacketList *next; int serial;} MyAVPacketList;typedef struct PacketQueue { //链表 MyAVPacketList *first_pkt, *last_pkt; //多少个packet int nb_packets; //每个packet大小 int size; //每个packet时长 int64_t duration; //请求标志 int abort_request; int serial; //锁 SDL_mutex *mutex; //条件变量 SDL_cond *cond;} PacketQueue;
然后把解封装的数据,放到packet队列。看看packet的封装结构,实际就是一个链表。详细源码如下:
typedef struct MyAVPacketList { AVPacket pkt; struct MyAVPacketList *next; int serial;} MyAVPacketList;typedef struct PacketQueue { //链表 MyAVPacketList *first_pkt, *last_pkt; //多少个packet int nb_packets; //每个packet大小 int size; //每个packet时长 int64_t duration; //请求标志 int abort_request; int serial; //锁 SDL_mutex *mutex; //条件变量 SDL_cond *cond;} PacketQueue;
视频解码线程,然后就从video的packet 队列取数据,看看做了什么?大致就是使用packet_queue_get去取packet,然后发送解码,avcode_send_packet,avcode_receive_frame,并刷新解码后的frame队列。详细看看源码,如下:
可以看出ffplay.c还支持过滤器功能,功能还是很全。
取数据。
可以看出音视频和subtitle的所调用的函数是不一样。
解码:
解码完后的数据,插入frame队列。注意,视频和音频,都各自有自己frame队列。
涉及到线程安全,就要加锁。
音频基本上也是走的这个流程。就不再叙述了。
最后就是显示模块了,取出数据,然后根据当前能支持的播放格式和尺寸,看看是否要格式转换,也就是是否要调用sws_scale。
static void video_image_display(VideoState *is){ Frame *vp; Frame *sp = NULL; SDL_Rect rect;//从frame队列取数据 vp = frame_queue_peek_last(&is->pictq); if (is->subtitle_st) { if (frame_queue_nb_remaining(&is->subpq) > 0) { sp = frame_queue_peek(&is->subpq); if (vp->pts >= sp->pts + ((float) sp->sub.start_display_time / 1000)) { if (!sp->uploaded) { uint8_t* pixels[4]; int pitch[4]; int i; if (!sp->width || !sp->height) { sp->width = vp->width; sp->height = vp->height; } if (realloc_texture(&is->sub_texture, SDL_PIXELFORMAT_ARGB8888, sp->width, sp->height, SDL_BLENDMODE_BLEND, 1) < 0) return; for (i = 0; i < sp->sub.num_rects; i++) { AVSubtitleRect *sub_rect = sp->sub.rects[i]; sub_rect->x = av_clip(sub_rect->x, 0, sp->width ); sub_rect->y = av_clip(sub_rect->y, 0, sp->height); sub_rect->w = av_clip(sub_rect->w, 0, sp->width - sub_rect->x); sub_rect->h = av_clip(sub_rect->h, 0, sp->height - sub_rect->y); is->sub_convert_ctx = sws_getCachedContext(is->sub_convert_ctx, sub_rect->w, sub_rect->h, AV_PIX_FMT_PAL8, sub_rect->w, sub_rect->h, AV_PIX_FMT_BGRA, 0, NULL, NULL, NULL); if (!is->sub_convert_ctx) { av_log(NULL, AV_LOG_FATAL, "Cannot initialize the conversion context"); return; } if (!SDL_LockTexture(is->sub_texture, (SDL_Rect *)sub_rect, (void **)pixels, pitch)) { //格式转换 sws_scale(is->sub_convert_ctx, (const uint8_t * const *)sub_rect->data, sub_rect->linesize, 0, sub_rect->h, pixels, pitch); SDL_UnlockTexture(is->sub_texture); } } sp->uploaded = 1; } } else sp = NULL; } }//计算显示区域 calculate_display_rect(&rect, is->xleft, is->ytop, is->width, is->height, vp->width, vp->height, vp->sar); if (!vp->uploaded) { if (upload_texture(&is->vid_texture, vp->frame, &is->img_convert_ctx) < 0) return; vp->uploaded = 1; vp->flip_v = vp->frame->linesize[0] < 0; } set_sdl_yuv_conversion_mode(vp->frame); SDL_RenderCopyEx(renderer, is->vid_texture, NULL, &rect, 0, NULL, vp->flip_v ? SDL_FLIP_VERTICAL : 0); set_sdl_yuv_conversion_mode(NULL); if (sp) {#if USE_ONEPASS_SUBTITLE_RENDER SDL_RenderCopy(renderer, is->sub_texture, NULL, &rect);#else int i; double xratio = (double)rect.w / (double)sp->width; double yratio = (double)rect.h / (double)sp->height; for (i = 0; i < sp->sub.num_rects; i++) { SDL_Rect *sub_rect = (SDL_Rect*)sp->sub.rects[i]; SDL_Rect target = {.x = rect.x + sub_rect->x * xratio, .y = rect.y + sub_rect->y * yratio, .w = sub_rect->w * xratio, .h = sub_rect->h * yratio}; SDL_RenderCopy(renderer, is->sub_texture, sub_rect, &target); }#endif }}
static int audio_decode_frame(VideoState *is){ int data_size, resampled_data_size; int64_t dec_channel_layout; av_unused double audio_clock0; int wanted_nb_samples; Frame *af; if (is->paused) return -1; do {#if defined(_WIN32) while (frame_queue_nb_remaining(&is->sampq) == 0) { if ((av_gettime_relative() - audio_callback_time) > 1000000LL * is->audio_hw_buf_size / is->audio_tgt.bytes_per_sec / 2) return -1; av_usleep (1000); }#endif if (!(af = frame_queue_peek_readable(&is->sampq))) return -1; frame_queue_next(&is->sampq); } while (af->serial != is->audioq.serial); data_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); if (af->frame->format != is->audio_src.fmt || dec_channel_layout != is->audio_src.channel_layout || af->frame->sample_rate != is->audio_src.freq || (wanted_nb_samples != af->frame->nb_samples && !is->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!", 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) { const uint8_t **in = (const uint8_t **)af->frame->extended_data; uint8_t **out = &is->audio_buf1; 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"); return -1; } if (wanted_nb_samples != af->frame->nb_samples) { 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"); 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"); return -1; } if (len2 == out_count) { av_log(NULL, AV_LOG_WARNING, "audio buffer is probably too small"); 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 { is->audio_buf = af->frame->data[0]; resampled_data_size = data_size; } audio_clock0 = is->audio_clock; /* update the audio clock with the pts */ if (!isnan(af->pts)) is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate; else is->audio_clock = NAN; is->audio_clock_serial = af->serial;#ifdef DEBUG { static double last_clock; printf("audio: delay=%0.3f clock=%0.3f clock0=%0.3f", is->audio_clock - last_clock, is->audio_clock, audio_clock0); last_clock = is->audio_clock; }#endif return resampled_data_size;}
音频也是类似的情况,调用SDL去播放声音。
总体上总结下:
(1)数据读取线程主要的工作:
打开媒体⽂件。
打开对应码流的decoder以及初始化对应的audio、video、subtitle输出。
创建decoder线程,audio、video和subtitle的解码线程独⽴。
调⽤av_read_frame读取packet,并根据steam_index放⼊不同stream对应的packet队列。
(2)⾳频解码
从packet queue读取packet,解出frame后放⼊frame queue。
(3)视频解码
从packet queue读取packet,解出frame后放⼊frame queue。
(4)字幕解码
从packet queue读取packet,解出frame后放⼊frame queue。
(5)⾳频播放(或者回调函数)
从frame queue读取frame进⾏播放。
(6)视频播放,ffplay⽬前是在main主线程进⾏视频播放。我个人觉得放到子线程比较好。
从frame queue读取frame进⾏播放。
(7)字幕播放,ffplay⽬前是在main主线程进⾏字幕播放。我个人觉得放到子线程比较好。
从frame queue读取frame进⾏播放。
(8)控制响应(播放/暂停/快进/快退等)
ffplay⽬前是在main主线程进⾏播放控制。
(9)packet队列的设计
线程安全,⽀持互斥、等待、唤醒。
缓存数据⼤⼩。
缓存包数。
队列播放可持续时间。
进队列/出队列等。
(10)frame队列的设计
线程安全,⽀持互斥、等待、唤醒。
缓存帧数。
⽀持读取数据⽽不出队列。
进队列/出队列等。
(11)⾳视频同步
⾳频同步。
视频同步。
外部时钟同步。
(12)⾳频处理
⾳量调节。
静⾳。
重采样。
(12)视频处理
图像格式转换RGB->YUV等。
图像缩放1280*720->800*480等。
(13)播放器控制
播放。
暂停。
停⽌。
快进/快退。
逐帧。
静⾳。
本篇文章分析到这里,后面还会继续分析,欢迎关注,点赞,转发,收藏。