Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android源碼分析:VoIP

Android源碼分析:VoIP

編輯:關於Android編程

概述

Android的voip功能支持位於目錄frameworks/base/voip中。它包括支持rtp功能的package

RTP支持

RTP支持包位於目錄frameworks/base/voip/java/android/net/rtp下,主要包含四個Java類:代表著基於RTP協議的流RtpStream、基於RTP協議的語音流AudioStream、描述了語音Codec信息的AudioCodec和語音會話組的AudioGroup、。

RTP流:RtpStream

它是基於RTP(Real-time Transport Protocol)協議的數據流。Java層的API類是android.net.rtp.RtpStream,代表著一個通過RTP協議發送和接收網絡多媒體數據包的流。一個流主要包括本機網絡地址和端口號、遠程主機網絡地址和端口號、socket號和流模式。

RtpStream支持三種流模式,可由setMode函數設定:

MODE_NORMAL:正常模式,接收和發送數據包

MODE_SEND_ONLY: 只發送數據包

MODE_RECEIVE_ONLY:只接收數據包

本地主機IP地址(InetAddress,支持IPv4和IPv6)由調用構造函數時傳入。在構造函數中,會調用native層實現的create函數獲取一個本地主機端口號(依據RFC 3550);同時,native層的create函數還會得到一個socket連接號,socket號會在native層中更新到該Java類實例中。

遠程主機地址和端口號由函數associate指定:

public void associate(InetAddress address, int port)

獲取socket號的過程如下:

android.net.rtp.RtpStream的一個私有成員整型變量mNative存放的是socket號:

private int mNative;

它由JNI層在調用socket函數後得到一個socket號後存入裡面。具體如下:在JNI層(frameworks/base/voip/jni/rtp/RtpStream.cpp)中標識它的變量是:

jfieldID gNative;

在函數registerRtpStream中獲取具體的值:

(gNative = env->GetFieldID(clazz, “mNative”, “I”)) == NULL ||

在JNI層的create函數中,會調用socket函數得到一個socket號:

int socket = ::socket(ss.ss_family, SOCK_DGRAM, 0);

這個socket號會被指定給Java層的mNative變量:

env->SetIntField(thiz, gNative, socket);

端口號port則由create函數直接返回。create函數已經支持IPv6。

語音流:AudioStream

android.net.rtp.AudioStream繼承自RtpStream,代表著一個建立在RTP協議之上的與對方通話的語音流。語音流需要使用一個語音Codec來描述其對應的編解碼信息。在建立通話之前,語音流需加入(join)到會話組android.net.rtp.AudioGroup中。因此,它包含了:所在的語音組、語音Codec以及DTMF(Dual-Tone Multi-Frequency)類型(RFC 2833)等信息。

語音Codec:AudioCodec

一個android.net.rtp.AudioStream需要有一個android.net.rtp.AudioCodec為其編解碼。Java層的AudioCodec只是描述了Codec信息的類,主要包含了三樣信息:

public final int type; //The RTP payload type of the encoding.

public final String rtpmap; //The encoding parameters to be used in the corresponding SDP attribute.

public final String fmtp; //The format parameters to be used in the corresponding SDP attribute.

可以使用AudioCodec.getCodec輕松得到一個Codec:

public static AudioCodec getCodec(int type, String rtpmap, String fmtp)

為方便使用,在AudioCodec中Android定義了常用的幾個Codec:PCMU、PCMA、GSM、GSM_EFR和AMR。

語音組:AudioGroup

android.net.rtp.AudioGroup代表的是一個會話,可能只是兩人通話,也可能是多於兩人的電話會議。可以同時有多組會話,因為麥克和揚聲器只能是排他性使用,故只能有一組會話為活動的,其它必須是HOLD狀態。

語音組通過一個映射表來維護加入它裡面的語音流:

private final Map mStreams;

一個AudioStream加入到AudioGroup的流程如下:

首先是AudioStream調用join加入到某個AudioGroup中:

public void join(AudioGroup group)

然後調用AudioGroup.add,接著調用:

private native void nativeAdd(int mode, int socket, String remoteAddress, int remotePort, String codecSpec, int dtmfType);

其中前四個參數:mode、socket號、遠程地址、遠程端口號來自於語音流的父類RTPStream中的信息,codecSpec來自於語音流對應的Codec的三樣信息,最後一個參數dtmf類型也來自語音流。

在JNI層(frameworks/base/voip/jni/rtp/AudioGroup.cpp)的add函數中,首先將遠程網絡地址和端口號保存到結構體sockaddr_storage[TODO:參見UNIX socket編程]中:

