Live-client-6-直播功能

做了那么久鋪墊击纬,終于要寫本項目中最為重要的直播功能了。首先介紹一下項目中用到的一些知識點和協(xié)議

零钾麸、知識點與協(xié)議

(一)音視頻基礎知識

1. 音頻相關概念:

  1. 采樣率(SampleRateInHz):采樣率指每秒采樣點個數(8000/44100Hz)更振,采樣率單位用Hz(赫茲)表示
  2. 聲道(Sound Channel):是指聲音在錄制或播放時在不同空間位置采集或回放的相互獨立的音頻信號,所以聲道數也是聲音錄制或者播放時的揚聲器數量喂走。
    常見聲道數:單聲道殃饿、立體聲道谋作、4聲道芋肠、5.1聲道、7.1聲道等
  3. 量化精度:量化精度表示可以將模擬信號分成多少個等級遵蚜,量化精度越高帖池,音樂的聲壓振幅越接近原始音樂。量化精度的單位是bit(比特)吭净,也可以理解為一個采樣點用多少比特表示(8/16/24/32 bit)睡汹。在Android中,量化精度由AudioFormat.ENCODING_PCM_16BIT寂殉、ENCODING_PCM_8BIT等表示囚巴。
  4. 數據量(字節(jié)/秒)= (采樣頻率(Hz) * 采樣位數(量化精度,bit) * 聲道數)/ 8

2. 視頻相關概念

  1. 幀率(Frame Rate):用于測量顯示幀數的量度友扰,測量單位是每秒顯示幀數(frames per second彤叉,簡稱fps)。每秒顯示幀數(fps)或者幀率表示圖形處理器場時每秒能夠更新的次數村怪。一般而言秽浇,30fps是能夠接受的幀率,而60fps則更為逼真甚负、更為流暢柬焕,超過75fps后就不容易察覺有明顯的流暢度提升。
  2. 分辨率:指視頻成像產品所形成的圖像大小或者尺寸梭域。
  3. 刷新率:刷新率是指屏幕每秒畫面被刷新的次數斑举,刷新率分為垂直刷新率和水平刷新率,一般指垂直刷新率病涨。垂直刷新率表示屏幕上圖像每秒重繪多少次富玷,也就是每秒屏幕刷新的次數,以Hz為單位。刷新率越高凌彬,圖像越穩(wěn)定沸柔,圖像顯示就越清晰。
  4. 碼率:碼率也就是比特率铲敛,比特率是單位時間播放連續(xù)的媒體的比特數量褐澎。比特率越高,帶寬消耗得越多伐蒋。
    文件大小(b) = 碼率(b/s) * 時長(s)
  5. 視頻幀:常見的視頻幀有I工三、P、S幀等
  • I幀表示關鍵幀先鱼,可以理解為這一幀畫面的完整保留俭正,解碼時只需要本幀數據就可以完成。
  • P幀表示這一幀和之前的一個關鍵幀(或者P幀)的差別焙畔,解碼時需要用之前的畫面疊加上本幀定義的差別生成最終畫面掸读。
  • B幀是雙向差別幀,B幀記錄的是本幀和前后幀的差別宏多,也就是解碼B幀時儿惫,不僅需要之前的緩存畫面,還需要解碼之后的畫面伸但,通過前后的畫面和B幀進行疊加肾请,得到最后畫面。
  1. 數據量 = 分辨率(width * height) *每個像素所占的字節(jié)數更胖。比如YUV420 = 分辨率 * 3 / 2

(二)相關協(xié)議與庫

1. AAC音頻格式

AAC音頻格式有兩種:ADIF和ADTS

  • ADIF:Audio Data Interchange Format铛铁,音頻數據交換格式,這種格式的特征是可以確定找到這個音頻數據的開始却妨,不需要在音頻數據流中間開始的解碼饵逐,即其解碼必須在明確的位置開始進行。適用于磁盤文件管呵。
  • ADTS:Audio Data Transport Stream梳毙,音頻數據傳輸流,這種格式的特征是它有一個同步字的比特流捐下,解碼可以在這個流中的任何位置開始账锹。

兩者的區(qū)別:ADTS可以在任意幀進行解碼,每一幀都有頭信息坷襟。ADIF只有一個統(tǒng)一的頭奸柬,必須得到所有的數據后才能進行解碼。

