1 前言
1.1 總覽
短文將記錄一個(gè)基本的攝像頭直播APP開發(fā)的全部流程和技術(shù)點(diǎn)惠奸。項(xiàng)目使用x264進(jìn)行視頻數(shù)據(jù)處理茴她,使用FAAC進(jìn)行音頻數(shù)據(jù)處理,使用RTMP協(xié)議進(jìn)行數(shù)據(jù)推流嚼沿,整個(gè)過程的大體如下宣脉。
整個(gè)APP實(shí)現(xiàn)了以下功能:
- 直播前視頻預(yù)覽
- 開始直播车柠、編碼、推流
- 切換攝像頭
- 停止直播
- 退出應(yīng)用
短文將以各個(gè)功能為切入點(diǎn)記錄各個(gè)技術(shù)點(diǎn)。
1.2 CameraX
1.2.1 簡介
CameraX 是一個(gè) Jetpack 支持庫竹祷,旨在幫助您簡化相機(jī)應(yīng)用的開發(fā)工作谈跛。它提供一致且易于使用的 API 界面,適用于大多數(shù) Android 設(shè)備塑陵,并可向后兼容至 Android 5.0(API 級(jí)別 21)感憾。
雖然它利用的是 camera2 的功能,但使用的是更為簡單且基于用例的方法令花,該方法具有生命周期感知能力阻桅。它還解決了設(shè)備兼容性問題,因此您無需在代碼庫中包含設(shè)備專屬代碼兼都。這些功能減少了將相機(jī)功能添加到應(yīng)用時(shí)需要編寫的代碼量嫂沉。
最后,借助 CameraX扮碧,開發(fā)者只需兩行代碼就能利用與預(yù)安裝的相機(jī)應(yīng)用相同的相機(jī)體驗(yàn)和功能趟章。 CameraX Extensions 是可選插件,通過該插件慎王,您可以在支持的設(shè)備上向自己的應(yīng)用中添加人像蚓土、HDR、夜間模式和美顏等效果赖淤。
1.2.2 CameraX基本使用
請參考Google官方的CameraX Demo蜀漆,示例使用Kotlin編寫。
git clone github.com/android/cam…
1.2.3 獲取原始圖片幀數(shù)據(jù)
如何獲取原始圖片幀數(shù)據(jù)漫蛔?
CameraX提供了一個(gè)圖像分析接口:ImageAnalysis.Analyzer
嗜愈,實(shí)現(xiàn)這個(gè)接口需要實(shí)現(xiàn)接口中的analyze()
方法可以獲得一個(gè)ImageProxy
,顯然莽龟,該ImageProxy
是Image
的一個(gè)代理,Image
的所有方法锨天,ImageProxy
都能調(diào)用毯盈。
//Java
package androidx.camera.core;
//import ...
public final class ImageAnalysis extends UseCase {
//...
public interface Analyzer {
/**
* Analyzes an image to produce a result.
*
* <p>This method is called once for each image from the camera, and called at the
* frame rate of the camera. Each analyze call is executed sequentially.
*
* <p>The caller is responsible for ensuring this analysis method can be executed quickly
* enough to prevent stalls in the image acquisition pipeline. Otherwise, newly available
* images will not be acquired and analyzed.
*
* <p>The image passed to this method becomes invalid after this method returns. The caller
* should not store external references to this image, as these references will become
* invalid.
*
* <p>Processing should complete within a single frame time of latency, or the image data
* should be copied out for longer processing. Applications can be skip analyzing a frame
* by having the analyzer return immediately.
*
* @param image The image to analyze
* @param rotationDegrees The rotation which if applied to the image would make it match
* the current target rotation of {@link ImageAnalysis}, expressed in
* degrees in the range {@code [0..360)}.
*/
void analyze(ImageProxy image, int rotationDegrees);
}
//...
}
ImageProxy
有哪些方法呢?
//Java
//獲得一個(gè)裁剪矩形
Rect cropRect = image.getCropRect();
//獲得圖像格式
int format = image.getFormat();
//獲得圖像高度
int height = image.getHeight();
//獲得圖像寬度
int width = image.getWidth();
//獲得圖像
Image image1 = image.getImage();
//獲得圖像信息
ImageInfo imageInfo = image.getImageInfo();
//獲得圖像的平面代理
ImageProxy.PlaneProxy[] planes = image.getPlanes();
//獲得圖像的時(shí)間戳
long timestamp = image.getTimestamp();
復(fù)制代碼
★著重說明病袄!
第一搂赋,根據(jù)官方文檔的介紹,CameraX
生產(chǎn)的圖像數(shù)據(jù)格式為YUV_420_888益缠,因此脑奠,在使用ImageProxy
對象時(shí),如果得到的圖像格式不匹配幅慌,應(yīng)該報(bào)錯(cuò)宋欺;
//Java
if (format != ImageFormat.YUV_420_888) {
//拋出異常
}
復(fù)制代碼
第二,在第一條通過的前提下,ImageProxy.PlaneProxy[]
數(shù)組包含著YUV的Y數(shù)據(jù)齿诞、U數(shù)據(jù)酸休、V數(shù)據(jù),即planes.length
值為3祷杈。
到這里斑司,我們找到了原始圖片的幀數(shù)據(jù),接下來我們需要將Y數(shù)據(jù)但汞、U數(shù)據(jù)宿刮、V數(shù)據(jù)取出來,按照I420格式進(jìn)行排列私蕾。
Why I420?
因?yàn)楂@取的圖像數(shù)據(jù)接下來需要編碼糙置,而在編碼時(shí),一般編碼器接收的待編碼數(shù)據(jù)格式為I420是目。
1.3 YUV_420_888
通過CameraX
生產(chǎn)的圖像數(shù)據(jù)格式為YUV_420_888谤饭,本節(jié)將介紹YUV_420_888中的NV21和I420兩種格式。
1.3.1 YUV_420_888格式
YUV即通過Y懊纳、U和V三個(gè)分量表示顏色空間揉抵,其中Y表示亮度,U和V表示色度嗤疯。( 如果UV數(shù)據(jù)都為0冤今,那么我們將得到一個(gè)黑白的圖像。)
RGB中每個(gè)像素點(diǎn)都有獨(dú)立的R茂缚、G和B三個(gè)顏色分量值戏罢,YUV根據(jù)U和V采樣數(shù)目的不同,分為如YUV444脚囊、YUV422和YUV420等龟糕,而YUV420表示的就是每個(gè)像素點(diǎn)有一個(gè)獨(dú)立的亮度表示,即Y分量悔耘;而色度讲岁,即U和V分量則由每4個(gè)像素點(diǎn)共享一個(gè)。舉例來說衬以,對于4x4的圖片缓艳,在YUV420下,有16個(gè)Y值看峻,4個(gè)U值和4個(gè)V值阶淘。
- YUV420中,每個(gè)像素獨(dú)有一個(gè)Y互妓,每四個(gè)像素共享一個(gè)U溪窒,每四個(gè)像素共享一個(gè)V坤塞;
- YUV420中,Y數(shù)據(jù)有效字節(jié)數(shù)=Height×Width霉猛;
- YUV420中尺锚,U數(shù)據(jù)有效字節(jié)數(shù)=(Height/2)×(Width/2);
- YUV420中惜浅,V數(shù)據(jù)有效字節(jié)數(shù)=(Height/2)×(Width/2)瘫辩;
- YUV420中,每一個(gè)像素點(diǎn)的YUV數(shù)據(jù)示意圖如下坛悉。
★YUV格式認(rèn)為伐厌,圖片是由一個(gè)亮度平面和兩個(gè)顏色平面疊加形成(Y plane+U plane+V plane);
★官方將YUV三個(gè)平面都稱為顏色平面(color plane)裸影,我們一般習(xí)慣將Y plane稱為亮度平面挣轨;
★
ImageProxy
對象調(diào)用getPlanes()
方法即可得到ImageProxy.PlaneProxy[] planes
,planes[0]
是Y plane轩猩,planes[1]
是U plane卷扮,planes[2]
是V plane;
★
ImageProxy.PlaneProxy
對象有三個(gè)方法:
getBuffer()
獲得包含YUV數(shù)據(jù)字節(jié)的ByteBuffer
均践;
getPixelStride()
獲得UV數(shù)據(jù)的存儲(chǔ)方式(詳見1.2.2)晤锹;
getRowStride()
獲得行跨距(詳見1.2.3)。
1.3.2 YUV420下的UV排列順序
YUV420根據(jù)顏色數(shù)據(jù)的存儲(chǔ)順序不同彤委,又分為了多種不同的格式鞭铆,這些格式實(shí)際存儲(chǔ)的信息還是完全一致的。
舉例來說焦影,對于4x4的圖片车遂,在YUV420下,任何格式都有16個(gè)Y值斯辰,4個(gè)U值和4個(gè)V值舶担,不同格式只是Y、U和V的排列順序變化椒涯。
- I420存儲(chǔ)示意圖如下柄沮,U和V是分開的:
- NV21存儲(chǔ)示意圖如下,planes[1]中废岂,UVU;planes[2]中狱意,VUV:
★YUV420是一類格式的集合湖苞,包含I420在內(nèi)。YUV_420_888中的888表示YUV三分量都用8 bits/1 byte表示详囤。YUV420并不能完全確定顏色數(shù)據(jù)(即UV數(shù)據(jù))的存儲(chǔ)順序财骨,因此接下來需要分兩種情況分別處理
planes[1]
和planes[2]
兩個(gè)數(shù)據(jù)镐作,至于planes[0]
表示的Y數(shù)據(jù),在不同存儲(chǔ)順序中是一樣的隆箩,不需要分別處理该贾。★NV21存儲(chǔ)格式中的UV數(shù)據(jù)是有冗余的,我們?nèi)?code>planes[1]每一排索引為偶數(shù)的字節(jié)即可得到所有的U數(shù)據(jù)捌臊,取
planes[2]
每一排索引為偶數(shù)的字節(jié)即可得到所有的V數(shù)據(jù)杨蛋,冗余的可以忽略。★如何判斷當(dāng)前的圖像格式是I420或者NV21呢理澎?
ImageProxy.PlaneProxy[]
數(shù)組中的每一個(gè)plane
元素都有一個(gè)getPixelStride()
方法逞力,該方法的返回值如果是1,則格式是I420糠爬;如果是2寇荧,則格式是NV21。★對于包含Y數(shù)據(jù)的
planes[0]
执隧,其getPixelStride()
的結(jié)果只可能是1揩抡。
1.2.3 行跨距RowStride
Google對getRowStride()
方法的注釋如下:
/**
* <p>The row stride for this color plane, in bytes.</p>
*
* <p>This is the distance between the start of two consecutive rows of
* pixels in the image. Note that row stried is undefined for some formats
* such as
* {@link android.graphics.ImageFormat#RAW_PRIVATE RAW_PRIVATE},
* and calling getRowStride on images of these formats will
* cause an UnsupportedOperationException being thrown.
* For formats where row stride is well defined, the row stride
* is always greater than 0.</p>
*/
public abstract int getRowStride();
簡單翻譯一下:此顏色平面的行跨距,以字節(jié)為單位镀琉,是圖像中連續(xù)兩行像素開始之間的距離峦嗤。注意,對于某些格式滚粟,行跨距是未定義的寻仗,嘗試從這些格式獲取行跨距將會(huì)觸發(fā)UnsupportedOperationException
異常。對于有行跨距定義的格式凡壤,其行跨距的值一定大于0署尤。
★換句話說,planes[0/1/2].getBuffer()
中的每一行亚侠,除了有效數(shù)據(jù)曹体,還可能有無效數(shù)據(jù),這取決于rowStride
值與圖像寬度width
值的關(guān)系硝烂。有一點(diǎn)可以確定的是箕别,rowStride
一定大于或等于有效數(shù)據(jù)長度。
繼續(xù)以4×4的圖像為例:
I420/NV21 Y plane
-
rowStride
=width
-
rowStride
>width
I420 U/V plane
-
rowStride
=width/2
-
rowStride
>width/2
NV21 U/V plane
-
rowStride
=width-1
-
rowStride
>width-1
1.3 項(xiàng)目結(jié)構(gòu)
1.4 第三方庫
本項(xiàng)目在native
層使用到了4個(gè)第三方庫:
- x264用于視頻數(shù)據(jù)處理滞谢;
- FAAC用于音頻數(shù)據(jù)處理串稀;
- libyuv用于圖像數(shù)據(jù)處理(旋轉(zhuǎn)、縮放)狮杨;
- RTMPDump用于使用rtmp協(xié)議發(fā)送數(shù)據(jù)包母截。
其中x264和FAAC使用前,已經(jīng)在Linux系統(tǒng)下編譯了靜態(tài)連接庫橄教,其編譯過程參考文章在Linux下用NDK編譯第三方庫(合集)清寇;
libyuv和RTMPDump庫直接使用源代碼喘漏。
配置項(xiàng)目CMakeLists
項(xiàng)目的CMakeLists.txt
編輯如下:
/cpp/CMakeLists.txt
#CMake
#/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
#native-lib.cpp
add_library(native-lib SHARED native-lib.cpp JavaCallHelper.cpp VideoChannel.cpp)
#rtmp
include_directories(${CMAKE_SOURCE_DIR}/librtmp)
add_subdirectory(librtmp)
#x264
include_directories(${CMAKE_SOURCE_DIR}/x264/armeabi-v7a/include)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/x264/armeabi-v7a/lib")
#faac
include_directories(${CMAKE_SOURCE_DIR}/faac/armeabi-v7a/include)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/faac/armeabi-v7a/lib")
#log-lib
find_library(log-lib log)
#將native-lib及其使用的庫鏈接起來
target_link_libraries(native-lib ${log-lib} x264 faac rtmp)
#libyuv
include_directories(${CMAKE_SOURCE_DIR}/libyuv/include)
add_subdirectory(libyuv)
add_library(ImageUtils SHARED ImageUtils.cpp)
#將ImageUtils及其使用的庫鏈接起來
target_link_libraries(ImageUtils yuv)
/cpp/librtmp/CMakeLists.txt
#CMake
#/cpp/librtmp/CMakeLists.txt
#關(guān)閉ssl 不支持rtmps
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DNO_CRYPTO")
#所有源文件放入 rtmp_source 變量
file(GLOB rtmp_source *.c)
#編譯靜態(tài)庫
add_library(rtmp STATIC ${rtmp_source})
/cpp/libyuv/CMakeLists.txt
#CMake
#/cpp/libyuv/CMakeLists.txt
aux_source_directory(source LIBYUV)
add_library(yuv STATIC ${LIBYUV})
2 直播前視頻預(yù)覽
2.1 時(shí)序圖
當(dāng)用戶打開APP,進(jìn)入首頁华烟,即打開攝像頭翩迈,將預(yù)覽畫面渲染到TextureView
。當(dāng)MainActivity
進(jìn)入onCreate
生命周期盔夜,時(shí)序圖如下(時(shí)序圖很大负饲,字很小,建議電腦查看):
2.2 過程描述
用戶打開APP比吭,當(dāng)MainActivity
進(jìn)入onCreate
生命周期绽族,主要執(zhí)行了三個(gè)動(dòng)作:
- 第一步,創(chuàng)建
RtmpClient
對象衩藤,由MainActivity
執(zhí)有該對象吧慢;在其構(gòu)造方法中調(diào)用nativeInit()
方法,初始化native層的必要組件赏表,創(chuàng)建JavaCallHelper
對象检诗,由native-lib.cpp
執(zhí)有該對象。 - 第二步瓢剿,調(diào)用對象
rtmpClient
的initVideo()
方法逢慌,在該方法中,創(chuàng)建Java層的VideoChannel
對象间狂,由rtmpClient
執(zhí)有該對象攻泼;在VideoChannel
的構(gòu)造方法中,新建并開啟了一個(gè)“Analyze-Thread”線程鉴象,在該新線程中忙菠,會(huì)周期調(diào)用CameraX
提供的analyze()
回調(diào),在該回調(diào)中纺弊,我們首先監(jiān)測rtmpClient.isConnected
標(biāo)識(shí)牛欢,判斷是否處于直播狀態(tài),若是淆游,則進(jìn)入編碼推流操作傍睹;否則,維持當(dāng)前的僅預(yù)覽操作犹菱。完成VideoChannel
的構(gòu)造操作之后拾稳,initVideo()
繼續(xù)調(diào)用native方法initVideoEnc()
,對native層的視頻編碼器進(jìn)行初始化腊脱,時(shí)刻準(zhǔn)備進(jìn)入直播狀態(tài)熊赖。 - 第三步,調(diào)用對象
rtmpClient
的initAudio()
方法虑椎,在該方法中震鹉,創(chuàng)建Java層的AudioChannel
對象,由rtmpClient
執(zhí)有該對象捆姜;在AudioChannel
的構(gòu)造方法中传趾,新建并開啟了一個(gè)“Audio-Recode”線程,在該線程中泥技,直播狀態(tài)下會(huì)進(jìn)行音頻的編碼浆兰;完成AudioChannel
的構(gòu)造之后,initAudio()
方法繼續(xù)調(diào)用native方法initAudioEnc()
珊豹,對native層的音頻編碼器進(jìn)行初始化簸呈,時(shí)刻準(zhǔn)備進(jìn)入直播狀態(tài)。
2.3 關(guān)鍵代碼
2.3.1 準(zhǔn)備視頻編碼器
以下代碼對應(yīng)著時(shí)序圖中的videoChannel.openCodec:(int,int,int,int)->void
方法店茶,在該方法內(nèi)蜕便,重點(diǎn)對一些x264視頻參數(shù)進(jìn)行了設(shè)置。注意看代碼注釋贩幻。
//C++
//VideoChannel.cpp
void VideoChannel::openCodec(int width, int height, int fps, int bitrate) {
//編碼器參數(shù)
x264_param_t param;
//ultrafast: 編碼速度與質(zhì)量的控制 ,使用最快的模式編碼
//zerolatency: 無延遲編碼 轿腺, 實(shí)時(shí)通信方面
x264_param_default_preset(¶m, "ultrafast", "zerolatency");
//main base_line high
//base_line 3.2 編碼規(guī)格 無B幀(數(shù)據(jù)量最小,但是解碼速度最慢)
param.i_level_idc = 32;
//輸入數(shù)據(jù)格式
param.i_csp = X264_CSP_I420;
param.i_width = width;
param.i_height = height;
//無b幀
param.i_bframe = 0;
//參數(shù)i_rc_method表示碼率控制丛楚,CQP(恒定質(zhì)量)族壳,CRF(恒定碼率),ABR(平均碼率)
param.rc.i_rc_method = X264_RC_ABR;
//碼率(比特率,單位Kbps)
param.rc.i_bitrate = bitrate / 1000;
//瞬時(shí)最大碼率
param.rc.i_vbv_max_bitrate = bitrate / 1000 * 1.2;
//幀率
param.i_fps_num = fps;
param.i_fps_den = 1;
param.pf_log = x264_log_default2;
//幀距離(關(guān)鍵幀) 2s一個(gè)關(guān)鍵幀
param.i_keyint_max = fps * 2;
//是否復(fù)制sps和pps放在每個(gè)關(guān)鍵幀的前面趣些,該參數(shù)設(shè)置是讓每個(gè)關(guān)鍵幀(I幀)都附帶sps/pps
param.b_repeat_headers = 1;
//不使用并行編碼仿荆。zerolatency場景下設(shè)置param.rc.i_lookahead=0
//那么編碼器來一幀編碼一幀,無并行坏平、無延時(shí)
param.i_threads = 1;
param.rc.i_lookahead = 0;
x264_param_apply_profile(¶m, "baseline");
codec = x264_encoder_open(¶m);
ySize = width * height;
uSize = (width >> 1) * (height >> 1);
this->width = width;
this->height = height;
}
2.3.2 準(zhǔn)備音頻編碼器
以下代碼對應(yīng)時(shí)序圖中的audioChannel.openCodec:(int,int)->void
方法拢操,主要對FAAC音頻參數(shù)進(jìn)行了設(shè)置。注意看代碼注釋功茴。
//C++
//AudioChannel.cpp
void AudioChannel::openCodec(int sampleRate, int channels) {
//輸入樣本:要送給編碼器編碼的樣本數(shù)
unsigned long inputSamples;
codec = faacEncOpen(sampleRate, channels, &inputSamples, &maxOutputBytes);
//樣本是16位的庐冯,那么一個(gè)樣本就是2個(gè)字節(jié)
inputByteNum = inputSamples * 2;
outputBuffer = static_cast<unsigned char *>(malloc(maxOutputBytes));
//得到當(dāng)前編碼器的各種參數(shù)配置
faacEncConfigurationPtr configurationPtr = faacEncGetCurrentConfiguration(codec);
configurationPtr->mpegVersion = MPEG4;
configurationPtr->aacObjectType = LOW;
//1.每一幀音頻編碼的結(jié)果數(shù)據(jù)都會(huì)攜帶ADTS(包含了采樣、聲道等信息的一個(gè)數(shù)據(jù)頭)
//0.編碼出aac裸數(shù)據(jù)
configurationPtr->outputFormat = 0;
configurationPtr->inputFormat = FAAC_INPUT_16BIT;
faacEncSetConfiguration(codec, configurationPtr);
}
3 開始直播
3.1 連接流媒體服務(wù)器
3.1.1 時(shí)序圖
當(dāng)用戶點(diǎn)擊頁面上的開始直播按鈕時(shí)坎穿,客戶端首先需要做的是連接流媒體服務(wù)器展父。時(shí)序圖如下:
3.1.2 過程描述
連接流媒體服務(wù)器需要在native層借助RTMPDump
庫進(jìn)行,同時(shí)玲昧,該過程涉及到網(wǎng)絡(luò)請求栖茉,所以連接過程需要在新的線程進(jìn)行。
- 當(dāng)用戶點(diǎn)擊開始直播按鈕孵延,APP最終將在native層新建一個(gè)
pthread
吕漂,異步執(zhí)行連接服務(wù)器操作; - 在異步執(zhí)行的
void *connect(void *args)
方法中尘应,將借助RTMPDump
庫嘗試連接流媒體服務(wù)器惶凝; - 成功連接流媒體服務(wù)器后吼虎,native層將利用
JavaCallHelper
對象——helper
的onPrepare()
方法,調(diào)用Java層的rtmpClient
對象的onPrepare()
方法苍鲜;在這個(gè)方法內(nèi)思灰,第一,設(shè)置rtmpClient
的isConnected
標(biāo)識(shí)為true
混滔,開始編碼視頻洒疚;調(diào)用audioChannel.start()
方法,將音頻編碼任務(wù)post
到“Audio-Recode”線程坯屿,開始編碼音頻油湖。
3.1.3 關(guān)鍵代碼
開啟連接流媒體服務(wù)器線程
以下代碼對應(yīng)時(shí)序圖中的JNI_connect:(JNIEnv *,jobject,jstring)->void
函數(shù),在該函數(shù)中领跛,程序開啟了新的線程乏德。
//C++
//native-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_tongbo_mycameralive_RtmpClient_connect(
JNIEnv *env,
jobject thiz,
jstring url_
) {
const char *url = env->GetStringUTFChars(url_, 0);
path = new char[strlen(url) + 1];
strcpy(path, url);
pthread_create(&ptid, NULL, connect, 0);
env->ReleaseStringUTFChars(url_, url);
}
嘗試連接流媒體服務(wù)器
注意!其中隔节,RtmpClient
的成員方法connect(String url)
是一個(gè)native方法鹅经,在其JNI實(shí)現(xiàn)中開啟新的線程,異步執(zhí)行void *connect(void *args)
函數(shù)怎诫,在該函數(shù)中瘾晃,我們借助RTMPDump
庫提供的API,實(shí)現(xiàn)連接流媒體服務(wù)器幻妓。void *connect(void *args)
異步函數(shù)的具體實(shí)現(xiàn)如下蹦误,對應(yīng)時(shí)序圖中的connect:(void *)->void *
異步函數(shù)。
//C++
//native-lib.cpp
//...
VideoChannel *videoChannel = 0;
AudioChannel *audioChannel = 0;
JavaVM *javaVM = 0;
JavaCallHelper *helper = 0;
pthread_t pid;
char *path = 0;
RTMP *rtmp = 0;
uint64_t startTime;
//...
void *connect(void *args) {
int ret;
rtmp = RTMP_Alloc();
RTMP_Init(rtmp);
do {
//解析url地址(可能失敗肉津,地址不合法)
ret = RTMP_SetupURL(rtmp, path);
if (!ret) {
//TODO:通知Java地址傳的有問題(未實(shí)現(xiàn))
break;
}
//開啟輸出模式强胰,僅拉流播放的話,不需要開啟
RTMP_EnableWrite(rtmp);
ret = RTMP_Connect(rtmp, 0);
if (!ret) {
//TODO:通知Java服務(wù)器連接失斆蒙场(未實(shí)現(xiàn))
break;
}
ret = RTMP_ConnectStream(rtmp, 0);
if (!ret) {
//TODO:通知Java未連接到流(未實(shí)現(xiàn))
break;
}
//發(fā)送audio specific config(告訴播放器怎么解碼我推流的音頻)
RTMPPacket *packet = audioChannel->getAudioConfig();
callback(packet);
} while (false);
//TODO:清理變量空間偶洋,防止內(nèi)存泄漏
if (!ret) {
RTMP_Close(rtmp);
RTMP_Free(rtmp);
rtmp = 0;
}
delete (path);
path = 0;
//TODO:通知Java層可以開始推流了
helper->onParpare(ret);
startTime = RTMP_GetTime();
return 0;
}
借助JavaCallHelper告知Java層連接成功
native層連接成功之后,通過以下代碼調(diào)用Java層的rtmpClient.onPrepare()
方法距糖,以下代碼對應(yīng)時(shí)序圖中的helper.onPrepare:(jboolean,int)->void
玄窝。
//C++
//JavaCallHelper.cpp
void JavaCallHelper::onParpare(jboolean isConnect, int thread) {
if (thread == THREAD_CHILD) {
JNIEnv *jniEnv;
if (javaVM->AttachCurrentThread(&jniEnv, 0) != JNI_OK) {
return;
}
jniEnv->CallVoidMethod(jobj, jmid_prepare, isConnect);
javaVM->DetachCurrentThread();
} else {
env->CallVoidMethod(jobj, jmid_prepare);
}
}
3.2 視頻編碼
3.2.1 時(shí)序圖
成功連接上流媒體服務(wù)器后,rtmpClient.isConnected
標(biāo)識(shí)被設(shè)置成true
悍引,CameraX
在執(zhí)行回調(diào)analyze()
時(shí)恩脂,將能進(jìn)入視頻編碼操作,時(shí)序圖如下:
3.2.2 過程描述
其實(shí)趣斤,在用戶進(jìn)入APP初始化CameraX
并開始預(yù)覽時(shí)俩块,就已經(jīng)開始執(zhí)行analyze()
回調(diào)了,當(dāng)時(shí)的rtmpClient.isConnected
標(biāo)識(shí)為false
,當(dāng)該標(biāo)識(shí)被設(shè)置成true
后玉凯,視頻編碼過程大體分為兩個(gè)步驟:
- 第一步势腮,獲取圖像字節(jié);
- 第二步壮啊,發(fā)送圖像數(shù)據(jù)嫉鲸。
3.2.3 關(guān)鍵代碼
獲取圖像字節(jié)
針對1.2.3中的六種情況,我們對每一個(gè)plane的每一行以一個(gè)字節(jié)為單位進(jìn)行處理歹啼。以下代碼對應(yīng)時(shí)序圖中的[rtmpClient.isConnected==true]ImageUtils.getBytes:(ImageProxy,int,int,int)->byte[]
部分:
//Java
//ImageUtils.java
package com.tongbo.mycameralive;
import android.graphics.ImageFormat;
import androidx.camera.core.ImageProxy;
import java.nio.ByteBuffer;
public class ImageUtils {
static {
System.loadLibrary("ImageUtils");
}
static ByteBuffer i420;
static byte[] scaleBytes;
public static byte[] getBytes(ImageProxy image, int rotationDegrees, int width, int height) {
int format = image.getFormat();
if (format != ImageFormat.YUV_420_888) {
//拋出異常
}
//創(chuàng)建一個(gè)ByteBuffer i420對象,其字節(jié)數(shù)是height*width*3/2座菠,存放最后的I420圖像數(shù)據(jù)
int size = height * width * 3 / 2;
//TODO:防止內(nèi)存抖動(dòng)
if (i420 == null || i420.capacity() < size) {
i420 = ByteBuffer.allocate(size);
}
i420.position(0);
//YUV planes數(shù)組
ImageProxy.PlaneProxy[] planes = image.getPlanes();
//TODO:取出Y數(shù)據(jù)狸眼,放入i420
int pixelStride = planes[0].getPixelStride();
ByteBuffer yBuffer = planes[0].getBuffer();
int rowStride = planes[0].getRowStride();
//1.若rowStride等于Width,skipRow是一個(gè)空數(shù)組
//2.若rowStride大于Width浴滴,skipRow就剛好可以存儲(chǔ)每行多出來的幾個(gè)byte
byte[] skipRow = new byte[rowStride - width];
byte[] row = new byte[width];
for (int i = 0; i < height; i++) {
yBuffer.get(row);
i420.put(row);
//1.若不是最后一行拓萌,將無效占位數(shù)據(jù)放入skipRow數(shù)組
//2.若是最后一行,不存在無效無效占位數(shù)據(jù)升略,不需要處理微王,否則報(bào)錯(cuò)
if (i < height - 1) {
yBuffer.get(skipRow);
}
}
//TODO:取出U/V數(shù)據(jù),放入i420
for (int i = 1; i < 3; i++) {
ImageProxy.PlaneProxy plane = planes[i];
pixelStride = plane.getPixelStride();
rowStride = plane.getRowStride();
ByteBuffer buffer = plane.getBuffer();
int uvWidth = width / 2;
int uvHeight = height / 2;
//一次處理一行
for (int j = 0; j < uvHeight; j++) {
//一次處理一個(gè)字節(jié)
for (int k = 0; k < rowStride; k++) {
//1.最后一行
if (j == uvHeight - 1) {
//1.I420:UV沒有混合在一起品嚣,rowStride大于等于Width/2炕倘,如果是最后一行,不理會(huì)占位數(shù)據(jù)
if (pixelStride == 1 && k >= uvWidth) {
break;
}
//2.NV21:UV混合在一起翰撑,rowStride大于等于Width-1罩旋,如果是最后一行,不理會(huì)占位數(shù)
if (pixelStride == 2 && k >= width - 1) {
break;
}
}
//2.非最后一行
byte b = buffer.get();
//1.I420:UV沒有混合在一起眶诈,僅保存索引為偶數(shù)的有效數(shù)據(jù)涨醋,不理會(huì)占位數(shù)據(jù)
if (pixelStride == 1 && k < uvWidth) {
i420.put(b);
continue;
}
//2.NV21:UV混合在一起,僅保存索引為偶數(shù)的有效數(shù)據(jù)逝撬,不理會(huì)占位數(shù)據(jù)
if (pixelStride == 2 && k < width - 1 && k % 2 == 0) {
i420.put(b);
continue;
}
}
}
}
//TODO:將i420數(shù)據(jù)轉(zhuǎn)成byte數(shù)組浴骂,執(zhí)行旋轉(zhuǎn),并返回
int srcWidth = image.getWidth();
int srcHeight = image.getHeight();
byte[] result = i420.array();
if (rotationDegrees == 90 || rotationDegrees == 270) {
result = rotate(result, width, height, rotationDegrees);
srcWidth = image.getHeight();
srcHeight = image.getWidth();
}
if (srcWidth != width || srcHeight != height) {
//todo jni對scaleBytes修改值宪潮,避免內(nèi)存抖動(dòng)
int scaleSize = width * height * 3 / 2;
if (scaleBytes == null || scaleBytes.length < scaleSize) {
scaleBytes = new byte[scaleSize];
}
scale(result, scaleBytes, srcWidth, srcHeight, width, height);
return scaleBytes;
}
return result;
}
private static native byte[] rotate(byte[] data, int width, int height, int degress);
private native static void scale(byte[] src, byte[] dst, int srcWidth, int srcHeight, int dstWidth, int dstHeight);
}
★其中溯警,native
函數(shù)rotate()
的實(shí)現(xiàn)如下,圖像旋轉(zhuǎn)的核心代碼是libyuv::I420Rotate()
函數(shù)坎炼。之所以需要執(zhí)行旋轉(zhuǎn)是因?yàn)槔颍瑪?shù)據(jù)默認(rèn)有90度或270度的偏移,需要手動(dòng)還原谣光。
//C++
//ImageUtils.cpp
#include <jni.h>
#include <libyuv.h>
extern "C"
JNIEXPORT jbyteArray JNICALL
Java_com_tongbo_mycameralive_ImageUtils_rotate(JNIEnv *env, jclass thiz, jbyteArray data_, jint width,
jint height, jint degress) {
//TODO:為libyuv::I420Rotate()函數(shù)準(zhǔn)備傳入?yún)?shù)
jbyte *data = env->GetByteArrayElements(data_, 0);
uint8_t *src = reinterpret_cast<uint8_t *>(data);
int ySize = width * height;
int uSize = (width >> 1) * (height >> 1);
int size = (ySize * 3) >> 1;
uint8_t dst[size];
uint8_t *src_y = src;
uint8_t *src_u = src + ySize;
uint8_t *src_v = src + ySize + uSize;
uint8_t *dst_y = dst;
uint8_t *dst_u = dst + ySize;
uint8_t *dst_v = dst + ySize + uSize;
//TODO:調(diào)用libyuv::I420Rotate()函數(shù)
libyuv::I420Rotate(src_y, width, src_u, width >> 1, src_v, width >> 1,
dst_y, height, dst_u, height >> 1, dst_v, height >> 1,
width, height, static_cast<libyuv::RotationMode>(degress));
//TODO:準(zhǔn)備返回值
jbyteArray result = env->NewByteArray(size);
env->SetByteArrayRegion(result, 0, size, reinterpret_cast<const jbyte *>(dst));
env->ReleaseByteArrayElements(data_, data, 0);
return result;
}
★native
函數(shù)scale()
的實(shí)現(xiàn)是:
//C++
//ImageUtils.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_tongbo_mycameralive_ImageUtils_scale(JNIEnv *env, jclass clazz, jbyteArray src_, jbyteArray dst_,
jint srcWidth,
jint srcHeight, jint dstWidth, jint dstHeight) {
jbyte *data = env->GetByteArrayElements(src_, 0);
uint8_t *src = reinterpret_cast<uint8_t *>(data);
int64_t size = (dstWidth * dstHeight * 3) >> 1;
uint8_t dst[size];
uint8_t *src_y;
uint8_t *src_u;
uint8_t *src_v;
int src_stride_y;
int src_stride_u;
int src_stride_v;
uint8_t *dst_y;
uint8_t *dst_u;
uint8_t *dst_v;
int dst_stride_y;
int dst_stride_u;
int dst_stride_v;
src_stride_y = srcWidth;
src_stride_u = srcWidth >> 1;
src_stride_v = src_stride_u;
dst_stride_y = dstWidth;
dst_stride_u = dstWidth >> 1;
dst_stride_v = dst_stride_u;
int src_y_size = srcWidth * srcHeight;
int src_u_size = src_stride_u * (srcHeight >> 1);
src_y = src;
src_u = src + src_y_size;
src_v = src + src_y_size + src_u_size;
int dst_y_size = dstWidth * dstHeight;
int dst_u_size = dst_stride_u * (dstHeight >> 1);
dst_y = dst;
dst_u = dst + dst_y_size;
dst_v = dst + dst_y_size + dst_u_size;
libyuv::I420Scale(src_y, src_stride_y,
src_u, src_stride_u,
src_v, src_stride_v,
srcWidth, srcHeight,
dst_y, dst_stride_y,
dst_u, dst_stride_u,
dst_v, dst_stride_v,
dstWidth, dstHeight,
libyuv::FilterMode::kFilterNone);
env->ReleaseByteArrayElements(src_, data, 0);
env->SetByteArrayRegion(dst_, 0, size, reinterpret_cast<const jbyte *>(dst));
}
x264視頻編碼
借助x264庫的API進(jìn)行視頻編碼檩淋,以下代碼對應(yīng)時(shí)序圖中的videoChannel.encode:(uint8_t *)->void
部分:
//C++
//VideoChannel.cpp
void VideoChannel::encode(uint8_t *data) {
//輸出的待編碼數(shù)據(jù)
x264_picture_t pic_in;
x264_picture_alloc(&pic_in, X264_CSP_I420, width, height);
pic_in.img.plane[0] = data;
pic_in.img.plane[1] = data + ySize;
pic_in.img.plane[2] = data + ySize + uSize;
//TODO:編碼的i_pts,每次需要增長
pic_in.i_pts = i_pts++;
x264_picture_t pic_out;
x264_nal_t *pp_nal;
int pi_nal;
//pi_nal:輸出了多少nal
int error = x264_encoder_encode(codec, &pp_nal, &pi_nal, &pic_in, &pic_out);
if (error <= 0) {
return;
}
int spslen, ppslen;
uint8_t *sps;
uint8_t *pps;
for (int i = 0; i < pi_nal; ++i) {
int type = pp_nal[i].i_type;
//數(shù)據(jù)
uint8_t *p_payload = pp_nal[i].p_payload;
//數(shù)據(jù)長度
int i_payload = pp_nal[i].i_payload;
if (type == NAL_SPS) {
//sps后面肯定跟著pps
spslen = i_payload - 4; //去掉間隔 00 00 00 01
sps = (uint8_t *) alloca(spslen); //棧中申請,不需要釋放
memcpy(sps, p_payload + 4, spslen);
} else if (type == NAL_PPS) {
ppslen = i_payload - 4; //去掉間隔 00 00 00 01
pps = (uint8_t *) alloca(ppslen);
memcpy(pps, p_payload + 4, ppslen);
//pps后面肯定有I幀,發(fā)I幀之前要發(fā)一個(gè)sps與pps
sendVideoConfig(sps, pps, spslen, ppslen);
} else {
sendFrame(type, p_payload, i_payload);
}
}
}
x264包裝視頻配置信息
根據(jù)x264和視頻格式標(biāo)準(zhǔn)蟀悦,視頻配置信息幀和視頻圖像數(shù)據(jù)幀的頭不一樣媚朦,因此分了兩個(gè)函數(shù)分別包裝數(shù)據(jù),以下代碼對應(yīng)時(shí)序圖中的[data[i] is config]sendVideoConfig:(uint8_t *,uint8_t *,int,int)->void
部分:
//C++
//VideoChannel.cpp
void VideoChannel::sendVideoConfig(uint8_t *sps, uint8_t *pps, int spslen, int ppslen) {
int bodySize = 13 + spslen + 3 + ppslen;
RTMPPacket *packet = new RTMPPacket;
RTMPPacket_Alloc(packet, bodySize);
int i = 0;
//固定頭
packet->m_body[i++] = 0x17;
//類型
packet->m_body[i++] = 0x00;
//composition time 0x000000
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
packet->m_body[i++] = 0x00;
//版本
packet->m_body[i++] = 0x01;
//編碼規(guī)格
packet->m_body[i++] = sps[1];
packet->m_body[i++] = sps[2];
packet->m_body[i++] = sps[3];
packet->m_body[i++] = 0xFF;
//整個(gè)sps
packet->m_body[i++] = 0xE1;
//sps長度
packet->m_body[i++] = (spslen >> 8) & 0xff;
packet->m_body[i++] = spslen & 0xff;
memcpy(&packet->m_body[i], sps, spslen);
i += spslen;
//pps
packet->m_body[i++] = 0x01;
packet->m_body[i++] = (ppslen >> 8) & 0xff;
packet->m_body[i++] = (ppslen) & 0xff;
memcpy(&packet->m_body[i], pps, ppslen);
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nBodySize = bodySize;
packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
//時(shí)間戳 sps與pps(不是圖像) 沒有時(shí)間戳
packet->m_nTimeStamp = 0;
//使用相對時(shí)間
packet->m_hasAbsTimestamp = 0;
//隨便給一個(gè)通道 日戈,避免rtmp.c中使用的就行
packet->m_nChannel = 0x10;
callback(packet);
}
x264包裝視頻圖像數(shù)據(jù)
以下代碼對應(yīng)時(shí)序圖中的[data[i] is config]sendFrame:(int,uint8_t *,int)->void
部分:
//C++
//VideoChannel.cpp
void VideoChannel::sendFrame(int type, uint8_t *p_payload, int i_payload) {
//去掉 00 00 00 01 / 00 00 01
if (p_payload[2] == 0x00) {
i_payload -= 4;
p_payload += 4;
} else if (p_payload[2] == 0x01) {
i_payload -= 3;
p_payload += 3;
}
RTMPPacket *packet = new RTMPPacket;
int bodysize = 9 + i_payload;
RTMPPacket_Alloc(packet, bodysize);
RTMPPacket_Reset(packet);
//int type = payload[0] & 0x1f;
packet->m_body[0] = 0x27;
//關(guān)鍵幀
if (type == NAL_SLICE_IDR) {
packet->m_body[0] = 0x17;
}
//類型
packet->m_body[1] = 0x01;
//時(shí)間戳
packet->m_body[2] = 0x00;
packet->m_body[3] = 0x00;
packet->m_body[4] = 0x00;
//數(shù)據(jù)長度 int 4個(gè)字節(jié) 相當(dāng)于把int轉(zhuǎn)成4個(gè)字節(jié)的byte數(shù)組
packet->m_body[5] = (i_payload >> 24) & 0xff;
packet->m_body[6] = (i_payload >> 16) & 0xff;
packet->m_body[7] = (i_payload >> 8) & 0xff;
packet->m_body[8] = (i_payload) & 0xff;
//圖片數(shù)據(jù)
memcpy(&packet->m_body[9], p_payload, i_payload);
packet->m_hasAbsTimestamp = 0;
packet->m_nBodySize = bodysize;
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nChannel = 0x10;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
callback(packet);
}
3.3 音頻編碼
3.3.1 時(shí)序圖
3.3.2 過程描述
- 以上時(shí)序圖從
JavaCallHelper
調(diào)用rtmpClient.onPrepare()
方法開始询张,在audioChannel.start()
方法中將音頻編碼任務(wù)通過post(new Runnable(){...})
提交到handler
綁定的looper
。 - 在音頻編碼任務(wù)中浙炼,首先創(chuàng)建一個(gè)
AudioRecord
對象——audioRecord
份氧,該對象由audioChannel
執(zhí)有,之后利用該audioRecord
開始錄音弯屈,并循環(huán)讀取蜗帜,直到退出錄音狀態(tài)。
3.3.3 關(guān)鍵代碼
提交音頻編碼任務(wù)
上面調(diào)用了rtmpClient.onPrepare()
方法资厉,在該方法內(nèi)厅缺,設(shè)置了isConnected
標(biāo)識(shí)為true
;然后宴偿,調(diào)用了audioChannel.start()
方法湘捎,將音頻編碼任務(wù)提交到“Audio-Recode”線程,以下代碼對應(yīng)時(shí)序圖中的audioChannel.start:()->void
和handler.post:(Runnable)->boolean
部分:
//Java
//AudioChannel.java
public void start() {
handler.post(new Runnable() {
@Override
public void run() {
audioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC,
sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
minBufferSize
);
audioRecord.startRecording();
while (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
int len = audioRecord.read(buffer, 0, buffer.length);
if (len > 0) {
//樣本數(shù)=字節(jié)數(shù)/2字節(jié)(16位)
rtmpClient.sendAudio(buffer, len >> 1);
}
}
}
});
}
音頻編碼
以下代碼對應(yīng)時(shí)序圖的audioChannel.encode:(int32_t *)->void
部分:
//C++
//AudioChannel.cpp
void AudioChannel::encode(int32_t *data, int len) {
//len:輸入的樣本數(shù)
//outputBuffer:輸出窄刘,編碼之后的結(jié)果
//maxOutputBytes:編碼結(jié)果緩存區(qū)能接收數(shù)據(jù)的個(gè)數(shù)
int bytelen = faacEncEncode(codec, data, len, outputBuffer, maxOutputBytes);
if (bytelen > 0) {
RTMPPacket *packet = new RTMPPacket;
RTMPPacket_Alloc(packet, bytelen + 2);
packet->m_body[0] = 0xAF;
packet->m_body[1] = 0x01;
memcpy(&packet->m_body[2], outputBuffer, bytelen);
packet->m_hasAbsTimestamp = 0;
packet->m_nBodySize = bytelen + 2;
packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
packet->m_nChannel = 0x11;
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
callback(packet);
}
}
3.4 音視頻推流
應(yīng)該注意到窥妇,每一次按照協(xié)議包裝好一個(gè)音頻幀或者視頻幀后,都會(huì)調(diào)用一個(gè)callback(RTMPPacket *packet)
回調(diào)都哭,利用RTMPDump庫將數(shù)據(jù)發(fā)送出去秩伞。音視頻使用的是同一個(gè)callback(RTMPPacket *packet)
回調(diào),只是傳入的數(shù)據(jù)分別是音頻和視頻罷了欺矫。
- 該
callback()
回調(diào)的定義是
//C++
//Callback.h
#ifndef PUSHER_CALLBACK_H
#define PUSHER_CALLBACK_H
#include <rtmp.h>
typedef void (*Callback)(RTMPPacket *);
#endif //PUSHER_CALLBACK_H
- 其具體實(shí)現(xiàn)是
//C++
//native-lib.cpp
void callback(RTMPPacket *packet) {
if (rtmp) {
packet->m_nInfoField2 = rtmp->m_stream_id;
//使用相對時(shí)間
packet->m_nTimeStamp = RTMP_GetTime() - startTime;
//放到隊(duì)列中
RTMP_SendPacket(rtmp, packet, 1);
}
RTMPPacket_Free(packet);
delete (packet);
}
4 攝像頭切換
4.1 時(shí)序圖
4.2 過程描述
從時(shí)序圖可以看出纱新,基于CameraX
的攝像頭切換實(shí)現(xiàn)十分簡單,當(dāng)用戶點(diǎn)擊切換攝像頭按鈕穆趴,首先調(diào)用MainActivity
中綁定的方法脸爱,MainActivity
與RtmpClient
的對象rtmpClient
交互,通過rtmpClient
分別操作音視頻未妹,最終在rtmpClient.toggleCamera()
方法中調(diào)用到具體切換攝像頭的實(shí)現(xiàn)videoChannel.toggleCamera()
方法簿废。
4.3 關(guān)鍵代碼
具體切換攝像頭的實(shí)現(xiàn)videoChannel.toggleCamera()
方法:
//Java
//VideoChannel.java
public void toggleCamera() {
CameraX.unbindAll();
if (currentFacing == CameraX.LensFacing.BACK) {
currentFacing = CameraX.LensFacing.FRONT;
} else {
currentFacing = CameraX.LensFacing.BACK;
}
CameraX.bindToLifecycle(lifecycleOwner, getPreView(), getAnalysis());
}
5 停止直播
5.1 時(shí)序圖
5.2 過程描述
以上過程從用戶點(diǎn)擊停止直播按鈕開始。首先MainActivity
中綁定按鈕的stopLive(View view)
方法被調(diào)用络它,調(diào)用rtmpClient.stop()
方法族檬,分別去停止音視頻的直播。
- 視頻:注意化戳!停止直播并不代表退出APP单料,因此還是需要保留視頻預(yù)覽,所以時(shí)序圖中對視頻的處理主要在于對視頻編碼器的重置
i_pts=0
,以及在CameraX
的analyze()
回調(diào)中都會(huì)檢查的標(biāo)志位rtmpClient.isConnected
扫尖,將該標(biāo)志位置為false
白对。 - 音頻:關(guān)鍵需要調(diào)用
audioRecord.stop()
方法,停止錄音换怖。 - RTMP:在
JNI_disConnect:(JNIEnv *,jobject)->void
函數(shù)中甩恼,釋放rtmp
指針指向的內(nèi)容。
5.3 關(guān)鍵代碼
JNI層斷開連接
JNI_disConnect:(JNIEnv *,jobject)->void
函數(shù):
//C++
//native-lib.cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_tongbo_mycameralive_RtmpClient_disConnect(JNIEnv *env, jobject thiz) {
pthread_mutex_lock(&mutex);
if (rtmp) {
RTMP_Close(rtmp);
RTMP_Free(rtmp);
rtmp = 0;
}
if (videoChannel) {
videoChannel->resetPts();
}
pthread_mutex_unlock(&mutex);
}
6 退出應(yīng)用
6.1 時(shí)序圖
6.2 過程描述
當(dāng)Android的執(zhí)行生命周期回調(diào)onDestroy()
方法時(shí)沉颂,我們重寫onDestroy()
方法条摸,并在其中調(diào)用rtmpClient.release()
方法,然后在其中分別停止和釋放內(nèi)存資源兆览。其中核心的釋放過程已經(jīng)在時(shí)序圖中標(biāo)紅屈溉。至此,此基礎(chǔ)直播APP所有功能都已經(jīng)實(shí)現(xiàn)抬探。
歡迎找茬。
作者:樂為
鏈接:https://juejin.im/post/5e0b2627e51d45412862a921
來源:掘金