/* * 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.net.*; import java.util.*; import javax.sdp.*; import javax.media.Time; import net.java.sip.communicator.impl.media.codec.*; import net.java.sip.communicator.impl.media.device.*; import net.java.sip.communicator.service.media.*; import net.java.sip.communicator.service.media.event.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.util.*; /** * The service is meant to be a wrapper of media libraries such as JMF, * (J)FFMPEG, JMFPAPI, and others. It takes care of all media play and capture * as well as media transport (e.g. over RTP). * * Before being able to use this service calls would have to make sure that it * is initialized (i.e. consult the isInitialized() method). * * @author Emil Ivov * @author Martin Andre * @author Ryan Ricard * @author Symphorien Wanko * @author Ken Larson */ public class MediaServiceImpl implements MediaService { /** * Our logger. */ private final Logger logger = Logger.getLogger(MediaServiceImpl.class); /** * The SdpFactory instance that we use for construction of all sdp * descriptions. */ private SdpFactory sdpFactory = null; /** * A flag indicating whether the media service implementation is ready * to be used. */ private boolean isStarted = false; /** * A flag indicating whether the media service implementation is currently * in the process of being started. */ private boolean isStarting = false; /** * The lock object that we use for synchronization during startup. */ private final Object startingLock = new Object(); /** * Our event dispatcher. */ private MediaEventDispatcher mediaDispatcher = new MediaEventDispatcher(); /** * Our device configuration helper. */ private DeviceConfiguration deviceConfiguration = new DeviceConfiguration(); /** * Our encoding configuration helper. */ private EncodingConfiguration encodingConfiguration = new EncodingConfiguration(); /** * Our media control helper. The media control instance that we will be * using for reading media for all calls that do not have a custom media * control mapping. */ private final MediaControl defaultMediaControl = new MediaControl(); /** * Mappings of calls to instances of <tt>MediaControl</tt>. In case a call * has been mapped to a media control instance, it is going to be used for * retrieving media that we are going to be sending inside this call. * Calls that have custom media control mappings are for example calls that * have been answered by a mailbox plug-in and that will be using a file * as their sound source. */ private final Map<Call, MediaControl> callMediaControlMappings = new Hashtable<Call, MediaControl>(); /** * Mappings of calls to custom data sinks. Used by mailbox plug-ins for * sending audio/video flows to a file instead of the sound card or the * screen. */ private final Map<Call, URL> callDataSinkMappings = new Hashtable<Call, URL>(); /** * Currently open call sessions. */ //private Map activeCallSessions = new Hashtable(); /** * Default constructor */ public MediaServiceImpl() { } /** * Implements <tt>getSupportedAudioEncodings</tt> from interface * <tt>MediaService</tt> * * @return an array of Strings containing audio formats in the order of * preference. */ public String[] getSupportedAudioEncodings() { return getMediaControl().getSupportedAudioEncodings(); } /** * Implements <tt>getSupportedVideoEncodings</tt> from interface * <tt>MediaService</tt> * * @return an array of Strings containing video formats in the order of * preference. */ public String[] getSupportedVideoEncodings() { return getMediaControl().getSupportedVideoEncodings(); } /** * Creates a call session for <tt>call</tt>. The method allocates audio * and video ports which won't be released until the corresponding call * gets into a DISCONNECTED state. If a session already exists for call, * it is returned and no new session is created. * <p> * @param call the Call that we'll be encapsulating in the newly created * session. * @return a <tt>CallSession</tt> encapsulating <tt>call</tt>. * @throws MediaException with code IO_ERROR if we fail allocating ports. */ public CallSession createCallSession(Call call) throws MediaException { waitUntilStarted(); //if we have this call mapped to a custom data destination, pass that //destination on to the callSession. CallSessionImpl callSession = new CallSessionImpl( call, this, callDataSinkMappings.get(call)); // commented out because it leaks memory, and activeCallSessions isn't // used anyway (by Michael Koch) // activeCallSessions.put(call, callSession); /** @todo make sure you remove the session once its over. */ return callSession; } /** * A <tt>RtpFlow</tt> is an object which role is to handle media data * transfer, capture and playback. It's build between two points, a local * and a remote. The media transfered will be in a format specified by the * <tt>mediaEncodings</tt> parameter. * * @param localIP local address of this RtpFlow * @param localPort local port of this RtpFlow * @param remoteIP remote address of this RtpFlow * @param remotePort remote port of this RtpFlow * @param mediaEncodings format used to encode data on this flow * @return rtpFlow the newly created <tt>RtpFlow</tt> * @throws MediaException if operation fails */ public RtpFlow createRtpFlow(String localIP, int localPort, String remoteIP, int remotePort, Map mediaEncodings) throws MediaException { waitUntilStarted(); return new RtpFlowImpl(this, localIP, remoteIP, localPort, remotePort, new Hashtable(mediaEncodings)); } /** * Adds a listener that will be listening for incoming media and changes * in the state of the media listener. * * @param listener the listener to register */ public void addMediaListener(MediaListener listener) { mediaDispatcher.addMediaListener(listener); } /** * Removes a listener that was listening for incoming media and changes * in the state of the media listener * @param listener the listener to remove */ public void removeMediaListener(MediaListener listener) { mediaDispatcher.removeMediaListener(listener); } /** * Initializes the service implementation, and puts it in a state where it * could interoperate with other services. */ public void start() { /* * TODO The method is called only once (in MediaActivator) at the time * of this writing. However, it being public suggests it may be called * more than once and, if it becomes the case one day, care should be * taken to not start a new DeviceConfigurationThread while a previous * one is running. */ // new DeviceConfigurationThread().start(); } /** * Releases all resources and prepares for shutdown. */ public void stop() { try { this.closeCaptureDevices(); } catch (MediaException ex) { logger.error("Failed to properly close capture devices.", ex); } isStarted = false; } /** * Returns true if the media service implementation is initialized and ready * for use by other services, and false otherwise. * * @return true if the media manager is initialized and false otherwise. */ public boolean isStarted() { return isStarted; } /** * Verifies whether the media service is started and ready for use and * throws an exception otherwise. * * @throws MediaException if the media service is not started and ready for * use. */ protected void assertStarted() throws MediaException { if (!isStarted()) { logger.error("The MediaServiceImpl had not been properly started! " + "Impossible to continue."); throw new MediaException( "The MediaManager had not been properly started! " + "Impossible to continue." , MediaException.SERVICE_NOT_STARTED); } } /** * Close capture devices specified by configuration service. * * @throws MediaException if opening the devices fails. */ private void closeCaptureDevices() throws MediaException { getMediaControl().closeCaptureDevices(); } /** * Makes the service implementation close all release any devices or other * resources that it might have allocated and prepare for shutdown/garbage * collection. */ public void shutdown() { isStarted = false; } /** * A valid instance of an SDP factory that call session may use for * manipulating sdp data. * * @return a valid instance of an SDP factory that call session may use for * manipulating sdp data. */ public SdpFactory getSdpFactory() { return sdpFactory; } /** * A valid instance of the Media Control that the call session may use to * query for supported audio video encodings. * * @return the default instance of the Media Control that the call session * may use to query for supported audio video encodings. */ public MediaControl getMediaControl() { try { waitUntilStarted(); } catch (MediaException ex) { throw new IllegalStateException(ex); } return defaultMediaControl; } /** * The MediaControl instance that is mapped to <tt>call</tt>. If * <tt>call</tt> is not mapped to a particular <tt>MediaControl</tt> * instance, the default instance will be returned * * @param call the call whose MediaControl we will fetch * @return the instance of MediaControl that is mapped to <tt>call</tt> * or the <tt>defaultMediaControl</tt> if no custom one is registered for * <tt>call</tt>. */ public MediaControl getMediaControl(Call call) { MediaControl mediaControl = callMediaControlMappings.get(call); return (mediaControl != null) ? mediaControl : getMediaControl(); } /** * A valid instance of the DeviceConfiguration that a call session may use * to query for supported support of audio/video capture. * * @return a valid instance of the DeviceConfiguration that a call session * may use to query for supported support of audio/video capture. */ public DeviceConfiguration getDeviceConfiguration() { return deviceConfiguration; } /** * A valid instance of the EncodingConfiguration that a call session may use * to query for encodings and their priority. * * @return a valid instance of the EncodingConfiguration */ public EncodingConfiguration getEncodingConfiguration() { return encodingConfiguration; } /** * We use this thread to detect, initialize and configure all capture * devices. */ private class DeviceConfigurationThread extends Thread { /** * Sets a thread name and gives the thread a daemon status. */ public DeviceConfigurationThread() { super("DeviceConfigurationThread"); setDaemon(true); } /** * Initializes device configuration. */ public void run() { synchronized(startingLock) { isStarting = true; try { deviceConfiguration.initialize(); defaultMediaControl. initialize(deviceConfiguration, encodingConfiguration); sdpFactory = SdpFactory.getInstance(); isStarted = true; } catch (Throwable ex) { logger.error("Failed to initialize media control", ex); isStarted = false; } isStarting = false; startingLock.notifyAll(); } } } /** * A utility method that would block until the media service has been * started or, in case it already is started, return immediately. * * @throws MediaException if the media service is not started and ready for * use. */ private void waitUntilStarted() throws MediaException { synchronized (startingLock) { if (isStarting) { try { startingLock.wait(); } catch (InterruptedException ex) { logger.warn( "Interrupted while waiting for the stack to start", ex); } } } assertStarted(); } /** * Sets the data source for <tt>call</tt> to the URL <tt>dataSourceURL</tt> * instead of the default data source. This is used (for instance) * to play audio from a file instead of from a the microphone. * * @param call the <tt>Call</tt> whose data source will be changed * @param dataSourceURL the <tt>URL</tt> of the new data source */ public void setCallDataSource(Call call, URL dataSourceURL) throws MediaException { //create a new instance of MediaControl for this call MediaControl callMediaControl = new MediaControl(); callMediaControl.initDataSourceFromURL(dataSourceURL); callMediaControlMappings.put(call, callMediaControl); } /** * Returns the duration (in milliseconds) of the data source * being used for the given call. If the data source is not time-based, * IE a microphone, or the duration cannot be determined, returns -1 * * @param call the call whose data source duration will be retrieved * @return -1 or the duration of the data source */ public double getDataSourceDurationSeconds(Call call) { Time duration = getMediaControl(call).getOutputDuration(); if (duration == javax.media.Duration.DURATION_UNKNOWN) return -1; else return duration.getSeconds(); } /** * Unsets the data source for <tt>call</tt>, which will now use the default * data source. * * @param call the call whose data source mapping will be released */ public void unsetCallDataSource(Call call) { callMediaControlMappings.remove(call); } /** * Sets the Data Destination for <tt>call</tt> to the URL * <tt>dataSinkURL</tt> instead of the default data destination. This is * used (for instance) to record incoming data to a file instead of sending * it to the speakers/screen. * * @param call the call whose data destination will be changed * @param dataSinkURL the URL of the new data sink. */ public void setCallDataSink(Call call, URL dataSinkURL) { callDataSinkMappings.put(call, dataSinkURL); } /** * Unsets the data destination for <tt>call</tt>, which will now send data * to the default destination. * * @param call the call whose data destination mapping will be released */ public void unsetCallDataSink(Call call) { callDataSinkMappings.remove(call); } //------------------ main method for testing --------------------------------- /** * This method is here most probably only temporarily for the sake of * testing. @todo remove main method. * * @param args String[] * * @throws java.lang.Throwable if it doesn't feel like executing today. */ public static void main(String[] args) throws Throwable { new MediaServiceImpl().start(); System.out.println("done"); } }