ADTS是幀序列婴程,本身就具備流特征廓奕,在音頻流的傳輸和處理方面更加合適,ADST幀格式如下:


ADST幀格式.png

而ADST幀的Header結構如下:


ADST幀的Header結構.png
  • syncword :同步頭 總是0xFFF, all bits must be 1,代表著一個ADTS幀的開始
  • ID:MPEG Version: 0 for MPEG-4, 1 for MPEG-2
  • Layer:always: '00'
  • profile:表示使用哪個級別的AAC桌粉,有些芯片只支持AAC LC 蒸绩。在MPEG-2 AAC中定義了3種:
    0:Main profile; 1:Low Complexity profile(LC) ; 2:Scalable Sampling Rate Profile(SSR)
  • sampling_frequency_index:表示使用的采樣率下標,通過這個下標在 Sampling Frequencies[ ]數組中查找得知采樣率的值铃肯。
    0: 96000 Hz
    1: 88200 Hz
    2: 64000 Hz
    3: 48000 Hz
    4: 44100 Hz
    5: 32000 Hz
  • channel_configuration: 表示聲道數
    0: AOT默認設置
    1: 1個聲道數:前中央
    2: 2個聲道數:左前患亿、右前
    3: 3個聲道數:左前、右前押逼、前中央
    4: 4個聲道數:左前步藕、后前、前中央挑格、后中央
    5: 5個聲道數:左前咙冗、后前、前中央漂彤、左后雾消、右后
  • frame_length : 一個ADTS幀的長度包括ADTS頭和AAC原始流.
  • adts_buffer_fullness:0x7FF 說明是碼率可變的碼流

2. H264視頻編碼

1. H264碼流文件分為2層:

(1)VCL(Video Coding Layer):視頻編碼層,負責高效的視頻內容表示显歧,VCL數據即編碼處理的輸出仪或,表示被壓縮數據編碼后的視頻數據序列。
(2)NAL(Network Abstraction Layer):網絡提取層士骤,復雜以網絡所要求的恰當的方式對數據進行打包和傳送,是傳輸層蕾域,不管在本地播放還是在網絡播放的傳輸拷肌,都需要這一層來傳輸。

2. H264編碼格式

在VCL數據傳輸或者存儲之前旨巷,這些編碼的 VCL 數據巨缘,先被映射或封裝進NAL 單元中。每個 NAL 單元包括一個原始字節(jié)序列負荷( RBSP, Raw Byte Sequence Payload)和一組對應于視頻編碼的 NAL 頭信息采呐。RBSP 的基本結構是:在原始編碼數據的后面填加了結尾比特若锁。一個 bit“1”若干比特“0”,以便字節(jié)對齊斧吐。


NAL單元序列.png

3. NAL Header

NAL頭由一個字節(jié)組成又固,包含禁止位(1bit)、重要性位(2bit)煤率、NALU類型(5bit)

+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI|  TYPE   |
+---------------+
0 1-2 3-7
簡稱 F NRI TYPE
全稱 forbidden_zero_bit nal_ref_idc nal_unite_type
中文 禁止位 重要性指示位 NALU類型
作用 當網絡發(fā)現NAL單元有比特錯誤時仰冠,可以設置該比特為1,以便接受方丟掉該單元 標志該NAL單元用于重建時的重要性蝶糯,值越大洋只,越重要,取值為00-11 1-23表示單個NAL包,24-31需要分包或者組合發(fā)送

NALU類型:
0:沒有定義
(1-23 NAL單元识虚,單個 NAL 單元包)
1:不分區(qū)肢扯,非IDR圖像的片
2:片分區(qū)A
3:片分區(qū)B
4:片分區(qū)C
5:IDR圖像中的片
6:補充增強信息單元(SEI)
7:SPS(Sequence Parameter Set序列參數集,作用于一串連續(xù)的視頻圖像担锤,即視頻序列)
8:PPS(Picture Parameter Set圖像參數集鹃彻,作用于視頻序列中的一個或多個圖像)
9:序列結束
10:序列結束
11:碼流結束
12:填充
13-23:保留
24:STAP-A單一時間的組合包
25:STAP-B單一時間的組合包
26:MTAP16單一時間的組合包
27:MTAP24多個時間的組合包
28:FU-A分片的單元
29:FU-B分片的單元
30-32:未定義

