最近遇到一個問題,使用某個RTSP攝像頭,Android端顯示碼流,在一分鐘左右時畫面會卡死,同時Native層內存瘋漲到幾百兆後崩潰。
查看崩潰時的調用棧,定位到如下函數,是創建BufferedPacket時內存不夠了拋的異常。
BufferedPacket* BufferedPacketFactory::createNewPacket(MultiFramedRTPSource*) {
return new BufferedPacket;
}
BufferedPacket* ReorderingPacketBuffer::getFreePacket(MultiFramedRTPSource* ourSource) {
if (fSavedPacket == NULL) {
fSavedPacket = fPacketFactory->createNewPacket(ourSource);
fSavedPacketFree = True;
}
if (fSavedPacketFree == True) {
fSavedPacketFree = False;
return fSavedPacket;
} else {
return fPacketFactory->createNewPacket(ourSource);
}
}
於是開始翻看該函數上下文代碼,漸漸理清了流程,本地與服務器創建了兩條連接,一條用於控制,一條用於幀數據傳輸。網絡數據讀進來後先通過live555模塊Demux,再進行Decode,最後渲染。這個鏈條上任一個出了問題都可能導致畫面卡住,而根據profiler查看網絡流量仍保持穩定說明數據傳輸仍在進行,而內存瘋長也印證了這一點,必然是因爲收到數據後寫入了本地的buffer中導致的。因此,我們從整個環節的上游開始查起,即上面提到的Demux模塊。
爲便於分析執行流程,我們在Demux模塊中一些關鍵函數內部打了日誌,在native層打日誌按如下方式,
#include <android/log.h>
__android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__);
中間遇到過一個坑,在MultiFramedRTPSource.cpp中加的日誌一直不生效,原因是VLC的編譯腳本壓根沒有重新編譯這個文件,我們查看vlc/contrib/contrib-android-arm-linux-androideabi/live555下的Makefile,發現所有的.o文件最終會編譯成一個libliveMedia.a文件,但是我們注意到VLC編譯完後這個libliveMedia.a文件時間戳還是舊的,這個模塊沒有被重新編譯。
我們猜想這些vlc的底層模塊在第一次完整編譯後生成的.a文件會被拷到某個固定目錄中,然後整體編譯成so,供上層的libvlc來調用。之後的每次編譯只會查看該固定目錄的.a文件是否有更新,如果我們改了vlc的底層模塊,需要手動編譯這個模塊然後將.a文件拷到該固定目錄中才會生效。
通過在VLC的目錄中查找libliveMedia.a文件,一共發現兩處,除了live555中之外,另一處是vlc/contrib/arm-linux-androideabi/lib中,這應該就是VLC用來拷貝這些編譯好的庫文件的目錄。
後來的實踐驗證了這一點。
我們分析日誌發現畫面正常的時候,流程就是重複的StreamRead和Demux,在手機畫面卡住的時候,會多出來一個函數調用,之後就停止了Demux。這個函數是TimeoutPrevention,
static void TimeoutPrevention( void *p_data )
{
demux_t *p_demux = (demux_t *) p_data;
demux_sys_t *p_sys = p_demux->p_sys;
char *bye = NULL;
/* Protect Live555 from us calling their functions simultaneously
with Demux() or Control() */
vlc_mutex_locker locker(&p_sys->timeout_mutex);
if( p_sys->rtsp == NULL || p_sys->ms == NULL )
return;
bool use_get_param = p_sys->b_get_param;
if( var_GetBool( p_demux, "rtsp-wmserver" ) )
use_get_param = true;
if( use_get_param )
p_sys->rtsp->sendGetParameterCommand( *p_sys->ms,
default_live555_callback, bye );
else
p_sys->rtsp->sendOptionsCommand( default_live555_callback, NULL );
if( !wait_Live555_response( p_demux ) )
{
msg_Err( p_demux, "keep-alive failed: %s",
p_sys->env->getResultMsg() );
/* Just continue, worst case is we get timed out later */
}
}
這個函數是用於keep-alive的,隔一分鐘左右向服務器發一個GetParameter或GetOptions請求,然後等待Response。問題是這個函數會鎖p_sys->timeout_mutex,而Demux也需要獲取這個鎖,如果這裏發出的請求服務器沒有響應,這裏的wait_Live555_response就一直沒有返回,導致鎖一直沒有釋放。所以Demux就無法繼續進行。此處wait_Live555_response應該有超時機制,但是沒生效。接下來看其代碼,
static bool wait_Live555_response( demux_t *p_demux, int i_timeout = 0) {
TaskToken task;
demux_sys_t * p_sys = p_demux->p_sys;
p_sys->event_rtsp = 0;
if( i_timeout > 0 )
{
/* Create a task that will be called if we wait more than timeout ms */
task = p_sys->scheduler->scheduleDelayedTask( i_timeout*1000,
TaskInterruptRTSP,
p_demux );
}
p_sys->event_rtsp = 0;
p_sys->b_error = true;
p_sys->i_live555_ret = 0;
p_sys->scheduler->doEventLoop( &p_sys->event_rtsp );
//here, if b_error is true and i_live555_ret = 0 we didn't receive a response
if( i_timeout > 0 )
{
/* remove the task */
p_sys->scheduler->unscheduleDelayedTask( task );
}
return !p_sys->b_error;
}
注意到這裏沒傳i_timeout所以默認是0的,於是沒有開啓scheduleDelayedTask,這個是超時控制,
static void TaskInterruptRTSP( void *p_private ) {
demux_t *p_demux = (demux_t*)p_private;
/* Avoid lock */
p_demux->p_sys->event_rtsp = 0xff;
}
只是簡單的設置event_rtsp爲0xff,這個標誌位很重要,就是wait_Live555_response中調用doEventLoop時傳入的。而doEventLoop會在while循環中不斷檢測這個標誌位是否非0,只有非0纔會break,否則不斷調SingleStep。
void BasicTaskScheduler0::doEventLoop(char volatile* watchVariable) {
// Repeatedly loop, handling readble sockets and timed events:
while (1) {
if (watchVariable != NULL && *watchVariable != 0) break;
SingleStep();
}
}
SingleStep中不斷select可讀寫的fd,然後執行相應的回調,包括控制流和幀數據流的回調。控制流回調爲incomingDataHandler,檢測的是event_rtsp,數據流回調爲networkReadHandler,檢測的是event_data。此處TimeoutPrevention走的是控制流回調:
void RTSPClient::incomingDataHandler(void* instance, int /*mask*/) {
RTSPClient* client = (RTSPClient*)instance;
client->incomingDataHandler1();
}
void RTSPClient::incomingDataHandler1() {
struct sockaddr_in dummy; // 'from' address - not used
int bytesRead = readSocket(envir(), fInputSocketNum, (unsigned char*)&fResponseBuffer[fResponseBytesAlreadySeen], fResponseBufferBytesLeft, dummy);
handleResponseBytes(bytesRead);
}
這裏的handleResponseBytes是處理server端回覆的數據,會回調到default_live555_callback,我們注意到其中也會設置event_rtsp爲非0。
static void default_live555_callback( RTSPClient* client, int result_code, char* result_string )
{
RTSPClientVlc *client_vlc = static_cast<RTSPClientVlc *> ( client );
demux_sys_t *p_sys = client_vlc->p_sys;
delete []result_string;
p_sys->i_live555_ret = result_code;
p_sys->b_error = p_sys->i_live555_ret != 0;
p_sys->event_rtsp = 1;
}
因此,當服務器無響應時,event_rtsp會一直爲0,doEventLoop會陷入死循環。
綜上,本地向server端發起請求後,會阻塞在wait_Live555_response中,等待服務器返回或超時。而某些RTSP相機對於本地發起的用於keep-alive的GET_PARAMETER請求沒有任何響應,同時由於未顯式指定超時時間導致超時檢測沒啓動,因此keep-alive請求在持有鎖的情況下長期阻塞,Demux無法獲得鎖而停止工作。不過此處也不能指定超時,超時後RTSP會認爲EOF而斷開,直接將整個TimeoutPrevention注掉即可。