/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.java.sip.communicator.impl.neomedia;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.concurrent.*;

import javax.media.*;
import javax.media.MediaException;
import javax.media.protocol.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

import net.java.sip.communicator.impl.neomedia.codec.video.h264.*;
import net.java.sip.communicator.plugin.desktoputil.*;
import net.java.sip.communicator.plugin.desktoputil.TransparentPanel;
import net.java.sip.communicator.util.*;

import org.jitsi.impl.neomedia.*;
import org.jitsi.impl.neomedia.device.*;
import org.jitsi.service.audionotifier.*;
import org.jitsi.service.configuration.*;
import org.jitsi.service.neomedia.*;
import org.jitsi.service.neomedia.codec.*;
import org.jitsi.service.neomedia.device.*;
import org.jitsi.service.neomedia.event.*;
import org.jitsi.service.resources.*;
import org.jitsi.util.OSUtils;
import org.jitsi.util.swing.*;
import org.jitsi.utils.*;

/**
 * Implements <tt>MediaConfigurationService</tt> i.e. represents a factory of
 * user interface which allows the user to configure the media-related
 * functionality of the application.
 *
 * @author Lyubomir Marinov
 * @author Damian Minkov
 * @author Yana Stamcheva
 * @author Boris Grozev
 * @author Vincent Lucas
 */