4. H264傳輸

H264的編碼視頻序列包括一系列的NAL單元,每個單元包含一個RBSP妻献。每個單元都按獨立的NAL單元傳輸蛛株。NAL單元的信息頭定義了RBSP的類型,NAL單元的其余部分為RBSP數據育拨。

5. H264碼流結構

這部分內容比較復雜姨伤,重點在于如果一個NALU單元內有SPS金踪、PPS相連,那么該幀就是關鍵幀。

6. H264的Level和profile說明

H264的Level用來約束分辨率玄糟、幀率和碼率的。Level越高所支持的分辨率懂诗、碼率鹰晨、幀率就越大。
H264的profile則是用來說明視頻序列的幀類型:

  1. BP(Baseline profile)基線檔次:提供I/P幀闷畸,僅支持Progressive(逐行掃描)和CAVLC尝盼。多應用于“視頻會話”,如可視電話佑菩、會議電視盾沫、遠程教學、視頻監(jiān)控等實時通信領域殿漠;
  2. XP(Extended profile)進階檔次:提供I/P/B/SP/SI幀赴精,僅支持Progressive和CAVLC。多應用于流媒體領域绞幌,如視頻點播蕾哟、基于網絡的視頻監(jiān)控等;
  3. MP(Main profile)主要檔次:提供I/P/B幀莲蜘,支持Progressive和Interlaced(隔行掃描)谭确,提供CAVLC和CABAC。多應用于數字電視廣播菇夸、數字視頻存儲等領域琼富;
  4. HiP(High profile)高級檔次:(Fidelity Range Extensions,FRExt)在Main profile基礎上新增8*8幀內預測,Custom Quant庄新,Lossless Video Coding,更多YUV格式(4:2:2,4:4:4)鞠眉,像素精度提高到10位或14位薯鼠。多應用于對高分辨率和高清晰度有特別要求的領域。

3. RTMP協(xié)議

RTMP中的數據類型有Message(消息)和Chunk(消息塊)械蹋。Message是RTMP協(xié)議中的基本數據單元出皇,而RTMP協(xié)議在互聯網中傳輸時,將Message拆分成更小的單元哗戈,這個單元就是Chunk
Message格式如下:


Message格式.png
  • Message Type 消息類型:1字節(jié)郊艘,0x04表示Ping包,0x08為Audio唯咬、0x09為video
  • Payload Length 負載消息長度:3字節(jié)纱注,表述了tag中數據段的大小
  • Time Stamp 時間戳:4字節(jié),記錄每一個tag相對于第一個tag的相對時間胆胰。
  • Stream ID 媒體流ID:3字節(jié)狞贱,標識消息所屬的媒體流
  • Message Body 音視頻消息

一、架構設計

首先來介紹一下rtmp直播的要點:

  1. 服務器:通過使用nginx+nginx-rtmp-module模塊來實現rtmp的流媒體服務器蜀涨,提供用戶連接瞎嬉、數據中轉、直播數據存儲等功能厚柳,該服務器的搭建見Live-Server-9-Maven打包氧枣,部署+Nginx服務器這篇文章。
  2. 客戶端:客戶端要處理的東西就非常多了别垮,分為兩大部分:推流和拉流便监。
  • 推流:采集麥克風的音頻數據,通過faac編碼成aac格式的音頻宰闰;采集相機數據茬贵,通過x264編碼成H.264格式的視頻;然后通過rtmp將aac音頻和H.264視頻封裝成flv的視頻格式移袍,然后向nginx-rtmp服務器發(fā)送數據。
  • 拉流:通過支持rtmp直播流的播放器拉取nginx-rtmp中的數據老充,并播放葡盗。播放器可供選擇的有:Bilibili的ijkplayer播放器、ffmpeg播放器啡浊。


    推流拉流過程.png

一觅够、推流

推流部分是最復雜的一部分,也是拓展性最強的部分巷嚣。目前的FAAC喘先、x264、RTMP只是最簡單的方案廷粒,還可以對視頻圖像進行人像優(yōu)化窘拯、添加濾鏡红且、圖像裁剪、圖標等涤姊;也可以對麥克風的pcm音頻進行變聲暇番、變調、去雜音思喊、添加背景聲音等壁酬。

