/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jitsi.impl.neomedia;

import java.awt.*;
import java.awt.event.*;
import java.beans.*;
import java.io.*;
import java.util.*;
import java.util.List;

import javax.media.*;
import javax.media.control.*;
import javax.media.format.*;
import javax.media.protocol.*;
import javax.swing.*;

import net.java.sip.communicator.impl.neomedia.codec.*;

import org.jitsi.impl.neomedia.codec.*;
import org.jitsi.impl.neomedia.codec.video.*;
import org.jitsi.impl.neomedia.device.*;
import org.jitsi.impl.neomedia.format.*;
import org.jitsi.impl.neomedia.transform.sdes.*;
import org.jitsi.service.configuration.*;
import org.jitsi.service.libjitsi.*;
import org.jitsi.service.neomedia.*;
import org.jitsi.service.neomedia.codec.*;
import org.jitsi.service.neomedia.device.*;
import org.jitsi.service.neomedia.format.*;
import org.jitsi.service.resources.*;
import org.jitsi.util.*;
import org.jitsi.util.event.*;
import org.jitsi.util.swing.*;
import org.json.*;

import com.sun.media.util.*;

/**
 * Implements <tt>MediaService</tt> for JMF.
 *
 * @author Lyubomir Marinov
 * @author Dmitri Melnikov
 */
