Qt+FFmpeg仿VLC接收RTSP流並播放

2023-12-20 06:00:37

關鍵詞:Qt FFmpeg C++ RTSP RTP VLC 記憶體漏失 摘要認證 花屏 原始碼 UDP

本系列原文地址

下載直接可執行的原始碼,在原文頂部

效果

產生RTSP流

比播放檔案複雜一點是,為了接收RTSP流,我們需要產生RTSP流。簡單搭建一個RTSP推流環境:

EasyDarwin開啟RTSP服務作為RTSP伺服器。

ffmpeg命令列作為使用者端,向EasyDarwin迴圈推播一個視訊檔。

./ffmpeg.exe -re -stream_loop -1 -i test.mp4 -c copy -f rtsp rtsp://127.0.0.1/stream

這樣就可以從EasyDarwin接收RTSP流了。

我們用vlc接收RTSP流看看。

成功接收。

FFmepg接收RTSP流程式碼

FFmpeg接收RTSP流並播放的流程和播放mp4檔案的流程差不多,只不過播放mp4檔案時,檔案作為播放源,而接收RTSP流時,RTSP流作為了播放源:

我們依舊看下流程中的關鍵程式碼:

if (avformat_open_input(&fileFmtCtx, url.toStdString().c_str(), nullptr, nullptr) != 0) {
    qDebug() << "avformat_open_input() failed";
    return;
}

用於開啟一個RTSP地址,跟開啟一個檔案相比,不僅要查詢流資訊,還需要和RTSP伺服器建立連線,讓RTSP伺服器開始推流。

接收上述RTSP流後,我們列印AVFormatContext的相關屬性:

qDebug() << "stream name: " << streamFmtCtx->url;
    qDebug() << "stream iformat: " << streamFmtCtx->iformat->name;
    qDebug() << "stream duration: " << streamFmtCtx->duration << " microseconds";
    qDebug() << "stream bit_rate: " << streamFmtCtx->bit_rate;
/* 
stream name:  rtsp://127.0.0.1/stream
stream iformat:  rtsp
stream duration:  -9223372036854775808  microseconds
stream bit_rate:  0
*/

這次由於是RTSP流,並不能獲取準確的duration。繼續列印流相關的資訊:

qDebug() << "nb_streams:";
    for (unsigned int i = 0; i < streamFmtCtx->nb_streams; i++) {
        AVStream *stream = streamFmtCtx->streams[i];
        qDebug() << "Stream " << i + 1 << ":";
        qDebug() << "  Codec: " << avcodec_get_name(stream->codecpar->codec_id);
        qDebug() << "  Duration: " << stream->duration << " microseconds";
    }
/*
nb_streams:
Stream  1 :
  Codec:  h264
  Duration:  -9223372036854775808  microseconds
Stream  2 :
  Codec:  aac
  Duration:  -9223372036854775808  microseconds
*/

可以看到和上次直接讀取檔案的結果一樣,包括1個H264視訊流和1個AAC音訊流。

swsCtx = sws_getContext(decoderCtx->width, decoderCtx->height, decoderCtx->pix_fmt,
                                     decoderCtx->width, decoderCtx->height, FMT_PIC_SHOW,
                                     SWS_BICUBIC, NULL, NULL, NULL);
qDebug() << "decoderCtx->pix_fmt:" << av_get_pix_fmt_name(decoderCtx->pix_fmt);
//decoderCtx->pix_fmt: yuv420p

sws_getContext()用於將RTSP流格式轉換為將要顯示的格式,這裡是yuv420p=>AV_PIX_FMT_RGB24

int numBytes = av_image_get_buffer_size(FMT_PIC_SHOW, decoderCtx->width, decoderCtx->height, 1);
showBuffer = (unsigned char*)av_malloc(static_cast<unsigned long long>(numBytes) * sizeof(unsigned char));
if(av_image_fill_arrays(showFrame->data, showFrame->linesize,
                        showBuffer, FMT_PIC_SHOW, decoderCtx->width, decoderCtx->height, 1) < 0)
{
    qDebug() << "av_image_fill_arrays() failed";
    return;
}

av_image_get_buffer_size計算了計算影象資料的緩衝區大小。av_malloc分配了1個記憶體塊給showBufferav_image_fill_arrays用影象引數和showBuffer初始化AVFramedatalinesize成員,並且讓AVFrameshowBuffer關聯。

