【Android RTMP】RTMPDump 推流過程 ( 獨立線程推流 | 創建推流器 | 初始化操作 | 設置推流地址 | 啓用寫出 | 連接 RTMP 服務器 | 發送 RTMP 數據包 )





安卓直播推流專欄博客總結



Android RTMP 直播推流技術專欄 :


0 . 資源和源碼地址 :


1. 搭建 RTMP 服務器 : 下面的博客中講解了如何在 VMWare 虛擬機中搭建 RTMP 直播推流服務器 ;

2. 準備視頻編碼的 x264 編碼器開源庫 , 和 RTMP 數據包封裝開源庫 :

3. 講解 RTMP 數據包封裝格式 :

4. 圖像數據採集 : 從 Camera 攝像頭中採集 NV21 格式的圖像數據 , 並預覽該數據 ;

5. NV21 格式的圖像數據編碼成 H.264 格式的視頻數據 :

6. 將 H.264 格式的視頻數據封裝到 RTMP 數據包中 :

7. 階段總結 : 阿里雲服務器中搭建 RTMP 服務器 , 並使用電腦軟件推流和觀看直播內容 ;

8. 處理 Camera 圖像傳感器導致的 NV21 格式圖像旋轉問題 :

9. 下面這篇博客比較重要 , 裏面有一個快速搭建 RTMP 服務器的腳本 , 強烈建議使用 ;

10. 編碼 AAC 音頻數據的開源庫 FAAC 交叉編譯與 Android Studio 環境搭建 :

11. 解析 AAC 音頻格式 :

12 . 將麥克風採集的 PCM 音頻採樣編碼成 AAC 格式音頻 , 並封裝到 RTMP 包中 , 推流到客戶端 :






Android 直播推流流程 : 手機採集視頻 / 音頻數據 , 視頻數據使用 H.264 編碼 , 音頻數據使用 AAC 編碼 , 最後將音視頻數據都打包到 RTMP 數據包中 , 使用 RTMP 協議上傳到 RTMP 服務器中 ;


Android 端中主要完成手機端採集視頻數據操作 , 並將視頻數據傳遞給 JNI , 在 NDK 中使用 x264 將圖像轉爲 H.264 格式的視頻 , 最後將 H.264 格式的視頻打包到 RTMP 數據包中 , 上傳到 RTMP 服務器中 ;


本篇博客中將介紹 , 使用 RTMPDump 開源庫 , 將編碼好的 RTMP 數據包 , 推送到遠程 RTMP 服務器 ; 即 RTMPDump 推流過程 ;





一、 Java 層傳入的 RTMP 推流地址處理



1 . Java 傳遞字符串數據到 JNI : 啓動推流時 , Java 層會將 RTMP 推流地址傳遞給 JNI ;


2 . jstring 類型轉爲 char* 類型 : 將 Java 字符串轉爲 C 字符串 ;

// 獲取 Rtmp 推流地址
// 該 pushPathFromJava 引用是局部引用, 超過作用域就無效了
// 局部引用不能跨方法 , 跨線程調用
const char* pushPathFromJava = env->GetStringUTFChars(path, 0);

3 . 局部引用變量處理 : 該轉換後的 const char* pushPathFromJava 字符串是局部引用變量 , 不能跨進程 , 跨作用域使用 , 之後的推流操作在獨立的線程中使用 , 因此需要將字符串數據在堆內存中存儲 ;

// 獲取地址的長度, 加上 '\0' 長度
char * pushPathNative = new char[strlen(pushPathFromJava) + 1];
// 拷貝 pushPathFromJava 到堆內存 pushPathNative 中
// 局部引用不能跨方法 , 跨線程調用, 這裏需要在線程中使用該地址
// 因此需要將該局部引用拷貝到堆內存中, 然後傳遞到對應線程中
strcpy(pushPathNative, pushPathFromJava);

4 . 釋放局部引用 : JNI 中的局部引用變量 , 使用完畢後及時釋放 ;

// 釋放從 Java 層獲取的字符串
// 釋放局部引用
env->ReleaseStringUTFChars(path, pushPathFromJava);




二、 RTMPDump 推流線程



1 . 獨立線程推流 : RTMP 推流操作需要在一個獨立的線程中完成 , 涉及到網絡的操作都是耗時操作 , 在 Android 中都要在線程中執行 ;


2 . 線程 ID 聲明 : 需要導入 #include <pthread.h> 包 , 之後才能使用線程 , 先聲明線程 ID , pthread_t 類型 ;

