1. 程式人生 > >OBS視訊資料輸出流程(模組載入,編碼,推流)詳細說明

OBS視訊資料輸出流程(模組載入,編碼,推流)詳細說明

宣告:本文章內容僅代表個人觀點,不能保證完全的正確性,僅供參考!

先上個自己畫的圖,結合流程圖和文字解釋,理解起來會更快些

1、視訊輸出初始化
	程式執行時,初始化OBS,視訊相關的初始化是再mainWindow中進行的
	OBSApp::OBSInit() -> mainWindow->OBSInit()
		InitBasicConfig()讀取appdata目錄下配置檔案中Video相關的引數,沒有設定的引數使用介面
		InitBasicConfigDefaults()介面中載入的預設引數
	OBSBasic::RetsetVideo(),重置視訊設定
		obs_video_info ovi;獲取視訊設定的引數,包括:幀率,顏色格式,YUV顏色空間,YUV顏色範圍,背景及
		輸出解析度等
		呼叫 AttemptToResetVideo() -> obs_reset_video(),將當前引數嘗試重置給Video
		停止當前的video,使用新引數ovi重新初始化video,obs_init_video(ovi)
		obs_init_video(struct obs_video_info *ovi)
		通過make_video_info函式,將ovi引數設定給video_out_info vi;
		呼叫video_output_open函式啟動視訊資料輸出執行緒
		int video_output_open(video_t **video, struct video_output_info *info)
			建立video_output *out物件,拷貝info中的資料到out->info,設定out->frame_time每一幀的時間差
			啟動執行緒函式 video_thread,並將out作為引數傳入
			初始化out->cache,呼叫video_frame_init將cache中每一幀的內容按照視訊格式初始化
			完成後,將out物件賦值給obs->video->video
			其中執行緒執行函式video_thread就是視訊輸出執行緒,等待訊號量video->update_semaphore 被喚醒
			執行video_output_cur_frame函式,獲取視訊快取中第一幀,從video->inputs中獲取輸出型別
			呼叫編碼器繫結的回撥函式input->callback,receive_video(),進行視訊資料編碼.
			而video->update_semaphore 訊號量是在所有畫面合成完成後被喚醒,後面將介紹是如何喚醒的
			video->inputs中儲存的是輸出型別,包括推流和錄影,後面將會說到是如何新增的
		啟動畫面合成執行緒函式 obs_graphics_thread(),後面單獨介紹畫面合成執行緒的流程
	至此視訊輸出的初始化完成,輸出執行緒和畫面合成執行緒已啟動
	
	
2、obs-x264、obs-qsv11、obs-ffmpeg、rtmp模組載入
	obs-x264是軟編,obs-qsv11是intel硬編,obs-ffmpeg中包含ffmpeg_aac、ffmpeg_opus、以及nvenc編碼
	rtmp是推流模組
	在OBSInit()函式初始化視訊後,將執行載入模組的操作,這裡將介紹obs-x264模組是怎麼載入並且被呼叫的,
	其他幾個模組的載入是類似的;
	OBSInit() -> AddExtraModulePaths(),新增載入模組的路徑 -> obs_load_all_modules()載入所有模組
	void obs_load_all_modules(void)
		obs_find_modules()遍歷所有模組目錄,load_all_callback是對找到的模組執行的回撥函式,
		find_modules_in_path()在每個目錄中查詢dll檔案,並執行函式process_found_module() -> 執行回撥
		也就是load_all_callback函式
	load_all_callback() -> obs_open_module() -> os_dlopen()獲取開啟模組的控制代碼,接著執行的
		load_module_exports函式獲取模組中的介面地址繫結給obs_module各個函式指標,obs-x264模組只有
		一個介面地址obs_module_load,被繫結至mod->load;將模組的一些資訊填充,包括模組的名稱,路徑等
		執行obs_init_module(module),該函式的作用就是為了呼叫剛才繫結的module->load()介面,也就是
		obs-x264模組中的obs_module_load函式
	obs_module_load() -> 巨集obs_register_encoder(&obs_x264_encoder),其中obs_x264_encoder是個全域性變數,
	再obs-x264.c檔案中完成了obs_x264_encoder的初始化,繫結函式介面,id,編碼型別,編碼方式;
	-> obs_register_encoder_s()做一系列的檢查 -> 巨集REGISTER_OBS_DEF 將obs_x264_encoder新增到
	obs->encoder_types
	至此obs-x264模組的載入已完成,後面會介紹如何使用x264編碼器
