在實際的開發過程當中,咱們是能夠不用本身來搭建流媒體服務器的,訪問後臺的接口會返回媒體房間和 IM 房間。但如今咱們本身測試就沒法用公司的接口了,固然也能夠去抓一些第三方的直播接口,我強烈不推薦你們這麼作。最好的辦法就是本身搭建一個簡單的流媒體服務器。java
首先登陸本身的雲主機,下載解壓 nginx 和 rtmpnginx
sudo wget https://github.com/nginx/nginx/archive/release-1.17.1.tar.gz
sudo wget https://github.com/arut/nginx-rtmp-module/archive/v1.2.1.tar.gz
sudo tar -zxvf release-1.17.1.tar.gz
sudo tar -zxvf v1.2.1.tar.gz
複製代碼
./auto/configure --add-module=/lib/nginx/nginx-rtmp-module-1.2.1
make
make install
複製代碼
最後配置測試流媒體服務器github
cd /usr/local/nginx/sbin/
./nginx
.\ffmpeg.exe -re -i 01.mp4 -vcodec libx264 -acodec aac -f flv rtmp://148.70.96.230/myapp/mystream
複製代碼
當咱們的流媒體服務器搭建好後,要用 ffmpeg 測試一下,確保流媒體服務器搭建成功後,咱們再來集成 RTMP 推流的源碼。算法
git clone git://git.ffmpeg.org/rtmpdump
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DNO_CRYPTO")
/**
* 初始化鏈接流媒體服務器
*/
void *initConnectFun(void *context) {
DZLivePush *pLivePush = (DZLivePush *) context;
// 建立 RTMP
pLivePush->pRtmp = RTMP_Alloc();
// 初始化 RTMP
RTMP_Init(pLivePush->pRtmp);
// 設置鏈接超時
pLivePush->pRtmp->Link.timeout = 10;
pLivePush->pRtmp->Link.lFlags |= RTMP_LF_LIVE;
RTMP_SetupURL(pLivePush->pRtmp, pLivePush->url);
RTMP_EnableWrite(pLivePush->pRtmp);
// 鏈接失敗回調到 java 層
if (!RTMP_Connect(pLivePush->pRtmp, NULL)) {
LOGE("connect url error");
pLivePush->pJniCall->callConnectError(THREAD_CHILD, RTMP_CONNECT_ERROR_CODE, "connect url error");
return (void *) RTMP_CONNECT_ERROR_CODE;
}
if (!RTMP_ConnectStream(pLivePush->pRtmp, 0)) {
LOGE("connect stream url error");
pLivePush->pJniCall->callConnectError(THREAD_CHILD, RTMP_STREAM_CONNECT_ERROR_CODE, "connect stream url error");
return (void *) RTMP_STREAM_CONNECT_ERROR_CODE;
}
// 鏈接成功也回調到 Java 層,能夠開始推流了
LOGE("connect succeed");
pLivePush->pJniCall->callConnectSuccess(THREAD_CHILD);
return (void *) 0;
}
複製代碼
咱們打算採用最多見的 H.264 來編碼推流,那麼如今咱們不得不來了解一下 H.264 的協議了,這些東西雖然說看似比較枯燥複雜,但這也是最最重要的部分。首先須要明確 H264 能夠分爲兩層:1.VCL video codinglayer(視頻編碼層),2.NAL network abstraction layer(網絡提取層)。對於 VCL 具體的編解碼算法這裏暫時先不介紹,只介紹經常使用的 NAL 層,即網絡提取層,這是解碼的基礎。 bash
SPS:序列參數集 PPS:圖像參數集 I幀:幀內編碼幀,可獨立解碼生成完整的圖片。 P幀: 前向預測編碼幀,須要參考其前面的一個I 或者B 來生成一張完整的圖片。 B幀: 雙向預測內插編碼幀,則要參考其前一個I或者P幀及其後面的一個P幀來生成一張完整的圖片服務器
根據上面所說,如今咱們就得思考幾個問題了:網絡
關於怎麼預覽相機,怎麼編碼成 H264,怎麼獲取 SPS 和 PPS 你們須要先看看以前的《FFmpeg - 朋友圈錄製視頻添加背景音樂》。爲了確保直播過程當中進來的用戶也能夠正常的觀看直播,咱們須要在每一個關鍵幀前先把 SPS 和 PPS 推送到流媒體服務器。app
/**
* 發送 sps 和 pps 到流媒體服務器
* @param spsData sps 的數據
* @param spsLen sps 的數據長度
* @param ppsData pps 的數據
* @param ppsLen pps 的數據長度
*/
void DZLivePush::pushSpsPps(jbyte *spsData, jint spsLen, jbyte *ppsData, jint ppsLen) {
// frame type : 1關鍵幀,2 非關鍵幀 (4bit)
// CodecID : 7表示 AVC (4bit) , 與 frame type 組合起來恰好是 1 個字節 0x17
// fixed : 0x00 0x00 0x00 0x00 (4byte)
// configurationVersion (1byte) 0x01版本
// AVCProfileIndication (1byte) sps[1] profile
// profile_compatibility (1byte) sps[2] compatibility
// AVCLevelIndication (1byte) sps[3] Profile level
// lengthSizeMinusOne (1byte) 0xff 包長數據所使用的字節數
// sps + pps 的數據
// sps number (1byte) 0xe1 sps 個數
// sps data length (2byte) sps 長度
// sps data sps 的內容
// pps number (1byte) 0x01 pps 個數
// pps data length (2byte) pps 長度
// pps data pps 的內容
// body 長度 = spsLen + ppsLen + 上面所羅列出來的 16 字節
int bodySize = spsLen + ppsLen + 16;
// 初始化建立 RTMPPacket
RTMPPacket *pPacket = static_cast<RTMPPacket *>(malloc(sizeof(RTMPPacket)));
RTMPPacket_Alloc(pPacket, bodySize);
RTMPPacket_Reset(pPacket);
// 按照上面的協議,開始一個一個給 body 賦值
char *body = pPacket->m_body;
int index = 0;
// CodecID 與 frame type 組合起來恰好是 1 個字節 0x17
body[index++] = 0x17;
// fixed : 0x00 0x00 0x00 0x00 (4byte)
body[index++] = 0x00;
body[index++] = 0x00;
body[index++] = 0x00;
body[index++] = 0x00;
//0x01版本
body[index++] = 0x01;
// sps[1] profile
body[index++] = spsData[1];
// sps[2] compatibility
body[index++] = spsData[2];
// sps[3] Profile level
body[index++] = spsData[3];
// 0xff 包長數據所使用的字節數
body[index++] = 0xff;
// 0xe1 sps 個數
body[index++] = 0xe1;
// sps 長度
body[index++] = (spsLen >> 8) & 0xff;
body[index++] = spsLen & 0xff;
// sps 的內容
memcpy(&body[index], spsData, spsLen);
index += spsLen;
// 0x01 pps 個數
body[index++] = 0x01;
// pps 長度
body[index++] = (ppsLen >> 8) & 0xff;
body[index++] = ppsLen & 0xff;
// pps 的內容
memcpy(&body[index], ppsData, ppsLen);
// 設置 RTMPPacket 的參數
pPacket->m_packetType = RTMP_PACKET_TYPE_VIDEO;
pPacket->m_nBodySize = bodySize;
pPacket->m_nTimeStamp = 0;
pPacket->m_hasAbsTimestamp = 0;
pPacket->m_nChannel = 0x04;
pPacket->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
pPacket->m_nInfoField2 = this->pRtmp->m_stream_id;
// 添加到發送隊列
pPacketQueue->push(pPacket);
}
複製代碼
緊接着發送每一幀的數據ide
/**
* 發送每一幀的視頻數據到服務器
* @param videoData
* @param dataLen
* @param keyFrame
*/
void DZLivePush::pushVideo(jbyte *videoData, jint dataLen, jboolean keyFrame) {
// frame type : 1關鍵幀,2 非關鍵幀 (4bit)
// CodecID : 7表示 AVC (4bit) , 與 frame type 組合起來恰好是 1 個字節 0x17
// fixed : 0x01 0x00 0x00 0x00 (4byte) 0x01 表示 NALU 單元
// video data length (4byte) video 長度
// video data
// body 長度 = dataLen + 上面所羅列出來的 9 字節
int bodySize = dataLen + 9;
// 初始化建立 RTMPPacket
RTMPPacket *pPacket = static_cast<RTMPPacket *>(malloc(sizeof(RTMPPacket)));
RTMPPacket_Alloc(pPacket, bodySize);
RTMPPacket_Reset(pPacket);
// 按照上面的協議,開始一個一個給 body 賦值
char *body = pPacket->m_body;
int index = 0;
// CodecID 與 frame type 組合起來恰好是 1 個字節 0x17
if (keyFrame) {
body[index++] = 0x17;
} else {
body[index++] = 0x27;
}
// fixed : 0x01 0x00 0x00 0x00 (4byte) 0x01 表示 NALU 單元
body[index++] = 0x01;
body[index++] = 0x00;
body[index++] = 0x00;
body[index++] = 0x00;
// (4byte) video 長度
body[index++] = (dataLen >> 24) & 0xff;
body[index++] = (dataLen >> 16) & 0xff;
body[index++] = (dataLen >> 8) & 0xff;
body[index++] = dataLen & 0xff;
// video data
memcpy(&body[index], videoData, dataLen);
// 設置 RTMPPacket 的參數
pPacket->m_packetType = RTMP_PACKET_TYPE_VIDEO;
pPacket->m_nBodySize = bodySize;
pPacket->m_nTimeStamp = RTMP_GetTime() - startPushTime;
pPacket->m_hasAbsTimestamp = 0;
pPacket->m_nChannel = 0x04;
pPacket->m_headerType = RTMP_PACKET_SIZE_LARGE;
pPacket->m_nInfoField2 = this->pRtmp->m_stream_id;
pPacketQueue->push(pPacket);
}
複製代碼
最後就是把錄製的聲音數據推到媒體房間,這部分流程跟視頻推流相似。
/**
* 發送音頻數據到服務器
* @param audioData
* @param dataLen
*/
void DZLivePush::pushAudio(jbyte *audioData, jint dataLen) {
// 2 字節頭信息
// 前四位表示音頻數據格式 AAC 10(A)
// 五六位表示採樣率 0 = 5.5k 1 = 11k 2 = 22k 3(11) = 44k
// 七位表示採樣採樣的精度 0 = 8bits 1 = 16bits
// 八位表示音頻類型 0 = mono 1 = stereo
// 咱們這裏算出來第一個字節是 0xAF
// 0x01 表明 aac 原始數據
// body 長度 = dataLen + 上面所羅列出來的 2 字節
int bodySize = dataLen + 2;
// 初始化建立 RTMPPacket
RTMPPacket *pPacket = static_cast<RTMPPacket *>(malloc(sizeof(RTMPPacket)));
RTMPPacket_Alloc(pPacket, bodySize);
RTMPPacket_Reset(pPacket);
// 按照上面的協議,開始一個一個給 body 賦值
char *body = pPacket->m_body;
int index = 0;
// 咱們這裏算出來第一個字節是 0xAF
body[index++] = 0xAF;
body[index++] = 0x01;
// audio data
memcpy(&body[index], audioData, dataLen);
// 設置 RTMPPacket 的參數
pPacket->m_packetType = RTMP_PACKET_TYPE_AUDIO;
pPacket->m_nBodySize = bodySize;
pPacket->m_nTimeStamp = RTMP_GetTime() - startPushTime;
pPacket->m_hasAbsTimestamp = 0;
pPacket->m_nChannel = 0x04;
pPacket->m_headerType = RTMP_PACKET_SIZE_LARGE;
pPacket->m_nInfoField2 = this->pRtmp->m_stream_id;
pPacketQueue->push(pPacket);
}
複製代碼
萬丈高樓平地起,這些的確都很簡單基礎,後面其實還有不少擴展,好比美顏濾鏡,IM 聊天房間,禮物動畫貼圖等等。
這是 NDK 實戰的最後一篇文章了,週六日咱們花了接近一年的時間來學習,我知道不少同窗並未從事這方面的開發,最後我再囉嗦囉嗦,給你們打點雞血。我我的是很幸運的,能把學到的東西用到工做中,但在三年前我其實也不知道,本身之後會從事這方面的工做。
這部分知識也是你們從中級到高級進階的一個必通過程,經過學習 NDK 咱們能把 Android 的上下層打通,之前只能是閱讀 Java 層的代碼,到如今能閱讀 FrameWorker 的 Native 層源碼,且 Android 不少的核心代碼都是在 C/C++ 中,所以當咱們沒法看懂這部分代碼時,咱們很難說本身理解了 Android,也不能算是一個高級工程師,我但願你們能夠從這些方面去花些時間。
從開發佈局上來講,咱們已經能作一些別人不能作的東西了,我的的價值在於別人不能作的咱們能作,這其實就是一個學習成本的問題,所以目前從事 NDK 開發的工做相比於只是簡單的作 Android 應用的薪資來講要高個 1.5 - 2 倍,至少咱們公司是這樣。且如今不少企業招聘崗位也都是要求會 NDK 開發者優先。
爲什麼咱們不繼續接着講 OpenGL 和音視頻的一些高級知識呢?目前講的這些東西都是很基礎的,高級進階內容實際上是在咱們後面規劃中的,這些知識其實不多有人能跟上來,從最近的上課活躍度就能體現出來,這也是爲何在網上咱們幾乎找不到系統的學習方案,出了問題也很難查到答案的一個緣由,但願你們能夠多花些時間。
視頻地址:pan.baidu.com/s/19eFV02Ty… 視頻密碼:6u50