在用手機做直播推流時,不管是錄屏直播,還是攝像頭直播,都要用到音頻的元數據.
在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.