為了便于實現該部分功能和添加拓展性,將推流部分按如下代碼結構來實現:


推流代碼結構.png

總體實現思路如下:

  1. 定義一個抽象類BasePusher恨课,并定義開始直播舆乔、停止直播、釋放資源等控制直播的抽象方法剂公;
  2. 定義視頻推流器VideoPusher希俩、音頻推流器AudioPusher繼承抽象類BasePusher,實現上述抽象方法诬留。
    VideoPusher要控制手機攝像頭的開關及數據采集轉碼等斜纪,
    AudioPusher要控制麥克風開關和音頻數據錄取轉碼等;
  3. 定義直播推流器LivePusher控制視頻推流器和音頻推流器的執(zhí)行文兑。
  4. 視頻推流器盒刚、音頻推流器、直播推流器都各自擁有PushNative類對象绿贞,該類是NDK的native方法定義因块,用來控制NDK原生代碼實現視頻編碼、音頻編碼籍铁、RTMP推流等功能涡上。

(一)PushNative

既然在AudioPusher、VideoPusher拒名、LivePusher中都擁有一個PusherNative類對象吩愧,那么我們先來看看這個類需要做什么:

  • 視頻編碼:設置視頻編碼格式、發(fā)送視頻數據包
  • 音頻編碼:設置音頻編碼格式增显、發(fā)送音頻數據包
  • 推流:開始推流雁佳、停止推流、釋放資源
/**
 * 調用C代碼進行編碼和推流
 * @author Ljh 2019/6/15 14:07
 */
public class PushNative {

    /**
     * 異常標識
     * 
     * CONNECT_FAILED:連接異常
     * INIT_FAILED:初始化異常
     * WHAT_FAILED:未知異常
     */
    public static final int CONNECT_FAILED = 101;
    public static final int INIT_FAILED = 102;
    public static final int WHAT_FAILED = 103;

    /**
     * 異惩疲回調監(jiān)聽
     */
    LiveStateChangeListener liveStateChangeListener;
    
    /**
     * 接受Native層拋出的錯誤
     * @param code
     */
    public void throwNativeError(int code){
        if(liveStateChangeListener != null) {
            liveStateChangeListener.onError(code);
        }
    }

    /**
     * 開始推流
     * @param url 推流地址
     */
    public native void startPush(String url);

    /**
     * 停止推流
     */
    public native void stopPush();

    /**
     * 釋放資源
     */
    public native void release();

    /**
     * 設置視頻參數
     * @param width 視頻寬
     * @param height 視頻高
     * @param bitrate 比特率(碼率)
     * @param fps 幀率
     */
    public native void setVideoOptions(int width, int height, int bitrate, int fps);

    /**
     * 設置音頻參數
     * @param sampleRateInHz 采樣率
     * @param channel 聲道數
     */
    public native void setAudioOptions(int sampleRateInHz, int channel);

    /**
     * 發(fā)送視頻數據
     * @param data 視頻數據
     */
    public native void fireVideo(byte[] data, int width, int height);

    /**
     * 發(fā)送音頻數據
     * @param data 音頻數據
     * @param len 數據長度
     */
    public native void fireAudio(byte[] data, int len);

    public void removeLiveStateChangeListener() {
        this.liveStateChangeListener = null;
    }

    public void setLiveStateChangeListener(LiveStateChangeListener liveStateChangeListener) {
        this.liveStateChangeListener = liveStateChangeListener;
    }

    static {
        System.loadLibrary("faac");
        System.loadLibrary("x2641");
        System.loadLibrary("rtmp");
        System.loadLibrary("native-lib");
    }
}

在代碼中糖权,可以看到PushNative和BasePusher類一樣定義了startPush()、stopPush()炸站、release()三個方法來控制直播推流星澳,不過這些方法都是native方法,也就是需要使用C/C++代碼進行實現旱易,也就是我們常說的JNI編程禁偎。

除了上述幾個方法以外腿堤,還包含setVideoOptions、setAudioOptions届垫、fireVideo释液、fireAudio四個方法來實現音視頻的格式參數的設置和發(fā)送音視頻數據包,最后還用了一個LiveStateChangeListener用來監(jiān)聽native代碼的異常装处,在Activity中實現監(jiān)聽的回調就可以完成對native代碼異常的處理误债。