/**
 * 開始推流工作線程的線程 ID
 */
pthread_t startRtmpPushPid;

3 . 創建並執行線程 : 創建並執行線程 , 在線程中執行 startRtmpPush 方法 , 傳入 pushPathNative 字符串參數 ;

// 創建線程
pthread_create(&startRtmpPushPid, 0, startRtmpPush, pushPathNative);

4 . 線程方法 : 定義線程方法 , 參數和返回值都是 void* 類型 , 在開始位置獲取傳入的參數 ;

void* startRtmpPush (void* args){
    // 0. 獲取 Rtmp 推流地址
    char* pushPath = static_cast<char *>(args);
	// ...
}




三、 創建 RTMP 對象



創建 RTMP 對象 , 如果創建失敗 , 直接停止整個推流方法 ;

// 1. 創建 RTMP 對象, 申請內存
rtmp = RTMP_Alloc();
if (!rtmp) {
    __android_log_print(ANDROID_LOG_INFO, "RTMP", "申請 RTMP 內存失敗");
    break;
}




四、 初始化 RTMP 對象



初始化 RTMP 對象 , 並設置超時時間 ;

// 2. 初始化 RTMP
RTMP_Init(rtmp);
// 設置超時時間 5 秒
rtmp->Link.timeout = 5;




五、 設置 RTMP 推流地址



設置 RTMP 推流地址 , 如果設置失敗 , 直接退出推流操作 ;

該地址就是 Java 層傳給 JNI 的字符串 , 剛獲取時是局部引用變量 , 將其拷貝到了堆內存中 , 纔可以在推流線程中使用 ;

// 3. 設置 RTMP 推流服務器地址
int ret = RTMP_SetupURL(rtmp, pushPath);
if (!ret) {
    __android_log_print(ANDROID_LOG_INFO, "RTMP", "設置 RTMP 推流服務器地址 %s 失敗", pushPath);
    break;
}




六、 啓用 RTMP 寫出功能



啓用 RTMP 寫出功能 ;

// 4. 啓用 RTMP 寫出功能
RTMP_EnableWrite(rtmp);




七、 連接 RTMP 服務器



連接 RTMP 服務器 , 如果連接失敗 , 直接退出該方法 ;

// 5. 連接 RTMP 服務器
ret = RTMP_Connect(rtmp, 0);
if (!ret) {
    __android_log_print(ANDROID_LOG_INFO, "RTMP", "連接 RTMP 服務器 %s 失敗", pushPath);
    break;
}




八、 連接 RTMP 流



連接 RTMP 流 , 如果連接失敗 , 退出方法 ;

// 6. 連接 RTMP 流
ret = RTMP_ConnectStream(rtmp, 0);
if (!ret) {
    __android_log_print(ANDROID_LOG_INFO, "RTMP", "連接 RTMP 流 %s 失敗", pushPath);
    break;
}




九、 發送 RTMP 數據包



將 RTMP 數據包發送到服務器中 ;

// 7. 將 RTMP 數據包發送到服務器中
ret = RTMP_SendPacket(rtmp, packet, 1);




十、 斷開 RTMP 連接並釋放資源



推流結束後 , 關閉與 RTMP 服務器連接 , 釋放資源 ;

// 8. 推流結束, 關閉與 RTMP 服務器連接, 釋放資源
if(rtmp){
    RTMP_Close(rtmp);
    RTMP_Free(rtmp);
}




十一、 RTMPDump 推流代碼



RTMPDump 推流代碼 :

/**
 * 開始向遠程 RTMP 服務器推送數據
 */
extern "C"
JNIEXPORT void JNICALL
Java_kim_hsl_rtmp_LivePusher_native_1startRtmpPush(JNIEnv *env, jobject thiz,
                                                                jstring path) {
    if(isStartRtmpPush){
        // 防止該方法多次調用, 如果之前調用過, 那麼屏蔽本次調用
        return;
    }
    // 執行過一次後, 馬上標記已執行狀態, 下一次就不再執行該方法了
    isStartRtmpPush = TRUE;

    // 獲取 Rtmp 推流地址
    // 該 pushPathFromJava 引用是局部引用, 超過作用域就無效了
    // 局部引用不能跨方法 , 跨線程調用
    const char* pushPathFromJava = env->GetStringUTFChars(path, 0);

    // 獲取地址的長度, 加上 '\0' 長度
    char * pushPathNative = new char[strlen(pushPathFromJava) + 1];
    // 拷貝 pushPathFromJava 到堆內存 pushPathNative 中
    // 局部引用不能跨方法 , 跨線程調用, 這裏需要在線程中使用該地址
    // 因此需要將該局部引用拷貝到堆內存中, 然後傳遞到對應線程中
    strcpy(pushPathNative, pushPathFromJava);

    // 創建線程
    pthread_create(&startRtmpPushPid, 0, startRtmpPush, pushPathNative);

    // 釋放從 Java 層獲取的字符串
    // 釋放局部引用
    env->ReleaseStringUTFChars(path, pushPathFromJava);    
}


