哈嘍,我是老吳。
今天分享一個比較復(fù)雜的開源項目:
http://www.live555.com
live555 是一個開源的流媒體庫,用于實現(xiàn)實時流媒體的傳輸和處理。
它提供了一套跨平臺的 C++ 類庫,以便使用者快速地構(gòu)建出高效、可靠的流媒體服務(wù)器和客戶端應(yīng)用程序。
代碼量比較龐大,目前約 9w 行代碼。如果只專注核心邏輯,代碼量縮減到約 8K 行。
能收獲什么?
1、獲得一個高效可靠的流媒體庫;
2、了解一個產(chǎn)品級的 c++ 項目是如何設(shè)計的;
3、了解音視頻相關(guān)基礎(chǔ)知識;
4、獲得一個基于 select() 的 c++ 事件循環(huán)庫;
live555 的使用者涵蓋了各個領(lǐng)域的流媒體應(yīng)用,包括媒體播放器、流媒體服務(wù)器、視頻監(jiān)控系統(tǒng)等,VLC、FFmpeg、GStreamer 均使用 live555 來實現(xiàn)對流媒體的接收和播放功能,開源界對 live555 的認可度是非常高的。
live555 基于 C++ 03,語法相對簡單,因此很適合我們專注于學(xué)習(xí) C++ 類的設(shè)計,和模仿編寫專業(yè)的 C++ 軟件。
最后,為了理解 live555 的源碼,我們需要補充一些多媒體、流媒體的相關(guān)理論知識,而通過閱讀和運行 live555 相關(guān)的應(yīng)用,又可以加深我們對理論知識的理解。
編譯 live555 庫
$ ./genMakefiles linux
$ make -j16
編譯完成后,會生成下面 4 個靜態(tài)庫:
$ find . -name *.a
./BasicUsageEnvironment/libBasicUsageEnvironment.a
./liveMedia/libliveMedia.a
./groupsock/libgroupsock.a
./UsageEnvironment/libUsageEnvironment.a
libBasicUsageEnvironment.a 和 libUsageEnvironment.a 是基礎(chǔ)設(shè)施庫,它們負責(zé)實現(xiàn)事件循環(huán)、上下文管理、任務(wù)管理等;
libliveMedia.a 負責(zé)實現(xiàn)各種多媒體的流化,包括音視頻的編解碼、流媒體協(xié)議的實現(xiàn);
libgroupsock.a 負責(zé)網(wǎng)絡(luò) IO 功能,核心就是 TCP、UDP 的讀寫;
最簡單的示例:RTP 傳輸 MP3 音頻
這里涉及 server 和 client 兩個程序,下面的 server 端的代碼。
UsageEnvironment* env;
struct sessionState_t {
FramedSource* source;
RTPSink* sink;
Groupsock* rtpGroupsock;
} sessionState;
charconst* inputFileName = "test.mp3";
int main()
{
TaskScheduler* scheduler = BasicTaskScheduler::createNew();
env = BasicUsageEnvironment::createNew(*scheduler);
charconst* destinationAddressStr = "239.255.42.42";
constunsigned short rtpPortNum = 6666;
NetAddressList destinationAddresses(destinationAddressStr);
struct sockaddr_storage destinationAddress;
copyAddress(destinationAddress, destinationAddresses.firstAddress());
const Port rtpPort(rtpPortNum);
sessionState.rtpGroupsock
= new Groupsock(*env, destinationAddress, rtpPort, 1);
// Create a 'MP3 RTP' sink from the RTP 'groupsock':
sessionState.sink = MPEG1or2AudioRTPSink::createNew(*env, sessionState.rtpGroupsock);
play();
env->taskScheduler().doEventLoop();
return0;
}
void play()
{
sessionState.source = MP3FileSource::createNew(*env, inputFileName);
if (sessionState.source == NULL) {
*env << "Unable to open file \"" << inputFileName << "\" as a MP3 file source\n";
exit(1);
}
*env << "Beginning streaming...\n";
sessionState.sink->startPlaying(*sessionState.source, afterPlaying, NULL);
}
在 live555 的 testProgs 目錄下,有 30 多個官方提供的示例程序,我們挑這個最簡單的來分析。
server 程序的核心邏輯:
1、準(zhǔn)備運行環(huán)境;
2、設(shè)置數(shù)據(jù)來源;
3、設(shè)置數(shù)據(jù)目的地;
TaskScheduler 用于任務(wù)管理,它基于 select() 實現(xiàn)了事件循環(huán),關(guān)于事件循環(huán)可以看看我前面寫的文章,這里就不展開分析了:
不懂就問:什么是 Eventloop?
每日開源之 libuev
BasicUsageEnvironment 則用于上下文管理,live555 和核心 API 基本上都有有這個參數(shù)。
設(shè)置好上下文后,接下來就是數(shù)據(jù)的流化。流媒體的本質(zhì)就是數(shù)據(jù)的網(wǎng)絡(luò)傳輸。一般用 Source 來稱呼數(shù)據(jù)源,用 Sink 來稱呼數(shù)據(jù)的目的地。在我們這個例子中,Source 是 MP3FileSource,Sink 是 MPEG1or2AudioRTPSink。
顧名思義,MP3FileSource 負責(zé)封裝 MP3 文件的解析和數(shù)據(jù)的提取,MPEG1or2AudioRTPSink 則負責(zé)將每一幀 MP3 數(shù)據(jù)通過 RTP 協(xié)議發(fā)送出去。
相應(yīng)的, client 端程序同樣也是初始化 Source 和 Sink:
struct sessionState_t {
FramedSource* source;
FileSink* sink;
} sessionState;
UsageEnvironment* env;
int main()
{
TaskScheduler* scheduler = BasicTaskScheduler::createNew();
env = BasicUsageEnvironment::createNew(*scheduler);
sessionState.sink = FileSink::createNew(*env, "stdout");
charconst* sessionAddressStr = "239.255.42.42";
constunsigned short rtpPortNum = 6666;
NetAddressList sessionAddresses(sessionAddressStr);
struct sockaddr_storage sessionAddress;
copyAddress(sessionAddress, sessionAddresses.firstAddress());
const Port rtpPort(rtpPortNum);
Groupsock rtpGroupsock(*env, sessionAddress, rtpPort, 1);
// Create the data source: a "MP3 *ADU* RTP source"
sessionState.source = MPEG1or2AudioRTPSource::createNew(*env, &rtpGroupsock);
sessionState.sink->startPlaying(*sessionState.source, afterPlaying, NULL);
env->taskScheduler().doEventLoop();
return0;
}
client 端的數(shù)據(jù) Source 來源于 RTP 傳輸,用 MPEG1or2AudioRTPSource 來實現(xiàn),數(shù)據(jù)的目的地則是標(biāo)準(zhǔn)輸出,用 FileSink("stdout") 來實現(xiàn)。
運行效果如下:
$ ./testMP3Streamer # server
Beginning streaming...
$ ./testMP3Receiver | ffplay - # client
Beginning receiving multicast stream...
testMP3Streamer 會將當(dāng)前目錄下的 test.mp3 通過 RTP 協(xié)議廣播出來,testMP3Receiver 則會將收到的 mp3 數(shù)據(jù)寫到標(biāo)準(zhǔn)輸出,我們使用 ffplay 將音頻數(shù)據(jù)播放出來。
RTP 協(xié)議簡介
這里補充一些 RTP 協(xié)議的基礎(chǔ)知識。
RTP(Real-time Transport Protocol)是一種用于實時傳輸音頻和視頻數(shù)據(jù)的網(wǎng)絡(luò)傳輸協(xié)議。它是基于 UDP,用于在 IP 網(wǎng)絡(luò)上傳輸實時媒體數(shù)據(jù)。
RTP協(xié)議的設(shè)計目標(biāo)是提供低延遲、高效率的傳輸,以滿足實時應(yīng)用的需求,如音視頻會議、流媒體傳輸?shù)取K捎昧艘恍┨囟ǖ臋C制來保證實時數(shù)據(jù)的傳輸質(zhì)量,包括時間戳、序列號、負載類型等。
RTP協(xié)議的主要特點包括:
時間戳:每個 RTP 數(shù)據(jù)包都包含一個時間戳,用于指示數(shù)據(jù)的播放時間。接收方可以根據(jù)時間戳來同步和播放媒體數(shù)據(jù)。
序列號:每個RTP數(shù)據(jù)包都有一個唯一的序列號,用于檢測和糾正數(shù)據(jù)包的丟失、重復(fù)和亂序。
負載類型:RTP 協(xié)議支持各種不同的媒體類型,如音頻、視頻、文本等。每個RTP數(shù)據(jù)包都包含一個負載類型字段,用于指示數(shù)據(jù)的類型和編碼方式。
NACK 反饋:RTP協(xié)議支持NACK(Negative Acknowledgement)機制,接收方可以通過發(fā)送NACK報文來請求丟失的數(shù)據(jù)包重新發(fā)送。
RTCP:RTP協(xié)議還定義了RTCP(Real-time Transport Control Protocol),用于傳輸控制信息,如網(wǎng)絡(luò)延遲、丟包率、接收方的緩沖狀態(tài)等。
RTP 協(xié)議通常與其他協(xié)議結(jié)合使用,如 RTSP(Real-Time Streaming Protocol)用于媒體會話的控制,以及SDP(Session Description Protocol)用于描述媒體會話的參數(shù)。
總的來說,RTP 協(xié)議是一種專門用于實時媒體傳輸?shù)膮f(xié)議,提供了一系列特性和機制,以保證音視頻數(shù)據(jù)的實時性和可靠性。它在實時應(yīng)用領(lǐng)域廣泛應(yīng)用,如音視頻會議、流媒體傳輸、IP電話等。
最關(guān)鍵的問題:數(shù)據(jù)是如何一幀幀流化的?
我們關(guān)注的重點即不是某個具體音視頻格式的解析,也不是具體某個協(xié)議的實現(xiàn),而是 live555 對音視頻流化的整體框架。
從前面的示例,我們已經(jīng)得知,live555 本質(zhì)上就是在做一件事:將音視頻數(shù)據(jù)逐幀解碼出來,然后通過 RTP 協(xié)議經(jīng)網(wǎng)絡(luò)發(fā)送出去。
live555 雖然封裝了許多數(shù)據(jù) Source 和 Sink,但是我們沒必要一一詳細了解。
為了避免迷失在過多的陌生概念之中,下面仍從 RTP 傳輸 MP3 數(shù)據(jù)這個最簡單的示例入手,去分析 live555 的工作流程。
首先,我們需要對相關(guān)類的關(guān)系有個大概的概念:

