/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Distributable under LGPL license. See terms of license at gnu.org.
*/
package net.java.sip.communicator.impl.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.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.swing.*;
/**
*
* @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.getDevice(AudioSystem.CAPTURE_INDEX)
: 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;
}
}
/**
* The name of the property which specifies if the audio system interface
* is disabled.
*/
private static final String AUDIO_SYSTEM_DISABLED_PROP
= "net.java.sip.communicator.impl.neomedia.audiosystem.DISABLED";
/**
* 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.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());
}
});
final JSpinner videoMaxBandwidth = new JSpinner(new SpinnerNumberModel(
deviceConfig.getVideoMaxBandwidth(),
1, Integer.MAX_VALUE, 1));
videoMaxBandwidth.addChangeListener(new ChangeListener()
{
public void stateChanged(ChangeEvent e)
{
deviceConfig.setVideoMaxBandwidth(
((SpinnerNumberModel)videoMaxBandwidth.getModel())
.getNumber().intValue());
}
});
constraints.gridx = 1;
constraints.gridy = 2;
constraints.insets = new Insets(0, 0, 5, 5);
centerPanel.add(videoMaxBandwidth, 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_MAX_BANDWIDTH);
}
});
// 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>AudioSystem</tt> for which the UI controls to
* control its details are to be created
* @param container the <tt>JComponent</tt> into which the UI controls which
* are to control the details of the specified <tt>audioSystem</tt> are to
* be added
*/
public void createAudioSystemControls(
AudioSystem audioSystem,
JComponent container)
{
GridBagConstraints constraints = new GridBagConstraints();
constraints.anchor = GridBagConstraints.NORTHWEST;
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.weighty = 0;
int audioSystemFeatures = audioSystem.getFeatures();
boolean featureNotifyAndPlaybackDevices
= ((audioSystemFeatures
& AudioSystem.FEATURE_NOTIFY_AND_PLAYBACK_DEVICES)
!= 0);
constraints.gridx = 0;
constraints.insets = new Insets(3, 0, 3, 3);
constraints.weightx = 0;
constraints.gridy = 0;
container.add(new JLabel(getLabelText(
DeviceConfigurationComboBoxModel.AUDIO_CAPTURE)), constraints);
if (featureNotifyAndPlaybackDevices)
{
constraints.gridy = 2;
container.add(new JLabel(getLabelText(
DeviceConfigurationComboBoxModel.AUDIO_PLAYBACK)), constraints);
constraints.gridy = 3;
container.add(new JLabel(getLabelText(
DeviceConfigurationComboBoxModel.AUDIO_NOTIFY)), constraints);
}
constraints.gridx = 1;
constraints.insets = new Insets(3, 3, 3, 0);
constraints.weightx = 1;
JComboBox captureCombo = null;
if (featureNotifyAndPlaybackDevices)
{
captureCombo = new JComboBox();
captureCombo.setEditable(false);
captureCombo.setModel(
new DeviceConfigurationComboBoxModel(
captureCombo,
mediaService.getDeviceConfiguration(),
DeviceConfigurationComboBoxModel.AUDIO_CAPTURE));
constraints.gridy = 0;
container.add(captureCombo, constraints);
}
int anchor = constraints.anchor;
SoundLevelIndicator capturePreview
= new SoundLevelIndicator(
SimpleAudioLevelListener.MIN_LEVEL,
SimpleAudioLevelListener.MAX_LEVEL);
constraints.anchor = GridBagConstraints.CENTER;
constraints.gridy = (captureCombo == null) ? 0 : 1;
container.add(capturePreview, constraints);
constraints.anchor = anchor;
constraints.gridy = GridBagConstraints.RELATIVE;
if (featureNotifyAndPlaybackDevices)
{
playbackCombo = new JComboBox();
playbackCombo.setEditable(false);
playbackCombo.setModel(
new DeviceConfigurationComboBoxModel(
captureCombo,
mediaService.getDeviceConfiguration(),
DeviceConfigurationComboBoxModel.AUDIO_PLAYBACK));
playbackCombo.addActionListener(this);
container.add(playbackCombo, constraints);
notifyCombo = new JComboBox();
notifyCombo.setEditable(false);
notifyCombo.setModel(
new DeviceConfigurationComboBoxModel(
captureCombo,
mediaService.getDeviceConfiguration(),
DeviceConfigurationComboBoxModel.AUDIO_NOTIFY));
notifyCombo.addActionListener(this);
container.add(notifyCombo, constraints);
}
if ((AudioSystem.FEATURE_ECHO_CANCELLATION & audioSystemFeatures) != 0)
{
final SIPCommCheckBox echoCancelCheckBox
= new SIPCommCheckBox(
NeomediaActivator.getResources().getI18NString(
"impl.media.configform.ECHOCANCEL"));
/*
* First set the selected one, then add the listener in order to
* avoid saving the value when using the default one and only
* showing to user without modification.
*/
echoCancelCheckBox.setSelected(
mediaService.getDeviceConfiguration().isEchoCancel());
echoCancelCheckBox.addItemListener(
new ItemListener()
{
public void itemStateChanged(ItemEvent e)
{
mediaService.getDeviceConfiguration().setEchoCancel(
echoCancelCheckBox.isSelected());
}
});
container.add(echoCancelCheckBox, constraints);
}
if ((AudioSystem.FEATURE_DENOISE & audioSystemFeatures) != 0)
{
final SIPCommCheckBox denoiseCheckBox
= new SIPCommCheckBox(
NeomediaActivator.getResources().getI18NString(
"impl.media.configform.DENOISE"));
/*
* First set the selected one, then add the listener in order to
* avoid saving the value when using the default one and only
* showing to user without modification.
*/
denoiseCheckBox.setSelected(
mediaService.getDeviceConfiguration().isDenoise());
denoiseCheckBox.addItemListener(
new ItemListener()
{
public void itemStateChanged(ItemEvent e)
{
mediaService.getDeviceConfiguration().setDenoise(
denoiseCheckBox.isSelected());
}
});
container.add(denoiseCheckBox, constraints);
}
// Adds the play buttons for testing playback and notification devices.
constraints.gridx = 2;
constraints.insets = new Insets(3, 3, 3, 0);
constraints.weightx = 0;
if (featureNotifyAndPlaybackDevices)
{
// Playback play sound button.
constraints.gridy = 2;
playbackPlaySoundButton
= new JButton(new ImageIcon(NeomediaActivator.getResources()
.getImageInBytes(
"plugin.notificationconfig.PLAY_ICON")));
playbackPlaySoundButton.setMinimumSize(new Dimension(30,30));
playbackPlaySoundButton.setPreferredSize(new Dimension(30,30));
if(((DeviceConfigurationComboBoxModel.CaptureDevice)
playbackCombo.getSelectedItem()).info == null)
{
playbackPlaySoundButton.setEnabled(false);
}
playbackPlaySoundButton.setOpaque(false);
playbackPlaySoundButton.addActionListener(this);
container.add(playbackPlaySoundButton, constraints);
// Notification play sound button.
constraints.gridy = 3;
notificationPlaySoundButton
= new JButton(new ImageIcon(NeomediaActivator.getResources()
.getImageInBytes(
"plugin.notificationconfig.PLAY_ICON")));
notificationPlaySoundButton.setMinimumSize(new Dimension(30,30));
notificationPlaySoundButton.setPreferredSize(new Dimension(30,30));
if(((DeviceConfigurationComboBoxModel.CaptureDevice)
notifyCombo.getSelectedItem()).info == null)
{
notificationPlaySoundButton.setEnabled(false);
}
notificationPlaySoundButton.setOpaque(false);
notificationPlaySoundButton.addActionListener(this);
container.add(notificationPlaySoundButton, constraints);
}
if(audioLevelListenerThread == null)
{
audioLevelListenerThread = new AudioLevelListenerThread(
audioSystem,
captureCombo,
capturePreview);
}
else
{
audioLevelListenerThread.init(
audioSystem,
captureCombo,
capturePreview);
}
}
/**
* Creates basic controls for a type (AUDIO or VIDEO).
*
* @param type the type.
* @return the build Component.
*/
private Component createBasicControls(final int type)
{
final JComboBox deviceComboBox = new JComboBox();
deviceComboBox.setEditable(false);
deviceComboBox.setModel(
new DeviceConfigurationComboBoxModel(
deviceComboBox,
mediaService.getDeviceConfiguration(),
type));
JLabel deviceLabel = new JLabel(getLabelText(type));
deviceLabel.setDisplayedMnemonic(getDisplayedMnemonic(type));
deviceLabel.setLabelFor(deviceComboBox);
final Container devicePanel
= new TransparentPanel(new FlowLayout(FlowLayout.CENTER));
devicePanel.setMaximumSize(new Dimension(WIDTH, 25));
final boolean isAudioSystemComboDisabled
= (type == DeviceConfigurationComboBoxModel.AUDIO)
&& NeomediaActivator.getConfigurationService().getBoolean(
AUDIO_SYSTEM_DISABLED_PROP,
false);
// For audio configuration form first check if the audio system
// property is disabled.
if (!isAudioSystemComboDisabled)
{
devicePanel.add(deviceLabel);
devicePanel.add(deviceComboBox);
}
final JPanel deviceAndPreviewPanel
= new TransparentPanel(new BorderLayout());
int preferredDeviceAndPreviewPanelHeight;
switch (type)
{
case DeviceConfigurationComboBoxModel.AUDIO:
preferredDeviceAndPreviewPanelHeight
= isAudioSystemComboDisabled ? 180 : 225;
break;
case DeviceConfigurationComboBoxModel.VIDEO:
preferredDeviceAndPreviewPanelHeight = 305;
break;
default:
preferredDeviceAndPreviewPanelHeight = 0;
break;
}
if (preferredDeviceAndPreviewPanelHeight > 0)
{
deviceAndPreviewPanel.setPreferredSize(
new Dimension(WIDTH, preferredDeviceAndPreviewPanelHeight));
}
deviceAndPreviewPanel.add(devicePanel, BorderLayout.NORTH);
// For audio configuration if the audio system combo is disabled we're
// going to look directly in the device configuration and show the
// preview panel, which in this case contains audio configuration
// components.
if (isAudioSystemComboDisabled)
{
Component preview = null;
if (mediaService.getDeviceConfiguration().getAudioSystem() != null)
{
preview = createPreview(type, deviceComboBox,
deviceAndPreviewPanel.getPreferredSize());
}
if (preview != null)
{
deviceAndPreviewPanel.add(preview, BorderLayout.CENTER);
}
}
final ActionListener deviceComboBoxActionListener
= new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
boolean revalidateAndRepaint = false;
for (int i = deviceAndPreviewPanel.getComponentCount() - 1;
i >= 0;
i--)
{
Component c = deviceAndPreviewPanel.getComponent(i);
if (c != devicePanel)
{
deviceAndPreviewPanel.remove(i);
revalidateAndRepaint = true;
}
}
Component preview = null;
if ((deviceComboBox.getSelectedItem() != null)
&& (deviceComboBox.isShowing()
|| isAudioSystemComboDisabled))
{
preview
= createPreview(
type,
deviceComboBox,
deviceAndPreviewPanel.getPreferredSize());
}
if (preview != null)
{
deviceAndPreviewPanel.add(preview, BorderLayout.CENTER);
revalidateAndRepaint = true;
}
if (revalidateAndRepaint)
{
deviceAndPreviewPanel.revalidate();
deviceAndPreviewPanel.repaint();
}
}
};
deviceComboBox.addActionListener(deviceComboBoxActionListener);
/*
* We have to initialize the controls to reflect the configuration
* at the time of creating this instance. Additionally, because the
* video preview will stop when it and its associated controls
* become unnecessary, we have to restart it when the mentioned
* controls become necessary again. We'll address the two goals
* described by pretending there's a selection in the video combo
* box when the combo box in question becomes displayable.
*/
deviceComboBox.addHierarchyListener(
new HierarchyListener()
{
public void hierarchyChanged(HierarchyEvent event)
{
if ((event.getChangeFlags()
& HierarchyEvent.SHOWING_CHANGED)
!= 0)
{
SwingUtilities.invokeLater(
new Runnable()
{
public void run()
{
deviceComboBoxActionListener
.actionPerformed(null);
}
});
}
}
});
return deviceAndPreviewPanel;
}
/**
* Creates all the controls (including encoding) for a type(AUDIO or VIDEO)
*
* @param type the type.
* @return the build Component.
*/
private Component createControls(int type)
{
ConfigurationService cfg = NeomediaActivator.getConfigurationService();
Component devicesComponent = null;
Component encodingsComponent = null;
Component videoComponent = null;
int compCount = 0;
if (cfg == null || !cfg.getBoolean(DEVICES_DISABLED_PROP, false))
{
compCount++;
devicesComponent = createBasicControls(type);
}
if (cfg == null || !cfg.getBoolean(ENCODINGS_DISABLED_PROP, false))
{
compCount++;
encodingsComponent = createEncodingControls(type, null);
}
if (type == DeviceConfigurationComboBoxModel.VIDEO
&& (cfg == null
|| !cfg.getBoolean(VIDEO_MORE_SETTINGS_DISABLED_PROP, false)))
{
compCount++;
videoComponent = createVideoAdvancedSettings();
}
ResourceManagementService res = NeomediaActivator.getResources();
Container container;
// If we only have one configuration form we don't need to create
// a tabbed pane.
if (compCount < 2)
{
container = new TransparentPanel(new BorderLayout());
if (devicesComponent != null)
container.add(devicesComponent);
else if (encodingsComponent != null)
container.add(encodingsComponent);
else if (videoComponent != null)
container.add(videoComponent);
}
else
{
container = new SIPCommTabbedPane();
SIPCommTabbedPane tabbedPane = (SIPCommTabbedPane) container;
int index = 0;
if (devicesComponent != null)
{
tabbedPane.insertTab(
res.getI18NString("impl.media.configform.DEVICES"),
null,
devicesComponent,
null,
index);
index = 1;
}
if (encodingsComponent != null)
{
if (tabbedPane.getTabCount() >= 1)
index = 1;
tabbedPane.insertTab(
res.getI18NString("impl.media.configform.ENCODINGS"),
null,
encodingsComponent,
null,
index);
}
if (videoComponent != null)
{
if (tabbedPane.getTabCount() >= 2)
index = 2;
tabbedPane.insertTab(
res.getI18NString(
"impl.media.configform.VIDEO_MORE_SETTINGS"),
null,
videoComponent,
null,
index);
}
}
return container;
}
/**
* Creates Component for the encodings of type(AUDIO or VIDEO).
*
* @param type the type, either DeviceConfigurationComboBoxModel.AUDIO or
* DeviceConfigurationComboBoxModel.AUDIO
* @param encodingConfiguration The <tt>EncodingConfiguration</tt> instance
* to use. If null, it will use the current encoding configuration from
* the media service.
* @return the component.
*/
private Component createEncodingControls(int type,
EncodingConfiguration encodingConfiguration)
{
if(encodingConfiguration == null)
{
encodingConfiguration
= mediaService.getCurrentEncodingConfiguration();
}
ResourceManagementService resources = NeomediaActivator.getResources();
String key;
final JTable table = new JTable();
table.setShowGrid(false);
table.setTableHeader(null);
table.setDefaultRenderer(Object.class, new DefaultTableCellRenderer()
{
@Override
public Component getTableCellRendererComponent(JTable rtable,
Object value, boolean isSelected, boolean hasFocus, int row,
int column)
{
Component component = super.getTableCellRendererComponent(
rtable, value, isSelected, hasFocus, row, column);
component.setEnabled(rtable != null && rtable.isEnabled());
return component;
}
});
key = "impl.media.configform.UP";
final JButton upButton = new JButton(resources.getI18NString(key));
upButton.setMnemonic(resources.getI18nMnemonic(key));
upButton.setOpaque(false);
key = "impl.media.configform.DOWN";
final JButton downButton = new JButton(resources.getI18NString(key));
downButton.setMnemonic(resources.getI18nMnemonic(key));
downButton.setOpaque(false);
Container buttonBar = new TransparentPanel(new GridLayout(0, 1));
buttonBar.add(upButton);
buttonBar.add(downButton);
Container parentButtonBar = new TransparentPanel(new BorderLayout());
parentButtonBar.add(buttonBar, BorderLayout.NORTH);
table.setModel(new EncodingConfigurationTableModel(type,
encodingConfiguration));
/*
* The first column contains the check boxes which enable/disable their
* associated encodings and it doesn't make sense to make it wider than
* the check boxes.
*/
TableColumnModel tableColumnModel = table.getColumnModel();
TableColumn tableColumn = tableColumnModel.getColumn(0);
tableColumn.setMaxWidth(tableColumn.getMinWidth());
final ListSelectionListener tableSelectionListener =
new ListSelectionListener()
{
public void valueChanged(ListSelectionEvent event)
{
if (table.getSelectedRowCount() == 1)
{
int selectedRow = table.getSelectedRow();
if (selectedRow > -1)
{
upButton.setEnabled(selectedRow > 0);
downButton.setEnabled(selectedRow < (table
.getRowCount() - 1));
return;
}
}
upButton.setEnabled(false);
downButton.setEnabled(false);
}
};
table.getSelectionModel().addListSelectionListener(
tableSelectionListener);
tableSelectionListener.valueChanged(null);
ActionListener buttonListener = new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
Object source = event.getSource();
boolean up;
if (source == upButton)
up = true;
else if (source == downButton)
up = false;
else
return;
move(table, up);
}
};
upButton.addActionListener(buttonListener);
downButton.addActionListener(buttonListener);
Container container = new TransparentPanel(new BorderLayout())
{
@Override
public void setEnabled(boolean enabled)
{
super.setEnabled(enabled);
table.setEnabled(enabled);
if (enabled)
{
tableSelectionListener.valueChanged(null);
}
else
{
upButton.setEnabled(false);
downButton.setEnabled(false);
}
}
};
container.setPreferredSize(new Dimension(WIDTH, 100));
container.setMaximumSize(new Dimension(WIDTH, 100));
container.add(new JScrollPane(table), BorderLayout.CENTER);
container.add(parentButtonBar, BorderLayout.EAST);
return container;
}
/**
* Returns a component for encodings configuration for the given
* <tt>mediaType</tt>
*
* @param mediaType Either <tt>MediaType.AUDIO</tt> or
* <tt>MediaType.VIDEO</tt>
* @param encodingConfiguration The <tt>EncodingConfiguration</tt> instance
* to use. If null, it will use the current encoding configuration from
* the media service.
* @return The component for encodings configuration.
*/
public Component createEncodingControls(
MediaType mediaType,
EncodingConfiguration encodingConfiguration)
{
if(encodingConfiguration == null)
{
encodingConfiguration
= mediaService.getCurrentEncodingConfiguration();
}
int deviceConfigurationComboBoxModelType;
switch (mediaType)
{
case AUDIO:
deviceConfigurationComboBoxModelType
= DeviceConfigurationComboBoxModel.AUDIO;
break;
case VIDEO:
deviceConfigurationComboBoxModelType
= DeviceConfigurationComboBoxModel.VIDEO;
break;
default:
throw new IllegalArgumentException("mediaType");
}
return
createEncodingControls(
deviceConfigurationComboBoxModelType,
encodingConfiguration);
}
/**
* Create preview component.
* @param type type
* @param comboBox the options.
* @param prefSize the preferred size
* @return the component.
*/
private Component createPreview(
int type,
final JComboBox comboBox,
Dimension prefSize)
{
JComponent preview = null;
if (type == DeviceConfigurationComboBoxModel.AUDIO)
{
AudioSystem audioSystem = null;
// If the Audio System combo box is disabled we're looking for the
// default device configuration.
if (NeomediaActivator.getConfigurationService()
.getBoolean(AUDIO_SYSTEM_DISABLED_PROP, false)
&& mediaService.getDeviceConfiguration().getAudioSystem()
!= null)
{
audioSystem
= mediaService.getDeviceConfiguration().getAudioSystem();
}
else
{
Object selectedItem = comboBox.getSelectedItem();
if (selectedItem instanceof AudioSystem)
audioSystem = (AudioSystem) selectedItem;
}
if (audioSystem != null
&& !NoneAudioSystem.LOCATOR_PROTOCOL.equalsIgnoreCase(
audioSystem.getLocatorProtocol()))
{
preview = new TransparentPanel(new GridBagLayout());
createAudioSystemControls(audioSystem, preview);
}
else
{
AudioSystem[] availableAudioSystems
= AudioSystem.getAudioSystems();
AudioSystem[] activeAudioSystems = mediaService
.getDeviceConfiguration().getAvailableAudioSystems();
// If the only one active audio system which is "None" and there
// is(are) other(s) available audio system(s), then it means
// that the other(s) audio system(s) do(es) not have detected
// any device.
if(availableAudioSystems != null
&& availableAudioSystems.length > 1
&& activeAudioSystems != null
&& activeAudioSystems.length == 1)
{
String noAvailableAudioDevice
= NeomediaActivator.getResources().getI18NString(
"impl.media.configform.NO_AVAILABLE_AUDIO_DEVICE");
preview = new TransparentPanel(new GridBagLayout());
preview.add(new JLabel(noAvailableAudioDevice));
}
}
}
else if (type == DeviceConfigurationComboBoxModel.VIDEO)
{
JLabel noPreview
= new JLabel(
NeomediaActivator.getResources().getI18NString(
"impl.media.configform.NO_PREVIEW"));
noPreview.setHorizontalAlignment(SwingConstants.CENTER);
noPreview.setVerticalAlignment(SwingConstants.CENTER);
preview = createVideoContainer(noPreview);
preview.setPreferredSize(prefSize);
Object selectedItem = comboBox.getSelectedItem();
CaptureDeviceInfo device = null;
if (selectedItem
instanceof
DeviceConfigurationComboBoxModel.CaptureDevice)
device
= ((DeviceConfigurationComboBoxModel.CaptureDevice)
selectedItem)
.info;
Exception exception;
try
{
createVideoPreview(device, preview);
exception = null;
}
catch (IOException ex)
{
exception = ex;
}
catch (MediaException ex)
{
exception = ex;
}
if (exception != null)
{
logger.error(
"Failed to create preview for device " + device,
exception);
device = null;
}
}
return preview;
}
/**
* Returns the video configuration panel.
*
* @return the video configuration panel
*/
public Component createVideoConfigPanel()
{
return createControls(DeviceConfigurationComboBoxModel.VIDEO);
}
/**
* Returns the <tt>MediaService</tt> instance.
*
* @return the <tt>MediaService</tt> instance
*/
public MediaService getMediaService()
{
return mediaService;
}
}