全手動(dòng)寫Android攝像頭直播應(yīng)用

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 簡介

引自developer.android

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,顯然莽龟,該ImageProxyImage的一個(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中的NV21I420兩種格式。

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[] planesplanes[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è)第三方庫:

  1. x264用于視頻數(shù)據(jù)處理滞谢;
  2. FAAC用于音頻數(shù)據(jù)處理串稀;
  3. libyuv用于圖像數(shù)據(jù)處理(旋轉(zhuǎn)、縮放)狮杨;
  4. 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)用對象rtmpClientinitVideo()方法逢慌,在該方法中,創(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)用對象rtmpClientinitAudio()方法虑椎,在該方法中震鹉,創(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(&param, "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(&param, "baseline");
    codec = x264_encoder_open(&param);
    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對象——helperonPrepare()方法,調(diào)用Java層的rtmpClient對象的onPrepare()方法苍鲜;在這個(gè)方法內(nèi)思灰,第一,設(shè)置rtmpClientisConnected標(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:()->voidhandler.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中綁定的方法脸爱,MainActivityRtmpClient的對象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,以及在CameraXanalyze()回調(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
來源:掘金

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末帆赢,一起剝皮案震驚了整個(gè)濱河市小压,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌椰于,老刑警劉巖怠益,帶你破解...
    沈念sama閱讀 221,273評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異瘾婿,居然都是意外死亡蜻牢,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門偏陪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抢呆,“玉大人,你說我怎么就攤上這事笛谦”埃” “怎么了?”我有些...
    開封第一講書人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵饥脑,是天一觀的道長恳邀。 經(jīng)常有香客問我讶踪,道長夸浅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任人乓,我火速辦了婚禮笋颤,結(jié)果婚禮上乳附,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好许溅,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開白布瓤鼻。 她就那樣靜靜地躺著,像睡著了一般贤重。 火紅的嫁衣襯著肌膚如雪茬祷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評(píng)論 1 308
  • 那天并蝗,我揣著相機(jī)與錄音祭犯,去河邊找鬼。 笑死滚停,一個(gè)胖子當(dāng)著我的面吹牛沃粗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播键畴,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼最盅,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了起惕?” 一聲冷哼從身側(cè)響起涡贱,我...
    開封第一講書人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎惹想,沒想到半個(gè)月后问词,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嘀粱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評(píng)論 3 340
  • 正文 我和宋清朗相戀三年激挪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锋叨。...
    茶點(diǎn)故事閱讀 40,427評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡垄分,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出悲柱,到底是詐尸還是另有隱情锋喜,我是刑警寧澤,帶...
    沈念sama閱讀 36,122評(píng)論 5 349
  • 正文 年R本政府宣布豌鸡,位于F島的核電站嘿般,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏涯冠。R本人自食惡果不足惜炉奴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蛇更。 院中可真熱鬧瞻赶,春花似錦赛糟、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至师逸,卻和暖如春司倚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背篓像。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來泰國打工动知, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人员辩。 一個(gè)月前我還...
    沈念sama閱讀 48,808評(píng)論 3 376
  • 正文 我出身青樓盒粮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親奠滑。 傳聞我的和親對象是個(gè)殘疾皇子丹皱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評(píng)論 2 359

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