关于OBS中视频的采集和编码,在OBSVideoCapture.cpp文件中包含两个线程函数EncodeThread和MainCaptureThread,分别调用函数EncodeLoop和MainCaptureLoop。通过事件变量HANDLE hVideoEvent;来同步采集和编码视频。具体就是在OBS::Start函数中创建这两个线程,然后采集视频线程中通过while循环WaitForSingleObject(hVideoEvent, INFINITE)来等待事件hVideoEvent的状态,然后在编码线程中根据编码速度来调用SetEvent(hVideoEvent);唤醒采集视频。
在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值的变化。
在编码线程处理函数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变量清零,说明虽然编码发送视频帧大于半个帧间隔时间,但是还是在一个帧间隔时间内完成,说明编码线程不算慢不至于导致达不到帧率的设置。
基于以上分析,在编码线程处理函数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)进行编码发送处理。
对于一个正常的视频采集编码运行过程,当程序运行到函数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变量来赋值就会落后于音频,因为另外的声音采集线程已经采集到了声音,所以用上一个流编码时间来作为第一个视频帧的编码时间戳更加合适,这样的话音视频的时间戳的差值更小。
函数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。因为有一种情况音频数据的采集过快,但是渲染的时候也是需要正常速度播放渲染的。