關於OBS中視頻的採集和編碼,在OBSVideoCapture.cpp文件中包含兩個線程函數EncodeThread和MainCaptureThread,分別調用函數EncodeLoop和MainCaptureLoop。經過事件變量HANDLE hVideoEvent;來同步採集和編碼視頻。具體就是在OBS::Start函數中建立這兩個線程,而後採集視頻線程中經過while循環WaitForSingleObject(hVideoEvent, INFINITE)來等待事件hVideoEvent的狀態,而後在編碼線程中根據編碼速度來調用SetEvent(hVideoEvent);喚醒採集視頻。web
在EncodeLoop函數中的while循環內,兩次調用SleepToNS,每次sleepTargetTime都加上(frameTimeNS/2)時間,其中frameTimeNS爲幀間隔時間,這樣的目的是避免編碼線程空轉耗費cpu。若是編碼線程耗費時間過長(即SleepToNS的參數時間滯後於當前時間),則會累加no_sleep_counter變量,當該變量達到skipThreshold(encoderSkipThreshold*2)時,就再也不執行SetEvent(hVideoEvent);,這樣採集線程就會阻塞再也不採集視頻。若是接下來因爲採集線程中止了採集,編碼線程有足夠的cpu時間去編碼處理,SleepToNS會返回true(即當前時間滯後於目標時間sleepTargetTime,睡眠目標時間或者叫睡眠到sleepTargetTime這個時間點),則說明編碼線程又遇上節奏,那就將no_sleep_counter變量清零,而後會設置messageTime時間爲latestVideoTime+3000,這個latestVideoTime時間基本跟當前時間同樣,這樣當3秒鐘以後就會將以前因爲編碼線程滯後調用AddStreamInfo添加的信息刪除,即調用RemoveStreamInfo函數,這個經過追蹤messageTime變量可知。
在OBSVideoCapture.cpp文件中的視頻編碼和採集線程中會進行一些時間戳的處理,在編碼線程處理函數OBS::EncodeLoop中對OBS的類成員變量QWORD latestVideoTimeNS;賦值,NS表明的是納秒。在視頻採集線程處理函數OBS::MainCaptureLoop中會根據這個變量來賦值局部變量QWORD curStreamTime = latestVideoTimeNS;,根據變量字面意思理解即:將當前流時間賦值爲最新的視頻時間(在編碼線程中不斷去更新這個時間即latestVideoTimeNS變量),注意在編碼線程中這個變量latestVideoTimeNS值的變化。windows
在編碼線程處理函數OBS::EncodeLoop中首先定義並初始化變量QWORD streamTimeStart = GetQPCTimeNS();,根據變量字面意思即編碼流開始的時間,而後定義並初始化變量QWORD frameTimeNS = 1000000000/fps;,這個也就是以納秒爲單位的幀間隔時間。咱們以frameTimeNS爲單位,指望之後在frameTimeNS、2*frameTimeNS、3*frameTimeNS……n*frameTimeNS這樣的時間點來做爲每個幀的編碼時間戳。考慮到編碼視頻幀所耗費的時間,並且也不想讓編碼線程空轉耗費cpu,因此就經過SleepToNS函數來將幀間隔時間減掉編碼時間所剩下的時間sleep掉。因此在編碼線程處理函數OBS::EncodeLoop的while循環中,主要就是編碼處理視頻幀即調用ProcessFrame函數,而後經過SleepToNS函數控制速度,若是過快會空轉浪費cpu,若是太慢則中止喚醒採集視頻線程節約cpu時間,若是編碼發送視頻幀即ProcessFrame函數執行時間比較小不到幀間隔時間,則SleepToNS函數會將幀間隔時間減掉ProcessFrame函數執行時間所剩下的時間sleep掉。因此兩次調用SleepToNS的參數爲(sleepTargetTime += (frameTimeNS/2)。緩存
在第一次調用SleepToNS函數時,sleepTargetTime爲一個整時間點(即streamTimeStart+i*frameTimeNS),也就是說前面所說的編碼碼流開始後的frameTimeNS、2*frameTimeNS、3*frameTimeNS……n*frameTimeNS這樣的時間點,也即指望的每一幀的編碼時間戳,暫時稱這些時間點爲整時間點,而後調用SleepToNS函數sleep到半個幀間隔後調用SetEvent(hVideoEvent);喚醒視頻採集線程,而後接着去調用SleepToNS函數sleep半個幀間隔,此時到達整時間點開始調用編碼處理視頻幀函數ProcessFrame。再下一次編碼循環到while循環的第一個SleepToNS函數時,參數依然爲下一個半個幀間隔,若是上次編碼所耗費時間小於幀間隔,則是最但願的狀況,編碼發送視頻幀比較快,若是耗費時間大於半個幀間隔,則累加no_sleep_counter變量,而後去喚醒採集視頻線程,而後接着去調用SleepToNS函數去sleep一個幀間隔的後一半時間,此時若是剛剛的視頻幀處理完,那就將no_sleep_counter變量清零,說明雖然編碼發送視頻幀大於半個幀間隔時間,可是仍是在一個幀間隔時間內完成,說明編碼線程不算慢不至於致使達不到幀率的設置。ide
基於以上分析,在編碼線程處理函數OBS::EncodeLoop中對視頻幀while循環處理,其中當整點時候開始去編碼發送視頻幀,而這個所耗費的時間可能超過半個幀間隔時間也可能不超過半個幀間隔時間:若是當前視頻幀編碼發送須要耗費的時間大於半個幀間隔,則在第一個SleepToNS執行完後其還在處理中,此時假設尚未達到skipThreshold,此時會喚醒視頻採集線程,而後去採集下一幀;另外一種狀況若是當前視頻幀編碼發送須要耗費的時間小於半個幀間隔,即早於第一個SleepToNS函數的目標時間,對於這兩種狀況,在喚醒的採集線程函數中QWORD curStreamTime = latestVideoTimeNS;即須要對curStreamTime進行賦值,因此爲了折中,就在編碼線程處理函數OBS::EncodeLoop的第一個SleepToNS函數調用後賦值latestVideoTimeNS;,即將其賦值爲半個幀間隔時間也就是當前時間。其實這個地方也不能算作是爲了折中,由於喚醒視頻採集線程前的latestVideoTimeNS = sleepTargetTime;語句中,當前時間確定是>=sleepTargetTime值的,由於只有>=第一個SleepToNS纔會返回。(這一句註釋是錯誤的,仍是第一次的理解是正確的,這裏的確是爲了折中,由於這個latestVideoTime在編碼線程中看其實就是視頻幀的編碼時間戳,而latestVideoTimeNS相對來講只是一個更加細化的時間表示粒度,下一段明確指出這個值會賦值給curStreamTime,從名字字面理解是當前媒體流的時間)函數
在視頻採集線程處理函數OBS::MainCaptureLoop中定義QWORD lastStreamTime變量,並在每次採集視頻幀時賦值lastStreamTime = curStreamTime;,而由QWORD curStreamTime = latestVideoTimeNS;可知是將上一幀的編碼時間戳(這個編碼時間戳並非整點時間戳,而是整點時間戳再加上半個幀間隔,能夠理解成視頻幀採集時間戳),並且從frameDelta這個名字看也知道,這個主要是用來根據兩個幀的間隔即QWORD frameDelta = curStreamTime-lastStreamTime;作相應的處理,好比肯定將來等多久請求關鍵幀,而lastStreamTime這個變量主要用來賦值給第一個視頻幀的編碼時間戳。在函數OBS::MainCaptureLoop中首先必須是先賦值了第一個視頻幀的編碼時間戳即firstFrameTimestamp,而後才採集到視頻幀數據,這樣編碼線程中就能夠根據條件(curFramePic && firstFrameTimestamp)進行編碼發送處理。oop
對於一個正常的視頻採集編碼運行過程,當程序運行到函數OBS::MainCaptureLoop中的while循環內部時,QWORD frameDelta = curStreamTime-lastStreamTime;語句來獲取到當前待採集的幀與前一幀的間隔,理想狀況下這個frameDelta應該爲幀間隔。在該函數中定義了變量int copyWait = NUM_RENDER_BUFFERS-1;,這個copyWait變量表明瞭採集到的前面多少個視頻幀不顯示,固然也就不編碼,若是這個值一開始就賦值0表示從採集的第一個幀就要顯示編碼,此時就須要經過lastStreamTime對firstFrameTimestamp賦值,這就要求lastStreamTime不能爲0,因此在函數OBS::MainCaptureLoop中的while循環進行判斷並保證該變量不爲0,根據前面正常視頻採集編碼運行過程,即QWORD frameDelta = curStreamTime-lastStreamTime;語句執行後frameDelta爲幀間隔大小,因此這裏用lastStreamTime = curStreamTime-frameLengthNS;來賦值。編碼
在函數OBS::MainCaptureLoop中對firstFrameTimestamp變量只賦值一次firstFrameTimestamp = lastStreamTime/1000000;,這個地方沒有用curStreamTime變量來賦值,而是用lastStreamTime變量,由前面的分析,這兩個變量的差值理想狀態爲一個幀間隔,一個是表明當前流編碼時間,一個表明上一次的流編碼時間,這個地方用lastStreamTime主要是爲了音視頻的同步,根據註釋"//audio sometimes takes a bit to start -- do not start processing frames until audio has started capturing",意思就是聲音的採集會花費一點點時間,只有當聲音開始採集時纔開始處理視頻幀,根據if條件分支只有當探測到聲音採集了纔去初始化變量firstFrameTimestamp。若是此時用curStreamTime變量來賦值就會落後於音頻,由於另外的聲音採集線程已經採集到了聲音,因此用上一個流編碼時間來做爲第一個視頻幀的編碼時間戳更加合適,這樣的話音視頻的時間戳的差值更小。spa
函數OBS::MainCaptureLoop中的curStreamTime其實就是當前採集的視頻幀的編碼時間戳,這個能夠假定一種理想狀況,在編碼線程處理函數OBS::EncodeLoop的while循環中,當執行完第一個SleepToNS函數sleep到半個幀間隔後調用SetEvent(hVideoEvent);喚醒視頻採集線程,而後採集線程能夠執行同時編碼線程繼續第二個SleepToNS函數sleep到整時間點,若是在後半個幀間隔中完成了視頻採集,則編碼線程就能夠編碼發送幀,在編碼線程函數中調用DWORD curFrameTimestamp = DWORD(bufferedTimes[0] - firstFrameTimestamp);來做爲當前幀的編碼時間戳。這樣若是編碼線程往bufferedTimes這個CircularList<UINT>類型的變量中插入latestVideoTime,而後若是視頻採集線程中可以採集到與latestVideoTime(與該變量對應的latestVideoTimeNS被賦值給了視頻採集線程中的QWORD curStreamTime = latestVideoTimeNS;)對應的幀,則就造成了前面所說的,即函數OBS::MainCaptureLoop中的curStreamTime其實就是當前採集的視頻幀的編碼時間戳。可是視頻採集線程中當採集的第一幀直接忽略,第一個編碼直接返回,若是在編碼線程處理函數OBS::EncodeLoop內的DWORD curFrameTimestamp = DWORD(bufferedTimes[0] - firstFrameTimestamp);語句處打上斷點,調試執行到這裏時,bufferedTimes這個CircularList<UINT>類型的變量大大小爲4,這是由於在編碼線程函數中對應firstFrameTimestamp的latestVideoTime插入bufferedTimes的時候,尚未探測到聲音採集,而後下一個latestVideoTime時,探測到聲音採集可是第一幀直接忽略,而後下一個latestVideoTime時第一個編碼直接返回,直到再下一個latestVideoTime時才採集到數據幀即curFramePic指針不空,而後編碼線程的while循環中的(curFramePic && firstFrameTimestamp)條件成立,而後首先就是將bufferedTimes這個循環鏈表中小於firstFrameTimestamp的全清掉,由上面分析因此調試到這裏時這個鏈表大小爲4,而後就開始依次從bufferedTimes彈出第一個元素做爲當前待編碼發送的幀的編碼時間戳。插件
關於編碼線程處理函數OBS::EncodeLoop的while循環內兩次調用SleepToNS所傳遞的參數(sleepTargetTime += (frameTimeNS/2)),每次加半個幀間隔,這樣兩次加起來就是一個完整的幀間隔,平均分紅兩個半幀間隔也是個經驗值,若是前面分得的時間過長,致使在喚醒採集線程以後只有不到半個幀間隔,可能會致使尚未採集到視頻就須要編碼了;若是前面分析的時間太短,後面分析的時間過長則可能致使在編碼發送函數ProcessFrame沒有足夠的處理時間,這樣在第一次調用SleepToNS的時候就會致使no_sleep_counter累加,雖而後面可能會將該變量清零,可是這樣的抖動老是不夠平穩的。
當須要編碼發送視頻幀時,編碼線程處理函數OBS::EncodeLoop的while循環調用OBS::ProcessFrame函數,在該函數中首先進行編碼,而後調用BufferVideoData函數,註釋爲"//buffer video data before sending out",該函數其實是將剛剛編碼的數據放到緩存中,而後將編碼緩存中第一個編碼數據的時間戳與音頻緩存中的音頻幀時間戳對比肯定當前是否須要發送編碼視頻緩存中的第一個編碼數據。若是須要發送則將第一個視頻編碼數據返回到BufferVideoData函數的最後一個參數,該參數爲引用類型,OBS::ProcessFrame函數中根據該函數的返回值發送視頻編碼數據。因此在發送視頻編碼數據時候就是在這裏根據與音頻緩存中的編碼數據的時間戳對比來進行音視頻同步的。
程序在運行的時候主界面上的標識聲音採集和播放的進度條一直閃動,這個就是OBSCapture.cpp文件中OBS::MainAudioLoop函數內循環不斷的往這兩個控件上PostMessage消息,註釋爲"update the meter about every 50ms"。這兩個控件的id跟mfc對話框上的控件id同樣,只不過這裏是預先定義後在調用CreateWindow的時候傳遞進去進行標記,這個使用方式跟peerconnection中的相似。
在OBS的音頻線程函數OBS::MainAudioLoop中調用了AvSetMmThreadCharacteristics和AvRevertMmThreadCharacteristics,根據名字可知是設置和回覆Mm線程特性的函數,在webrtc的audio_device工程的audio_device_core_win.cc函數中也調用了這些函數及AvSetMmThreadPriority函數,註釋爲"Use Multimedia Class Scheduler Service (MMCSS) to boost the thread priority"。在webrtc中先調用LoadLibrary來加載Avrt.dll,而後經過GetProcAddress來獲取這些函數的入口地址,經過函數指針對象來調用這些函數,Avrt.dll則是一個windows的dll,其描述爲"Multimedia Realtime Runtime"。根據webrtc中的描述可知這些函數就是爲了修改音頻線程的線程優先級。
在OBS的音頻線程函數OBS::MainAudioLoop的while循環中,當調用QueryNewAudio返回真(根據名字應該是偵測到新的音頻數據),總共分三個地方調用AudioSource::GetBuffer和AudioSource::GetNewestFrame函數,其中desktopAudio是在OBS::Start函數中賦值desktopAudio=CreateAudioSource(false, strPlaybackDevice);,根據參數名字這個應該表明的是播放設備即揚聲器,而OBS的成員變量List<AudioSource*> auxAudioSources;則表明了場景中視頻文件的聲音源,若是使用了VideoSourcePlugin插件且場景中添加了不一樣的視頻文件,則在依次播放這些視頻文件時會不斷地添加和移除AudioSource*項到OBS的成員變量auxAudioSources中,同一個場景中對應的auxAudioSources某一時刻只有一個AudioSource*項,另一個micAudio也是在OBS::Start函數中賦值則表明了音頻採集源。其中調用AudioSource::GetBuffer是爲了獲取音頻幀,須要調用MixAudio函數對這些音頻數據進行混合,這些音頻數據包括了揚聲器播放數據、附加的視頻文件中的音頻數據及麥克風採集的音頻數據。最後調用OBS::EncodeAudioSegment函數將這些混合後的音頻數據編碼並添加到OBS的成員變量List<FrameAudio> pendingAudioFrames;中等待發送。
而調用AudioSource::GetNewestFrame函數根據名字應該是獲取最後一幀數據,而後根據最後一幀數據調用CalculateVolumeLevels函數,查看代碼可知該函數獲取音頻幀的均方根和採樣最大值,調用toDB函數將這些數據進行轉換(根據字面意思應該是轉換爲分貝單位),而後根據這兩個值能夠計算OBS成員變量desktopMax, micMax;、desktopPeak, micPeak;、desktopMag, micMag;這些成員變量在OBS::UpdateAudioMeters函數中用到,用來響應調用PostMessage函數所發送的WM_COMMAND消息,去更新窗口上的表明揚聲器和麥克風音量的控件。當調用CalculateVolumeLevels函數獲取麥克風的音量時只須要調用micAudio->GetNewestFrame便可,因此不須要調用MixAudio進行混合,而獲取揚聲器的音量所用的音頻幀則須要將desktopAudio->GetNewestFrame獲取到的音頻幀及auxAudioSources[i]->GetNewestFrame獲取到的音頻幀進行混合,因此須要調用MixAudio函數進行混合。注意在調用CalculateVolumeLevels(該函數的註釋爲"multiply samples by volume and compute RMS and max of samples")獲取均方根和採樣最大值時,揚聲器和麥克風的第三個參數有所不一樣,註釋爲"Use 1.0f instead of curDesktopVol, since aux audio sources already have their volume set, and shouldn't be boosted anyway"。
在OBS的主界面中包含了兩個標識音量的圖標控件,這兩個控件實際上是在OBS的構造函數中建立的,這兩個控件的窗口類名稱爲VOLUME_CONTROL_CLASS,具體是在OBSApi\VolumeControl.cpp文件中定義的。一樣的在每一個音量控件下面還有一個音量度量的控件即VOLUME_METER_CLASS,具體是在OBSApi\VolumeMeter.cpp文件中定義的。在OBS類中定義float類型成員變量desktopVol, micVol,以desktopVol爲例共有三處來修改該值,在OBS::Start函數中經過.ini這個config文件來讀取該值,另外兩處修改該值一處是在OBSHotkeyHandlers.cpp文件中的OBS::MuteDesktopHotkey函數,根據函數名應該經過快捷鍵來修改;另外一處則是WindowStuff.cpp文件的OBS::OBSProc函數內,即主窗口的窗口過程函數,在該窗口過程函數中對前面所述的標識音量的控件ID_DESKTOPVOLUME的響應,當用鼠標來調整音量大小時進行響應更新desktopVol值。一樣的micVol這個表明了麥克風音量的也有三處來修改該值。
主界面中對應VolumeControl的兩個音量控件表明了當前揚聲器和麥克風的音量,程序運行過程當中保持不變除非鼠標調整大小,而音量控件下面的則是對應VolumeMeter的兩個控件,表明了當前揚聲器和麥克風的當前音量大小,是動態變化的。在WindowStuff.cpp文件的OBS::OBSProc函數內即主窗口的窗口過程函數,在該函數中經過WM_COMMAND消息對前面所述的標識音量的控件ID_DESKTOPVOLUME的響應,其中根據VOLN_ADJUSTING和VOLN_FINALVALUE來判斷是否須要寫入ini文件保存,根據字面意思一個是音量調整中,一個則是調整後的音量。由於當鼠標按下不鬆手而在對應VolumeControl的音量控件上滑動時,則是VOLN_ADJUSTING,當鼠標左鍵彈起或者按下靜音則對應的是VOLN_FINALVALUE。在主窗口的窗口過程函數OBS::OBSProc中響應VOLN_FINALVALUE過程爲:因爲該WM_COMMAND消息是在OBSApi\VolumeControl.cpp文件中發送的,因此在WindowStuff.cpp文件內的OBS::OBSProc函數中先根據LOWORD(wParam)判斷到ID_DESKTOPVOLUME,而後再根據HIWORD(wParam)是否爲VOLN_ADJUSTING或VOLN_FINALVALUE進行相應的處理,lParam則表明的是這個控件的句柄。在OBSApi\VolumeControl.cpp文件當調用SendMessage發送WM_COMMAND消息時,將控件句柄做爲lParam參數,而後經過句柄傳遞參數GWLP_ID調用GetWindowLongPtr函數來獲得窗口的標識符,而後將其和經過VOLN_ADJUSTING或VOLN_FINALVALUEMAKEWPARAM宏構形成WPARAM參數。傳遞參數GWLP_ID調用GetWindowLongPtr函數來獲得窗口的標識符也即普通的對話框程序拖動控件後在控件的屬性頁中的ID屬性,這裏的兩個音量控件是在OBS的構造函數中經過調用CreateWindow傳遞的,在調用CreateWindow時傳遞id參數。而對應VolumeControl的控件的窗口過程函數在OBSApi\VolumeControl.cpp文件的VolumeControlProc中定義,在OBS的Source\API.cpp文件中會調用PostMessage函數發送WM_COMMAND消息,這裏在構造WPARAM參數時直接就用的ID值,並且經過id來獲取窗口句柄,而OBSApi\VolumeControl.cpp文件中則相反根據句柄獲取id值。查了下msdn,GetWindowLongPtr函數替代了GetWindowLong函數,當調用這兩個函數獲取窗口對應的id時傳遞的參數對應是GWLP_ID和GWL_ID。註釋爲"If you are retrieving a pointer or a handle, GetWindowLong has been superseded by the GetWindowLongPtr function. (Pointers and handles are 32 bits on 32-bit Windows and 64 bits on 64-bit Windows.) To write code that is compatible with both 32-bit and 64-bit versions of Windows, use GetWindowLongPtr"。
OBS中的VideoSourcePlugin插件,在VideoSource.cpp文件中定義了VideoSource類,該類包含AudioOutputStreamHandler成員變量,而AudioOutputStreamHandler類則封裝了VideoAudioSource類型變量,在該類中設置vlc的回調當有聲音播放時經過VideoAudioSource類來存入到緩衝區。VideoSource.cpp文件中在VideoSource的構造函數中調用其成員函數VideoSource::UpdateSettings,該函數中建立AudioOutputStreamHandler類變量並調用該類的成員函數SetOutputParameters來初始化其成員變量isAudioOutputToStream,這個變量跟視頻設置窗口的單選按鈕關聯,肯定是否採集vlc播放視頻的聲音到VideoAudioSource類中。
類VideoAudioSource繼承自OBSApi\AudioSource.h中定義的AudioSource,與OBS工程中MMDeviceAudioSource.cpp文件內定義的MMDeviceAudioSource相似,也繼承並實現了基類中的虛函數GetNextBuffer。當vlc播放視頻時調用VideoAudioSource::PushAudio函數將音頻存入到VideoAudioSource類的成員變量List<BYTE> sampleBuffer;中,而後VideoAudioSource::GetNextBuffer函數從該變量中讀取音頻幀並設置音頻幀的時間戳。在設置vlc音頻的時間戳時先參考OBS類內定義的latestAudioTime變量,若是須要修改以該時間戳爲準,修改vlc內部時鐘(即流媒體的pts),而後若是pts大於當前設置的vlc的時間戳則修改時間戳爲pts大小,這樣指明該vlc音頻幀不要太超前render。
在OBSCapture.cpp文件的音頻循環函數OBS::MainAudioLoop中經過while循環不斷地調用QueryNewAudio函數來查詢是否有新的音頻幀須要處理,在不少開源工程中音頻的處理都是以10ms爲單位。在OBS::QueryNewAudio函數中經過調用AudioSource::QueryAudio2來進行新的音頻幀的探測查詢。在AudioSource::QueryAudio2函數中經過調用其成員函數虛函數GetNextBuffer獲取音頻幀的數據和時間戳,若是是播放的vlc文件則調用的是VideoAudioSource::GetNextBuffer函數,若是是播放和採集則調用的是MMDeviceAudioSource::GetNextBuffer函數。
在MMDeviceAudioSource::GetNextBuffer函數中,經過調用IAudioCaptureClient::GetBuffer來獲取到音頻幀數據和時間戳(即局部變量qpcTimestamp),並將其保存到MMDeviceAudioSource類的成員變量List<float> inputBuffer;中,這樣在調用MMDeviceAudioSource::GetNextBuffer函數時從該緩存中讀取音頻幀數據;並將時間戳來賦值MMDeviceAudioSource類的成員變量QWORD lastQPCTimestamp;,這樣當調用MMDeviceAudioSource::GetNextBuffer函數時,若是有足夠的數據就會根據局部變量 bool bFirstRun = true;來判斷成員變量lastQPCTimestamp是否+10ms。其實這個意思是若是數據充足不須要等待調用IAudioCaptureClient::GetBuffer來獲取音頻幀就以上一次的時間戳lastQPCTimestamp再加10毫秒做爲參數調用MMDeviceAudioSource::GetTimestamp函數,若是沒有充足的數據就以調用IAudioCaptureClient::GetBuffer獲得的時間戳修正好賦值給MMDeviceAudioSource類的成員變量QWORD lastQPCTimestamp;做爲參數調用MMDeviceAudioSource::GetTimestamp函數。
函數MMDeviceAudioSource::GetTimestamp根據類的成員變量lastQPCTimestamp來獲取時間戳並保存到類成員變量QWORD firstTimestamp;中,該成員變量做爲當前音頻幀的時間戳。在MMDeviceAudioSource::GetTimestamp函數中會根據音頻的採集和播放分別處理。若是是獲取麥克風採集音頻的音頻時間戳,則判斷若是配置文件中UseMicQPC爲真則直接用成員變量lastQPCTimestamp這個變量,不然用App->GetAudioTime()函數返回值即OBS::QueryAudioBuffers函數中賦值的OBS成員變量latestAudioTime。而後將該值加上偏移值後返回賦值給MMDeviceAudioSource類的成員變量firstTimestamp做爲音頻幀的時間戳。若是是播放聲音則判斷是否爲第一幀,若是是第一幀則判斷是否須要以視頻幀時間戳爲參考,若是配置文件中的SyncToVideoTime爲真,或者音頻時間戳太過於超前(即時間戳加上音頻緩衝時間還小於當前時間)、或者音頻時間戳太過於滯後(即音頻時間戳大於當前時間+2000ms,即該音頻幀的render要在當前時間過了2秒後)則以視頻時間戳爲準;若是音頻播放中不是第一幀則先將App->GetVideoTime()保存爲局部變量QWORD newVideoTime,而後將該變量與MMDeviceAudioSource類的成員變量QWORD lastVideoTime;進行對比,由於音頻是10ms做爲一個處理單位,而音頻的話則是1000/幀率,因此有多是在兩個幀之間有不少音頻幀,若是相等的話就僅僅是將成員變量QWORD curVideoTime;+10ms,若是不等則更新類的這兩個成員變量即curVideoTime和lastVideoTime。而後判斷是否須要以視頻時間戳來同步音頻幀時間戳,而後同麥克風採集的音頻幀時間戳同樣作相應的偏移後即返回。
由以上三段分析可知在函數AudioSource::QueryAudio2中經過調用GetNextBuffer後就能夠獲取到音頻幀數據和對應的時間戳,而後若是數據格式不是float則進行處理,若是不是2個通道則進行處理,而後若是須要重採樣則進行相應的處理,而後經過調用AudioSource::AddAudioSegment函數將音頻幀保存到AudioSource類的成員變量List<AudioSegment*> audioSegments;中。
在音頻處理線程函數OBS::MainAudioLoop中會調用OBS::QueryNewAudio函數,若是爲真即探測到音頻幀則從OBS的類成員變量CircularList<QWORD> bufferedAudioTimes;中移出第一個時間戳變量,做爲當前去編碼的音頻幀的時間戳,即調用OBS::EncodeAudioSegment函數編碼以後存入到OBS的成員變量List<FrameAudio> pendingAudioFrames;中等待發送到服務端。如今以OBS類的成員變量bufferedAudioTimes爲線索,音頻線程處理函數OBS::MainAudioLoop中是取出,而OBS::QueryAudioBuffers則是寫入時間戳到緩衝中,而調用該函數的函數則是OBS::QueryNewAudio,其實應用程序中是以desktopAudio這個AudioSource來做爲音頻幀時間戳的參考來源的,desktopAudio表明的也就是音頻的揚聲器播放。
在OBS::QueryNewAudio函數的while循環中,以desktopAudio來做爲參考,若是音頻的緩衝超過了配置文件中設置的SceneBufferingTime值,默認爲700也就是說可以最多緩衝70個音頻幀,若是AudioSource類的成員變量List<AudioSegment*> audioSegments;中保存了多於70個音頻幀則會超過應用程序設置的bufferingTime。若是超過了這個值的1.5倍則直接將局部變量bAudioBufferFilled置爲true挑用執行QueryAudioBuffers(false);後跳出循環而後判斷出存在新的音頻幀執行OBS::MainAudioLoop函數中的音頻混音編碼。
在OBS::QueryNewAudio函數中若是判斷出沒有超過音頻緩衝的1.5倍則執行AudioSource::QueryAudio2函數,即獲取音頻幀並保存到AudioSource的List<AudioSegment*> audioSegments;中,即AudioSource::QueryAudio2函數先調用類的虛函數GetNextBuffer,獲取到音頻幀後調用類的函數AddAudioSegment將其保存到成員變量audioSegments;中。若是desktopAudio->QueryAudio2返回AudioAvailable即獲取到desktopAudio的音頻幀則調用QueryAudioBuffers(true);函數,該函數中作兩件事,一是先將latestAudioTime這個OBS的成員變量存到OBS的成員變量bufferedAudioTimes中,這樣在音頻處理線程函數中就能夠不斷的移出第一個時間戳進行音頻編碼並保存到成員變量pendingAudioFrames中等待發送到服務端。第二件事是調用輔助音頻源(如vlc的音頻)及麥克風的QueryAudio2函數將這些音頻源的音頻數據保存到AudioSource的List<AudioSegment*> audioSegments;中,這樣當在音頻處理線程函數混音時就能夠取到對應的音頻幀。
再回到OBS::QueryNewAudio函數中,當執行完desktopAudio->QueryAudio2後須要判斷此時desktopAudio的緩衝是否滿即bAudioBufferFilled是否爲真,由於該值是須要做爲OBS::QueryNewAudio函數的返回值,音頻處理線程函數根據此值來肯定是否混音編碼,因此在OBS::QueryNewAudio函數的while循環的最後部分,若是沒有獲取到desktopAudio的音頻幀(即bGotAudio爲false)且desktopAudio的緩衝滿(bAudioBufferFilled爲true)則須要調用QueryAudioBuffers(false);函數,由於經過該函數能夠獲取輔助音頻源及麥克風的音頻幀,由於bAudioBufferFilled爲true即須要混音編碼,而bGotAudio爲false則前面並無執行QueryAudioBuffers(true);因此這裏須要執行QueryAudioBuffers(false);,不然後面混音編碼中得不到輔助音頻源及麥克風的音頻幀,也就不能正常混音了。
在OBS::QueryNewAudio函數的while循環的最後,若是bAudioBufferFilled爲true則表示desktopAudio的緩衝爲滿就不須要再while循環了,表示有數據能夠進行混音編碼因此就跳出循環;或者是沒有探測到desktopAudio的音頻幀(即bGotAudio爲false),此時也無需再等直接跳出由於音頻處理線程中有不少事情須要處理並且下一次循環中又會執行到OBS::QueryNewAudio函數的while循環這裏,因此能夠直接跳出循環。而後若是此時bAudioBufferFilled爲false,則須要去調用輔助音頻源和麥克風的AudioSource::QueryAudio2函數,由於有一種狀況是處理速度剛好保證desktopAudio這個音頻源緩衝的音頻不大於配置文件中設置的緩衝值,此時也須要獲取輔助音頻源和麥克風的音頻以等待desktopAudio的下一幀音頻從而進行混音。
在OBS::QueryNewAudio的while循環中,當調用desktopAudio->QueryAudio2獲取到音頻幀,此時繼續執行判斷出bAudioBufferFilled並不爲true,即desktopAudio中存入的音頻幀尚未達到應用程序設置的音頻緩衝,則OBS::QueryNewAudio的while循環就會繼續執行。
在OBS::QueryAudioBuffers函數中,若是調用速度過快則會出現往OBS類的成員變量CircularList<QWORD> bufferedAudioTimes;中寫入不少音頻幀時間戳,這些時間戳可能小於10ms,這固然是不合理的,因此就直接返回false。由於有一種狀況音頻數據的採集過快,可是渲染的時候也是須要正常速度播放渲染的。