Android源码分析:VoIP

概述

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<AudioStream, Integer> 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;//
创建一个nativeAudioGroup
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)) {//
nativestream添加到Group
jniThrowException(env, “java/lang/IllegalStateException”,
“cannot add audio stream”);
goto error;
}
// Succeed.
env->SetIntField(thiz, gNative, (int)group);//
nativeGroup指针转换为整型变量存放于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-FRGSM 06.10、 GSM, FR
{“AMR”, newAmrCodec},//
适应性多速率窄带语音编码AMRAMR-NB,当前不支持 CRC校验、健壮性排序(robust sorting)和交织(interleaving) ,更多features参见RFC 4867.
{“GSM-EFR”, newGsmEfrCodec},//
增强型GSM全速率语音编码,也称作GSM-EFR, GSM 06.60EFR
{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的用法?

Android源码分析:VoIP,布布扣,bubuko.com

Android源码分析:VoIP

上一篇:《OD学微信开发》微信小程序入门示例


下一篇:Android 如何在任意界面按某个预设定的硬体按键进入某个Activity?