MediaSource 是所有 Source 的父類; 各種具體的音視頻 Source 都是基于其派生出來的,Source 類最關(guān)鍵的成員函數(shù)是 getNextFrame(),該 API 負責(zé)提供一幀數(shù)據(jù)。
MediaSink 是所有 Sink 的父類; 它派生出了 FileSink、RTPSink 等眾多 Sink 類。Sink 類最關(guān)鍵的成員函數(shù)是 startPlaying(),該 API 會使用 Source 對象獲取幀數(shù)據(jù),然后發(fā)送到網(wǎng)絡(luò)上。
RTP 傳輸 MP3 的主要邏輯:
MediaSink::startPlaying()
--->MultiFramedRTPSink::continuePlaying()
--->MultiFramedRTPSink::buildAndSendPacket()
--->MultiFramedRTPSink::packFrame()
--->FramedSource::getNextFrame(afterGettingFrame)
--->MP3FileSource::doGetNextFrame()
MultiFramedRTPSink::afterGettingFrame()
--->MultiFramedRTPSink::sendPacketIfNecessary()
--->RTPInterface::sendPacket()
--->envir().taskScheduler().scheduleDelayedTask(sendNext)
MultiFramedRTPSink::sendNext()
--->MultiFramedRTPSink::buildAndSendPacket();
當(dāng)一切準(zhǔn)備就緒后,程序會調(diào)用 MediaSink::startPlaying() 來啟動數(shù)據(jù)的流化。最后在 packFrame() 里會調(diào)用到 Source 對象的 getNextFrame()。