sockaddr_storage remote;
if (parse(env, jRemoteAddress, remotePort, &remote) < 0) {//遍歷得到地址,存放於sockaddr_storage中
// Exception already thrown.
return;
}

接著得到codec信息,創建一個native層AudioCodec:

sscanf(codecSpec, “%d %15[^/]%*c%d”, &codecType, codecName, &sampleRate);
codec = newAudioCodec(codecName);//根據名稱創建對應的native層的Codec

再接著創建一個native層的語音流AudioStream:

// Create audio stream.
stream = new AudioStream;//創建語音流
if (!stream->set(mode, socket, &remote, codec, sampleRate, sampleCount, codecType, dtmfType)) {//將相關信息設置給語音流
jniThrowException(env, “java/lang/IllegalStateException”, “cannot initialize audio stream”);
goto error;
}

最後獲取或創建native層的AudioGroup,並將native的AudioStream添加到native的AudioGroup中:

// Create audio group.
group = (AudioGroup *)env->GetIntField(thiz, gNative);
if (!group) {//若Java層的AudioGroup中還沒有一個對應的native層的Group。注意,Java層多次對add的調用,也只執行第一次下面的代碼
int mode = env->GetIntField(thiz, gMode);
group = new AudioGroup;//創建一個native的AudioGroup
if (!group->set(8000, 256) || !group->setMode(mode)) {//詳見後文對這兩個函數的解釋
jniThrowException(env, “java/lang/IllegalStateException”,
“cannot initialize audio group”);
goto error;
}
}

// Add audio stream into audio group.
if (!group->add(stream)) {//將native的stream添加到Group中
jniThrowException(env, “java/lang/IllegalStateException”,
“cannot add audio stream”);
goto error;
}
// Succeed.
env->SetIntField(thiz, gNative, (int)group);//將native的Group指針轉換為整型變量存放於Java實例的成員變量中

再來詳細看一下上述的幾個過程。在創建native層的AudioCodec時,會根據查詢預設的數組裡保存的名稱,得到與之對應創建函數,從而調用創建函數得到AudioCodec。在AudioCodec.cpp中的預設的數組如下:

struct AudioCodecType {//結構體定義
const char *name; //Codec名稱
AudioCodec *(*create)();//對應的創建函數
} gAudioCodecTypes[] = {//全局數組
{“PCMA”, newAlawCodec},//G.711 a-law語音編碼
{“PCMU”, newUlawCodec}, //G.711 u-law語音編碼
{“GSM”, newGsmCodec}, //GSM全速率語音編碼,也稱作GSM-FR、GSM 06.10、 GSM, 或FR
{“AMR”, newAmrCodec},//適應性多速率窄帶語音編碼AMR或AMR-NB,當前不支持 CRC校驗、健壯性排序(robust sorting)和交織(interleaving) ,更多features參見RFC 4867.
{“GSM-EFR”, newGsmEfrCodec},//增強型GSM全速率語音編碼,也稱作GSM-EFR, GSM 06.60或EFR
{NULL, NULL},
};

這些C++實現的Codec都繼承自AudioCodec,實現了其set、encode和decode函數,如下圖:

函數encode和decode用於編碼和解碼,set函數用於設置相關信息給Codec。

上述add函數的native實現在創建完native層的語音流後,調用了其set函數,將相關信息設置給AudioStream,這些信息包括:mode、socket、遠程地址、采樣率、采樣數、解碼器類型和DTMF類型等。在set函數中,另一個重要的操作是為抖動緩沖器(Jitter Buffer)分配內存。由於網絡擁塞、時間漂移或路由變更,網絡數據包多半都不是均勻到來,為了讓語音不失真,抖動緩沖器收集數據包並均勻地將其送到語音處理器,這樣就可以清晰地回放對方的聲音。

上述add函數的native實現最後創建AudioGroup對象,在創建AudioGroup對象時會創建兩個線程NetworkThread和DeviceThread,接著會調用AudioGroup的set函數和setMode函數;最後將AudioStream對象添加到AudioGroup維護的映射表中。

在AudioGroup的set函數中,調用epoll_create創建一個輪詢描述符,然後調用socketpair得到一個連接的socket對,其中第一個套接字pair[0]賦值給成員變量mDeviceSocket,第二個套接字pair[1]用於新創建的AudioStream:

