VLC卡死內存溢出解決

最近遇到一個問題,使用某個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注掉即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章