public class MediaConfigurationImpl
    implements ActionListener,
               MediaConfigurationService
{
    /**
     * Creates a new listener to combo box and affect changes to the audio level
     * indicator. The level indicator is updated via a thread in order to avoid
     * deadlock of the user interface.
     */
    private class AudioLevelListenerThread
        implements ActionListener,
                   HierarchyListener
    {
        /**
         * Listener to update the audio level indicator.
         */
        private final SimpleAudioLevelListener audioLevelListener
            = new SimpleAudioLevelListener()
            {
                public void audioLevelChanged(int level)
                {
                    soundLevelIndicator.updateSoundLevel(level);
                }
            };

        /**
         * The audio system used to get and set the sound devices.
         */
        private AudioSystem audioSystem;

        /**
         * The combo box used to select the device the user wants to use.
         */
        private JComboBox comboBox;

        /**
         * The current capture device.
         */
        private AudioMediaDeviceSession deviceSession;

        /**
         * The new device chosen by the user and that we need to initialize as
         * the new capture device.
         */
        private AudioMediaDeviceSession deviceSessionToSet;

        /**
         * The indicator which determines whether
         * {@link #setDeviceSession(AudioMediaDeviceSession)} is to be invoked
         * when {@link #deviceSessionToSet} is <tt>null</tt>.
         */
        private boolean deviceSessionToSetIsNull;

        /**
         * The <tt>ExecutorService</tt> which is to asynchronously invoke
         * {@link #setDeviceSession(AudioMediaDeviceSession)} with
         * {@link #deviceSessionToSet}.
         */
        private final ExecutorService setDeviceSessionExecutor
            = Executors.newSingleThreadExecutor();

        private final Runnable setDeviceSessionTask
            = new Runnable()
            {
                public void run()
                {
                    AudioMediaDeviceSession deviceSession = null;
                    boolean deviceSessionIsNull = false;

                    synchronized (AudioLevelListenerThread.this)
                    {
                        if ((deviceSessionToSet != null)
                                || deviceSessionToSetIsNull)
                        {
                            /*
                             * Invoke #setDeviceSession(AudioMediaDeviceSession)
                             * outside the synchronized block to avoid a GUI
                             * deadlock.
                             */
                            deviceSession = deviceSessionToSet;
                            deviceSessionIsNull = deviceSessionToSetIsNull;
                            deviceSessionToSet = null;
                            deviceSessionToSetIsNull = false;
                        }
                    }

                    if ((deviceSession != null) || deviceSessionIsNull)
                    {
                        /*
                         * XXX The method blocks on Mac OS X for Bluetooth
                         * devices which are paired but disconnected.
                         */
                        setDeviceSession(deviceSession);
                    }
                }
            };

        /**
         * The sound level indicator used to show the effectiveness of the
         * capture device.
         */
        private SoundLevelIndicator soundLevelIndicator;

        /**
         *  Provides an handler which reads the stream into the
         *  "transferHandlerBuffer".
         */
        private final BufferTransferHandler transferHandler
            = new BufferTransferHandler()
            {
                public void transferData(PushBufferStream stream)
                {
                    try
                    {
                        stream.read(transferHandlerBuffer);
                    }
                    catch (IOException ioe)
                    {
                    }
                }
            };

        /**
         * The buffer used for reading the capture device.
         */
        private final Buffer transferHandlerBuffer = new Buffer();

        /**
         * Creates a new listener to combo box and affect changes to the audio
         * level indicator.
         *
         * @param audioSystem The audio system used to get and set the sound
         * devices.
         * @param comboBox The combo box used to select the device the user
         * wants to use.
         * @param soundLevelIndicator The sound level indicator used to show the
         * effectiveness of the capture device.
         */
        public AudioLevelListenerThread(
                AudioSystem audioSystem,
                JComboBox comboBox,
                SoundLevelIndicator soundLevelIndicator)
        {
            init(audioSystem, comboBox, soundLevelIndicator);
        }

        /**
         * Refresh combo box when the user click on it.
         *
         * @param ev The click on the combo box.
         */
        public void actionPerformed(ActionEvent ev)
        {
            synchronized (this)
            {
                deviceSessionToSet = null;
                deviceSessionToSetIsNull = true;
                setDeviceSessionExecutor.execute(setDeviceSessionTask);
            }

            CaptureDeviceInfo cdi;

            if (comboBox == null)
            {
                cdi
                    = soundLevelIndicator.isShowing()
                        ? audioSystem.getSelectedDevice(
                                AudioSystem.DataFlow.CAPTURE)
                        : null;
            }
            else
            {
                Object selectedItem
                    = soundLevelIndicator.isShowing()
                        ? comboBox.getSelectedItem()
                        : null;

                cdi
                    = (selectedItem
                            instanceof
                                DeviceConfigurationComboBoxModel.CaptureDevice)
                        ? ((DeviceConfigurationComboBoxModel.CaptureDevice)
                                selectedItem)
                            .info
                        : null;
            }

            if (cdi != null)
            {
                for (MediaDevice md: mediaService.getDevices(
                            MediaType.AUDIO,
                            MediaUseCase.ANY))
                {
                    if (md instanceof AudioMediaDeviceImpl)
                    {
                        AudioMediaDeviceImpl amd = (AudioMediaDeviceImpl) md;

                        if (cdi.equals(amd.getCaptureDeviceInfo()))
                        {
                            try
                            {
                                MediaDeviceSession deviceSession
                                    = amd.createSession();
                                boolean deviceSessionIsSet = false;

                                try
                                {
                                    if (deviceSession instanceof
                                            AudioMediaDeviceSession)
                                    {
                                        synchronized (this)
                                        {
                                            deviceSessionToSet
                                                = (AudioMediaDeviceSession)
                                                    deviceSession;
                                            deviceSessionToSetIsNull
                                                = (deviceSessionToSet == null);
                                            setDeviceSessionExecutor.execute(
                                                    setDeviceSessionTask);
                                        }
                                        deviceSessionIsSet = true;
                                    }
                                }
                                finally
                                {
                                    if (!deviceSessionIsSet)
                                        deviceSession.close();
                                }
                            }
                            catch (Throwable t)
                            {
                                if (t instanceof ThreadDeath)
                                    throw (ThreadDeath) t;
                            }
                            break;
                        }
                    }
                }
            }
        }

        public void hierarchyChanged(HierarchyEvent ev)
        {
            if ((ev.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0)
            {
                SwingUtilities.invokeLater(
                        new Runnable()
                        {
                            public void run()
                            {
                                actionPerformed(null);
                            }
                        });
            }
        }

        /**
         * Creates a new listener to combo box and affect changes to the audio
         * level indicator.
         *
         * @param audioSystem The audio system used to get and set the sound
         * devices.
         * @param comboBox The combo box used to select the device the user
         * wants to use.
         * @param soundLevelIndicator The sound level indicator used to show the
         * effectiveness of the capture device.
         */
        public void init(
                AudioSystem audioSystem,
                JComboBox comboBox,
                SoundLevelIndicator soundLevelIndicator)
        {
            this.audioSystem = audioSystem;

            if (this.comboBox != comboBox)
            {
                if (this.comboBox != null)
                    this.comboBox.removeActionListener(this);
                this.comboBox = comboBox;
                if (comboBox != null)
                    comboBox.addActionListener(this);
            }

            if (this.soundLevelIndicator != soundLevelIndicator)
            {
                if (this.soundLevelIndicator != null)
                    this.soundLevelIndicator.removeHierarchyListener(this);
                this.soundLevelIndicator = soundLevelIndicator;
                if (soundLevelIndicator != null)
                    soundLevelIndicator.addHierarchyListener(this);
            }
        }

        /**
         * Sets the new capture device used by the audio level indicator.
         *
         * @param deviceSession The new capture device used by the audio level
         * indicator.
         */
        private void setDeviceSession(AudioMediaDeviceSession deviceSession)
        {
            if (this.deviceSession == deviceSession)
                return;

            if (this.deviceSession != null)
            {
                try
                {
                    this.deviceSession.close();
                }
                finally
                {
                    this.deviceSession.setLocalUserAudioLevelListener(null);
                    soundLevelIndicator.resetSoundLevel();
                }
            }

            this.deviceSession = deviceSession;

            if (deviceSession != null)
            {
                deviceSession.setContentDescriptor(
                        new ContentDescriptor(ContentDescriptor.RAW));
                deviceSession.setLocalUserAudioLevelListener(
                        audioLevelListener);

                deviceSession.start(MediaDirection.SENDONLY);

                try
                {
                    DataSource dataSource = deviceSession.getOutputDataSource();

                    dataSource.connect();

                    PushBufferStream[] streams
                        = ((PushBufferDataSource) dataSource).getStreams();

                    for (PushBufferStream stream : streams)
                        stream.setTransferHandler(transferHandler);

                    dataSource.start();
                }
                catch (Throwable t)
                {
                    if (t instanceof ThreadDeath)
                        throw (ThreadDeath) t;
                }
            }
        }
    }

    /**
     * Renders the available resolutions in the combo box.
     */
    private static class ResolutionCellRenderer
        extends DefaultListCellRenderer
    {
        /**
         * The serialization version number of the
         * <tt>ResolutionCellRenderer</tt> class. Defined to the value of
         * <tt>0</tt> because the <tt>ResolutionCellRenderer</tt> instances do
         * not have state of their own.
         */
        private static final long serialVersionUID = 0L;

        /**
         * Sets readable text describing the resolution if the selected
         * value is null we return the string "Auto".
         *
         * @param list
         * @param value
         * @param index
         * @param isSelected
         * @param cellHasFocus
         * @return Component
         */
        @Override
        public Component getListCellRendererComponent(
            JList list,
            Object value,
            int index,
            boolean isSelected,
            boolean cellHasFocus)
        {
            // call super to set backgrounds and fonts
            super.getListCellRendererComponent(
                    list,
                    value,
                    index,
                    isSelected,
                    cellHasFocus);

            // now just change the text
            if(value == null)
                setText("Auto");
            else if(value instanceof Dimension)
            {
                Dimension d = (Dimension)value;

                setText(((int) d.getWidth()) + "x" + ((int) d.getHeight()));
            }
            return this;
        }
    }

    /**
     * Wrapper for the device list field.
     */
    private static class DeviceComboBoxField
    {
        /**
         * The combo box with the devices.
         */
        private JComboBox deviceComboBox = null;

        /**
         * The <tt>JList</tt> with the devices.
         */
        private JList deviceList = null;

        /**
         * The current component that displays the list with the devices.
         */
        private Component deviceComponent;

        /**
         * The listener for the field.
         */
        private Listener listener;

        /**
         * Model for the field.
         */
        final DeviceConfigurationComboBoxModel model;

        /**
         * Constructs <tt>DeviceComboBoxField</tt> instance.
         * @param type the type of the configuration panel
         * @param devicePanel the container of the field.
         */
        public DeviceComboBoxField(final int type, Container devicePanel)
        {
            model = new DeviceConfigurationComboBoxModel(
                mediaService.getDeviceConfiguration(),
                type);

            if(!OSUtils.IS_WINDOWS
                || type != DeviceConfigurationComboBoxModel.VIDEO)
            {
                deviceComboBox = new JComboBox();
                deviceComboBox.setEditable(false);
                deviceComboBox.setModel(model);
                devicePanel.add(deviceComboBox);
                deviceComponent = deviceComboBox;
            }
            else
            {
                deviceList = new JList();
                deviceList.setModel(model);
                JScrollPane listScroller = new JScrollPane(deviceList);
                listScroller.setPreferredSize(new Dimension(200, 38));
                deviceList.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
                deviceList.setLayoutOrientation(JList.VERTICAL);
                deviceList.setVisibleRowCount(-1);
                deviceList.setSelectedValue(model.getSelectedItem(), true);
                devicePanel.add(listScroller);
                deviceComponent = deviceList;
            }
        }

        /**
         * Returns the field component
         * @return the field component
         */
        public Component getComponent()
        {
            return deviceComponent;
        }

        /**
         * Returns the selected device
         * @return the selected device
         */
        public Object getSelectedItem()
        {
            return (deviceComboBox != null)?
                deviceComboBox.getSelectedItem() : deviceList.getSelectedValue();
        }

        /**
         * Adds a listener to the field.
         * @param listener the listener to be added.
         */
        public void addListener(final Listener listener)
        {
            this.listener = listener;
            if(deviceComboBox != null)
            {
                deviceComboBox.addActionListener(new ActionListener()
                {

                    @Override
                    public void actionPerformed(ActionEvent e)
                    {
                        listener.onAction();
                    }
                });
            }
            else
            {
                deviceList.addListSelectionListener(new ListSelectionListener()
                {
                    @Override
                    public void valueChanged(ListSelectionEvent e)
                    {
                        model.setSelectedItem(deviceList.getSelectedValue());

                        // the list is still changing, but we're only
                        // interested in the final state
                        if (e.getValueIsAdjusting())
                        {
                            return;
                        }

                        listener.onAction();
                    }
                });
            }
        }

        /**
         * Interface for the listener attached to the field.
         */
        public static interface Listener
        {
            public void onAction();
        }
    }

    /**
     * Indicates if the Devices settings configuration tab
     * should be disabled, i.e. not visible to the user.
     */
    private static final String DEVICES_DISABLED_PROP
        = "net.java.sip.communicator.impl.neomedia.devicesconfig.DISABLED";

    /**
     * Indicates if the Audio/Video encodings configuration tab
     * should be disabled, i.e. not visible to the user.
     */
    private static final String ENCODINGS_DISABLED_PROP
        = "net.java.sip.communicator.impl.neomedia.encodingsconfig.DISABLED";

    /**
     * The <tt>Logger</tt> used by the <tt>MediaConfigurationServiceImpl</tt>
     * class for logging output.
     */
    private static final Logger logger
        = Logger.getLogger(MediaConfigurationImpl.class);

    /**
     * The <tt>MediaService</tt> implementation used by
     * <tt>MediaConfigurationImpl</tt>.
     */
    private static final MediaServiceImpl mediaService
        = NeomediaActivator.getMediaServiceImpl();

    /**
     * The name of the sound file used to test the playback and the notification
     * devices.
     */
    private static final String TEST_SOUND_FILENAME_PROP
        = "net.java.sip.communicator.impl.neomedia.TestSoundFilename";

    /**
     * Indicates if the Video/More Settings configuration tab
     * should be disabled, i.e. not visible to the user.
     */
    private static final String VIDEO_MORE_SETTINGS_DISABLED_PROP
        = "net.java.sip.communicator.impl.neomedia.videomoresettingsconfig.DISABLED";

    /**
     * The preferred width of all panels.
     */
    private final static int WIDTH = 350;

    /**
     * Creates the video advanced settings.
     *
     * @return video advanced settings panel.
     */
    private static Component createVideoAdvancedSettings()
    {
        ResourceManagementService resources = NeomediaActivator.getResources();

        final DeviceConfiguration deviceConfig =
            mediaService.getDeviceConfiguration();

        TransparentPanel centerPanel =
            new TransparentPanel(new GridBagLayout());
        centerPanel.setMaximumSize(new Dimension(WIDTH, 150));

        JButton resetDefaultsButton = new JButton(
            resources.getI18NString(
                    "impl.media.configform.VIDEO_RESET"));
        JPanel resetButtonPanel = new TransparentPanel(
                new FlowLayout(FlowLayout.RIGHT));
        resetButtonPanel.add(resetDefaultsButton);

        final JPanel centerAdvancedPanel
            = new TransparentPanel(new BorderLayout());
        centerAdvancedPanel.add(centerPanel, BorderLayout.NORTH);
        centerAdvancedPanel.add(resetButtonPanel, BorderLayout.SOUTH);

        GridBagConstraints constraints = new GridBagConstraints();
        constraints.fill = GridBagConstraints.HORIZONTAL;
        constraints.anchor = GridBagConstraints.NORTHWEST;
        constraints.insets = new Insets(5, 5, 0, 0);
        constraints.gridx = 0;
        constraints.weightx = 0;
        constraints.weighty = 0;
        constraints.gridy = 0;

        centerPanel.add(new JLabel(
            resources.getI18NString("impl.media.configform.VIDEO_RESOLUTION")),
            constraints);
        constraints.gridy = 1;
        constraints.insets = new Insets(0, 0, 0, 0);
        final JCheckBox frameRateCheck = new SIPCommCheckBox(
            resources.getI18NString("impl.media.configform.VIDEO_FRAME_RATE"));
        centerPanel.add(frameRateCheck, constraints);
        constraints.gridy = 2;
        constraints.insets = new Insets(5, 5, 0, 0);
        centerPanel.add(new JLabel(
            resources.getI18NString(
                    "impl.media.configform.VIDEO_PACKETS_POLICY")),
            constraints);
        constraints.gridy = 3;
        centerPanel.add(new JLabel(
            resources.getI18NString(
                    "impl.media.configform.VIDEO_BITRATE")),
            constraints);

        constraints.weightx = 1;
        constraints.gridx = 1;
        constraints.gridy = 0;
        constraints.insets = new Insets(5, 0, 0, 5);
        Object[] resolutionValues
            = new Object[DeviceConfiguration.SUPPORTED_RESOLUTIONS.length + 1];
        System.arraycopy(DeviceConfiguration.SUPPORTED_RESOLUTIONS, 0,
                        resolutionValues, 1,
                        DeviceConfiguration.SUPPORTED_RESOLUTIONS.length);
        final JComboBox sizeCombo = new JComboBox(resolutionValues);
        sizeCombo.setRenderer(new ResolutionCellRenderer());
        sizeCombo.setEditable(false);
        centerPanel.add(sizeCombo, constraints);

        // default value is 20
        final JSpinner frameRate = new JSpinner(new SpinnerNumberModel(
            20, 5, 30, 1));
        frameRate.addChangeListener(new ChangeListener()
        {
            public void stateChanged(ChangeEvent e)
            {
                deviceConfig.setFrameRate(
                        ((SpinnerNumberModel)frameRate.getModel())
                            .getNumber().intValue());
            }
        });
        constraints.gridy = 1;
        constraints.insets = new Insets(0, 0, 0, 5);
        centerPanel.add(frameRate, constraints);

        frameRateCheck.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                if(frameRateCheck.isSelected())
                {
                    deviceConfig.setFrameRate(
                        ((SpinnerNumberModel)frameRate.getModel())
                            .getNumber().intValue());
                }
                else // unlimited framerate
                    deviceConfig.setFrameRate(-1);

                frameRate.setEnabled(frameRateCheck.isSelected());
            }
        });

        int videoMaxBandwith = deviceConfig.getVideoRTPPacingThreshold();
        // Accord the current value with the maximum allowed value. Fixes
        // existing configurations that have been set to a number larger than
        // the advised maximum value.
        videoMaxBandwith = ((videoMaxBandwith > 999) ? 999 : videoMaxBandwith);

        final JSpinner videoMaxBandwidth = new JSpinner(new SpinnerNumberModel(
            videoMaxBandwith,
            1, 999, 1));
        videoMaxBandwidth.addChangeListener(new ChangeListener()
        {
            public void stateChanged(ChangeEvent e)
            {
                deviceConfig.setVideoRTPPacingThreshold(
                        ((SpinnerNumberModel) videoMaxBandwidth.getModel())
                                .getNumber().intValue());
            }
        });
        constraints.gridx = 1;
        constraints.gridy = 2;
        constraints.insets = new Insets(0, 0, 5, 5);
        centerPanel.add(videoMaxBandwidth, constraints);

        final JSpinner videoBitrate = new JSpinner(new SpinnerNumberModel(
            deviceConfig.getVideoBitrate(),
            1, Integer.MAX_VALUE, 1));
        videoBitrate.addChangeListener(new ChangeListener()
        {
            public void stateChanged(ChangeEvent e)
            {
                deviceConfig.setVideoBitrate(
                        ((SpinnerNumberModel) videoBitrate.getModel())
                                .getNumber().intValue());
            }
        });
        constraints.gridy = 3;
        centerPanel.add(videoBitrate, constraints);

        resetDefaultsButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                // reset to defaults
                sizeCombo.setSelectedIndex(0);
                frameRateCheck.setSelected(false);
                frameRate.setEnabled(false);
                frameRate.setValue(20);
                // unlimited framerate
                deviceConfig.setFrameRate(-1);
                videoMaxBandwidth.setValue(
                        DeviceConfiguration.DEFAULT_VIDEO_RTP_PACING_THRESHOLD);
                videoBitrate.setValue(
                        DeviceConfiguration.DEFAULT_VIDEO_BITRATE);
            }
        });

        // load selected value or auto
        Dimension videoSize = deviceConfig.getVideoSize();

        if((videoSize.getHeight() != DeviceConfiguration.DEFAULT_VIDEO_HEIGHT)
                && (videoSize.getWidth()
                        != DeviceConfiguration.DEFAULT_VIDEO_WIDTH))
            sizeCombo.setSelectedItem(deviceConfig.getVideoSize());
        else
            sizeCombo.setSelectedIndex(0);
        sizeCombo.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                Dimension selectedVideoSize
                    = (Dimension) sizeCombo.getSelectedItem();

                if(selectedVideoSize == null)
                {
                    // the auto value, default one
                    selectedVideoSize
                        = new Dimension(
                                DeviceConfiguration.DEFAULT_VIDEO_WIDTH,
                                DeviceConfiguration.DEFAULT_VIDEO_HEIGHT);
                }
                deviceConfig.setVideoSize(selectedVideoSize);
            }
        });

        frameRateCheck.setSelected(
            deviceConfig.getFrameRate()
                != DeviceConfiguration.DEFAULT_VIDEO_FRAMERATE);
        frameRate.setEnabled(frameRateCheck.isSelected());

        if(frameRate.isEnabled())
            frameRate.setValue(deviceConfig.getFrameRate());

        return centerAdvancedPanel;
    }

    /**
     * Creates the video container.
     * @param noVideoComponent the container component.
     * @return the video container.
     */
    private static JComponent createVideoContainer(Component noVideoComponent)
    {
        return new VideoContainer(noVideoComponent, false);
    }

    /**
     * Creates preview for the (video) device in the video container.
     *
     * @param device the device
     * @param videoContainer the video container
     * @throws IOException a problem accessing the device
     * @throws MediaException a problem getting preview
     */
    private static void createVideoPreview(
            CaptureDeviceInfo device,
            JComponent videoContainer)
        throws IOException,
               MediaException
    {
        videoContainer.removeAll();

        videoContainer.revalidate();
        videoContainer.repaint();

        if (device == null)
            return;

        for (MediaDevice mediaDevice
                : mediaService.getDevices(MediaType.VIDEO, MediaUseCase.ANY))
        {
            if(((MediaDeviceImpl) mediaDevice).getCaptureDeviceInfo().equals(
                    device))
            {
                Dimension videoContainerSize
                    = videoContainer.getPreferredSize();
                Component preview
                    = (Component)
                        mediaService.getVideoPreviewComponent(
                                mediaDevice,
                                videoContainerSize.width,
                                videoContainerSize.height);

                if (preview != null)
                    videoContainer.add(preview);
                break;
            }
        }
    }

    /**
     * The mnemonic for a type.
     * @param type audio or video type.
     * @return the mnemonic.
     */
    private static char getDisplayedMnemonic(int type)
    {
        switch (type)
        {
        case DeviceConfigurationComboBoxModel.AUDIO:
            return NeomediaActivator.getResources().getI18nMnemonic(
                "impl.media.configform.AUDIO");
        case DeviceConfigurationComboBoxModel.VIDEO:
            return NeomediaActivator.getResources().getI18nMnemonic(
                "impl.media.configform.VIDEO");
        default:
            throw new IllegalArgumentException("type");
        }
    }

    /**
     * A label for a type.
     * @param type the type.
     * @return the label.
     */
    private static String getLabelText(int type)
    {
        switch (type)
        {
        case DeviceConfigurationComboBoxModel.AUDIO:
            return NeomediaActivator.getResources().getI18NString(
                "impl.media.configform.AUDIO");
        case DeviceConfigurationComboBoxModel.AUDIO_CAPTURE:
            return NeomediaActivator.getResources().getI18NString(
                "impl.media.configform.AUDIO_IN");
        case DeviceConfigurationComboBoxModel.AUDIO_NOTIFY:
            return NeomediaActivator.getResources().getI18NString(
                "impl.media.configform.AUDIO_NOTIFY");
        case DeviceConfigurationComboBoxModel.AUDIO_PLAYBACK:
            return NeomediaActivator.getResources().getI18NString(
                "impl.media.configform.AUDIO_OUT");
        case DeviceConfigurationComboBoxModel.VIDEO:
            return NeomediaActivator.getResources().getI18NString(
                "impl.media.configform.VIDEO");
        default:
            throw new IllegalArgumentException("type");
        }
    }

    /**
     * Used to move encoding options.
     *
     * @param table the table with encodings
     * @param up move direction.
     */
    private static void move(JTable table, boolean up)
    {
        int index
            = ((EncodingConfigurationTableModel) table.getModel()).move(
                    table.getSelectedRow(),
                    up);

        table.getSelectionModel().setSelectionInterval(index, index);
    }

    /**
     * The thread which updates the capture device as selected by the user. This
     * prevent the UI to lock while changing the device.
     */
    private AudioLevelListenerThread audioLevelListenerThread = null;

    /**
     * The button used to play a sound in order to test notification devices.
     */
    private JButton notificationPlaySoundButton;

    /**
     * The combo box used to selected the notification device.
     */
    private JComboBox notifyCombo;

    /**
     * The combo box used to selected the playback device.
     */
    private JComboBox playbackCombo;

    /**
     * The button used to play a sound in order to test playback device.
     */
    private JButton playbackPlaySoundButton;

    /**
     * Indicates that one of the contained in this panel buttons has been
     * clicked.
     * @param e the <tt>ActionEvent</tt> that notified us
     */
    public void actionPerformed(ActionEvent e)
    {
        boolean isPlaybackEvent = (e.getSource() == playbackPlaySoundButton);

        // If the user clicked on one pley sound button.
        if(isPlaybackEvent
                || e.getSource() == notificationPlaySoundButton)
        {
            AudioNotifierService audioNotifServ
                = NeomediaActivator.getAudioNotifierService();
            String testSoundFilename
                = NeomediaActivator.getConfigurationService()
                    .getString(
                            TEST_SOUND_FILENAME_PROP,
                            NeomediaActivator.getResources().getSoundPath(
                                "TEST_SOUND")
                            );
            SCAudioClip sound = audioNotifServ.createAudio(
                    testSoundFilename,
                    isPlaybackEvent);
            sound.play();
        }
        // If the selected item of the playback or notify combobox has changed.
        else if(e.getSource() == playbackCombo
                || e.getSource() == notifyCombo)
        {
            DeviceConfigurationComboBoxModel.CaptureDevice device
                = (DeviceConfigurationComboBoxModel.CaptureDevice)
                    ((JComboBox) e.getSource()).getSelectedItem();

            boolean isEnabled = (device.info != null);
            if(e.getSource() == playbackCombo)
            {
                playbackPlaySoundButton.setEnabled(isEnabled);
            }
            else
            {
                notificationPlaySoundButton.setEnabled(isEnabled);
            }
        }
    }

    /**
     * Returns the audio configuration panel.
     *
     * @return the audio configuration panel
     */
    public Component createAudioConfigPanel()
    {
        return createControls(DeviceConfigurationComboBoxModel.AUDIO);
    }

    /**
     * Creates the UI controls which are to control the details of a specific
     * <tt>AudioSystem</tt>.
     *
     * @param audioSystem the <tt>A