bool AudioGroup::set(int sampleRate, int sampleCount)
{
mEventQueue = epoll_create(2);//創建一個輪詢描述符,用於監控socket上的IO事件
if (mEventQueue == -1) {//若創建失敗
LOGE(“epoll_create: %s”, strerror(errno));
return false;
}

mSampleRate = sampleRate;
mSampleCount = sampleCount;
// Create device socket.
int pair[2];
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, pair)) {//得到連接的套接字對
LOGE(“socketpair: %s”, strerror(errno));
return false;
}
mDeviceSocket = pair[0];
// Create device stream.
mChain = new AudioStream;//創建一設備流(Device AudioStream)
if (!mChain->set(AudioStream::NORMAL, pair[1], NULL, NULL,
sampleRate, sampleCount, -1, -1)) {//這個設備流AudioStream使用套接字對的第二個套接字,無遠程地址,無Codec
close(pair[1]);//失敗則關閉第二個套接字
LOGE(“cannot initialize device stream”);
return false;
}

// Give device socket a reasonable timeout.
timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 1000 * sampleCount / sampleRate * 500;//計算超時時間
if (setsockopt(pair[0], SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)))//為第一個套接字設置超時時間 {
LOGE(“setsockopt: %s”, strerror(errno));
return false;
}

// Add device stream into event queue.
epoll_event event;//輪詢事件
event.events = EPOLLIN;//監控讀事件
event.data.ptr = mChain;
if (epoll_ctl(mEventQueue, EPOLL_CTL_ADD, pair[1], &event)) {//注冊第二個socket,讓epoll監控其讀事件
LOGE(“epoll_ctl: %s”, strerror(errno));
return false;
}

// Anything else?
LOGD(“stream[%d] joins group[%d]“, pair[1], pair[0]);
return true;
}

回聲抑制EchoSuppressor

回聲抑制的實現在C++類EchoSuppressor中,它是依據一定的算法減少通話回聲,但沒有完全消除。法實現在其run函數中。在EchoSuppressor的代碼注釋中,對其做了相關說明。

DeviceThread線程

線程DeviceThread的任務與設備的聲音IO打交道,它使用AudioTrack去播放對方聲音(輸出,output),使用AudioRecord去錄制自己聲音(輸入,input)。它在聲明了AudioRecord和AudioTrack局部變量後,對AudioTrack和AudioRecord進行參數設置;然後設置套接字mDeviceSocket(即AudioGroup的set函數中的一個套接字pair[0])的接收和發送緩沖區;再接著檢查平台是否支持AEC音效,如果支持,則創建該音效,若不支持,則使用回聲抑制;再接著讓AudioRecord和AudioTrack開始工作(調用它們的start函數);最後,進入while循環。在while循環中,使用recv接收數據,然後將這些數據送到(memcpy)AudioTrack。同時,也將AudioRecord中錄入的音頻數據通過send送往出去。

AudioGroup的mDeviceSocket

設備Audiostream的mSocket

send

AudioRecord

pair[0]

pair[1]

socket pair

recv

AudioTrack

下面的代碼片段摘自線程DeviceThread的線程循環函數threadLoop,它先將從套接字對上的另一端送來的數據接收到output緩沖區中,然後從AudioTrack中獲取一個可寫的緩沖區buffer,將output緩沖區中的數據拷貝到AudioTrack的緩沖區buffer中,送由AudioFlinger進行播放:

int16_t output[sampleCount];
if (recv(deviceSocket, output, sizeof(output), 0) <= 0) {//從套接字對的另一端接收數據
memset(output, 0, sizeof(output));
}
…//省略部分代碼

