经常我们会在流媒体推送端提到“数据回调”这个词,在多媒体编程中,我们会比较常用到线程数据回调,在EasyClient管理类代码中用到了两个数据回调函数,分别是DShow原始音视频数据采集回调函数和EasyRTSPClient网络接收线程中回调音视频编码数据回调函数;虽然两者采集到的数据不同,但是我们的用途是一致的,都是用来推送,所以我们通常会用一个数据回调管理函数来进行统一管理。
int CSourceManager::SourceManager(int _channelId, int *_channelPtr, int _frameType, char *pBuf, RTSP_FRAME_INFO* _frameInfo)
一、DirectShow采集库中的回调
DirectShow采集库中的回调机制在我的另一篇文章EasyDarwin EasyClient中DirectShow采集音视频流程及几种采集方式介绍中第三点提到过,两种模式都是通过统一的设置回调函数接口函数实现:
virtual void WINAPI SetDShowCaptureCallback(RealDataCallback realDataCalBack, void* pMaster) = 0;
回调函数的设置函数通常都带有一个设置参数,该设置参数通常是一个指针变量,主要用于在回调函数体中进行调用控制;最常用的做法是:将其设置为当前类的实例指针this,通过该指针调用不同的实例类的处理函数对回调数据进行处理。
二、libEasyPlayer库中的回调
libEasyPlayer库提供的设置回调函数的接口主要来自其所依赖的库EasyRTSPClient,该回调函数主要是回调网络接收的Rtsp流解析的音视频编码流数据,用于转发或者解码播放;由于libEasyPlayer库(及其依赖库)均不是本人的作品(libEasyPlayer库及其依赖库的作者是EasyDarwin团队的Gavin大神,向大神致敬~~~!!!),所以,我对这个库也只有大致的了解,如果有理解不对或者不合理的地方,欢迎指正,大家相互学习!
1、网络Rtsp流回调
流回调函数在EasyClient中提供了设置接口函数,底层用libEasyPlayer提供的接口函数中进行设置,对应EasyRTSPClient库提供的接口函数进行设置,三者对应的程序代码如下:
EasyClient中的回调设置函数接口:
int EasyPlayerManager::Start(char* szURL, HWND hShowWnd,
RENDER_FORMAT eRenderFormat, int rtpovertcp, const char *username, const char *password, MediaSourceCallBack callback, void *userPtr)
{
//Stop
if (m_sSourceInfo.rtspSourceId > 0)
{
Close();
return -1;
}
m_sSourceInfo.rtspSourceId = EasyPlayer_OpenStream(szURL, hShowWnd, eRenderFormat, rtpovertcp, username, password, callback, userPtr);
return m_sSourceInfo.rtspSourceId ;
}
libEasyPlayer中主要的功能在类CChannelManager中实现,该类提供了回调设置函数接口:
int CChannelManager::OpenStream(const char *url, HWND hWnd, RENDER_FORMAT renderFormat, int _rtpovertcp, const char *username, const char *password, MediaSourceCallBack callback, void *userPtr)
{
if (NULL == pRealtimePlayThread) return -1;
if ( (NULL == url) || (0==strcmp(url, "\0"))) return -1;
int iNvsIdx = -1;
EnterCriticalSection(&crit);
do
{
for (int i=0; iif (NULL == pRealtimePlayThread[i].nvsHandle)
{
iNvsIdx = i;
break;
}
}
if (iNvsIdx == -1) break;
EasyRTSP_Init(&pRealtimePlayThread[iNvsIdx].nvsHandle);
if (NULL == pRealtimePlayThread[iNvsIdx].nvsHandle) break; //退出while循环
unsigned int mediaType = MEDIA_TYPE_VIDEO;
mediaType |= MEDIA_TYPE_AUDIO; //换为NVSource, 屏蔽声音
EasyRTSP_SetCallback(pRealtimePlayThread[iNvsIdx].nvsHandle, __RTSPSourceCallBack);
EasyRTSP_OpenStream(pRealtimePlayThread[iNvsIdx].nvsHandle, iNvsIdx, (char*)url, _rtpovertcp==0x01?RTP_OVER_TCP:RTP_OVER_UDP, mediaType, (char*)username, (char*)password, (int*)&pRealtimePlayThread[iNvsIdx], 1000, 0);
pRealtimePlayThread[iNvsIdx].pCallback = callback;
pRealtimePlayThread[iNvsIdx].pUserPtr = userPtr;
pRealtimePlayThread[iNvsIdx].hWnd = hWnd;
pRealtimePlayThread[iNvsIdx].renderFormat = (D3D_SUPPORT_FORMAT)renderFormat;
CreatePlayThread(&pRealtimePlayThread[iNvsIdx]);
}while (0);
LeaveCriticalSection(&crit);
if (iNvsIdx >= 0) iNvsIdx += CHANNEL_ID_GAIN;
return iNvsIdx;
}
其中,调用的EasyRTSPClient库函数接口EasyRTSP_SetCallback(pRealtimePlayThread[iNvsIdx].nvsHandle, __RTSPSourceCallBack);实现网络流编码数据的回调函数的设置;在以上代码中,作者除了初始化EasyRTSPClient库的操作以及设置回调函数外,还创建了两个线程,分别用于解码和播放;
void CChannelManager::CreatePlayThread(PLAY_THREAD_OBJ *_pPlayThread)
{
if (NULL == _pPlayThread) return;
if (_pPlayThread->decodeThread.flag == 0x00)
{
//_pPlayThread->ch_tally = 0;
_pPlayThread->decodeThread.flag = 0x01;
_pPlayThread->decodeThread.hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)_lpDecodeThread, _pPlayThread, 0, NULL);
while (_pPlayThread->decodeThread.flag!=0x02 && _pPlayThread->decodeThread.flag!=0x00) {Sleep(100);}
if (NULL != _pPlayThread->decodeThread.hThread)
{
SetThreadPriority(_pPlayThread->decodeThread.hThread, THREAD_PRIORITY_HIGHEST);
}
}
if (_pPlayThread->displayThread.flag == 0x00)
{
_pPlayThread->displayThread.flag = 0x01;
_pPlayThread->displayThread.hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)_lpDisplayThread, _pPlayThread, 0, NULL);
while (_pPlayThread->displayThread.flag!=0x02 && _pPlayThread->displayThread.flag!=0x00) {Sleep(100);}
if (NULL != _pPlayThread->displayThread.hThread)
{
SetThreadPriority(_pPlayThread->displayThread.hThread, THREAD_PRIORITY_HIGHEST);
}
}
}
需要重要说明的是,在类CChannelManager中一个重要的成员结构体指针变量pRealtimePlayThread,该变量是贯穿整个程序流程,涉及到网络流数据接收线程->编码数据回调||缓存->写文件||解码线程->播放线程 整个流程,在其中扮演了重要的角色,结构体原型如下:
typedef struct __PLAY_THREAD_OBJ
{
THREAD_OBJ decodeThread; //解码线程
THREAD_OBJ displayThread; //显示线程
Easy_RTSP_Handle nvsHandle;
HWND hWnd; //显示视频的窗口句柄
int channelId; //通道号
int showStatisticalInfo;//显示统计信息
int frameCache; //帧缓存(用于调整流畅度),由上层应用设置
int initQueue; //初始化队列标识
SS_QUEUE_OBJ_T *pAVQueue; //接收rtsp的帧队列
int frameQueue; //队列中的帧数
int findKeyframe; //是否需要查找关键帧标识
int decodeYuvIdx;
DWORD dwLosspacketTime; //丢包时间
DWORD dwDisconnectTime; //断线时间
DECODER_OBJ decoderObj[MAX_DECODER_NUM];
D3D_HANDLE d3dHandle; //显示句柄
D3D_SUPPORT_FORMAT renderFormat; //显示格式
int ShownToScale; //按比例显示
int decodeKeyFrameOnly; //仅解码显示关键帧
unsigned int rtpTimestamp;
LARGE_INTEGER cpuFreq; //cpu频率
_LARGE_INTEGER lastRenderTime; //最后显示时间
int yuvFrameNo; //当前显示的yuv帧号
YUV_FRAME_INFO yuvFrame[MAX_YUV_FRAME_NUM];
CRITICAL_SECTION crit;
bool resetD3d; //是否需要重建d3dRender
RECT rcSrcRender;
D3D9_LINE d3d9Line;
char manuRecordingFile[MAX_PATH];
int manuRecording;
MP4C_Handler mp4cHandle;
int vidFrameNum;
MediaSourceCallBack pCallback;
void *pUserPtr;
}PLAY_THREAD_OBJ;
其中,编码数据和解码数据分别缓存在队列结构pAVQueue和数组yuvFrame中,程序中用这个结构做了2级缓存,保证接收和解码播放过程的流畅性,其中,解码数据缓存只有3帧,确保播放的实时性,当然在机器性能或者网络资源不够的情况下可能出现卡帧或者花屏的情况,当然,程序中采用了先进的丢帧机制,确保花屏的情况最大限度的减少。
此外,pRealtimePlayThread这个结构指针,创建的是一个最大64的数组,也就是相当于的64个CChannelManager类的实例的Player对应的处理(当然,类实例只有一个CChannelManager *g_pChannelManager);对于libEasyPlaer库的这种处理方式可能部分程序员不太理解(包括我~~~哈哈哈),但是其中确实有很多处理精妙的地方值得我们去学习,所以,要读懂libEasyPlaer库中的代码,熟悉和理解pRealtimePlayThread这个变量的处理妙用即可!