/**
 * 開始推流任務線程
 * 主要是調用 RTMPDump 進行推流
 * @param args
 * @return
 */
void* startRtmpPush (void* args){
    // 0. 獲取 Rtmp 推流地址
    char* pushPath = static_cast<char *>(args);

    // rtmp 推流器
    RTMP* rtmp = 0;
    // rtmp 推流數據包
    RTMPPacket *packet = 0;

    /*
        將推流核心執行內容放在 do while 循環中
        在出錯後, 隨時 break 退出循環, 執行後面的釋放資源的代碼
        可以保證, 在最後將資源釋放掉, 避免內存泄漏
        避免執行失敗, 直接 return, 導致資源沒有釋放
     */
    do {
        // 1. 創建 RTMP 對象, 申請內存
        rtmp = RTMP_Alloc();
        if (!rtmp) {
            __android_log_print(ANDROID_LOG_INFO, "RTMP", "申請 RTMP 內存失敗");
            break;
        }

        // 2. 初始化 RTMP
        RTMP_Init(rtmp);
        // 設置超時時間 5 秒
        rtmp->Link.timeout = 5;

        // 3. 設置 RTMP 推流服務器地址
        int ret = RTMP_SetupURL(rtmp, pushPath);
        if (!ret) {
            __android_log_print(ANDROID_LOG_INFO, "RTMP", "設置 RTMP 推流服務器地址 %s 失敗", pushPath);
            break;
        }
        // 4. 啓用 RTMP 寫出功能
        RTMP_EnableWrite(rtmp);

        // 5. 連接 RTMP 服務器
        ret = RTMP_Connect(rtmp, 0);
        if (!ret) {
            __android_log_print(ANDROID_LOG_INFO, "RTMP", "連接 RTMP 服務器 %s 失敗", pushPath);
            break;
        }

        // 6. 連接 RTMP 流
        ret = RTMP_ConnectStream(rtmp, 0);
        if (!ret) {
            __android_log_print(ANDROID_LOG_INFO, "RTMP", "連接 RTMP 流 %s 失敗", pushPath);
            break;
        }

        // 準備推流相關的數據, 如線程安全隊列
        readyForPush = TRUE;
        // 記錄推流開始時間
        pushStartTime = RTMP_GetTime();
        // 線程安全隊列開始工作
        packets.setWork(1);

        while (isStartRtmpPush) {
            // 從線程安全隊列中
            // 取出一包已經打包好的 RTMP 數據包
            packets.pop(packet);

            // 確保當前處於推流狀態
            if (!isStartRtmpPush) {
                break;
            }

            // 確保不會取出空的 RTMP 數據包
            if (!packet) {
                continue;
            }

            // 設置直播的流 ID
            packet->m_nInfoField2 = rtmp->m_stream_id;

            // 7. 將 RTMP 數據包發送到服務器中
            ret = RTMP_SendPacket(rtmp, packet, 1);

            // RTMP 數據包使用完畢後, 釋放該數據包
            if (packet) {
                RTMPPacket_Free(packet);
                delete packet;
                packet = 0;
            }

            if (!ret) {
                __android_log_print(ANDROID_LOG_INFO, "RTMP", "RTMP 數據包推流失敗");
                break;
            }
        }

    }while (0);


    // 面的部分是收尾部分, 釋放資源


    // 8. 推流結束, 關閉與 RTMP 服務器連接, 釋放資源
    if(rtmp){
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
    }

    // 推流數據包 線程安全隊列釋放
    // 防止中途退出導致沒有釋放資源, 造成內存泄漏
    if (packet) {
        RTMPPacket_Free(packet);
        delete packet;
        packet = 0;
    }

    // 釋放推流地址
    if(pushPath){
        delete pushPath;
        pushPath = 0;
    }
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章