/*
* Copyright 2007 Sun Microsystems, Inc.
*
* This file is part of jVoiceBridge.
*
* jVoiceBridge is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation and distributed hereunder
* to you.
*
* jVoiceBridge is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Sun designates this particular file as subject to the "Classpath"
* exception as provided by Sun in the License file that accompanied this
* code.
*/
package com.sun.voip;
import java.util.LinkedList;
import java.util.NoSuchElementException;
public class JitterManager {
public static int DEFAULT_MIN_JITTER_BUFFER_SIZE = 3; // packets
public static int DEFAULT_MAX_JITTER_BUFFER_SIZE = 9;
private int minJitterBufferSize = DEFAULT_MIN_JITTER_BUFFER_SIZE;
private int maxJitterBufferSize = DEFAULT_MAX_JITTER_BUFFER_SIZE;
private int jitter;
private int maxJitter;
private int elapsed;
private String id;
private Plc plc;
private PlcFactory plcFactory;
private String plcClassName = "com.sun.voip.PlcCompress";
/*
* Manage jitter and lost or out of order packets.
*/
public JitterManager(String id) {
this.id = id;
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println(id + ": jitterManager "
+ " min size " + minJitterBufferSize
+ " max size " + maxJitterBufferSize);
}
plcFactory = PlcFactory.getInstance();
plc = plcFactory.createPlc(plcClassName);
plc.setId(id);
}
/*
* Keep track of max jitter
* When there's no jitter, elapsed is RtpPacket.PACKET_PERIOD
*/
private void updateJitter(int elapsed) {
if (elapsed < 0) {
if (Logger.logLevel >= Logger.LOG_DETAILINFO) {
Logger.println(id + ": bad elapsed! " + elapsed);
}
elapsed = 0;
return;
}
if (Logger.logLevel == -19) {
if (elapsed > this.elapsed + (2 * RtpPacket.PACKET_PERIOD)) {
Logger.println(id + ": long elapsed " + elapsed);
}
}
this.elapsed = elapsed;
if (elapsed >= packetArrivalDistribution.length) {
elapsed = packetArrivalDistribution.length - 1;
}
packetArrivalDistribution[elapsed]++;
int jitter = elapsed - RtpPacket.PACKET_PERIOD;
if (jitter > maxJitter) {
maxJitter = jitter;
}
if (Logger.logLevel == -20) {
Logger.println(id + ": " + "old jitter "
+ this.jitter + " new jitter " + jitter);
}
if (jitter > this.jitter) {
this.jitter = jitter;
} else {
/*
* Reduce jitter value slowly
*/
int change = this.jitter - jitter;
this.jitter = this.jitter - (change / 4);
if (this.jitter < 0) {
this.jitter = 0;
}
}
}
/*
* Determine where to place the next packet based on the
* amount of jitter. The packet will only be placed at this index
* if the jitter buffer is empty. Otherwise the packet is inserted
* at its correct place in the buffer based on its sequence number.
*/
private int getJitterIndex() {
if (maxJitterBufferSize == 0) {
return 0;
}
int jitter = this.jitter;
jitter = (jitter + RtpPacket.PACKET_PERIOD - 1) /
RtpPacket.PACKET_PERIOD * RtpPacket.PACKET_PERIOD;
int packetListIndex = jitter / RtpPacket.PACKET_PERIOD;
if (maxJitterBufferSize > 0 && Logger.logLevel >= Logger.LOG_MOREDETAIL) {
Logger.println(id + ": jitter " + jitter + " index " + packetListIndex);
}
if (packetListIndex < minJitterBufferSize) {
return minJitterBufferSize;
}
/*
* Don't fill the jitter buffer with silence packets.
*/
if (packetListIndex > maxJitterBufferSize / 2) {
packetListIndex = maxJitterBufferSize / 2;
}
return packetListIndex;
}
public void setMinJitterBufferSize(int minJitterBufferSize) {
if (minJitterBufferSize <= 0) {
this.minJitterBufferSize = 0;
return;
}
if (minJitterBufferSize > maxJitterBufferSize) {
this.minJitterBufferSize = maxJitterBufferSize;
return;
}
this.minJitterBufferSize = minJitterBufferSize;
}
public int getMinJitterBufferSize() {
return minJitterBufferSize;
}
public void setMaxJitterBufferSize(int maxJitterBufferSize) {
if (maxJitterBufferSize <= 0) {
Logger.println(id + " invalid maxJitterBufferSize "
+ maxJitterBufferSize + ". Using default value "
+ " of " + DEFAULT_MAX_JITTER_BUFFER_SIZE);
maxJitterBufferSize = DEFAULT_MAX_JITTER_BUFFER_SIZE;
}
if (maxJitterBufferSize < minJitterBufferSize) {
this.maxJitterBufferSize = minJitterBufferSize;
return;
}
this.maxJitterBufferSize = maxJitterBufferSize;
}
public int getMaxJitterBufferSize() {
return maxJitterBufferSize;
}
public void setPlcClassName(String plcClassName) {
if (this.plcClassName.equals(plcClassName)) {
return;
}
this.plcClassName = plcClassName;
synchronized (this) {
plc = plcFactory.createPlc(plcClassName);
plc.setId(id);
}
}
public String getPlcClassName() {
return plc.getClass().getCanonicalName();
}
public void printStatistics() {
Logger.writeFile(id + ": " + maxJitter + " maxJitter milliseconds");
Logger.writeFile(id + ": " + insertedSilence
+ " times jitter manager inserted silence");
Logger.writeFile(id + ": " + outOfOrderPackets + " missing packets");
Logger.writeFile(id + ": " + (outOfOrderPackets - failedToRecover)
+ " recovered missing packets");
Logger.writeFile(id + ": " + oldTossed + " old packets tossed");
Logger.writeFile(id + ": " + packetList.size()
+ " packets in jitter buffer");
Logger.writeFile(id + "");
Logger.writeFile(id + ": " + "Packet receive distribution");
Logger.writeFile(id + ": " + "ms\tPackets");
for (int i = 0; i < packetArrivalDistribution.length; i++) {
if (packetArrivalDistribution[i] != 0) {
Logger.writeFile(id + ": " + i + "\t"
+ packetArrivalDistribution[i]);
}
}
}
public int getNumberMissingPackets() {
return failedToRecover;
}
public int getPacketListSize() {
return packetList.size();
}
private LinkedList packetList = new LinkedList(); // list of 20ms rcv bufs
private short firstSequence;
private int insertedSilence;
private int outOfOrderPackets;
private int failedToRecover;
private int oldTossed;
private int[] packetArrivalDistribution = new int[500];
/*
* packetList holds JitterObjects
*
* - a JitterObject for a missing packet will have isMissing set
*
* _ a JitterObject for inserted silence will have data set to null
*
* - a JitterObject with data null is a place holder.
* Place holders preserve the sequence ordering and are used
* for non-media type packets such as comfort payload and
* telephone events and inserted silence.
*
* insertPacket() inserts packets in the right place in packetList as
* determined by the sequence number. If the packetList is empty
* then the jitter index is used to determine how much latency (silence)
* to add.
*
* The number of inserted silence packets is returned.
*/
public int insertPacket(short sequence, int elapsed) {
updateJitter(elapsed);
return insertPacket(sequence, (byte[]) null);
}
public int insertPacket(short sequence, byte[] data) {
return insertPacket(sequence, (Object) data);
}
public int insertPacket(short sequence, int[] data) {
return insertPacket(sequence, (Object) data);
}
private int insertPacket(short sequence, Object data) {
JitterObject jitterObject = new JitterObject(
sequence, false, data);
/*
* Shouldn't need to do this. If elapsed is bigger
* than the maxJitterBufferSize, then the jitter buffer
* should be empty.
* XXX
*/
if (maxJitterBufferSize > 0 &&
elapsed > maxJitterBufferSize * RtpPacket.PACKET_PERIOD) {
if (packetList.size() > 0) {
if (Logger.logLevel >= Logger.LOG_DETAILINFO ||
Logger.logLevel == -19) {
Logger.println(id
+ ": clearing jitter buffer, no data in a long time, "
+ "packetList size " + packetList.size());
}
packetList.clear();
}
plc.reset();
}
if (maxJitterBufferSize > 0 &&
packetList.size() >= maxJitterBufferSize) {
if (Logger.logLevel >= Logger.LOG_MOREINFO ||
Logger.logLevel == -19) {
Logger.println(id + ": JitterBuffer full, clearing "
+ packetList.size() + " packets");
}
packetList.clear();
plc.reset();
}
int silenceCount = 0;
int size = packetList.size();
if (size == 0) {
/*
* Insert JitterObjects for silence.
* Set firstSequence appropriately.
*/
silenceCount = insertSilence(jitterObject);
} else if (size >= minJitterBufferSize) {
/*
* If we get a burst of packets, try to remove
* the silence packets we inserted
*/
removeSilence();
}
/*
* Get the index in packetList where to place this packet.
*/
short index = (short) (sequence - firstSequence);
if (index >= 0) {
if (index < packetList.size()) {
handleOldPacket(jitterObject, index);
} else {
handleNewPacket(jitterObject, index);
}
} else {
/*
* We've already delivered packets after this one so we
* have no choice but to toss it.
*/
if (Logger.logLevel >= Logger.LOG_MOREINFO ||
Logger.logLevel == -19) {
Logger.println(id + ": tossing old packet "
+ (sequence & 0xffff) + " index "
+ index + " firstSequence " + (firstSequence & 0xffff)
+ " packetList size " + packetList.size() + " elapsed "
+ elapsed);
}
oldTossed++;
}
return silenceCount;
}
private int insertSilence(JitterObject jo) {
int jitterIndex = getJitterIndex();
insertedSilence++;
firstSequence = (short) (jo.sequence - (short) jitterIndex);
if (jitterIndex > 0) {
if (Logger.logLevel >= Logger.LOG_MOREINFO ||
Logger.logLevel == -19) {
Logger.println(id + ": empty list, inserting "
+ jitterIndex + " silence packets, sequence "
+ (jo.sequence & 0xffff) + " firstSequence "
+ (firstSequence & 0xffff) + ", elapsed " + elapsed);
}
for (int i = 0; i < jitterIndex; i++) {
JitterObject silence = new JitterObject(
firstSequence + i, false, null);
packetList.add(silence);
}
}
return jitterIndex;
}
/*
* Handle a packet which has already been inserted or fill in a
* place holder.
*/
private void handleOldPacket(JitterObject jitterObject, int index) {
JitterObject jo = (JitterObject) packetList.get(index);
if (jo.isMissing) {
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println(id + ": got missing packet "
+ (jo.sequence & 0xffff)
+ " firstSequence " + (firstSequence & 0xffff)
+ " index " + index
+ " packetList size " + packetList.size());
}
//dumpList();
}
if (Logger.logLevel >= Logger.LOG_DETAILINFO) {
Logger.println(id + ": inserting "
+ (jitterObject.sequence & 0xffff)
+ " at " + index + " size " + packetList.size()
+ " jitterObject: " + jitterObject);
}
checkIndex(jitterObject.sequence, index);
packetList.set(index, jitterObject);
}
private void handleNewPacket(JitterObject jitterObject, int index) {
if (index > packetList.size()) {
handleOutOfOrderPackets(jitterObject, index);
}
if (Logger.logLevel >= Logger.LOG_MOREDETAIL ||
Logger.logLevel == -20) {
Logger.println(id + ": appending "
+ " " + (jitterObject.sequence & 0xffff)
+ " at " + packetList.size()
+ " jitterObject: " + jitterObject);
}
packetList.add(jitterObject);
}
private void handleOutOfOrderPackets(JitterObject jo, int index) {
/*
* One or more packets is missing.
* Insert JitterObjects with data set to null to reserve slots in
* case the missing packets arrive later.
*/
short expected = (short) (firstSequence + packetList.size());
int missingPackets = (int) (jo.sequence - expected);
if (Logger.logLevel >= Logger.LOG_MOREINFO) {
Logger.println(id + ": expected "
+ (expected & 0xffff) + " got "
+ (jo.sequence & 0xffff) + ", "
+ missingPackets + " missing packets"
+ " inserting at index " + index
+ " first sequence " + (firstSequence & 0xffff)
+ " list size " + packetList.size()
+ " elapsed " + elapsed);
}
outOfOrderPackets += missingPackets;
// XXX if there are too many missing packets, reset and start over
if (missingPackets >= maxJitterBufferSize) {
if (Logger.logLevel >= Logger.LOG_MOREINFO ||
Logger.logLevel == -19) {
Logger.println(id + ": resetting jitter buffer. "
+ " too many missing packets " + missingPackets);
}
packetList.clear();
plc.reset();
insertSilence(jo);
return;
}
Object data = null;
try {
JitterObject lastPacket = (JitterObject) packetList.getLast();
data = lastPacket.data;
} catch (NoSuchElementException e) {
}
for (int i = 0; i < missingPackets; i++) {
JitterObject missing = new JitterObject(expected + i, true, data);
packetList.add(missing);
}
}
private void dumpList() {
for (int i = 0; i < packetList.size(); i++) {
JitterObject jo = (JitterObject) packetList.get(i);
Logger.println(id + ": " + ((firstSequence + i) & 0xffff)
+ " " + jo);
}
}
private void checkIndex(int sequence, int index) {
if (index >= packetList.size()) {
return;
}
JitterObject jo = (JitterObject) packetList.get(index);
if (jo.sequence != sequence) {
Logger.println(id
+ ": jitterManager overwriting wrong sequence! index " + index
+ ", jo.sequence " + jo.sequence + " != "
+ "sequence " + sequence + ", firstSequence "
+ firstSequence
+ " jitterObject: " + jo);
}
if (jo.isMissing == false && jo.data != null) {
Logger.println(id
+ ": jitterManager overwriting valid packet! index "
+ index + " sequence " + jo.sequence + " firstSequence "
+ firstSequence + " jitterObject: " + jo);
}
}
private void removeSilence() {
if (packetList.size() == 0) {
return;
}
JitterObject jo = (JitterObject) packetList.get(0);
if (jo.isMissing || jo.data != null) {
return;
}
try {
getFirstPacket(); // remove silence packet
if (Logger.logLevel == -19) {
Logger.println(id + ": removed silence packet "
+ "size " + packetList.size());
}
} catch (NoSuchElementException e) {
}
}
public JitterObject getFirstPacket() throws NoSuchElementException {
JitterObject jo;
while (true) {
jo = (JitterObject) packetList.removeFirst();
if (Logger.logLevel >= Logger.LOG_DETAILINFO) {
Logger.println(id + ": getting "
+ (firstSequence & 0xffff) + " jitterObject: " + jo);
Logger.println("");
}
if (jo.sequence != firstSequence) {
Logger.println(id + ": jo seq " + jo.sequence + " != first seq "
+ firstSequence);
dumpList();
}
firstSequence++;
if (jo.isMissing) {
/*
* There are missing packets we didn't get.
* Try to repair the damage.
*/
failedToRecover++;
if (Logger.logLevel >= Logger.LOG_INFO) {
Logger.println(id + ": Failed to recover packet "
+ (jo.sequence & 0xffff));
}
jo = plc.repair(jo);
if (jo != null) {
/*
* Update data field in missing packets after this one.
*/
for (int i = 0; i < packetList.size(); i++) {
JitterObject jitterObject = (JitterObject)
packetList.get(i);
if (jitterObject.isMissing == false) {
break;
}
jitterObject.data = jo.data;
}
break;
}
} else {
if (jo.data != null) {
plc.addPacket(jo);
}
break;
}
}
return jo;
}
public int getJitterBufferSize() {
synchronized (this) {
return packetList.size();
}
}
public void flush() {
try {
while (getFirstPacket() != null) {
}
} catch (NoSuchElementException e) {
}
}
}