文章目錄
安卓直播推流專欄博客總結
0 . 資源和源碼地址 :
- 資源下載地址 : 資源下載地址 , 服務器搭建 , x264 , faac , RTMPDump , 源碼及交叉編譯庫 , 本專欄 Android 直播推流源碼 ;
- GitHub 源碼地址 : han1202012 / RTMP_Pusher
1. 搭建 RTMP 服務器 : 下面的博客中講解了如何在 VMWare 虛擬機中搭建 RTMP 直播推流服務器 ;
2. 準備視頻編碼的 x264 編碼器開源庫 , 和 RTMP 數據包封裝開源庫 :
-
【Android RTMP】RTMPDumb 源碼導入 Android Studio ( 交叉編譯 | 配置 CMakeList.txt 構建腳本 )
-
【Android RTMP】Android Studio 集成 x264 開源庫 ( Ubuntu 交叉編譯 | Android Studio 導入函數庫 )
3. 講解 RTMP 數據包封裝格式 :
4. 圖像數據採集 : 從 Camera 攝像頭中採集 NV21 格式的圖像數據 , 並預覽該數據 ;
-
【Android RTMP】Android Camera 視頻數據採集預覽 ( 視頻採集相關概念 | 攝像頭預覽參數設置 | 攝像頭預覽數據回調接口 )
-
【Android RTMP】Android Camera 視頻數據採集預覽 ( NV21 圖像格式 | I420 圖像格式 | NV21 與 I420 格式對比 | NV21 轉 I420 算法 )
-
【Android RTMP】Android Camera 視頻數據採集預覽 ( 圖像傳感器方向設置 | Camera 使用流程 | 動態權限申請 )
5. NV21 格式的圖像數據編碼成 H.264 格式的視頻數據 :
-
【Android RTMP】x264 編碼器初始化及設置 ( 獲取 x264 編碼參數 | 編碼規格 | 碼率 | 幀率 | B幀個數 | 關鍵幀間隔 | 關鍵幀解碼數據 SPS PPS )
-
【Android RTMP】x264 圖像數據編碼 ( Camera 圖像數據採集 | NV21 圖像數據傳到 Native 處理 | JNI 傳輸字節數組 | 局部引用變量處理 | 線程互斥 )
-
【Android RTMP】x264 圖像數據編碼 ( NV21 格式中的 YUV 數據排列 | Y 灰度數據拷貝 | U 色彩值數據拷貝 | V 飽和度數據拷貝 | 圖像編碼操作 )
6. 將 H.264 格式的視頻數據封裝到 RTMP 數據包中 :
-
【Android RTMP】RTMPDump 封裝 RTMPPacket 數據包 ( 封裝 SPS / PPS 數據包 )
-
【Android RTMP】RTMPDump 封裝 RTMPPacket 數據包 ( 關鍵幀數據格式 | 非關鍵幀數據格式 | x264 編碼後的數據處理 | 封裝 H.264 視頻數據幀 )
-
【Android RTMP】RTMPDump 推流過程 ( 獨立線程推流 | 創建推流器 | 初始化操作 | 設置推流地址 | 啓用寫出 | 連接 RTMP 服務器 | 發送 RTMP 數據包 )
7. 階段總結 : 阿里雲服務器中搭建 RTMP 服務器 , 並使用電腦軟件推流和觀看直播內容 ;
-
【Android RTMP】RTMP 直播推流 ( 阿里雲服務器購買 | 遠程服務器控制 | 搭建 RTMP 服務器 | 服務器配置 | 推流軟件配置 | 直播軟件配置 | 推流直播效果展示 )
-
【Android RTMP】RTMP 直播推流階段總結 ( 服務器端搭建 | Android 手機端編碼推流 | 電腦端觀看直播 | 服務器狀態查看 )
8. 處理 Camera 圖像傳感器導致的 NV21 格式圖像旋轉問題 :
-
【Android RTMP】NV21 圖像旋轉處理 ( 問題描述 | 圖像順時針旋轉 90 度方案 | YUV 圖像旋轉細節 | 手機屏幕旋轉方向 )
-
【Android RTMP】NV21 圖像旋轉處理 ( 圖像旋轉算法 | 後置攝像頭順時針旋轉 90 度 | 前置攝像頭順時針旋轉 90 度 )
9. 下面這篇博客比較重要 , 裏面有一個快速搭建 RTMP 服務器的腳本 , 強烈建議使用 ;
10. 編碼 AAC 音頻數據的開源庫 FAAC 交叉編譯與 Android Studio 環境搭建 :
-
【Android RTMP】音頻數據採集編碼 ( 音頻數據採集編碼 | AAC 高級音頻編碼 | FAAC 編碼器 | Ubuntu 交叉編譯 FAAC 編碼器 )
-
【Android RTMP】音頻數據採集編碼 ( FAAC 頭文件與靜態庫拷貝到 AS | CMakeList.txt 配置 FAAC | AudioRecord 音頻採樣 PCM 格式 )
11. 解析 AAC 音頻格式 :
12 . 將麥克風採集的 PCM 音頻採樣編碼成 AAC 格式音頻 , 並封裝到 RTMP 包中 , 推流到客戶端 :
-
【Android RTMP】音頻數據採集編碼 ( FAAC 音頻編碼參數設置 | FAAC 編碼器創建 | 獲取編碼器參數 | 設置 AAC 編碼規格 | 設置編碼器輸入輸出參數 )
-
【Android RTMP】音頻數據採集編碼 ( FAAC 編碼器編碼 AAC 音頻解碼信息 | 封裝 RTMP 音頻數據頭 | 設置 AAC 音頻數據類型 | 封裝 RTMP 數據包 )
-
【Android RTMP】音頻數據採集編碼 ( FAAC 編碼器編碼 AAC 音頻採樣數據 | 封裝 RTMP 音頻數據頭 | 設置 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;
}