Android中音頻元數據的採集,及RTMP推流

在用手機做直播推流時,不管是錄屏直播,還是攝像頭直播,都要用到音頻的元數據.

在Android中,可以藉助AudioRecord來採集音頻數據,然後通過faac編碼庫(加入用的音頻編碼器是faac),將編碼後的數據交給RTMP去封包後發送給服務器.

這篇主要討論從AudioRecord獲取音頻數據,然後通過faac編碼,經過RTMP封包後發送到直播服務器的過程.

一,

如果要使用手機的錄音功能,需要在AndroidManifest.xml中申請權限,

<uses-permission android:name="android.permission.RECORD_AUDIO"/>

除了在AndroidManifest.xml中聲明這個權限外,android6.0之後的版本還需要動態申請這個權限.

在Android6.0以後,也不是所有的權限,都需要動態申請,如果你不確定自己聲明的權限是不是需要動態申請,可以用命令查詢下,都有那些權限需要動態申請:

adb shell pm list permissions -g -d

執行這個命令,會列出需要動態申請的權限, 從這裏也可以看出android對權限的管理以Group的形式來管理的.

group:android.permission-group.MICROPHONE
  permission:android.permission.RECORD_AUDIO

你可能會有疑問,既然都去動態申請了,AndroidManifest.xml中的權限聲明是不是多餘的.答案肯定不是多餘的,如果要說明白這個Runtime Permissions的問題還是有點複雜的,這不是這篇文檔的主題,所以簡單的說下:

Zygote在孵化出一個進程後,會根據這個進程的Mount Mode,把這個應用程序掛在不同的掛在點,這個Mount mode的取值是由PackageManagerService解析應用的AndroidManifest.xml中的權限申請來得出的.當應用程序獲得Runtime permission後,PackageManagerService會通過mount service 向vold傳遞一個remount_uid的命令,這一命令會對進程的Mount point做出調整.

接着看怎麼使用AudioRecord來獲取音頻元數據.

先要創建一個AudioRecord對象,然後開啓錄音,當然這個要在一個子線程來做,AudioRecord構造函數的參數:依次是聲音的來源,採樣率,聲道數,採樣位,保存採樣數據的最小緩衝區大小.

AudioFormat.ENCODING_PCM_16BIT,每一次採集的數據用兩個字節表示,就是每次採集的數據佔多少位。
minBufferSize,存儲採樣數據的最小緩衝區的大小。

  private final int mSampleRate;
    private int channelConfig;
    private int minBufferSize;
    private AudioRecord mAudioRecord;
    private byte[] buffer;

minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);

mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
                        mSampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT, minBufferSize);

        //創建AudioRecord來錄音,作爲推流端,應該要實現碼率自適應,在網絡不佳時,降低推流碼率來降低上行帶寬壓力。
        //rtmp的使用,從協議層面講,累積延遲是它的一個特徵,因爲RTMP基於TCP,所以不會丟包,
        // 在網絡狀況不好時,有超時重傳策略,緩衝策略,自然會帶來累積延遲。

 

然後在一個子線程開啓錄音,就可以獲取到聲音數據了:

                mAudioRecord.startRecording();
                while (mAudioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                    int len = mAudioRecord.read(buffer, 0, buffer.length);
                    if (len > 0) {
             
                    }
                }

如果len >0表示正常獲取到聲音數據了,並保存在了字節數組buffer中,返回值len表示此次獲取的樣本數(不是字節數)

接着就可以把buffer中的數據送給faac編碼器編碼:

二,

faac編碼庫的使用.

使用faac聲音數據編碼的步驟:

1,使用faacEncOpen打開編碼器

2,使用faacEncSetConfiguration配置編碼器參數

3,使用 faacEncEncode編碼聲音數據,

faacEncHandle codec = 0;
unsigned long maxOutputBytes;
unsigned long inputByteNum;

//使用faac對聲音數據編碼
void AudioChannel::openCodec(int sampleRate, int channels) {
    unsigned long inputSamples;
    //創建編碼器,第三,四個參數,是兩個指針,這種形式是要給實參賦值,
    // inputSamples輸入樣本數,要送給編碼器編碼的樣本數,編碼器表示要輸入這麼多數據,才能編碼出一幀數據,
    codec = faacEncOpen(sampleRate, channels, &inputSamples, &maxOutputBytes);
    //從樣本數 ,*2,得到字節數,因爲sample是16bit(AudioFormat.ENCODING_PCM_16BIT),所以要得到字節數就 *2.
    //在錄製聲音時,mAudioRecord.read的buffer大小,可以設置爲跟inputByteNum相同,所以java層可以get這個大小,
    inputByteNum = inputSamples * 2;
    //
    outputBuffer = static_cast<unsigned char *>(malloc(maxOutputBytes));
    //得到編碼器當前默認的參數配置。
    faacEncConfigurationPtr configurationPtr = faacEncGetCurrentConfiguration(codec);
    //配置成自己需要的參數。
    configurationPtr->mpegVersion = MPEG4;
    configurationPtr->aacObjectType =LOW;
    //值 0,表示編碼出aac裸數據,
    //值 1,表示每一幀編碼出來的數據,都會攜帶ADTS頭信息(包含了採樣,聲道等信息),如果編碼後直接保存成aac文件,可以用這個。
    configurationPtr->outputFormat = 0;

    configurationPtr->inputFormat = FAAC_INPUT_16BIT;
    faacEncSetConfiguration(codec, configurationPtr);
}
對從AudioRecord讀取的音頻裸數據,進行編碼,
 unsigned char *outputBuffer = 0;