getNextFrame() 最終會調(diào)用到 MP3FileSource::doGetNextFrame(),由 MP3FileSource 這個最底層的 Source 類來負責(zé) MP3 音頻的解碼。解碼完成后,afterGettingFrame() 會被回調(diào),當(dāng)數(shù)據(jù)正常時,會調(diào)用 sendPacketIfNecessary() 將數(shù)據(jù)發(fā)送出去,并且往事件循環(huán)的調(diào)度器中添加下一個發(fā)送任務(wù)。
一段時間的延遲后,MultiFramedRTPSink::sendNext() 會被調(diào)用,它又會開始推動新一幀數(shù)據(jù)的傳輸,如此循環(huán)反復(fù),直到 Source 里的所有幀數(shù)據(jù)都被消費完。
live555 如何創(chuàng)建 RTSP server?
一般情況下,RTP 協(xié)議通常會與 RTSP 協(xié)議結(jié)合使用,對外提供 RTSP Server 服務(wù)。
RTSP 提供了標(biāo)準(zhǔn)化的方式來控制實時流媒體的傳輸和播放。有了它,就可以控制音視頻的播放、暫停、停止、快進、后退了。
添加下面這些代碼,就能創(chuàng)建 RTSP Server 了:
RTSPServer* rtspServer = RTSPServer::createNew(*env, 8554, authDB);
ServerMediaSession* sms = ServerMediaSession::createNew(*env, streamName, streamName, descriptionString);
sms->addSubsession(MP3AudioFileServerMediaSubsession
::createNew(*env, inputFileName, reuseFirstSource, useADUs, interleaving));
rtspServer->addServerMediaSession(sms);
3 個步驟:
1、創(chuàng)建 ServerMediaSubsession 對象;
2、創(chuàng)建 ServerMediaSession 對象,調(diào)用 addSubsession() 添加 ServerMediaSubsession;
3、創(chuàng)建 RTSPServer 對象,調(diào)用 addServerMediaSession() 添加 ServerMediaSession;
最容易理解的是類 RTSPServer,它負責(zé)封裝實現(xiàn) RTSP Server。RTSP 類似 HTTP 協(xié)議,都是文本協(xié)議。

