/*
* @(#)RTPManager.java
* Created: 25-Oct-2005
* Version: 1-1-alpha3
* Copyright (c) 2005-2006, University of Manchester All rights reserved.
*
* Andrew G D Rowley
* Christian Vincenot <sipcom@cyberspace7.net>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer. Redistributions in binary
* form must reproduce the above copyright notice, this list of conditions and
* the following disclaimer in the documentation and/or other materials
* provided with the distribution. Neither the name of the University of
* Manchester nor the names of its contributors may be used to endorse or
* promote products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package net.sf.fmj.media.rtp;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.awt.Dimension;
import javax.media.Format;
import javax.media.format.AudioFormat;
import javax.media.format.UnsupportedFormatException;
import javax.media.format.VideoFormat;
import javax.media.protocol.DataSource;
import javax.media.protocol.PullBufferDataSource;
import javax.media.protocol.PullBufferStream;
import javax.media.protocol.PushBufferDataSource;
import javax.media.protocol.PushBufferStream;
import javax.media.rtp.EncryptionInfo;
import javax.media.rtp.GlobalReceptionStats;
import javax.media.rtp.GlobalTransmissionStats;
import javax.media.rtp.LocalParticipant;
import javax.media.rtp.OutputDataStream;
import javax.media.rtp.Participant;
import javax.media.rtp.RTPConnector;
import javax.media.rtp.RTPStream;
import javax.media.rtp.ReceiveStream;
import javax.media.rtp.ReceiveStreamListener;
import javax.media.rtp.RemoteListener;
import javax.media.rtp.SendStream;
import javax.media.rtp.SendStreamListener;
import javax.media.rtp.SessionAddress;
import javax.media.rtp.SessionListener;
import javax.media.rtp.SessionManager;
import javax.media.rtp.TransmissionStats;
import javax.media.rtp.event.ActiveReceiveStreamEvent;
import javax.media.rtp.event.ByeEvent;
import javax.media.rtp.event.InactiveReceiveStreamEvent;
import javax.media.rtp.event.NewParticipantEvent;
import javax.media.rtp.event.NewReceiveStreamEvent;
import javax.media.rtp.event.NewSendStreamEvent;
import javax.media.rtp.event.ReceiveStreamEvent;
import javax.media.rtp.event.ReceiverReportEvent;
import javax.media.rtp.event.RemoteEvent;
import javax.media.rtp.event.SendStreamEvent;
import javax.media.rtp.event.SenderReportEvent;
import javax.media.rtp.event.SessionEvent;
import javax.media.rtp.event.StreamMappedEvent;
import javax.media.rtp.rtcp.SourceDescription;
import net.sf.fmj.media.datasink.rtp.RTPBonusFormatsMgr;
import net.sf.fmj.utility.FmjStartup;
import net.sf.fmj.utility.LoggerSingleton;
/**
* Kernel of the RTP stack. This is where almost everything is done.
* Instances of this class handle a single RTP session which is
* an association among a set of participants communicating with RTP.
* A participant may be involved in multiple RTP sessions at the same time.
* In a multimedia session, each medium is typically carried in a separate
* RTP session with its own RTCP packets (unless the the encoding itself
* multiplexes multiple media into a single data stream).
* @author Andrew G D Rowley
* @author Christian Vincenot
* @version 1-1-alpha3
*/
public class RTPSessionMgr extends javax.media.rtp.RTPManager implements
SessionManager {
private static final Logger logger = LoggerSingleton.logger;
// The minimum RTCP interval in ms
private static final int MIN_RTCP_INTERVAL = 5000;
// The period between consecutive participants timeout checks
// (should happen at least once per RTCP period)
private static final long REAPER_PERIOD = MIN_RTCP_INTERVAL - 1;
// Average size of the lower layers (network & transport)
// WARNING/TODO: a better solution should be found. Hardcoding this
// sucks and will give poor results with IPv6
private static final int LOWLAYERS = 20 + 8;
// The map of Integer -> format recognised
private HashMap formatMap = new HashMap();
// A vector of receive stream listeners
private Vector receiveStreamListeners = new Vector();
// A vector of remote listeners
private Vector remoteListeners = new Vector();
// A vector of send stream listeners
private Vector sendStreamListeners = new Vector();
// A vector of session listeners
private Vector sessionListeners = new Vector();
// The local participant
private RTPLocalParticipant localParticipant = null;
// A map of active participants (cname -> participant)
private HashMap activeParticipants = new HashMap();
// A map of inactive participants (cname -> participant)
private HashMap inactiveParticipants = new HashMap();
// The global reception statistics
private RTPGlobalReceptionStats globalReceptionStats =
new RTPGlobalReceptionStats();
// The global transmission statistics
private RTPGlobalTransmissionStats globalTransmissionStats =
new RTPGlobalTransmissionStats();
// A map of receive streams (ssrc -> stream)
private HashMap receiveStreams = new HashMap();
// A map of send streams (ssrc -> stream)
private HashMap sendStreams = new HashMap();
// A map of streams that are ignored as they cannot be recognised
private HashMap ignoredStreams = new HashMap();
// Streams which have been mapped until now
private HashMap unassignedStreams = new HashMap();
// A map of ssrcs to cnames
private HashMap senders = new HashMap();
// The RTCP Receiver Bandwidth fraction
private double rtcpReceiverBandwidthFraction = 0.0375;
// The RTCP Sender Bandwidth fraction
private double rtcpSenderBandwidthFraction = 0.0125;
// The local address to use to bind sockets to
private SessionAddress localAddress = null;
// The RTP Connectors for targets that have been set up
// (address -> connector)
private final HashMap<SessionAddress, RTPConnector> targets = new HashMap<SessionAddress, RTPConnector>();
// The RTP Handler
private RTPHandler rtpHandler = null;
// The RTCP Handler
private RTCPHandler rtcpHandler = null;
// The lock for sending events (so events are received in order)
private Integer eventLock = new Integer(0);
// True if the event lock has been obtained
private boolean eventLocked = false;
// True when the session is finished with
private boolean done = false;
// An RTCP timer
private Timer rtcpTimer = new Timer("RTCP Timer", true);
// The Timer Task meant to send the RTCP packets (NOTE: access to it might
// have to be synchronized using a non-blocking mutex system)
private RTCPTimerTask RTCPSendTask = null;
// The Grim Reaper Timer : checks the dying participants and kills those who
// have timed out
private Timer reaperTimer = new Timer("Reaper Timer", true);
// The time at which the last rtcpPacket was sent
private long lastRTCPSendTime = -1;
// Time at which the next RTCP packet must be sent
private long nextRTCPSendTime = -1;
// The average size of an RTCP packet received
// Initialized to the size of an RTCP RR packet
private int averageRTCPSize = 60;
// How many rtcp packets were sent
private long rtcpPacketsSent = 0;
// Flag set if we're about to leave the session (a BYE packet is waiting to be sent)
private boolean sayonara = false;
// Position in the round-robin (used when having to send RTCP RR packets with
// too many report blocks to fit in a single RTCP packet)
private int RRRoundRobin = 0;
// The we_sent variable described in the RFC. Its value is true if we've recently
// sent an RTP packet
private boolean we_sent = false;
// The number of members when the last RTCP delay was calculated
private long pmembers=1;
// The ssrc if we are not sending
protected long ssrc=0;
// Our SSRC generator
private SSRCGenerator generator;
// the socket adapter (RTPConnector) used to send and receive RTP packets
RTPConnector socket = null;
private void getEventLock() {
synchronized (eventLock) {
while (eventLocked) {
try {
eventLock.wait();
} catch (InterruptedException e) {
// Do Nothing
}
}
eventLocked = true;
}
}
private void releaseEventLock() {
synchronized (eventLock) {
eventLocked = false;
eventLock.notifyAll();
}
}
/**
* Creates a new RTPManager
*/
public RTPSessionMgr() {
//new Error().printStackTrace();
String user = "username";
if ( !FmjStartup.isApplet )
{
user = System.getProperty("user.name");
}
String host = "localhost";
if ( !FmjStartup.isApplet )
{
try
{
host = InetAddress.getLocalHost().getHostName();
}
catch (UnknownHostException e)
{
// Do Nothing
}
}
this.localParticipant = new RTPLocalParticipant(user + "@FMJ." + host);
this.generator = new SSRCGenerator();
//this.ssrc = generateSSRC();
addFormat(new AudioFormat(AudioFormat.ULAW_RTP, 8000, 8, 1), 0);
addFormat(new AudioFormat(AudioFormat.GSM_RTP, 8000,
Format.NOT_SPECIFIED, 1), 3);
addFormat(new AudioFormat(AudioFormat.G723_RTP, 8000,
Format.NOT_SPECIFIED, 1), 4);
addFormat(new AudioFormat(AudioFormat.DVI_RTP, 8000, 4, 1), 5);
addFormat(new AudioFormat(AudioFormat.MPEG_RTP), 14);
addFormat(new AudioFormat(AudioFormat.G728_RTP, 8000.0, Format.NOT_SPECIFIED, 1), 15);
addFormat(new AudioFormat(AudioFormat.DVI_RTP, 11025, 4, 1), 16);
addFormat(new AudioFormat(AudioFormat.DVI_RTP, 22050, 4, 1), 17);
addFormat(new AudioFormat(AudioFormat.G729_RTP, 8000.0, Format.NOT_SPECIFIED, 1), 18);
addFormat(new VideoFormat(VideoFormat.JPEG_RTP), 26);
addFormat(new VideoFormat(VideoFormat.H261_RTP), 31);
addFormat(new VideoFormat(VideoFormat.MPEG_RTP), 32);
addFormat(new VideoFormat(VideoFormat.H263_RTP), 34);
addFormat(new VideoFormat(VideoFormat.H263_1998_RTP), 42);
}
/**
* This is a function provided by Sun's implementation, which is not in the API as AFAIK - kenlars99.
* This does not include formats in the RTP bonus formats mgr.
* This is a somewhat dubious function, since the actual RTPSessionMgr used is determined by RTPManager.newInstance().
* To the extend that Sun's code calls this function, it is assuming that its own RTPSessionMgr will be used, which
* may be the source of some problems.
* @param f the format which we want to test
* @return true if the format is supported
*
* Note: the format list in JMF is static, and in FMJ it is not. So you can add a format once with
* JMF, but you have to add it every time you instantiate the manager with FMJ.
*
*/
public static boolean formatSupported(Format f) {
final RTPSessionMgr mgr = new RTPSessionMgr();
try
{
RTPBonusFormatsMgr.addBonusFormats(mgr);
final Iterator iter = mgr.formatMap.keySet().iterator();
while (iter.hasNext()) {
Integer id = (Integer) iter.next();
Format testFormat = (Format) mgr.formatMap.get(id);
if (testFormat.matches(f)) {
return true;
}
}
return false;
}
finally
{ mgr.dispose();
}
}
/** Return the SupportedFormats via RTP **/
public static Format[] getSupportedFormats()
{
final RTPSessionMgr mgr = new RTPSessionMgr();
RTPBonusFormatsMgr.addBonusFormats(mgr);
return (Format[]) mgr.formatMap.values().toArray(new Format[0]);
}
/** Another function provided by Sun's implementation, not in the spec/API. */
public Format getFormat(int payload) {
return (Format) formatMap.get(new Integer(payload));
}
/**
* Add a format to the list of supported formats.
* @param format format to add
* @param payload type of payload corresponding to this format
*/
@Override
public void addFormat(Format format, int payload) {
formatMap.put(new Integer(payload), format);
}
/**
* Adds a listener to the list of listeners wanting to be informed when a receive stream is created.
* @param listener the listener to add
*/
@Override
public void addReceiveStreamListener(ReceiveStreamListener listener) {
receiveStreamListeners.add(listener);
}
/**
* Adds a listener to the list of remote listeners.
* @param listener listener to add
*/
@Override
public void addRemoteListener(RemoteListener listener) {
remoteListeners.add(listener);
}
/**
* Add this listener to the list of listeners wanting to be informed when a send stream is created.
* @param listener the listener to add
*/
@Override
public void addSendStreamListener(SendStreamListener listener) {
sendStreamListeners.add(listener);
}
/**
* Add this listener to the list of session listeners.
* @param listener listener to add
*/
@Override
public void addSessionListener(SessionListener listener) {
sessionListeners.add(listener);
}
/**
* Removes this listener from the list of listeners wanting to be be informed
* of the creation of new receive streams.
* @param listener listener to remove
*/
@Override
public void removeReceiveStreamListener(ReceiveStreamListener listener) {
receiveStreamListeners.remove(listener);
}
/**
* Removes this listener from the list of remote listeners.
* @param listener listener to remove
*/
@Override
public void removeRemoteListener(RemoteListener listener) {
remoteListeners.remove(listener);
}
/**
* Removes this listener from the list of listeners wanting to be informed of
* the creation of send streams.
* @param listener listener to remove
*/
@Override
public void removeSendStreamListener(SendStreamListener listener) {
sendStreamListeners.remove(listener);
}
/**
* Removes this listener from the list of session listeners.
* @param listener listener to remove
*/
@Override
public void removeSessionListener(SessionListener listener) {
sessionListeners.remove(listener);
}
/**
* Returns the list of active participants in this RTP session.
* @return the list of active participants in this RTP session
*/
@Override
public Vector getActiveParticipants() {
Vector participants = new Vector(activeParticipants.values());
if (localParticipant.isActive()) {
participants.add(localParticipant);
}
return participants;
}
/**
* Returns the list of all participants in this RTP session.
* @return the list of all participants in this RTP session
*/
@Override
public Vector getAllParticipants() {
Vector participants = new Vector();
participants.addAll(activeParticipants.values());
participants.addAll(inactiveParticipants.values());
participants.add(localParticipant);
return participants;
}
/**
* Returns the local participant.
* @return the local participant
*/
@Override
public LocalParticipant getLocalParticipant() {
return localParticipant;
}
/**
* Returns the list of passive participants in this RTP session.
* @return the list of passive participants in this RTP session
*/
@Override
public Vector getPassiveParticipants() {
Vector participants = new Vector(inactiveParticipants.values());
if (!localParticipant.isActive()) {
participants.add(localParticipant);
}
return participants;
}
/**
* Returns the list of remote participants in this RTP session.
* @return the list of passive participants in this RTP session
*/
@Override
public Vector getRemoteParticipants() {
Vector participants = new Vector();
participants.addAll(activeParticipants.values());
participants.addAll(inactiveParticipants.values());
return participants;
}
/**
* Returns the global reception statistics for this RTP session.
* @return the global reception stats
*/
@Override
public GlobalReceptionStats getGlobalReceptionStats() {
return globalReceptionStats;
}
/**
* Returns the global transmission statistics for this RTP session.
* @return the global transmission statistics for this RTP session
*/
@Override
public GlobalTransmissionStats getGlobalTransmissionStats() {
return globalTransmissionStats;
}
/**
* Returns the list of receive streams in this RTP session.
* This is the list of streams used to receive data,
* ie connected to senders.
* @return the list of receive streams in this RTP session
*/
@Override
public Vector getReceiveStreams() {
return new Vector(receiveStreams.values());
}
/**
* Returns the list of send streams in this RTP session.
* This is the list of streams used to send data,
* ie connected to receivers.
* @return the list of send streams in this RTP session
*/
@Override
public Vector getSendStreams() {
return new Vector(sendStreams.values());
}
/**
* Initializes and starts the RTP Session.
* @param localAddress the local address
* @throws java.io.IOException I/O Exception
*/
@Override
public void initialize(SessionAddress localAddress)
throws IOException {
//System.out.println("FMJ : INITIALISATION RTP !!");
String user = "username";
if ( !FmjStartup.isApplet )
{
user = System.getProperty("user.name");
}
initialize(new SessionAddress[] { localAddress },
new SourceDescription[] {
new SourceDescription(
SourceDescription.SOURCE_DESC_CNAME, user
+ "@"
+ InetAddress.getLocalHost()
.getHostName(), 1, false),
new SourceDescription(
SourceDescription.SOURCE_DESC_NAME, user
+ "@"
+ InetAddress.getLocalHost()
.getHostName(), 3, false) },
0.05, 0.25, null);
}
/**
* Initializes and starts the RTP Session.
* @param localAddresses the local address
* @param sourceDescription the source description to be used when sending SDES reports
* @param rtcpBandwidthFraction the bandwidth fraction allocated for RTCP traffic
* @param rtcpSenderBandwidthFraction the RTCP bandwidth fraction allocated for senders
* @param encryptionInfo Encryption information - UNUSED at the moment
*/
@Override
public void initialize(SessionAddress[] localAddresses,
SourceDescription[] sourceDescription,
double rtcpBandwidthFraction, double rtcpSenderBandwidthFraction,
EncryptionInfo encryptionInfo) {
//System.out.println("FMJ : INITIALISATION RTP !!");
/* Note: the two parameters supplied here are the global bandwidth % assigned
* to the RTCP traffic and the % of the RTCP traffic allocated for senders.
* This is not exactly what is suggested by the RFC (S : bw % for senders, R : bw % for receivers),
* but it's ok (if those parameters are the one really supplied to the RTP stack).
* BUG correction: original code was: sender_bw = bw * bw , which is obviously wrong.
*/
this.rtcpSenderBandwidthFraction = rtcpSenderBandwidthFraction
* rtcpBandwidthFraction;
this.rtcpReceiverBandwidthFraction = rtcpBandwidthFraction
- this.rtcpSenderBandwidthFraction;
/* BUG/STRANGE: Erases the localParticipant created in the constructor ???? */
//this.localParticipant = new RTPLocalParticipant("");
localParticipant.setSourceDescription(sourceDescription);
localAddress = localAddresses[0];
this.ssrc = generateSSRC();
inactiveParticipants.put(localParticipant.getCNAME(), localParticipant);
start();
}
/**
* Add a network target to which we'll send our RTP packets.
* This network target can be one receiver (unicast address)
* or several receivers (multicast address).
* @param remoteAddress remote address (unicast or multicast) to which we want to send our RTP packets
* @throws java.io.IOException I/O Exception
*/
@Override
public void addTarget(SessionAddress remoteAddress)
throws IOException {
//System.out.println("FMJ : Add Target !!");
if (socket == null) {
socket = new RTPSocketAdapter(localAddress.getDataAddress(),
localAddress.getDataPort(),
localAddress.getTimeToLive());
rtpHandler = new RTPHandler(this);
rtcpHandler = new RTCPHandler(this);
socket.getControlInputStream().setTransferHandler(rtcpHandler);
socket.getDataInputStream().setTransferHandler(rtpHandler);
}
if ( socket instanceof RTPSocketAdapter )
{
((RTPSocketAdapter)socket).addTarget(remoteAddress);
}
targets.put(remoteAddress, socket);
}
/**
* Initializes and starts the RTP Session.
* @param connector the RTP connector used to retrieve the control & data streams
*/
@Override
public void initialize(RTPConnector connector) {
//System.out.println("FMJ : INITIALISATION RTP !!");
try {
ssrc = generateSSRC();
socket = connector;
rtpHandler = new RTPHandler(this);
rtcpHandler = new RTCPHandler(this);
connector.getControlInputStream().setTransferHandler(rtcpHandler);
connector.getDataInputStream().setTransferHandler(rtpHandler);
targets.put((SessionAddress) null, connector);
start();
} catch (IOException e) {
logger.log(Level.WARNING, "" + e, e);
}
}
/**
* Remove a network target .
* @param remoteAddress remote address (unicast or multicast) to which we want to send our RTP packets
* @param reason reason for which we remove this target (used in the RTCP BYE packet following this call)
*/
@Override
public void removeTarget(SessionAddress remoteAddress, String reason)
{
Vector sendStreams = localParticipant.getStreams();
if ( sendStreams != null )
{
// TODO: check for multicast and broadcast
RTPSendStream sendStream = null;
if ( sendStreams.size() > 0 )
{
sendStream = (RTPSendStream)sendStreams.get(0);
}
sendBYE(sendStream, remoteAddress, reason);
}
if ( socket instanceof RTPSocketAdapter )
{
((RTPSocketAdapter)socket).removeTarget(remoteAddress);
}
targets.remove(remoteAddress);
}
/**
* Remove a network target .
* @param reason reason for which we remove this target (used in the RTCP BYE packet following this call)
*/
@Override
public void removeTargets(String reason) {
final Iterator<SessionAddress> iter = targets.keySet().iterator();
while (iter.hasNext()) {
final SessionAddress addr = iter.next();
removeTarget(addr, reason);
}
if (socket != null)
{
socket.close(); // removed all target, can close own socket also
socket = null;
}
}
private void sendBYE(RTPSendStream sendStream, SessionAddress remoteAddress, String reason)
{
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(bytes);
try
{
// RFC 3550 Page 33, MUST NOT send a BYE when we never send a RTP or RTCP packet
boolean fSendBYE = (rtcpPacketsSent > 0) ? true : false;
if ( (null != sendStream) && (sendStream.getSourceTransmissionStats().getBytesTransmitted() > 0) )
{
fSendBYE = true;
}
if ( fSendBYE )
{
// Determine the packet type
int packetType = RTCPPacket.PT_RR;
int packetSize = 1;
if ( we_sent )
{
packetType = RTCPPacket.PT_SR;
packetSize = 6;
}
// Send a empty RR/SR packet ( must be always the first rtcp packet in a compound packet )
output.writeByte(0x80);
output.writeByte(packetType & 0xFF);
output.writeShort(packetSize);
if ( we_sent )
{
writeSenderInfo(output, sendStream);
}
else
{
output.writeInt((int)(ssrc & 0xFFFFFFFF));
}
writeSDESPacket(output);
// calculate goodbye packet length
int byelen = 4;
if ( (null != reason) && (reason.length() > 0) )
{
byte[] reasonBytes = reason.getBytes("UTF-8");
byelen += reasonBytes.length + 1;
}
// do we need padding to next 32-bit boundary
int padding = 0;
if ( (byelen % 4) != 0 )
{
padding = 4 - (byelen % 4);
byelen += padding;
}
// Send a goodbye packet ( must be the last packet in a compound packet )
output.writeByte(0x81);
output.writeByte(RTCPPacket.PT_BYE & 0xFF);
output.writeShort(byelen / 4);
output.writeInt((int)(ssrc & 0xFFFFFFFF));
// Goodbye RTCP Packet, send the reason for leaving
if ( (null != reason) && (reason.length() > 0) )
{
byte[] reasonBytes = reason.getBytes("UTF-8");
output.writeByte(reasonBytes.length & 0xFF);
output.write(reasonBytes);
for (int i=0; i<padding; i++)
{
output.writeByte(0);
}
}
output.close();
bytes.close();
byte[] data = bytes.toByteArray();
if ( socket.getControlOutputStream() instanceof SocketOutputStream )
{
SocketOutputStream out = (SocketOutputStream) socket.getControlOutputStream();
out.writeToRemote(data, 0, data.length, remoteAddress.getControlAddress(), remoteAddress.getControlPort());
}
else
{
OutputDataStream outstream = socket.getControlOutputStream();
outstream.write(data, 0, data.length);
}
}
}
catch (IOException ex)
{
logger.log(Level.WARNING, "" + ex, ex);
}
}
/**
* Creates a send stream used to carry data from a source to some receivers over the network.
* @param dataSource the datasource producing the data to send
* @param streamIndex the index of the stream in the datasource's streams table
* @return the created SendStream
* @throws javax.media.format.UnsupportedFormatException thrown if the datasource's stream only outputs formats which aren't supported
* @throws java.io.IOException I/O Exception thrown in case of problem with streams
*/
@Override
public SendStream createSendStream(DataSource dataSource, int streamIndex)
throws UnsupportedFormatException, IOException {
int format = -1;
Format fmt = null;
double clockRate = 90000;
if (dataSource instanceof PushBufferDataSource) {
PushBufferStream stream =
((PushBufferDataSource) dataSource).getStreams()[streamIndex];
fmt = stream.getFormat();
} else if (dataSource instanceof PullBufferDataSource) {
PullBufferStream stream =
((PullBufferDataSource) dataSource).getStreams()[streamIndex];
fmt = stream.getFormat();
} else {
throw new IOException("Cannot use stream sources");
}
//System.out.println("createSendStream : Trying to find handler for format "+ fmt);
Iterator iter = formatMap.keySet().iterator();
while (iter.hasNext()) {
Integer id = (Integer) iter.next();
Format testFormat = (Format) formatMap.get(id);
//System.out.println("Comparing with format "+testFormat);
if (testFormat.matches(fmt)) {
format = id.intValue();
//System.out.println("GOT IT!");
}
}
if (format == -1) {
throw new UnsupportedFormatException(fmt);
}
if (fmt instanceof AudioFormat) {
clockRate = ((AudioFormat) fmt).getSampleRate();
}
OutputDataStream stream = socket.getDataOutputStream();
// see RFC 3550 Page 8, a RTP Session has ONLY one medium per session, multiplexing is done thru different ports
RTPSendStream RTPStream = new RTPSendStream(ssrc,
dataSource, stream, streamIndex, localParticipant, format,
clockRate, this);
sendStreams.put(new Long(ssrc), RTPStream);
/* Chris: Also add it to the local participant's streams?*/
localParticipant.addStream(RTPStream);
getEventLock();
SendStreamEvent ev = new NewSendStreamEvent(this, RTPStream);
new SendStreamNotifier(sendStreamListeners,ev);
return RTPStream;
}
/**
* Called when wishing to quit the RTP session.
*/
@Override
public void dispose() {
//removeTargets("Quitting");
closeSession("Quitting");
rtcpTimer.cancel();
reaperTimer.cancel();
done = true;
}
/**
* Get control for the specified type. DUMMY.
* @param controlClass the control class
* @return null
*/
public Object getControl(String controlClass) {
return null;
}
/**
* Get the list of all controls possible. DUMMY.
* @return an EMPTY ARRAY
*/
public Object[] getControls() {
return new Object[] {};
}
/**
* Handles an incoming RTP packet
*
* @param data The raw packet data
* @param offset The packet offset after which we must start investigating
* @param length The total packet length
*/
protected void handleRTPPacket(byte[] data, int offset, int length) {
//System.out.println("FMJ : RTP PACKET!!");
/* If we're about to leave the session, return to avoid messing up with the
* BYE sending procedure. */
if (sayonara) return;
try
{
// mgodehardt: we should add some RTP header sanity checks here or anywhere else ?
/* Statistics & RTP size calculation */
globalReceptionStats.addPacketRecd();
globalReceptionStats.addBytesRecd(length);
computeAverageRTPSize(length);
RTPHeader header = new RTPHeader(data, offset, length);
long ssrc = header.getSsrc();
/* This should be considered as loops, but for the moment we just exclude the
* packets using the same SSRC as ours (because they most probably come from us).
* THIS MUST BE MODIFIED TO CONFORM TO THE RFC-3550.
*/
Object test = sendStreams.get(new Long(ssrc));
if (test != null) {
logger.fine("Collision/Loop ??");
return;
}
Integer packetType = (Integer) ignoredStreams.get(new Long(ssrc));
if (packetType != null) {
if (packetType.intValue() != header.getPacketType()) {
ignoredStreams.remove(new Long(ssrc));
packetType = null;
}
}
if (packetType == null) {
RTPReceiveStream stream =
(RTPReceiveStream) receiveStreams.get(new Long(ssrc));
if (stream == null) {
int type = header.getPacketType();
Format format = (Format) formatMap.get(new Integer(type));
if (format == null) {
globalReceptionStats.addUnknownType();
logger.warning("Unknown format identifier: "
+ type);
ignoredStreams.put(new Long(ssrc), new Integer(type));
}
else
{
if ( format instanceof VideoFormat )
{
format = findVideoSize(data, offset + header.getSize(), (VideoFormat)format);
}
RTPDataSource dataSource = new RTPDataSource(ssrc, format);
stream = new RTPReceiveStream(dataSource, ssrc);
receiveStreams.put(new Long(ssrc), stream);
unassignedStreams.put(new Long(ssrc), stream);
ReceiveStreamEvent event = new NewReceiveStreamEvent(this, stream);
new ReceiveStreamNotifier(receiveStreamListeners, event);
}
}
if (stream != null) {
if (stream.checkResumeActivity()) {
try {
String cname = (String)senders.get(new Long(ssrc));
RTPRemoteParticipant pal =
(RTPRemoteParticipant) activeParticipants.get(cname);
/* Trigger InactiveReceiveStreamEvent */
getEventLock();
new ReceiveStreamNotifier(receiveStreamListeners,
new ActiveReceiveStreamEvent(this, pal, stream));
} catch (Exception e) {
e.printStackTrace();
}
}
RTPDataSource dataSource =
(RTPDataSource) stream.getDataSource();
dataSource.handleRTPPacket(header,
data, offset + header.getSize(),
length - header.getSize());
}
}
} catch (IOException e) {
globalReceptionStats.addBadRTPkt();
}
}
private VideoFormat findVideoSize(byte[] data, int offset, VideoFormat format)
{
if ( format.getEncoding().equalsIgnoreCase(VideoFormat.JPEG_RTP) )
{
int width = data[offset + 6] << 3;
int height = data[offset + 7] << 3;
return new VideoFormat(VideoFormat.JPEG_RTP, new Dimension(width, height), -1, format.getDataType(), -1);
}
return format;
}
/**
* Handles an incoming RTCP packet
* @param data The packet data
* @param offset The packet offset
* @param length The packet length
*/
protected void handleRTCPPacket(byte[] data, int offset, int length) {
//System.out.println("FMJ : RTCP PACKET!!");
//TODO : special case of a participant sending only an ignored stream
// (will be counted as receiver for the moment but shouldn't)
try {
globalReceptionStats.addRTCPRecd();
globalReceptionStats.addBytesRecd(length);
RTCPHeader header = new RTCPHeader(data, offset, length);
// Get the stream of the participant, if available
long ssrc = header.getSsrc();
RTPReceiveStream stream =
(RTPReceiveStream) receiveStreams.get(new Long(ssrc));
/* To be tried:
if (stream == null)
stream = (RTPReceiveStream) ignoredStreams.get(new Long(ssrc));
*/
RTCPReport report = null;
RemoteEvent remoteEvent = null;
// If the packet is SR, read the sender info
if (header.getPacketType() == RTCPPacket.PT_SR) {
report = new RTCPSenderReport(data, offset, length);
((RTCPSenderReport) report).setStream(stream);
remoteEvent = new SenderReportEvent(this,
(RTCPSenderReport) report);
globalReceptionStats.addSRRecd();
}
// If the packet is RR, read the receiver info
if (header.getPacketType() == RTCPPacket.PT_RR) {
report = new RTCPReceiverReport(data, offset, length);
remoteEvent = new ReceiverReportEvent(this,
(RTCPReceiverReport) report);
}
// If the report is not null
if (report != null) {
/* If we're quitting the RTP session and this packet is a BYE,
* see RFC-3550 6.3.7 */
if (sayonara) {
if (report.isByePacket()) {
/* Dummy entries */
inactiveParticipants.put(new Long(System.currentTimeMillis()), "BYE");
computeAverageRTPSize(length);
}
return;
}
/* Calculate average RTP packet size if not BYE. */
if (!report.isByePacket())
computeAverageRTPSize(length);
String cname = report.getCName();
if (cname == null) {
cname = (String) senders.get(new Long(ssrc));
}
if (stream != null) {
stream.setReport(report);
}
// If the cname is in the report
if (cname != null) {
// Store the cname for later
unassignedStreams.remove(new Long(ssrc));
senders.put(new Long(ssrc), cname);
// Get the participant
RTPRemoteParticipant participant =
(RTPRemoteParticipant) activeParticipants.get(cname);
if (participant == null) {
participant = (RTPRemoteParticipant)
inactiveParticipants.get(cname);
}
// If there is no participant, create one
if (participant == null) {
participant = new RTPRemoteParticipant(cname);
getEventLock();
SessionEvent event =
new NewParticipantEvent(this, participant);
new SessionNotifier(sessionListeners, event);
inactiveParticipants.put(cname, participant);
}
// Set the participant of the report
report.setParticipant(participant);
participant.addReport(report);
// If this is a bye packet, program the removal of the stream
if (report.isByePacket()) {
new RemoveStreamTimer(this, participant, stream, cname,
report);
} else {
// If the stream is not null, map the stream
if (stream != null) {
if (!activeParticipants.containsKey(cname)) {
inactiveParticipants.remove(cname);
activeParticipants.put(cname, participant);
}
if (stream.getParticipant() == null) {
participant.addStream(stream);
stream.setParticipant(participant);
getEventLock();
ReceiveStreamEvent event =
new StreamMappedEvent(this, stream,
participant);
new ReceiveStreamNotifier(
receiveStreamListeners, event);
}
}
}
}
// Notify listeners of this packet
getEventLock();
new RemoteNotifier(remoteListeners, remoteEvent);
} else {
throw new IOException("Unknown report type: " +
header.getPacketType());
}
} catch (IOException e) {
globalReceptionStats.addBadRTCPPkt();
}
}
/* Conforming to RFC-3550 6.2.1 page 27 :
* When a BYE packet is received, the stream shouldn't be deleted directly,
* but after a delay (to avoid that remaining data packets recreate the
* stream). We chose this delay as 3 times the computed deterministic
* RTCP transmission interval for a sender.
*/
private class RemoveStreamTimer extends Thread {
/* Number of delays to sleep before erasing the stream/participant */
private final long DELAY_MULTIPLIER = 3;
RTPRemoteParticipant participant;
RTPReceiveStream stream;
String cname;
RTCPReport report;
javax.media.rtp.RTPManager rtpmgr;
public RemoveStreamTimer(javax.media.rtp.RTPManager rtpmgr,
RTPRemoteParticipant participant,
RTPReceiveStream stream,
String cname,
RTCPReport report) {
this.participant = participant;
this.stream = stream;
this.cname = cname;
this.report = report;
this.rtpmgr = rtpmgr;
//System.out.println("Programming the removal of "+participant);
setName("RemoveStreamTimer for " + RTPSessionMgr.this);
start();
}
@Override
public void run() {
long delay = calculateRTCPDelay(true, true);
try
{
Thread.sleep(DELAY_MULTIPLIER*delay);
} catch (InterruptedException ex)
{
return; // if we've been interrupted, then just exit.
}
if (stream != null) {
Long thisSSRC = new Long(stream.getSSRC());
senders.remove(thisSSRC);
participant.removeStream(stream);
receiveStreams.remove(thisSSRC);
}
getEventLock();
new ReceiveStreamNotifier(receiveStreamListeners,
new ByeEvent((SessionManager)rtpmgr, (Participant)participant, (ReceiveStream)stream,
report.getByeReason(),
participant.getStreams().size() == 0));
if (participant.getStreams().size() == 0) {
activeParticipants.remove(cname);
/* TODO: Was originally following in the code but should it be?
* Since we can't be sure if the partipant will still be a
* receiver or not, and considering that the RFC says that the
* number of participants should better be overestimated than
* underestimated, we add the participant to the receivers, and
* if it's really inactive, it will time out anyway?
*/
//inactiveParticipants.put(cname, participant);
/* If a participant has left, we must apply the reverse reconsideration
* algorithm.
*/
doReverseReconsideration();
}
//System.out.println(participant+" REMOVED");
}
}
// A notifier of send stream events
private class SendStreamNotifier extends Thread {
// The send stream listener
private Vector listeners = null;
// The event
private SendStreamEvent event = null;
private SendStreamNotifier(Vector listeners,
SendStreamEvent event) {
this.listeners = listeners;
this.event = event;
start();
}
@Override
public void run() {
//System.out.println("FMJ : Dispatching SendStreamEvent!!");
for (int i = 0; i < listeners.size(); i++) {
SendStreamListener listener =
(SendStreamListener) listeners.get(i);
listener.update(event);
}
releaseEventLock();
}
}
// A notifier of receive stream events
private class ReceiveStreamNotifier extends Thread {
// The receive stream listener
private Vector listeners = null;
// The event
private ReceiveStreamEvent event = null;
private ReceiveStreamNotifier(Vector listeners,
ReceiveStreamEvent event) {
this.listeners = listeners;
this.event = event;
start();
}
@Override
public void run() {
//System.out.println("FMJ : RTP STREAM!!");
for (int i = 0; i < listeners.size(); i++) {
ReceiveStreamListener listener =
(ReceiveStreamListener) listeners.get(i);
listener.update(event);
}
releaseEventLock();
}
}
// A notifier of receive session events
private class SessionNotifier extends Thread {
// The session listener
private Vector listeners = null;
// The event
private SessionEvent event = null;
private SessionNotifier(Vector listeners,
SessionEvent event) {
this.listeners = listeners;
this.event = event;
start();
}
@Override
public void run() {
//System.out.println("FMJ : RTP SESSION!!");
for (int i = 0; i < listeners.size(); i++) {
SessionListener listener =
(SessionListener) listeners.get(i);
listener.update(event);
}
releaseEventLock();
}
}
// A notifier of remote events
private class RemoteNotifier extends Thread {
// The remote listeners
private Vector listeners = null;
// The event
private RemoteEvent event = null;
private RemoteNotifier(Vector listeners,
RemoteEvent event) {
this.listeners = listeners;
this.event = event;
start();
}
@Override
public void run() {
//System.out.println("FMJ : RTP NOTIFIER!!");
for (int i = 0; i < listeners.size(); i++) {
RemoteListener listener =
(RemoteListener) listeners.get(i);
listener.update(event);
}
releaseEventLock();
}
}
/**
* Initializes and starts the session.
* Wrapper for the "initialize" methods, needed by JMF.
* @param localAddress the local address
* @param defaultSSRC the default SSRC. UNUSED.
* @param defaultUserDesc the default SDES
* @param rtcp_bw_fraction the bandwidth fraction allocated for RTCP traffic
* @param rtcp_sender_bw_fraction the RTCP bandwidth fraction allocated for senders
* @return 0
*/
public int initSession(SessionAddress localAddress, long defaultSSRC,
SourceDescription[] defaultUserDesc, double rtcp_bw_fraction,
double rtcp_sender_bw_fraction) {
initialize(new SessionAddress[]{localAddress}, defaultUserDesc,
rtcp_bw_fraction,
rtcp_sender_bw_fraction, null);
return 0;
}
/**
* Initializes and starts the session.
* Wrapper for the "initialize" methods, needed by JMF.
* @param localAddress the local address
* @param defaultUserDesc the default SDES
* @param rtcp_bw_fraction the bandwidth fraction allocated for RTCP traffic
* @param rtcp_sender_bw_fraction the RTCP bandwidth fraction allocated for senders
* @return 0
*/
public int initSession(SessionAddress localAddress,
SourceDescription[] defaultUserDesc, double rtcp_bw_fraction,
double rtcp_sender_bw_fraction) {
initialize(new SessionAddress[]{localAddress}, defaultUserDesc,
rtcp_bw_fraction,
rtcp_sender_bw_fraction, null);
return 0;
}
/**
* Starts a session.
* Wrapper for addTarget, needed by JMF.
* @param destAddress destination address to add as target
* @param mcastScope multicast scope. UNUSED.
* @param encryptionInfo encryption information. UNUSED.
* @return 0
* @throws java.io.IOException I/O Exception
*/
public int startSession(SessionAddress destAddress, int mcastScope,
EncryptionInfo encryptionInfo) throws IOException {
//System.out.println("FMJ : START!!");
addTarget(destAddress);
return 0;
}
/**
* Starts a session.
* Wrapper for addTarget, needed by JMF.
* @param localReceiverAddress local receiver address. UNUSED.
* @param localSenderAddress local sender address. UNUSED.
* @param remoteReceiverAddress remote receiver address to add as target
* @param encryptionInfo encryption information. UNUSED
* @return 0
* @throws java.io.IOException I/O Exception
*/
public int startSession(SessionAddress localReceiverAddress,
SessionAddress localSenderAddress,
SessionAddress remoteReceiverAddress,
EncryptionInfo encryptionInfo) throws IOException {
addTarget(remoteReceiverAddress);
return 0;
}
/**
* Returns the default SSRC in this RTP session.
* @return the current default SSRC for this RTP session
*/
public long getDefaultSSRC() {
/* We had to return SSRC_UNSPEC in the past if the session wasn't initialized,
* but this is deprecated. */
return ssrc;
}
/**
* Returns the RTPStream corresponding to this SSRC. DUMMY.
* Needed by JMF.
* @param filterssrc SSRC. UNUSED.
* @return null
*/
public RTPStream getStream(long filterssrc) {
return null;
}
/**
* Returns the multicast scope.
* @return 127
*/
public int getMulticastScope() {
return 127;
}
/**
* Sets the multicast scope. DUMMY.
* Needed by JMF.
* @param multicastScope multicast scope. UNUSED.
*/
public void setMulticastScope(int multicastScope) {
// Does Nothing
}
/**
* Closes this RTP Session.
* @param reason Reason why this session is closed (used in the RTCP BYE packet following this call)
*/
public void closeSession(String reason) {
sayonara=true;
/* If the sender RTCP bandwidth is inexistant, we leave without BYE */
if (rtcpReceiverBandwidthFraction == 0 && !we_sent) {
return;
}
/* We want to quit the whole session, so we must send a BYE packet.
* But we must conform to RFC-3550 6.3.7 if we have more than 50 participants
* to avoid flooding the RTP Session.
*/
if (getAllParticipants().size() < 50) {
removeTargets(reason);
return;
}
long now = System.currentTimeMillis();
RTCPSendTask.cancel();
RTCPSendTask=null;
reaperTimer.cancel();
reaperTimer=null;
we_sent=false;
lastRTCPSendTime = -1;
activeParticipants.clear();
inactiveParticipants.clear();
inactiveParticipants.put(localParticipant.getCNAME(), localParticipant);
pmembers = 0;
averageRTCPSize = LOWLAYERS + 4 + localParticipant.getStreams().size()*4;
long delay = calculateRTCPDelay(false, false);
lastRTCPSendTime = now;
/* We're waiting to send our final BYE */
while (((now = System.currentTimeMillis()) < (lastRTCPSendTime + delay))) {
nextRTCPSendTime = lastRTCPSendTime + delay;
try
{
Thread.sleep(delay);
} catch (InterruptedException ex)
{
ex.printStackTrace();
}
long temp = lastRTCPSendTime;
lastRTCPSendTime = -1;
delay = calculateRTCPDelay(false, false);
lastRTCPSendTime = temp;
}
removeTargets(reason);
}
/**
* Generate a CNAME. In our case, it just returns the CNAME generated
* for the local participant.
* @return the CNAME generated (ie, the local participant's one)
*/
public String generateCNAME() {
return localParticipant.getCNAME();
}
/**
* Generates a Synchronization SouRCe (SSRC) number.
* @return the ssrc generated
*/
public long generateSSRC() {
/* Chris: conforming to RFC 3550 - section 8.1 page 58.
* A simple random() call is not sufficient */
//return (long) (Math.random() * Integer.MAX_VALUE);
/* The cast to long is required by JMF... weird since the SSRC is
* 32 bits long and AFAIK there's no reason there could be any overflow */
return generator.generate();
}
/**
* Returns the session address. DUMMY.
* Needed by JMF.
* @return null
*/
public SessionAddress getSessionAddress() {
return null;
}
/**
* Returns the remote session address. DUMMY.
* Needed by JMF.
* @return null
*/
public SessionAddress getRemoteSessionAddress() {
return null; // TODO: added only for JMF Compatibility. Called, for example in AVReceiver example.
}
/**
* Returns the local session address.
* @return the local session address
*/
public SessionAddress getLocalSessionAddress() {
return localAddress;
}
/**
* Creates a send stream used to carry data from a source to some receivers over the network.
* Wrapper for createSendStream, needed by JMF.
* @param ssrc the stream's SSRC. UNUSED.
* @param ds the datasource producing the data to send
* @param streamindex the index of the stream in the datasource's streams table
* @return the created SendStream
* @throws javax.media.format.UnsupportedFormatException thrown if the datasource's stream only outputs formats which aren't supported
* @throws java.io.IOException I/O Exception thrown in case of problem with streams
*/
public SendStream createSendStream(int ssrc, DataSource ds,
int streamindex) throws UnsupportedFormatException,
IOException {
return createSendStream(ds, streamindex);
}
/**
* Starts a session. DUMMY.
* Needed by JMF.
* @param mcastScope multicast scope. UNUSED.
* @param encryptionInfo encryption information. UNUSED.
* @return -1
*/
public int startSession(int mcastScope, EncryptionInfo encryptionInfo) {
return -1;
}
/**
* Add a peer to send RTP data to.
* Wrapper for addTarger, needed by JMF.
* @param peerAddress the remote address used as target
* @throws java.io.IOException I/O Exception
*/
public void addPeer(SessionAddress peerAddress) throws IOException {
addTarget(peerAddress);
}
/**
* Removes a receiver peer from the session.
* Wrapper for removeTarget, needed by JMF.
* @param peerAddress the peer's address which is used as target
*/
public void removePeer(SessionAddress peerAddress) {
removeTarget(peerAddress, "Leaving");
}
/**
* Removes all the peers from this RTP session.
* Wrapper for removeTargets, needed by JMF.
*/
public void removeAllPeers() {
removeTargets("Leaving");
}
/**
* Returns all the receiver & senders peers we're sending RTP data to.
* Wrapper for getAllParticipants, needed by JMF.
* @return the list of all the peers/participants taking part in this RTP session
*/
public Vector getPeers() {
return getAllParticipants();
}
/**
* Starts the sending of RTCP packets
*/
public void start() {
// Send the first RTCP packet
long delay = (long) (generator.random() * 1000) + 500;
rtcpTimer.schedule(new RTCPTimerTask(this), delay);
globalReceptionStats.resetBytesRecd();
//lastRTCPSendTime = System.currentTimeMillis();
/* Our the Grim Reaper */
reaperTimer.schedule(new CheckStreamsTimeout(this), new Date(), REAPER_PERIOD);
}
/**
* Calculate the delay which must seperate two RTCP packets transmission
* in this RTP session (RTCP Transmission Interval).
*
* The control traffic (RTCP) is not self-limiting. If the reception
* reports from each participant were sent at a constant rate, the
* control traffic would grow linearly with the number of participants.
* Therefore, the rate must be scaled down by dynamically calculating
* the interval between RTCP packet transmissions.
*
* Refer to the RFC-3550 6.2 for more details.
* @return the computed RTCP Transmission Interval
*/
private long calculateRTCPDelay() {
return calculateRTCPDelay(false, we_sent /*localParticipant.getStreams().size() > 0*/);
}
/**
* Calculate the delay which must seperate two RTCP packets transmission
* in this RTP session (RTCP Transmission Interval).
*
* This version allows the programmer to calculate a customized delay, specifying
* if the RTCP Transmission Interval wanted must be deterministic (without the
* randomizing and compensation factors) and if it must be computer for a sender
* of receiver.
* @param deterministic specifies if the RTCP Transmission Interval wanted must be deterministic (without the
* randomizing and compensation factors). This is useful to calculate the deterministic
* interval of an other participant.
* @param is_sender true if we want to know the delay for a sender participant
* @return the RTCP Transmission Interval for this session considering the parameters passed
*/
private long calculateRTCPDelay(boolean deterministic, boolean is_sender) {
long delay = MIN_RTCP_INTERVAL;
long rtcp_min_time = MIN_RTCP_INTERVAL;
/* Bandwidth in bytes/ms !!*/
double bandwidth = ((double) globalReceptionStats.getBytesRecd() /
(System.currentTimeMillis() - lastRTCPSendTime));
/* To make things a bit more readable */
int nbSenders = activeParticipants.size();
int nbReceivers = inactiveParticipants.size();
long nbMembers = nbSenders + nbReceivers;
/*TESTING*/
/*System.out.println("Bytes "+globalReceptionStats.getBytesRecd()
+" / Time "+(System.currentTimeMillis() - lastRTCPSendTime)+" => "+bandwidth
+"\n Last RTCP "+lastRTCPSendTime);
*/
/* To be sure to avoid DIV/0, we set bandwidth to 1 if it's equal to 0 */
//if (bandwidth <= 0.1) bandwidth = 1;
/* On startup, when no RTCP packet has been sent, we can use half of the
* minimum delay to get a quicker notification.
*/
if (!deterministic && lastRTCPSendTime < 0)
rtcp_min_time /= 2;
//TODO: Putting the number of bytes reveived to 0 when sending an RTCP packet (in sendRTCPPacket)
// looks strange. We'd better use a bandwidth calculation based on the data received in
// the whole session and using a formula like the one used for the averageRTCPSize to get
// rapid reactions to bandwidth changes.
/* 20 packets sounds like a good limit after which we can start using
* the dynamic RTCP minimum delay computation */
if (!deterministic && globalReceptionStats.getBytesRecd() > 20*averageRTCPSize && bandwidth > 1) {
/* We first calculate the precise bandwidth in kbits/s
* and then we can compute the minimum RTCP delay (in ms).
* (conforming to RFC-3550 end of page 25) */
double bw = bandwidth * 8 * 1000 / 1024;
rtcp_min_time = (long)((360 / bw) * 1000);
//System.out.println("RTCP Dynamic delay : "+rtcp_min_time+"ms ["+bw+"]");
}
if (bandwidth < 0.1) {
delay = rtcp_min_time;
} else {
double senderFraction = 0;
/* Calculate the ratio senders/members.
* activeParticipants = senders,
* inactivePartipants = receivers
*/
if (nbMembers > 0) {
senderFraction = nbSenders / nbMembers;
}
/* Allocate sufficient bandwidth to senders :
* . if the senders represent less than a specified fraction of
* the members (default is 25%), reserve the specified fraction
* for them;
* . else simply let them share the bandwidth proportionaly to
* their number
*/
double ratio = rtcpSenderBandwidthFraction /
(rtcpSenderBandwidthFraction + rtcpReceiverBandwidthFraction);
if ((nbSenders > 0) && (senderFraction < ratio)) {
if (is_sender) {
delay = (long) ((averageRTCPSize *nbSenders) /
(bandwidth * rtcpSenderBandwidthFraction));
} else {
/* Special test : if rtcpReceiverBandwidth == 0, we have ratio = 1
* => senderFraction < ratio, and if is_sender == false,
* we end here with a division by zero.
* To avoid this, we set the delay to rtcp_min_time.
*/
if (rtcpReceiverBandwidthFraction == 0) delay = rtcp_min_time;
else delay = (long) ((averageRTCPSize * nbReceivers) /
(bandwidth * rtcpReceiverBandwidthFraction));
}
} else {
delay = (long) ((averageRTCPSize * nbMembers) /
(bandwidth * (rtcpSenderBandwidthFraction +
rtcpReceiverBandwidthFraction)));
}
if (delay < rtcp_min_time) {
delay = rtcp_min_time;
}
}
/* If we're calculating the deterministic interval Td, we return here */
if (deterministic) return delay;
/* Conforming to RFC-3550 : the delay must be made uniformly random
* on the entire 0.5*T < delay < 1.5*T interval to avoid traffic bursts
* from unintended synchronization with other sites.
* We use SSRC.nextFastTruncatedDouble() with as parameter the space on
* which we want the generation to be done. Since we need a generation
* on 48 bits, we pass 6 bytes as length.
*/
delay *= (generator.random() + 0.5);
/* We compensate the fact that the timer reconsideration algorithm
* converges to a value of the RTCP bandwidth below the intended average.
* Therefore, we divide delay by e-3/2 as specified in the RFC-3550.
*/
delay /= 1.21828;
//System.out.println("Generated delay : " + delay);
pmembers = nbMembers;
return delay;
}
private void writeSenderInfo(DataOutputStream output, RTPSendStream sendStream) throws IOException
{
/* Chris: conforming to the RFC, we must send the NTP time at which
* this report is sent, not the time at which the last packets were
* sent. RFC 3550 Page 36
*/
// this is the wallclock time, TODO: increase resolution, we can use System.nanoseconds, but we must sync
// both clocks, nanoseconds is only good for relative timestamp !
// currentTimeMillis() has a bad resolution under Win98 ~50ms WinXP has a resolution of ~15ms, linux ~1ms
long sendtime = System.currentTimeMillis();
// NTP starts at 1.Jan.1900 ( but sendtime starts at 1.Jan.1970, adjust sendtime)
sendtime = sendtime - RTCPSenderInfo.MSB_1_BASE_TIME;
// NTP timestamp MSW
long sendTimeSeconds = sendtime / 1000;
// mgodehardt: NTP timestamp LWS (a 1 in the fractional part is (1 / 2**32) second, ~233 picoseconds)
double fractionalPart = (double)(sendtime % 1000);
fractionalPart = fractionalPart / 1000.0d;
fractionalPart = fractionalPart * 4294967296.0d;
long sendTimeFractional = (long)fractionalPart;
// RFC 3550 Page 37 (Corresponds to the same time as the NTP timestamp, but in
// the same units and with the same random offset as the RTP timestamps in data packets)
// is calculated base on the ntp timestamp using the relationship between rtp timestamp and real time
double timestamp = (double)(sendStream.getLastSendTime() - sendStream.getInitialSendTime()) * ((double)sendStream.getClockRate() / 1000.0d);
long rtpTimestamp = sendStream.getInitialTimestamp() + Math.round(timestamp);
TransmissionStats stats = sendStream.getSourceTransmissionStats();
output.writeInt((int) (sendStream.getSSRC() & 0xFFFFFFFF));
output.writeInt((int) (sendTimeSeconds & 0xFFFFFFFF));
output.writeInt((int) (sendTimeFractional & 0xFFFFFFFF));
output.writeInt((int) (rtpTimestamp & 0xFFFFFFFF));
output.writeInt(stats.getPDUTransmitted());
output.writeInt(stats.getBytesTransmitted());
}
private void writeSDESPacket(DataOutputStream output) throws IOException
{
Vector sdesItems = localParticipant.getSourceDescription();
if ( sdesItems.size() > 0 )
{
writeSDESPacketImpl(output, ssrc, sdesItems, ((rtcpPacketsSent % 3) == 0) ? false : true);
}
}
private void writeSDESPacketImpl(DataOutputStream output, long ssrc, Vector sdesItems, boolean onlyCNAME) throws IOException
{
// see RFC 3550 (6.5 SDES: Source Description RTCP Packet) for details
// only every third packet all SDES items are sent
// CNAME is mandatory and must be included in every RTCP report send ( ssrc may change )
// initial length (SSRC and the terminating SDES item, a zero octet)
int sdesLength = 5;
for (int i=0; i<sdesItems.size(); i++)
{
SourceDescription sdesItem = (SourceDescription)sdesItems.elementAt(i);
if ( !onlyCNAME || (sdesItem.getType() == SourceDescription.SOURCE_DESC_CNAME) )
{
sdesLength += sdesItem.getDescription().getBytes("UTF-8").length + 2;
}
}
// do we need padding to next 32-bit boundary
int padding = 0;
if ( (sdesLength % 4) != 0 )
{
padding = 4 - (sdesLength % 4);
sdesLength += padding;
}
// write the rtcp sdes packet
output.writeByte(0x81);
output.writeByte(RTCPPacket.PT_SDES & 0xFF);
output.writeShort(sdesLength / 4);
// only one chunk
output.writeInt((int)(ssrc & 0xFFFFFFFF));
// write sdes item(s) for ssrc
for (int i=0; i<sdesItems.size(); i++)
{
SourceDescription sdesItem = (SourceDescription)sdesItems.elementAt(i);
if ( !onlyCNAME || (sdesItem.getType() == SourceDescription.SOURCE_DESC_CNAME) )
{
output.writeByte(sdesItem.getType() & 0xFF);
byte[] desc = sdesItem.getDescription().getBytes("UTF-8");
output.writeByte(desc.length & 0xFF);
output.write(desc);
}
}
// terminating zero octet, marks end of sdes items
output.writeByte(0);
for (int i=0; i<padding; i++)
{
output.writeByte(0);
}
}
/**
* Sends an RTCP packet, and schedules the next one
*/
public void sendRTCPPacket()
{
// mgodehardt, TODO: multi threading issues, this is a separate thread, should be synced with RTP and RTCP receive threads
// otherwise some statistics may be wrong ( lost packets, jitter calculations )
/* If we're about to leave the session, return to avoid messing up with the
* BYE sending procedure. */
if (sayonara) return;
long delay = calculateRTCPDelay();
long now = System.currentTimeMillis();
/* Wait until later if :
* . it's too early
* . OR if we're a receiver (!we_sent)
* and no bandwidth was allocated for receivers
*/
if ((now < (lastRTCPSendTime + delay))
| (rtcpReceiverBandwidthFraction == 0 && !we_sent)) {
nextRTCPSendTime = lastRTCPSendTime + delay;
RTCPSendTask = new RTCPTimerTask(this);
rtcpTimer.schedule(RTCPSendTask, new Date(nextRTCPSendTime));
} else {
/* If we have more than 31 report blocks to send, we use a round-robin
* to send each part in a different RTCP interval. */
Vector streams = new Vector(receiveStreams.values());
int rc = streams.size();
if (rc > 31) {
if (RRRoundRobin >= rc) RRRoundRobin = 0;
rc = (rc - RRRoundRobin > 31)? 31: rc - RRRoundRobin;
} else RRRoundRobin = 0;
// Reset the stats
globalReceptionStats.resetBytesRecd();
// Get the packet details
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
DataOutputStream output = new DataOutputStream(bytes);
try {
// Determine the packet type
int packetType = RTCPPacket.PT_RR;
int packetSize = (rc * RTCPFeedback.SIZE) + 8;
if (we_sent)
{
packetType = RTCPPacket.PT_SR;
packetSize += RTCPSenderInfo.SIZE;
}
// Add a RTCP header
output.writeByte(0x80 | 0 | (rc & 0x1F));
output.writeByte(packetType & 0xFF);
output.writeShort(((packetSize) / 4) - 1);
// If we are a sender, add sender stats
if (we_sent)
{
/* As specified in the RFC, we randomly pick a stream and send
* information about it */
// in which RFC ???
//int senderIndex = (int) (Math.random() * localParticipant.getStreams().size());
//RTPSendStream sendStream = (RTPSendStream) localParticipant.getStreams().get(senderIndex);
// mogdehardt: for unicast only one media per RTP session is used, so we have one stream
// TODO: check multicast and broadcast
RTPSendStream sendStream = (RTPSendStream)localParticipant.getStreams().get(0);
writeSenderInfo(output, sendStream);
}
else
{
/* Add the generic SSRC if we're not a sender */
output.writeInt((int) (ssrc & 0xFFFFFFFF));
}
// Add a reception report block ( RFC 3550 Page 38, TODO: do not send if all packets were lost )
if ( streams.size() > 0 )
{
now = System.currentTimeMillis();
for (int i = RRRoundRobin; i < RRRoundRobin + rc; i++)
{
RTPReceiveStream stream = (RTPReceiveStream) streams.get(i);
RTPReceptionStats stats = (RTPReceptionStats) stream.getSourceReceptionStats();
RTPDataSource dataSource = (RTPDataSource) stream.getDataSource();
RTPDataStream dataStream = (RTPDataStream) dataSource.getStreams()[0];
// lossFraction since the previous SR/RR (RFC 3550 Page 38), TODO: do not send a RR when all packets are lost (RFC 3550
long lossFraction = 0;
// RFC 3550 Page 38 (extended highest sequence number received)
long extendedHighestSequenceNumber = dataStream.itsRTPBuffer.getExtendedHighestSequenceNumber();
// TODO: mgodehardt calc loss of fraction
/*if ( dataStream.lastPacketsReceived > 0 )
{
// RFC 3550 Page 82
long expected_interval = extendedHighestSequenceNumber - dataStream.lastExtendedHighestSequenceNumber;
long received_interval = dataStream.getPacketsReceived() - dataStream.lastPacketsReceived;
long lost_interval = expected_interval - received_interval;
if ( (expected_interval == 0) || (lost_interval <= 0) )
{
lossFraction = 0;
}
else
{
double lf = (double)(lost_interval << 8) / (double)expected_interval;
lossFraction = Math.round(lf);
}
}*/
int cumulativePacketLoss = dataStream.itsRTPBuffer.getCumulativePacketLoss();
long jitter = dataSource.getJitter();
// RFC 3550 Page 11 the middle 32 Bits of the 64 Bit NTP Timestamp of the last sender report
long lsrMSW = stream.getLastSRReportTimestampMSW();
long lsrLSW = stream.getLastSRReportTimestampLSW();
// RFC 3550 Page 39 ( for round trip calculations )
long DLSR = 0;
if ( stream.getLastSRReportTime() > 0 )
{
DLSR = ((now - stream.getLastSRReportTime()) * 65536) / 1000;
}
output.writeInt((int) (stream.getSSRC() & 0xFFFFFFFF));
output.writeByte((int)(lossFraction & 0xFF));
output.writeByte((cumulativePacketLoss >> 16) & 0xFF);
output.writeShort((cumulativePacketLoss & 0xFFFF));
output.writeInt((int) (extendedHighestSequenceNumber & 0xFFFFFFFF));
output.writeInt((int) (jitter & 0xFFFFFFFF));
output.writeShort((int) (lsrMSW & 0xFFFF));
output.writeShort((int) ((lsrLSW >> 16) & 0xFFFF));
output.writeInt((int) (DLSR & 0xFFFFFFFF));
}
}
writeSDESPacket(output);
}
catch (IOException e)
{
logger.log(Level.WARNING, "" + e, e);
}
byte[] data = bytes.toByteArray();
try
{
OutputDataStream outputStream = socket.getControlOutputStream();
output.close();
bytes.close();
data = bytes.toByteArray();
outputStream.write(data, 0, data.length);
rtcpPacketsSent++;
}
catch (IOException e)
{
logger.log(Level.WARNING, "" + e, e);
dispose();
}
// RTP average Size computation
if (data != null) computeAverageRTPSize(data.length);
// Prepare to send the next packet
if (!done) {
delay = calculateRTCPDelay();
lastRTCPSendTime = System.currentTimeMillis();
nextRTCPSendTime = lastRTCPSendTime + delay;
rtcpTimer.schedule(new RTCPTimerTask(this), new Date(nextRTCPSendTime));
if (rc > 31) {
RRRoundRobin += 31;
if (RRRoundRobin > rc) RRRoundRobin = 0;
}
}
}
}
/**
* A timer task for sending RTCP packets
*/
class RTCPTimerTask extends TimerTask {
private RTPSessionMgr rtpSessionManager = null;
private RTCPTimerTask(RTPSessionMgr rtpSessionManager) {
this.rtpSessionManager = rtpSessionManager;
}
@Override
public void run() {
if (!sayonara) {
rtpSessionManager.sendRTCPPacket();
return;
}
}
}
/**
* A private class to periodically check the streams to remove those
* which have timed-out.
*/
private class CheckStreamsTimeout extends TimerTask {
/* The timeout multipliers (cf. RFC3550 6.3.5) */
private final int TIMEOUT_MULT = 5;
private final int SENDER_TIMEOUT_MULT = 2;
javax.media.rtp.RTPManager rtpmgr;
public CheckStreamsTimeout(javax.media.rtp.RTPManager rtpmgr) {
this.rtpmgr = rtpmgr;
//System.out.println("RTP: Reaping streams - Checking for timeouts...");
}
@Override
public void run() {
// TODO : Access to the Hashmaps should be synchronized !!
boolean modified = false;
logger.finer("Reaping...");
/* FIRST PASS : Check the receivers
* --------------------------------
*/
/* Compute the deterministic interval Td for receivers */
long delay = calculateRTCPDelay(true, false);
long timeLimit = System.currentTimeMillis() - TIMEOUT_MULT*delay;
Set cnames = inactiveParticipants.keySet();
Iterator it = cnames.iterator();
while (it.hasNext()) {
String cname = (String)it.next();
RTPParticipant pal = (RTPParticipant)inactiveParticipants.get(cname);
if (!pal.equals(localParticipant)) {
/*Vector streamsV = pal.getStreams();
for (int i = 0; i < streamsV.size(); i++) {
RTPReceiveStream stream = (RTPReceiveStream)streamsV.get(i);
stream.getLastSRReportTime();
}*/
long lastSR = pal.getLastReportTime();
if (lastSR < timeLimit) {
/* This participant timed out! */
// TODO: Stop our send streams ?
// Remove the participant
inactiveParticipants.remove(pal.getCNAME());
//System.out.println(pal+" TIMED OUT");
modified = true;
}
}
}
/* SECOND PASS : Check the senders
* -------------------------------
*/
/* Compute the deterministic interval T for senders */
delay = calculateRTCPDelay(true, true);
timeLimit = System.currentTimeMillis() - SENDER_TIMEOUT_MULT*delay;
cnames = activeParticipants.keySet();
it = cnames.iterator();
while (it.hasNext()) {
String cname = (String)it.next();
RTPParticipant pal = (RTPParticipant)activeParticipants.get(cname);
long lastSR = pal.getLastReportTime();
if (lastSR < timeLimit) {
/* This participant timed out! */
/* If it's the localParticipant, then it's no real timeout,
* just part of the algorithm to update we_sent described in
* RFC-3550 6.3.4.
*/
if (pal.equals(localParticipant)) {
we_sent = false;
inactiveParticipants.put(localParticipant.getCNAME(), localParticipant);
} else {
// Remove its streams
Vector streamsV = pal.getStreams();
for (int i = 0; i < streamsV.size(); i++) {
RTPStream stream = (RTPStream)streamsV.get(i);
receiveStreams.remove(new Long(stream.getSSRC()));
senders.remove(new Long(stream.getSSRC()));
}
modified = true;
}
// Remove the participant
activeParticipants.remove(pal.getCNAME());
//System.out.println(pal+" TIMED OUT [sender]");
}
/* Check only streams timeout to send InactiveReceiveStreamEvent */
else if (!pal.equals(localParticipant)) {
Vector streamsV = pal.getStreams();
for (int i = 0; i < streamsV.size(); i++) {
RTPReceiveStream stream = (RTPReceiveStream)streamsV.get(i);
if (stream.checkInactivity())
{
// mgodehardt: resetting bitratecontrol
DataSource ds = stream.getDataSource();
if ( ds instanceof PushBufferDataSource )
{
PushBufferDataSource pbds = (PushBufferDataSource)ds;
PushBufferStream pbs = pbds.getStreams()[0];
if ( pbs instanceof RTPDataStream )
{
((RTPDataStream)pbs).bitsPerSecond = 0;
}
}
/* Trigger InactiveReceiveStreamEvent */
getEventLock();
new ReceiveStreamNotifier(receiveStreamListeners,
new InactiveReceiveStreamEvent((SessionManager)rtpmgr, pal, stream, (streamsV.size() == 1)));
}
}
}
}
/* THIRD PASS : Check the unmapped streams
* ---------------------------------------
*/
Set unassigned = unassignedStreams.keySet();
it = unassigned.iterator();
while (it.hasNext()) {
RTPReceiveStream stream = (RTPReceiveStream)unassignedStreams.get(it.next());
if (stream.checkInactivity()) {
/* Trigger InactiveReceiveStreamEvent */
getEventLock();
new ReceiveStreamNotifier(receiveStreamListeners,
new InactiveReceiveStreamEvent((SessionManager)rtpmgr, new RTPParticipant("Unknown"), stream, true));
}
}
if (modified) {
/* Reverse reconsideration algorithm */
doReverseReconsideration();
}
/* TODO: maybe add a BYE event when participants time out ? */
/*getEventLock();
new ReceiveStreamNotifier(receiveStreamListeners,
new ByeEvent((SessionManager)rtpmgr, (Participant)participant, (ReceiveStream)stream,
report.getByeReason(),
participant.getStreams().size() == 0));
*/
/* TODO: Find a way to check dead streams not assigned to any participant */
}
}
/**
* Sort of callback method called by RTPSendStreams to inform the RTPSessionManager they're linked to
* that an RTP packet has been sent by the local participant. This is used to maintain some local
* variables and statistics used in several algorithms (reaper timer and RTCP delay calculation for example).
* @param time time when the packet was sent
* @param size size of the packet sent
*/
public synchronized void RTPPacketSent(long time, int size) {
/* If we're about to leave the session, return to avoid messing up with the
* BYE sending procedure. */
if (sayonara) return;
/* We update the lastReportTime of the localParticipant */
localParticipant.touch(time);
/* We set we_sent to true and move the localParticipant to the active member list */
we_sent = true;
activeParticipants.put(localParticipant.getCNAME(), localParticipant);
inactiveParticipants.remove(localParticipant.getCNAME());
/* We use this to compute the average RTP/RTCP size */
computeAverageRTPSize(size);
}
/**
* Apply the reverse reconsideration.
*
* This algorithm is meant to reduce the incidence and duration of false
* participant timeouts when the number of participants drops rapidly.
* Reverse reconsideration is also used to possibly shorten the delay
* before sending RTCP SR when transitioning from passive receiver to
* active sender mode.
*
* Refer to the RFC-3550 Sections 6.3.3, 6.3.6 and 6.3.7 for more details.
*/
synchronized private void doReverseReconsideration() {
if (RTCPSendTask != null && !sayonara) {
/* Cancel the programmed task */
RTCPSendTask.cancel();
/* Apply the reverse reconsideration algorithm */
long now = System.currentTimeMillis();
long nbMembers = inactiveParticipants.size()+activeParticipants.size();
nextRTCPSendTime = now + (nbMembers/pmembers) * (nextRTCPSendTime - now);
lastRTCPSendTime = now - (nbMembers/pmembers) * (now - lastRTCPSendTime);
pmembers = nbMembers;
/* Program the new task */
RTCPSendTask = new RTCPTimerTask(this);
rtcpTimer.schedule(RTCPSendTask, new Date(nextRTCPSendTime));
}
}
/**
* Returns the average RTP packet size computed until now.
* @return the average RTP packet size
*/
public int getAverageRTCPSize()
{
return averageRTCPSize;
}
/**
* Calculate the average RTP/RTCP packet size if the report
* is not a BYE. Conforming to the RFC-3550 6.3.3.
* @param length the length of the packet that just arrived
*/
synchronized void computeAverageRTPSize(int length) {
averageRTCPSize = (length + LOWLAYERS) * (1/16) + averageRTCPSize * (15/16);
}
}