diff options
Diffstat (limited to 'voip')
25 files changed, 4798 insertions, 0 deletions
diff --git a/voip/java/android/net/rtp/AudioCodec.java b/voip/java/android/net/rtp/AudioCodec.java new file mode 100644 index 0000000..89e6aa9 --- /dev/null +++ b/voip/java/android/net/rtp/AudioCodec.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.rtp; + +/** @hide */ +public class AudioCodec { + public static final AudioCodec ULAW = new AudioCodec("PCMU", 8000, 160, 0); + public static final AudioCodec ALAW = new AudioCodec("PCMA", 8000, 160, 8); + + /** + * Returns system supported codecs. + */ + public static AudioCodec[] getSystemSupportedCodecs() { + return new AudioCodec[] {AudioCodec.ULAW, AudioCodec.ALAW}; + } + + /** + * Returns the codec instance if it is supported by the system. + * + * @param name name of the codec + * @return the matched codec or null if the codec name is not supported by + * the system + */ + public static AudioCodec getSystemSupportedCodec(String name) { + for (AudioCodec codec : getSystemSupportedCodecs()) { + if (codec.name.equals(name)) return codec; + } + return null; + } + + public final String name; + public final int sampleRate; + public final int sampleCount; + public final int defaultType; + + private AudioCodec(String name, int sampleRate, int sampleCount, int defaultType) { + this.name = name; + this.sampleRate = sampleRate; + this.sampleCount = sampleCount; + this.defaultType = defaultType; + } +} diff --git a/voip/java/android/net/rtp/AudioGroup.java b/voip/java/android/net/rtp/AudioGroup.java new file mode 100644 index 0000000..37cc121 --- /dev/null +++ b/voip/java/android/net/rtp/AudioGroup.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.rtp; + +import java.util.HashMap; +import java.util.Map; + +/** + */ +/** @hide */ +public class AudioGroup { + public static final int MODE_ON_HOLD = 0; + public static final int MODE_MUTED = 1; + public static final int MODE_NORMAL = 2; + public static final int MODE_EC_ENABLED = 3; + + private final Map<AudioStream, Integer> mStreams; + private int mMode = MODE_ON_HOLD; + + private int mNative; + static { + System.loadLibrary("rtp_jni"); + } + + public AudioGroup() { + mStreams = new HashMap<AudioStream, Integer>(); + } + + public int getMode() { + return mMode; + } + + public synchronized native void setMode(int mode); + + synchronized void add(AudioStream stream, AudioCodec codec, int codecType, int dtmfType) { + if (!mStreams.containsKey(stream)) { + try { + int socket = stream.dup(); + add(stream.getMode(), socket, + stream.getRemoteAddress().getHostAddress(), stream.getRemotePort(), + codec.name, codec.sampleRate, codec.sampleCount, codecType, dtmfType); + mStreams.put(stream, socket); + } catch (NullPointerException e) { + throw new IllegalStateException(e); + } + } + } + + private native void add(int mode, int socket, String remoteAddress, int remotePort, + String codecName, int sampleRate, int sampleCount, int codecType, int dtmfType); + + synchronized void remove(AudioStream stream) { + Integer socket = mStreams.remove(stream); + if (socket != null) { + remove(socket); + } + } + + private native void remove(int socket); + + /** + * Sends a DTMF digit to every {@link AudioStream} in this group. Currently + * only event {@code 0} to {@code 15} are supported. + * + * @throws IllegalArgumentException if the event is invalid. + */ + public native synchronized void sendDtmf(int event); + + public synchronized void reset() { + remove(-1); + } + + @Override + protected void finalize() throws Throwable { + reset(); + super.finalize(); + } +} diff --git a/voip/java/android/net/rtp/AudioStream.java b/voip/java/android/net/rtp/AudioStream.java new file mode 100644 index 0000000..a955fd2 --- /dev/null +++ b/voip/java/android/net/rtp/AudioStream.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.rtp; + +import java.net.InetAddress; +import java.net.SocketException; + +/** + * AudioStream represents a RTP stream carrying audio payloads. + */ +/** @hide */ +public class AudioStream extends RtpStream { + private AudioCodec mCodec; + private int mCodecType = -1; + private int mDtmfType = -1; + private AudioGroup mGroup; + + /** + * Creates an AudioStream on the given local address. Note that the local + * port is assigned automatically to conform with RFC 3550. + * + * @param address The network address of the local host to bind to. + * @throws SocketException if the address cannot be bound or a problem + * occurs during binding. + */ + public AudioStream(InetAddress address) throws SocketException { + super(address); + } + + /** + * Returns {@code true} if the stream already joined an {@link AudioGroup}. + */ + @Override + public final boolean isBusy() { + return mGroup != null; + } + + /** + * Returns the joined {@link AudioGroup}. + */ + public AudioGroup getAudioGroup() { + return mGroup; + } + + /** + * Joins an {@link AudioGroup}. Each stream can join only one group at a + * time. The group can be changed by passing a different one or removed + * by calling this method with {@code null}. + * + * @param group The AudioGroup to join or {@code null} to leave. + * @throws IllegalStateException if the stream is not properly configured. + * @see AudioGroup + */ + public void join(AudioGroup group) { + if (mGroup == group) { + return; + } + if (mGroup != null) { + mGroup.remove(this); + mGroup = null; + } + if (group != null) { + group.add(this, mCodec, mCodecType, mDtmfType); + mGroup = group; + } + } + + /** + * Sets the {@link AudioCodec} and its RTP payload type. According to RFC + * 3551, the type must be in the range of 0 and 127, where 96 and above are + * dynamic types. For codecs with static mappings (non-negative + * {@link AudioCodec#defaultType}), assigning a different non-dynamic type + * is disallowed. + * + * @param codec The AudioCodec to be used. + * @param type The RTP payload type. + * @throws IllegalArgumentException if the type is invalid or used by DTMF. + * @throws IllegalStateException if the stream is busy. + */ + public void setCodec(AudioCodec codec, int type) { + if (isBusy()) { + throw new IllegalStateException("Busy"); + } + if (type < 0 || type > 127 || (type != codec.defaultType && type < 96)) { + throw new IllegalArgumentException("Invalid type"); + } + if (type == mDtmfType) { + throw new IllegalArgumentException("The type is used by DTMF"); + } + mCodec = codec; + mCodecType = type; + } + + /** + * Sets the RTP payload type for dual-tone multi-frequency (DTMF) digits. + * The primary usage is to send digits to the remote gateway to perform + * certain tasks, such as second-stage dialing. According to RFC 2833, the + * RTP payload type for DTMF is assigned dynamically, so it must be in the + * range of 96 and 127. One can use {@code -1} to disable DTMF and free up + * the previous assigned value. This method cannot be called when the stream + * already joined an {@link AudioGroup}. + * + * @param type The RTP payload type to be used or {@code -1} to disable it. + * @throws IllegalArgumentException if the type is invalid or used by codec. + * @throws IllegalStateException if the stream is busy. + * @see AudioGroup#sendDtmf(int) + */ + public void setDtmfType(int type) { + if (isBusy()) { + throw new IllegalStateException("Busy"); + } + if (type != -1) { + if (type < 96 || type > 127) { + throw new IllegalArgumentException("Invalid type"); + } + if (type == mCodecType) { + throw new IllegalArgumentException("The type is used by codec"); + } + } + mDtmfType = type; + } +} diff --git a/voip/java/android/net/rtp/RtpStream.java b/voip/java/android/net/rtp/RtpStream.java new file mode 100644 index 0000000..ef5ca17 --- /dev/null +++ b/voip/java/android/net/rtp/RtpStream.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.rtp; + +import java.net.InetAddress; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.SocketException; + +/** + * RtpStream represents a base class of media streams running over + * Real-time Transport Protocol (RTP). + */ +/** @hide */ +public class RtpStream { + public static final int MODE_NORMAL = 0; + public static final int MODE_SEND_ONLY = 1; + public static final int MODE_RECEIVE_ONLY = 2; + + private final InetAddress mLocalAddress; + private final int mLocalPort; + + private InetAddress mRemoteAddress; + private int mRemotePort = -1; + private int mMode = MODE_NORMAL; + + private int mNative; + static { + System.loadLibrary("rtp_jni"); + } + + /** + * Creates a RtpStream on the given local address. Note that the local + * port is assigned automatically to conform with RFC 3550. + * + * @param address The network address of the local host to bind to. + * @throws SocketException if the address cannot be bound or a problem + * occurs during binding. + */ + RtpStream(InetAddress address) throws SocketException { + mLocalPort = create(address.getHostAddress()); + mLocalAddress = address; + } + + private native int create(String address) throws SocketException; + + /** + * Returns the network address of the local host. + */ + public InetAddress getLocalAddress() { + return mLocalAddress; + } + + /** + * Returns the network port of the local host. + */ + public int getLocalPort() { + return mLocalPort; + } + + /** + * Returns the network address of the remote host or {@code null} if the + * stream is not associated. + */ + public InetAddress getRemoteAddress() { + return mRemoteAddress; + } + + /** + * Returns the network port of the remote host or {@code -1} if the stream + * is not associated. + */ + public int getRemotePort() { + return mRemotePort; + } + + /** + * Returns {@code true} if the stream is busy. This method is intended to be + * overridden by subclasses. + */ + public boolean isBusy() { + return false; + } + + /** + * Returns the current mode. The initial mode is {@link #MODE_NORMAL}. + */ + public int getMode() { + return mMode; + } + + /** + * Changes the current mode. It must be one of {@link #MODE_NORMAL}, + * {@link #MODE_SEND_ONLY}, and {@link #MODE_RECEIVE_ONLY}. + * + * @param mode The mode to change to. + * @throws IllegalArgumentException if the mode is invalid. + * @throws IllegalStateException if the stream is busy. + * @see #isBusy() + */ + public void setMode(int mode) { + if (isBusy()) { + throw new IllegalStateException("Busy"); + } + if (mode != MODE_NORMAL && mode != MODE_SEND_ONLY && mode != MODE_RECEIVE_ONLY) { + throw new IllegalArgumentException("Invalid mode"); + } + mMode = mode; + } + + /** + * Associates with a remote host. + * + * @param address The network address of the remote host. + * @param port The network port of the remote host. + * @throws IllegalArgumentException if the address is not supported or the + * port is invalid. + * @throws IllegalStateException if the stream is busy. + * @see #isBusy() + */ + public void associate(InetAddress address, int port) { + if (isBusy()) { + throw new IllegalStateException("Busy"); + } + if (!(address instanceof Inet4Address && mLocalAddress instanceof Inet4Address) && + !(address instanceof Inet6Address && mLocalAddress instanceof Inet6Address)) { + throw new IllegalArgumentException("Unsupported address"); + } + if (port < 0 || port > 65535) { + throw new IllegalArgumentException("Invalid port"); + } + mRemoteAddress = address; + mRemotePort = port; + } + + synchronized native int dup(); + + /** + * Releases allocated resources. The stream becomes inoperable after calling + * this method. + * + * @throws IllegalStateException if the stream is busy. + * @see #isBusy() + */ + public void release() { + if (isBusy()) { + throw new IllegalStateException("Busy"); + } + close(); + } + + private synchronized native void close(); + + @Override + protected void finalize() throws Throwable { + close(); + super.finalize(); + } +} diff --git a/voip/java/android/net/sip/ISipService.aidl b/voip/java/android/net/sip/ISipService.aidl new file mode 100644 index 0000000..6c68213 --- /dev/null +++ b/voip/java/android/net/sip/ISipService.aidl @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import android.net.sip.ISipSession; +import android.net.sip.ISipSessionListener; +import android.net.sip.SipProfile; + +/** + * {@hide} + */ +interface ISipService { + void open(in SipProfile localProfile); + void open3(in SipProfile localProfile, + String incomingCallBroadcastAction, + in ISipSessionListener listener); + void close(in String localProfileUri); + boolean isOpened(String localProfileUri); + boolean isRegistered(String localProfileUri); + void setRegistrationListener(String localProfileUri, + ISipSessionListener listener); + + ISipSession createSession(in SipProfile localProfile, + in ISipSessionListener listener); + ISipSession getPendingSession(String callId); + + SipProfile[] getListOfProfiles(); +} diff --git a/voip/java/android/net/sip/ISipSession.aidl b/voip/java/android/net/sip/ISipSession.aidl new file mode 100644 index 0000000..fbcb056 --- /dev/null +++ b/voip/java/android/net/sip/ISipSession.aidl @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import android.net.sip.ISipSessionListener; +import android.net.sip.SessionDescription; +import android.net.sip.SipProfile; + +/** + * A SIP session that is associated with a SIP dialog or a transaction + * (e.g., registration) not within a dialog. + * @hide + */ +interface ISipSession { + /** + * Gets the IP address of the local host on which this SIP session runs. + * + * @return the IP address of the local host + */ + String getLocalIp(); + + /** + * Gets the SIP profile that this session is associated with. + * + * @return the SIP profile that this session is associated with + */ + SipProfile getLocalProfile(); + + /** + * Gets the SIP profile that this session is connected to. Only available + * when the session is associated with a SIP dialog. + * + * @return the SIP profile that this session is connected to + */ + SipProfile getPeerProfile(); + + /** + * Gets the session state. The value returned must be one of the states in + * {@link SipSessionState}. One may convert it to {@link SipSessionState} by + * <code> + * Enum.valueOf(SipSessionState.class, session.getState()); + * </code> + * + * @return the session state + */ + String getState(); + + /** + * Checks if the session is in a call. + * + * @return true if the session is in a call + */ + boolean isInCall(); + + /** + * Gets the call ID of the session. + * + * @return the call ID + */ + String getCallId(); + + + /** + * Sets the listener to listen to the session events. A {@link ISipSession} + * can only hold one listener at a time. Subsequent calls to this method + * override the previous listener. + * + * @param listener to listen to the session events of this object + */ + void setListener(in ISipSessionListener listener); + + + /** + * Performs registration to the server specified by the associated local + * profile. The session listener is called back upon success or failure of + * registration. The method is only valid to call when the session state is + * in {@link SipSessionState#READY_TO_CALL}. + * + * @param duration duration in second before the registration expires + * @see ISipSessionListener + */ + void register(int duration); + + /** + * Performs unregistration to the server specified by the associated local + * profile. Unregistration is technically the same as registration with zero + * expiration duration. The session listener is called back upon success or + * failure of unregistration. The method is only valid to call when the + * session state is in {@link SipSessionState#READY_TO_CALL}. + * + * @see ISipSessionListener + */ + void unregister(); + + /** + * Initiates a call to the specified profile. The session listener is called + * back upon defined session events. The method is only valid to call when + * the session state is in {@link SipSessionState#READY_TO_CALL}. + * + * @param callee the SIP profile to make the call to + * @param sessionDescription the session description of this call + * @see ISipSessionListener + */ + void makeCall(in SipProfile callee, + in SessionDescription sessionDescription); + + /** + * Answers an incoming call with the specified session description. The + * method is only valid to call when the session state is in + * {@link SipSessionState#INCOMING_CALL}. + * + * @param sessionDescription the session description to answer this call + */ + void answerCall(in SessionDescription sessionDescription); + + /** + * Ends an established call, terminates an outgoing call or rejects an + * incoming call. The method is only valid to call when the session state is + * in {@link SipSessionState#IN_CALL}, + * {@link SipSessionState#INCOMING_CALL}, + * {@link SipSessionState#OUTGOING_CALL} or + * {@link SipSessionState#OUTGOING_CALL_RING_BACK}. + */ + void endCall(); + + /** + * Changes the session description during a call. The method is only valid + * to call when the session state is in {@link SipSessionState#IN_CALL}. + * + * @param sessionDescription the new session description + */ + void changeCall(in SessionDescription sessionDescription); +} diff --git a/voip/java/android/net/sip/ISipSessionListener.aidl b/voip/java/android/net/sip/ISipSessionListener.aidl new file mode 100644 index 0000000..8570958 --- /dev/null +++ b/voip/java/android/net/sip/ISipSessionListener.aidl @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import android.net.sip.ISipSession; +import android.net.sip.SipProfile; + +/** + * Listener class to listen to SIP session events. + * @hide + */ +interface ISipSessionListener { + /** + * Called when an INVITE request is sent to initiate a new call. + * + * @param session the session object that carries out the transaction + */ + void onCalling(in ISipSession session); + + /** + * Called when an INVITE request is received. + * + * @param session the session object that carries out the transaction + * @param caller the SIP profile of the caller + * @param sessionDescription the caller's session description + */ + void onRinging(in ISipSession session, in SipProfile caller, + in byte[] sessionDescription); + + /** + * Called when a RINGING response is received for the INVITE request sent + * + * @param session the session object that carries out the transaction + */ + void onRingingBack(in ISipSession session); + + /** + * Called when the session is established. + * + * @param session the session object that is associated with the dialog + * @param sessionDescription the peer's session description + */ + void onCallEstablished(in ISipSession session, + in byte[] sessionDescription); + + /** + * Called when the session is terminated. + * + * @param session the session object that is associated with the dialog + */ + void onCallEnded(in ISipSession session); + + /** + * Called when the peer is busy during session initialization. + * + * @param session the session object that carries out the transaction + */ + void onCallBusy(in ISipSession session); + + /** + * Called when an error occurs during session initialization and + * termination. + * + * @param session the session object that carries out the transaction + * @param errorClass name of the exception class + * @param errorMessage error message + */ + void onError(in ISipSession session, String errorClass, + String errorMessage); + + /** + * Called when an error occurs during session modification negotiation. + * + * @param session the session object that carries out the transaction + * @param errorClass name of the exception class + * @param errorMessage error message + */ + void onCallChangeFailed(in ISipSession session, String errorClass, + String errorMessage); + + /** + * Called when a registration request is sent. + * + * @param session the session object that carries out the transaction + */ + void onRegistering(in ISipSession session); + + /** + * Called when registration is successfully done. + * + * @param session the session object that carries out the transaction + * @param duration duration in second before the registration expires + */ + void onRegistrationDone(in ISipSession session, int duration); + + /** + * Called when the registration fails. + * + * @param session the session object that carries out the transaction + * @param errorClass name of the exception class + * @param errorMessage error message + */ + void onRegistrationFailed(in ISipSession session, String errorClass, + String errorMessage); + + /** + * Called when the registration gets timed out. + * + * @param session the session object that carries out the transaction + */ + void onRegistrationTimeout(in ISipSession session); +} diff --git a/voip/java/android/net/sip/SdpSessionDescription.java b/voip/java/android/net/sip/SdpSessionDescription.java new file mode 100644 index 0000000..0c29935 --- /dev/null +++ b/voip/java/android/net/sip/SdpSessionDescription.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import gov.nist.javax.sdp.SessionDescriptionImpl; +import gov.nist.javax.sdp.fields.AttributeField; +import gov.nist.javax.sdp.fields.ConnectionField; +import gov.nist.javax.sdp.fields.MediaField; +import gov.nist.javax.sdp.fields.OriginField; +import gov.nist.javax.sdp.fields.ProtoVersionField; +import gov.nist.javax.sdp.fields.SessionNameField; +import gov.nist.javax.sdp.fields.TimeField; +import gov.nist.javax.sdp.parser.SDPAnnounceParser; + +import android.util.Log; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Vector; +import javax.sdp.Connection; +import javax.sdp.MediaDescription; +import javax.sdp.SdpException; + +/** + * A session description that follows SDP (Session Description Protocol). + * Refer to <a href="http://tools.ietf.org/html/rfc4566">RFC 4566</a>. + * @hide + */ +public class SdpSessionDescription extends SessionDescription { + private static final String TAG = "SDP"; + private static final String AUDIO = "audio"; + private static final String RTPMAP = "rtpmap"; + private static final String PTIME = "ptime"; + private static final String SENDONLY = "sendonly"; + private static final String RECVONLY = "recvonly"; + private static final String INACTIVE = "inactive"; + + private SessionDescriptionImpl mSessionDescription; + + /** + * The audio codec information parsed from "rtpmap". + */ + public static class AudioCodec { + public final int payloadType; + public final String name; + public final int sampleRate; + public final int sampleCount; + + public AudioCodec(int payloadType, String name, int sampleRate, + int sampleCount) { + this.payloadType = payloadType; + this.name = name; + this.sampleRate = sampleRate; + this.sampleCount = sampleCount; + } + } + + /** + * The builder class used to create an {@link SdpSessionDescription} object. + */ + public static class Builder { + private SdpSessionDescription mSdp = new SdpSessionDescription(); + private SessionDescriptionImpl mSessionDescription; + + public Builder(String sessionName) throws SdpException { + mSessionDescription = new SessionDescriptionImpl(); + mSdp.mSessionDescription = mSessionDescription; + try { + ProtoVersionField proto = new ProtoVersionField(); + proto.setVersion(0); + mSessionDescription.addField(proto); + + TimeField time = new TimeField(); + time.setZero(); + mSessionDescription.addField(time); + + SessionNameField session = new SessionNameField(); + session.setValue(sessionName); + mSessionDescription.addField(session); + } catch (Exception e) { + throwSdpException(e); + } + } + + public Builder setConnectionInfo(String networkType, String addressType, + String addr) throws SdpException { + try { + ConnectionField connection = new ConnectionField(); + connection.setNetworkType(networkType); + connection.setAddressType(addressType); + connection.setAddress(addr); + mSessionDescription.addField(connection); + } catch (Exception e) { + throwSdpException(e); + } + return this; + } + + public Builder setOrigin(SipProfile user, long sessionId, + long sessionVersion, String networkType, String addressType, + String address) throws SdpException { + try { + OriginField origin = new OriginField(); + origin.setUsername(user.getUserName()); + origin.setSessionId(sessionId); + origin.setSessionVersion(sessionVersion); + origin.setAddressType(addressType); + origin.setNetworkType(networkType); + origin.setAddress(address); + mSessionDescription.addField(origin); + } catch (Exception e) { + throwSdpException(e); + } + return this; + } + + public Builder addMedia(String media, int port, int numPorts, + String transport, Integer... types) throws SdpException { + MediaField field = new MediaField(); + Vector<Integer> typeVector = new Vector<Integer>(); + Collections.addAll(typeVector, types); + try { + field.setMediaType(media); + field.setMediaPort(port); + field.setPortCount(numPorts); + field.setProtocol(transport); + field.setMediaFormats(typeVector); + mSessionDescription.addField(field); + } catch (Exception e) { + throwSdpException(e); + } + return this; + } + + public Builder addMediaAttribute(String type, String name, String value) + throws SdpException { + try { + MediaDescription md = mSdp.getMediaDescription(type); + if (md == null) { + throw new SdpException("Should add media first!"); + } + AttributeField attribute = new AttributeField(); + attribute.setName(name); + attribute.setValueAllowNull(value); + mSessionDescription.addField(attribute); + } catch (Exception e) { + throwSdpException(e); + } + return this; + } + + public Builder addSessionAttribute(String name, String value) + throws SdpException { + try { + AttributeField attribute = new AttributeField(); + attribute.setName(name); + attribute.setValueAllowNull(value); + mSessionDescription.addField(attribute); + } catch (Exception e) { + throwSdpException(e); + } + return this; + } + + private void throwSdpException(Exception e) throws SdpException { + if (e instanceof SdpException) { + throw (SdpException) e; + } else { + throw new SdpException(e.toString(), e); + } + } + + public SdpSessionDescription build() { + return mSdp; + } + } + + private SdpSessionDescription() { + } + + /** + * Constructor. + * + * @param sdpString an SDP session description to parse + */ + public SdpSessionDescription(String sdpString) throws SdpException { + try { + mSessionDescription = new SDPAnnounceParser(sdpString).parse(); + } catch (ParseException e) { + throw new SdpException(e.toString(), e); + } + verify(); + } + + /** + * Constructor. + * + * @param content a raw SDP session description to parse + */ + public SdpSessionDescription(byte[] content) throws SdpException { + this(new String(content)); + } + + private void verify() throws SdpException { + // make sure the syntax is correct over the fields we're interested in + Vector<MediaDescription> descriptions = (Vector<MediaDescription>) + mSessionDescription.getMediaDescriptions(false); + for (MediaDescription md : descriptions) { + md.getMedia().getMediaPort(); + Connection connection = md.getConnection(); + if (connection != null) connection.getAddress(); + md.getMedia().getFormats(); + } + Connection connection = mSessionDescription.getConnection(); + if (connection != null) connection.getAddress(); + } + + /** + * Gets the connection address of the media. + * + * @param type the media type; e.g., "AUDIO" + * @return the media connection address of the peer + */ + public String getPeerMediaAddress(String type) { + try { + MediaDescription md = getMediaDescription(type); + Connection connection = md.getConnection(); + if (connection == null) { + connection = mSessionDescription.getConnection(); + } + return ((connection == null) ? null : connection.getAddress()); + } catch (SdpException e) { + // should not occur + return null; + } + } + + /** + * Gets the connection port number of the media. + * + * @param type the media type; e.g., "AUDIO" + * @return the media connection port number of the peer + */ + public int getPeerMediaPort(String type) { + try { + MediaDescription md = getMediaDescription(type); + return md.getMedia().getMediaPort(); + } catch (SdpException e) { + // should not occur + return -1; + } + } + + private boolean containsAttribute(String type, String name) { + if (name == null) return false; + MediaDescription md = getMediaDescription(type); + Vector<AttributeField> v = (Vector<AttributeField>) + md.getAttributeFields(); + for (AttributeField field : v) { + if (name.equals(field.getAttribute().getName())) return true; + } + return false; + } + + /** + * Checks if the media is "sendonly". + * + * @param type the media type; e.g., "AUDIO" + * @return true if the media is "sendonly" + */ + public boolean isSendOnly(String type) { + boolean answer = containsAttribute(type, SENDONLY); + Log.d(TAG, " sendonly? " + answer); + return answer; + } + + /** + * Checks if the media is "recvonly". + * + * @param type the media type; e.g., "AUDIO" + * @return true if the media is "recvonly" + */ + public boolean isReceiveOnly(String type) { + boolean answer = containsAttribute(type, RECVONLY); + Log.d(TAG, " recvonly? " + answer); + return answer; + } + + /** + * Checks if the media is in sending; i.e., not "recvonly" and not + * "inactive". + * + * @param type the media type; e.g., "AUDIO" + * @return true if the media is sending + */ + public boolean isSending(String type) { + boolean answer = !containsAttribute(type, RECVONLY) + && !containsAttribute(type, INACTIVE); + + Log.d(TAG, " sending? " + answer); + return answer; + } + + /** + * Checks if the media is in receiving; i.e., not "sendonly" and not + * "inactive". + * + * @param type the media type; e.g., "AUDIO" + * @return true if the media is receiving + */ + public boolean isReceiving(String type) { + boolean answer = !containsAttribute(type, SENDONLY) + && !containsAttribute(type, INACTIVE); + Log.d(TAG, " receiving? " + answer); + return answer; + } + + private AudioCodec parseAudioCodec(String rtpmap, int ptime) { + String[] ss = rtpmap.split(" "); + int payloadType = Integer.parseInt(ss[0]); + + ss = ss[1].split("/"); + String name = ss[0]; + int sampleRate = Integer.parseInt(ss[1]); + int channelCount = 1; + if (ss.length > 2) channelCount = Integer.parseInt(ss[2]); + int sampleCount = sampleRate / (1000 / ptime) * channelCount; + return new AudioCodec(payloadType, name, sampleRate, sampleCount); + } + + /** + * Gets the list of audio codecs in this session description. + * + * @return the list of audio codecs in this session description + */ + public List<AudioCodec> getAudioCodecs() { + MediaDescription md = getMediaDescription(AUDIO); + if (md == null) return new ArrayList<AudioCodec>(); + + // FIXME: what happens if ptime is missing + int ptime = 20; + try { + String value = md.getAttribute(PTIME); + if (value != null) ptime = Integer.parseInt(value); + } catch (Throwable t) { + Log.w(TAG, "getCodecs(): ignored: " + t); + } + + List<AudioCodec> codecs = new ArrayList<AudioCodec>(); + Vector<AttributeField> v = (Vector<AttributeField>) + md.getAttributeFields(); + for (AttributeField field : v) { + try { + if (RTPMAP.equals(field.getName())) { + AudioCodec codec = parseAudioCodec(field.getValue(), ptime); + if (codec != null) codecs.add(codec); + } + } catch (Throwable t) { + Log.w(TAG, "getCodecs(): ignored: " + t); + } + } + return codecs; + } + + /** + * Gets the media description of the specified type. + * + * @param type the media type; e.g., "AUDIO" + * @return the media description of the specified type + */ + public MediaDescription getMediaDescription(String type) { + MediaDescription[] all = getMediaDescriptions(); + if ((all == null) || (all.length == 0)) return null; + for (MediaDescription md : all) { + String t = md.getMedia().getMedia(); + if (t.equalsIgnoreCase(type)) return md; + } + return null; + } + + /** + * Gets all the media descriptions in this session description. + * + * @return all the media descriptions in this session description + */ + public MediaDescription[] getMediaDescriptions() { + try { + Vector<MediaDescription> descriptions = (Vector<MediaDescription>) + mSessionDescription.getMediaDescriptions(false); + MediaDescription[] all = new MediaDescription[descriptions.size()]; + return descriptions.toArray(all); + } catch (SdpException e) { + Log.e(TAG, "getMediaDescriptions", e); + } + return null; + } + + @Override + public String getType() { + return "sdp"; + } + + @Override + public byte[] getContent() { + return mSessionDescription.toString().getBytes(); + } + + @Override + public String toString() { + return mSessionDescription.toString(); + } +} diff --git a/voip/java/android/net/sip/SessionDescription.aidl b/voip/java/android/net/sip/SessionDescription.aidl new file mode 100644 index 0000000..a120d16 --- /dev/null +++ b/voip/java/android/net/sip/SessionDescription.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2010, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +parcelable SessionDescription; diff --git a/voip/java/android/net/sip/SessionDescription.java b/voip/java/android/net/sip/SessionDescription.java new file mode 100644 index 0000000..d476f0b --- /dev/null +++ b/voip/java/android/net/sip/SessionDescription.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Abstract class of a session description. + * @hide + */ +public abstract class SessionDescription implements Parcelable { + /** @hide */ + public static final Parcelable.Creator<SessionDescription> CREATOR = + new Parcelable.Creator<SessionDescription>() { + public SessionDescription createFromParcel(Parcel in) { + return new SessionDescriptionImpl(in); + } + + public SessionDescription[] newArray(int size) { + return new SessionDescriptionImpl[size]; + } + }; + + /** + * Gets the type of the session description; e.g., "SDP". + * + * @return the session description type + */ + public abstract String getType(); + + /** + * Gets the raw content of the session description. + * + * @return the content of the session description + */ + public abstract byte[] getContent(); + + /** @hide */ + public void writeToParcel(Parcel out, int flags) { + out.writeString(getType()); + out.writeByteArray(getContent()); + } + + /** @hide */ + public int describeContents() { + return 0; + } + + private static class SessionDescriptionImpl extends SessionDescription { + private String mType; + private byte[] mContent; + + SessionDescriptionImpl(Parcel in) { + mType = in.readString(); + mContent = in.createByteArray(); + } + + @Override + public String getType() { + return mType; + } + + @Override + public byte[] getContent() { + return mContent; + } + } +} diff --git a/voip/java/android/net/sip/SipAudioCall.java b/voip/java/android/net/sip/SipAudioCall.java new file mode 100644 index 0000000..f4be839 --- /dev/null +++ b/voip/java/android/net/sip/SipAudioCall.java @@ -0,0 +1,303 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import android.net.rtp.AudioGroup; +import android.net.rtp.AudioStream; +import android.os.Message; + +import javax.sip.SipException; + +/** + * Interface for making audio calls over SIP. + * @hide + */ +public interface SipAudioCall { + /** Listener class for all event callbacks. */ + public interface Listener { + /** + * Called when the call object is ready to make another call. + * + * @param call the call object that is ready to make another call + */ + void onReadyToCall(SipAudioCall call); + + /** + * Called when a request is sent out to initiate a new call. + * + * @param call the call object that carries out the audio call + */ + void onCalling(SipAudioCall call); + + /** + * Called when a new call comes in. + * + * @param call the call object that carries out the audio call + * @param caller the SIP profile of the caller + */ + void onRinging(SipAudioCall call, SipProfile caller); + + /** + * Called when a RINGING response is received for the INVITE request sent + * + * @param call the call object that carries out the audio call + */ + void onRingingBack(SipAudioCall call); + + /** + * Called when the session is established. + * + * @param call the call object that carries out the audio call + */ + void onCallEstablished(SipAudioCall call); + + /** + * Called when the session is terminated. + * + * @param call the call object that carries out the audio call + */ + void onCallEnded(SipAudioCall call); + + /** + * Called when the peer is busy during session initialization. + * + * @param call the call object that carries out the audio call + */ + void onCallBusy(SipAudioCall call); + + /** + * Called when the call is on hold. + * + * @param call the call object that carries out the audio call + */ + void onCallHeld(SipAudioCall call); + + /** + * Called when an error occurs. + * + * @param call the call object that carries out the audio call + * @param errorMessage error message + */ + void onError(SipAudioCall call, String errorMessage); + } + + /** + * The adapter class for {@link SipAudioCall#Listener}. The default + * implementation of all callback methods is no-op. + */ + public class Adapter implements Listener { + protected void onChanged(SipAudioCall call) { + } + public void onReadyToCall(SipAudioCall call) { + onChanged(call); + } + public void onCalling(SipAudioCall call) { + onChanged(call); + } + public void onRinging(SipAudioCall call, SipProfile caller) { + onChanged(call); + } + public void onRingingBack(SipAudioCall call) { + onChanged(call); + } + public void onCallEstablished(SipAudioCall call) { + onChanged(call); + } + public void onCallEnded(SipAudioCall call) { + onChanged(call); + } + public void onCallBusy(SipAudioCall call) { + onChanged(call); + } + public void onCallHeld(SipAudioCall call) { + onChanged(call); + } + public void onError(SipAudioCall call, String errorMessage) { + onChanged(call); + } + } + + /** + * Sets the listener to listen to the audio call events. The method calls + * {@link #setListener(Listener, false)}. + * + * @param listener to listen to the audio call events of this object + * @see #setListener(Listener, boolean) + */ + void setListener(Listener listener); + + /** + * Sets the listener to listen to the audio call events. A + * {@link SipAudioCall} can only hold one listener at a time. Subsequent + * calls to this method override the previous listener. + * + * @param listener to listen to the audio call events of this object + * @param callbackImmediately set to true if the caller wants to be called + * back immediately on the current state + */ + void setListener(Listener listener, boolean callbackImmediately); + + /** + * Closes this object. The object is not usable after being closed. + */ + void close(); + + /** + * Initiates an audio call to the specified profile. + * + * @param callee the SIP profile to make the call to + * @param sipManager the {@link SipManager} object to help make call with + */ + void makeCall(SipProfile callee, SipManager sipManager) throws SipException; + + /** + * Attaches an incoming call to this call object. + * + * @param session the session that receives the incoming call + * @param sdp the session description of the incoming call + */ + void attachCall(ISipSession session, SdpSessionDescription sdp) + throws SipException; + + /** Ends a call. */ + void endCall() throws SipException; + + /** + * Puts a call on hold. When succeeds, + * {@link #Listener#onCallHeld(SipAudioCall)} is called. + */ + void holdCall() throws SipException; + + /** Answers a call. */ + void answerCall() throws SipException; + + /** + * Continues a call that's on hold. When succeeds, + * {@link #Listener#onCallEstablished(SipAudioCall)} is called. + */ + void continueCall() throws SipException; + + /** Puts the device to speaker mode. */ + void setSpeakerMode(boolean speakerMode); + + /** Toggles mute. */ + void toggleMute(); + + /** + * Checks if the call is on hold. + * + * @return true if the call is on hold + */ + boolean isOnHold(); + + /** + * Checks if the call is muted. + * + * @return true if the call is muted + */ + boolean isMuted(); + + /** + * Sends a DTMF code. + * + * @param code the DTMF code to send + */ + void sendDtmf(int code); + + /** + * Sends a DTMF code. + * + * @param code the DTMF code to send + * @param result the result message to send when done + */ + void sendDtmf(int code, Message result); + + /** + * Gets the {@link AudioStream} object used in this call. The object + * represents the RTP stream that carries the audio data to and from the + * peer. The object may not be created before the call is established. And + * it is undefined after the call ends or the {@link #close} method is + * called. + * + * @return the {@link AudioStream} object or null if the RTP stream has not + * yet been set up + */ + AudioStream getAudioStream(); + + /** + * Gets the {@link AudioGroup} object which the {@link AudioStream} object + * joins. The group object may not exist before the call is established. + * Also, the {@code AudioStream} may change its group during a call (e.g., + * after the call is held/un-held). Finally, the {@code AudioGroup} object + * returned by this method is undefined after the call ends or the + * {@link #close} method is called. + * + * @return the {@link AudioGroup} object or null if the RTP stream has not + * yet been set up + * @see #getAudioStream + */ + AudioGroup getAudioGroup(); + + /** + * Checks if the call is established. + * + * @return true if the call is established + */ + boolean isInCall(); + + /** + * Gets the local SIP profile. + * + * @return the local SIP profile + */ + SipProfile getLocalProfile(); + + /** + * Gets the peer's SIP profile. + * + * @return the peer's SIP profile + */ + SipProfile getPeerProfile(); + + /** + * Gets the state of the {@link ISipSession} that carries this call. + * + * @return the session state + */ + SipSessionState getState(); + + /** + * Gets the {@link ISipSession} that carries this call. + * + * @return the session object that carries this call + */ + ISipSession getSipSession(); + + /** + * Enables/disables the ring-back tone. + * + * @param enabled true to enable; false to disable + */ + void setRingbackToneEnabled(boolean enabled); + + /** + * Enables/disables the ring tone. + * + * @param enabled true to enable; false to disable + */ + void setRingtoneEnabled(boolean enabled); +} diff --git a/voip/java/android/net/sip/SipAudioCallImpl.java b/voip/java/android/net/sip/SipAudioCallImpl.java new file mode 100644 index 0000000..7161309 --- /dev/null +++ b/voip/java/android/net/sip/SipAudioCallImpl.java @@ -0,0 +1,703 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import gov.nist.javax.sdp.fields.SDPKeywords; + +import android.content.Context; +import android.media.AudioManager; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.media.ToneGenerator; +import android.net.Uri; +import android.net.rtp.AudioCodec; +import android.net.rtp.AudioGroup; +import android.net.rtp.AudioStream; +import android.net.rtp.RtpStream; +import android.net.sip.ISipSession; +import android.net.sip.SdpSessionDescription; +import android.net.sip.SessionDescription; +import android.net.sip.SipAudioCall; +import android.net.sip.SipManager; +import android.net.sip.SipProfile; +import android.net.sip.SipSessionAdapter; +import android.net.sip.SipSessionState; +import android.os.Message; +import android.os.RemoteException; +import android.os.Vibrator; +import android.provider.Settings; +import android.util.Log; + +import java.io.IOException; +import java.net.InetAddress; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.sdp.SdpException; +import javax.sip.SipException; + +/** + * Class that handles an audio call over SIP. + */ +/** @hide */ +public class SipAudioCallImpl extends SipSessionAdapter + implements SipAudioCall { + private static final String TAG = SipAudioCallImpl.class.getSimpleName(); + private static final boolean RELEASE_SOCKET = true; + private static final boolean DONT_RELEASE_SOCKET = false; + private static final String AUDIO = "audio"; + private static final int DTMF = 101; + + private Context mContext; + private SipProfile mLocalProfile; + private SipAudioCall.Listener mListener; + private ISipSession mSipSession; + private SdpSessionDescription mPeerSd; + + private AudioStream mRtpSession; + private SdpSessionDescription.AudioCodec mCodec; + private long mSessionId = -1L; // SDP session ID + private boolean mInCall = false; + private boolean mMuted = false; + private boolean mHold = false; + + private boolean mRingbackToneEnabled = true; + private boolean mRingtoneEnabled = true; + private Ringtone mRingtone; + private ToneGenerator mRingbackTone; + + private SipProfile mPendingCallRequest; + + public SipAudioCallImpl(Context context, SipProfile localProfile) { + mContext = context; + mLocalProfile = localProfile; + } + + public void setListener(SipAudioCall.Listener listener) { + setListener(listener, false); + } + + public void setListener(SipAudioCall.Listener listener, + boolean callbackImmediately) { + mListener = listener; + if ((listener == null) || !callbackImmediately) return; + try { + SipSessionState state = getState(); + switch (state) { + case READY_TO_CALL: + listener.onReadyToCall(this); + break; + case INCOMING_CALL: + listener.onRinging(this, getPeerProfile(mSipSession)); + startRinging(); + break; + case OUTGOING_CALL: + listener.onCalling(this); + break; + default: + listener.onError(this, "wrong state to attach call: " + state); + } + } catch (Throwable t) { + Log.e(TAG, "setListener()", t); + } + } + + public synchronized boolean isInCall() { + return mInCall; + } + + public synchronized boolean isOnHold() { + return mHold; + } + + public void close() { + close(true); + } + + private synchronized void close(boolean closeRtp) { + if (closeRtp) stopCall(RELEASE_SOCKET); + stopRingbackTone(); + stopRinging(); + mSipSession = null; + mInCall = false; + mHold = false; + mSessionId = -1L; + } + + public synchronized SipProfile getLocalProfile() { + return mLocalProfile; + } + + public synchronized SipProfile getPeerProfile() { + try { + return (mSipSession == null) ? null : mSipSession.getPeerProfile(); + } catch (RemoteException e) { + return null; + } + } + + public synchronized SipSessionState getState() { + if (mSipSession == null) return SipSessionState.READY_TO_CALL; + try { + return Enum.valueOf(SipSessionState.class, mSipSession.getState()); + } catch (RemoteException e) { + return SipSessionState.REMOTE_ERROR; + } + } + + + public synchronized ISipSession getSipSession() { + return mSipSession; + } + + @Override + public void onCalling(ISipSession session) { + Log.d(TAG, "calling... " + session); + Listener listener = mListener; + if (listener != null) { + try { + listener.onCalling(SipAudioCallImpl.this); + } catch (Throwable t) { + Log.e(TAG, "onCalling()", t); + } + } + } + + @Override + public void onRingingBack(ISipSession session) { + Log.d(TAG, "sip call ringing back: " + session); + if (!mInCall) startRingbackTone(); + Listener listener = mListener; + if (listener != null) { + try { + listener.onRingingBack(SipAudioCallImpl.this); + } catch (Throwable t) { + Log.e(TAG, "onRingingBack()", t); + } + } + } + + @Override + public synchronized void onRinging(ISipSession session, + SipProfile peerProfile, byte[] sessionDescription) { + try { + if ((mSipSession == null) || !mInCall + || !session.getCallId().equals(mSipSession.getCallId())) { + // should not happen + session.endCall(); + return; + } + + // session changing request + try { + mPeerSd = new SdpSessionDescription(sessionDescription); + answerCall(); + } catch (Throwable e) { + Log.e(TAG, "onRinging()", e); + session.endCall(); + } + } catch (RemoteException e) { + Log.e(TAG, "onRinging()", e); + } + } + + private synchronized void establishCall(byte[] sessionDescription) { + stopRingbackTone(); + stopRinging(); + try { + SdpSessionDescription sd = + new SdpSessionDescription(sessionDescription); + Log.d(TAG, "sip call established: " + sd); + startCall(sd); + mInCall = true; + } catch (SdpException e) { + Log.e(TAG, "createSessionDescription()", e); + } + } + + @Override + public void onCallEstablished(ISipSession session, + byte[] sessionDescription) { + establishCall(sessionDescription); + Listener listener = mListener; + if (listener != null) { + try { + if (mHold) { + listener.onCallHeld(SipAudioCallImpl.this); + } else { + listener.onCallEstablished(SipAudioCallImpl.this); + } + } catch (Throwable t) { + Log.e(TAG, "onCallEstablished()", t); + } + } + } + + @Override + public void onCallEnded(ISipSession session) { + Log.d(TAG, "sip call ended: " + session); + close(); + Listener listener = mListener; + if (listener != null) { + try { + listener.onCallEnded(SipAudioCallImpl.this); + } catch (Throwable t) { + Log.e(TAG, "onCallEnded()", t); + } + } + } + + @Override + public void onCallBusy(ISipSession session) { + Log.d(TAG, "sip call busy: " + session); + close(false); + Listener listener = mListener; + if (listener != null) { + try { + listener.onCallBusy(SipAudioCallImpl.this); + } catch (Throwable t) { + Log.e(TAG, "onCallBusy()", t); + } + } + } + + @Override + public void onCallChangeFailed(ISipSession session, + String className, String message) { + Log.d(TAG, "sip call change failed: " + message); + Listener listener = mListener; + if (listener != null) { + try { + listener.onError(SipAudioCallImpl.this, + className + ": " + message); + } catch (Throwable t) { + Log.e(TAG, "onCallBusy()", t); + } + } + } + + @Override + public void onError(ISipSession session, String className, + String message) { + Log.d(TAG, "sip session error: " + className + ": " + message); + synchronized (this) { + if (!isInCall()) close(true); + } + Listener listener = mListener; + if (listener != null) { + try { + listener.onError(SipAudioCallImpl.this, + className + ": " + message); + } catch (Throwable t) { + Log.e(TAG, "onError()", t); + } + } + } + + public synchronized void attachCall(ISipSession session, + SdpSessionDescription sdp) throws SipException { + mSipSession = session; + mPeerSd = sdp; + try { + session.setListener(this); + } catch (Throwable e) { + Log.e(TAG, "attachCall()", e); + throwSipException(e); + } + } + + public synchronized void makeCall(SipProfile peerProfile, + SipManager sipManager) throws SipException { + try { + mSipSession = sipManager.createSipSession(mLocalProfile, this); + if (mSipSession == null) { + throw new SipException( + "Failed to create SipSession; network available?"); + } + mSipSession.makeCall(peerProfile, createOfferSessionDescription()); + } catch (Throwable e) { + if (e instanceof SipException) { + throw (SipException) e; + } else { + throwSipException(e); + } + } + } + + public synchronized void endCall() throws SipException { + try { + stopRinging(); + stopCall(true); + mInCall = false; + + // perform the above local ops first and then network op + if (mSipSession != null) mSipSession.endCall(); + } catch (Throwable e) { + throwSipException(e); + } + } + + public synchronized void holdCall() throws SipException { + if (mHold) return; + try { + mSipSession.changeCall(createHoldSessionDescription()); + mHold = true; + } catch (Throwable e) { + throwSipException(e); + } + + AudioGroup audioGroup = getAudioGroup(); + if (audioGroup != null) audioGroup.setMode(AudioGroup.MODE_ON_HOLD); + } + + public synchronized void answerCall() throws SipException { + try { + stopRinging(); + mSipSession.answerCall(createAnswerSessionDescription()); + } catch (Throwable e) { + Log.e(TAG, "answerCall()", e); + throwSipException(e); + } + } + + public synchronized void continueCall() throws SipException { + if (!mHold) return; + try { + mHold = false; + mSipSession.changeCall(createContinueSessionDescription()); + } catch (Throwable e) { + throwSipException(e); + } + + AudioGroup audioGroup = getAudioGroup(); + if (audioGroup != null) audioGroup.setMode(AudioGroup.MODE_NORMAL); + } + + private SessionDescription createOfferSessionDescription() { + AudioCodec[] codecs = AudioCodec.getSystemSupportedCodecs(); + return createSdpBuilder(true, convert(codecs)).build(); + } + + private SessionDescription createAnswerSessionDescription() { + try { + // choose an acceptable media from mPeerSd to answer + SdpSessionDescription.AudioCodec codec = getCodec(mPeerSd); + SdpSessionDescription.Builder sdpBuilder = + createSdpBuilder(false, codec); + if (mPeerSd.isSendOnly(AUDIO)) { + sdpBuilder.addMediaAttribute(AUDIO, "recvonly", (String) null); + } else if (mPeerSd.isReceiveOnly(AUDIO)) { + sdpBuilder.addMediaAttribute(AUDIO, "sendonly", (String) null); + } + return sdpBuilder.build(); + } catch (SdpException e) { + throw new RuntimeException(e); + } + } + + private SessionDescription createHoldSessionDescription() { + try { + return createSdpBuilder(false, mCodec) + .addMediaAttribute(AUDIO, "sendonly", (String) null) + .build(); + } catch (SdpException e) { + throw new RuntimeException(e); + } + } + + private SessionDescription createContinueSessionDescription() { + return createSdpBuilder(true, mCodec).build(); + } + + private String getMediaDescription(SdpSessionDescription.AudioCodec codec) { + return String.format("%d %s/%d", codec.payloadType, codec.name, + codec.sampleRate); + } + + private long getSessionId() { + if (mSessionId < 0) { + mSessionId = System.currentTimeMillis(); + } + return mSessionId; + } + + private SdpSessionDescription.Builder createSdpBuilder( + boolean addTelephoneEvent, + SdpSessionDescription.AudioCodec... codecs) { + String localIp = getLocalIp(); + SdpSessionDescription.Builder sdpBuilder; + try { + long sessionVersion = System.currentTimeMillis(); + sdpBuilder = new SdpSessionDescription.Builder("SIP Call") + .setOrigin(mLocalProfile, getSessionId(), sessionVersion, + SDPKeywords.IN, SDPKeywords.IPV4, localIp) + .setConnectionInfo(SDPKeywords.IN, SDPKeywords.IPV4, + localIp); + List<Integer> codecIds = new ArrayList<Integer>(); + for (SdpSessionDescription.AudioCodec codec : codecs) { + codecIds.add(codec.payloadType); + } + if (addTelephoneEvent) codecIds.add(DTMF); + sdpBuilder.addMedia(AUDIO, getLocalMediaPort(), 1, "RTP/AVP", + codecIds.toArray(new Integer[codecIds.size()])); + for (SdpSessionDescription.AudioCodec codec : codecs) { + sdpBuilder.addMediaAttribute(AUDIO, "rtpmap", + getMediaDescription(codec)); + } + if (addTelephoneEvent) { + sdpBuilder.addMediaAttribute(AUDIO, "rtpmap", + DTMF + " telephone-event/8000"); + } + // FIXME: deal with vbr codec + sdpBuilder.addMediaAttribute(AUDIO, "ptime", "20"); + } catch (SdpException e) { + throw new RuntimeException(e); + } + return sdpBuilder; + } + + public synchronized void toggleMute() { + AudioGroup audioGroup = getAudioGroup(); + if (audioGroup != null) { + audioGroup.setMode( + mMuted ? AudioGroup.MODE_NORMAL : AudioGroup.MODE_MUTED); + mMuted = !mMuted; + } + } + + public synchronized boolean isMuted() { + return mMuted; + } + + public synchronized void setSpeakerMode(boolean speakerMode) { + ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)) + .setSpeakerphoneOn(speakerMode); + } + + public void sendDtmf(int code) { + sendDtmf(code, null); + } + + public synchronized void sendDtmf(int code, Message result) { + AudioGroup audioGroup = getAudioGroup(); + if ((audioGroup != null) && (mSipSession != null) + && (SipSessionState.IN_CALL == getState())) { + Log.v(TAG, "send DTMF: " + code); + audioGroup.sendDtmf(code); + } + if (result != null) result.sendToTarget(); + } + + public synchronized AudioStream getAudioStream() { + return mRtpSession; + } + + public synchronized AudioGroup getAudioGroup() { + return ((mRtpSession == null) ? null : mRtpSession.getAudioGroup()); + } + + private SdpSessionDescription.AudioCodec getCodec(SdpSessionDescription sd) { + HashMap<String, AudioCodec> acceptableCodecs = + new HashMap<String, AudioCodec>(); + for (AudioCodec codec : AudioCodec.getSystemSupportedCodecs()) { + acceptableCodecs.put(codec.name, codec); + } + for (SdpSessionDescription.AudioCodec codec : sd.getAudioCodecs()) { + AudioCodec matchedCodec = acceptableCodecs.get(codec.name); + if (matchedCodec != null) return codec; + } + Log.w(TAG, "no common codec is found, use PCM/0"); + return convert(AudioCodec.ULAW); + } + + private AudioCodec convert(SdpSessionDescription.AudioCodec codec) { + AudioCodec c = AudioCodec.getSystemSupportedCodec(codec.name); + return ((c == null) ? AudioCodec.ULAW : c); + } + + private SdpSessionDescription.AudioCodec convert(AudioCodec codec) { + return new SdpSessionDescription.AudioCodec(codec.defaultType, + codec.name, codec.sampleRate, codec.sampleCount); + } + + private SdpSessionDescription.AudioCodec[] convert(AudioCodec[] codecs) { + SdpSessionDescription.AudioCodec[] copies = + new SdpSessionDescription.AudioCodec[codecs.length]; + for (int i = 0, len = codecs.length; i < len; i++) { + copies[i] = convert(codecs[i]); + } + return copies; + } + + private void startCall(SdpSessionDescription peerSd) { + stopCall(DONT_RELEASE_SOCKET); + + mPeerSd = peerSd; + String peerMediaAddress = peerSd.getPeerMediaAddress(AUDIO); + // TODO: handle multiple media fields + int peerMediaPort = peerSd.getPeerMediaPort(AUDIO); + Log.i(TAG, "start audiocall " + peerMediaAddress + ":" + peerMediaPort); + + int localPort = getLocalMediaPort(); + int sampleRate = 8000; + int frameSize = sampleRate / 50; // 160 + try { + // TODO: get sample rate from sdp + mCodec = getCodec(peerSd); + + AudioStream audioStream = mRtpSession; + audioStream.associate(InetAddress.getByName(peerMediaAddress), + peerMediaPort); + audioStream.setCodec(convert(mCodec), mCodec.payloadType); + audioStream.setDtmfType(DTMF); + Log.d(TAG, "start media: localPort=" + localPort + ", peer=" + + peerMediaAddress + ":" + peerMediaPort); + + audioStream.setMode(RtpStream.MODE_NORMAL); + if (!mHold) { + // FIXME: won't work if peer is not sending nor receiving + if (!peerSd.isSending(AUDIO)) { + Log.d(TAG, " not receiving"); + audioStream.setMode(RtpStream.MODE_SEND_ONLY); + } + if (!peerSd.isReceiving(AUDIO)) { + Log.d(TAG, " not sending"); + audioStream.setMode(RtpStream.MODE_RECEIVE_ONLY); + } + } else { + /* The recorder volume will be very low if the device is in + * IN_CALL mode. Therefore, we have to set the mode to NORMAL + * in order to have the normal microphone level. + */ + ((AudioManager) mContext.getSystemService + (Context.AUDIO_SERVICE)) + .setMode(AudioManager.MODE_NORMAL); + } + + AudioGroup audioGroup = new AudioGroup(); + audioStream.join(audioGroup); + if (mHold) { + audioGroup.setMode(AudioGroup.MODE_ON_HOLD); + } else if (mMuted) { + audioGroup.setMode(AudioGroup.MODE_MUTED); + } else { + audioGroup.setMode(AudioGroup.MODE_NORMAL); + } + } catch (Exception e) { + Log.e(TAG, "call()", e); + } + } + + private void stopCall(boolean releaseSocket) { + Log.d(TAG, "stop audiocall"); + if (mRtpSession != null) { + mRtpSession.join(null); + + if (releaseSocket) { + mRtpSession.release(); + mRtpSession = null; + } + } + } + + private int getLocalMediaPort() { + if (mRtpSession != null) return mRtpSession.getLocalPort(); + try { + AudioStream s = mRtpSession = + new AudioStream(InetAddress.getByName(getLocalIp())); + return s.getLocalPort(); + } catch (IOException e) { + Log.w(TAG, "getLocalMediaPort(): " + e); + throw new RuntimeException(e); + } + } + + private String getLocalIp() { + try { + return mSipSession.getLocalIp(); + } catch (RemoteException e) { + // FIXME + return "127.0.0.1"; + } + } + + public synchronized void setRingbackToneEnabled(boolean enabled) { + mRingbackToneEnabled = enabled; + } + + public synchronized void setRingtoneEnabled(boolean enabled) { + mRingtoneEnabled = enabled; + } + + private void startRingbackTone() { + if (!mRingbackToneEnabled) return; + if (mRingbackTone == null) { + // The volume relative to other sounds in the stream + int toneVolume = 80; + mRingbackTone = new ToneGenerator( + AudioManager.STREAM_VOICE_CALL, toneVolume); + } + mRingbackTone.startTone(ToneGenerator.TONE_CDMA_LOW_PBX_L); + } + + private void stopRingbackTone() { + if (mRingbackTone != null) { + mRingbackTone.stopTone(); + mRingbackTone.release(); + mRingbackTone = null; + } + } + + private void startRinging() { + if (!mRingtoneEnabled) return; + ((Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE)) + .vibrate(new long[] {0, 1000, 1000}, 1); + AudioManager am = (AudioManager) + mContext.getSystemService(Context.AUDIO_SERVICE); + if (am.getStreamVolume(AudioManager.STREAM_RING) > 0) { + String ringtoneUri = + Settings.System.DEFAULT_RINGTONE_URI.toString(); + mRingtone = RingtoneManager.getRingtone(mContext, + Uri.parse(ringtoneUri)); + mRingtone.play(); + } + } + + private void stopRinging() { + ((Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE)) + .cancel(); + if (mRingtone != null) mRingtone.stop(); + } + + private void throwSipException(Throwable throwable) throws SipException { + if (throwable instanceof SipException) { + throw (SipException) throwable; + } else { + throw new SipException("", throwable); + } + } + + private SipProfile getPeerProfile(ISipSession session) { + try { + return session.getPeerProfile(); + } catch (RemoteException e) { + return null; + } + } +} diff --git a/voip/java/android/net/sip/SipManager.java b/voip/java/android/net/sip/SipManager.java new file mode 100644 index 0000000..287a13a --- /dev/null +++ b/voip/java/android/net/sip/SipManager.java @@ -0,0 +1,496 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.os.Looper; +import android.os.RemoteException; +import android.os.ServiceManager; + +import java.text.ParseException; +import javax.sip.SipException; + +/** + * The class provides API for various SIP related tasks. Specifically, the API + * allows the application to: + * <ul> + * <li>register a {@link SipProfile} to have the background SIP service listen + * to incoming calls and broadcast them with registered command string. See + * {@link #open(SipProfile, String, SipRegistrationListener)}, + * {@link #open(SipProfile)}, {@link #close(String)}, + * {@link #isOpened(String)} and {@link isRegistered(String)}. It also + * facilitates handling of the incoming call broadcast intent. See + * {@link #isIncomingCallIntent(Intent)}, {@link #getCallId(Intent)}, + * {@link #getOfferSessionDescription(Intent)} and + * {@link #takeAudioCall(Context, Intent, SipAudioCall.Listener)}.</li> + * <li>make/take SIP-based audio calls. See + * {@link #makeAudioCall(Context, SipProfile, SipProfile, SipAudioCall.Listener)} + * and {@link #takeAudioCall(Context, Intent, SipAudioCall.Listener}.</li> + * <li>register/unregister with a SIP service provider. See + * {@link #register(SipProfile, int, ISipSessionListener)} and + * {@link #unregister(SipProfile, ISipSessionListener)}.</li> + * <li>process SIP events directly with a {@link ISipSession} created by + * {@link createSipSession(SipProfile, ISipSessionListener)}.</li> + * </ul> + * @hide + */ +public class SipManager { + /** @hide */ + public static final String SIP_INCOMING_CALL_ACTION = + "com.android.phone.SIP_INCOMING_CALL"; + /** @hide */ + public static final String SIP_ADD_PHONE_ACTION = + "com.android.phone.SIP_ADD_PHONE"; + /** @hide */ + public static final String SIP_REMOVE_PHONE_ACTION = + "com.android.phone.SIP_REMOVE_PHONE"; + /** @hide */ + public static final String LOCAL_URI_KEY = "LOCAL SIPURI"; + + private static final String CALL_ID_KEY = "CallID"; + private static final String OFFER_SD_KEY = "OfferSD"; + + private ISipService mSipService; + + /** + * Creates a manager instance and initializes the background SIP service. + * Will be removed once the SIP service is integrated into framework. + * + * @param context context to start the SIP service + * @return the manager instance + */ + public static SipManager getInstance(final Context context) { + final SipManager manager = new SipManager(); + manager.createSipService(context); + return manager; + } + + private SipManager() { + } + + private void createSipService(Context context) { + if (mSipService != null) return; + IBinder b = ServiceManager.getService(Context.SIP_SERVICE); + mSipService = ISipService.Stub.asInterface(b); + } + + /** + * Opens the profile for making calls and/or receiving calls. Subsequent + * SIP calls can be made through the default phone UI. The caller may also + * make subsequent calls through + * {@link #makeAudioCall(Context, String, String, SipAudioCall.Listener)}. + * If the receiving-call option is enabled in the profile, the SIP service + * will register the profile to the corresponding server periodically in + * order to receive calls from the server. + * + * @param localProfile the SIP profile to make calls from + * @throws SipException if the profile contains incorrect settings or + * calling the SIP service results in an error + */ + public void open(SipProfile localProfile) throws SipException { + try { + mSipService.open(localProfile); + } catch (RemoteException e) { + throw new SipException("open()", e); + } + } + + /** + * Opens the profile for making calls and/or receiving calls. Subsequent + * SIP calls can be made through the default phone UI. The caller may also + * make subsequent calls through + * {@link #makeAudioCall(Context, String, String, SipAudioCall.Listener)}. + * If the receiving-call option is enabled in the profile, the SIP service + * will register the profile to the corresponding server periodically in + * order to receive calls from the server. + * + * @param localProfile the SIP profile to receive incoming calls for + * @param incomingCallBroadcastAction the action to be broadcast when an + * incoming call is received + * @param listener to listen to registration events; can be null + * @throws SipException if the profile contains incorrect settings or + * calling the SIP service results in an error + */ + public void open(SipProfile localProfile, + String incomingCallBroadcastAction, + SipRegistrationListener listener) throws SipException { + try { + mSipService.open3(localProfile, incomingCallBroadcastAction, + createRelay(listener)); + } catch (RemoteException e) { + throw new SipException("open()", e); + } + } + + /** + * Sets the listener to listen to registration events. No effect if the + * profile has not been opened to receive calls + * (see {@link #open(SipProfile, String, SipRegistrationListener)} and + * {@link #open(SipProfile)}). + * + * @param localProfileUri the URI of the profile + * @param listener to listen to registration events; can be null + * @throws SipException if calling the SIP service results in an error + */ + public void setRegistrationListener(String localProfileUri, + SipRegistrationListener listener) throws SipException { + try { + mSipService.setRegistrationListener( + localProfileUri, createRelay(listener)); + } catch (RemoteException e) { + throw new SipException("setRegistrationListener()", e); + } + } + + /** + * Closes the specified profile to not make/receive calls. All the resources + * that were allocated to the profile are also released. + * + * @param localProfileUri the URI of the profile to close + * @throws SipException if calling the SIP service results in an error + */ + public void close(String localProfileUri) throws SipException { + try { + mSipService.close(localProfileUri); + } catch (RemoteException e) { + throw new SipException("close()", e); + } + } + + /** + * Checks if the specified profile is enabled to receive calls. + * + * @param localProfileUri the URI of the profile in question + * @return true if the profile is enabled to receive calls + * @throws SipException if calling the SIP service results in an error + */ + public boolean isOpened(String localProfileUri) throws SipException { + try { + return mSipService.isOpened(localProfileUri); + } catch (RemoteException e) { + throw new SipException("isOpened()", e); + } + } + + /** + * Checks if the specified profile is registered to the server for + * receiving calls. + * + * @param localProfileUri the URI of the profile in question + * @return true if the profile is registered to the server + * @throws SipException if calling the SIP service results in an error + */ + public boolean isRegistered(String localProfileUri) throws SipException { + try { + return mSipService.isRegistered(localProfileUri); + } catch (RemoteException e) { + throw new SipException("isRegistered()", e); + } + } + + /** + * Creates a {@link SipAudioCall} to make a call. + * + * @param context context to create a {@link SipAudioCall} object + * @param localProfile the SIP profile to make the call from + * @param peerProfile the SIP profile to make the call to + * @param listener to listen to the call events from {@link SipAudioCall}; + * can be null + * @return a {@link SipAudioCall} object + * @throws SipException if calling the SIP service results in an error + */ + public SipAudioCall makeAudioCall(Context context, SipProfile localProfile, + SipProfile peerProfile, SipAudioCall.Listener listener) + throws SipException { + SipAudioCall call = new SipAudioCallImpl(context, localProfile); + call.setListener(listener); + call.makeCall(peerProfile, this); + return call; + } + + /** + * Creates a {@link SipAudioCall} to make a call. To use this method, one + * must call {@link #open(SipProfile)} first. + * + * @param context context to create a {@link SipAudioCall} object + * @param localProfileUri URI of the SIP profile to make the call from + * @param peerProfileUri URI of the SIP profile to make the call to + * @param listener to listen to the call events from {@link SipAudioCall}; + * can be null + * @return a {@link SipAudioCall} object + * @throws SipException if calling the SIP service results in an error + */ + public SipAudioCall makeAudioCall(Context context, String localProfileUri, + String peerProfileUri, SipAudioCall.Listener listener) + throws SipException { + try { + return makeAudioCall(context, + new SipProfile.Builder(localProfileUri).build(), + new SipProfile.Builder(peerProfileUri).build(), listener); + } catch (ParseException e) { + throw new SipException("build SipProfile", e); + } + } + + /** + * The method calls {@code takeAudioCall(context, incomingCallIntent, + * listener, true}. + * + * @see #takeAudioCall(Context, Intent, SipAudioCall.Listener, boolean) + */ + public SipAudioCall takeAudioCall(Context context, + Intent incomingCallIntent, SipAudioCall.Listener listener) + throws SipException { + return takeAudioCall(context, incomingCallIntent, listener, true); + } + + /** + * Creates a {@link SipAudioCall} to take an incoming call. Before the call + * is returned, the listener will receive a + * {@link SipAudioCall#Listener.onRinging(SipAudioCall, SipProfile)} + * callback. + * + * @param context context to create a {@link SipAudioCall} object + * @param incomingCallIntent the incoming call broadcast intent + * @param listener to listen to the call events from {@link SipAudioCall}; + * can be null + * @return a {@link SipAudioCall} object + * @throws SipException if calling the SIP service results in an error + */ + public SipAudioCall takeAudioCall(Context context, + Intent incomingCallIntent, SipAudioCall.Listener listener, + boolean ringtoneEnabled) throws SipException { + if (incomingCallIntent == null) return null; + + String callId = getCallId(incomingCallIntent); + if (callId == null) { + throw new SipException("Call ID missing in incoming call intent"); + } + + byte[] offerSd = getOfferSessionDescription(incomingCallIntent); + if (offerSd == null) { + throw new SipException("Session description missing in incoming " + + "call intent"); + } + + try { + SdpSessionDescription sdp = new SdpSessionDescription(offerSd); + + ISipSession session = mSipService.getPendingSession(callId); + if (session == null) return null; + SipAudioCall call = new SipAudioCallImpl( + context, session.getLocalProfile()); + call.setRingtoneEnabled(ringtoneEnabled); + call.attachCall(session, sdp); + call.setListener(listener); + return call; + } catch (Throwable t) { + throw new SipException("takeAudioCall()", t); + } + } + + /** + * Checks if the intent is an incoming call broadcast intent. + * + * @param intent the intent in question + * @return true if the intent is an incoming call broadcast intent + */ + public static boolean isIncomingCallIntent(Intent intent) { + if (intent == null) return false; + String callId = getCallId(intent); + byte[] offerSd = getOfferSessionDescription(intent); + return ((callId != null) && (offerSd != null)); + } + + /** + * Gets the call ID from the specified incoming call broadcast intent. + * + * @param incomingCallIntent the incoming call broadcast intent + * @return the call ID or null if the intent does not contain it + */ + public static String getCallId(Intent incomingCallIntent) { + return incomingCallIntent.getStringExtra(CALL_ID_KEY); + } + + /** + * Gets the offer session description from the specified incoming call + * broadcast intent. + * + * @param incomingCallIntent the incoming call broadcast intent + * @return the offer session description or null if the intent does not + * have it + */ + public static byte[] getOfferSessionDescription(Intent incomingCallIntent) { + return incomingCallIntent.getByteArrayExtra(OFFER_SD_KEY); + } + + /** + * Creates an incoming call broadcast intent. + * + * @param action the action string to broadcast + * @param callId the call ID of the incoming call + * @param sessionDescription the session description of the incoming call + * @return the incoming call intent + * @hide + */ + public static Intent createIncomingCallBroadcast(String action, + String callId, byte[] sessionDescription) { + Intent intent = new Intent(action); + intent.putExtra(CALL_ID_KEY, callId); + intent.putExtra(OFFER_SD_KEY, sessionDescription); + return intent; + } + + /** + * Registers the profile to the corresponding server for receiving calls. + * {@link #open(SipProfile, String, SipRegistrationListener)} is still + * needed to be called at least once in order for the SIP service to + * broadcast an intent when an incoming call is received. + * + * @param localProfile the SIP profile to register with + * @param expiryTime registration expiration time (in second) + * @param listener to listen to the registration events + * @throws SipException if calling the SIP service results in an error + */ + public void register(SipProfile localProfile, int expiryTime, + SipRegistrationListener listener) throws SipException { + try { + ISipSession session = mSipService.createSession( + localProfile, createRelay(listener)); + session.register(expiryTime); + } catch (RemoteException e) { + throw new SipException("register()", e); + } + } + + /** + * Unregisters the profile from the corresponding server for not receiving + * further calls. + * + * @param localProfile the SIP profile to register with + * @param listener to listen to the registration events + * @throws SipException if calling the SIP service results in an error + */ + public void unregister(SipProfile localProfile, + SipRegistrationListener listener) throws SipException { + try { + ISipSession session = mSipService.createSession( + localProfile, createRelay(listener)); + session.unregister(); + } catch (RemoteException e) { + throw new SipException("unregister()", e); + } + } + + /** + * Gets the {@link ISipSession} that handles the incoming call. For audio + * calls, consider to use {@link SipAudioCall} to handle the incoming call. + * See {@link #takeAudioCall(Context, Intent, SipAudioCall.Listener)}. + * Note that the method may be called only once for the same intent. For + * subsequent calls on the same intent, the method returns null. + * + * @param incomingCallIntent the incoming call broadcast intent + * @return the session object that handles the incoming call + */ + public ISipSession getSessionFor(Intent incomingCallIntent) + throws SipException { + try { + String callId = getCallId(incomingCallIntent); + return mSipService.getPendingSession(callId); + } catch (RemoteException e) { + throw new SipException("getSessionFor()", e); + } + } + + private static ISipSessionListener createRelay( + SipRegistrationListener listener) { + return ((listener == null) ? null : new ListenerRelay(listener)); + } + + /** + * Creates a {@link ISipSession} with the specified profile. Use other + * methods, if applicable, instead of interacting with {@link ISipSession} + * directly. + * + * @param localProfile the SIP profile the session is associated with + * @param listener to listen to SIP session events + */ + public ISipSession createSipSession(SipProfile localProfile, + ISipSessionListener listener) throws SipException { + try { + return mSipService.createSession(localProfile, listener); + } catch (RemoteException e) { + throw new SipException("createSipSession()", e); + } + } + + /** + * Gets the list of profiles hosted by the SIP service. The user information + * (username, password and display name) are crossed out. + * @hide + */ + public SipProfile[] getListOfProfiles() { + try { + return mSipService.getListOfProfiles(); + } catch (RemoteException e) { + return null; + } + } + + private static class ListenerRelay extends SipSessionAdapter { + private SipRegistrationListener mListener; + + // listener must not be null + public ListenerRelay(SipRegistrationListener listener) { + mListener = listener; + } + + private String getUri(ISipSession session) { + try { + return session.getLocalProfile().getUriString(); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + @Override + public void onRegistering(ISipSession session) { + mListener.onRegistering(getUri(session)); + } + + @Override + public void onRegistrationDone(ISipSession session, int duration) { + long expiryTime = duration; + if (duration > 0) expiryTime += System.currentTimeMillis(); + mListener.onRegistrationDone(getUri(session), expiryTime); + } + + @Override + public void onRegistrationFailed(ISipSession session, String className, + String message) { + mListener.onRegistrationFailed(getUri(session), className, message); + } + + @Override + public void onRegistrationTimeout(ISipSession session) { + mListener.onRegistrationFailed(getUri(session), + SipException.class.getName(), "registration timed out"); + } + } +} diff --git a/voip/java/android/net/sip/SipProfile.aidl b/voip/java/android/net/sip/SipProfile.aidl new file mode 100644 index 0000000..3b6f68f --- /dev/null +++ b/voip/java/android/net/sip/SipProfile.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2010, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +parcelable SipProfile; diff --git a/voip/java/android/net/sip/SipProfile.java b/voip/java/android/net/sip/SipProfile.java new file mode 100644 index 0000000..e71c293 --- /dev/null +++ b/voip/java/android/net/sip/SipProfile.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import java.io.Serializable; +import java.text.ParseException; +import javax.sip.InvalidArgumentException; +import javax.sip.ListeningPoint; +import javax.sip.PeerUnavailableException; +import javax.sip.SipFactory; +import javax.sip.address.Address; +import javax.sip.address.AddressFactory; +import javax.sip.address.SipURI; +import javax.sip.address.URI; + +/** + * Class containing a SIP account, domain and server information. + * @hide + */ +public class SipProfile implements Parcelable, Serializable { + private static final long serialVersionUID = 1L; + private static final int DEFAULT_PORT = 5060; + private Address mAddress; + private String mProxyAddress; + private String mPassword; + private String mDomain; + private String mProtocol = ListeningPoint.UDP; + private String mProfileName; + private boolean mSendKeepAlive = false; + private boolean mAutoRegistration = true; + + /** @hide */ + public static final Parcelable.Creator<SipProfile> CREATOR = + new Parcelable.Creator<SipProfile>() { + public SipProfile createFromParcel(Parcel in) { + return new SipProfile(in); + } + + public SipProfile[] newArray(int size) { + return new SipProfile[size]; + } + }; + + /** + * Class to help create a {@link SipProfile}. + */ + public static class Builder { + private AddressFactory mAddressFactory; + private SipProfile mProfile = new SipProfile(); + private SipURI mUri; + private String mDisplayName; + private String mProxyAddress; + + { + try { + mAddressFactory = + SipFactory.getInstance().createAddressFactory(); + } catch (PeerUnavailableException e) { + throw new RuntimeException(e); + } + } + + /** + * Constructor. + * + * @param uriString the URI string as "sip:<user_name>@<domain>" + * @throws ParseException if the string is not a valid URI + */ + public Builder(String uriString) throws ParseException { + if (uriString == null) { + throw new NullPointerException("uriString cannot be null"); + } + URI uri = mAddressFactory.createURI(fix(uriString)); + if (uri instanceof SipURI) { + mUri = (SipURI) uri; + } else { + throw new ParseException(uriString + " is not a SIP URI", 0); + } + mProfile.mDomain = mUri.getHost(); + } + + /** + * Constructor. + * + * @param username username of the SIP account + * @param serverDomain the SIP server domain; if the network address + * is different from the domain, use + * {@link #setOutboundProxy(String)} to set server address + * @throws ParseException if the parameters are not valid + */ + public Builder(String username, String serverDomain) + throws ParseException { + if ((username == null) || (serverDomain == null)) { + throw new NullPointerException( + "username and serverDomain cannot be null"); + } + mUri = mAddressFactory.createSipURI(username, serverDomain); + mProfile.mDomain = serverDomain; + } + + private String fix(String uriString) { + return (uriString.trim().toLowerCase().startsWith("sip:") + ? uriString + : "sip:" + uriString); + } + + /** + * Sets the name of the profile. This name is given by user. + * + * @param name name of the profile + * @return this builder object + */ + public Builder setProfileName(String name) { + mProfile.mProfileName = name; + return this; + } + + /** + * Sets the password of the SIP account + * + * @param password password of the SIP account + * @return this builder object + */ + public Builder setPassword(String password) { + mUri.setUserPassword(password); + return this; + } + + /** + * Sets the port number of the server. By default, it is 5060. + * + * @param port port number of the server + * @return this builder object + * @throws InvalidArgumentException if the port number is out of range + */ + public Builder setPort(int port) throws InvalidArgumentException { + mUri.setPort(port); + return this; + } + + /** + * Sets the protocol used to connect to the SIP server. Currently, + * only "UDP" and "TCP" are supported. + * + * @param protocol the protocol string + * @return this builder object + * @throws InvalidArgumentException if the protocol is not recognized + */ + public Builder setProtocol(String protocol) + throws InvalidArgumentException { + if (protocol == null) { + throw new NullPointerException("protocol cannot be null"); + } + protocol = protocol.toUpperCase(); + if (!protocol.equals("UDP") && !protocol.equals("TCP")) { + throw new InvalidArgumentException( + "unsupported protocol: " + protocol); + } + mProfile.mProtocol = protocol; + return this; + } + + /** + * Sets the outbound proxy of the SIP server. + * + * @param outboundProxy the network address of the outbound proxy + * @return this builder object + */ + public Builder setOutboundProxy(String outboundProxy) { + mProxyAddress = outboundProxy; + return this; + } + + /** + * Sets the display name of the user. + * + * @param displayName display name of the user + * @return this builder object + */ + public Builder setDisplayName(String displayName) { + mDisplayName = displayName; + return this; + } + + /** + * Sets the send keep-alive flag. + * + * @param flag true if sending keep-alive message is required, + * false otherwise + * @return this builder object + */ + public Builder setSendKeepAlive(boolean flag) { + mProfile.mSendKeepAlive = flag; + return this; + } + + + /** + * Sets the auto. registration flag. + * + * @param flag true if the profile will be registered automatically, + * false otherwise + * @return this builder object + */ + public Builder setAutoRegistration(boolean flag) { + mProfile.mAutoRegistration = flag; + return this; + } + + /** + * Builds and returns the SIP profile object. + * + * @return the profile object created + */ + public SipProfile build() { + // remove password from URI + mProfile.mPassword = mUri.getUserPassword(); + mUri.setUserPassword(null); + try { + mProfile.mAddress = mAddressFactory.createAddress( + mDisplayName, mUri); + if (!TextUtils.isEmpty(mProxyAddress)) { + SipURI uri = (SipURI) + mAddressFactory.createURI(fix(mProxyAddress)); + mProfile.mProxyAddress = uri.getHost(); + } + } catch (ParseException e) { + // must not occur + throw new RuntimeException(e); + } + return mProfile; + } + } + + private SipProfile() { + } + + private SipProfile(Parcel in) { + mAddress = (Address) in.readSerializable(); + mProxyAddress = in.readString(); + mPassword = in.readString(); + mDomain = in.readString(); + mProtocol = in.readString(); + mProfileName = in.readString(); + mSendKeepAlive = (in.readInt() == 0) ? false : true; + mAutoRegistration = (in.readInt() == 0) ? false : true; + } + + /** @hide */ + public void writeToParcel(Parcel out, int flags) { + out.writeSerializable(mAddress); + out.writeString(mProxyAddress); + out.writeString(mPassword); + out.writeString(mDomain); + out.writeString(mProtocol); + out.writeString(mProfileName); + out.writeInt(mSendKeepAlive ? 1 : 0); + out.writeInt(mAutoRegistration ? 1 : 0); + } + + /** @hide */ + public int describeContents() { + return 0; + } + + /** + * Gets the SIP URI of this profile. + * + * @return the SIP URI of this profile + */ + public SipURI getUri() { + return (SipURI) mAddress.getURI(); + } + + /** + * Gets the SIP URI string of this profile. + * + * @return the SIP URI string of this profile + */ + public String getUriString() { + return mAddress.getURI().toString(); + } + + /** + * Gets the SIP address of this profile. + * + * @return the SIP address of this profile + */ + public Address getSipAddress() { + return mAddress; + } + + /** + * Gets the display name of the user. + * + * @return the display name of the user + */ + public String getDisplayName() { + return mAddress.getDisplayName(); + } + + /** + * Gets the username. + * + * @return the username + */ + public String getUserName() { + return getUri().getUser(); + } + + /** + * Gets the password. + * + * @return the password + */ + public String getPassword() { + return mPassword; + } + + /** + * Gets the SIP domain. + * + * @return the SIP domain + */ + public String getSipDomain() { + return mDomain; + } + + /** + * Gets the port number of the SIP server. + * + * @return the port number of the SIP server + */ + public int getPort() { + int port = getUri().getPort(); + return (port == -1) ? DEFAULT_PORT : port; + } + + /** + * Gets the protocol used to connect to the server. + * + * @return the protocol + */ + public String getProtocol() { + return mProtocol; + } + + /** + * Gets the network address of the server outbound proxy. + * + * @return the network address of the server outbound proxy + */ + public String getProxyAddress() { + return mProxyAddress; + } + + /** + * Gets the (user-defined) name of the profile. + * + * @return name of the profile + */ + public String getProfileName() { + return mProfileName; + } + + /** + * Gets the flag of 'Sending keep-alive'. + * + * @return the flag of sending SIP keep-alive messages. + */ + public boolean getSendKeepAlive() { + return mSendKeepAlive; + } + + /** + * Gets the flag of 'Auto Registration'. + * + * @return the flag of registering the profile automatically. + */ + public boolean getAutoRegistration() { + return mAutoRegistration; + } +} diff --git a/voip/java/android/net/sip/SipRegistrationListener.java b/voip/java/android/net/sip/SipRegistrationListener.java new file mode 100644 index 0000000..63faaf8 --- /dev/null +++ b/voip/java/android/net/sip/SipRegistrationListener.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +/** + * Listener class to listen to SIP registration events. + * @hide + */ +public interface SipRegistrationListener { + /** + * Called when a registration request is sent. + * + * @param localProfileUri the URI string of the SIP profile to register with + */ + void onRegistering(String localProfileUri); + + /** + * Called when registration is successfully done. + * + * @param localProfileUri the URI string of the SIP profile to register with + * @param expiryTime duration in second before the registration expires + */ + void onRegistrationDone(String localProfileUri, long expiryTime); + + /** + * Called when the registration fails. + * + * @param localProfileUri the URI string of the SIP profile to register with + * @param errorClass name of the exception class + * @param errorMessage error message + */ + void onRegistrationFailed(String localProfileUri, String errorClass, + String errorMessage); +} diff --git a/voip/java/android/net/sip/SipSessionAdapter.java b/voip/java/android/net/sip/SipSessionAdapter.java new file mode 100644 index 0000000..cfb71d7 --- /dev/null +++ b/voip/java/android/net/sip/SipSessionAdapter.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +/** + * Adapter class for {@link ISipSessionListener}. Default implementation of all + * callback methods is no-op. + * @hide + */ +public class SipSessionAdapter extends ISipSessionListener.Stub { + public void onCalling(ISipSession session) { + } + + public void onRinging(ISipSession session, SipProfile caller, + byte[] sessionDescription) { + } + + public void onRingingBack(ISipSession session) { + } + + public void onCallEstablished( + ISipSession session, byte[] sessionDescription) { + } + + public void onCallEnded(ISipSession session) { + } + + public void onCallBusy(ISipSession session) { + } + + public void onCallChanged(ISipSession session, byte[] sessionDescription) { + } + + public void onCallChangeFailed(ISipSession session, String className, + String message) { + } + + public void onError(ISipSession session, String className, String message) { + } + + public void onRegistering(ISipSession session) { + } + + public void onRegistrationDone(ISipSession session, int duration) { + } + + public void onRegistrationFailed(ISipSession session, String className, + String message) { + } + + public void onRegistrationTimeout(ISipSession session) { + } +} diff --git a/voip/java/android/net/sip/SipSessionState.java b/voip/java/android/net/sip/SipSessionState.java new file mode 100644 index 0000000..5bab112 --- /dev/null +++ b/voip/java/android/net/sip/SipSessionState.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.sip; + +/** + * Defines {@link ISipSession} states. + * @hide + */ +public enum SipSessionState { + /** When session is ready to initiate a call or transaction. */ + READY_TO_CALL, + + /** When the registration request is sent out. */ + REGISTERING, + + /** When the unregistration request is sent out. */ + DEREGISTERING, + + /** When an INVITE request is received. */ + INCOMING_CALL, + + /** When an OK response is sent for the INVITE request received. */ + INCOMING_CALL_ANSWERING, + + /** When an INVITE request is sent. */ + OUTGOING_CALL, + + /** When a RINGING response is received for the INVITE request sent. */ + OUTGOING_CALL_RING_BACK, + + /** When a CANCEL request is sent for the INVITE request sent. */ + OUTGOING_CALL_CANCELING, + + /** When a call is established. */ + IN_CALL, + + /** Some error occurs when making a remote call to {@link ISipSession}. */ + REMOTE_ERROR, + + /** When an OPTIONS request is sent. */ + PINGING; + + /** + * Checks if the specified string represents the same state as this object. + * + * @return true if the specified string represents the same state as this + * object + */ + public boolean equals(String state) { + return toString().equals(state); + } +} diff --git a/voip/jni/rtp/Android.mk b/voip/jni/rtp/Android.mk new file mode 100644 index 0000000..a364355 --- /dev/null +++ b/voip/jni/rtp/Android.mk @@ -0,0 +1,44 @@ +# +# Copyright (C) 2010 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) + +LOCAL_MODULE := librtp_jni + +LOCAL_SRC_FILES := \ + AudioCodec.cpp \ + AudioGroup.cpp \ + RtpStream.cpp \ + util.cpp \ + rtp_jni.cpp + +LOCAL_SHARED_LIBRARIES := \ + libnativehelper \ + libcutils \ + libutils \ + libmedia + +LOCAL_STATIC_LIBRARIES := + +LOCAL_C_INCLUDES += \ + $(JNI_H_INCLUDE) + +LOCAL_CFLAGS += -fvisibility=hidden + +LOCAL_PRELINK_MODULE := false + +include $(BUILD_SHARED_LIBRARY) diff --git a/voip/jni/rtp/AudioCodec.cpp b/voip/jni/rtp/AudioCodec.cpp new file mode 100644 index 0000000..ddd07fc --- /dev/null +++ b/voip/jni/rtp/AudioCodec.cpp @@ -0,0 +1,161 @@ +/* + * Copyrightm (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <string.h> + +#include "AudioCodec.h" + +namespace { + +int8_t gExponents[128] = { + 0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, +}; + +//------------------------------------------------------------------------------ + +class UlawCodec : public AudioCodec +{ +public: + bool set(int sampleRate, int sampleCount) { + mSampleCount = sampleCount; + return sampleCount > 0; + } + int encode(void *payload, int16_t *samples); + int decode(int16_t *samples, void *payload, int length); +private: + int mSampleCount; +}; + +int UlawCodec::encode(void *payload, int16_t *samples) +{ + int8_t *ulaws = (int8_t *)payload; + for (int i = 0; i < mSampleCount; ++i) { + int sample = samples[i]; + int sign = (sample >> 8) & 0x80; + if (sample < 0) { + sample = -sample; + } + sample += 132; + if (sample > 32767) { + sample = 32767; + } + int exponent = gExponents[sample >> 8]; + int mantissa = (sample >> (exponent + 3)) & 0x0F; + ulaws[i] = ~(sign | (exponent << 4) | mantissa); + } + return mSampleCount; +} + +int UlawCodec::decode(int16_t *samples, void *payload, int length) +{ + int8_t *ulaws = (int8_t *)payload; + for (int i = 0; i < length; ++i) { + int ulaw = ~ulaws[i]; + int exponent = (ulaw >> 4) & 0x07; + int mantissa = ulaw & 0x0F; + int sample = (((mantissa << 3) + 132) << exponent) - 132; + samples[i] = (ulaw < 0 ? -sample : sample); + } + return length; +} + +AudioCodec *newUlawCodec() +{ + return new UlawCodec; +} + +//------------------------------------------------------------------------------ + +class AlawCodec : public AudioCodec +{ +public: + bool set(int sampleRate, int sampleCount) { + mSampleCount = sampleCount; + return sampleCount > 0; + } + int encode(void *payload, int16_t *samples); + int decode(int16_t *samples, void *payload, int length); +private: + int mSampleCount; +}; + +int AlawCodec::encode(void *payload, int16_t *samples) +{ + int8_t *alaws = (int8_t *)payload; + for (int i = 0; i < mSampleCount; ++i) { + int sample = samples[i]; + int sign = (sample >> 8) & 0x80; + if (sample < 0) { + sample = -sample; + } + if (sample > 32767) { + sample = 32767; + } + int exponent = gExponents[sample >> 8]; + int mantissa = (sample >> (exponent == 0 ? 4 : exponent + 3)) & 0x0F; + alaws[i] = (sign | (exponent << 4) | mantissa) ^ 0xD5; + } + return mSampleCount; +} + +int AlawCodec::decode(int16_t *samples, void *payload, int length) +{ + int8_t *alaws = (int8_t *)payload; + for (int i = 0; i < length; ++i) { + int alaw = alaws[i] ^ 0x55; + int exponent = (alaw >> 4) & 0x07; + int mantissa = alaw & 0x0F; + int sample = (exponent == 0 ? (mantissa << 4) + 8 : + ((mantissa << 3) + 132) << exponent); + samples[i] = (alaw < 0 ? sample : -sample); + } + return length; +} + +AudioCodec *newAlawCodec() +{ + return new AlawCodec; +} + +struct AudioCodecType { + const char *name; + AudioCodec *(*create)(); +} gAudioCodecTypes[] = { + {"PCMA", newAlawCodec}, + {"PCMU", newUlawCodec}, + {NULL, NULL}, +}; + +} // namespace + +AudioCodec *newAudioCodec(const char *codecName) +{ + AudioCodecType *type = gAudioCodecTypes; + while (type->name != NULL) { + if (strcmp(codecName, type->name) == 0) { + return type->create(); + } + ++type; + } + return NULL; +} diff --git a/voip/jni/rtp/AudioCodec.h b/voip/jni/rtp/AudioCodec.h new file mode 100644 index 0000000..797494c --- /dev/null +++ b/voip/jni/rtp/AudioCodec.h @@ -0,0 +1,36 @@ +/* + * Copyrightm (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <stdint.h> + +#ifndef __AUDIO_CODEC_H__ +#define __AUDIO_CODEC_H__ + +class AudioCodec +{ +public: + virtual ~AudioCodec() {} + // Returns true if initialization succeeds. + virtual bool set(int sampleRate, int sampleCount) = 0; + // Returns the length of payload in bytes. + virtual int encode(void *payload, int16_t *samples) = 0; + // Returns the number of decoded samples. + virtual int decode(int16_t *samples, void *payload, int length) = 0; +}; + +AudioCodec *newAudioCodec(const char *codecName); + +#endif diff --git a/voip/jni/rtp/AudioGroup.cpp b/voip/jni/rtp/AudioGroup.cpp new file mode 100644 index 0000000..08a8d1c --- /dev/null +++ b/voip/jni/rtp/AudioGroup.cpp @@ -0,0 +1,933 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <stdio.h> +#include <stdint.h> +#include <string.h> +#include <errno.h> +#include <fcntl.h> +#include <sys/epoll.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <time.h> +#include <arpa/inet.h> +#include <netinet/in.h> + +#define LOG_TAG "AudioGroup" +#include <cutils/atomic.h> +#include <utils/Log.h> +#include <utils/Errors.h> +#include <utils/RefBase.h> +#include <utils/threads.h> +#include <utils/SystemClock.h> +#include <media/AudioSystem.h> +#include <media/AudioRecord.h> +#include <media/AudioTrack.h> +#include <media/mediarecorder.h> + +#include "jni.h" +#include "JNIHelp.h" + +#include "AudioCodec.h" + +extern int parse(JNIEnv *env, jstring jAddress, int port, sockaddr_storage *ss); + +namespace { + +using namespace android; + +int gRandom = -1; + +// We use a circular array to implement jitter buffer. The simplest way is doing +// a modulo operation on the index while accessing the array. However modulo can +// be expensive on some platforms, such as ARM. Thus we round up the size of the +// array to the nearest power of 2 and then use bitwise-and instead of modulo. +// Currently we make it 256ms long and assume packet interval is 32ms or less. +// The first 64ms is the place where samples get mixed. The rest 192ms is the +// real jitter buffer. For a stream at 8000Hz it takes 4096 bytes. These numbers +// are chosen by experiments and each of them can be adjusted as needed. + +// Other notes: +// + We use elapsedRealtime() to get the time. Since we use 32bit variables +// instead of 64bit ones, comparison must be done by subtraction. +// + Sampling rate must be multiple of 1000Hz, and packet length must be in +// milliseconds. No floating points. +// + If we cannot get enough CPU, we drop samples and simulate packet loss. +// + Resampling is not done yet, so streams in one group must use the same rate. +// For the first release we might only support 8kHz and 16kHz. + +class AudioStream +{ +public: + AudioStream(); + ~AudioStream(); + bool set(int mode, int socket, sockaddr_storage *remote, + const char *codecName, int sampleRate, int sampleCount, + int codecType, int dtmfType); + + void sendDtmf(int event); + bool mix(int32_t *output, int head, int tail, int sampleRate); + void encode(int tick, AudioStream *chain); + void decode(int tick); + +private: + enum { + NORMAL = 0, + SEND_ONLY = 1, + RECEIVE_ONLY = 2, + LAST_MODE = 2, + }; + + int mMode; + int mSocket; + sockaddr_storage mRemote; + AudioCodec *mCodec; + uint32_t mCodecMagic; + uint32_t mDtmfMagic; + + int mTick; + int mSampleRate; + int mSampleCount; + int mInterval; + + int16_t *mBuffer; + int mBufferMask; + int mBufferHead; + int mBufferTail; + int mLatencyScore; + + uint16_t mSequence; + uint32_t mTimestamp; + uint32_t mSsrc; + + int mDtmfEvent; + int mDtmfStart; + + AudioStream *mNext; + + friend class AudioGroup; +}; + +AudioStream::AudioStream() +{ + mSocket = -1; + mCodec = NULL; + mBuffer = NULL; + mNext = NULL; +} + +AudioStream::~AudioStream() +{ + close(mSocket); + delete mCodec; + delete [] mBuffer; + LOGD("stream[%d] is dead", mSocket); +} + +bool AudioStream::set(int mode, int socket, sockaddr_storage *remote, + const char *codecName, int sampleRate, int sampleCount, + int codecType, int dtmfType) +{ + if (mode < 0 || mode > LAST_MODE) { + return false; + } + mMode = mode; + + if (codecName) { + mRemote = *remote; + mCodec = newAudioCodec(codecName); + if (!mCodec || !mCodec->set(sampleRate, sampleCount)) { + return false; + } + } + + mCodecMagic = (0x8000 | codecType) << 16; + mDtmfMagic = (dtmfType == -1) ? 0 : (0x8000 | dtmfType) << 16; + + mTick = elapsedRealtime(); + mSampleRate = sampleRate / 1000; + mSampleCount = sampleCount; + mInterval = mSampleCount / mSampleRate; + + // Allocate jitter buffer. + for (mBufferMask = 8192; mBufferMask < sampleRate; mBufferMask <<= 1); + mBufferMask >>= 2; + mBuffer = new int16_t[mBufferMask]; + --mBufferMask; + mBufferHead = 0; + mBufferTail = 0; + mLatencyScore = 0; + + // Initialize random bits. + read(gRandom, &mSequence, sizeof(mSequence)); + read(gRandom, &mTimestamp, sizeof(mTimestamp)); + read(gRandom, &mSsrc, sizeof(mSsrc)); + + mDtmfEvent = -1; + mDtmfStart = 0; + + // Only take over the socket when succeeded. + mSocket = socket; + + LOGD("stream[%d] is configured as %s %dkHz %dms", mSocket, + (codecName ? codecName : "RAW"), mSampleRate, mInterval); + return true; +} + +void AudioStream::sendDtmf(int event) +{ + if (mDtmfMagic != 0) { + mDtmfEvent = event << 24; + mDtmfStart = mTimestamp + mSampleCount; + } +} + +bool AudioStream::mix(int32_t *output, int head, int tail, int sampleRate) +{ + if (mMode == SEND_ONLY) { + return false; + } + + if (head - mBufferHead < 0) { + head = mBufferHead; + } + if (tail - mBufferTail > 0) { + tail = mBufferTail; + } + if (tail - head <= 0) { + return false; + } + + head *= mSampleRate; + tail *= mSampleRate; + + if (sampleRate == mSampleRate) { + for (int i = head; i - tail < 0; ++i) { + output[i - head] += mBuffer[i & mBufferMask]; + } + } else { + // TODO: implement resampling. + return false; + } + return true; +} + +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; + LOGD("stream[%d] skips %d packets", mSocket, skipped); + } + + tick = mTick; + mTick += mInterval; + ++mSequence; + mTimestamp += mSampleCount; + + if (mMode == RECEIVE_ONLY) { + return; + } + + // If there is an ongoing DTMF event, send it now. + if (mDtmfEvent != -1) { + int duration = mTimestamp - mDtmfStart; + // Make sure duration is reasonable. + if (duration >= 0 && duration < mSampleRate * 100) { + duration += mSampleCount; + int32_t buffer[4] = { + htonl(mDtmfMagic | mSequence), + htonl(mDtmfStart), + mSsrc, + htonl(mDtmfEvent | duration), + }; + if (duration >= mSampleRate * 100) { + buffer[3] |= htonl(1 << 23); + mDtmfEvent = -1; + } + sendto(mSocket, buffer, sizeof(buffer), MSG_DONTWAIT, + (sockaddr *)&mRemote, sizeof(mRemote)); + return; + } + mDtmfEvent = -1; + } + + // It is time to mix streams. + bool mixed = false; + int32_t buffer[mSampleCount + 3]; + memset(buffer, 0, sizeof(buffer)); + while (chain) { + if (chain != this && + chain->mix(buffer, tick - mInterval, tick, mSampleRate)) { + mixed = true; + } + chain = chain->mNext; + } + if (!mixed) { + LOGD("stream[%d] no data", mSocket); + return; + } + + // Cook the packet and send it out. + int16_t samples[mSampleCount]; + 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; + } + if (!mCodec) { + // Special case for device stream. + send(mSocket, samples, sizeof(samples), MSG_DONTWAIT); + return; + } + + buffer[0] = htonl(mCodecMagic | mSequence); + buffer[1] = htonl(mTimestamp); + buffer[2] = mSsrc; + int length = mCodec->encode(&buffer[3], samples); + if (length <= 0) { + LOGD("stream[%d] encoder error", mSocket); + return; + } + sendto(mSocket, buffer, length + 12, MSG_DONTWAIT, (sockaddr *)&mRemote, + sizeof(mRemote)); +} + +void AudioStream::decode(int tick) +{ + char c; + if (mMode == SEND_ONLY) { + recv(mSocket, &c, 1, MSG_DONTWAIT); + return; + } + + // Make sure mBufferHead and mBufferTail are reasonable. + if ((unsigned int)(tick + 256 - mBufferHead) > 1024) { + mBufferHead = tick - 64; + mBufferTail = mBufferHead; + } + + if (tick - mBufferHead > 64) { + // Throw away outdated samples. + mBufferHead = tick - 64; + if (mBufferTail - mBufferHead < 0) { + mBufferTail = mBufferHead; + } + } + + if (mBufferTail - tick <= 80) { + mLatencyScore = tick; + } else if (tick - mLatencyScore >= 5000) { + // Reset the jitter buffer to 40ms if the latency keeps larger than 80ms + // in the past 5s. This rarely happens, so let us just keep it simple. + LOGD("stream[%d] latency control", mSocket); + mBufferTail = tick + 40; + } + + if (mBufferTail - mBufferHead > 256 - mInterval) { + // Buffer overflow. Drop the packet. + LOGD("stream[%d] buffer overflow", mSocket); + recv(mSocket, &c, 1, MSG_DONTWAIT); + return; + } + + // Receive the packet and decode it. + int16_t samples[mSampleCount]; + int length = 0; + if (!mCodec) { + // Special case for device stream. + length = recv(mSocket, samples, sizeof(samples), + MSG_TRUNC | MSG_DONTWAIT) >> 1; + } else { + __attribute__((aligned(4))) uint8_t buffer[2048]; + length = recv(mSocket, buffer, sizeof(buffer), + MSG_TRUNC | MSG_DONTWAIT); + + // 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) { + LOGD("stream[%d] malformed packet", mSocket); + return; + } + int offset = 12 + ((buffer[0] & 0x0F) << 2); + if ((buffer[0] & 0x10) != 0) { + offset += 4 + (ntohs(*(uint16_t *)&buffer[offset + 2]) << 2); + } + if ((buffer[0] & 0x20) != 0) { + length -= buffer[length - 1]; + } + length -= offset; + if (length >= 0) { + length = mCodec->decode(samples, &buffer[offset], length); + } + } + if (length != mSampleCount) { + LOGD("stream[%d] decoder error", mSocket); + return; + } + + if (tick - mBufferTail > 0) { + // Buffer underrun. Reset the jitter buffer to 40ms. + LOGD("stream[%d] buffer underrun", mSocket); + if (mBufferTail - mBufferHead <= 0) { + mBufferHead = tick + 40; + mBufferTail = mBufferHead; + } else { + int tail = (tick + 40) * mSampleRate; + for (int i = mBufferTail * mSampleRate; i - tail < 0; ++i) { + mBuffer[i & mBufferMask] = 0; + } + mBufferTail = tick + 40; + } + } + + // Append to the jitter buffer. + int tail = mBufferTail * mSampleRate; + for (int i = 0; i < mSampleCount; ++i) { + mBuffer[tail & mBufferMask] = samples[i]; + ++tail; + } + mBufferTail += mInterval; +} + +//------------------------------------------------------------------------------ + +class AudioGroup +{ +public: + AudioGroup(); + ~AudioGroup(); + bool set(int sampleRate, int sampleCount); + + bool setMode(int mode); + bool sendDtmf(int event); + bool add(AudioStream *stream); + bool remove(int socket); + +private: + enum { + ON_HOLD = 0, + MUTED = 1, + NORMAL = 2, + EC_ENABLED = 3, + LAST_MODE = 3, + }; + int mMode; + AudioStream *mChain; + int mEventQueue; + volatile int mDtmfEvent; + + int mSampleCount; + int mDeviceSocket; + AudioTrack mTrack; + AudioRecord mRecord; + + bool networkLoop(); + bool deviceLoop(); + + class NetworkThread : public Thread + { + public: + NetworkThread(AudioGroup *group) : Thread(false), mGroup(group) {} + + bool start() + { + if (run("Network", ANDROID_PRIORITY_AUDIO) != NO_ERROR) { + LOGE("cannot start network thread"); + return false; + } + return true; + } + + private: + AudioGroup *mGroup; + bool threadLoop() + { + return mGroup->networkLoop(); + } + }; + sp<NetworkThread> mNetworkThread; + + class DeviceThread : public Thread + { + public: + DeviceThread(AudioGroup *group) : Thread(false), mGroup(group) {} + + bool start() + { + char c; + while (recv(mGroup->mDeviceSocket, &c, 1, MSG_DONTWAIT) == 1); + + if (run("Device", ANDROID_PRIORITY_AUDIO) != NO_ERROR) { + LOGE("cannot start device thread"); + return false; + } + return true; + } + + private: + AudioGroup *mGroup; + bool threadLoop() + { + return mGroup->deviceLoop(); + } + }; + sp<DeviceThread> mDeviceThread; +}; + +AudioGroup::AudioGroup() +{ + mMode = ON_HOLD; + mChain = NULL; + mEventQueue = -1; + mDtmfEvent = -1; + mDeviceSocket = -1; + mNetworkThread = new NetworkThread(this); + mDeviceThread = new DeviceThread(this); +} + +AudioGroup::~AudioGroup() +{ + mNetworkThread->requestExitAndWait(); + mDeviceThread->requestExitAndWait(); + mTrack.stop(); + mRecord.stop(); + close(mEventQueue); + close(mDeviceSocket); + while (mChain) { + AudioStream *next = mChain->mNext; + delete mChain; + mChain = next; + } + LOGD("group[%d] is dead", mDeviceSocket); +} + +bool AudioGroup::set(int sampleRate, int sampleCount) +{ + mEventQueue = epoll_create(2); + if (mEventQueue == -1) { + LOGE("epoll_create: %s", strerror(errno)); + return false; + } + + mSampleCount = sampleCount; + + // Find out the frame count for AudioTrack and AudioRecord. + int output = 0; + int input = 0; + if (AudioTrack::getMinFrameCount(&output, AudioSystem::VOICE_CALL, + sampleRate) != NO_ERROR || output <= 0 || + AudioRecord::getMinFrameCount(&input, sampleRate, + AudioSystem::PCM_16_BIT, 1) != NO_ERROR || input <= 0) { + LOGE("cannot compute frame count"); + return false; + } + LOGD("reported frame count: output %d, input %d", output, input); + + output = (output + sampleCount - 1) / sampleCount * sampleCount; + input = (input + sampleCount - 1) / sampleCount * sampleCount; + if (input < output * 2) { + input = output * 2; + } + LOGD("adjusted frame count: output %d, input %d", output, input); + + // Initialize AudioTrack and AudioRecord. + if (mTrack.set(AudioSystem::VOICE_CALL, sampleRate, AudioSystem::PCM_16_BIT, + AudioSystem::CHANNEL_OUT_MONO, output) != NO_ERROR || + mRecord.set(AUDIO_SOURCE_MIC, sampleRate, AudioSystem::PCM_16_BIT, + AudioSystem::CHANNEL_IN_MONO, input) != NO_ERROR) { + LOGE("cannot initialize audio device"); + return false; + } + LOGD("latency: output %d, input %d", mTrack.latency(), mRecord.latency()); + + // TODO: initialize echo canceler here. + + // 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; + if (!mChain->set(AudioStream::NORMAL, pair[1], NULL, NULL, + sampleRate, sampleCount, -1, -1)) { + close(pair[1]); + LOGE("cannot initialize device stream"); + return false; + } + + // Give device socket a reasonable timeout and buffer size. + timeval tv; + tv.tv_sec = 0; + tv.tv_usec = 1000 * sampleCount / sampleRate * 500; + if (setsockopt(pair[0], SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) || + setsockopt(pair[0], SOL_SOCKET, SO_RCVBUF, &output, sizeof(output)) || + setsockopt(pair[1], SOL_SOCKET, SO_SNDBUF, &output, sizeof(output))) { + 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)) { + LOGE("epoll_ctl: %s", strerror(errno)); + return false; + } + + // Anything else? + LOGD("stream[%d] joins group[%d]", pair[1], pair[0]); + return true; +} + +bool AudioGroup::setMode(int mode) +{ + if (mode < 0 || mode > LAST_MODE) { + return false; + } + if (mMode == mode) { + return true; + } + + LOGD("group[%d] switches from mode %d to %d", mDeviceSocket, mMode, mode); + mMode = mode; + + mDeviceThread->requestExitAndWait(); + if (mode == ON_HOLD) { + mTrack.stop(); + mRecord.stop(); + return true; + } + + mTrack.start(); + if (mode == MUTED) { + mRecord.stop(); + } else { + mRecord.start(); + } + + if (!mDeviceThread->start()) { + mTrack.stop(); + mRecord.stop(); + return false; + } + return true; +} + +bool AudioGroup::sendDtmf(int event) +{ + if (event < 0 || event > 15) { + return false; + } + + // DTMF is rarely used, so we try to make it as lightweight as possible. + // Using volatile might be dodgy, but using a pipe or pthread primitives + // or stop-set-restart threads seems too heavy. Will investigate later. + timespec ts; + ts.tv_sec = 0; + ts.tv_nsec = 100000000; + for (int i = 0; mDtmfEvent != -1 && i < 20; ++i) { + nanosleep(&ts, NULL); + } + if (mDtmfEvent != -1) { + return false; + } + mDtmfEvent = event; + nanosleep(&ts, NULL); + return true; +} + +bool AudioGroup::add(AudioStream *stream) +{ + mNetworkThread->requestExitAndWait(); + + epoll_event event; + event.events = EPOLLIN; + event.data.ptr = stream; + if (epoll_ctl(mEventQueue, EPOLL_CTL_ADD, stream->mSocket, &event)) { + LOGE("epoll_ctl: %s", strerror(errno)); + return false; + } + + stream->mNext = mChain->mNext; + mChain->mNext = stream; + if (!mNetworkThread->start()) { + // Only take over the stream when succeeded. + mChain->mNext = stream->mNext; + return false; + } + + LOGD("stream[%d] joins group[%d]", stream->mSocket, mDeviceSocket); + return true; +} + +bool AudioGroup::remove(int socket) +{ + mNetworkThread->requestExitAndWait(); + + for (AudioStream *stream = mChain; stream->mNext; stream = stream->mNext) { + AudioStream *target = stream->mNext; + if (target->mSocket == socket) { + if (epoll_ctl(mEventQueue, EPOLL_CTL_DEL, socket, NULL)) { + LOGE("epoll_ctl: %s", strerror(errno)); + return false; + } + stream->mNext = target->mNext; + LOGD("stream[%d] leaves group[%d]", socket, mDeviceSocket); + delete target; + break; + } + } + + // Do not start network thread if there is only one stream. + if (!mChain->mNext || !mNetworkThread->start()) { + return false; + } + return true; +} + +bool AudioGroup::networkLoop() +{ + int tick = elapsedRealtime(); + int deadline = tick + 10; + int count = 0; + + for (AudioStream *stream = mChain; stream; stream = stream->mNext) { + if (!stream->mTick || tick - stream->mTick >= 0) { + stream->encode(tick, mChain); + } + if (deadline - stream->mTick > 0) { + deadline = stream->mTick; + } + ++count; + } + + if (mDtmfEvent != -1) { + int event = mDtmfEvent; + for (AudioStream *stream = mChain; stream; stream = stream->mNext) { + stream->sendDtmf(event); + } + mDtmfEvent = -1; + } + + deadline -= tick; + if (deadline < 1) { + deadline = 1; + } + + epoll_event events[count]; + count = epoll_wait(mEventQueue, events, count, deadline); + 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; +} + +bool AudioGroup::deviceLoop() +{ + int16_t output[mSampleCount]; + + if (recv(mDeviceSocket, output, sizeof(output), 0) <= 0) { + memset(output, 0, sizeof(output)); + } + if (mTrack.write(output, sizeof(output)) != (int)sizeof(output)) { + LOGE("cannot write to AudioTrack"); + return false; + } + + if (mMode != MUTED) { + uint32_t frameCount = mRecord.frameCount(); + AudioRecord::Buffer input; + input.frameCount = frameCount; + + if (mRecord.obtainBuffer(&input, -1) != NO_ERROR) { + LOGE("cannot read from AudioRecord"); + return false; + } + + if (input.frameCount < (uint32_t)mSampleCount) { + input.frameCount = 0; + } else { + if (mMode == NORMAL) { + send(mDeviceSocket, input.i8, sizeof(output), MSG_DONTWAIT); + } else { + // TODO: Echo canceller runs here. + send(mDeviceSocket, input.i8, sizeof(output), MSG_DONTWAIT); + } + if (input.frameCount < frameCount) { + input.frameCount = mSampleCount; + } + } + + mRecord.releaseBuffer(&input); + } + + return true; +} + +//------------------------------------------------------------------------------ + +static jfieldID gNative; +static jfieldID gMode; + +void add(JNIEnv *env, jobject thiz, jint mode, + jint socket, jstring jRemoteAddress, jint remotePort, + jstring jCodecName, jint sampleRate, jint sampleCount, + jint codecType, jint dtmfType) +{ + const char *codecName = NULL; + AudioStream *stream = NULL; + AudioGroup *group = NULL; + + // Sanity check. + sockaddr_storage remote; + if (parse(env, jRemoteAddress, remotePort, &remote) < 0) { + // Exception already thrown. + goto error; + } + if (sampleRate < 0 || sampleCount < 0 || codecType < 0 || codecType > 127) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + goto error; + } + if (!jCodecName) { + jniThrowNullPointerException(env, "codecName"); + goto error; + } + codecName = env->GetStringUTFChars(jCodecName, NULL); + if (!codecName) { + // Exception already thrown. + goto error; + } + + // Create audio stream. + stream = new AudioStream; + if (!stream->set(mode, socket, &remote, codecName, sampleRate, sampleCount, + codecType, dtmfType)) { + jniThrowException(env, "java/lang/IllegalStateException", + "cannot initialize audio stream"); + env->ReleaseStringUTFChars(jCodecName, codecName); + goto error; + } + env->ReleaseStringUTFChars(jCodecName, codecName); + socket = -1; + + // Create audio group. + group = (AudioGroup *)env->GetIntField(thiz, gNative); + if (!group) { + int mode = env->GetIntField(thiz, gMode); + group = new 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)) { + jniThrowException(env, "java/lang/IllegalStateException", + "cannot add audio stream"); + goto error; + } + + // Succeed. + env->SetIntField(thiz, gNative, (int)group); + return; + +error: + delete group; + delete stream; + close(socket); + env->SetIntField(thiz, gNative, NULL); +} + +void remove(JNIEnv *env, jobject thiz, jint socket) +{ + AudioGroup *group = (AudioGroup *)env->GetIntField(thiz, gNative); + if (group) { + if (socket == -1 || !group->remove(socket)) { + delete group; + env->SetIntField(thiz, gNative, NULL); + } + } +} + +void setMode(JNIEnv *env, jobject thiz, jint mode) +{ + AudioGroup *group = (AudioGroup *)env->GetIntField(thiz, gNative); + if (group && !group->setMode(mode)) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return; + } + env->SetIntField(thiz, gMode, mode); +} + +void sendDtmf(JNIEnv *env, jobject thiz, jint event) +{ + AudioGroup *group = (AudioGroup *)env->GetIntField(thiz, gNative); + if (group && !group->sendDtmf(event)) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + } +} + +JNINativeMethod gMethods[] = { + {"add", "(IILjava/lang/String;ILjava/lang/String;IIII)V", (void *)add}, + {"remove", "(I)V", (void *)remove}, + {"setMode", "(I)V", (void *)setMode}, + {"sendDtmf", "(I)V", (void *)sendDtmf}, +}; + +} // namespace + +int registerAudioGroup(JNIEnv *env) +{ + gRandom = open("/dev/urandom", O_RDONLY); + if (gRandom == -1) { + LOGE("urandom: %s", strerror(errno)); + return -1; + } + + jclass clazz; + if ((clazz = env->FindClass("android/net/rtp/AudioGroup")) == NULL || + (gNative = env->GetFieldID(clazz, "mNative", "I")) == NULL || + (gMode = env->GetFieldID(clazz, "mMode", "I")) == NULL || + env->RegisterNatives(clazz, gMethods, NELEM(gMethods)) < 0) { + LOGE("JNI registration failed"); + return -1; + } + return 0; +} diff --git a/voip/jni/rtp/RtpStream.cpp b/voip/jni/rtp/RtpStream.cpp new file mode 100644 index 0000000..33b88e4 --- /dev/null +++ b/voip/jni/rtp/RtpStream.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <stdio.h> +#include <stdint.h> +#include <string.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <arpa/inet.h> +#include <netinet/in.h> + +#define LOG_TAG "RtpStream" +#include <utils/Log.h> + +#include "jni.h" +#include "JNIHelp.h" + +extern int parse(JNIEnv *env, jstring jAddress, int port, sockaddr_storage *ss); + +namespace { + +jfieldID gNative; + +jint create(JNIEnv *env, jobject thiz, jstring jAddress) +{ + env->SetIntField(thiz, gNative, -1); + + sockaddr_storage ss; + if (parse(env, jAddress, 0, &ss) < 0) { + // Exception already thrown. + return -1; + } + + int socket = ::socket(ss.ss_family, SOCK_DGRAM, 0); + socklen_t len = sizeof(ss); + if (socket == -1 || bind(socket, (sockaddr *)&ss, sizeof(ss)) != 0 || + getsockname(socket, (sockaddr *)&ss, &len) != 0) { + jniThrowException(env, "java/net/SocketException", strerror(errno)); + ::close(socket); + return -1; + } + + uint16_t *p = (ss.ss_family == AF_INET) ? + &((sockaddr_in *)&ss)->sin_port : &((sockaddr_in6 *)&ss)->sin6_port; + uint16_t port = ntohs(*p); + if ((port & 1) == 0) { + env->SetIntField(thiz, gNative, socket); + return port; + } + ::close(socket); + + socket = ::socket(ss.ss_family, SOCK_DGRAM, 0); + if (socket != -1) { + uint16_t delta = port << 1; + ++port; + + for (int i = 0; i < 1000; ++i) { + do { + port += delta; + } while (port < 1024); + *p = htons(port); + + if (bind(socket, (sockaddr *)&ss, sizeof(ss)) == 0) { + env->SetIntField(thiz, gNative, socket); + return port; + } + } + } + + jniThrowException(env, "java/net/SocketException", strerror(errno)); + ::close(socket); + return -1; +} + +jint dup(JNIEnv *env, jobject thiz) +{ + int socket1 = env->GetIntField(thiz, gNative); + int socket2 = ::dup(socket1); + if (socket2 == -1) { + jniThrowException(env, "java/lang/IllegalStateException", strerror(errno)); + } + LOGD("dup %d to %d", socket1, socket2); + return socket2; +} + +void close(JNIEnv *env, jobject thiz) +{ + int socket = env->GetIntField(thiz, gNative); + ::close(socket); + env->SetIntField(thiz, gNative, -1); + LOGD("close %d", socket); +} + +JNINativeMethod gMethods[] = { + {"create", "(Ljava/lang/String;)I", (void *)create}, + {"dup", "()I", (void *)dup}, + {"close", "()V", (void *)close}, +}; + +} // namespace + +int registerRtpStream(JNIEnv *env) +{ + jclass clazz; + if ((clazz = env->FindClass("android/net/rtp/RtpStream")) == NULL || + (gNative = env->GetFieldID(clazz, "mNative", "I")) == NULL || + env->RegisterNatives(clazz, gMethods, NELEM(gMethods)) < 0) { + LOGE("JNI registration failed"); + return -1; + } + return 0; +} diff --git a/voip/jni/rtp/rtp_jni.cpp b/voip/jni/rtp/rtp_jni.cpp new file mode 100644 index 0000000..9f4bff9 --- /dev/null +++ b/voip/jni/rtp/rtp_jni.cpp @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <stdio.h> + +#include "jni.h" + +extern int registerRtpStream(JNIEnv *env); +extern int registerAudioGroup(JNIEnv *env); + +__attribute__((visibility("default"))) jint JNI_OnLoad(JavaVM *vm, void *unused) +{ + JNIEnv *env = NULL; + if (vm->GetEnv((void **)&env, JNI_VERSION_1_4) != JNI_OK || + registerRtpStream(env) < 0 || registerAudioGroup(env) < 0) { + return -1; + } + return JNI_VERSION_1_4; +} diff --git a/voip/jni/rtp/util.cpp b/voip/jni/rtp/util.cpp new file mode 100644 index 0000000..1d702fc --- /dev/null +++ b/voip/jni/rtp/util.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <stdio.h> +#include <string.h> +#include <arpa/inet.h> +#include <netinet/in.h> + +#include "jni.h" +#include "JNIHelp.h" + +int parse(JNIEnv *env, jstring jAddress, int port, sockaddr_storage *ss) +{ + if (!jAddress) { + jniThrowNullPointerException(env, "address"); + return -1; + } + if (port < 0 || port > 65535) { + jniThrowException(env, "java/lang/IllegalArgumentException", "port"); + return -1; + } + const char *address = env->GetStringUTFChars(jAddress, NULL); + if (!address) { + // Exception already thrown. + return -1; + } + memset(ss, 0, sizeof(*ss)); + + sockaddr_in *sin = (sockaddr_in *)ss; + if (inet_pton(AF_INET, address, &(sin->sin_addr)) > 0) { + sin->sin_family = AF_INET; + sin->sin_port = htons(port); + env->ReleaseStringUTFChars(jAddress, address); + return 0; + } + + sockaddr_in6 *sin6 = (sockaddr_in6 *)ss; + if (inet_pton(AF_INET6, address, &(sin6->sin6_addr)) > 0) { + sin6->sin6_family = AF_INET6; + sin6->sin6_port = htons(port); + env->ReleaseStringUTFChars(jAddress, address); + return 0; + } + + env->ReleaseStringUTFChars(jAddress, address); + jniThrowException(env, "java/lang/IllegalArgumentException", "address"); + return -1; +} |