/* * 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) { } } }