public class MediaServiceImpl
    extends PropertyChangeNotifier
    implements MediaService
{
    /**
     * The <tt>Logger</tt> used by the <tt>MediaServiceImpl</tt> class and its
     * instances for logging output.
     */
    private static final Logger logger
        = Logger.getLogger(MediaServiceImpl.class);

    /**
     * The name of the <tt>boolean</tt> <tt>ConfigurationService</tt> property
     * which indicates whether the detection of audio <tt>CaptureDevice</tt>s is
     * to be disabled. The default value is <tt>false</tt> i.e. the audio
     * <tt>CaptureDevice</tt>s are detected.
     */
    public static final String DISABLE_AUDIO_SUPPORT_PNAME
        = "net.java.sip.communicator.service.media.DISABLE_AUDIO_SUPPORT";

    /**
     * The name of the <tt>boolean</tt> <tt>ConfigurationService</tt> property
     * which indicates whether the detection of video <tt>CaptureDevice</tt>s is
     * to be disabled. The default value is <tt>false</tt> i.e. the video
     * <tt>CaptureDevice</tt>s are detected.
     */
    public static final String DISABLE_VIDEO_SUPPORT_PNAME
        = "net.java.sip.communicator.service.media.DISABLE_VIDEO_SUPPORT";

    /**
     * The prefix of the property names the values of which specify the dynamic
     * payload type preferences.
     */
    private static final String DYNAMIC_PAYLOAD_TYPE_PREFERENCES_PNAME_PREFIX
        = "net.java.sip.communicator.impl.neomedia.dynamicPayloadTypePreferences";

    /**
     * The value of the <tt>devices</tt> property of <tt>MediaServiceImpl</tt>
     * when no <tt>MediaDevice</tt>s are available. Explicitly defined in order
     * to reduce unnecessary allocations.
     */
    private static final List<MediaDevice> EMPTY_DEVICES
        = Collections.emptyList();

    /**
     * The name of the <tt>System</tt> boolean property which specifies whether
     * the loading of the JMF/FMJ <tt>Registry</tt> is to be disabled. 
     */
    private static final String JMF_REGISTRY_DISABLE_LOAD
        = "net.sf.fmj.utility.JmfRegistry.disableLoad";

    /**
     * The indicator which determines whether the loading of the JMF/FMJ
     * <tt>Registry</tt> is disabled.
     */
    private static boolean jmfRegistryDisableLoad;

    /**
     * The indicator which determined whether
     * {@link #postInitializeOnce(MediaServiceImpl)} has been executed in order
     * to perform one-time initialization after initializing the first instance
     * of <tt>MediaServiceImpl</tt>.
     */
    private static boolean postInitializeOnce;

    /**
     * The prefix that is used to store configuration for encodings preference.
     */
    private static final String ENCODING_CONFIG_PROP_PREFIX
        = "net.java.sip.communicator.impl.neomedia.codec.EncodingConfiguration";

    /**
     * The <tt>CaptureDevice</tt> user choices such as the default audio and
     * video capture devices.
     */
    private final DeviceConfiguration deviceConfiguration
        = new DeviceConfiguration();

    /**
     * The <tt>PropertyChangeListener</tt> which listens to
     * {@link #deviceConfiguration}.
     */
    private final PropertyChangeListener
        deviceConfigurationPropertyChangeListener
            = new PropertyChangeListener()
                    {
                        public void propertyChange(PropertyChangeEvent event)
                        {
                            deviceConfigurationPropertyChange(event);
                        }
                    };

    /**
     * The list of audio <tt>MediaDevice</tt>s reported by this instance when
     * its {@link MediaService#getDevices(MediaType, MediaUseCase)} method is
     * called with an argument {@link MediaType#AUDIO}.
     */
    private final List<MediaDeviceImpl> audioDevices
        = new ArrayList<MediaDeviceImpl>();

    /**
     * The {@link EncodingConfiguration} instance that holds the current (global)
     * list of formats and their preference.
     */
    private final EncodingConfiguration currentEncodingConfiguration;

    /**
     * The <tt>MediaFormatFactory</tt> through which <tt>MediaFormat</tt>
     * instances may be created for the purposes of working with the
     * <tt>MediaStream</tt>s created by this <tt>MediaService</tt>.
     */
    private MediaFormatFactory formatFactory;

    /**
     * The one and only <tt>MediaDevice</tt> instance with
     * <tt>MediaDirection</tt> not allowing sending and <tt>MediaType</tt> equal
     * to <tt>AUDIO</tt>.
     */
    private MediaDevice nonSendAudioDevice;

    /**
     * The one and only <tt>MediaDevice</tt> instance with
     * <tt>MediaDirection</tt> not allowing sending and <tt>MediaType</tt> equal
     * to <tt>VIDEO</tt>.
     */
    private MediaDevice nonSendVideoDevice;

    /**
     * The list of video <tt>MediaDevice</tt>s reported by this instance when
     * its {@link MediaService#getDevices(MediaType, MediaUseCase)} method is
     * called with an argument {@link MediaType#VIDEO}.
     */
    private final List<MediaDeviceImpl> videoDevices
        = new ArrayList<MediaDeviceImpl>();

    /**
     * A {@link Map} that binds indicates whatever preferences this
     * media service implementation may have for the RTP payload type numbers
     * that get dynamically assigned to {@link MediaFormat}s with no static
     * payload type. The method is useful for formats such as "telephone-event"
     * for example that is statically assigned the 101 payload type by some
     * legacy systems. Signalling protocol implementations such as SIP and XMPP
     * should make sure that, whenever this is possible, they assign to formats
     * the dynamic payload type returned in this {@link Map}.
     */
    private static Map<MediaFormat, Byte> dynamicPayloadTypePreferences;

    /**
     * The volume control of the media service playback.
     */
    private static VolumeControl outputVolumeControl;

    /**
     * The volume control of the media service capture.
     */
    private static VolumeControl inputVolumeControl;

    /**
     * Listeners interested in Recorder events without the need to
     * have access to their instances.
     */
    private final List<Recorder.Listener> recorderListeners =
            new ArrayList<Recorder.Listener>();

    static
    {
        setupFMJ();
    }

    /**
     * Initializes a new <tt>MediaServiceImpl</tt> instance.
     */
    public MediaServiceImpl()
    {
        /*
         * XXX The deviceConfiguration is initialized and referenced by this
         * instance so adding deviceConfigurationPropertyChangeListener does not
         * need a matching removal.
         */
        deviceConfiguration.addPropertyChangeListener(
                deviceConfigurationPropertyChangeListener);

        currentEncodingConfiguration
             = new EncodingConfigurationConfigImpl(ENCODING_CONFIG_PROP_PREFIX);


        /*
         * Perform one-time initialization after initializing the first instance
         * of MediaServiceImpl.
         */
        synchronized (MediaServiceImpl.class)
        {
            if (!postInitializeOnce)
            {
                postInitializeOnce = true;
                postInitializeOnce(this);
            }
        }
    }

    /**
     * Create a <tt>MediaStream</tt> which will use a specific
     * <tt>MediaDevice</tt> for capture and playback of media. The new instance
     * will not have a <tt>StreamConnector</tt> at the time of its construction
     * and a <tt>StreamConnector</tt> will be specified later on in order to
     * enable the new instance to send and receive media.
     *
     * @param device the <tt>MediaDevice</tt> to be used by the new instance for
     * capture and playback of media
     * @return a newly-created <tt>MediaStream</tt> which will use the specified
     * <tt>device</tt> for capture and playback of media
     * @see MediaService#createMediaStream(MediaDevice)
     */
    public MediaStream createMediaStream(MediaDevice device)
    {
        return createMediaStream(null, device);
    }

    /**
     * {@inheritDoc}
     *
     * Implements {@link MediaService#createMediaStream(MediaType)}. Initializes
     * a new <tt>AudioMediaStreamImpl</tt> or <tt>VideoMediaStreamImpl</tt> in
     * accord with <tt>mediaType</tt>
     */
    public MediaStream createMediaStream(MediaType mediaType)
    {
        return createMediaStream(mediaType, null, null, null);
    }

    /**
     * Creates a new <tt>MediaStream</tt> instance which will use the specified
     * <tt>MediaDevice</tt> for both capture and playback of media exchanged
     * via the specified <tt>StreamConnector</tt>.
     *
     * @param connector the <tt>StreamConnector</tt> that the new
     * <tt>MediaStream</tt> instance is to use for sending and receiving media
     * @param device the <tt>MediaDevice</tt> that the new <tt>MediaStream</tt>
     * instance is to use for both capture and playback of media exchanged via
     * the specified <tt>connector</tt>
     * @return a new <tt>MediaStream</tt> instance
     * @see MediaService#createMediaStream(StreamConnector, MediaDevice)
     */
    public MediaStream createMediaStream(
            StreamConnector connector,
            MediaDevice device)
    {
        return createMediaStream(connector, device, null);
    }

    /**
     * Creates a new <tt>MediaStream</tt> instance which will use the specified
     * <tt>MediaDevice</tt> for both capture and playback of media exchanged
     * via the specified <tt>StreamConnector</tt>.
     *
     * @param connector the <tt>StreamConnector</tt> that the new
     * <tt>MediaStream</tt> instance is to use for sending and receiving media
     * @param device the <tt>MediaDevice</tt> that the new <tt>MediaStream</tt>
     * instance is to use for both capture and playback of media exchanged via
     * the specified <tt>connector</tt>
     * @param srtpControl a control which is already created, used to control
     * the SRTP operations.
     *
     * @return a new <tt>MediaStream</tt> instance
     * @see MediaService#createMediaStream(StreamConnector, MediaDevice)
     */
    public MediaStream createMediaStream(
            StreamConnector connector,
            MediaDevice device,
            SrtpControl srtpControl)
    {
        return createMediaStream(null, connector, device, srtpControl);
    }

    /**
     * Initializes a new <tt>MediaStream</tt> instance. The method is the actual
     * implementation to which the public <tt>createMediaStream</tt> methods of
     * <tt>MediaServiceImpl</tt> delegate.
     *
     * @param mediaType the <tt>MediaType</tt> of the new <tt>MediaStream</tt>
     * instance to be initialized. If <tt>null</tt>, <tt>device</tt> must be
     * non-<tt>null</tt> and its {@link MediaDevice#getMediaType()} will be used
     * to determine the <tt>MediaType</tt> of the new instance. If
     * non-<tt>null</tt>, <tt>device</tt> may be <tt>null</tt>. If
     * non-<tt>null</tt> and <tt>device</tt> is non-<tt>null</tt>, the
     * <tt>MediaType</tt> of <tt>device</tt> must be (equal to)
     * <tt>mediaType</tt>.
     * @param connector the <tt>StreamConnector</tt> to be used by the new
     * instance if non-<tt>null</tt>
     * @param device the <tt>MediaDevice</tt> to be used by the instance if
     * non-<tt>null</tt>
     * @param srtpControl the <tt>SrtpControl</tt> to be used by the new
     * instance if non-<tt>null</tt>
     * @return a new <tt>MediaStream</tt> instance
     */
    private MediaStream createMediaStream(
            MediaType mediaType,
            StreamConnector connector,
            MediaDevice device,
            SrtpControl srtpControl)
    {
        // Make sure that mediaType and device are in accord.
        if (mediaType == null)
        {
            if (device == null)
                throw new NullPointerException("device");
            else
                mediaType = device.getMediaType();
        }
        else if ((device != null) && !mediaType.equals(device.getMediaType()))
            throw new IllegalArgumentException("device");

        switch (mediaType)
        {
        case AUDIO:
            return new AudioMediaStreamImpl(connector, device, srtpControl);
        case VIDEO:
            return new VideoMediaStreamImpl(connector, device, srtpControl);
        default:
            return null;
        }
    }

    /**
     * Creates a new <tt>MediaDevice</tt> which uses a specific
     * <tt>MediaDevice</tt> to capture and play back media and performs mixing
     * of the captured media and the media played back by any other users of the
     * returned <tt>MediaDevice</tt>. For the <tt>AUDIO</tt> <tt>MediaType</tt>,
     * the returned device is commonly referred to as an audio mixer. The
     * <tt>MediaType</tt> of the returned <tt>MediaDevice</tt> is the same as
     * the <tt>MediaType</tt> of the specified <tt>device</tt>.
     *
     * @param device the <tt>MediaDevice</tt> which is to be used by the
     * returned <tt>MediaDevice</tt> to actually capture and play back media
     * @return a new <tt>MediaDevice</tt> instance which uses <tt>device</tt> to
     * capture and play back media and performs mixing of the captured media and
     * the media played back by any other users of the returned
     * <tt>MediaDevice</tt> instance
     * @see MediaService#createMixer(MediaDevice)
     */
    public MediaDevice createMixer(MediaDevice device)
    {
        switch (device.getMediaType())
        {
        case AUDIO:
            return new AudioMixerMediaDevice((AudioMediaDeviceImpl) device);
        case VIDEO:
            return new VideoTranslatorMediaDevice((MediaDeviceImpl) device);
        default:
            /*
             * TODO If we do not support mixing, should we return null or rather
             * a MediaDevice with INACTIVE MediaDirection?
             */
            return null;
        }
    }

    /**
     * Gets the default <tt>MediaDevice</tt> for the specified
     * <tt>MediaType</tt>.
     *
     * @param mediaType a <tt>MediaType</tt> value indicating the type of media
     * to be handled by the <tt>MediaDevice</tt> to be obtained
     * @param useCase the <tt>MediaUseCase</tt> to obtain the
     * <tt>MediaDevice</tt> list for
     * @return the default <tt>MediaDevice</tt> for the specified
     * <tt>mediaType</tt> if such a <tt>MediaDevice</tt> exists; otherwise,
     * <tt>null</tt>
     * @see MediaService#getDefaultDevice(MediaType, MediaUseCase)
     */
    public MediaDevice getDefaultDevice(
            MediaType mediaType,
            MediaUseCase useCase)
    {
        CaptureDeviceInfo captureDeviceInfo;

        switch (mediaType)
        {
        case AUDIO:
            captureDeviceInfo
                = getDeviceConfiguration().getAudioCaptureDevice();
            break;
        case VIDEO:
            captureDeviceInfo
                = getDeviceConfiguration().getVideoCaptureDevice(useCase);
            break;
        default:
            captureDeviceInfo = null;
            break;
        }

        MediaDevice defaultDevice = null;

        if (captureDeviceInfo != null)
        {
            for (MediaDevice device : getDevices(mediaType, useCase))
            {
                if ((device instanceof MediaDeviceImpl)
                        && captureDeviceInfo.equals(
                                ((MediaDeviceImpl) device)
                                    .getCaptureDeviceInfo()))
                {
                    defaultDevice = device;
                    break;
                }
            }
        }
        if (defaultDevice == null)
        {
            switch (mediaType)
            {
            case AUDIO:
                defaultDevice = getNonSendAudioDevice();
                break;
            case VIDEO:
                defaultDevice = getNonSendVideoDevice();
                break;
            default:
                /*
                 * There is no MediaDevice with direction which does not allow
                 * sending and mediaType other than AUDIO and VIDEO.
                 */
                break;
            }
        }

        return defaultDevice;
    }

    /**
     * Gets the <tt>CaptureDevice</tt> user choices such as the default audio
     * and video capture devices.
     *
     * @return the <tt>CaptureDevice</tt> user choices such as the default audio
     * and video capture devices.
     */
    public DeviceConfiguration getDeviceConfiguration()
    {
        return deviceConfiguration;
    }

    /**
     * Gets a list of the <tt>MediaDevice</tt>s known to this
     * <tt>MediaService</tt> and handling the specified <tt>MediaType</tt>.
     *
     * @param mediaType the <tt>MediaType</tt> to obtain the
     * <tt>MediaDevice</tt> list for
     * @param useCase the <tt>MediaUseCase</tt> to obtain the
     * <tt>MediaDevice</tt> list for
     * @return a new <tt>List</tt> of <tt>MediaDevice</tt>s known to this
     * <tt>MediaService</tt> and handling the specified <tt>MediaType</tt>. The
     * returned <tt>List</tt> is a copy of the internal storage and,
     * consequently, modifications to it do not affect this instance. Despite
     * the fact that a new <tt>List</tt> instance is returned by each call to
     * this method, the <tt>MediaDevice</tt> instances are the same if they are
     * still known to this <tt>MediaService</tt> to be available.
     * @see MediaService#getDevices(MediaType, MediaUseCase)
     */
    public List<MediaDevice> getDevices(
            MediaType mediaType,
            MediaUseCase useCase)
    {
        List<CaptureDeviceInfo> cdis;
        List<MediaDeviceImpl> privateDevices;

        if (MediaType.VIDEO.equals(mediaType))
        {
            /*
             * In case a video capture device has been added to or removed from
             * system (i.e. webcam, monitor, etc.), rescan the video capture
             * devices.
             */
            DeviceSystem.initializeDeviceSystems(MediaType.VIDEO);
        }

        switch (mediaType)
        {
        case AUDIO:
            cdis = getDeviceConfiguration().getAvailableAudioCaptureDevices();
            privateDevices = audioDevices;
            break;
        case VIDEO:
            cdis
                = getDeviceConfiguration().getAvailableVideoCaptureDevices(
                        useCase);
            privateDevices = videoDevices;
            break;
        default:
            /*
             * MediaService does not understand MediaTypes other than AUDIO and
             * VIDEO.
             */
            return EMPTY_DEVICES;
        }

        List<MediaDevice> publicDevices;

        synchronized (privateDevices)
        {
            if ((cdis == null)
                    || (cdis.size() <= 0))
                privateDevices.clear();
            else
            {
                Iterator<MediaDeviceImpl> deviceIter
                    = privateDevices.iterator();

                while (deviceIter.hasNext())
                {
                    Iterator<CaptureDeviceInfo> cdiIter = cdis.iterator();
                    CaptureDeviceInfo captureDeviceInfo
                        = deviceIter.next().getCaptureDeviceInfo();
                    boolean deviceIsFound = false;

                    while (cdiIter.hasNext())
                    {
                        if (captureDeviceInfo.equals(cdiIter.next()))
                        {
                            deviceIsFound = true;
                            cdiIter.remove();
                            break;
                        }
                    }
                    if (!deviceIsFound)
                        deviceIter.remove();
                }

                for (CaptureDeviceInfo cdi : cdis)
                {
                    if (cdi == null)
                        continue;

                    MediaDeviceImpl device;

                    switch (mediaType)
                    {
                    case AUDIO:
                        device = new AudioMediaDeviceImpl(cdi);
                        break;
                    case VIDEO:
                        device
                            = new MediaDeviceImpl(cdi, mediaType);
                        break;
                    default:
                        device = null;
                        break;
                    }
                    if (device != null)
                        privateDevices.add(device);
                }
            }

            publicDevices = new ArrayList<MediaDevice>(privateDevices);
        }

        /*
         * If there are no MediaDevice instances of the specified mediaType,
         * make sure that there is at least one MediaDevice which does not allow
         * sending.
         */
        if (publicDevices.isEmpty())
        {
            MediaDevice nonSendDevice;

            switch (mediaType)
            {
            case AUDIO:
                nonSendDevice = getNonSendAudioDevice();
                break;
            case VIDEO:
                nonSendDevice = getNonSendVideoDevice();
                break;
            default:
                /*
                 * There is no MediaDevice with direction not allowing sending
                 * and mediaType other than AUDIO and VIDEO.
                 */
                nonSendDevice = null;
                break;
            }
            if (nonSendDevice != null)
                publicDevices.add(nonSendDevice);
        }

        return publicDevices;
    }

    /**
     * Returns the current encoding configuration -- the instance that contains
     * the global settings. Note that any changes made to this instance will
     * have immediate effect on the configuration.
     *
     * @return the current encoding configuration -- the instance that contains
     * the global settings.
     */
    public EncodingConfiguration getCurrentEncodingConfiguration()
    {
        return currentEncodingConfiguration;
    }

    /**
     * Gets the <tt>MediaFormatFactory</tt> through which <tt>MediaFormat</tt>
     * instances may be created for the purposes of working with the
     * <tt>MediaStream</tt>s created by this <tt>MediaService</tt>.
     *
     * @return the <tt>MediaFormatFactory</tt> through which
     * <tt>MediaFormat</tt> instances may be created for the purposes of working
     * with the <tt>MediaStream</tt>s created by this <tt>MediaService</tt>
     * @see MediaService#getFormatFactory()
     */
    public MediaFormatFactory getFormatFactory()
    {
        if (formatFactory == null)
            formatFactory = new MediaFormatFactoryImpl();
        return formatFactory;
    }

    /**
     * Gets the one and only <tt>MediaDevice</tt> instance with
     * <tt>MediaDirection</tt> not allowing sending and <tt>MediaType</tt> equal
     * to <tt>AUDIO</tt>.
     *
     * @return the one and only <tt>MediaDevice</tt> instance with
     * <tt>MediaDirection</tt> not allowing sending and <tt>MediaType</tt> equal
     * to <tt>AUDIO</tt>
     */
    private MediaDevice getNonSendAudioDevice()
    {
        if (nonSendAudioDevice == null)
            nonSendAudioDevice = new AudioMediaDeviceImpl();
        return nonSendAudioDevice;
    }

    /**
     * Gets the one and only <tt>MediaDevice</tt> instance with
     * <tt>MediaDirection</tt> not allowing sending and <tt>MediaType</tt> equal
     * to <tt>VIDEO</tt>.
     *
     * @return the one and only <tt>MediaDevice</tt> instance with
     * <tt>MediaDirection</tt> not allowing sending and <tt>MediaType</tt> equal
     * to <tt>VIDEO</tt>
     */
    private MediaDevice getNonSendVideoDevice()
    {
        if (nonSendVideoDevice == null)
            nonSendVideoDevice = new MediaDeviceImpl(MediaType.VIDEO);
        return nonSendVideoDevice;
    }

    /**
     * Initializes a new <tt>ZrtpControl</tt> instance which is to control all
     * ZRTP options.
     *
     * @return a new <tt>ZrtpControl</tt> instance which is to control all ZRTP
     * options
     */
    public ZrtpControl createZrtpControl()
    {
        return new ZrtpControlImpl();
    }

    /**
     * Initializes a new <tt>SDesControl</tt> instance which is to control all
     * SDes options.
     *
     * @return a new <tt>SDesControl</tt> instance which is to control all SDes
     * options
     */
    public SDesControl createSDesControl()
    {
        return new SDesControlImpl();
    }

    /**
     * Gets the <tt>VolumeControl</tt> which controls the volume level of audio
     * output/playback.
     *
     * @return the <tt>VolumeControl</tt> which controls the volume level of
     * audio output/playback
     * @see MediaService#getOutputVolumeControl()
     */
    public VolumeControl getOutputVolumeControl()
    {
        if (outputVolumeControl == null)
        {
            outputVolumeControl
                = new AbstractVolumeControl(
                        VolumeControl.PLAYBACK_VOLUME_LEVEL_PROPERTY_NAME);
        }
        return outputVolumeControl;
    }

    /**
     * Gets the <tt>VolumeControl</tt> which controls the volume level of audio
     * input/capture.
     *
     * @return the <tt>VolumeControl</tt> which controls the volume level of
     * audio input/capture
     * @see MediaService#getInputVolumeControl()
     */
    public VolumeControl getInputVolumeControl()
    {
        if (inputVolumeControl == null)
        {
            inputVolumeControl
                = new AbstractVolumeControl(
                        VolumeControl.CAPTURE_VOLUME_LEVEL_PROPERTY_NAME);
        }
        return inputVolumeControl;
    }

    /**
     * Get available screens.
     *
     * @return screens
     */
    public List<ScreenDevice> getAvailableScreenDevices()
    {
        ScreenDevice screens[] = ScreenDeviceImpl.getAvailableScreenDevices();
        List<ScreenDevice> screenList;

        if ((screens != null) && (screens.length != 0))
            screenList = new ArrayList<ScreenDevice>(Arrays.asList(screens));
        else
            screenList = Collections.emptyList();
        return screenList;
    }

    /**
     * Get default screen device.
     *
     * @return default screen device
     */
    public ScreenDevice getDefaultScreenDevice()
    {
        List<ScreenDevice> screens = getAvailableScreenDevices();
        int width = 0;
        int height = 0;
        ScreenDevice best = null;

        for (ScreenDevice screen : screens)
        {
            java.awt.Dimension res = screen.getSize();

            if ((res != null) && ((width < res.width) || (height < res.height)))
            {
                width = res.width;
                height = res.height;
                best = screen;
            }
        }
        return best;
    }

    /**
     * Creates a new <tt>Recorder</tt> instance that can be used to record a
     * call which captures and plays back media using a specific
     * <tt>MediaDevice</tt>.
     *
     * @param device the <tt>MediaDevice</tt> which is used for media capture
     * and playback by the call to be recorded
     * @return a new <tt>Recorder</tt> instance that can be used to record a
     * call which captures and plays back media using the specified
     * <tt>MediaDevice</tt>
     * @see MediaService#createRecorder(MediaDevice)
     */
    public Recorder createRecorder(MediaDevice device)
    {
        if (device instanceof AudioMixerMediaDevice)
            return new RecorderImpl((AudioMixerMediaDevice) device);
        else
            return null;
    }

    /**
     * Returns a {@link Map} that binds indicates whatever preferences this
     * media service implementation may have for the RTP payload type numbers
     * that get dynamically assigned to {@link MediaFormat}s with no static
     * payload type. The method is useful for formats such as "telephone-event"
     * for example that is statically assigned the 101 payload type by some
     * legacy systems. Signaling protocol implementations such as SIP and XMPP
     * should make sure that, whenever this is possible, they assign to formats
     * the dynamic payload type returned in this {@link Map}.
     *
     * @return a {@link Map} binding some formats to a preferred dynamic RTP
     * payload type number.
     */
    public Map<MediaFormat, Byte> getDynamicPayloadTypePreferences()
    {
        if(dynamicPayloadTypePreferences == null)
        {
            dynamicPayloadTypePreferences = new HashMap<MediaFormat, Byte>();

            /*
             * Set the dynamicPayloadTypePreferences to their default values. If
             * the user chooses to override them through the
             * ConfigurationService, they will be overwritten later on.
             */
            MediaFormat telephoneEvent
                = MediaUtils.getMediaFormat("telephone-event", 8000);
            if (telephoneEvent != null)
                dynamicPayloadTypePreferences.put(telephoneEvent, (byte) 101);

            MediaFormat h264
                = MediaUtils.getMediaFormat(
                        "H264",
                        VideoMediaFormatImpl.DEFAULT_CLOCK_RATE);
            if (h264 != null)
                dynamicPayloadTypePreferences.put(h264, (byte) 99);

            /*
             * Try to load dynamicPayloadTypePreferences from the
             * ConfigurationService.
             */
            ConfigurationService cfg = LibJitsi.getConfigurationService();

            if (cfg != null)
            {
                String prefix = DYNAMIC_PAYLOAD_TYPE_PREFERENCES_PNAME_PREFIX;
                List<String> propertyNames
                    = cfg.getPropertyNamesByPrefix(prefix, true);

                for (String propertyName : propertyNames)
                {
                    /*
                     * The dynamic payload type is the name of the property name
                     * and the format which prefers it is the property value.
                     */
                    byte dynamicPayloadTypePreference = 0;
                    Throwable exception = null;

                    try
                    {
                        dynamicPayloadTypePreference
                            = Byte.parseByte(
                                    propertyName.substring(
                                            prefix.length() + 1));
                    }
                    catch (IndexOutOfBoundsException ioobe)
                    {
                        exception = ioobe;
                    }
                    catch (NumberFormatException nfe)
                    {
                        exception = nfe;
                    }
                    if (exception != null)
                    {
                        logger.warn(
                                "Ignoring dynamic payload type preference"
                                    + " which could not be parsed: "
                                    + propertyName,
                                exception);
                        continue;
                    }

                    String source = cfg.getString(propertyName);

                    if ((source != null) && (source.length() != 0))
                    {
                        try
                        {
                            JSONObject json = new JSONObject(source);
                            String encoding
                                = json.getString(
                                        MediaFormatImpl.ENCODING_PNAME);
                            int clockRate
                                = json.getInt(MediaFormatImpl.CLOCK_RATE_PNAME);
                            Map<String, String> fmtps
                                = new HashMap<String, String>();

                            if (json.has(
                                    MediaFormatImpl.FORMAT_PARAMETERS_PNAME))
                            {
                                JSONObject jsonFmtps
                                    = json.getJSONObject(
                                            MediaFormatImpl
                                                .FORMAT_PARAMETERS_PNAME);
                                Iterator<?> jsonFmtpsIter = jsonFmtps.keys();

                                while (jsonFmtpsIter.hasNext())
                                {
                                    String key
                                        = jsonFmtpsIter.next().toString();
                                    String value = jsonFmtps.getString(key);

                                    fmtps.put(key, value);
                                }
                            }

                            MediaFormat mediaFormat
                                = MediaUtils.getMediaFormat(
                                        encoding, clockRate,
                                        fmtps);

                            if (mediaFormat != null)
                            {
                                dynamicPayloadTypePreferences.put(
                                        mediaFormat,
                                        dynamicPayloadTypePreference);
                            }
                        }
                        catch (JSONException jsone)
                        {
                            logger.warn(
                                    "Ignoring dynamic payload type preference"
                                        + " which could not be parsed: "
                                        + source,
                                    jsone);
                        }
                    }
                }
            }
        }
        return dynamicPayloadTypePreferences;
    }

    /**
     * Creates a preview component for the specified device(video device) used
     * to show video preview from that device.
     *
     * @param device the video device
     * @param preferredWidth the width we prefer for the component
     * @param preferredHeight the height we prefer for the component
     * @return the preview component.
     */
    public Object getVideoPreviewComponent(
            MediaDevice device,
            int preferredWidth, int preferredHeight)
    {
        ResourceManagementService resources
            = LibJitsi.getResourceManagementService();
        String noPreviewText
            = (resources == null)
                ? ""
                : resources.getI18NString("impl.media.configform.NO_PREVIEW");
        JLabel noPreview = new JLabel(noPreviewText);

        noPreview.setHorizontalAlignment(SwingConstants.CENTER);
        noPreview.setVerticalAlignment(SwingConstants.CENTER);

        final JComponent videoContainer = new VideoContainer(noPreview, false);

        if ((preferredWidth > 0) && (preferredHeight > 0))
            videoContainer.setPreferredSize(
                    new Dimension(preferredWidth, preferredHeight));

        try
        {
            CaptureDeviceInfo captureDeviceInfo;

            if ((device != null) &&
                    ((captureDeviceInfo
                                = ((MediaDeviceImpl)device)
                                    .getCaptureDeviceInfo())
                            != null))
            {
                DataSource dataSource
                    = Manager.createDataSource(captureDeviceInfo.getLocator());

                /*
                 * Don't let the size be uselessly small just because the
                 * videoContainer has too small a preferred size.
                 */
                if ((preferredWidth < 128) || (preferredHeight < 96))
                {
                    preferredWidth = 128;
                    preferredHeight = 96;
                }
                VideoMediaStreamImpl.selectVideoSize(
                        dataSource,
                        preferredWidth, preferredHeight);

                // A Player is documented to be created on a connected
                // DataSource.
                dataSource.connect();

                Processor player = Manager.createProcessor(dataSource);
                final MediaLocator locator = dataSource.getLocator();

                player.addControllerListener(new ControllerListener()
                {
                    public void controllerUpdate(ControllerEvent event)
                    {
                        controllerUpdateForPreview(
                                event,
                                videoContainer,
                                locator);
                    }
                });
                player.configure();
            }
        }
        catch (Throwable t)
        {
            if (t instanceof ThreadDeath)
                throw (ThreadDeath) t;
            logger.error("Failed to create video preview", t);
        }

        return videoContainer;
    }

    /**
     * Listens and shows the video in the video container when needed.
     * @param event the event when player has ready visual component.
     * @param videoContainer the container.
     * @param locator input DataSource locator
     */
    private static void controllerUpdateForPreview(
            ControllerEvent event,
            JComponent videoContainer,
            MediaLocator locator)
    {
        if (event instanceof ConfigureCompleteEvent)
        {
            Processor player = (Processor) event.getSourceController();

            /*
             * Use SwScaler for the scaling since it produces an image with
             * better quality and add the "flip" effect to the video.
             */
            TrackControl[] trackControls = player.getTrackControls();

            if ((trackControls != null) && (trackControls.length != 0))
                try
                {
                    for (TrackControl trackControl : trackControls)
                    {
                        Codec codecs[] = null;
                        SwScaler scaler = new SwScaler();

                        // do not flip desktop
                        if (DeviceSystem.LOCATOR_PROTOCOL_IMGSTREAMING.equals(
                                locator.getProtocol()))
                            codecs = new Codec[] { scaler };
                        else
                            codecs = new Codec[] { new HFlip(), scaler };

                        trackControl.setCodecChain(codecs);
                        break;
                    }
                }
                catch (UnsupportedPlugInException upiex)
                {
                    logger.warn(
                            "Failed to add SwScaler/VideoFlipEffect to " +
                            "codec chain", upiex);
                }

            // Turn the Processor into a Player.
            try
            {
                player.setContentDescriptor(null);
            }
            catch (NotConfiguredError nce)
            {
                logger.error(
                    "Failed to set ContentDescriptor of Processor",
                    nce);
            }

            player.realize();
        }
        else if (event instanceof RealizeCompleteEvent)
        {
            Player player = (Player) event.getSourceController();
            Component video = player.getVisualComponent();

            showPreview(videoContainer, video, player);
        }
    }

    /**
     * Shows the preview panel.
     * @param previewContainer the container
     * @param preview the preview component.
     * @param player the player.
     */
    private static void showPreview(
            final JComponent previewContainer,
            final Component preview,
            final Player player)
    {
        if (!SwingUtilities.isEventDispatchThread())
        {
            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    showPreview(previewContainer, preview, player);
                }
            });
            return;
        }

        previewContainer.removeAll();

        if (preview != null)
        {
            HierarchyListener hierarchyListener = new HierarchyListener()
            {
                private Window window;

                private WindowListener windowListener;

                public void dispose()
                {
                    if (windowListener != null)
                    {
                        if (window != null)
                        {
                            window.removeWindowListener(windowListener);
                            window = null;
                        }
                        windowListener = null;
                    }
                    preview.removeHierarchyListener(this);

                    disposePlayer(player);

                    /*
                     * We've just disposed the player which created the preview
                     * component so the preview component is of no use
                     * regardless of whether the Media configuration form will
                     * be redisplayed or not. And since the preview component
                     * appears to be a huge object even after its player is
                     * disposed, make sure to not reference it.
                     */
                    previewContainer.remove(preview);
                }

                public void hierarchyChanged(HierarchyEvent event)
                {
                    if ((event.getChangeFlags()
                                    & HierarchyEvent.DISPLAYABILITY_CHANGED)
                                == 0)
                        return;

                    if (!preview.isDisplayable())
                    {
                        dispose();
                        return;
                    }
                    else
                        player.start();

                    if (windowListener == null)
                    {
                        window = SwingUtilities.windowForComponent(preview);
                        if (window != null)
                        {
                            windowListener = new WindowAdapter()
                            {
                                @Override
                                public void windowClosing(WindowEvent event)
                                {
                                    dispose();
                                }
                            };
                            window.addWindowListener(windowListener);
                        }
                    }
                }
            };
            preview.addHierarchyListener(hierarchyListener);

            previewContainer.add(preview);

            previewContainer.revalidate();
            previewContainer.repaint();
        }
        else
            disposePlayer(player);
    }

    /**
     * Dispose the player used for the preview.
     * @param player the player.
     */
    private static void disposePlayer(Player player)
    {
        player.stop();
        player.deallocate();
        player.close();
    }

    /**
     * Get a <tt>MediaDevice</tt> for a part of desktop streaming/sharing.
     *
     * @param width width of the part
     * @param height height of the part
     * @param x origin of the x coordinate (relative to the full desktop)
     * @param y origin of the y coordinate (relative to the full desktop)
     * @return <tt>MediaDevice</tt> representing the part of desktop or null
     * if problem
     */
    public MediaDevice getMediaDeviceForPartialDesktopStreaming(
            int width, int height, int x, int y)
    {
        MediaDevice device = null;
        String name = "Partial desktop streaming";
        Dimension size = null;
        int multiple = 0;
        Point p = new Point((x < 0) ? 0 : x, (y < 0) ? 0 : y);
        ScreenDevice dev = getScreenForPoint(p);
        int display = -1;

        if(dev != null)
            display = dev.getIndex();
        else
            return null;

        /* on Mac OS X, width have to be a multiple of 16 */
        if(OSUtils.IS_MAC)
        {
            multiple =  Math.round(width / 16f);
            width = multiple * 16;
        }
        else
        {
            /* JMF filter graph seems to not like odd width */
            multiple = Math.round(width / 2f);
            width = multiple * 2;
        }

        /* JMF filter graph seems to not like odd height */
        multiple = Math.round(height / 2f);
        height = multiple * 2;

        size = new Dimension(width, height);

        Format formats[] = new Format[]
                            {
                                   new AVFrameFormat(
                                        size,
                                        Format.NOT_SPECIFIED,
                                        FFmpeg.PIX_FMT_ARGB,
                                        Format.NOT_SPECIFIED),
                                   new RGBFormat(
                                        size, // size
                                        Format.NOT_SPECIFIED, // maxDataLength
                                        Format.byteArray, // dataType
                                        Format.NOT_SPECIFIED, // frameRate
                                        32, // bitsPerPixel
                                        2 /* red */,
                                        3 /* green */,
                                        4 /* blue */)
                             };

        Rectangle bounds = ((ScreenDeviceImpl)dev).getBounds();
        x -= bounds.x;
        y -= bounds.y;

        CaptureDeviceInfo devInfo
            = new CaptureDeviceInfo(
                name + " " + display,
                new MediaLocator(
                        DeviceSystem.LOCATOR_PROTOCOL_IMGSTREAMING
                            + ":"
                            + display
                            + ","
                            + x
                            + ","
                            + y),
                formats);

        device = new MediaDeviceImpl(devInfo, MediaType.VIDEO);
        return device;
    }

    /**
     * If the <tt>MediaDevice</tt> corresponds to partial desktop streaming
     * device.
     *
     * @param mediaDevice <tt>MediaDevice</tt>
     * @return true if <tt>MediaDevice</tt> is a partial desktop streaming
     * device, false otherwise
     */
    public boolean isPartialStreaming(MediaDevice mediaDevice)
    {
        if(mediaDevice == null)
            return false;

        MediaDeviceImpl dev = (MediaDeviceImpl)mediaDevice;
        CaptureDeviceInfo cdi = dev.getCaptureDeviceInfo();

        return
            (cdi != null)
                && cdi.getName().startsWith("Partial desktop streaming");
    }

    /**
      * Find the screen device that contains specified point.
      *
      * @param p point coordinates
      * @return screen device that contains point
      */
    public ScreenDevice getScreenForPoint(Point p)
    {
        for(ScreenDevice dev : getAvailableScreenDevices())
            if(dev.containsPoint(p))
                return dev;
        return null;
    }

    /**
     * Gets the origin of a specific desktop streaming device.
     *
     * @param mediaDevice the desktop streaming device to get the origin on
     * @return the origin of the specified desktop streaming device
     */
    public Point getOriginForDesktopStreamingDevice(MediaDevice mediaDevice)
    {
        MediaDeviceImpl dev = (MediaDeviceImpl)mediaDevice;
        CaptureDeviceInfo cdi = dev.getCaptureDeviceInfo();

        if(cdi == null)
            return null;

        MediaLocator locator = cdi.getLocator();

        if(!DeviceSystem.LOCATOR_PROTOCOL_IMGSTREAMING.equals(
                locator.getProtocol()))
            return null;

        String remainder = locator.getRemainder();
        String split[] = remainder.split(",");
        int index
            = Integer.parseInt(
                    ((split != null) && (split.length > 1))
                        ? split[0]
                        : remainder);

        List<ScreenDevice> devs = getAvailableScreenDevices();

        if (devs.size() - 1 >= index)
        {
            Rectangle r = ((ScreenDeviceImpl) devs.get(index)).getBounds();

            return new Point(r.x, r.y);
        }

        return null;
    }

    /**
     * Those interested in Recorder events add listener through MediaService.
     * This way they don't need to have access to the Recorder instance.
     * Adds a new <tt>Recorder.Listener</tt> to the list of listeners
     * interested in notifications from a <tt>Recorder</tt>.
     *
     * @param listener the new <tt>Recorder.Listener</tt> to be added to the
     * list of listeners interested in notifications from <tt>Recorder</tt>s.
     */
    public void addRecorderListener(Recorder.Listener listener)
    {
        synchronized(recorderListeners)
        {
            if(!recorderListeners.contains(listener))
                recorderListeners.add(listener);
        }
    }

    /**
     * Removes an existing <tt>Recorder.Listener</tt> from the list of listeners
     * interested in notifications from <tt>Recorder</tt>s.
     *
     * @param listener the existing <tt>Listener</tt> to be removed from the
     * list of listeners interested in notifications from <tt>Recorder</tt>s
     */
    public void removeRecorderListener(Recorder.Listener listener)
    {
        synchronized(recorderListeners)
        {
            recorderListeners.remove(listener);
        }
    }

    /**
     * Gives access to currently registered <tt>Recorder.Listener</tt>s.
     * @return currently registered <tt>Recorder.Listener</tt>s.
     */
    public Iterator<Recorder.Listener> getRecorderListeners()
    {
        return recorderListeners.iterator();
    }

    /**
     * Notifies this instance that the value of a property of
     * {@link #deviceConfiguration} has changed.
     *
     * @param event a <tt>PropertyChangeEvent</tt> which specifies the name of
     * the property which had its value changed and the old and the new values
     * of that property
     */
    private void deviceConfigurationPropertyChange(PropertyChangeEvent event)
    {
        String propertyName = event.getPropertyName();

        /*
         * While AUDIO_CAPTURE_DEVICE is sure to affect the DEFAULT_DEVICE,
         * AUDIO_PLAYBACK_DEVICE is not. Anyway, MediaDevice is supposed to
         * represent the device to be used for capture AND playback (though its
         * current implementation MediaDeviceImpl may be incomplete with respect
         * to the playback representation). Since it is not clear at this point
         * of the execution whether AUDIO_PLAYBACK_DEVICE really affects the
         * DEFAULT_DEVICE and for the sake of completeness, throw in the changes
         * to the AUDIO_NOTIFY_DEVICE as well.
         */
        if (DeviceConfiguration.AUDIO_CAPTURE_DEVICE.equals(propertyName)
                || DeviceConfiguration.AUDIO_NOTIFY_DEVICE.equals(propertyName)
                || DeviceConfiguration.AUDIO_PLAYBACK_DEVICE.equals(
                        propertyName)
                || DeviceConfiguration.VIDEO_CAPTURE_DEVICE.equals(
                        propertyName))
        {
            /*
             * We do not know the old value of the property at the time of this
             * writing. We cannot report the new value either because we do not
             * know the MediaType and the MediaUseCase.
             */
            firePropertyChange(DEFAULT_DEVICE, null, null);
        }
    }

    /**
     * Initializes a new <tt>RTPTranslator</tt> which is to forward RTP and RTCP
     * traffic between multiple <tt>MediaStream</tt>s.
     *
     * @return a new <tt>RTPTranslator</tt> which is to forward RTP and RTCP
     * traffic between multiple <tt>MediaStream</tt>s
     * @see MediaService#createRTPTranslator()
     */
    public RTPTranslator createRTPTranslator()
    {
        return new RTPTranslatorImpl();
    }

    /**
     * Gets the indicator which determines whether the loading of the JMF/FMJ
     * <tt>Registry</tt> has been disabled.
     *
     * @return <tt>true</tt> if the loading of the JMF/FMJ <tt>Registry</tt> has
     * been disabled; otherwise, <tt>false</tt>
     */
    public static boolean isJmfRegistryDisableLoad()
    {
        return jmfRegistryDisableLoad;
    }

    /**
     * Performs one-time initialization after initializing the first instance of
     * <tt>MediaServiceImpl</tt>.
     *
     * @param mediaServiceImpl the <tt>MediaServiceImpl</tt> instance which has
     * caused the need to perform the one-time initialization
     */
    private static void postInitializeOnce(MediaServiceImpl mediaServiceImpl)
    {
        new ZrtpFortunaEntropyGatherer(
                mediaServiceImpl.getDeviceConfiguration())
            .setEntropy();
    }

    /**
     * Sets up FMJ for execution. For example, sets properties which instruct
     * FMJ whether it is to create a log, where the log is to be created.
     */
    private static void setupFMJ()
    {
        /*
         * Since FMJ is part of neomedia, FMJ's log should be enabled when
         * neomedia's log is enabled.
         */
        Registry.set("allowLogging", logger.isDebugEnabled());

        /*
         * Disable the loading of .fmj.registry because Kertesz Laszlo has
         * reported that audio input devices duplicate after restarting Jitsi.
         * Besides, Jitsi does not really need .fmj.registry on startup.
         */
        if (System.getProperty(JMF_REGISTRY_DISABLE_LOAD) == null)
            System.setProperty(JMF_REGISTRY_DISABLE_LOAD, "true");
        jmfRegistryDisableLoad
            = "true".equalsIgnoreCase(System.getProperty(
                    JMF_REGISTRY_DISABLE_LOAD));

        String scHomeDirLocation
            = System.getProperty(
                ConfigurationService.PNAME_SC_HOME_DIR_LOCATION);

        if (scHomeDirLocation != null)
        {
            String scHomeDirName
                = System.getProperty(
                    ConfigurationService.PNAME_SC_HOME_DIR_NAME);

            if (scHomeDirName != null)
            {
                File scHomeDir = new File(scHomeDirLocation, scHomeDirName);

                /* Write FMJ's log in Jitsi's log directory. */
                Registry.set(
                    "secure.logDir",
                    new File(scHomeDir, "log").getPath());

                /* Write FMJ's registry in Jitsi's user data directory. */
                String jmfRegistryFilename
                    = "net.sf.fmj.utility.JmfRegistry.filename";

                if (System.getProperty(jmfRegistryFilename) == null)
                {
                    System.setProperty(
                        jmfRegistryFilename,
                        new File(scHomeDir, ".fmj.registry").getAbsolutePath());
                }
            }
        }

        FMJPlugInConfiguration.registerCustomPackages();
        FMJPlugInConfiguration.registerCustomCodecs();
    }

    /**
     * Returns a new {@link EncodingConfiguration} instance that can be
     * used by other bundles.
     *
     * @return a new {@link EncodingConfiguration} instance.
     */
    public EncodingConfiguration createEmptyEncodingConfiguration()
    {
        return new EncodingConfigurationImpl();
    }
}