void AudioChannel::encode(int32_t *data, int len) {
    //第三個參數,是輸入的樣本數,而不是字節數
    //第四個參數,輸出編碼後的結果,
    //第五個參數,輸出緩衝區(第4個參數)能接收的數據的字節數
    int byteLen = faacEncEncode(codec, data, len, outputBuffer, maxOutputBytes);
    if (byteLen > 0) {
        RTMPPacket *packet = new RTMPPacket;
        //因爲還要拼接audio specific config(0XAF 0x00)來記錄音頻格式,或者普通數據 0XAF 0x01,所以多加2個字節。
        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;
        //這個通道的值沒有特別要求,但是不能跟rtmp.c中使用的相同,也不能跟發送Video的Packet中設置的相同。
        packet->m_nChannel = 0x11;
        packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
        //在使用完packet,要釋放。
        callback(packet);
    }
}

音頻數據推流到服務端,要讓播放器拉流後能正常播放,需要指定音頻格式信息:audio specific config.
音頻的這個信息只需要在發送聲音數據包之前發送一個就可以了.

//如果不拼audio specific config(0XAF 0x01),播放器將不知道怎麼解碼,也就播放不出聲音。

//所以這段audio specific config數據,需要在發送音頻數據前,發送給服務器。

void AudioChannel::sendAudioConfig() {
    u_char *buf;
    u_long len;
    faacEncGetDecoderSpecificInfo(codec, &buf, &len);
    RTMPPacket *packet = new RTMPPacket;
    RTMPPacket_Alloc(packet, len +2);
    packet->m_body[0] = 0xAF;
    packet->m_body[1] = 0x00;
    memcpy(&packet->m_body[2], buf, len);
    packet->m_hasAbsTimestamp = 0;
    packet->m_nBodySize = len +2;
    packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
    packet->m_nChannel = 0x11;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    callback(packet);
}

三,使用RTMP推流的流程:

extern "C"
JNIEXPORT jboolean JNICALL
Java_com_test_livedemo_connect(JNIEnv *env, jobject thiz, jstring url_) {
    int ret;
    const char *url = env->GetStringUTFChars(url_, 0);
    //rtmp的使用,從協議層面講,累積延遲是它的一個特徵,因爲RTMP基於TCP,所以不會丟包,
    // 在網絡狀況不好時,有超時重傳策略,緩衝策略,自然會帶來累積延遲。
    do {
        //申請內存,
        rtmp = RTMP_Alloc();
        //初始化,
        RTMP_Init(rtmp);
        //設置地址,如果地址不合法,會返回0,
        if (!(ret = RTMP_SetupURL(rtmp, const_cast<char *>(url)))) {
            break;
        }
        //開啓輸出模式,
        RTMP_EnableWrite(rtmp);
        //連接服務器,
        if (!(ret = RTMP_Connect(rtmp, 0))) {
            break;
        }
        //連接流,
        if (!(ret = RTMP_ConnectStream(rtmp, 0))) {
            break;
        }

    } while (0);
    //第一個參數jstring變量,要釋放的本地字符串的來源,第二個參數,要釋放的本地字符串,
    env->ReleaseStringUTFChars(url_, url);
    return ret;
}
//每編碼一幀,就發送出去。如在不同線程,都會操作rtmp,需要加鎖
連接服務器成功後,獲取一個當前時間:
startTime = RTMP_GetTime();

void callback(RTMPPacket *packet) {
    pthread_mutex_lock(&sendMutex);
    if (rtmp) {
        //指定這個packet要推向的stream_id;建立rtmp時,是要跟某個stream建立連接,數據就推到這個流中,
        packet->m_nInfoField2 = rtmp->m_stream_id;
        //時間是相對連接服務器成功後的時間,
        packet->m_nTimeStamp = RTMP_GetTime() - startTime;
        //第三個參數1,表示把數據先放入一個隊列,按次序發送,這一中異步發送的情況。
        RTMP_SendPacket(rtmp, packet, 1);
    }
    pthread_mutex_unlock(&sendMutex);
    RTMPPacket_Free(packet);
    delete packet;
}

 

四, 爲什麼RTMP封包要多加2個字節,0XAF 0x01, 或0XAF 0x00

RTMP 包中封裝的音視頻數據流,其實和FLV/tag封裝音頻和視頻數據的方式是相同的,按照FLV格式封裝音視頻,在裸數據前需要添加的標記如下:

0xAF的來歷,參考這個配置信息:

編碼配置爲,10:AAC,3:44100採樣率,1:採樣長度,1:聲道,按照位數表示數據就爲:0xAF.

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章