(二)LivePusher直播推流器

接下來看下直播推流器需要做什么?在上面的推流代碼結構圖中妄迁,LivePusher可以看成是直播推流的一個封裝類寝蹈,封裝好所需要的接口提供給Activity、Presenter來使用登淘。

前面提到BasePusher中定義了開始推流箫老、停止推流和釋放資源三個方法,在VideoPusher黔州、AudioPusher耍鬓、PushNative中都有實現,那么這些實現方法由誰調用呢流妻?既然是LivePusher來封裝牲蜀,那肯定是由LivePusher來調用,于是在LivePusher中又定義了這三個方法绅这,分別調用推流器中對應的方法涣达。那么問題又來了,如果直接進行推流(startPush()方法)证薇,那么怎么知道我推流的音視頻編碼格式和封裝格式呢度苔?不能每次推流前才去設置吧,如果我直播直到一半浑度,突然想暫停寇窑,過幾分鐘又開始直播,就沒必要重新設置音視頻格式了吧 箩张?那需要在LivePusher中事先設置好音視頻編碼格式等屬性疗认,于是定義一個prepare()方法,并調用PushNative中的setAudio(Video)Options來實現伏钠。

在直播中可能需要切換攝像頭,讓觀眾看看主播那精致的面容谨设,那就定義一個切換攝像頭的操作熟掂,提供給上一層來調用吧。同時還需要將相機預覽的數據顯示在屏幕上扎拣,就需要從Activity中獲取一個可以顯示預覽數據的TextureView給下一層的VideoPusher來進行處理赴肚。

public class LivePusher2 implements TextureView.SurfaceTextureListener {

    private TextureView textureView;
    private VideoPusher2 videoPusher;
    private AudioPusher audioPusher;
    private PushNative pushNative;

    public LivePusher2(TextureView textureView, Context context) {
        this.textureView = textureView;
        textureView.setSurfaceTextureListener(this);
        prepare(context);
    }

    //準備音頻素跺、視頻推流器
    private void prepare(Context context) {
        pushNative = new PushNative();
        //實例化視頻推流器
        VideoParam videoParam = new VideoParam(480, 360, Camera.CameraInfo.CAMERA_FACING_BACK);;

        videoPusher = new VideoPusher2(textureView, videoParam, pushNative, context);

        //實例化音頻推流器
        AudioParam audioParam = new AudioParam();
        audioPusher = new AudioPusher(audioParam, pushNative);
    }

    /**
     * 切換攝像頭
     */
    public void switchCamera() {
        videoPusher.switchCamera();
    }

    /**
     * 開始推流
     *
     * @param url 推流服務器地址
     */
    public void startPush(final String url, LiveStateChangeListener liveStateChangeListener) {
        pushNative.startPush(url);
        videoPusher.startPusher();
        audioPusher.startPusher();
        pushNative.setLiveStateChangeListener(liveStateChangeListener);
    }

    /**
     * 停止推流
     */
    public void stopPush() {
        videoPusher.stopPusher();
        audioPusher.stopPusher();
        pushNative.stopPush();
        pushNative.removeLiveStateChangeListener();
    }