3、輸出設定(簡單模式)
	模組載入完成後,將會對輸出進行設定,以下是對視訊輸出設定的說明
	OBSInit() -> ResetOutputs() 從配置檔案中讀取當前的視訊設定是否是簡單模式,重置outputHandler,並建立
	簡單輸出模式的指標賦值給outputHandler -> CreateSimpleOutputHandler() -> new SimpleOutput -> 建構函式
	SimpleOutput::SimpleOutput
		獲取當前編碼型別:軟編、硬編(QSV)、硬編(nvenc),呼叫LoadStreamingPreset_h264根據不同的編碼型別
		建立不同的編碼器,賦值給成員變數h264Streaming指標,該指標後面將會新增到視訊輸出的編碼器中
		obs_video_encoder_create,根據編碼器id建立編碼器 -> 呼叫create_encoder()函式,根據編碼器id和
		編碼器型別建立,在create_encoder函式中構造obs_encoder *encoder,根據id呼叫find_encoder()函式,從
		obs->encoder_types找到指定的編碼器,賦值給encoder->info,構造完成後,h264Streaming指標就是當前選
		擇的編碼器
4、開啟推流
	OBSBasic::StartStreaming() -> outputHandler->StartStreaming()
	SimpleOutput::StartStreaming(obs_service_t *service)
		Active()是判斷推流、錄影、回放快取是否啟用;正常狀態下返回false,也就需要執行SetupOututs()函式
		SetupOutputs中呼叫了Update()函式,在Update中如果視訊格式不是NV12或者I420,將編碼器的首要格式設定
		為NV12;更新編碼器h264Streaming中的設定引數,如果編碼器註冊了update介面(軟編和qsv有nvenc沒有)
		將新的引數更新到編碼器中,回到SetupOutputs,呼叫obs_encoer_set_video,設定h264Streaming->media為
		obs->video.video,設定timebase_num = fps_den;timebase_den = fps_num;注意這裡是把幀率的分子分母反
		著賦值給編碼器,完成後回到StartStreaming建立推流的物件
		呼叫obs_output_create()函式,根據輸出id建立推流物件,與建立編碼物件類似,推流物件在載入模組時已
		新增到obs->output_types中,獲取到的推流輸出物件賦值給streamOutput指標
		呼叫obs_output_set_video_encoder()函式,將推流輸出streamOutput->video_encoder設定為編碼器
		建立好的h264Streaming
		呼叫obs_output_start() -> obs_output_actual_start() 回撥推流物件output->info.start()回撥函式開啟
		推流,其中start繫結至rtmp_stream_start
	static bool rtmp_stream_start(void *data)
		建立執行緒,執行connect_thread()函式,
		static void *connect_thread(void *data)
			init_connect()初始化推流;呼叫free_packets清空stream->packets,獲取推流設定,賦值到stream
			try_connect()連線rtmp伺服器,RTMP_Init(&stream->rtmp)初始化rtmp客戶端,設定推流伺服器地址、
			使用者名稱、密碼、流地址、音訊編碼名稱(為何沒有新增視訊編碼名稱,母雞)
			RTMP_Connect()連線rtmp伺服器,RTMP_ConnectStream()連線rtmp流地址
			init_send()啟動傳送函式,reset_semaphore()重置傳送訊號量,建立推流執行執行緒send_thread,傳送
			視訊關鍵資料send_meta_data(),開啟推流資料捕獲obs_output_begin_data_capture()
			其中 send_thread()執行緒函式:迴圈等待訊號量stream->send_sem 被喚醒,喚醒後 get_next_packet()
			取出佇列中的第一個已編碼資料包,執行 send_packet 函式,呼叫flv_packet_mux進行flv資料封包,
			再呼叫RTMP_Write()傳送資料包,完成視訊資料推流.
		bool obs_output_begin_data_capture(obs_output_t *output, uint32_t flags)
			獲取output->info也就是rtmp物件設定的flags,是否包含編碼,音視訊資料,繫結伺服器;開啟捕獲,
			hook_data_capture(),這裡是繫結編碼完成後的音視訊資料到rtmp推流的回撥函式,音視訊編碼完成的
			資料回撥都是interleave_packets(),start_audio_encoders()新增音訊已編碼資料捕獲,
			obs_encoder_start()新增視訊已編碼資料捕獲,呼叫obs_encoder_start_internal(),將回調函式
			interleave_packets,引數param也就是推流輸出物件output構造成結構體encoder_callback cb;
				static void interleave_packets (void *data, struct encoder_packet *packet)
					呼叫obs_encoder_packet_create_instance(&out, packet);拷貝packet中的資料到區域性變數out
					中,其中進行malloc的時候,多申請了一個long型別長度的記憶體,這個pref是這個資料包的引用
					計數器;如果音視訊資料都收到時,呼叫apply_interleaved_packet_offset,這個函式是調整
					時間補償或者時間修復的嗎?否則呼叫check_received介面將當前的音訊或視訊已收到標識設定
					為true
						was_started = output->received_audio && output->received_video;
						......
						if (was_started)
							apply_interleaved_packet_offset(output, &out);
						else
							check_received(output, packet);
					根據編碼時間戳,將當前資料包插入到輸出佇列中,並將output->highest_audio_ts設定為當前
					資料包的編碼時間戳
						insert_interleaved_packet(output, &out);
						set_higher_ts(output, &out);
					如果當前是否第一次收到了音訊以及視訊資料包,呼叫prune_interleaved_packets(output)對數
					據包中的內容進行修剪,修剪規則如下:
					先找出第一幀音訊和第一幀視訊的資料包,以第一幀視訊資料包的index為基準,對比兩個資料包
					的時間戳的差值:
						如果音訊資料包的時間戳減去視訊資料包的時間戳的數值大於每幀視訊間隔的時間差,那麼需
						要刪除這個音訊資料包時間戳之前的所有音視訊資料包
						如果沒有找到這樣的音訊資料包,那麼就需要找出音視訊資料包的時間戳差距最小的那個數
						據包的index,如果這個index的值比第一幀視訊資料包的index小,那麼需要刪除這個index
						之前的所有資料包,如果比第一幀視訊資料包的index大,那麼需要刪除第一幀視訊資料包
						之前的所有資料包
					通過對第一次傳送的音視訊資料包的裁剪後,當前的待發送資料包中第一幀音視訊資料包的時間戳
					的差距最小,以達到首次傳送的音視訊資料是同步的,調整修正完成後的待發送資料包相關的時間
					戳,再次確保傳送的第一幀音視訊資料包的準確性,並且重新調整待發送資料包的index,呼叫傳送
					資料包函式 send_interleaved
					如果是後續收到的音視訊資料包,則直接呼叫傳送函式 send_interleaved
						if (output->received_audio && output->received_video) {
							if (!was_started) {
								if (prune_interleaved_packets(output)) {
									if (initialize_interleaved_packets(output)) {
										resort_interleaved_packets(output);
										send_interleaved(output);
									}
								}
							} else {
								send_interleaved(output);
							}
						}
					static inline void send_interleaved (struct obs_output *output)
						確認待發送資料包中的第一個資料包時間戳是合法的
							if (!has_higher_opposing_ts(output, &out))
								return;
						把第一個資料包從佇列中移除
							da_erase(output->interleaved_packets, 0);
						如果是視訊資料包的話,在這裡統計總的傳送幀數
							if (out.type == OBS_ENCODER_VIDEO) {
								output->total_frames++;
						呼叫output->info.encoded_packet回撥函式 rtmp_stream_data,進入rtmp準備傳送
						static void rtmp_stream_data (void *data, struct encoder_packet *packet)
							將資料包的資料拷貝至區域性變數new_packet,資料包的引用技術+1,將new_packet添
							加到待推流資料塊中,視訊資料包:add_video_packet,其中視訊資料包在新增之前
							檢查是否有需要丟棄的幀,檢查完成後呼叫add_packet,將資料包追加到
							stream->packets佇列中,新增成功後,喚醒訊號量stream->send_sem,通知執行緒
							send_thread(),執行傳送
					
			將cb新增到視訊編碼器encoder->callbacks佇列中,如果新增的是第一個已編碼資料推流回調,呼叫
			add_connection(),啟動音訊資料輸出捕獲start_raw_video(),video->raw_active的值增加,說明下
			raw_active的值,是控制視訊資料是否輸出的開關,後面在輸出視訊資料時要用到;呼叫
			video_output_connect()函式,關聯視訊資料到編碼的回撥,建立video_input input結構體,將資料編
			碼的回撥函式 receive_video,編碼器物件encoder,視訊資料資訊,填充到input中,並將input新增到
			video->inputs佇列裡,這個佇列後面將會用到,其作用是合成後的視訊資料呼叫這個佇列中的回撥
			進行視訊資料輸出(音訊資料新增編碼回撥跟視訊類似,在add_connection時呼叫audio_output_connect
			,構造audio_input input;將其加入到指定混音器mix->inputs中
				static void receive_video(void *param, struct video_data *frame)
					拷貝視訊資料到encoder_frame enc_frame,呼叫do_encode()執行編碼,在do_encode函式中,
					初始化待完成的資料包encoder_packet pkt;呼叫編碼器繫結的編碼函式,此處舉例x264編碼
					回撥obs_x264_encode(),編碼完成後呼叫send_packet,如果當前時視訊幀的第一幀,需要單獨
					呼叫send_first_video_packet函式,將視訊的SEI資訊新增到視訊資料中,呼叫之前繫結的推流
					回撥函式 interleave_packets(),進行視訊資料傳送
5、視訊畫面生成
	在初始化視訊時,啟動了一個執行緒函式obs_graphics_thread(),所有畫面源的合成,畫面顯示以及視訊輸出都在
	這個函式裡觸發,說白了這裡就時畫面生成和輸出的源頭
	void *obs_graphics_thread(void *param)
		迴圈處理畫面,會根據設定的視訊幀數,每隔固定時間處理一次畫面
		tick_sources(),沒有深入研究具體是什麼內容
		output_frame():輸出當前視訊幀
		static inline void output_frame(bool raw_active)
			呼叫render_video(),渲染視訊資料,在開啟推流和錄影功能時,呼叫render_output_texture(),渲染輸
			出幀,並儲存在video->convert_textures和video->output_textures中,再呼叫stage_output_texture
			將畫面儲存到video->copy_surfaces
			呼叫download_frme,從video->copy_surfaces中拷貝出當前視訊幀資料到video_data *frame,這樣就
			拿到了需要輸出的視訊畫面;
			將frame傳入output_video_data(),在該函式中,呼叫video_output_lock_frame()函式,拷貝
			input->cache[last_add]給output_frame,需要注意的是,這個拷貝是將cache[]中的指標地址拷貝
			過來了,通過格式轉換函式例如copy_rgb_frame,將input_frame中的資料內容拷貝到output_frame,
			實際上也就是將視訊內容拷貝到了input->cache[last_add]中,再呼叫video_output_unlock_frame()函
			數,喚醒訊號量video->update_semaphore,通知執行緒video_thread視訊輸出資料已就緒,執行資料輸出
			、編碼、rtmp推流
		呼叫render_displays()將當前視訊畫面顯示在視窗中,
		sleep直到下一幀視訊資料時間戳