FFmpeg - Android 直播推拉流

1. 搭建自己的流媒體服務器

在實際的開發過程中,我們是可以不用自己來搭建流媒體服務器的,訪問後臺的接口會返回媒體房間和 IM 房間。但現在我們自己測試就無法用公司的接口了,當然也可以去抓一些第三方的直播接口,我強烈不推薦大家這麼做。最好的辦法就是自己搭建一個簡單的流媒體服務器。

首先登錄自己的雲主機,下載解壓 nginxrtmp

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

然後編譯安裝 nginxrtmp

./auto/configure --add-module=/lib/nginx/nginx-rtmp-module-1.2.1
make
make install

最後配置測試流媒體服務器

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

2. 集成 RTMP 推流的源碼

當我們的流媒體服務器搭建好後,要用 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;
}

3. H.264 協議介紹

我們打算採用最常見的 H.264 來編碼推流,那麼現在我們不得不來了解一下 H.264 的協議了,這些東西雖說看似比較枯燥複雜,但這也是最最重要的部分。首先需要明確 H264 可以分爲兩層:1.VCL video codinglayer(視頻編碼層),2.NAL network abstraction layer(網絡提取層)。對於 VCL 具體的編解碼算法這裏暫時先不介紹,只介紹常用的 NAL 層,即網絡提取層,這是解碼的基礎。


SPS:序列參數集
PPS:圖像參數集
I幀:幀內編碼幀,可獨立解碼生成完整的圖片。
P幀: 前向預測編碼幀,需要參考其前面的一個I 或者B 來生成一張完整的圖片。
B幀: 雙向預測內插編碼幀,則要參考其前一個I或者P幀及其後面的一個P幀來生成一張完整的圖片

根據上面所說,現在我們就得思考幾個問題了:

  1. SPS 和 PPS 到底存的是什麼數據?
  2. 我們怎麼判斷獲取每一個 NALU ?
  3. 如何判斷某一個 NALU 是 I 幀、P 幀、 B 幀還是其他?

4. 直播推流視頻數據

關於怎麼預覽相機,怎麼編碼成 H264,怎麼獲取 SPS 和 PPS 大家需要先看看之前的《FFmpeg - 朋友圈錄製視頻添加背景音樂》。爲了確保直播過程中進來的用戶也可以正常的觀看直播,我們需要在每個關鍵幀前先把 SPS 和 PPS 推送到流媒體服務器。

/**
 * 發送 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);
}

緊接着發送每一幀的數據

/**
 * 發送每一幀的視頻數據到服務器
 * @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);
}

5. 直播推流音頻數據

最後就是把錄製的聲音數據推到媒體房間,這部分流程跟視頻推流類似。

/**
 * 發送音頻數據到服務器
 * @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 和音視頻的一些高級知識呢?目前講的這些東西都是很基礎的,高級進階內容其實是在我們後面規劃中的,這些知識其實很少有人能跟上來,從最近的上課活躍度就能體現出來,這也是爲什麼在網上我們幾乎找不到系統的學習方案,出了問題也很難查到答案的一個原因,希望大家可以多花些時間。

視頻地址:https://pan.baidu.com/s/19eFV02TyjOD3few0HlZs1w
視頻密碼:6u50

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