diff options
author | Hung-ying Tyan <tyanh@google.com> | 2010-03-11 18:35:58 +0800 |
---|---|---|
committer | Hung-ying Tyan <tyanh@google.com> | 2010-03-11 19:08:08 +0800 |
commit | 61614b6f15f23e1721f9c278aa4b7976cc6f1f52 (patch) | |
tree | c4095c4cee1c900922de8dd7b65eb26ffd3b3086 | |
parent | ebadf5174f8ab1efe437686e868a27ac4e92fb20 (diff) | |
download | nist-sip-61614b6f15f23e1721f9c278aa4b7976cc6f1f52.tar.gz |
Add G711 codec and minimal RTP.
-rw-r--r-- | src/android/net/sip/SdpSessionDescription.java | 7 | ||||
-rw-r--r-- | src/com/android/sip/SipMain.java | 57 | ||||
-rw-r--r-- | src/com/android/sip/media/AudioStream.java | 503 | ||||
-rw-r--r-- | src/com/android/sip/media/Decoder.java | 34 | ||||
-rw-r--r-- | src/com/android/sip/media/Encoder.java | 31 | ||||
-rw-r--r-- | src/com/android/sip/media/G711Codec.java | 83 | ||||
-rw-r--r-- | src/com/android/sip/media/RtpPacket.java | 134 |
7 files changed, 823 insertions, 26 deletions
diff --git a/src/android/net/sip/SdpSessionDescription.java b/src/android/net/sip/SdpSessionDescription.java index dc97570..d169a62 100644 --- a/src/android/net/sip/SdpSessionDescription.java +++ b/src/android/net/sip/SdpSessionDescription.java @@ -27,6 +27,7 @@ import gov.nist.javax.sdp.fields.TimeField; import gov.nist.javax.sdp.parser.SDPAnnounceParser; import java.text.ParseException; +import java.util.Collections; import java.util.Vector; import javax.sdp.Connection; import javax.sdp.MediaDescription; @@ -94,14 +95,16 @@ public class SdpSessionDescription implements SessionDescription { } public Builder addMedia(String media, int port, int numPorts, - String transport, Vector types) throws SdpException { + 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(types); + field.setMediaFormats(typeVector); mSessionDescription.addField(field); } catch (Exception e) { throw new SdpException(e.toString(), e); diff --git a/src/com/android/sip/SipMain.java b/src/com/android/sip/SipMain.java index 3ad4743..39c594a 100644 --- a/src/com/android/sip/SipMain.java +++ b/src/com/android/sip/SipMain.java @@ -16,6 +16,8 @@ package com.android.sip; +import com.android.sip.media.AudioStream; + import android.net.sip.SdpSessionDescription; import android.net.sip.SessionDescription; import android.net.sip.SipProfile; @@ -39,7 +41,6 @@ import java.io.IOException; import java.net.DatagramSocket; import java.net.InetAddress; import java.text.ParseException; -import java.util.Vector; import javax.sdp.SdpException; import javax.sip.SipException; @@ -64,6 +65,7 @@ public class SipMain extends PreferenceActivity private SipSessionLayer mSipSessionLayer; private SipSession mSipSession; private SipSession mSipCallSession; + private AudioStream mAudio; private boolean mHolding = false; @Override @@ -337,23 +339,16 @@ public class SipMain extends PreferenceActivity } private SdpSessionDescription.Builder getSdpSampleBuilder() { - // TODO: integrate with SDP String localIp = getLocalIp(); SdpSessionDescription.Builder sdpBuilder; try { - Vector v = new Vector(); - v.add(0); - v.add(4); - v.add(18); sdpBuilder = new SdpSessionDescription.Builder("SIP Call") .setOrigin(mLocalProfile, (long)Math.random() * 10000000L, (long)Math.random() * 10000000L, SDPKeywords.IN, SDPKeywords.IPV4, localIp) .setConnectionInfo(SDPKeywords.IN, SDPKeywords.IPV4, localIp) - .addMedia("audio", getLocalMediaPort(), 1, "RTP/AVP", v) - .addMediaAttribute("rtpmap", "0 PCMU/8000") - .addMediaAttribute("rtpmap", "4 G723/8000") - .addMediaAttribute("rtpmap", "18 G729A/8000") + .addMedia("audio", getLocalMediaPort(), 1, "RTP/AVP", 8) + .addMediaAttribute("rtpmap", "8 PCMA/8000") .addMediaAttribute("ptime", "20"); } catch (SdpException e) { throw new RuntimeException(e); @@ -375,18 +370,42 @@ public class SipMain extends PreferenceActivity return getSdpSampleBuilder().build(); } + private void setAllPreferencesEnabled(final boolean enabled) { + runOnUiThread(new Runnable() { + public void run() { + for (Preference preference : allPreferences()) { + preference.setEnabled(enabled); + } + } + }); + } + private void startAudioCall(SdpSessionDescription sd) { String peerMediaAddress = sd.getPeerMediaAddress(); int peerMediaPort = sd.getPeerMediaPort(); Log.i(TAG, "start audiocall " + peerMediaAddress + ":" + peerMediaPort); - // TODO + setAllPreferencesEnabled(false); + + int localPort = getLocalMediaPort(); + int sampleRate = 8000; + int frameSize = sampleRate / 50; // 160 + try { + // TODO: get sample rate from sdp + mAudio = new AudioStream(sampleRate, sampleRate, localPort, + peerMediaAddress, peerMediaPort); + mAudio.start(); + } catch (Exception e) { + Log.e(TAG, "call(): " + e); + } + Log.v(TAG, " ~~~~~~~~~~~ start media: localPort=" + localPort + + ", peer=" + peerMediaAddress + ":" + peerMediaPort); } private void stopAudioCall() { Log.i(TAG, "stop audiocall"); - - // TODO + if (mAudio != null) mAudio.stop(); + setAllPreferencesEnabled(true); } private Preference[] allPreferences() { @@ -427,11 +446,6 @@ public class SipMain extends PreferenceActivity return getText(mPeerUri); } - private String getPeerMediaAddress() { - // TODO: from peer SDP - return createPeerSipProfile().getServerAddress(); - } - private String getServerUri() { return getText(mServerUri); } @@ -440,11 +454,6 @@ public class SipMain extends PreferenceActivity return Integer.parseInt(getText(mLocalMediaPort)); } - private int getPeerMediaPort() { - // TODO: from peer SDP - return getLocalMediaPort(); - } - private String getLocalIp() { try { DatagramSocket s = new DatagramSocket(); @@ -452,7 +461,7 @@ public class SipMain extends PreferenceActivity return s.getLocalAddress().getHostAddress(); } catch (IOException e) { Log.w(TAG, "getLocalIp(): " + e); - return "127.0.0.1"; + return "127.0.0.1"; } } diff --git a/src/com/android/sip/media/AudioStream.java b/src/com/android/sip/media/AudioStream.java new file mode 100644 index 0000000..1173199 --- /dev/null +++ b/src/com/android/sip/media/AudioStream.java @@ -0,0 +1,503 @@ +/* + * Copyright (C) 2009 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 com.android.sip.media; + +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.net.SocketException; +import java.net.UnknownHostException; + +import android.media.AudioFormat; +import android.media.AudioManager; +import android.media.AudioRecord; +import android.media.AudioTrack; +import android.media.MediaRecorder; +import android.os.Process; +import android.text.TextUtils; +import android.util.Log; + +public class AudioStream { + private static final String TAG = AudioStream.class.getSimpleName(); + + private boolean mRunning = false; + private RecordTask mRecordTask; + private PlayTask mPlayTask; + + private DatagramSocket mSocket; + private InetAddress mRemoteAddr; + private int mRemotePort; + private int mLocalPort; + + public AudioStream(int localSampleRate, int remoteSampleRate, + int localPort) { + mLocalPort = localPort; + int localFrameSize = localSampleRate / 50; // 50 frames / sec + mRecordTask = new RecordTask(localSampleRate, localFrameSize); + mPlayTask = new PlayTask(remoteSampleRate, localFrameSize); + } + + public AudioStream(int localSampleRate, int remoteSampleRate, int localPort, + String remoteIp, int remotePort) throws UnknownHostException { + this(localSampleRate, remoteSampleRate, localPort); + mRemoteAddr = TextUtils.isEmpty(remoteIp) + ? null + : InetAddress.getByName(remoteIp); + mRemotePort = remotePort; + } + + public void start() throws SocketException { + if (mRunning) return; + + mSocket = new DatagramSocket(mLocalPort); + + mRunning = true; + Log.v(TAG, "start AudioStream: remoteAddr=" + mRemoteAddr); + if (mRemoteAddr != null) { + mRecordTask.start(mRemoteAddr, mRemotePort); + } + mPlayTask.start(); + } + + public void stop() { + mRunning = false; + mSocket.close(); + // TODO: wait until both threads are stopped + } + + private void startRecordTask(InetAddress remoteAddr, int remotePort) { + mRemoteAddr = remoteAddr; + mRemotePort = remotePort; + if ((mRemoteAddr != null) && mRunning && !mSocket.isConnected()) { + mRecordTask.start(remoteAddr, remotePort); + } + } + + private class PlayTask implements Runnable { + private int mSampleRate; + private int mFrameSize; + + PlayTask(int sampleRate, int frameSize) { + mSampleRate = sampleRate; + mFrameSize = frameSize; + } + + void start() { + new Thread(this).start(); + } + + public void run() { + Decoder decoder = new G711Codec(); + int playBufferSize = decoder.getSampleCount(mFrameSize); + short[] playBuffer = new short[playBufferSize]; + + RtpReceiver receiver = new RtpReceiver(mFrameSize); + byte[] buffer = receiver.getBuffer(); + int offset = receiver.getPayloadOffset(); + + int minBufferSize = AudioTrack.getMinBufferSize(mSampleRate, + AudioFormat.CHANNEL_CONFIGURATION_MONO, + AudioFormat.ENCODING_PCM_16BIT) * 6; + minBufferSize = Math.max(minBufferSize, playBufferSize); + int bufferHighMark = minBufferSize / 2 * 8 / 10; + Log.d(TAG, " play buffer = " + minBufferSize + ", high water mark=" + + bufferHighMark); + AudioTrack aplayer = new AudioTrack(AudioManager.STREAM_MUSIC, + mSampleRate, AudioFormat.CHANNEL_CONFIGURATION_MONO, + AudioFormat.ENCODING_PCM_16BIT, minBufferSize, + AudioTrack.MODE_STREAM); + AudioPlayer player = + new AudioPlayer(aplayer, minBufferSize, mFrameSize); + + Process.setThreadPriority(Process.THREAD_PRIORITY_AUDIO); + player.play(); + int playState = player.getPlayState(); + boolean socketConnected = mSocket.isConnected(); + long receiveCount = 0; + long cyclePeriod = mFrameSize * 1000L / mSampleRate; + long accumulatedExcessDelay = 0; + long accumulatedBytes = 0; + long bytesPerMillis = mSampleRate / 1000L; + long cycleStart = 0, thisCycle; + long time; + int seqNo = 0; + int packetLossCount = 0; + int bytesDropped = 0; + + player.flush(); + int writeHead = player.getPlaybackHeadPosition(); + + // skip the first byte + receiver.receive(); + cycleStart = System.currentTimeMillis(); + seqNo = receiver.getSequenceNumber(); + + if (!socketConnected) { + socketConnected = true; + startRecordTask(receiver.getRemoteAddress(), + receiver.getRemotePort()); + } + + long startTime = System.currentTimeMillis(); + long virtualClock = startTime; + float delta = 0f; + + while (mRunning) { + int count = receiver.receive(); + if (count > 0) { + try { + int decodeCount = decoder.decode( + playBuffer, buffer, count, offset); + if (playState == AudioTrack.PLAYSTATE_STOPPED) { + player.play(); + playState = player.getPlayState(); + } + + receiveCount ++; + int sn = receiver.getSequenceNumber(); + int lossCount = sn - seqNo - 1; + if (lossCount > 0) { + packetLossCount += lossCount; + virtualClock += lossCount * cyclePeriod; + } + virtualClock += cyclePeriod; + long now = System.currentTimeMillis(); + long late = now - virtualClock; + if (late < 0) { + Log.d(TAG, " move vc back: " + late); + virtualClock = now; + } + + delta = delta * 0.96f + late * 0.04f; + late -= (long) delta; + + if (late > 100) { + // drop + bytesDropped += decodeCount; + Log.d(TAG, " drop " + decodeCount + ", late: " + + late + ", d=" + delta); + cycleStart = now; + seqNo = sn; + continue; + } + int playHead = player.getPlaybackHeadPosition(); + int buffered = writeHead - playHead; + if (buffered > bufferHighMark) { + player.flush(); + buffered = 0; + writeHead = player.getPlaybackHeadPosition(); + Log.d(TAG, " ~~~ flush: " + writeHead); + } + + time = System.currentTimeMillis(); + writeHead += + player.write(playBuffer, 0, decodeCount); + thisCycle = time - cycleStart; + + accumulatedExcessDelay = late; + accumulatedBytes = late * bytesPerMillis; + + cycleStart = time; + seqNo = sn; + } catch (IOException e) { + Log.w(TAG, " ~~~ xxx ~~~ decode error: " + e); + } + } else { + Log.w(TAG, "network disconnected; playback stopped"); + player.stop(); + playState = player.getPlayState(); + } + } + Log.d(TAG, " receiveCount = " + receiveCount); + Log.d(TAG, " acc excess delay =" + accumulatedExcessDelay); + Log.d(TAG, " acc bytes =" + accumulatedBytes); + Log.d(TAG, " # packets lost =" + packetLossCount); + Log.d(TAG, " bytes dropped =" + bytesDropped); + Log.d(TAG, "stop sound playing..."); + player.stop(); + player.release(); + } + } + + private class RecordTask implements Runnable { + private int mSampleRate; + private int mFrameSize; + + RecordTask(int sampleRate, int frameSize) { + mSampleRate = sampleRate; + mFrameSize = frameSize; + } + + void start(InetAddress addr, int port) { + Log.d(TAG, "start RecordTask, connect to " + addr + ":" + port); + mSocket.connect(addr, port); + new Thread(this).start(); + } + + public void run() { + Encoder encoder = new G711Codec(); + int recordBufferSize = encoder.getSampleCount(mFrameSize); + short[] recordBuffer = new short[recordBufferSize]; + RtpSender sender = new RtpSender(mFrameSize); + byte[] buffer = sender.getBuffer(); + int offset = sender.getPayloadOffset(); + + int bufferSize = AudioRecord.getMinBufferSize(mSampleRate, + AudioFormat.CHANNEL_CONFIGURATION_MONO, + AudioFormat.ENCODING_PCM_16BIT) * 3 / 2; + + AudioRecord recorder = new AudioRecord( + MediaRecorder.AudioSource.MIC, mSampleRate, + AudioFormat.CHANNEL_CONFIGURATION_MONO, + AudioFormat.ENCODING_PCM_16BIT, bufferSize); + + recorder.startRecording(); + Log.d(TAG, "start sound recording..." + recorder.getState()); + + // skip the first read, kick off read pipeline + recorder.read(recordBuffer, 0, recordBufferSize); + + long sendCount = 0; + long startTime = System.currentTimeMillis(); + + while (mRunning) { + int count = recorder.read(recordBuffer, 0, recordBufferSize); + + int encodeCount = + encoder.encode(recordBuffer, count, buffer, offset); + sender.send(encodeCount); + sendCount ++; + } + long now = System.currentTimeMillis(); + Log.d(TAG, " sendCount = " + sendCount); + Log.d(TAG, " avg send cycle =" + + ((double) (now - startTime) / sendCount)); + Log.d(TAG, "stop sound recording..."); + recorder.stop(); + } + } + + private class RtpReceiver { + RtpPacket mPacket; + DatagramPacket mDatagram; + + RtpReceiver(int size) { + byte[] buffer = new byte[size + 12]; + mPacket = new RtpPacket(buffer); + mPacket.setPayloadType(8); + mDatagram = new DatagramPacket(buffer, buffer.length); + } + + byte[] getBuffer() { + return mPacket.getRawPacket(); + } + + int getPayloadOffset() { + return 12; + } + + // return received payload size + int receive() { + DatagramPacket datagram = mDatagram; + + try { + mSocket.receive(datagram); + } catch (IOException e) { + return 0; + } + + return datagram.getLength() - 12; + } + + InetAddress getRemoteAddress() { + return mDatagram.getAddress(); + } + + int getRemotePort() { + return mDatagram.getPort(); + } + + int getSequenceNumber() { + return mPacket.getSequenceNumber(); + } + } + + private class RtpSender extends RtpReceiver { + private int mSequence = 0; + private long mTimeStamp = 0; + + RtpSender(int size) { + super(size); + } + + void send(int count) { + mTimeStamp += count; + RtpPacket packet = mPacket; + packet.setSequenceNumber(mSequence++); + packet.setTimestamp(mTimeStamp); + packet.setPayloadLength(count); + + DatagramPacket datagram = mDatagram; + datagram.setLength(packet.getPacketLength()); + try { + mSocket.send(datagram); + } catch (IOException e) { + Log.w("RtpSender", "running..." + e); + } + } + } + + // Use another thread to play back to avoid playback blocks network + // receiving thread + private class AudioPlayer implements + AudioTrack.OnPlaybackPositionUpdateListener { + private short[] mBuffer; + private int mStartMarker; + private int mEndMarker; + private AudioTrack mTrack; + private int mFrameSize; + private int mOffset; + private boolean mIsPlaying = false; + private boolean mNotificationStarted = false; + + AudioPlayer(AudioTrack track, int bufferSize, int frameSize) { + mTrack = track; + mBuffer = new short[bufferSize]; + mFrameSize = frameSize; + } + + synchronized int write(short[] buffer, int offset, int count) { + int bufferSize = mBuffer.length; + while (getBufferedDataSize() + count > bufferSize) { + try { + wait(); + } catch (Exception e) { + // + } + } + + int end = mEndMarker % bufferSize; + if (end + count > bufferSize) { + int partialSize = bufferSize - end; + System.arraycopy(buffer, offset, mBuffer, end, partialSize); + System.arraycopy(buffer, offset + partialSize, mBuffer, 0, + count - partialSize); + } else { + System.arraycopy(buffer, 0, mBuffer, end, count); + } + mEndMarker += count; + + if (!mNotificationStarted) { + writeToTrack(); + } + + return count; + } + + synchronized void flush() { + mEndMarker = mStartMarker; + notify(); + } + + int getBufferedDataSize() { + return mEndMarker - mStartMarker; + } + + synchronized void play() { + if (!mIsPlaying) { + mTrack.setPositionNotificationPeriod(mFrameSize); + mTrack.setPlaybackPositionUpdateListener(this); + mIsPlaying = true; + mTrack.play(); + mOffset = mTrack.getPlaybackHeadPosition(); + } + } + + synchronized void stop() { + mTrack.stop(); + mTrack.setPlaybackPositionUpdateListener(null); + mIsPlaying = false; + Log.d(TAG, "mIsPlaying set false"); + } + + synchronized void release() { + mTrack.release(); + } + + int getPlaybackHeadPosition() { + return mStartMarker; + } + + int getPlayState() { + return mTrack.getPlayState(); + } + + int getState() { + return mTrack.getState(); + } + + // callback + public void onMarkerReached(AudioTrack track) { + } + + // callback + public synchronized void onPeriodicNotification(AudioTrack track) { + if (!mNotificationStarted) { + mNotificationStarted = true; + Log.d(TAG, " ~~~ notification callback started"); + } else if (!mIsPlaying) { + Log.d(TAG, " ~x~ notification callback quit"); + return; + } + try { + writeToTrack(); + } catch (IllegalStateException e) { + Log.e(TAG, "writeToTrack()", e); + } + } + + private synchronized void writeToTrack() { + int bufferSize = mBuffer.length; + int start = mStartMarker % bufferSize; + int end = mEndMarker % bufferSize; + if (end == start) { + int head = mTrack.getPlaybackHeadPosition() - mOffset; + if (mStartMarker == head) { + // feed noise; keep mTrack busy + // TODO: create real noise + mTrack.write(mBuffer, 0, mFrameSize); + mOffset += mFrameSize; + } + return; + } + + int count = mFrameSize; + if (count < getBufferedDataSize()) count = getBufferedDataSize(); + + if ((start + count) <= bufferSize) { + mTrack.write(mBuffer, start, count); + } else { + int partialSize = bufferSize - start; + mTrack.write(mBuffer, start, partialSize); + mTrack.write(mBuffer, 0, count - partialSize); + } + mStartMarker += count; + notify(); + } + } +} diff --git a/src/com/android/sip/media/Decoder.java b/src/com/android/sip/media/Decoder.java new file mode 100644 index 0000000..c0f1060 --- /dev/null +++ b/src/com/android/sip/media/Decoder.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2009 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 com.android.sip.media; + +import java.io.IOException; + +public interface Decoder { + int getSampleCount(int frameSize); + + /** + * Decodes from the encoded data array. + * + * @param result the decoded result array + * @param src encoded data + * @param count valid data length in src + * @param offset offset of data bytes in src + */ + int decode(short[] result, byte[] src, int count, int offset) + throws IOException; +} diff --git a/src/com/android/sip/media/Encoder.java b/src/com/android/sip/media/Encoder.java new file mode 100644 index 0000000..f1845e5 --- /dev/null +++ b/src/com/android/sip/media/Encoder.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2009 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 com.android.sip.media; + +public interface Encoder { + int getSampleCount(int frameSize); + + /** + * Encodes the sample data. + * + * @param src sample data + * @param count valid data length in src + * @param result the encoded result array + * @param offset offset of data bytes in result + */ + int encode(short[] src, int count, byte[] result, int offset); +} diff --git a/src/com/android/sip/media/G711Codec.java b/src/com/android/sip/media/G711Codec.java new file mode 100644 index 0000000..f58edfd --- /dev/null +++ b/src/com/android/sip/media/G711Codec.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2009 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 com.android.sip.media; + +/** + * G.711 codec. This class provides a-law conversion. + */ +public class G711Codec implements Encoder, Decoder { + // s0000000wxyz...s000wxyz + // s0000001wxyz...s001wxyz + // s000001wxyza...s010wxyz + // s00001wxyzab...s011wxyz + // s0001wxyzabc...s100wxyz + // s001wxyzabcd...s101wxyz + // s01wxyzabcde...s110wxyz + // s1wxyzabcdef...s111wxyz + + private static int b8to16(int b8) { + int shift = ((b8 & 0x70) >> 4) - 1; + int b12 = b8 & 0xF; + if (shift >= 0) b12 = (b12 + 0x10) << shift; + return b12 << 4; + } + + private static int b12to8(int b12) { + int shift = 0; + for (int tmp = b12 / 32; tmp > 0; tmp/=2) shift++; + int b8 = ((b12 >> shift) & 0xF); + if (b12 >= 16) b8 += ((shift + 1) << 4); + + // invert even bits + return (b8 ^ 0x55); + } + + private static byte[] table12to8 = new byte[4096]; + private static short[] table8to16 = new short[256]; + + static { + for (int i = 0; i < 2048; i++) { + int v = b12to8(i); + table12to8[i] = (byte) v; + table12to8[4095 - i] = (byte) (v + 128); + } + + for (int i = 0; i < 128; i++) { + int v = b8to16(i); + table8to16[i ^ 0x55] = (short) v; + table8to16[(i + 128) ^ 0x55] = (short) ((65536 - v) & 0xFFFF); + } + } + + public int decode(short[] b16, byte[] b8, int count, int offset) { + for (int i = 0, j = offset; i < count; i++, j++) { + b16[i] = table8to16[b8[j] & 0xff]; + } + return count; + } + + public int encode(short[] b16, int count, byte[] b8, int offset) { + for (int i = 0, j = offset; i < count; i++, j++) { + b8[j] = table12to8[(b16[i] & 0xffff) >> 4]; + } + return count; + } + + public int getSampleCount(int frameSize) { + return frameSize; + } +} diff --git a/src/com/android/sip/media/RtpPacket.java b/src/com/android/sip/media/RtpPacket.java new file mode 100644 index 0000000..60d79f2 --- /dev/null +++ b/src/com/android/sip/media/RtpPacket.java @@ -0,0 +1,134 @@ +/*
+ * Copyright (C) 2009 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 com.android.sip.media;
+
+import java.util.Random;
+
+/**
+ * RtpPacket implements a RTP packet.
+ */
+public class RtpPacket {
+ private static Random sRandom = new Random();
+
+ // |0 0 1 2 |
+ // |0.......8.......6.......4.......|
+ // |V PXC MT Seqnum (16) |
+ // |................................|
+ // |Timestamp (32) |
+ // | |
+ // |................................|
+ // | SSRC (32) |
+ // | |
+ // |................................|
+ // | CSRC list (16 items x 32 bits) |
+ // | |
+ // |................................|
+ // V: version, 2 bits
+ // P: padding, 1 bit
+ // X: extension, 1 bit
+ // C: CSRC count, 4 bits
+ // M: marker, 1 bit
+ // T: payload type: 7 bits
+
+ private byte[] packet; // RTP header + payload
+ private int packetLength;
+
+ public RtpPacket(byte[] buffer) {
+ packet = buffer;
+ setVersion(2);
+ setPayloadType(0x0F);
+ setSequenceNumber(sRandom.nextInt());
+ setSscr(sRandom.nextLong());
+ }
+
+ /** Returns the RTP packet in raw bytes. */
+ public byte[] getRawPacket() {
+ return packet;
+ }
+
+ public int getPacketLength() {
+ return packetLength;
+ }
+
+ public int getHeaderLength() {
+ return (12 + 4 * getCscrCount());
+ }
+
+ public int getPayloadLength() {
+ return (packetLength - getHeaderLength());
+ }
+
+ public void setPayloadLength(int length) {
+ packetLength = getHeaderLength() + length;
+ }
+
+ public int getVersion() {
+ return ((packet[0] >> 6) & 0x03);
+ }
+
+ public void setVersion(int v) {
+ if (v > 3) throw new RuntimeException("illegal version: " + v);
+ packet[0] = (byte) ((packet[0] & 0x3F) | ((v & 0x03) << 6));
+ }
+
+ int getCscrCount() {
+ return (packet[0] & 0x0F);
+ }
+
+ public int getPayloadType() {
+ return (packet[1] & 0x7F);
+ }
+
+ public void setPayloadType(int pt) {
+ packet[1] = (byte) ((packet[1] & 0x80) | (pt & 0x7F));
+ }
+
+ public int getSequenceNumber() {
+ return (int) get(2, 2);
+ }
+
+ public void setSequenceNumber(int sn) {
+ set((long) sn, 2, 2);
+ }
+
+ public long getTimestamp() {
+ return get(4, 4);
+ }
+
+ public void setTimestamp(long timestamp) {
+ set(timestamp, 4, 4);
+ }
+
+ void setSscr(long ssrc) {
+ set(ssrc, 8, 4);
+ }
+
+ private long get(int begin, int length) {
+ long n = 0;
+ for (int i = begin, end = i + length; i < end; i++) {
+ n = (n << 8) | ((long) packet[i] & 0xFF);
+ }
+ return n;
+ }
+
+ private void set(long n, int begin, int length) {
+ for (int i = begin + length - 1; i >= begin; i--) {
+ packet[i] = (byte) (n & 0x0FFL);
+ n >>= 8;
+ }
+ }
+}
|