while(av_read_frame(streamFmtCtx, packet) >= 0){
    if(packet->stream_index == nVideoIndex){
        if(avcodec_send_packet(decoderCtx, packet)>=0){
            while((ret = avcodec_receive_frame(decoderCtx, decodedFrame)) >= 0){
                //...
            }
        }
    }
}

和播放mp4檔案類似的解碼步驟,從RTSP流中讀取一個封包AVPacket,將AVPacket送入解碼器進行解碼,嘗試從解碼器中接收已解碼的視訊幀,並將接收到的幀資料儲存在decodedFrame中。

經過上述基本步驟,我們的程式碼已經可以和VLC一樣,從RTSP伺服器接收RTSP流並播放了。

RTSP協定簡述及驗證

FFmpeg內部將RTSP連線建立處理得很好,但我們有必要進一步學習一下RTSP協定。RTSP全稱Real Time Sreaming Protocol,是TCP/IP協定體系中的一個應用層協定。資料傳輸由RTP/RTCP完成,底層通過TCP/UDP實現。

一個標準的RTSP的收流協定層的互動流程如下:

話不多說,我們直接在上面的推流環境下(由於EasyDarwin似乎加密了某些資訊,我們選擇了一個其他的RTSP伺服器,效果是一樣的),用VLC收流,並用wireshark抓包看看協定流程是不是這樣的:

直接看看每條資訊都是什麼:

client => server

Real Time Streaming Protocol
    Request: OPTIONS rtsp://127.0.0.1:554/stream RTSP/1.0\r\n
        Method: OPTIONS
        URL: rtsp://127.0.0.1:554/stream
    CSeq: 2\r\n
    User-Agent: LibVLC/3.0.18 (LIVE555 Streaming Media v2016.11.28)\r\n
    \r\n

client傳送OPTIONSrtsp://127.0.0.1:554/stream詢問server支援哪些RTSP方法。

server=> client

Real Time Streaming Protocol
    Response: RTSP/1.0 200 OK\r\n
        Status: 200
    CSeq: 2\r\n
    Session: 4J_bOCNSg
    Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS, ANNOUNCE, RECORD\r\n
    \r\n

server回覆支援DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS, ANNOUNCE, RECORD

client => server

Real Time Streaming Protocol
    Request: DESCRIBE rtsp://127.0.0.1:554/stream RTSP/1.0\r\n
        Method: DESCRIBE
        URL: rtsp://127.0.0.1:554/stream
    CSeq: 3\r\n
    User-Agent: LibVLC/3.0.18 (LIVE555 Streaming Media v2016.11.28)\r\n
    Accept: application/sdp\r\n
    \r\n

client請求媒體描述檔案,格式為application/sdp

一般server會進行使用者認證,如果未攜帶Authorization鑑權資訊,或者認證失敗,server會返回錯誤號為401的響應,client接收到401響應時,需要根據已知的使用者鑑權資訊,生成Authorization,再次傳送DESCRIBE,如果認證成功,伺服器返回攜帶有SDP的響應資訊。

是否進行認證和RTSP伺服器有關,這裡我們沒有為EasyDarwin設定認證。

server=> client

Real Time Streaming Protocol
    Response: RTSP/1.0 200 OK\r\n
    CSeq: 3\r\n
    Session: _ZLZ7_NSR
    Content-type: application/sdp
    Content-length: 511
    \r\n
    Session Description Protocol
        Session Description Protocol Version (v): 0
        Owner/Creator, Session Id (o): - 0 0 IN IP4 127.0.0.1
        Session Name (s): No Name
        Connection Information (c): IN IP4 127.0.0.1
        Time Description, active time (t): 0 0
        Session Attribute (a): tool:libavformat 58.76.100
        Media Description, name and address (m): video 0 RTP/AVP 96
        Bandwidth Information (b): AS:1894
        Media Attribute (a): rtpmap:96 H264/90000
        Media Attribute (a): fmtp:96 packetization-mode=1; sprop-parameter-sets=Z2QAKqwspQFAFumoCAgKAAADAAIAAAMAYcTAAc/YABW+f4xwEA==,aOkJNSU=; profile-level-id=64002A
        Media Attribute (a): control:streamid=0
        Media Description, name and address (m): audio 0 RTP/AVP 97
        Bandwidth Information (b): AS:317
        Media Attribute (a): rtpmap:97 MPEG4-GENERIC/48000/2
        Media Attribute (a): fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=1190
        Media Attribute (a): control:streamid=1

server返回SDP資訊,告訴client當前有哪些音視訊流和屬性,sdp協定不做展開。這裡我們要關注的比較重要的資訊是:server可以傳送streamid=0H264視訊流和streamid=1AAC音訊流。

client => server

Real Time Streaming Protocol
    Request: SETUP rtsp://127.0.0.1:554/stream/streamid=0 RTSP/1.0\r\n
        Method: SETUP
        URL: rtsp://127.0.0.1:554/stream/streamid=0
    CSeq: 4\r\n
    User-Agent: LibVLC/3.0.18 (LIVE555 Streaming Media v2016.11.28)\r\n
    Transport: RTP/AVP;unicast;client_port=52024-52025
    \r\n

client傳送SETUP告訴server需要建立streamid=0即視訊流的連線,這裡RTP/AVP表示通過UDP傳輸,unicast表示單播,client_port=52024-52025需要單獨解釋一下,前面說到RTSP協定資料傳輸通過RTP+RTCP完成。RTPRTCP都是建立在UDP之上的,RTP預設使用1個偶數埠號,而RTCP則預設使用RTP埠的下1個奇數埠號,就是這裡的52024和52025。

server => client

Real Time Streaming Protocol
    Response: RTSP/1.0 200 OK\r\n
        Status: 200
    CSeq: 4\r\n
    Session: 4J_bOCNSg
    Transport: RTP/AVP;unicast;client_port=52024-52025
    \r\n


server向client返回確認。

client => server

Real Time Streaming Protocol
    Request: SETUP rtsp://127.0.0.1:554/stream/streamid=1 RTSP/1.0\r\n
        Method: SETUP
        URL: rtsp://127.0.0.1:554/stream/streamid=1
    CSeq: 5\r\n
    User-Agent: LibVLC/3.0.18 (LIVE555 Streaming Media v2016.11.28)\r\n
    Transport: RTP/AVP;unicast;client_port=52028-52029
    Session: 4J_bOCNSg
    \r\n

client告訴server需要建立streamid=1的音訊流的連線,RTPRTCP的埠分別在52028和52029。

server => client

Real Time Streaming Protocol
    Response: RTSP/1.0 200 OK\r\n
        Status: 200
    Transport: RTP/AVP;unicast;client_port=52028-52029
    CSeq: 5\r\n
    Session: 4J_bOCNSg
    \r\n

server向client返回確認。

client=>server

Real Time Streaming Protocol
    Request: PLAY rtsp://127.0.0.1:554/stream RTSP/1.0\r\n
        Method: PLAY
        URL: rtsp://127.0.0.1:554/stream
    CSeq: 6\r\n
    User-Agent: LibVLC/3.0.18 (LIVE555 Streaming Media v2016.11.28)\r\n
    Session: 4J_bOCNSg
    Range: npt=0.000-\r\n
    \r\n

client傳送PLAY告訴server開始傳輸,Range代表媒體播放時間,server會根據Range的值播放指定段的資料流,對於實時流,一般只會指定起點,即Range: npt=0.000-

server=>client

Real Time Streaming Protocol
    Response: RTSP/1.0 200 OK\r\n
        Status: 200
    CSeq: 6\r\n
    Session: 4J_bOCNSg
    Range: npt=0.000-\r\n
    \r\n

server返回確認,使用同一Session

client=>server

Real Time Streaming Protocol
    Request: TEARDOWN rtsp://127.0.0.1:554/stream RTSP/1.0\r\n
        Method: TEARDOWN
        URL: rtsp://127.0.0.1:554/stream
    CSeq: 7\r\n
    User-Agent: LibVLC/3.0.18 (LIVE555 Streaming Media v2016.11.28)\r\n
    Session: 4J_bOCNSg
    \r\n

client傳送TEARDOWN發起停止流傳輸請求。

server=>client

Real Time Streaming Protocol
    Response: RTSP/1.0 200 OK\r\n
        Status: 200
    CSeq: 7\r\n
    Session: 4J_bOCNSg
    \r\n

server返回確認,使用同一Session,停止流傳輸。

搭建摘要認證環境

上面說到了server可能會進行使用者認證,那我們現在得創造一個需要認證的環境,直接看看EasyDarwin能不能直接選擇認證,開啟easydarwin.ini

[http]
port=10008
default_username=admin
default_password=admin
#...
;是否使能向伺服器推流或者從伺服器播放時驗證使用者名稱密碼. [注意] 因為伺服器端並不儲存明文密碼,所以推播或者播放時,使用者端應該輸入密碼的md5後的值。
;password should be the hex of md5(original password)
authorization_enable=0
#...

可以看到authorization_enable變數是控制認證的,把它的值改為1,重新啟動服務。這時候發現原來的ffmpeg命令推流不成功了。

那就是說,向EasyDarwin推流的時候,也需要進行認證。從註釋上來看,需要加入使用者名稱和密碼的md5值,我們用正確的引數再推流(下面mad5ofpassword換成你密碼的md5):

./ffmpeg.exe -re -stream_loop -1 -i test.mp4 -c copy -f rtsp rtsp://admin:[email protected]/stream

成功了:

這時候用vlc接收試試,果然要進行認證,要求輸入使用者名稱和密碼:

注意這裡密碼也要輸入md5後的值。輸入正確的密碼後,vlc可以接收RTSP流了:

同樣地,用wireshark抓包看看帶有認證的流程是什麼樣的:

client=>server

Real Time Streaming Protocol
    Request: DESCRIBE rtsp://127.0.0.1:554/stream RTSP/1.0\r\n
        Method: DESCRIBE
        URL: rtsp://127.0.0.1:554/stream
    CSeq: 6\r\n
    User-Agent: LibVLC/3.0.18 (LIVE555 Streaming Media v2016.11.28)\r\n
    Accept: application/sdp\r\n
    \r\n

首先client同樣發起DESCRIBE

server=>client

Real Time Streaming Protocol
    Response: RTSP/1.0 401 Unauthorized\r\n
        Status: 401
    CSeq: 6\r\n
    Session: ayQBojNIg
    WWW-Authenticate: Digest realm="EasyDarwin", nonce="539c6afee35b8edd354e983a6af947bf", algorithm="MD5"\r\n
    \r\n

server返回401,WWW-Authenticate: Digest表示需要摘要認證,realmnonce用於生成responsealgorithm="MD5"表示需要md5演演算法生成response

client=>server

Real Time Streaming Protocol
    Request: DESCRIBE rtsp://127.0.0.1:554/stream RTSP/1.0\r\n
        Method: DESCRIBE
        URL: rtsp://127.0.0.1:554/stream
    CSeq: 7\r\n
    Authorization: Digest username="admin", realm="EasyDarwin", nonce="539c6afee35b8edd354e983a6af947bf", uri="rtsp://127.0.0.1:554/stream", response="d6a48b37f2010b3ddfad1eef18692648"\r\n
    User-Agent: LibVLC/3.0.18 (LIVE555 Streaming Media v2016.11.28)\r\n
    Accept: application/sdp\r\n
    \r\n

client用對應演演算法生成response並返回給server,response的計算方法單獨再講。

server=>client

Real Time Streaming Protocol
    Response: RTSP/1.0 200 OK\r\n
        Status: 200
    Content-length: 511
    CSeq: 7\r\n
    Session: ayQBojNIg
    \r\n
    Data (511 bytes)

server驗證response通過,則返回200。

這裡其實和上面一樣返回了SDP資訊(Data 511 bytes中的資訊),但EasyDarwin是做了加密處理還是什麼,是無法解析出來的。

之後的流程就和沒有摘要認證的過程是一樣的了。

完善程式碼,處理摘要認證

既然可能會存在認證,那我們程式碼中得處理server有認證的情況,否則肯定收不到RTSP流。首先我們定位server的返回在哪裡被捕捉了,經過一番嘗試,發現在方法avformat_open_input中:

if ((ret = avformat_open_input(&streamFmtCtx, url.toStdString().c_str(), nullptr, nullptr)) != 0) {
    qDebug() << "ret:" << ret;
}
//列印輸出
//ret: -825242872
//ffmpeg紀錄檔輸出
//[rtsp @ 000001d2d3940ec0] method DESCRIBE failed: 401 Unauthorized

在需要認證的情況下,avformat_open_input直接返回了一個負數。再結合ffmpeg的紀錄檔,大致可以斷定這是server返回Unauthorized時的情況。但我們需要更具體的確認,所以檢視avformat_open_input的宣告:

//avformat.h
/*
* @return 0 on success, a negative AVERROR on failure.
*/
int avformat_open_input(AVFormatContext **ps, const char *url, ff_const59 AVInputFormat *fmt, AVDictionary **options);

返回值是一個int,註釋中寫到如果是失敗,則返回AVERROR,那麼接下來,我們可以去ffmpeg的原始碼中,找關於AVERROR的內容了。

如果編譯了ffmpeg原始碼,直接debug就可以看到最終是如何返回的,但現在我們不想花額外的時間去編譯原始碼,所以我們用宇宙第一IDE——Visual Studio,開啟ffmpeg的原始碼資料夾,直接搜尋AVERROR,很方便找到了AVERROR的定義:

//error.h
#define AVERROR(e) (-(e))   ///< Returns a negative error code from a POSIX error code, to return from library functions.

可以看到AVERROR是用來取POSIX中標準錯誤相反數的宏,繼續追蹤沒有發現相關返回的地方。但我們在標頭檔案卻看見了Unauthorized的相關定義:

//error.h
#define AVERROR_HTTP_UNAUTHORIZED  FFERRTAG(0xF8,'4','0','1')
#define FFERRTAG(a, b, c, d) (-(int)MKTAG(a, b, c, d))
//common.h
#define MKTAG(a,b,c,d) ((a) | ((b) << 8) | ((c) << 16) | ((unsigned)(d) << 24))

按照定義,AVERROR_HTTP_UNAUTHORIZED實際上是(0xF8,'4','0','1')組合的移位,按照定義計算後AVERROR_HTTP_UNAUTHORIZED確實等於-825242872。為了驗證,我們把宏定義從ffmpeg原始碼中複製出來,直接在我們專案中列印:

//mainwindow.h
#define AVERROR_HTTP_UNAUTHORIZED  FFERRTAG(0xF8,'4','0','1')
#define MKTAG(a, b, c, d) ((a) | ((b) << 8) | ((c) << 16) | ((unsigned)(d) << 24))
#define FFERRTAG(a, b, c, d) (-(int)MKTAG(a, b, c, d))
//mainwindow.cpp
qDebug() << "AVERROR_HTTP_UNAUTHORIZED:" <<FFERRTAG(0xF8,'4','0','1');

//輸出
//AVERROR_HTTP_UNAUTHORIZED: -825242872

輸出和前面的紀錄檔輸出還有我們計算出來的結果都是一樣的,到這裡我們確定報出了AVERROR_HTTP_UNAUTHORIZED錯誤。順手把error.h中其他宏定義列印出來,ffmpeg常用錯誤碼錯誤碼錶如下:

錯誤碼宏定義 錯誤碼 錯誤說明
AVERROR_BSF_NOT_FOUND -1179861752 Bitstream filter not found
AVERROR_BUG -558323010 Internal bug, also see AVERROR_BUG2
AVERROR_BUFFER_TOO_SMALL -1397118274 Buffer too small
AVERROR_DECODER_NOT_FOUND -1128613112 Decoder not found
AVERROR_DEMUXER_NOT_FOUND -1296385272 Demuxer not found
AVERROR_ENCODER_NOT_FOUND -1129203192 Encoder not found
AVERROR_EOF -541478725 End of file
AVERROR_EXIT -1414092869 Immediate exit was requested; the called function should not be restarted
AVERROR_EXTERNAL -542398533 Generic error in an external library
AVERROR_FILTER_NOT_FOUND -1279870712 Filter not found
AVERROR_INVALIDDATA -1094995529 Invalid data found when processing input
AVERROR_MUXER_NOT_FOUND -1481985528 Muxer not found
AVERROR_OPTION_NOT_FOUND -1414549496 Option not found
AVERROR_PATCHWELCOME -1163346256 Not yet implemented in FFmpeg, patches welcome
AVERROR_PROTOCOL_NOT_FOUND -1330794744 Protocol not found
AVERROR_STREAM_NOT_FOUND -1381258232 Stream not found
AVERROR_BUG2 -541545794
AVERROR_UNKNOWN -1313558101
AVERROR_EXPERIMENTAL -733130664
AVERROR_INPUT_CHANGED -1668179713
AVERROR_OUTPUT_CHANGED -1668179714
AVERROR_HTTP_BAD_REQUEST -808465656
AVERROR_HTTP_UNAUTHORIZED -825242872
AVERROR_HTTP_FORBIDDEN -858797304
AVERROR_HTTP_NOT_FOUND -875574520
AVERROR_HTTP_OTHER_4XX -1482175736
AVERROR_HTTP_SERVER_ERROR -1482175992

於是可以在程式碼中增加Unauthorized情況的處理,如果Unauthorized則讓使用者輸入使用者名稱和密碼。

//ffmpegmanager.cpp
if ((ret = avformat_open_input(&streamFmtCtx, url.toStdString().c_str(), nullptr, nullptr)) != 0) {
    if (ret == AVERROR_HTTP_UNAUTHORIZED)
    {
        //...
        return;
    }else{
        //...
        return;
    }
}

