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(): %sn", 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 0n", startup_volume);
if (startup_volume > 100)
av_log(NULL, AV_LOG_WARNING, "-volume=%d > 100, setting to 100n", 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(): %sn", 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 contextn");
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!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) {
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() failedn");
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() failedn");
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() failedn");
return -1;
} if (len2 == out_count) {
av_log(NULL, AV_LOG_WARNING, "audio buffer is probably too smalln");
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.3fn",
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)播放器控制
播放。
暫停。
停?。
快進/快退。
逐幀。
靜?。
本篇文章分析到這里,后面還會繼續分析,歡迎關注,點贊,轉發,收藏。