aboutsummaryrefslogtreecommitdiffstats
path: root/src/net/java
diff options
context:
space:
mode:
authorEmil Ivov <emcho@jitsi.org>2006-10-23 16:19:34 +0000
committerEmil Ivov <emcho@jitsi.org>2006-10-23 16:19:34 +0000
commit7d6b1257f601f5382514b5c7e68ec42871d8ee30 (patch)
tree915ac6b31fa33cb3eab43f6491ac6e8493fa6fee /src/net/java
parentec735565708ef1ea397b803dda0c56cf20ccb682 (diff)
downloadjitsi-7d6b1257f601f5382514b5c7e68ec42871d8ee30.zip
jitsi-7d6b1257f601f5382514b5c7e68ec42871d8ee30.tar.gz
jitsi-7d6b1257f601f5382514b5c7e68ec42871d8ee30.tar.bz2
Working on the JMF implementation of the media service.
implementation of the call session interface. handles initialization and control of RTPManager-s
Diffstat (limited to 'src/net/java')
-rw-r--r--src/net/java/sip/communicator/impl/media/CallSessionImpl.java1332
1 files changed, 1332 insertions, 0 deletions
diff --git a/src/net/java/sip/communicator/impl/media/CallSessionImpl.java b/src/net/java/sip/communicator/impl/media/CallSessionImpl.java
new file mode 100644
index 0000000..afb31a7
--- /dev/null
+++ b/src/net/java/sip/communicator/impl/media/CallSessionImpl.java
@@ -0,0 +1,1332 @@
+/*
+ * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
+ *
+ * Distributable under LGPL license.
+ * See terms of license at gnu.org.
+ */
+package net.java.sip.communicator.impl.media;
+
+import java.io.*;
+import java.net.*;
+import java.text.*;
+import java.util.*;
+import javax.media.*;
+import javax.media.format.*;
+import javax.media.protocol.*;
+import javax.media.rtp.*;
+import javax.media.rtp.event.*;
+import javax.sdp.*;
+
+import net.java.sip.communicator.service.media.*;
+import net.java.sip.communicator.service.media.MediaException;
+import net.java.sip.communicator.service.netaddr.*;
+import net.java.sip.communicator.service.protocol.*;
+import net.java.sip.communicator.service.protocol.event.*;
+import net.java.sip.communicator.util.*;
+import javax.media.control.*;
+
+/**
+ * Contains parameters associated with a particular Call such as media (audio
+ * video), a reference to the call itself, RTPManagers and others.
+ * <p>
+ * Currently the class works the following way:<p>
+ * We create 2 rtp managers (one for video and one for audio) upon
+ * initialization of this call session and initialize/bind them on local
+ * addresses.
+ * <p>
+ * When we are asked to create an SDP offer we ask the <tt>MediaControl</tt>
+ * for the Formats/Encodings that we support and create a media description that
+ * would advertise these formats as well as the ports that our RTP managers are
+ * bound upon.
+ * <p>
+ * When we need to process an incoming offer we ask the <tt>MediaControl</tt>
+ * for the Formats/Encodings that we support, intersect them with those that
+ * were sent by the offerer and make <tt>MediaControl</tt> configure our source
+ * processor so that it would transmit in the format that it is expected to
+ * according to the format set that resulted from the intersection. We also
+ * prepare our <tt>RTPManager</tt>-s to send streams for every media type
+ * requested in the offer. (Note that these streams are not started until
+ * the associated call enters the CONNECTED state).
+ * <p>
+ * Processing an SDP answer is quite similar to processing an offer with the
+ * exception that the intersection of all supported formats has been performed
+ * bye the remote party and we only need to configure our processor and
+ * <tt>RTPManager</tt>s.
+ *
+ * @todo implement SendStreamListener.
+ * @todo implement ReceiveStreamListener.
+ *
+ * @author Emil Ivov
+ */
+public class CallSessionImpl
+ implements CallSession
+ , CallParticipantListener
+ , CallChangeListener
+ , ReceiveStreamListener
+ , SendStreamListener
+ , SessionListener
+ , ControllerListener
+
+{
+ private static final Logger logger
+ = Logger.getLogger(CallSessionImpl.class);
+
+ /**
+ * The call associated with this session.
+ */
+ private Call call = null;
+
+ /**
+ * The session address that is used for audio communication in this call.
+ */
+ private SessionAddress audioSessionAddress = null;
+
+ /**
+ * The session address that is used for video communication in this call.
+ */
+ private SessionAddress videoSessionAddress = null;
+
+ /**
+ * The rtpManager that handles audio streams in this session.
+ */
+ private RTPManager audioRtpManager = RTPManager.newInstance();
+
+ /**
+ * The rtpManager that handles video streams in this session.
+ */
+ private RTPManager videoRtpManager = RTPManager.newInstance();
+
+ /**
+ * The media service instance that created us.
+ */
+ private MediaServiceImpl mediaServCallback = null;
+
+ /**
+ * The minimum port number that we'd like our rtp managers to bind upon.
+ */
+ private int minPortNumber = 5000;
+
+ /**
+ * The maximum port number that we'd like our rtp managers to bind upon.
+ */
+ private int maxPortNumber = 6000;
+
+ /**
+ * Creates a new session for the specified <tt>call</tt>.
+ *
+ * @param call The call associated with this session.
+ * @param mediaServCallback the media service instance that created us.
+ */
+ public CallSessionImpl(Call call, MediaServiceImpl mediaServCallback)
+ {
+ this.call = call;
+ this.mediaServCallback = mediaServCallback;
+ }
+
+ /**
+ * Returns the call associated with this Session.
+ *
+ * @return the Call associated with this session.
+ */
+ public Call getCall()
+ {
+ return call;
+ }
+
+ /**
+ * Returns the port that we are using for receiving video data in this
+ * <tt>CallSession</tt>.
+ * <p>
+ * @return the port number we are using for receiving video data in this
+ * <tt>CallSession</tt>.
+ */
+ public int getVideoPort()
+ {
+ return videoSessionAddress.getDataPort();
+ }
+
+ /**
+ * Returns the port that we are using for receiving audio data in this
+ * <tt>CallSession</tt>.
+ * <p>
+ * @return the port number we are using for receiving audio data in this
+ * <tt>CallSession</tt>.
+ */
+ public int getAudioPort()
+ {
+ return audioSessionAddress.getDataPort();
+ }
+
+ /**
+ * Returns the rtp manager that we are using for audio streams.
+ * @return the RTPManager instance that we are using for audio streams.
+ */
+ public RTPManager getAudioRtpManager()
+ {
+ return this.audioRtpManager;
+ }
+
+ /**
+ * Returns the rtp manager that we are using for video streams.
+ * @return the RTPManager instance that we are using for audio streams.
+ */
+ public RTPManager getVideoRtpManager()
+ {
+ return this.videoRtpManager;
+ }
+
+ /**
+ * Opens all streams that have been initialized for local RTP managers.
+ *
+ * @throws MediaException if start() fails for all send streams.
+ */
+ private void startStreaming()
+ throws MediaException
+ {
+ //start all audio streams
+ boolean startedAtLeastOneStream = false;
+ RTPManager rtpManager = getAudioRtpManager();
+
+ Vector sendStreams = rtpManager.getSendStreams();
+ if(sendStreams != null && sendStreams.size() > 0)
+ {
+ logger.trace("Will be starting " + sendStreams.size()
+ + " audio send streams.");
+ Iterator ssIter = sendStreams.iterator();
+
+ while (ssIter.hasNext())
+ {
+ SendStream stream = (SendStream) ssIter.next();
+ try
+ {
+ stream.start();
+ startedAtLeastOneStream = true;
+ }
+ catch (IOException ex)
+ {
+ logger.warn("Failed to start stream.", ex);
+ }
+ }
+ }
+ else
+ {
+ logger.trace("No audio send streams will be started.");
+ }
+
+ //start video streams
+ rtpManager = getVideoRtpManager();
+ sendStreams = rtpManager.getSendStreams();
+ if(sendStreams != null && sendStreams.size() > 0)
+ {
+ logger.trace("Will be starting " + sendStreams.size()
+ + " video send streams.");
+ Iterator ssIter = sendStreams.iterator();
+
+ while (ssIter.hasNext())
+ {
+ SendStream stream = (SendStream) ssIter.next();
+ try
+ {
+ stream.start();
+ startedAtLeastOneStream = true;
+ }
+ catch (IOException ex)
+ {
+ logger.warn("Failed to start stream.", ex);
+ }
+ }
+ }
+ else
+ {
+ logger.trace("No video send streams will be started.");
+ }
+
+
+ if(!startedAtLeastOneStream && sendStreams.size() > 0)
+ throw new MediaException("Failed to start streaming"
+ , MediaException.INTERNAL_ERROR);
+ }
+
+ /**
+ * Stops and closes all streams that have been initialized for local
+ * RTP managers.
+ */
+ private void stopStreaming()
+ {
+ //stop all audio streams
+ RTPManager rtpManager = getAudioRtpManager();
+
+ Vector sendStreams = rtpManager.getSendStreams();
+ Iterator ssIter = sendStreams.iterator();
+
+ while(ssIter.hasNext())
+ {
+ SendStream stream = (SendStream) ssIter.next();
+ try
+ {
+ stream.stop();
+ stream.close();
+ }
+ catch (IOException ex)
+ {
+ logger.warn("Failed to stop stream.", ex);
+ }
+ }
+
+ //stop all video streams
+ rtpManager = getAudioRtpManager();
+
+ sendStreams = rtpManager.getSendStreams();
+ ssIter = sendStreams.iterator();
+
+ while(ssIter.hasNext())
+ {
+ SendStream stream = (SendStream) ssIter.next();
+ try
+ {
+ stream.stop();
+ stream.close();
+ }
+ catch (IOException ex)
+ {
+ logger.warn("Failed to stop stream.", ex);
+ }
+ }
+
+
+ }
+
+
+ /**
+ * The method is meant for use by protocol service implementations when
+ * willing to send an invitation to a remote callee. The
+ * resources (address and port) allocated for the <tt>callParticipant</tt>
+ * should be kept by the media service implementation until the originating
+ * <tt>callParticipant</tt> enters the DISCONNECTED state. Subsequent sdp
+ * offers/answers requested for the <tt>Call</tt> that the original
+ * <tt>callParticipant</tt> belonged to MUST receive the same IP/port couple
+ * as the first one in order to allow for conferencing. The associated port
+ * will be released once the call has ended.
+ *
+ * @todo implement ice.
+ *
+ * @return a new SDP description String advertising all params of
+ * <tt>callSession</tt>.
+ *
+ * @throws MediaException code SERVICE_NOT_STARTED if this method is called
+ * before the service was started.
+ */
+ public String createSdpOffer()
+ throws net.java.sip.communicator.service.media.MediaException
+ {
+ return createSessionDescription(null).toString();
+ }
+
+ /**
+ * The method is meant for use by protocol service implementations upon
+ * reception of an SDP answer in response to an offer sent by us earlier.
+ *
+ * @param sdpAnswerStr the SDP answer that we'd like to handle.
+ * @param responder the participant that has sent the answer.
+ *
+ * @throws MediaException code SERVICE_NOT_STARTED if this method is called
+ * before the service was started.
+ * @throws ParseException if sdpAnswerStr does not contain a valid sdp
+ * String.
+ */
+ public void processSdpAnswer(CallParticipant responder, String sdpAnswerStr)
+ throws MediaException, ParseException
+ {
+ logger.trace("Parsing sdp answer: " + sdpAnswerStr);
+ //first parse the answer
+ SessionDescription sdpAnswer = null;
+ try
+ {
+ sdpAnswer = mediaServCallback.getSdpFactory()
+ .createSessionDescription(sdpAnswerStr);
+ }
+ catch (SdpParseException ex)
+ {
+ throw new ParseException("Failed to parse SDPOffer: "
+ + ex.getMessage()
+ , ex.getCharOffset());
+ }
+
+ //extract media descriptions
+ Vector mediaDescriptions = null;
+ try
+ {
+ mediaDescriptions = sdpAnswer.getMediaDescriptions(true);
+ }
+ catch (SdpException exc)
+ {
+ logger.error("failed to extract media descriptions", exc);
+ throw new MediaException("failed to extract media descriptions"
+ , MediaException.INTERNAL_ERROR
+ , exc);
+ }
+
+
+ //add the RTP targets
+ this.initStreamTargets(sdpAnswer.getConnection(), mediaDescriptions);
+
+ //create and init the streams (don't start streaming just yet but wait
+ //for the call to enter the connected state).
+ createSendStreams(mediaDescriptions);
+ }
+
+ /**
+ * The method is meant for use by protocol service implementations when
+ * willing to respond to an invitation received from a remote caller. Apart
+ * from simply generating an SDP response description, the method records
+ * details
+ *
+ * @param sdpOfferStr the SDP offer that we'd like to create an answer for.
+ * @param offerer the participant that has sent the offer.
+ *
+ * @return a String containing an SDP answer descibing parameters of the
+ * <tt>Call</tt> associated with this session and matching those advertised
+ * by the caller in their <tt>sdpOffer</tt>.
+ *
+ * @throws MediaException code INTERNAL_ERROR if processing the offer and/or
+ * generating the anser fail for some reason.
+ * @throws ParseException if <tt>sdpOfferStr</tt> does not contain a valid
+ * sdp string.
+ */
+ public String processSdpOffer(CallParticipant offerer, String sdpOfferStr)
+ throws MediaException, ParseException
+ {
+ //first parse the offer
+ SessionDescription sdpOffer = null;
+ try
+ {
+ sdpOffer = mediaServCallback.getSdpFactory()
+ .createSessionDescription(sdpOfferStr);
+ }
+ catch (SdpParseException ex)
+ {
+ throw new ParseException("Failed to parse SDPOffer: "
+ + ex.getMessage()
+ , ex.getCharOffset());
+ }
+
+ //create an sdp answer.
+ SessionDescription sdpAnswer = createSessionDescription(sdpOffer);
+ Vector mediaDescriptions = null;
+ try
+ {
+ mediaDescriptions = sdpAnswer.getMediaDescriptions(true);
+ }
+ catch (SdpException exc)
+ {
+ logger.error("failed to extract media descriptions", exc);
+ throw new MediaException("failed to extract media descriptions"
+ , MediaException.INTERNAL_ERROR
+ , exc);
+ }
+
+
+ //add the RTP targets
+ this.initStreamTargets(sdpAnswer.getConnection(), mediaDescriptions);
+
+ //create and init the streams (don't start streaming just yet but wait
+ //for the call to enter the connected state).
+ createSendStreams(mediaDescriptions);
+
+ return sdpAnswer.toString();
+ }
+
+ /**
+ * Creates a DataSource for all encodings in the mediaDescriptions vector
+ * and initializes send streams in our rtp managers for every stream in the
+ * data source.
+ * @param mediaDescriptions a <tt>Vector</tt> containing
+ * <tt>MediaDescription</tt> instances as sent by the remote side with their
+ * SDP description.
+ * @throws MediaException if we fail to create our data source with the
+ * proper encodings and/or fail to initialize the RTP managers with the
+ * necessary streams.
+ */
+ private void createSendStreams(Vector mediaDescriptions)
+ throws MediaException
+ {
+ //extract the encodings these media descriptions specify
+ Hashtable mediaEncodings
+ = extractMediaEncodings(mediaDescriptions);
+
+ //make our processor output in these encodings.
+ DataSource dataSource = mediaServCallback.getMediaControl()
+ .createDataSourceForEncodings(mediaEncodings);
+
+ //get all the steams that our processor creates as output.
+ PushBufferStream[] streams
+ = ((PushBufferDataSource)dataSource).getStreams();
+
+ //for each stream - determine whether it is a video or an audio
+ //stream and assign it to the corresponding rtpmanager
+ for (int i = 0; i < streams.length; i++)
+ {
+ RTPManager rtpManager = null;
+ if(streams[i].getFormat() instanceof VideoFormat)
+ {
+ rtpManager = getVideoRtpManager();
+ }
+ else if (streams[i].getFormat() instanceof AudioFormat)
+ {
+ rtpManager = getAudioRtpManager();
+ }
+ else
+ {
+ logger.warn("We are apparently capable of sending a format "
+ +" that is neither videro nor audio. Is "
+ +"this really possible?:"
+ +streams[i].getFormat());
+ continue;
+ }
+
+ try
+ {
+ SendStream stream = rtpManager.createSendStream(dataSource, i);
+
+ logger.trace("Created a send stream for format "
+ + streams[i].getFormat());
+ }
+ catch (Exception exc)
+ {
+ throw new MediaException(
+ "Failed to create an RTP send stream for format "
+ + streams[i].getFormat()
+ , MediaException.IO_ERROR
+ , exc);
+ }
+ }
+ }
+
+ /**
+ * Extracts the addresses that our interlocutor has sent for receiving media
+ * and adds them as targets to our RTP manager.
+ *
+ * @param globalConnParam the global <tt>Connection</tt> (if there was one)
+ * specified by our interlocutor outside any media description.
+ * @param mediaDescriptions a Vector containing all media descriptions sent
+ * by our interlocutor, that we'd use to verify whether connection level
+ * parameters have been specified.
+ *
+ * @throws ParseException if there was a problem with the sdp
+ * @throws MediaException if we simply fail to initialize the remote
+ * addresses or set them as targets on our RTPManagers.
+ */
+ private void initStreamTargets(Connection globalConnParam,
+ Vector mediaDescriptions)
+ throws MediaException, ParseException
+ {
+ try
+ {
+ String globalConnectionAddress = globalConnParam.getAddress();
+
+ Iterator mediaDescsIter = mediaDescriptions.iterator();
+ while (mediaDescsIter.hasNext())
+ {
+ SessionAddress target = null;
+ MediaDescription mediaDescription
+ = (MediaDescription) mediaDescsIter.next();
+
+ int port = mediaDescription.getMedia().getMediaPort();
+ String type = mediaDescription.getMedia().getMediaType();
+
+ String address = globalConnectionAddress;
+
+ //check if we have a media level address
+ Connection mediaLevelConnection = mediaDescription.
+ getConnection();
+
+ if (mediaLevelConnection != null)
+ {
+ address = mediaLevelConnection.getAddress();
+ }
+
+ InetAddress inetAddress = null;
+ try
+ {
+ inetAddress = InetAddress.getByName(address);
+ }
+ catch (UnknownHostException exc)
+ {
+ throw new MediaException(
+ "Failed to resolve address " + address
+ , MediaException.NETWORK_ERROR
+ , exc);
+ }
+
+ //create the session address for this media type and add it to
+ //the RTPManager.
+ target = new SessionAddress(inetAddress, port);
+
+ /** @todo the following line assumes that we have a single rtp
+ * manager per media type which is not necessarily true (e.g. we
+ * may two distinct video streams: 1 for a webcam video and another
+ * one desktop capture stream) */
+ RTPManager rtpManager = type.equals("video")
+ ? getVideoRtpManager()
+ : getAudioRtpManager();
+
+ try
+ {
+ rtpManager.addTarget(target);
+ logger.trace("added target " + target
+ + " for type " + type);
+ }
+ catch (Exception exc)
+ {
+ throw new MediaException("Failed to add RTPManager target."
+ , MediaException.INTERNAL_ERROR
+ , exc);
+ }
+ }
+ }
+ catch(SdpParseException exc)
+ {
+ throw new ParseException("Failed to parse SDP data. Error on line "
+ + exc.getLineNumber() + " "
+ + exc.getMessage()
+ , exc.getCharOffset());
+ }
+ }
+
+ /**
+ * Creates an SDP description of this session using the offer descirption
+ * (if not null) for limiting
+ *
+ * @param offer the call participant meant to receive the offer or null if
+ * we are to construct our own offer.
+ * @return a SessionDescription of this CallSession.
+ *
+ * @throws MediaException code INTERNAL_ERROR if we get an SDP exception
+ * while creating and/or parsing the sdp description.
+ */
+ private SessionDescription createSessionDescription(
+ SessionDescription offer)
+ throws MediaException
+ {
+ try
+ {
+ SessionDescription sessDescr
+ = mediaServCallback.getSdpFactory().createSessionDescription();
+
+ //"v=0"
+ Version v = mediaServCallback.getSdpFactory().createVersion(0);
+
+ sessDescr.setVersion(v);
+
+ //we don't yet implement ice so just try to choose a local address
+ //that corresponds to the address of our participant.
+ NetworkAddressManagerService netAddressManager
+ = MediaActivator.getNetworkAddressManagerService();
+
+ InetSocketAddress publicVideoAddress
+ = netAddressManager.getPublicAddressFor(getVideoPort());
+
+ InetSocketAddress publicAudioAddress
+ = netAddressManager.getPublicAddressFor(getAudioPort());
+
+ InetAddress publicIpAddress = publicAudioAddress.getAddress();
+
+ String addrType
+ = publicIpAddress instanceof Inet6Address
+ ? Connection.IP6
+ : Connection.IP4;
+
+ //spaces in the user name mess everything up.
+ //bug report - Alessandro Melzi
+ Origin o = mediaServCallback.getSdpFactory().createOrigin(
+ call.getProtocolProvider().getAccountID().getUserID()
+ , 0
+ , 0
+ , "IN"
+ , addrType
+ , publicIpAddress.getHostAddress());
+
+ sessDescr.setOrigin(o);
+ //c=
+ Connection c = mediaServCallback.getSdpFactory().createConnection(
+ "IN"
+ , addrType
+ , publicIpAddress.getHostAddress());
+
+ sessDescr.setConnection(c);
+
+ //"s=-"
+ SessionName s
+ = mediaServCallback.getSdpFactory().createSessionName("-");
+ sessDescr.setSessionName(s);
+
+ //"t=0 0"
+ TimeDescription t
+ = mediaServCallback.getSdpFactory().createTimeDescription();
+ Vector timeDescs = new Vector();
+ timeDescs.add(t);
+
+ sessDescr.setTimeDescriptions(timeDescs);
+
+ //media descriptions.
+ Vector offeredMediaDescriptions = null;
+ if(offer != null)
+ offeredMediaDescriptions = offer.getMediaDescriptions(false);
+
+ Vector mediaDescs
+ = createMediaDescriptions(offeredMediaDescriptions
+ , publicAudioAddress
+ , publicVideoAddress);
+
+ sessDescr.setMediaDescriptions(mediaDescs);
+
+ if (logger.isTraceEnabled())
+ {
+ logger.trace("Generated SDP - " + sessDescr.toString());
+ }
+
+ return sessDescr;
+ }
+ catch (SdpException exc)
+ {
+ throw new MediaException(
+ "An SDP exception occurred while generating local "
+ + "sdp description"
+ , MediaException.INTERNAL_ERROR
+ , exc);
+ }
+
+ }
+
+ /**
+ * Creates a vector containing SDP descriptions of media types and formats
+ * that we support. If the offerVector is non null
+ * @param offerMediaDescs the media descriptions sent by the offerer (could
+ * be null).
+ *
+ * @param publicAudioAddress the <tt>InetSocketAddress</tt> that we should
+ * be using for sending audio.
+ * @param publicVideoAddress the <tt>InetSocketAddress</tt> that we should
+ * be using for sending video.
+ *
+ * @return a <tt>Vector</tt> containing media descriptions that we support
+ * and (if this is an answer to an offer) that the offering
+ * <tt>CallParticipant</tt> supports as well.
+ *
+ * @throws SdpException we fail creating the media descriptions
+ */
+ private Vector createMediaDescriptions(
+ Vector offerMediaDescs,
+ InetSocketAddress publicAudioAddress,
+ InetSocketAddress publicVideoAddress)
+ throws SdpException
+ {
+ //supported audio formats.
+ String[] supportedAudioEncodings = mediaServCallback.getMediaControl()
+ .getSupportedAudioEncodings();
+
+ //supported video formats
+ String[] supportedVideoEncodings = mediaServCallback.getMediaControl()
+ .getSupportedVideoEncodings();
+
+ //if there was an offer extract the offered media formats and use
+ //the intersection between the formats we support and those in the
+ //offer.
+ if (offerMediaDescs != null && offerMediaDescs.size() > 0)
+ {
+ Vector offeredVideoEncodings = null;
+ Vector offeredAudioEncodings = null;
+ Iterator offerDescsIter = offerMediaDescs.iterator();
+
+ while (offerDescsIter.hasNext())
+ {
+ MediaDescription desc
+ = (MediaDescription) offerDescsIter.next();
+ Media media = desc.getMedia();
+ String mediaType = media.getMediaType();
+
+ if (mediaType.equalsIgnoreCase("video"))
+ {
+ offeredVideoEncodings = media.getMediaFormats(false);
+ continue;
+ }
+
+ if (mediaType.equalsIgnoreCase("audio"))
+ {
+ offeredAudioEncodings = media.getMediaFormats(false);
+ continue;
+ }
+ }
+
+
+ //recreate the formats we create according to what the other party
+ //offered.
+ List supportedAudioEncsList =Arrays.asList(supportedAudioEncodings);
+ List intersectedAudioEncsList = new LinkedList();
+ List supportedVideoEncsList =Arrays.asList(supportedVideoEncodings);
+ List intersectedVideoEncsList = new LinkedList();
+
+ //intersect supported audio formats with offered audio formats
+ if ( offeredAudioEncodings != null
+ && offeredAudioEncodings.size() > 0)
+ {
+ Iterator offeredAudioEncsIter
+ = offeredAudioEncodings.iterator();
+
+ while (offeredAudioEncsIter.hasNext())
+ {
+ String format = (String) offeredAudioEncsIter.next();
+ if (supportedAudioEncsList.contains(format))
+ intersectedAudioEncsList.add(format);
+ }
+ }
+
+ if ( offeredVideoEncodings != null
+ && offeredVideoEncodings.size() > 0)
+ {
+ //intersect supported video formats with offered video formats
+ Iterator offeredVideoEncsIter = offeredVideoEncodings.iterator();
+
+ while (offeredVideoEncsIter.hasNext())
+ {
+ String format = (String) offeredVideoEncsIter.next();
+ if (supportedVideoEncsList.contains(format))
+ intersectedVideoEncsList.add(format);
+ }
+ }
+
+ //now replace the encodings arrays with the intersection
+ supportedAudioEncodings
+ = new String[intersectedAudioEncsList.size()];
+ supportedVideoEncodings
+ = new String[intersectedVideoEncsList.size()];
+
+ for (int i = 0; i < supportedAudioEncodings.length; i++)
+ supportedAudioEncodings[i]
+ = (String)intersectedAudioEncsList.get(i);
+
+ for (int i = 0; i < supportedVideoEncodings.length; i++)
+ supportedVideoEncodings[i]
+ = (String)intersectedVideoEncsList.get(i);
+
+ }
+
+ //--------Audio media description
+ //make sure preferred formats come first
+ MediaDescription am
+ = mediaServCallback.getSdpFactory().createMediaDescription(
+ "audio"
+ , publicAudioAddress.getPort()
+ , 1
+ , "RTP/AVP"
+ , supportedAudioEncodings);
+
+ if (!mediaServCallback.getDeviceConfiguration()
+ .isAudioCaptureSupported())
+ {
+ am.setAttribute("recvonly", null);
+ }
+
+ //--------Video media description
+ //"m=video 22222 RTP/AVP 34";
+ MediaDescription vm
+ = mediaServCallback.getSdpFactory().createMediaDescription(
+ "video"
+ , publicVideoAddress.getPort()
+ , 1
+ , "RTP/AVP"
+ , supportedVideoEncodings);
+
+ if (!mediaServCallback.getDeviceConfiguration()
+ .isVideoCaptureSupported())
+ {
+ vm.setAttribute("recvonly", null);
+ }
+
+ Vector mediaDescs = new Vector();
+ mediaDescs.add(am);
+ mediaDescs.add(vm);
+
+ /** @todo record formats for participant. */
+
+ return mediaDescs;
+ }
+
+ /**
+ * Returns a <tt>Hashtable</tt> mapping media types (e.g. audio or video)
+ * to lists of JMF encoding strings corresponding to the SDP formats
+ * specified in the <tt>mediaDescriptions</tt> vector.
+ * @param mediaDescriptions a <tt>Vector</tt> containing
+ * <tt>MediaDescription</tt> instances extracted from an SDP offer or
+ * answer.
+ * @return a <tt>Hashtable</tt> mapping media types (e.g. audio or video)
+ * to lists of JMF encoding strings corresponding to the SDP formats
+ * specified in the <tt>mediaDescriptions</tt> vector.
+
+ */
+ private Hashtable extractMediaEncodings(Vector mediaDescriptions)
+ {
+ Hashtable mediaEncodings = new Hashtable();
+
+ Iterator descriptionsIter = mediaDescriptions.iterator();
+
+ while(descriptionsIter.hasNext())
+ {
+ MediaDescription mediaDescription
+ = (MediaDescription)descriptionsIter.next();
+ Media media = mediaDescription.getMedia();
+ Vector mediaFormats = null;
+ String mediaType = null;
+ try
+ {
+ mediaFormats = media.getMediaFormats(true);
+ mediaType = media.getMediaType();
+ }
+ catch (SdpParseException ex)
+ {
+ //this shouldn't happen since nist-sdp is not doing
+ //lasy parsing but log anyway
+ logger.warn("Error parsing sdp.",ex);
+ continue;
+ }
+
+
+ if(mediaFormats.size() > 0)
+ {
+ List jmfEncodings = MediaUtils.sdpToJmfEncodings(mediaFormats);
+ if(jmfEncodings.size() > 0)
+ mediaEncodings.put(mediaType, jmfEncodings);
+ }
+ }
+
+ return mediaEncodings;
+ }
+
+ /**
+ * Create the RTP managers and bind them on some ports.
+ *
+ * @throws MediaException if we fail to initialize rtp manager.
+ */
+ public void initialize()
+ throws MediaException
+ {
+ String minPortNumberStr = MediaActivator.getConfigurationService()
+ .getString(MediaService.MIN_PORT_NUMBER_PROPERTY_NAME);
+
+ if (minPortNumberStr != null)
+ {
+ try{
+ minPortNumber = Integer.parseInt(minPortNumberStr);
+ }catch (NumberFormatException ex){
+ logger.warn(minPortNumberStr
+ + " is not a valid min port number value. "
+ +"using min port " + minPortNumber);
+ }
+ }
+
+ String maxPortNumberStr = MediaActivator.getConfigurationService()
+ .getString(MediaService.MAX_PORT_NUMBER_PROPERTY_NAME);
+
+ if (maxPortNumberStr != null)
+ {
+ try{
+ maxPortNumber = Integer.parseInt(maxPortNumberStr);
+ }catch (NumberFormatException ex){
+ logger.warn(maxPortNumberStr
+ + " is not a valid max port number value. "
+ +"using max port " + maxPortNumber);
+ }
+ }
+
+ audioSessionAddress = initializeRtpManager(audioRtpManager);
+ logger.debug("Bound audio rtp manager on port "
+ + audioSessionAddress.getDataPort());
+
+ videoSessionAddress = initializeRtpManager(videoRtpManager);
+ logger.debug("Bound video rtp manager on port "
+ + videoSessionAddress.getDataPort());
+
+ call.addCallChangeListener(this);
+ }
+
+ /**
+ * Allocates a local port for the RTP manager and calls its initialize
+ * method.
+ *
+ * @param rtpManager the RTPManager to initialize.
+ * @return the SessionAddress address that <tt>rtpManager</tt> was bound
+ * upon.
+ * @throws MediaException if we fail to initialize rtp manager.
+ */
+ private SessionAddress initializeRtpManager(RTPManager rtpManager)
+ throws MediaException
+ {
+ int port = minPortNumber;
+ //augment min port number so that no one else tries to bind here.
+ minPortNumber += 2;
+
+ InetAddress inAddrAny = null;
+
+ try
+ {
+ //create an ipv4 any address since it also works when accepting
+ //ipv6 connections.
+ inAddrAny = InetAddress.getByName(NetworkUtils.IN_ADDR_ANY);
+
+
+ /** @todo temp hack */
+ //inAddrAny = InetAddress.getLocalHost();
+ }
+ catch (UnknownHostException ex)
+ {
+ //this shouldn't happen.
+ throw new MediaException("Failed to create the ANY inet address."
+ , MediaException.INTERNAL_ERROR
+ , ex);
+ }
+
+ //check the number of times that we'd have to rety binding to local
+ //ports before giving up.
+ String bindRetriesStr
+ = MediaActivator.getConfigurationService().getString(
+ MediaService.BIND_RETRIES_PROPERTY_NAME);
+
+ int bindRetries = MediaService.BIND_RETRIES_DEFAULT_VALUE;
+ try
+ {
+ if(bindRetriesStr != null && bindRetriesStr.length() > 0)
+ bindRetries = Integer.parseInt(bindRetriesStr);
+ }
+ catch (NumberFormatException ex)
+ {
+ logger.warn(bindRetriesStr
+ + " is not a valid value for number of bind retries."
+ , ex);
+ }
+
+ //try to initialize the rtp manager.
+ boolean initialized = false;
+ SessionAddress address = null;
+ for (int i = bindRetries; i > 0; i--)
+ {
+ address = new SessionAddress(inAddrAny, port);
+ try
+ {
+ rtpManager.initialize(address);
+ initialized = true;
+ break;
+ }
+ catch (InvalidSessionAddressException exc)
+ {
+ logger.info("port " + port + " seemed busy. "
+ +"Will retry with another port", exc);
+ //get a pair port number between minPortNumber and
+ //maxPortNumber.
+ port = NetworkUtils.getRandomPortNumber(minPortNumber
+ , maxPortNumber
+ , true);
+ }
+ catch (IOException exc)
+ {
+ logger.error("Failed to init an RTP manager.", exc);
+ throw new MediaException("Failed to init an RTP manager."
+ , MediaException.IO_ERROR
+ , exc);
+ }
+ }
+
+ if(!initialized)
+ throw new MediaException("Failed to bind to a local port in "
+ + bindRetriesStr + " tries."
+ , MediaException.INTERNAL_ERROR);
+
+ //it appears that if we don't do this managers don't play
+ // You can try out some other buffer size to see
+ // if you can get better smoothness.
+ BufferControl bc = (BufferControl)rtpManager
+ .getControl(BufferControl.class.getName());
+ if (bc != null)
+ {
+ long buff = bc.setBufferLength(500);
+ logger.trace("set receiver buffer len to=" + buff);
+ bc.setEnabledThreshold(false);
+ }
+
+ //add listeners
+ rtpManager.addReceiveStreamListener(this);
+ rtpManager.addSendStreamListener(this);
+ rtpManager.addSessionListener(this);
+
+ return address;
+
+ }
+
+ /**
+ * Indicates that a change has occurred in the state of the source call.
+ * @param evt the <tt>CallChangeEvent</tt> instance containing the source
+ * calls and its old and new state.
+ */
+ public void callStateChanged(CallChangeEvent evt)
+ {
+ if( evt.getNewValue() == CallState.CALL_IN_PROGRESS
+ && evt.getNewValue() != evt.getOldValue())
+ {
+ try
+ {
+ startStreaming();
+ mediaServCallback.getMediaControl().startProcessingMedia(this);
+ }
+ catch (MediaException ex)
+ {
+ /** @todo need to notify someone */
+ logger.error("Failed to start streaming.", ex);
+ }
+ }
+ else if( evt.getNewValue() == CallState.CALL_ENDED
+ && evt.getNewValue() != evt.getOldValue())
+ {
+ stopStreaming();
+ mediaServCallback.getMediaControl().stopProcessingMedia(this);
+ /** @todo need to clean all the garbage and get rid of the session */
+ /** @todo remove ourselves as listeners from the call and
+ * call participant */
+ }
+ }
+
+
+ /**
+ * Indicates that a change has occurred in the status of the source
+ * CallParticipant.
+ *
+ * @param evt The <tt>CallParticipantChangeEvent</tt> instance containing
+ * the source event as well as its previous and its new status.
+ */
+ public void participantStateChanged(CallParticipantChangeEvent evt)
+ {
+ /** @todo implement participantStateChanged() */
+ /** @todo remove target for participant. */
+ }
+
+ /**
+ * Indicates that a new call participant has joined the source call.
+ * @param evt the <tt>CallParticipantEvent</tt> containing the source call
+ * and call participant.
+ */
+ public void callParticipantAdded(CallParticipantEvent evt)
+ {
+ CallParticipant sourceParticipant = evt.getSourceCallParticipant();
+ sourceParticipant.addCallParticipantListener(this);
+ }
+
+ /**
+ * Indicates that a call participant has left the source call.
+ * @param evt the <tt>CallParticipantEvent</tt> containing the source call
+ * and call participant.
+ */
+ public void callParticipantRemoved(CallParticipantEvent evt)
+ {
+
+ }
+
+ //-------- dummy implementations of listener methods that we don't need
+ /**
+ * Ignore - we're not concerned by this event inside a call session.
+ *
+ * @param evt ignore.
+ */
+ public void participantImageChanged(CallParticipantChangeEvent evt)
+ {
+ }
+
+ /**
+ * Ignore - we're not concerned by this event inside a call session.
+ *
+ * @param evt ignore.
+ */
+ public void participantDisplayNameChanged(CallParticipantChangeEvent evt)
+ {
+ }
+
+ /**
+ * Ignore - we're not concerned by this event inside a call session.
+ *
+ * @param evt ignore.
+ */
+ public void participantTransportAddressChanged(
+ CallParticipantChangeEvent evt)
+ {
+ /** @todo i am not sure we should be ignoring this one ... */
+ }
+
+ /**
+ * Ignore - we're not concerned by this event inside a call session.
+ *
+ * @param evt ignore.
+ */
+ public void participantAddressChanged(CallParticipantChangeEvent evt)
+ {
+ }
+
+ //implementation of jmf listener methods
+ /**
+ * Method called back in the SessionListener to notify
+ * listener of all Session Events.SessionEvents could be one
+ * of NewParticipantEvent or LocalCollisionEvent.
+ *
+ * @param event the newly received SessionEvent
+ */
+ public void update(SessionEvent event)
+ {
+ if (event instanceof NewParticipantEvent)
+ {
+ Participant participant
+ = ( (NewParticipantEvent) event).getParticipant();
+ if (logger.isDebugEnabled())
+ {
+ logger.debug("A new participant had just joined: "
+ + participant.getCNAME());
+ }
+ }
+ else
+ {
+ if (logger.isDebugEnabled())
+ {
+ logger.debug(
+ "Received the following JMF Session event - "
+ + event.getClass().getName() + "=" + event);
+ }
+ }
+
+ }
+ /**
+ * Method called back in the RTPSessionListener to notify
+ * listener of all SendStream Events.
+ *
+ * @param event the newly received SendStreamEvent
+ */
+ public void update(SendStreamEvent event)
+ {
+ logger.debug(
+ "received the following JMF SendStreamEvent - "
+ + event.getClass().getName() + "="+ event);
+
+ }
+
+ /**
+ * Method called back in the RTPSessionListener to notify
+ * listener of all ReceiveStream Events.
+ *
+ * @param evt the newly received ReceiveStreamEvent
+ */
+ public void update(ReceiveStreamEvent evt)
+ {
+ logger.debug("received a new incoming stream. " + evt);
+ RTPManager mgr = (RTPManager) evt.getSource();
+ Participant participant = evt.getParticipant(); // could be null.
+ ReceiveStream stream = evt.getReceiveStream(); // could be null.
+ if (evt instanceof NewReceiveStreamEvent)
+ {
+ try
+ {
+ stream = ( (NewReceiveStreamEvent) evt).getReceiveStream();
+ DataSource ds = stream.getDataSource();
+ // Find out the formats.
+ RTPControl ctl = (RTPControl) ds.getControl(
+ "javax.media.rtp.RTPControl");
+ if (logger.isDebugEnabled())
+ {
+ if (ctl != null)
+ {
+ logger.debug("Received new RTP stream: "
+ + ctl.getFormat());
+ }
+ else
+ {
+ logger.debug("Received new RTP stream");
+ }
+ }
+ Player player = Manager.createPlayer(ds);
+ player.addControllerListener(this);
+ player.realize();
+ }
+ catch (Exception e)
+ {
+ logger.error("NewReceiveStreamEvent exception ", e);
+ return;
+ }
+ }
+ else if (evt instanceof StreamMappedEvent)
+ {
+ if (stream != null && stream.getDataSource() != null)
+ {
+ DataSource ds = stream.getDataSource();
+ // Find out the formats.
+ RTPControl ctl = (RTPControl) ds.getControl(
+ "javax.media.rtp.RTPControl");
+ if (logger.isDebugEnabled())
+ {
+ String msg = "The previously unidentified stream ";
+ if (ctl != null)
+ {
+ msg += ctl.getFormat();
+ }
+ msg += " had now been identified as sent by: "
+ + participant.getCNAME();
+ logger.debug(msg);
+ }
+ }
+ }
+ else if (evt instanceof ByeEvent)
+ {
+ logger.debug("Got \"bye\" from: " + participant.getCNAME());
+ }
+
+ }
+
+ /**
+ * This method is called when an event is generated by a
+ * <code>Controller</code> that this listener is registered with.
+ *
+ * @param ce The event generated.
+ */
+ public void controllerUpdate(ControllerEvent ce)
+ {
+ logger.debug("Received a ControllerEvent: " + ce);
+ Player player = (Player) ce.getSourceController();
+ if (player == null) {
+ return;
+ }
+ // Get this when the internal players are realized.
+ if (ce instanceof RealizeCompleteEvent) {
+ logger.debug("A player was realized and will be started.");
+ player.start();
+/** @todo very ugly test code
+ * please don't forget to remove */
+GainControl gc = (GainControl)player.getControl(GainControl.class.getName());
+ if(gc != null)
+ {
+ logger.debug("Setting volue to max");
+ gc.setLevel(1);
+ }
+ else
+ logger.debug("Player does not have gain control.");
+//javax.swing.JFrame frame = new javax.swing.JFrame();
+//frame.getContentPane().add( player.getControlPanelComponent() );
+//frame.pack();
+//frame.setVisible(true);
+ }
+ if (ce instanceof StartEvent) {
+ logger.debug("Received a StartEvent");
+// mediaManager.firePlayerStarting(p);
+ }
+ if (ce instanceof ControllerErrorEvent) {
+ logger.error(
+ "The following error was reported while starting a player"
+ + ce);
+ }
+ if (ce instanceof ControllerClosedEvent) {
+ logger.debug("Received a ControllerClosedEvent");
+// mediaManager.firePlayerStopped();
+ }
+ }
+}