vlc中,如果輸入的使用者名稱和密碼無法通過驗證,則會重新彈出驗證框(且使用者名稱不用重新輸入),直至輸入正確或取消輸入(效果看開頭)。所以我們也加入RTSP地址合法性的檢查等操作:

//ffmpegmanager.cpp
int rtspIndex = url.indexOf("rtsp://");
int atIndex = url.lastIndexOf("@");
if(rtspIndex != -1 && atIndex != -1){
    QString couple = url.mid(rtspIndex + 7, atIndex - rtspIndex - 7);
    username = couple;
    if(couple.contains(':')){
        username = couple.mid(0, couple.lastIndexOf(':'));
    }
}

到這裡,我們的程式碼可以適配需要摘要認證的情況了。

增加錯誤視窗

vlc在無法開啟RTSP地址的時候會彈出錯誤視窗。

我們也增加一個錯誤視窗,把所有錯誤都歸為無法開啟地址,並列印出來。

解決記憶體漏失

最然程式可以正常接收RTSP流了,但出現了之前沒出現的情況:記憶體持續增加。這種情況下一般是發生了記憶體洩露,之前讀取MP4檔案沒有發現,可能是因為檔案大小固定,現在持續收流,現象比較明顯,我們得排查我們的程式碼。簡單定位之後,我們發現是下面的程式碼塊發生洩露:

while(av_read_frame(streamFmtCtx, packet) >= 0){
    if(packet->stream_index == nVideoIndex){
        if(avcodec_send_packet(decoderCtx, packet)>=0){
            while((ret = avcodec_receive_frame(decoderCtx, decodedFrame)) >= 0){
                //...
            }
        }
    }
}

接下來我們逐句排查,首先是av_read_frame,檢視它的宣告:

//avformat.h
/**
 *.....
 * On success, the returned packet is reference-counted (pkt->buf is set) and
 * valid indefinitely. The packet must be freed with av_packet_unref() when
 * it is no longer needed. 
 *.....
 */
int av_read_frame(AVFormatContext *s, AVPacket *pkt);

這裡面有些有用的資訊:pktreference-counted的,如果不av_packet_unref() ,則它將永久有效。繼續看它的定義,我們的目標是找出和pkt相關的進行reference-counted的語句:

//avformat.cpp
int av_read_frame(AVFormatContext *s, AVPacket *pkt){
    //...
    ret = read_frame_internal(s, pkt);
    ret = avpriv_packet_list_put(&s->internal->packet_buffer,
                                 &s->internal->packet_buffer_end,
                                 pkt, NULL, 0);
    //...
}

最終pkt都要執行這兩個函數,avpriv_packet_list_put就是我們要找的地方,繼續看它的宣告和定義:

//packet_internal.h
/**
 * Append an AVPacket to the list.
 *
 * @param head  List head element
 * @param tail  List tail element
 * @param pkt   The packet being appended. The data described in it will
 *              be made reference counted if it isn't already.
 */
int avpriv_packet_list_put(PacketList **head, PacketList **tail,
                           AVPacket *pkt,
                           int (*copy)(AVPacket *dst, const AVPacket *src),
                           int flags);
//avpacket.c
int avpriv_packet_list_put(PacketList **packet_buffer,
                           PacketList **plast_pktl,
                           AVPacket      *pkt,
                           int (*copy)(AVPacket *dst, const AVPacket *src),
                           int flags)
{
    //...
    if (*packet_buffer)
        (*plast_pktl)->next = pktl;
    else
        *packet_buffer = pktl;
    *plast_pktl = pktl;
    return 0;
}

最後pkt新增到了buffered packet中。其他細節我們可以不用深究,只需要知道pkt被新增到了一個list中,那麼這裡的確會產生記憶體漏失。根據前面宣告中的提示,我們需要使用av_packet_unref()來釋放pkt的參照,那麼直接在讀取和使用完1個AVPacket和結束時呼叫av_packet_unref()

while(av_read_frame(streamFmtCtx, packet) >= 0){
    //...
    av_packet_unref(packet);
}
av_packet_unref(packet);

加上後發現,記憶體漏失的問題被解決了,那就不再繼續向下排查了。

遺留問題

至此,一個簡單好用的RTSP收流功能就算是完成了,但別高興的太早,事情往往沒有我們想象的那麼簡單——經過測試,接收高解析度視訊一段時間後(甚至一開始),就會產生花屏現象:

考慮到篇幅原因,後面單獨篇章再去討論解決這個問題,依舊是需要從原始碼切入:)

TO-DO

  • 適配BASE認證