    /**
     * 釋放資源
     */
    public void release() {
        videoPusher.release();
        audioPusher.release();
        pushNative.release();
    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {

    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        stopPush();
        release();
        return true;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {

    }
}

(三)音頻推流與AudioPusher

2. 音頻推流實現

音頻推流的過程是設置音頻格式;通過麥克風獲取PCM音頻數據誉券,然后給native層的faac庫編碼成aac格式的音頻數據指厌,隨后通過rtmp產生音頻數據包。

(1)設置音頻格式

從上文中得知PushNative類中有setAudioOptions的方法踊跟,設置音頻格式就是給這個方法傳入采樣率踩验、聲道數等數據,然后在Jni中實現native方法商玫。設置音頻格式主要是設置faac編碼器的參數箕憾。

/**
 * 設置音頻參數
 */
extern "C"
JNIEXPORT void JNICALL
Java_com_ljh_live_jni_PushNative_setAudioOptions(JNIEnv *env, jobject instance, jint sampleRateInHz,
                                                 jint numChannel) {
    audio_encode_handle = faacEncOpen(sampleRateInHz, numChannel, &nInputSamples, &nMaxOutputBytes);
    if (!audio_encode_handle) {
        LOGE("%s", "音頻編碼器打開失敗");
        return;
    }
    //設置音頻參數
    faacEncConfigurationPtr p_config = faacEncGetCurrentConfiguration(audio_encode_handle);
    p_config->mpegVersion = MPEG4;
    p_config->allowMidside = 1;
    p_config->aacObjectType = LOW;
    p_config->outputFormat = 0; //輸出是否包含ADTS頭
    p_config->useTns = 1; //時域噪音控制,大概是消除爆破音
    p_config->useLfe = 0;
    p_config->quantqual = 100;
    p_config->bandWidth = 0; //頻寬
    p_config->shortctl = SHORTCTL_NORMAL;

    if (!faacEncSetConfiguration(audio_encode_handle, p_config)) {
        LOGE("%s", "音頻編碼器配置失敗");
        throwNativeError(env, INIT_FAILED);
        return;
    }
    LOGI("%s", "音頻編碼器配置成功");
}

(2)推流實現

1)Java層代碼

在Android系統(tǒng)中有這樣的一個API:AudioRecord拳昌,該類是用于啟動麥克風袭异,錄制音頻并產生PCM音頻數據。AudioRecord的使用步驟如下:

  1. 創(chuàng)建最小緩沖區(qū)
    最小緩沖區(qū)需要采樣率炬藤、聲道數御铃、采樣(量化)精度來設定。
minBufferSize = AudioRecord.getMinBufferSize(audioParam.getSampleRateInHz(), channelConfig, AudioFormat.ENCODING_PCM_16BIT);
  1. 創(chuàng)建AudioRecord對象
    AudioRecord對象的創(chuàng)建需要聲音源沈矿、采樣率上真、聲道數、采樣(量化)精度细睡、最小緩沖區(qū)來創(chuàng)建谷羞。
audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                audioParam.getSampleRateInHz(),
                channelConfig,
                AudioFormat.ENCODING_PCM_16BIT,
                minBufferSize);
  1. 開始錄制
audioRecord.startRecording();
  1. 獲取PCM數據
while(true){
    //通過AudioRecord不斷讀取音頻數據
    byte[] buffer = new byte[minBufferSize];
    int len = audioRecord.read(buffer, 0, buffer.length);
}

audioRecord.read()運行在當前線程中,如果不斷的調用該方法溜徙,會導致Android UI線程阻塞湃缎,導致ANR,因此需要創(chuàng)建一個新的線程來執(zhí)行蠢壹。

class AudioRecordTask implements Runnable {
        @Override
        public void run() {
            //開始錄音
            audioRecord.startRecording();

            while (isPushing) {
                //通過AudioRecord不斷讀取音頻數據
                byte[] buffer = new byte[minBufferSize];
                int len = audioRecord.read(buffer, 0, buffer.length);
            }
        }
    }

既然已經在子線程中獲取到了PCM數據嗓违,那么何不直接將其傳給PushNative來完成編碼和推送呢?于是在循環(huán)中添加如下代碼:

  if (len > 0) {
        //傳給Native代碼图贸,進行音頻編碼
        pushNative.fireAudio(buffer, len);
 }
2)Native代碼

接下來又是Native代碼的編寫蹂季。需要明確的是音頻推流的native部分主要是faac庫和rtmp庫的使用,在設置音頻格式時疏日,已經初始化了audio_encode_handle(faacEncHandle)偿洁,也就是初始化了faac音頻編碼處理器,獲取到數據時沟优,就可以直接進行AAC編碼了涕滋,編碼完成后就加入到RTMP消息隊列中。

每次從實時的pcm音頻隊列中讀出量化位數為8的pcm數據挠阁,用8個二進制位來表示一個采樣量化點(模數轉換)宾肺,然后調用faacEncEncode這個函數來編碼溯饵,需要傳入編碼處理器audio_encode_handle、轉換后的pcm流數組pcmbuf锨用、采樣數量audioLength(采樣的pcm數組大蟹峥)、編碼后的aac音頻數組增拥、最大輸出字節(jié)數啄巧。

/**
 * 對音頻采樣數據進行AAC編碼
 */
