FFmpeg 代碼 version 3.3:
ffplay中的線程模型
ffplay線程模型-視頻爲例.png
概述
ffplay.c 中線程模型簡單命令。主要是有如下幾個線程:
1. 渲染的線程-主線程
簡單的理解,來說就是main方法運行所在的線程。
實際上是SDL_CreateWindow
調用所在的線程。以Android爲例(筆者比較熟悉),創建的是OpenGL
的Surface
。也就是EGLContext
所在的線程了。
在EventLoop中,將會負責下面兩個功能
- 負責將解碼後的數據送顯
從解碼後的隊列中,取得數據。經過同步時間鐘的同步,睡眠後(需要同步的話),然後通過
SDL_UpdateTexture
/SDL_RenderCopy
/SDL_RenderPresent
,更新紋理數據,送顯。 - 對按鍵進行相應 還會按鈕事件的相應。
2. 讀取線程-read_thread 在main方法中會啓動的讀取的線程。
- 循環讀取
這個線程中,會進行讀取的循環。不斷的通過
av_read_frame
方法,讀取解碼前的數據packet。 - 送入隊列
最後將得到的數據,送入對應的流的
packet
隊列(視頻/音頻/字幕都對應視頻流自己的隊列) 3. 對應流的解碼線程-decode thread 在讀取線程中,對AVFormatContext
進行初始化,獲取AVStream
信息後,對應不同的碼流會開啓對應的解碼線程Decode Thread。ffplay
中這裏包括了3種流。視頻流。音頻流和字幕流。
以視頻流爲例子。
- 循環讀取
會從對應流的packet隊列中,得到數據。
然後送入解碼器通過
avcodec_decode_video2
(舊的API)進行解碼。 - 送入隊列
解碼之後,得到解碼前的數據
AVFrame
,並確定對應的pts
。 最後然後其再次送入隊列當中。
整體的流程就是這樣簡單。
ffplay初始化(main_thread)
1. 對FFmpeg的初始化
調用av_register_all
和avformat_network_init
。
如果有AVDevice
和AVFilter
也會對其進行初始化。
/* register all codecs, demux and protocols */ #if CONFIG_AVDEVICE avdevice_register_all(); #endif #if CONFIG_AVFILTER avfilter_register_all(); #endif av_register_all(); avformat_network_init();
2. 對傳遞的參數進行初始化
parse_options(NULL, argc, argv, options, opt_input_file);
這個方法就是去解析我們傳入的參數的。具體的方法寫在cmdutils.h
中。
ffplay支持的參數類型,可以在定義的地方看到。
static const OptionDef options[] //這個數組中,有許多選項,不是重點,暫時不做詳細的介紹了。
3. SDL的初始化
SDL會先初始化SDL_init 進行初始化。
flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER; if (audio_disable) flags &= ~SDL_INIT_AUDIO; else { /* Try to work around an occasional ALSA buffer underflow issue when the * period size is NPOT due to ALSA resampling by forcing the buffer size. */ if (!SDL_getenv("SDL_AUDIO_ALSA_SET_BUFFER_SIZE")) SDL_setenv("SDL_AUDIO_ALSA_SET_BUFFER_SIZE","1", 1); } if (display_disable) flags &= ~SDL_INIT_VIDEO; if (SDL_Init (flags)) { av_log(NULL, AV_LOG_FATAL, "Could not initialize SDL - %s\n", SDL_GetError()); av_log(NULL, AV_LOG_FATAL, "(Did you set the DISPLAY variable?)\n"); exit(1); }
後續的創建SDL_Window/SDL_Renderer/SDL_texture 的部分,會在後續初始化。
4. 通過stream_open
函數,開啓read_thread
讀取線程
is = stream_open(input_filename, file_iformat);
通過 stream_open函數,會對VideoState
進行初始化,包括解碼數據隊列(PacketQueue
)和解碼後數據隊列(FrameQueue
)和時間鍾(Clock
)等的初始化,並且開啓read_thread
。
- VideoState 結構體 這個結構體相當於一個全局的變量都囊括在一起了。 從源碼中可以看到。 包括幾個方面的參數。
- 視頻操作類的暫停,運行和seek
- 三種同步的時間鍾
- 三種碼流對應的
FrameQueue
/Decoder
/AVStream
/PacketQueue
,以及對應碼流各自的信息。 - 用來
Render
的一些變量。如SDL_Texture,窗口的位置參數等 -
read_thread
和SDL_cond *continue_read_thread
條件鎖 - 還有些中間變量
// 視頻狀態結構 typedef struct VideoState { SDL_Thread *read_tid; // 讀取線程 AVInputFormat *iformat; // 輸入格式 int abort_request; // 請求取消 int force_refresh; // 強制刷新 int paused; // 停止 int last_paused; // 最後停止 int queue_attachments_req; // 隊列附件請求 int seek_req; // 查找請求 int seek_flags; // 查找標誌 int64_t seek_pos; // 查找位置 int64_t seek_rel; // int read_pause_return; // 讀停止返回 AVFormatContext *ic; // 解碼格式上下文 int realtime; // 是否實時碼流 Clock audclk; // 音頻時鐘 Clock vidclk; // 視頻時鐘 Clock extclk; // 外部時鐘 FrameQueue pictq; // 視頻隊列 FrameQueue subpq; // 字幕隊列 FrameQueue sampq; // 音頻隊列 Decoder auddec; // 音頻解碼器 Decoder viddec; // 視頻解碼器 Decoder subdec; // 字幕解碼器 int audio_stream; // 音頻碼流Id int av_sync_type; // 同步類型 double audio_clock; // 音頻時鐘 int audio_clock_serial; // 音頻時鐘序列 double audio_diff_cum; // 用於音頻差分計算 /* used for AV difference average computation */ double audio_diff_avg_coef; // double audio_diff_threshold; // 音頻差分閾值 int audio_diff_avg_count; // 平均差分數量 AVStream *audio_st; // 音頻碼流 PacketQueue audioq; // 音頻包隊列 int audio_hw_buf_size; // 硬件緩衝大小 uint8_t *audio_buf; // 音頻緩衝區 uint8_t *audio_buf1; // 音頻緩衝區1 unsigned int audio_buf_size; // 音頻緩衝大小 /* in bytes */ unsigned int audio_buf1_size; // 音頻緩衝大小1 int audio_buf_index; // 音頻緩衝索引 /* in bytes */ int audio_write_buf_size; // 音頻寫入緩衝大小 int audio_volume; // 音量 int muted; // 是否靜音 struct AudioParams audio_src; // 音頻參數 #if CONFIG_AVFILTER struct AudioParams audio_filter_src; // 音頻過濾器 #endif struct AudioParams audio_tgt; // 音頻參數 struct SwrContext *swr_ctx; // 音頻轉碼上下文 int frame_drops_early; // int frame_drops_late; // 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; // 音頻Texture SDL_Texture *sub_texture; // 字幕Texture SDL_Texture *vid_texture; // 視頻Texture int subtitle_stream; // 字幕碼流Id AVStream *subtitle_st; // 字幕碼流 PacketQueue subtitleq; // 字幕包隊列 double frame_timer; // 幀計時器 double frame_last_returned_time; // 上一次返回時間 double frame_last_filter_delay; // 上一個過濾器延時 int video_stream; // 視頻碼流Id 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; // 寬高,其實座標 int step; // 步進 #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 // 上一個視頻碼流Id、上一個音頻碼流Id、上一個字幕碼流Id int last_video_stream, last_audio_stream, last_subtitle_stream; SDL_cond *continue_read_thread; // 連續讀線程 } VideoState;
5. 開啓EventLoop
refresh_loop_wait_event(cur_stream, &event); static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) { double remaining_time = 0.0; //取出事件 SDL_PumpEvents(); // 如果事件不是要處理的,就會進行渲染的判斷,然後繼續取出事件的循環 while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) { //是否顯示鼠標 if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) { SDL_ShowCursor(0); cursor_hidden = 1; } //使用av_usleep來控制視頻刷新的幀率 if (remaining_time > 0.0) av_usleep((int64_t)(remaining_time * 1000000.0)); remaining_time = REFRESH_RATE; //通知刷新的話。is->force_refresh==1. if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh)) //進入繪製 video_refresh(is, &remaining_time); //繼續下一個事件循環 SDL_PumpEvents(); } }
通過這個方法,不斷的來取出SDL的事件。同時通過這樣的事件循環,不斷video_refresh
送顯繪製。
讀取線程read_thread
如上部分所示,通過stream_open
方法,開啓read_thread
read_thread 初始化參數
read_thread作爲讀取線程。
- 需要初始化
AVformatContext
和AVStream
。 - 同時,在得到對應的
AVStream
之後,它還負責初始化好對應的AVCodec
和AVCodecContext
,然後開啓對應的解碼線程。 - 最後,通過不斷的
av_read_frame
,將數據讀取出,送入隊列。等待解碼。
需要初始化AVformatContext
和AVStream
。
1. 顯示先創建 AVformatContext
ic = avformat_alloc_context();
** 2. 設置解碼中斷回調方法** 這個方法,會在網絡中斷的時候,發生調用
ic->interrupt_callback.callback = decode_interrupt_cb; // 設置中斷回調參數 ic->interrupt_callback.opaque = is;
decode_interrupt_cb 方法
static int decode_interrupt_cb(void *ctx) { VideoState *is = ctx; return is->abort_request; }
3. 打開avformat_open_input
err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts); //同時,將其注入成全局的一個變量。但是具體如何使用呢?? av_format_inject_global_side_data(ic);
4. 查找AVStream
找到不同的AVStream
,並將其放入對應的數組中。等待後續使用。
找到視頻流之後,還會再次同步顯示的比例和視頻的長寬
for (i = 0; i < ic->nb_streams; i++) { AVStream *st = ic->streams[i]; enum AVMediaType type = st->codecpar->codec_type; st->discard = AVDISCARD_ALL; if (type >= 0 && wanted_stream_spec[type] && st_index[type] == -1) if (avformat_match_stream_specifier(ic, st, wanted_stream_spec[type]) > 0) st_index[type] = i; } for (i = 0; i < AVMEDIA_TYPE_NB; i++) { if (wanted_stream_spec[i] && st_index[i] == -1) { av_log(NULL, AV_LOG_ERROR, "Stream specifier %s does not match any %s stream\n", wanted_stream_spec[i], av_get_media_type_string(i)); st_index[i] = INT_MAX; } } if (!video_disable) st_index[AVMEDIA_TYPE_VIDEO] = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, st_index[AVMEDIA_TYPE_VIDEO], -1, NULL, 0); if (!audio_disable) st_index[AVMEDIA_TYPE_AUDIO] = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, st_index[AVMEDIA_TYPE_AUDIO], st_index[AVMEDIA_TYPE_VIDEO], NULL, 0); if (!video_disable && !subtitle_disable) st_index[AVMEDIA_TYPE_SUBTITLE] = av_find_best_stream(ic, AVMEDIA_TYPE_SUBTITLE, st_index[AVMEDIA_TYPE_SUBTITLE], (st_index[AVMEDIA_TYPE_AUDIO] >= 0 ? st_index[AVMEDIA_TYPE_AUDIO] : st_index[AVMEDIA_TYPE_VIDEO]), NULL, 0); is->show_mode = show_mode; //找到視頻流之後,還會再次同步顯示的比例和視頻的長寬 if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) { AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]]; AVCodecParameters *codecpar = st->codecpar; AVRational sar = av_guess_sample_aspect_ratio(ic, st, NULL); if (codecpar->width) set_default_window_size(codecpar->width, codecpar->height, sar); }
開啓對應的解碼線程
打開stream_component_open
對應的AVStream
。打開解碼線程。
ffplay
中對應三種碼流。(視頻、音頻和字幕,對應打開自己的解碼線程)
/* open the streams */ if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) { stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]); } ret = -1; if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) { ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]); } if (is->show_mode == SHOW_MODE_NONE) is->show_mode = ret >= 0 ? SHOW_MODE_VIDEO : SHOW_MODE_RDFT; if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) { stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]); }
開啓對應的線程之前,會初始化好每個碼流的AVCodec
和AVCodecContext
- 初始化
AVCodec
和AVCodecContext
//創建AVCodecContext avctx = avcodec_alloc_context3(NULL); if (!avctx) return AVERROR(ENOMEM); //從找到對應的流中的codecpar,codecpar其實是avcodec_parameters, // 然後將它完全複製到創建的AVCodecContext ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar); if (ret < 0) goto fail; avctx->pkt_timebase = ic->streams[stream_index]->time_base; //根據codec_id找出最合適的codec codec = avcodec_find_decoder(avctx->codec_id); switch(avctx->codec_type){ case AVMEDIA_TYPE_AUDIO : is->last_audio_stream = stream_index; forced_codec_name = audio_codec_name; break; case AVMEDIA_TYPE_SUBTITLE: is->last_subtitle_stream = stream_index; forced_codec_name = subtitle_codec_name; break; case AVMEDIA_TYPE_VIDEO : is->last_video_stream = stream_index; forced_codec_name = video_codec_name; break; } //強制通過解碼器的名字,來打開對應的解碼器 if (forced_codec_name) codec = avcodec_find_decoder_by_name(forced_codec_name); if (!codec) { if (forced_codec_name) av_log(NULL, AV_LOG_WARNING, "No codec could be found with name '%s'\n", forced_codec_name); else av_log(NULL, AV_LOG_WARNING, "No codec could be found with id %d\n", avctx->codec_id); ret = AVERROR(EINVAL); goto fail; } avctx->codec_id = codec->id; // 下面是設置屬性,也不大懂。。這些屬性的意思 if (stream_lowres > codec->max_lowres) { av_log(avctx, AV_LOG_WARNING, "The maximum value for lowres supported by the decoder is %d\n", codec->max_lowres); stream_lowres = codec->max_lowres; } avctx->lowres = stream_lowres; if (fast) avctx->flags2 |= AV_CODEC_FLAG2_FAST; opts = filter_codec_opts(codec_opts, avctx->codec_id, ic, ic->streams[stream_index], codec); //av_dict_set方法,傳入參數的效果,等同於我們用命令行時 - 的傳遞方式 if (!av_dict_get(opts, "threads", NULL, 0)) av_dict_set(&opts, "threads", "auto", 0); if (stream_lowres) av_dict_set_int(&opts, "lowres", stream_lowres, 0); if (avctx->codec_type == AVMEDIA_TYPE_VIDEO || avctx->codec_type == AVMEDIA_TYPE_AUDIO) av_dict_set(&opts, "refcounted_frames", "1", 0); if ((ret = avcodec_open2(avctx, codec, &opts)) < 0) { goto fail; } if ((t = av_dict_get(opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) { av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key); ret = AVERROR_OPTION_NOT_FOUND; goto fail; }
- 初始化對應的解碼線程
有了AVCodecContext 和AVCodec 之後,就可以初始化解碼線程了
對解碼器的參數再次配置(音頻需要),
然後
decoder_init
方法初始化Decoder
結構體
static void decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond) { memset(d, 0, sizeof(Decoder)); d->avctx = avctx; d->queue = queue; d->empty_queue_cond = empty_queue_cond; d->start_pts = AV_NOPTS_VALUE; d->pkt_serial = -1; }
和decoder_start
正式開啓線程。
static int decoder_start(Decoder *d, int (*fn)(void *), void *arg) { packet_queue_start(d->queue); d->decoder_tid = SDL_CreateThread(fn, "decoder", arg); if (!d->decoder_tid) { av_log(NULL, AV_LOG_ERROR, "SDL_CreateThread(): %s\n", SDL_GetError()); return AVERROR(ENOMEM); } return 0; }
packet_queue_start 開啓線程之前,將flush_pkt
送入PacketQueue
隊列當中。
static void packet_queue_start(PacketQueue *q) { SDL_LockMutex(q->mutex); q->abort_request = 0; packet_queue_put_private(q, &flush_pkt); SDL_UnlockMutex(q->mutex); }
不斷的av_read_frame
,將數據讀取出,送入隊列
ps:快進的邏輯後面分析
讀取av_read_frame
ret = av_read_frame(ic, pkt);
讀取失敗,各個流中送入空包,同時鎖住continue_read_thread
,等待10ms
if (ret < 0) { // 讀取結束或失敗。會想各個流,送入一個空的packet.爲什麼要送入空的packet?? if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) { if (is->video_stream >= 0) packet_queue_put_nullpacket(&is->videoq, is->video_stream); if (is->audio_stream >= 0) packet_queue_put_nullpacket(&is->audioq, is->audio_stream); if (is->subtitle_stream >= 0) packet_queue_put_nullpacket(&is->subtitleq, is->subtitle_stream); is->eof = 1; } if (ic->pb && ic->pb->error) break; //讀取失敗的話,讀取失敗的原因有很多,其他地方可能會重新Signal這個鎖condition。如果沒有singal這個condition的話,就會等待10ms之後,再釋放,重新循環讀取. 那這個continue_read_thread 到底是鎖了哪呢? SDL_LockMutex(wait_mutex); SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10); SDL_UnlockMutex(wait_mutex); continue; } else { is->eof = 0; }
todo:continue_read_thread 等同於隊列中的empty_queue_cond
???
讀取成功,送入隊列
變量pkt
中就是我們讀取到的數據。
/* check if packet is in play range specified by user, then queue, otherwise discard */ //記錄stream_start_time stream_start_time = ic->streams[pkt->stream_index]->start_time; //如果沒有pts, 就用dts pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts; //判斷是否在範圍內。如果duration還沒被定義的話,通過 //或者在定義的duration內纔可以,用當前的pts-start_time . //duration 會在解碼器打開之後,纔會被初始化 pkt_in_play_range = duration == AV_NOPTS_VALUE || (pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) * av_q2d(ic->streams[pkt->stream_index]->time_base) - (double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000 <= ((double)duration / 1000000); // 將解複用得到的數據包添加到對應的待解碼隊列中 if (pkt->stream_index == is->audio_stream && pkt_in_play_range) { packet_queue_put(&is->audioq, pkt); } else if (pkt->stream_index == is->video_stream && pkt_in_play_range && !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) { packet_queue_put(&is->videoq, pkt); } else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) { packet_queue_put(&is->subtitleq, pkt); } else { av_packet_unref(pkt); }
最後退出時,需要關閉對應資源 這個線程關閉的時候。會清空AVFormatContext 和鎖資源
if (ic && !is->ic) avformat_close_input(&ic); if (ret != 0) { SDL_Event event; event.type = FF_QUIT_EVENT; event.user.data1 = is; SDL_PushEvent(&event); } SDL_DestroyMutex(wait_mutex); return 0;
入列的操作
整體的流程如上,讓我們在特別關注一下,具體的入列的操作
MyAVPacketList結構體
PacketList中存儲的單元是自己定義的MyAVPacketList
結構體
typedef struct MyAVPacketList { AVPacket pkt; struct MyAVPacketList *next; int serial; } MyAVPacketList;
結構體中主要是保存了AVPacket
數據和隊列的下一個的指針。
同時還保留了一個serial
變量。它可以理解成操作數。在初始化和快進的時候,會增加操作數。小於的操作數,在下一次顯示的時候,會直接被拋棄。
隊列操作packet_queue_put
static int packet_queue_put(PacketQueue *q, AVPacket *pkt) { int ret; //鎖住隊列中的互斥鎖。這個鎖是每個隊列自己的 SDL_LockMutex(q->mutex); ret = packet_queue_put_private(q, pkt); SDL_UnlockMutex(q->mutex); // 如果不是flush類型的包,並且沒有成功入隊,則銷燬當前的包 if (pkt != &flush_pkt && ret < 0) av_packet_unref(pkt); return ret; } static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt) { MyAVPacketList *pkt1; // 如果隊列本身處於捨棄狀態,則直接返回-1 if (q->abort_request) return -1; // 創建一個包 pkt1 = av_malloc(sizeof(MyAVPacketList)); if (!pkt1) return -1; pkt1->pkt = *pkt; pkt1->next = NULL; // 判斷包是否數據flush類型,調整包序列 // 在創建decoder thread時,會刷入一個flush_pkt,這個時候會提高serial if (pkt == &flush_pkt) q->serial++; pkt1->serial = q->serial; // 調整指針。存入隊尾,若沒有隊尾,就放在開頭。 if (!q->last_pkt) q->first_pkt = pkt1; else q->last_pkt->next = pkt1; q->last_pkt = pkt1; q->nb_packets++; q->size += pkt1->pkt.size + sizeof(*pkt1); q->duration += pkt1->pkt.duration; /* XXX: should duplicate packet data in DV case */ // 條件信號 //通知讀的部分,有數據了,可以繼續讀取了 //q->cond 在讀取的時候,如果沒有數據就會鎖住,這樣,生產了數據,就會通知讀取。相等於一個讀鎖 SDL_CondSignal(q->cond); return 0; }
隊列操作packet_queue_put_nullpacket
在讀取錯誤時,也會丟入一個空白。
static int packet_queue_put_nullpacket(PacketQueue *q, int stream_index) { // 創建一個空數據的包 AVPacket pkt1, *pkt = &pkt1; av_init_packet(pkt); pkt->data = NULL; pkt->size = 0; pkt->stream_index = stream_index; return packet_queue_put(q, pkt); }
其他
弄清楚q->mutex 是什麼鎖 主線程初始化中,進行初始化的,是每個PacketQueue 都有一個自己的互斥鎖和條件鎖的。
static int packet_queue_init(PacketQueue *q) { // 爲一個包隊列分配內存 memset(q, 0, sizeof(PacketQueue)); // 創建互斥鎖 q->mutex = SDL_CreateMutex(); if (!q->mutex) { av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError()); return AVERROR(ENOMEM); } // 創建條件鎖 q->cond = SDL_CreateCond(); if (!q->cond) { av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError()); return AVERROR(ENOMEM); } // 默認情況捨棄入隊的數據 q->abort_request = 1; return 0; }
條件鎖q->cond
在讀取的時候,如果沒有數據就會鎖住,這樣,生產了數據,就會通知讀取。相等於一個讀鎖
視頻解碼線程video_thread
在read_thread
的中對應視頻流時,初始化好了AVCodec
和AVCodecContext
。通過decoder_start
方法,開啓了video_thread
。
在video_thread
中需要創建AVFrame來接受解碼後的數據,確定視頻的幀率。
然後開啓解碼循環。
不斷的從隊列中獲取解碼前的數據,然後送入解碼器解碼。
再得到解碼後的數據,在送入對應的隊列當中。
初始化參數
創建AVFrame和得到大致的視頻幀率
//創建AVFrame AVFrame *frame = av_frame_alloc(); //設置好time_base和frame_rate AVRational tb = is->video_st->time_base; // 猜測視頻幀率 AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
開始循環解碼
從隊列中取得解碼器的數據packet_queue_get
取到要解碼的數據,放到 &d->pkt_temp上
do { int ret = -1; // 如果處於捨棄狀態,直接返回 if (d->queue->abort_request) return -1; // 如果當前沒有包在等待,或者隊列的序列不相同時,取出下一幀 if (!d->packet_pending || d->queue->serial != d->pkt_serial) { AVPacket pkt; do { // 隊列爲空 if (d->queue->nb_packets == 0) //當隊列爲空的時候,就會通知釋放這個條件鎖。之前在讀取線程,讀取失敗的時候,也有鎖住這個鎖進行等待。 SDL_CondSignal(d->empty_queue_cond); // 從隊列中取數據 if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0) return -1; // 如果是第一個數據,則會把數據清空一遍。然後開始獲取 if (pkt.data == flush_pkt.data) { //重置解碼器的狀態,因爲第一次開始解碼或者快進的時候,會先存入一個flush_data,當取到這個的時候,就需要去/重置解碼器的狀態 //Reset the internal decoder state / flush internal buffers. Should be called avcodec_flush_buffers(d->avctx); d->finished = 0; d->next_pts = d->start_pts; d->next_pts_tb = d->start_pts_tb; } } while (pkt.data == flush_pkt.data || d->queue->serial != d->pkt_serial); // 釋放包 av_packet_unref(&d->pkt); // 更新包 d->pkt_temp = d->pkt = pkt; // 包等待 d->packet_pending = 1; }
得到pkt_temp之後,送入解碼器進行解碼 解碼成功後,還需要得去對應的pts。
// 根據解碼器類型判斷是音頻還是視頻還是字幕 switch (d->avctx->codec_type) { // 視頻解碼 case AVMEDIA_TYPE_VIDEO: // 解碼視頻 ret = avcodec_decode_video2(d->avctx, frame, &got_frame, &d->pkt_temp); // 解碼成功,更新時間戳 if (got_frame) { if (decoder_reorder_pts == -1) { frame->pts = av_frame_get_best_effort_timestamp(frame); } else if (!decoder_reorder_pts) { // 如果不重新排列時間戳,則需要更新幀的pts frame->pts = frame->pkt_dts; } } break; // 音頻解碼 case AVMEDIA_TYPE_AUDIO: // 音頻解碼 ret = avcodec_decode_audio4(d->avctx, frame, &got_frame, &d->pkt_temp); // 音頻解碼完成,更新時間戳 if (got_frame) { AVRational tb = (AVRational){1, frame->sample_rate}; // 更新幀的時間戳 if (frame->pts != AV_NOPTS_VALUE) frame->pts = av_rescale_q(frame->pts, av_codec_get_pkt_timebase(d->avctx), tb); else if (d->next_pts != AV_NOPTS_VALUE) frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb); // 更新下一時間戳 if (frame->pts != AV_NOPTS_VALUE) { d->next_pts = frame->pts + frame->nb_samples; d->next_pts_tb = tb; } } break; // 字幕解碼 case AVMEDIA_TYPE_SUBTITLE: ret = avcodec_decode_subtitle2(d->avctx, sub, &got_frame, &d->pkt_temp); break; } // 判斷是否解碼成功 // 如果解碼失敗的話,包繼續等待 if (ret < 0) { d->packet_pending = 0; } else { d->pkt_temp.dts = d->pkt_temp.pts = AV_NOPTS_VALUE; //下面這個操作不明白??? if (d->pkt_temp.data) { if (d->avctx->codec_type != AVMEDIA_TYPE_AUDIO) ret = d->pkt_temp.size; d->pkt_temp.data += ret; d->pkt_temp.size -= ret; if (d->pkt_temp.size <= 0) d->packet_pending = 0; } else { if (!got_frame) { d->packet_pending = 0; d->finished = d->pkt_serial; } } }
得到pkt_temp之後,還需要判斷,是否需要拋棄
static int get_video_frame(VideoState *is, AVFrame *frame) { int got_picture; // 解碼視頻幀 if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0) return -1; // 判斷是否解碼成功 if (got_picture) { double dpts = NAN; if (frame->pts != AV_NOPTS_VALUE) //通過 pts*av_q2d(timebase)可以得到準確的時間 dpts = av_q2d(is->video_st->time_base) * frame->pts; //重新得到視頻的比例 frame->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame); // 判斷是否需要捨棄該幀。 if (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) { if (frame->pts != AV_NOPTS_VALUE) { //得到的是當前的時間和時間鍾之間的差值。 double diff = dpts - get_master_clock(is); if (!isnan(diff) && fabs(diff) < AV_NOSYNC_THRESHOLD && diff - is->frame_last_filter_delay < 0 && is->viddec.pkt_serial == is->vidclk.serial && is->videoq.nb_packets) { is->frame_drops_early++; av_frame_unref(frame); got_picture = 0; } } } } return got_picture; }
計算時間和入列
// 計算幀的pts、duration等 //每一幀的時長,應該就是等於幀率的倒數 duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0); pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb); // 放入到已解碼隊列 ret = queue_picture(is, frame, pts, duration, av_frame_get_pkt_pos(frame), is->viddec.pkt_serial); av_frame_unref(frame);
將已經解碼的數據放到隊列當中queue_picture
static int queue_picture(VideoState *is, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial) { Frame *vp; #if defined(DEBUG_SYNC) printf("frame_type=%c pts=%0.3f\n", av_get_picture_type_char(src_frame->pict_type), pts); #endif //先從隊列中取。因爲要queue的大小有限,所以先判斷是否可以繼續寫入 if (!(vp = frame_queue_peek_writable(&is->pictq))) return -1; vp->sar = src_frame->sample_aspect_ratio; vp->uploaded = 0; vp->width = src_frame->width; vp->height = src_frame->height; vp->format = src_frame->format; vp->pts = pts; vp->duration = duration; vp->pos = pos; vp->serial = serial; set_default_window_size(vp->width, vp->height, vp->sar); //將src中的數據送入vp當中,並且重置src av_frame_move_ref(vp->frame, src_frame); //重新推入 frame_queue_push(&is->pictq); return 0; } /** * 查找可寫幀 * @param f [description] * @return [description] */ static Frame *frame_queue_peek_writable(FrameQueue *f) { /* wait until we have space to put a new frame */ SDL_LockMutex(f->mutex); // 如果幀隊列大於最大 while (f->size >= f->max_size && !f->pktq->abort_request) { SDL_CondWait(f->cond, f->mutex); } SDL_UnlockMutex(f->mutex); if (f->pktq->abort_request) return NULL; //writable index 是 windex return &f->queue[f->windex]; } static void frame_queue_push(FrameQueue *f) { //會將可寫的index偏移 if (++f->windex == f->max_size) f->windex = 0; SDL_LockMutex(f->mutex); f->size++; SDL_CondSignal(f->cond); SDL_UnlockMutex(f->mutex); }
已放入隊列,最後
av_frame_unref(frame)
原來的創建的frame就可以了釋放了。
其他
attached_pic入列 attached_pic的意思是有附帶的圖片。比如說一些MP3,AAC音頻文件附帶的專輯封面。所以,就是如果有,就去顯示吧。
is->queue_attachments_req = 1;
主線程視頻顯示部分
在主線程的EventLoop開啓之後,會進行繪製。先會根據主的時間鍾進行同步,然後進行顯示。這裏我們因爲只分析視頻部分。所以就不關注時間鐘的同步了。
視頻的顯示
同步後時間,取到具體的frame時,就送入顯示。
/* display the current picture, if any */ static void video_display(VideoState *is) { //當window還沒創建的時候,就是第一次,會去初始化 if (!window) video_open(is); //SDL常規操作兩則 SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); SDL_RenderClear(renderer); if (is->audio_st && is->show_mode != SHOW_MODE_VIDEO) video_audio_display(is); else if (is->video_st) //video走這裏 video_image_display(is); //送顯 SDL_RenderPresent(renderer); }
- 第一次會進入
video_open
。 先去創建SDL_Window
和SDL_Renderer
。 舉Android的例子來說,在Android中SDL使用的是OpenGL。SDL_CreateWindow
就是通過ANativeWindow 來創建一個GL Surface。同時創建GLContext。SDL_CreateRenderer
就是glmakeCurrent
, 同時對Renderer的各個方法進行初始化,以供後面調用。 - 確認打開做好
SDL_Window
和SDL_Renderer
之後,就會調用video_image_display
進行顯示。
static void video_image_display(VideoState *is) { Frame *vp; Frame *sp = NULL; SDL_Rect rect; vp = frame_queue_peek_last(&is->pictq); //省略了關於字幕的部分... //計算SDL_Rect 就是當前顯示的範圍 calculate_display_rect(&rect, is->xleft, is->ytop, is->width, is->height, vp->width, vp->height, vp->sar); //如果這一幀還沒顯示過 if (!vp->uploaded) { int sdl_pix_fmt = vp->frame->format == AV_PIX_FMT_YUV420P ? SDL_PIXELFORMAT_YV12 : SDL_PIXELFORMAT_ARGB8888; //如果需要重新創建紋理的話 if (realloc_texture(&is->vid_texture, sdl_pix_fmt, vp->frame->width, vp->frame->height, SDL_BLENDMODE_NONE, 0) < 0) return; //刷新紋理 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; } SDL_RenderCopyEx(renderer, is->vid_texture, NULL, &rect, 0, NULL, vp->flip_v ? SDL_FLIP_VERTICAL : 0); 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 } }
通過calculate_display_rectton
這裏的就是計算需要顯示的區域。
然後如果還沒創建紋理的話,在realloc_texture
內調用SDL_CreateTexture
來創建紋理。
在upload_texture
內調用SDL_UpdateTexture
來更新紋理。
最後是進行SDL_RendererCopy
.或者SDL_RendererCopyEx
(SDL_RendererCopy
的加強版,可以通知顯示區域和翻轉)。
然後送顯SDL_CreateTexture
。
這裏可以看到依舊是SDL視頻送顯的經典套路。
SDL視頻送顯的經典套路.png
處理按鍵的事件
這部分暫時掠過...
總結
整體的流程.jpg
image.png