status_t status = track.obtainBuffer(&buffer, 1);
if (status == NO_ERROR) {
int offset = sampleCount – toWrite;
memcpy(buffer.i8, &output[offset], buffer.size);//將接收的數據復制到AudioTrack的緩沖區,送往AudioFlinger播放
toWrite -= buffer.frameCount;
track.releaseBuffer(&buffer);

對應地,在線程循環中,它使用AudioRecord來獲取輸入設備(通過AudioFlinger)中的PCM數據,經過回聲抑制(若沒有使用音效)處理後,發送本地的聲音的數據發送出去。其主要代碼片段如下:

status_t status = record.obtainBuffer(&buffer, 1);
if (status == NO_ERROR) {
int offset = sampleCount – toRead;
memcpy(&input[offset], buffer.i8, buffer.size);//將來自AudiRecord中的數據(來自AudioFlinger側)復制到input緩沖區
toRead -= buffer.frameCount;
record.releaseBuffer(&buffer);

…//省略部分代碼

if (mode != MUTED) {//若沒有靜音
if (echo != NULL) {//若使用回聲抑制
LOGV(“echo->run()”);
echo->run(output, input);//抑制對端的聲音(output),得到新的錄音input
}
send(deviceSocket, input, sizeof(input), MSG_DONTWAIT);//將錄制的聲音送到套接字對的另一端
}

NetworkThread線程

線程NetworkThread主要用於調用AudioStream的encode函數編碼然後將數據發送出去、發送DTMF事件以及調用decode函數接收並解碼數據。其threadLoop函數如下:

bool AudioGroup::NetworkThread::threadLoop()
{
AudioStream *chain = mGroup->mChain;
int tick = elapsedRealtime();
int deadline = tick + 10;
int count = 0;
for (AudioStream *stream = chain; stream; stream = stream->mNext) {
if (tick – stream->mTick >= 0) {
stream->encode(tick, chain);//編碼數據並發送
}
if (deadline – stream->mTick > 0) {
deadline = stream->mTick;
}
++count;
}

//調用各個AudioStream發送DTMF
int event = mGroup->mDtmfEvent;
if (event != -1) {
for (AudioStream *stream = chain; stream; stream = stream->mNext) {
stream->sendDtmf(event);
}
mGroup->mDtmfEvent = -1;
}

deadline -= tick;
if (deadline < 1) {
deadline = 1;
}
epoll_event events[count];
count = epoll_wait(mGroup->mEventQueue, events, count, deadline);//等待讀事件發生,各個AudioStream都注冊了自己的socket,用於監聽
if (count == -1) {
LOGE(“epoll_wait: %s”, strerror(errno));
return false;
}
for (int i = 0; i < count; ++i) {
((AudioStream *)events[i].data.ptr)->decode(tick);//接收數據並解碼
}
return true;
}

AudioStream的編碼發送函數encode

void AudioStream::encode(int tick, AudioStream *chain)
{

if (tick – mTick >= mInterval) {
// We just missed the train. Pretend that packets in between are lost.
int skipped = (tick – mTick) / mInterval;
mTick += skipped * mInterval;
mSequence += skipped;
mTimestamp += skipped * mSampleCount;
LOGV(“stream[%d] skips %d packets”, mSocket, skipped);
}
tick = mTick;
mTick += mInterval;
++mSequence;
mTimestamp += mSampleCount;

// If there is an ongoing DTMF event, send it now.
if (mMode != RECEIVE_ONLY && mDtmfEvent != -1) {//下面的一段代碼發送DTMF
int duration = mTimestamp – mDtmfStart;
// Make sure duration is reasonable.
if (duration >= 0 && duration < mSampleRate * DTMF_PERIOD) {
duration += mSampleCount;
int32_t buffer[4] = {//填充32字節的數據
htonl(mDtmfMagic | mSequence),
htonl(mDtmfStart),
mSsrc,
htonl(mDtmfEvent | duration),
};
if (duration >= mSampleRate * DTMF_PERIOD) {
buffer[3] |= htonl(1 << 23);
mDtmfEvent = -1;
}
sendto(mSocket, buffer, sizeof(buffer), MSG_DONTWAIT,
(sockaddr *)&mRemote, sizeof(mRemote));//將上面的數據發送給遠程
return;
}
mDtmfEvent = -1;
}


int32_t buffer[mSampleCount + 3];//存放待發送數據緩沖區:采樣數再加3字節
bool data = false;
if (mMode != RECEIVE_ONLY) {
// Mix all other streams.
memset(buffer, 0, sizeof(buffer));
while (chain) {
if (chain != this) {
data |= chain->mix(buffer, tick – mInterval, tick, mSampleRate);//一個采樣間隔內的數據混合,來自抖動緩沖器中的數據
}
chain = chain->mNext;
}
}

int16_t samples[mSampleCount];
if (data) {//將32位緩沖區數據轉換為16位類型的緩沖區
// Saturate into 16 bits.
for (int i = 0; i < mSampleCount; ++i) {
int32_t sample = buffer[i];
if (sample < -32768) {
sample = -32768;
}
if (sample > 32767) {
sample = 32767;
}
samples[i] = sample;//賦值
}
} else {
if ((mTick ^ mKeepAlive) >> 10 == 0) {
return;
}
mKeepAlive = mTick;
memset(samples, 0, sizeof(samples));

if (mMode != RECEIVE_ONLY) {
LOGV(“stream[%d] no data”, mSocket);
}
}

if (!mCodec) {//設備AudioStream沒有對應的codec
// Special case for device stream.
send(mSocket, samples, sizeof(samples), MSG_DONTWAIT);//將緩沖區samples裡的數據送給套接字對中的第一個套接字。
return;
}


// Cook the packet and send it out.
//加上頭數據
buffer[0] = htonl(mCodecMagic | mSequence);
buffer[1] = htonl(mTimestamp);
buffer[2] = mSsrc;
int length = mCodec->encode(&buffer[3], samples);//調用編碼器對聲音數據進行編碼(不包括前面的3字節頭)
if (length <= 0) {
LOGV(“stream[%d] encoder error”, mSocket);
return;
}
sendto(mSocket, buffer, length + 12, MSG_DONTWAIT, (sockaddr *)&mRemote, sizeof(mRemote)); //發送給遠程主機

}

普通AudioStream

“普通”是相對設備流(Device Stream)而言,它與遠程主機交互。

對於普通的AudioStream,在encode函數中,首先添加包頭,然後編碼,接著發送給遠方主機:

// Cook the packet and send it out.
buffer[0] = htonl(mCodecMagic | mSequence);
buffer[1] = htonl(mTimestamp);
buffer[2] = mSsrc;//添加包頭
int length = mCodec->encode(&buffer[3], samples);//使用Codec編碼
if (length <= 0) {
LOGV(“stream[%d] encoder error”, mSocket);
return;
}
sendto(mSocket, buffer, length + 12, MSG_DONTWAIT, (sockaddr *)&mRemote, sizeof(mRemote));//發送給遠程主機

對於普通的AudioStream,在decode函數中,首先是接收數據存放到緩沖區數組中,然後解包頭,再接著使用Codec解碼:

__attribute__((aligned(4))) uint8_t buffer[2048];//緩沖區數組
sockaddr_storage remote;//此處應該是mRemote?
socklen_t addrlen = sizeof(remote);
int length = recvfrom(mSocket, buffer, sizeof(buffer), MSG_TRUNC | MSG_DONTWAIT, (sockaddr *)&remote, &addrlen);//接收遠程主機數據,存放於緩沖區數組buffer中
// Do we need to check SSRC, sequence, and timestamp? They are not
// reliable but at least they can be used to identify duplicates?
//下面的一部分代碼是解析包頭
if (length < 12 || length > (int)sizeof(buffer) || (ntohl(*(uint32_t *)buffer) & 0xC07F0000) != mCodecMagic) {
LOGV(“stream[%d] malformed packet”, mSocket);
return;
}
int offset = 12 + ((buffer[0] & 0x0F) << 2);
if ((buffer[0] & 0×10) != 0) {
offset += 4 + (ntohs(*(uint16_t *)&buffer[offset + 2]) << 2);
}
if ((buffer[0] & 0×20) != 0) {
length -= buffer[length - 1];
}
length -= offset;
if (length >= 0) {
length = mCodec->decode(samples, count, &buffer[offset], length);//使用Codec解碼,解碼後的采樣數據存放於samples數組中
}
if (length > 0 && mFixRemote) {
mRemote = remote;
mFixRemote = false;
}
count = length;

最後將解碼後的數據存放到mBuffer中:

for (int i = 0; i < count; ++i) {
mBuffer[tail & mBufferMask] = samples[i];//賦值給mBuffer數組
++tail;
}

設備流(Device Stream)

在decode函數中,對於設備流AuioStream來說,pair[1]套接字接收數據,即來自pair[0]發送來的數據,也就是本地錄音采樣的數據,被存放到緩沖區samples數組裡面。

int16_t samples[count];
if (!mCodec) {
// Special case for device stream.
count = recv(mSocket, samples, sizeof(samples),
MSG_TRUNC | MSG_DONTWAIT) >> 1;
}

最後這些數據存放到mBuffer緩沖區裡面:

// Append to the jitter buffer.
int tail = mBufferTail * mSampleRate;
for (int i = 0; i < count; ++i) {
mBuffer[tail & mBufferMask] = samples[i];
++tail;
}

對於Device AudioStream來說,在encode函數中,套接字pair[1]發送數據,在對端套接字pair[0](在DeviceThread中)接收數據,送往AudioTrack進行播放。

if (!mCodec) {

// Special case for device stream.

send(mSocket, samples, sizeof(samples), MSG_DONTWAIT);

return;

}

TODO:猜測,網絡側的流被普通AudioStream接收並解碼(decode函數),存放在抖動緩沖區中,然後被設備流中的encode函數mix後,發送往pair[0],也就是DeviceThread線程,接著被送往AudioTrack進行播放;相反地,DeviceThread線程中,來自於AuioRecord的聲音數據,設備流中的套接字pair[1]接收(decode函數)後,存放於緩沖區mBuffer中,然後被普通流的encode函數發送出去。TODO:mBuffer的用法?

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved