《Android音視頻系列-7》直播推流

這篇文章將介紹在Android平臺使用RTMPDump來進(jìn)行直播推流。

一捆昏、推流核心思想

推流流程圖:來自文末參考鏈接

推流邓线,可以推H264裸流淌友,也可以封裝成FLV格式再推送,
為什么不直接推H264裸流骇陈,而是要封裝成FLV格式再推震庭,多此一舉?
其實(shí)是為了兼容多種編碼格式的流你雌。

如果直接推H264裸流器联,服務(wù)端就對應(yīng)一套H264裸流的邏輯。
假如后面要推H265的流或者其它封裝格式的流匪蝙,那么無論是推流端還是服務(wù)端主籍,都要改邏輯。
而封裝成FLV格式再推流逛球,后面如果要推H265流千元,只需要將H265流封裝成FLV格式即可,服務(wù)端不需要任何更改颤绕,拉流端格式也沒變幸海。

RTMP協(xié)議采用的封裝格式是FLV

二祟身、集成RTMPDump

RTMP(Real Time Messaging Protocol):實(shí)時(shí)消息協(xié)議,目前主流的流媒體協(xié)議物独。

RTMPDump是一個(gè)用來處理RTMP流媒體的工具包袜硫,是一個(gè)C++的開源工程,我們只需要將音視頻流封裝成RTMPDump所需要的格式挡篓,然后調(diào)用推流方法RTMP_SendPacket即可婉陷。

RTMPDump源碼下載

下載最新的就行


解壓之后把源碼拷貝到Android工程

這里我創(chuàng)建一個(gè)文件夾 push_rtmp,然后將librtmp整個(gè)拷過去

配置cmake官研,主要添加的配置如下秽澳,生成一個(gè)新的so叫 push_rtmp_handle ,其它跟之前一樣戏羽。

# 添加 define  -DNO_CRYPTO担神,不然rtmp里面會報(bào)錯找不到 openssl
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DNO_CRYPTO")

AUX_SOURCE_DIRECTORY(${CMAKE_SOURCE_DIR}/src/main/cpp/push_rtmp PUSH_RTMP_SRC_LIST)
AUX_SOURCE_DIRECTORY(${CMAKE_SOURCE_DIR}/src/main/cpp/push_rtmp/librtmp RTMP_LIB_LIST)

add_library(
        # 編譯生成的庫的名稱叫 push_handle,對應(yīng)System.loadLibrary("push_handle");
        

target_link_libraries(
        push_rtmp_handle
        # 編解碼(最重要的庫)
        avcodec-57
        # 設(shè)備信息
        avdevice-57
        # 濾鏡特效處理庫
        avfilter-6
        # 封裝格式處理庫
        avformat-57
        # 工具庫(大部分庫都需要這個(gè)庫的支持)
        avutil-55
        # 后期處理
        postproc-54
        # 音頻采樣數(shù)據(jù)格式轉(zhuǎn)換庫
        swresample-2
        # 視頻像素?cái)?shù)據(jù)格式轉(zhuǎn)換
        swscale-4
        # 鏈接 android ndk 自帶的一些庫
        android
        # Links the target library to the log library
        # included in the NDK.
        # 鏈接 OpenSLES
        OpenSLES
        log)
        # Sets the library as a shared library.
        SHARED
        # Provides a relative path to your source file(s).
        ${PUSH_RTMP_SRC_LIST}
        ${RTMP_LIB_LIST}
)

三始花、Java層直播推流管理類 LivePushHandle

/**
 * 直播推流管理類
 */
public class LivePushHandle {

    static {
        System.loadLibrary("push_rtmp_handle");
    }

    /**
     * 主線程的 handler
     */
    private static Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());

    //默認(rèn)推流地址
    private String mLiveUrl = "rtmp://192.168.43.144:1935/test/live";

    public LivePushHandle() {
    }
    public LivePushHandle(String liveUrl) {
        this.mLiveUrl = liveUrl;
    }


    /**
     * 初始化連接
     */
    public void initConnect(){
        nInitConnect(mLiveUrl);
    }

    public void stop() {
        MAIN_HANDLER.post(new Runnable() {
            @Override
            public void run() {
                nStop();
            }
        });
    }

    //1.初始化連接
    private native void nInitConnect(String liveUrl);

    //2.推sps和pps妄讯,關(guān)鍵幀中的數(shù)據(jù)
    public native void pushSpsPps(byte[] spsData, int spsLen, byte[] ppsData, int ppsLen);

    //3.推送每一幀視頻
    public native void pushVideo(byte[] videoData, int dataLen, boolean keyFrame);

    //4.推送每一幀音頻
    public native void pushAudio(byte[] audioData, int dataLen);

    //5.停止推送
    private native void nStop();


    /**回調(diào)*/
    private ConnectListener mConnectListener;

    public void setOnConnectListener(ConnectListener connectListener) {
        this.mConnectListener = connectListener;
    }
    
    public interface ConnectListener{
        void connectError(int errorCode, String errorMsg);
        void connectSuccess();
        void onInfo(long pts, long dts, long duration, long index);
    }
    
    // 連接的回調(diào) called from jni
    private void onConnectError(int errorCode, String errorMsg){
        stop();
        if(mConnectListener != null){
            mConnectListener.connectError(errorCode,errorMsg);
        }
    }
    // 連接的回調(diào) called from jni
    private void onConnectSuccess(){
        if(mConnectListener != null){
            mConnectListener.connectSuccess();
        }
    }

    // 推流每一幀信息回調(diào) called from jni
    private void onInfo(long pts, long dts, long duration, long index) {
        if (mConnectListener != null) {
            mConnectListener.onInfo(pts, dts, duration, index);
        }
    }

}

四、JNI層實(shí)現(xiàn)方法

RtmpPushHandle.cpp酷宵,主要是做分發(fā)亥贸,代碼比較清晰

#include <jni.h>
#include "PushJniCall.h"
#include "PushStatus.h"
#include "LivePush.h"

//ffmpeg 是c寫的,要用c的include
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
//引入時(shí)間
#include "libavutil/time.h"
};

#include <iostream>

using namespace std;

//JNI回調(diào)處理忧吟,跟上一篇差不多砌函,可以自己按需修改
PushJniCall *pJniCall;
//推流的幾個(gè)方法封裝
LivePush *pLivePush;
//狀態(tài)處理,跟上一篇一樣
PushStatus *pushStatus;

JavaVM *pJavaVM = NULL;


// 重寫 so 被加載時(shí)會調(diào)用的一個(gè)方法,動態(tài)注冊了解一下
extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *javaVM, void *reserved) {
    pJavaVM = javaVM;
    JNIEnv *env;
    if (javaVM->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }
    return JNI_VERSION_1_6;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_lanshifu_ffmpegdemo_push_1live_LivePushHandle_nInitConnect(JNIEnv *env, jobject instance,
                                                                    jstring liveUrl_) {
    const char *liveUrl = env->GetStringUTFChars(liveUrl_, 0);
    LOGD("開始連接...");

    pJniCall = new PushJniCall(pJavaVM, env, instance);
    pLivePush = new LivePush(liveUrl, pJniCall);
    pLivePush->initConnect();

    env->ReleaseStringUTFChars(liveUrl_, liveUrl);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_lanshifu_ffmpegdemo_push_1live_LivePushHandle_pushSpsPps(JNIEnv *env, jobject instance,
                                                                  jbyteArray spsData_, jint spsLen,
                                                                  jbyteArray ppsData_,
                                                                  jint ppsLen) {
    jbyte *spsData = env->GetByteArrayElements(spsData_, NULL);
    jbyte *ppsData = env->GetByteArrayElements(ppsData_, NULL);

    LOGD("推sps和pps");
    if (pLivePush != NULL) {
        pLivePush->pushSpsPps(spsData, spsLen, ppsData, ppsLen);
    }

    env->ReleaseByteArrayElements(spsData_, spsData, 0);
    env->ReleaseByteArrayElements(ppsData_, ppsData, 0);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_lanshifu_ffmpegdemo_push_1live_LivePushHandle_pushVideo(JNIEnv *env, jobject instance,
                                                                 jbyteArray videoData_,
                                                                 jint dataLen, jboolean keyFrame) {
    jbyte *videoData = env->GetByteArrayElements(videoData_, NULL);

    //調(diào)用推視頻函數(shù)
    if (pLivePush != NULL) {
        pLivePush->pushVideo(videoData, dataLen, keyFrame);
    }

    env->ReleaseByteArrayElements(videoData_, videoData, 0);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_lanshifu_ffmpegdemo_push_1live_LivePushHandle_pushAudio(JNIEnv *env, jobject instance,
                                                                 jbyteArray audioData_,
                                                                 jint dataLen) {
    jbyte *audioData = env->GetByteArrayElements(audioData_, NULL);

    //調(diào)用推音頻函數(shù)
    if (pLivePush != NULL) {
        pLivePush->pushAudio(audioData, dataLen);
    }

    env->ReleaseByteArrayElements(audioData_, audioData, 0);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_lanshifu_ffmpegdemo_push_1live_LivePushHandle_nStop(JNIEnv *env, jobject instance) {

    LOGD("停止推流");
    if (pLivePush != NULL) {
        pLivePush->stop();
        delete (pLivePush);
        pLivePush = NULL;
    }

    if (pJniCall != NULL) {
        delete (pJniCall);
        pJniCall = NULL;
    }

}

上面并沒有真正去推流溜族,推流相關(guān)的操作封裝在LivePush中

LivePush.h 如下

#ifndef _LIVEPUSH_H
#define _LIVEPUSH_H

#include "PushJniCall.h"
#include "PacketQueue.h"
#include <malloc.h>
#include <string.h>

extern "C" {
#include "librtmp/rtmp.h"
}

class LivePush {
public:
    PushJniCall *pJniCall = NULL;
    char *liveUrl = NULL;
    PacketQueue *pPacketQueue;
    RTMP *pRtmp = NULL;
    bool isPushing = true;
    uint32_t startTime;
    pthread_t initConnectTid; //初始化連接的線程id
public:
    LivePush(const char *liveUrl, PushJniCall *pJniCall);

    ~LivePush();

    void initConnect();

    void pushSpsPps(jbyte *spsData, jint spsLen, jbyte *ppsData, jint ppsLen);


    void pushVideo(jbyte *videoData, jint dataLen, jboolean keyFrame);


    void pushAudio(jbyte *audioData, jint dataLen);

    void stop();
};


#endif //_LIVEPUSH_H

PushJniCall :封裝了回調(diào)Java的方法
PacketQueue :是一個(gè)存放RTMPPacket的隊(duì)列

采用生產(chǎn)者消費(fèi)者模式
消費(fèi)者:連接建立之后不斷從隊(duì)列中取出RTMPPacket讹俊,然后調(diào)用RTMPdump推流函數(shù),隊(duì)列空就阻塞煌抒。
生產(chǎn)者:App傳過來的流封裝成RTMPPacket仍劈,然后放到隊(duì)列去,喚醒消費(fèi)者

接下來介紹如何將音視頻幀數(shù)據(jù)封裝成RTMPPacket

五寡壮、推流步驟

5.1 初始化連接流媒體服務(wù)器

void *initConnectFun(void *context) {

    LivePush *pLivePush = (LivePush *)context;
    // 1. 創(chuàng)建 RTMP
    pLivePush->pRtmp = RTMP_Alloc();
    // 2. 初始化
    RTMP_Init(pLivePush->pRtmp);
    // 3. 設(shè)置參數(shù)贩疙,連接的超時(shí)時(shí)間等
    pLivePush->pRtmp->Link.timeout = 5;
    pLivePush->pRtmp->Link.lFlags |= RTMP_LF_LIVE;
    RTMP_SetupURL(pLivePush->pRtmp, pLivePush->liveUrl);
    RTMP_EnableWrite(pLivePush->pRtmp);
    // 開始連接
    if (!RTMP_Connect(pLivePush->pRtmp, NULL)) {
        // 回調(diào)到 java 層,這個(gè)錯誤一般是手機(jī)沒網(wǎng)絡(luò)况既,或者服務(wù)器沒打開
        LOGE("rtmp connect error,url = %s",pLivePush->liveUrl);
        pLivePush->pJniCall->callConnectError(THREAD_CHILD, INIT_RTMP_CONNECT_ERROR_CODE,
                                              "rtmp connect error");
        return (void *) INIT_RTMP_CONNECT_ERROR_CODE;
    }

    if (!RTMP_ConnectStream(pLivePush->pRtmp, 0)) {
        // 回調(diào)到 java 層
        LOGE("rtmp connect stream error");
        pLivePush->pJniCall->callConnectError(THREAD_CHILD, INIT_RTMP_CONNECT_STREAM_ERROR_CODE,
                                              "rtmp connect stream error");
        return (void *) INIT_RTMP_CONNECT_STREAM_ERROR_CODE;
    }
    LOGW("rtmp 連接成功这溅,回調(diào)給java層");
    pLivePush->pJniCall->callConnectSuccess(THREAD_CHILD);
    pLivePush->startTime = RTMP_GetTime();
    while (pLivePush->isPushing) {
        // 從隊(duì)列讀,不斷的往流媒體服務(wù)器上推(生產(chǎn)者消費(fèi)者模式)
        RTMPPacket *pPacket = pLivePush->pPacketQueue->pop();
        if (pPacket != NULL) {
            RTMP_SendPacket(pLivePush->pRtmp, pPacket, 1);
            RTMPPacket_Free(pPacket);
            free(pPacket);
        }
    }

    LOGE("推流結(jié)束棒仍,線程停止了");
    return 0;
}

集成RTMPDump源碼之后悲靴,就按照RTMP協(xié)議,先連接流媒體服務(wù)器莫其,連接失敗回調(diào)給Java層癞尚,連接成功則進(jìn)入循環(huán)耸三,從隊(duì)列讀RTMPPacket,然后往流媒體服務(wù)器上推浇揩。這里要能理解生產(chǎn)者消費(fèi)者模式仪壮。

生產(chǎn)者消費(fèi)者模式
消費(fèi)者線程:連接推流服務(wù)器是單獨(dú)一個(gè)線程,連接成功之后不斷從隊(duì)列拿數(shù)據(jù)進(jìn)行消費(fèi)胳徽,讀不到就等待积锅,需要生產(chǎn)者喚醒才繼續(xù)。
生產(chǎn)者線程:將編碼后的數(shù)據(jù)放入隊(duì)列膜廊,然后喚醒消費(fèi)者線程

5.2 推送視頻流

視頻數(shù)據(jù)是通過攝像頭采集(NV21格式)乏沸,在通過MediaCodec編碼(H264/avc格式)淫茵,然后傳到native層爪瓜,native層再將數(shù)據(jù)轉(zhuǎn)換成RTMPDump要求的格式,然后進(jìn)行推流匙瘪。

H264 可以分為兩層:
1.VCL video codinglayer(視頻編碼層)铆铆,
2.NAL network abstraction layer(網(wǎng)絡(luò)提取層)。
這里我們要關(guān)注的是 NAL 層丹喻,即網(wǎng)絡(luò)提取層薄货,這是解碼的基礎(chǔ)。


NAL

H264編碼格式涉及到I幀碍论、P幀谅猾、B幀、SPS鳍悠、PPS是什么意思呢税娜?
SPS:序列參數(shù)集,作用于一系列連續(xù)編碼圖像
PPS:圖像參數(shù)集藏研,作用于編碼視頻序列中一個(gè)或多個(gè)圖像
I幀:幀內(nèi)編碼幀敬矩,可獨(dú)立解碼生成完整的圖片映皆。
P幀: 前向預(yù)測編碼幀栖秕,需要參考其前面的一個(gè)I 或者B 來生成一張完整的圖片。
B幀: 雙向預(yù)測內(nèi)插編碼幀几晤,則要參考其前一個(gè)I或者P幀及其后面的一個(gè)P幀來生成一張完整的圖片

5.2.1 推送SPS和PPS

為了確保直播過程中進(jìn)來的用戶也可以正常的觀看直播业踏,我們需要在每個(gè)關(guān)鍵幀前先把 SPS 和 PPS 推送到流媒體服務(wù)器禽炬。

void LivePush::pushSpsPps(jbyte *spsData, jint spsLen, jbyte *ppsData, jint ppsLen) {
    // frame type : 1關(guān)鍵幀,2 非關(guān)鍵幀 (4bit)
    // CodecID : 7表示 AVC (4bit)  , 與 frame type 組合起來剛好是 1 個(gè)字節(jié)  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   包長數(shù)據(jù)所使用的字節(jié)數(shù)勤家,傳最大

    // sps + pps 的數(shù)據(jù)
    // sps number            (1byte)  0xe1   sps 個(gè)數(shù)
    // sps data length       (2byte)  sps 長度
    // sps data                       sps 的內(nèi)容
    // pps number            (1byte)  0x01   pps 個(gè)數(shù)
    // pps data length       (2byte)  pps 長度
    // pps data                       pps 的內(nèi)容

    // 數(shù)據(jù)的長度(大懈辜狻) = sps 大小 + pps 大小 + 16字節(jié)
    int bodySize = spsLen + ppsLen + 16;
    // 構(gòu)建 RTMPPacket
    RTMPPacket *pPacket = (RTMPPacket *) malloc(sizeof(RTMPPacket));
    RTMPPacket_Alloc(pPacket, bodySize);
    RTMPPacket_Reset(pPacket);

    // 構(gòu)建 body 按上面的一個(gè)一個(gè)開始賦值
    char *body = pPacket->m_body;
    int index = 0;
    // CodecID : 7表示 AVC (4bit)  , 與 frame type 組合起來剛好是 1 個(gè)字節(jié)  0x17
    body[index++] = 0x17;
    // fixed : 0x00 0x00 0x00 0x00 (4byte)
    body[index++] = 0x00;
    body[index++] = 0x00;
    body[index++] = 0x00;
    body[index++] = 0x00;
    // configurationVersion  (1byte)  0x01版本
    body[index++] = 0x01;
    // AVCProfileIndication  (1byte)  sps[1] profile
    body[index++] = spsData[1];  ///sps第1個(gè)字節(jié)
    // profile_compatibility (1byte)  sps[2] compatibility
    body[index++] = spsData[2];  ///sps第2個(gè)字節(jié)
    // AVCLevelIndication    (1byte)  sps[3] Profile level
    body[index++] = spsData[3];  /// ///sps第3個(gè)字節(jié)
    // lengthSizeMinusOne    (1byte)  0xff   包長數(shù)據(jù)所使用的字節(jié)數(shù)
    body[index++] = 0xff;
    // sps + pps 的數(shù)據(jù)
    // sps number            (1byte)  0xe1   sps 個(gè)數(shù)
    body[index++] = 0xe1;
    // sps data length       (2byte)  sps 長度
    body[index++] = (spsLen >> 8) & 0xFF; ///sps長度用兩個(gè)字節(jié)表示,第一個(gè)字節(jié)表示高八位却紧,256 -> 0000 0001 0000 0000 右移8位 -> 0000 0001
    body[index++] = spsLen & 0xFF; ///第二個(gè)字節(jié)放低八位桐臊,比如256胎撤,如果只放一個(gè)字節(jié),前面的1會被干掉断凶,變成 0000 0000
    // sps data                       sps 的內(nèi)容
    memcpy(&body[index], spsData, spsLen);  ///拷貝sps到body
    index += spsLen;
    // pps number            (1byte)  0x01   pps 個(gè)數(shù)
    body[index++] = 0x01;
    // pps data length       (2byte)  pps 長度
    body[index++] = (ppsLen >> 8) & 0xFF;
    body[index++] = ppsLen & 0xFF;
    // pps data                       pps 的內(nèi)容
    memcpy(&body[index], ppsData, ppsLen); ///拷貝pps到body

    // RTMPPacket 設(shè)置一些信息
    pPacket->m_hasAbsTimestamp = 0;
    pPacket->m_nTimeStamp = 0;
    pPacket->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
    pPacket->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    pPacket->m_nBodySize = bodySize;
    pPacket->m_nChannel = 0x04;
    pPacket->m_nInfoField2 = this->pRtmp->m_stream_id;

    pPacketQueue->push(pPacket);
}

封裝 RTMPPacket 數(shù)據(jù)伤提,一個(gè)RTMPPacket對應(yīng)RTMP協(xié)議規(guī)范里面的一個(gè)塊(Chunk),pPacket->m_body 中的每個(gè)字節(jié)有不同意思认烁,其實(shí)就是一種規(guī)范肿男,按照規(guī)范來就對了,SPS和PPS的封裝看起來有點(diǎn)小復(fù)雜却嗡,慢慢理解即可舶沛。

5.2.2 推送視頻幀
void LivePush::pushVideo(jbyte *videoData, jint dataLen, jboolean keyFrame) {
    // frame type : 1關(guān)鍵幀,2 非關(guān)鍵幀 (4bit)
    // CodecID : 7表示 AVC (4bit)  , 與 frame type 組合起來剛好是 1 個(gè)字節(jié)  0x17
    // fixed : 0x01 0x00 0x00 0x00 (4byte)  0x01  表示 NALU 單元

    // video data length       (4byte)  video 長度
    // video data
    // 數(shù)據(jù)的長度(大写凹邸) =  dataLen + 9
    int bodySize = dataLen + 9;
    // 構(gòu)建 RTMPPacket
    RTMPPacket *pPacket = (RTMPPacket *) malloc(sizeof(RTMPPacket));
    RTMPPacket_Alloc(pPacket, bodySize);
    RTMPPacket_Reset(pPacket);

    // 構(gòu)建 body 按上面的一個(gè)一個(gè)開始賦值
    char *body = pPacket->m_body;
    int index = 0;
    // frame type : 1關(guān)鍵幀如庭,2 非關(guān)鍵幀 (4bit)
    // CodecID : 7表示 AVC (4bit)  , 與 frame type 組合起來剛好是 1 個(gè)字節(jié)  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;

    // video data length       (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 設(shè)置一些信息
    pPacket->m_headerType = RTMP_PACKET_SIZE_LARGE;
    pPacket->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    pPacket->m_hasAbsTimestamp = 0;
    pPacket->m_nTimeStamp = RTMP_GetTime() - startTime; //時(shí)間戳
    pPacket->m_nBodySize = bodySize;
    pPacket->m_nChannel = 0x04;
    pPacket->m_nInfoField2 = this->pRtmp->m_stream_id;

    pPacketQueue->push(pPacket);
}

推送視頻幀(H264編碼)比推送SPS和PPS要簡單一些。
AVC是H.264編碼的mime類型撼港,
在Java層坪它,判斷是關(guān)鍵幀,要在關(guān)鍵幀之前先推SPS和PPS

5.2 推送音頻數(shù)據(jù)

void LivePush::pushAudio(jbyte *audioData, jint dataLen) {
// 2 字節(jié)頭信息
    // 前四位表示音頻數(shù)據(jù)格式 AAC  10  ->  1010  ->  A
    // 五六位表示采樣率 0 = 5.5k  1 = 11k  2 = 22k  3(11) = 44k
    // 七位表示采樣采樣的精度 0 = 8bits  1 = 16bits
    // 八位表示音頻類型  0 = mono  1 = stereo
    // 我們這里算出來第一個(gè)字節(jié)是 0xAF   1010   11 11

    // 數(shù)據(jù)的長度(大械勰怠) =  dataLen + 2
    int bodySize = dataLen + 2;
    // 構(gòu)建 RTMPPacket
    RTMPPacket *pPacket = (RTMPPacket *) malloc(sizeof(RTMPPacket));
    RTMPPacket_Alloc(pPacket, bodySize);
    RTMPPacket_Reset(pPacket);

    // 構(gòu)建 body 按上面的一個(gè)一個(gè)開始賦值
    char *body = pPacket->m_body;
    int index = 0;
    // 我們這里算出來第一個(gè)字節(jié)是 0xAF
    body[index++] = 0xAF;
    // 0x01 代表 aac 原始數(shù)據(jù)
    body[index++] = 0x01;
    // audio data
    memcpy(&body[index], audioData, dataLen);

    // RTMPPacket 設(shè)置一些信息
    pPacket->m_headerType = RTMP_PACKET_SIZE_LARGE;
    pPacket->m_packetType = RTMP_PACKET_TYPE_AUDIO;
    pPacket->m_hasAbsTimestamp = 0;
    pPacket->m_nTimeStamp = RTMP_GetTime() - startTime;
    pPacket->m_nBodySize = bodySize;
    pPacket->m_nChannel = 0x04;
    pPacket->m_nInfoField2 = this->pRtmp->m_stream_id;

    pPacketQueue->push(pPacket);
}

音頻幀(AAC編碼)的推流也是比較簡單往毡,m_packetType 不同,其它跟視頻的類似靶溜。

六开瞭、App層調(diào)用推流方法

上面基本把RTMPDump的使用介紹了,基礎(chǔ)就是這些罩息,實(shí)際開發(fā)中更多的應(yīng)該是處理視頻流嗤详,添加濾鏡、美顏效果等扣汪,然后再編碼成H264格式断楷,然后推流。

這里基于上一篇的基礎(chǔ)上添加推流功能崭别。
《Android音視頻系列-5》音視頻采集冬筒,生成mp4
只貼出需要改動的地方,不保證代碼的簡潔茅主。

需要改動的地方如下


編碼管理類舞痰、音頻編碼線程、視頻編碼線程

1诀姚、編碼管理類修改

創(chuàng)建 LivePushHandle

public LivePushHandle mLivePush = new LivePushHandle();

添加開始/結(jié)束推流方法

    public void startPush(){
        mLivePush.setOnConnectListener(new LivePushHandle.ConnectListener() {
            @Override
            public void connectError(int errorCode, String errorMsg) {
                Log.d(TAG, "connectError: ");
            }

            @Override
            public void connectSuccess() {
                Log.d(TAG, "connectSuccess: ");
                startEncode();
            }

            @Override
            public void onInfo(long pts, long dts, long duration, long index) {

            }
        });
        mLivePush.initConnect();
    }

    public void stopPush(){
        mLivePush.stop();
    }

收到連接成功的回調(diào)才去開啟編碼線程 startEncode();

2. 視頻編碼線程

創(chuàng)建兩個(gè)變量响牛,sps和pps

    public byte[] mVideoSps, mVideoPps;

if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {這個(gè)判斷里,獲取sps和pps

                ...
                mMediaEncodeManager.startMediaMuxer();

                // 推流要獲取 sps 和 pps。 ”csd-0” (sps) 呀打,”csd-1”(pps)
                ByteBuffer byteBuffer = videoCodec.getOutputFormat().getByteBuffer("csd-0");
                mVideoSps = new byte[byteBuffer.remaining()];
                byteBuffer.get(mVideoSps, 0, mVideoSps.length);
                byteBuffer = videoCodec.getOutputFormat().getByteBuffer("csd-1");
                mVideoPps = new byte[byteBuffer.remaining()];
                byteBuffer.get(mVideoPps, 0, mVideoPps.length);
                Log.d(TAG, " 成功獲取sps和pps ");

在寫入混合器的之后矢赁,加入推流邏輯

                    ...
                    mediaMuxer.writeSampleData(mVideoTrackIndex, outputBuffer, bufferInfo);

                    //1、 在關(guān)鍵幀前先把 sps 和 pps 推到流媒體服務(wù)器
                    if (bufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME) {
                        mMediaEncodeManager.mLivePush.pushSpsPps(mVideoSps,
                                mVideoSps.length, mVideoPps, mVideoPps.length);
                        Log.d(TAG, "推送關(guān)鍵幀sps和pps");
                    }

                    //2贬丛、推送每一幀
                    byte[] data = new byte[outputBuffer.remaining()];
                    outputBuffer.get(data, 0, data.length);
                    mMediaEncodeManager.mLivePush.pushVideo(data, data.length,
                            bufferInfo.flags == MediaCodec.BUFFER_FLAG_KEY_FRAME);

視頻編碼線程添加的代碼就這些

3. 音頻編碼線程

在寫入混合器的后面推音頻流

                    ...
                    mediaMuxer.writeSampleData(mAudioTrackIndex, outputBuffer, bufferInfo);

                    byte[] data = new byte[outputBuffer.remaining()];
                    outputBuffer.get(data, 0, data.length);
                    mMediaEncodeManager.mLivePush.pushAudio(data,data.length);


總結(jié)

到此撩银,這個(gè)流程就打通了,效果就不演示了豺憔,流程總結(jié)如下:

  1. 連接流媒體服務(wù)器额获,不斷從隊(duì)列讀取封裝好的數(shù)據(jù),推流恭应。
  2. 視頻流來源:通過采集攝像頭數(shù)據(jù)-編碼成H264格式(avc)抄邀,然后調(diào)用通過RTMPDump開源工具,將每一幀數(shù)據(jù)封裝成FLV格式昼榛,放到隊(duì)列中去境肾。
  3. 音頻流來源:通過AudioTrack采集音頻PCM數(shù)據(jù)-編碼成aac格式,然后通過通過RTMPDump褒纲,封裝成FLV格式放到隊(duì)列去准夷。

todo:
視頻數(shù)據(jù)是通過攝像頭+OpenGL渲染出來的,所以濾鏡莺掠、美顏等效果可以通過修改著色器代碼來實(shí)現(xiàn),之前OpenGL系列文章有介紹過濾鏡的實(shí)現(xiàn)读宙,可以拿過來用的彻秆。


參考:
Android客戶端音視頻推流
FFmpeg - Android 直播推拉流
RTMPdump源碼分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市结闸,隨后出現(xiàn)的幾起案子唇兑,更是在濱河造成了極大的恐慌,老刑警劉巖桦锄,帶你破解...
    沈念sama閱讀 212,029評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扎附,死亡現(xiàn)場離奇詭異,居然都是意外死亡结耀,警方通過查閱死者的電腦和手機(jī)留夜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,395評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來图甜,“玉大人碍粥,你說我怎么就攤上這事『谝悖” “怎么了嚼摩?”我有些...
    開封第一講書人閱讀 157,570評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我枕面,道長愿卒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,535評論 1 284
  • 正文 為了忘掉前任潮秘,我火速辦了婚禮掘猿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘唇跨。我一直安慰自己稠通,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,650評論 6 386
  • 文/花漫 我一把揭開白布买猖。 她就那樣靜靜地躺著改橘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪玉控。 梳的紋絲不亂的頭發(fā)上飞主,一...
    開封第一講書人閱讀 49,850評論 1 290
  • 那天,我揣著相機(jī)與錄音高诺,去河邊找鬼碌识。 笑死,一個(gè)胖子當(dāng)著我的面吹牛虱而,可吹牛的內(nèi)容都是我干的筏餐。 我是一名探鬼主播,決...
    沈念sama閱讀 39,006評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼牡拇,長吁一口氣:“原來是場噩夢啊……” “哼魁瞪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起惠呼,我...
    開封第一講書人閱讀 37,747評論 0 268
  • 序言:老撾萬榮一對情侶失蹤导俘,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后剔蹋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體旅薄,經(jīng)...
    沈念sama閱讀 44,207評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,536評論 2 327
  • 正文 我和宋清朗相戀三年泣崩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了少梁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,683評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡律想,死狀恐怖猎莲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情技即,我是刑警寧澤著洼,帶...
    沈念sama閱讀 34,342評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響身笤,放射性物質(zhì)發(fā)生泄漏豹悬。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,964評論 3 315
  • 文/蒙蒙 一液荸、第九天 我趴在偏房一處隱蔽的房頂上張望瞻佛。 院中可真熱鬧,春花似錦娇钱、人聲如沸伤柄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,772評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽适刀。三九已至,卻和暖如春煤蹭,著一層夾襖步出監(jiān)牢的瞬間笔喉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,004評論 1 266
  • 我被黑心中介騙來泰國打工硝皂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留常挚,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,401評論 2 360
  • 正文 我出身青樓稽物,卻偏偏與公主長得像奄毡,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子姨裸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,566評論 2 349

推薦閱讀更多精彩內(nèi)容