extern "C"
JNIEXPORT void JNICALL
Java_com_ljh_live_jni_PushNative_fireAudio(JNIEnv *env, jobject instance, jbyteArray buffer,
                                           jint len) {
    //轉換后的pcm流數組
    int *pcmbuf;
    //編碼后的數據buff
    unsigned char *bitbuf;
    jbyte *b_buffer = env->GetByteArrayElements(buffer, NULL);
    if (b_buffer == NULL) {
        LOGI("%s", "音頻數據為空");
    }
    pcmbuf = (int *) malloc(nInputSamples * sizeof(int));
    bitbuf = (unsigned char *) malloc(nMaxOutputBytes * sizeof(unsigned char));
    int nByteCount = 0;
    unsigned int nBufferSize = (unsigned int) len / 2;
    unsigned short *buf = (unsigned short *) b_buffer;
    while (nByteCount < nBufferSize) {
        int audioLength = nInputSamples;
        if ((nByteCount + nInputSamples) >= nBufferSize) {
            audioLength = nBufferSize - nByteCount;
        }
        int i;
        for (i = 0; i < audioLength; ++i) {
            //每次從實時的pcm音頻隊列中讀取量化位數為8的pcm數據
            int s = ((int16_t *) buf + nByteCount)[i];
            pcmbuf[i] = s << 8;  //用8個二進制位來表示一個采樣量化點(模數轉換)
        }
        nByteCount += nInputSamples;

        //利用FAAC進行編碼,pcmbuf為轉換后的pcm流數組跪者,audioLength為調用faacEncOpen時得到的輸入采樣數
        //bitbuf為編碼后的數據buff棵帽,nMaxOutputBytes為調用faacEncOpen時得到的最大輸出字節(jié)數
        int byteslen = faacEncEncode(audio_encode_handle, pcmbuf, audioLength, bitbuf,
                                     nMaxOutputBytes);
        if (byteslen < 1) {
            continue;
        }
        add_aac_body(bitbuf, byteslen); //從bitbuf中得到編碼后的aac數據流,放到數據隊列
    }

    env->ReleaseByteArrayElements(buffer, b_buffer, 0);
    if (bitbuf) {
        free(bitbuf);
    }
    if (pcmbuf) {
        free(pcmbuf);
    }
}

完成AAC編碼后渣玲,接下來就要將AAC數據傳給rtmp封裝成RTMP Packet逗概。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市忘衍,隨后出現的幾起案子逾苫,更是在濱河造成了極大的恐慌,老刑警劉巖枚钓,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铅搓,死亡現場離奇詭異,居然都是意外死亡搀捷,警方通過查閱死者的電腦和手機星掰,發(fā)現死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嫩舟,“玉大人氢烘,你說我怎么就攤上這事〖已幔” “怎么了播玖?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長饭于。 經常有香客問我蜀踏,道長,這世上最難降的妖魔是什么掰吕? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任果覆,我火速辦了婚禮,結果婚禮上殖熟,老公的妹妹穿的比我還像新娘随静。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布燎猛。 她就那樣靜靜地躺著照皆,像睡著了一般重绷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上膜毁,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天昭卓,我揣著相機與錄音,去河邊找鬼瘟滨。 笑死候醒,一個胖子當著我的面吹牛,可吹牛的內容都是我干的杂瘸。 我是一名探鬼主播倒淫,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼败玉!你這毒婦竟也來了敌土?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤运翼,失蹤者是張志新(化名)和其女友劉穎返干,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體血淌,經...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡矩欠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了悠夯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片癌淮。...
    茶點故事閱讀 40,015評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖疗疟,靈堂內的尸體忽然破棺而出该默,到底是詐尸還是另有隱情,我是刑警寧澤策彤,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布栓袖,位于F島的核電站,受9級特大地震影響店诗,放射性物質發(fā)生泄漏裹刮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一庞瘸、第九天 我趴在偏房一處隱蔽的房頂上張望捧弃。 院中可真熱鬧,春花似錦、人聲如沸违霞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽买鸽。三九已至涧郊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間眼五,已是汗流浹背妆艘。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留看幼,地道東北人批旺。 一個月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像诵姜,于是被迫代替她去往敵國和親汽煮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,969評論 2 355

推薦閱讀更多精彩內容