既然是一個 Server,那就肯定有 accept client 、read client、解析和處理數(shù)據(jù)的操作:
RTSPServer::createNew() 會先初始化 socket,然后往任務(wù)調(diào)度器里添加一個新任務(wù) incomingConnectionHandlerIPv4(),它的內(nèi)容就是等著 accept() client 的連接。
當(dāng)有 client 連接上來時,就會創(chuàng)建一個 RTSPClientConnection 對象,并添加到 RTSPServer::fClientConnections 中。在創(chuàng)建 RTSPClientConnection 對象時,會往任務(wù)調(diào)度器里添加一個新任務(wù) incomingRequestHandler(),當(dāng) client 發(fā)數(shù)據(jù)過來時,incomingRequestHandler() 會將數(shù)據(jù) read() 出來,然后交由 RTSPClientConnection::handleRequestBytes() 進行解析。
假設(shè) client 發(fā)來了一個 PLAY 的請求:

handleRequestBytes() 會調(diào)用 handleCmd_withinSession(), 該函數(shù)會根據(jù) streamName, 從 ServerMediaSession 對象里找到我們最初注冊的 ServerMediaSubsession,然后將 subsession 作為參數(shù)傳遞給 handleCmd_PLAY(),最后調(diào)用到 subsession->startStream(),進而調(diào)用到 MediaSink::startPlaying(),回到我們前面 RTP 傳輸 MP3 核心邏輯。
實踐應(yīng)用:給 MJPG-Streamer 添加一個 RTSP 輸出插件
為了說明一下 live555 的靈活用法,下面我們給 MJPG-Streamer 添加一個使用 live555 輸出 RTSP JPEG 流的功能。
關(guān)于 MJPG-Streamer,可以先看看我這篇文章:
五分鐘拆解流媒體入門項目 MJPG-Streamer
Mjpg-Streamer 是一個 JPEG 文件的傳輸流。它最常用的用途就是采集攝像頭的數(shù)據(jù),然后啟動 http server,用戶就可以通過瀏覽器查看圖像數(shù)據(jù)了。它將圖像的來源視為輸入,將圖像的展示視為輸出。每一種輸入或輸出是都抽象為一個插件。
如果我們要基于 live555 增加一個 RTSP JPEG 的輸出插件,只需要新建 2 個類:
1、創(chuàng)建新的 MediaSource 子類 MyJPEGVideoSource,它會重寫 doGetNextFrame(),以獲取一幀 JPEG 數(shù)據(jù),具體實現(xiàn)和 HTTP output 插件是類似的。
2、創(chuàng)建新的 Subsession 子類 LiveVideoServerMediaSubsession,它會重寫創(chuàng)建 createNewStreamSource() 和 createNewRTPSink() 。

有了這兩個類之后,就可以模仿前面 RTSP Server 的創(chuàng)建流程,給 MJPG-Streamer 編寫 RTSP output 插件了,源碼和前面 MP3 的 RTSP server 幾乎一樣,這里就不貼出來了。
到此,live555 的核心邏輯就分析完畢了。
live555 是一個開源的多媒體流媒體庫,用于實現(xiàn)實時流媒體的傳輸和處理。
它支持常見的流媒體協(xié)議,如 RTSP(Real Time Streaming Protocol)、RTP(Real-time Transport Protocol)、RTCP(RTP Control Protocol)等,并提供了靈活的接口和功能,適用于構(gòu)建流媒體服務(wù)器和客戶端應(yīng)用程序。
它的設(shè)計目標(biāo)是實現(xiàn)高效、可靠的流媒體傳輸,并提供靈活的接口和功能,以滿足不同應(yīng)用場景的需求。
要用好 live555 ,需要熟悉 C++ 編程和網(wǎng)絡(luò)編程的相關(guān)知識。官方提供了豐富的示例代碼,開發(fā)人員可以通過這些示例快速地熟悉 live555 的使用方法。
???????????????? END ???????????????
關(guān)注我的微信公眾號,回復(fù)“星球”加入知識星球,有問必答。
點擊“閱讀原文”查看知識星球詳情,歡迎點分享、收藏、點贊、在看。