/* * 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.beans.*; 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 net.java.sip.communicator.impl.neomedia.codec.video.*; import net.java.sip.communicator.impl.neomedia.device.*; import net.java.sip.communicator.impl.neomedia.format.*; import net.java.sip.communicator.impl.neomedia.transform.sdes.*; import net.java.sip.communicator.service.configuration.*; import net.java.sip.communicator.service.neomedia.*; import net.java.sip.communicator.service.neomedia.device.*; import net.java.sip.communicator.service.neomedia.format.*; import net.java.sip.communicator.util.*; import net.java.sip.communicator.util.event.*; import net.java.sip.communicator.util.swing.*; import org.json.*; /** * Implements MediaService for JMF. * * @author Lyubomir Marinov * @author Dmitri Melnikov */ public class MediaServiceImpl extends PropertyChangeNotifier implements MediaService { /** * The Logger used by the MediaServiceImpl class and its * instances for logging output. */ private static final Logger logger = Logger.getLogger(MediaServiceImpl.class); /** * The name of the boolean ConfigurationService property * which indicates whether the detection of audio CaptureDevices is * to be disabled. The default value is false i.e. the audio * CaptureDevices are detected. */ public static final String DISABLE_AUDIO_SUPPORT_PNAME = "net.java.sip.communicator.service.media.DISABLE_AUDIO_SUPPORT"; /** * The name of the boolean ConfigurationService property * which indicates whether the detection of video CaptureDevices is * to be disabled. The default value is false i.e. the video * CaptureDevices 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 devices property of MediaServiceImpl * when no MediaDevices are available. Explicitly defined in order * to reduce unnecessary allocations. */ private static final List EMPTY_DEVICES = Collections.emptyList(); /** * The CaptureDevice user choices such as the default audio and * video capture devices. */ private final DeviceConfiguration deviceConfiguration = new DeviceConfiguration(); /** * The PropertyChangeListener which listens to * {@link #deviceConfiguration}. */ private final PropertyChangeListener deviceConfigurationPropertyChangeListener = new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent event) { deviceConfigurationPropertyChange(event); } }; /** * The list of audio MediaDevices reported by this instance when * its {@link MediaService#getDevices(MediaType, MediaUseCase)} method is * called with an argument {@link MediaType#AUDIO}. */ private final List audioDevices = new ArrayList(); /** * The format-related user choices such as the enabled and disabled codecs * and the order of their preference. */ private final EncodingConfiguration encodingConfiguration = new EncodingConfiguration(); /** * The MediaFormatFactory through which MediaFormat * instances may be created for the purposes of working with the * MediaStreams created by this MediaService. */ private MediaFormatFactory formatFactory; /** * The one and only MediaDevice instance with * MediaDirection not allowing sending and MediaType equal * to AUDIO. */ private MediaDevice nonSendAudioDevice; /** * The one and only MediaDevice instance with * MediaDirection not allowing sending and MediaType equal * to VIDEO. */ private MediaDevice nonSendVideoDevice; /** * The list of video MediaDevices reported by this instance when * its {@link MediaService#getDevices(MediaType, MediaUseCase)} method is * called with an argument {@link MediaType#VIDEO}. */ private final List videoDevices = new ArrayList(); /** * 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 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 recorderListeners = new ArrayList(); /** * Initializes a new MediaServiceImpl instance. */ public MediaServiceImpl() { } /** * Create a MediaStream which will use a specific * MediaDevice for capture and playback of media. The new instance * will not have a StreamConnector at the time of its construction * and a StreamConnector will be specified later on in order to * enable the new instance to send and receive media. * * @param device the MediaDevice to be used by the new instance for * capture and playback of media * @return a newly-created MediaStream which will use the specified * device for capture and playback of media * @see MediaService#createMediaStream(MediaDevice) */ public MediaStream createMediaStream(MediaDevice device) { return createMediaStream(null, device); } /** * Creates a new MediaStream instance which will use the specified * MediaDevice for both capture and playback of media exchanged * via the specified StreamConnector. * * @param connector the StreamConnector that the new * MediaStream instance is to use for sending and receiving media * @param device the MediaDevice that the new MediaStream * instance is to use for both capture and playback of media exchanged via * the specified connector * @return a new MediaStream instance * @see MediaService#createMediaStream(StreamConnector, MediaDevice) */ public MediaStream createMediaStream( StreamConnector connector, MediaDevice device) { return createMediaStream(connector, device, null); } /** * Creates a new MediaStream instance which will use the specified * MediaDevice for both capture and playback of media exchanged * via the specified StreamConnector. * * @param connector the StreamConnector that the new * MediaStream instance is to use for sending and receiving media * @param device the MediaDevice that the new MediaStream * instance is to use for both capture and playback of media exchanged via * the specified connector * @param srtpControl a control which is already created, used to control * the srtp operations. * * @return a new MediaStream instance * @see MediaService#createMediaStream(StreamConnector, MediaDevice) */ public MediaStream createMediaStream( StreamConnector connector, MediaDevice device, SrtpControl srtpControl) { switch (device.getMediaType()) { case AUDIO: return new AudioMediaStreamImpl(connector, device, srtpControl); case VIDEO: return new VideoMediaStreamImpl(connector, device, srtpControl); default: return null; } } /** * Creates a new MediaDevice which uses a specific * MediaDevice 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 MediaDevice. For the AUDIO MediaType, * the returned device is commonly referred to as an audio mixer. The * MediaType of the returned MediaDevice is the same as * the MediaType of the specified device. * * @param device the MediaDevice which is to be used by the * returned MediaDevice to actually capture and play back media * @return a new MediaDevice instance which uses device 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 * MediaDevice 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 MediaDevice for the specified * MediaType. * * @param mediaType a MediaType value indicating the type of media * to be handled by the MediaDevice to be obtained * @param useCase the MediaUseCase to obtain the * MediaDevice list for * @return the default MediaDevice for the specified * mediaType if such a MediaDevice exists; otherwise, * null * @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; } } /* * Don't use the device in case the user has disabled all codecs for * that kind of media. */ if ((defaultDevice != null) && (defaultDevice.getSupportedFormats().isEmpty())) { defaultDevice = null; } return defaultDevice; } /** * Gets the CaptureDevice user choices such as the default audio * and video capture devices. * * @return the CaptureDevice user choices such as the default audio * and video capture devices. */ public DeviceConfiguration getDeviceConfiguration() { return deviceConfiguration; } /** * Gets a list of the MediaDevices known to this * MediaService and handling the specified MediaType. * * @param mediaType the MediaType to obtain the * MediaDevice list for * @param useCase the MediaUseCase to obtain the * MediaDevice list for * @return a new List of MediaDevices known to this * MediaService and handling the specified MediaType. The * returned List is a copy of the internal storage and, * consequently, modifications to it do not affect this instance. Despite * the fact that a new List instance is returned by each call to * this method, the MediaDevice instances are the same if they are * still known to this MediaService to be available. * @see MediaService#getDevices(MediaType, MediaUseCase) */ public List getDevices( MediaType mediaType, MediaUseCase useCase) { List cdis; List 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 publicDevices; synchronized (privateDevices) { if ((cdis == null) || (cdis.size() <= 0)) privateDevices.clear(); else { Iterator deviceIter = privateDevices.iterator(); while (deviceIter.hasNext()) { Iterator 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(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; } /** * Gets the format-related user choices such as the enabled and disabled * codecs and the order of their preference. * * @return the format-related user choices such as the enabled and disabled * codecs and the order of their preference */ public EncodingConfiguration getEncodingConfiguration() { return encodingConfiguration; } /** * Gets the MediaFormatFactory through which MediaFormat * instances may be created for the purposes of working with the * MediaStreams created by this MediaService. * * @return the MediaFormatFactory through which * MediaFormat instances may be created for the purposes of working * with the MediaStreams created by this MediaService * @see MediaService#getFormatFactory() */ public MediaFormatFactory getFormatFactory() { if (formatFactory == null) formatFactory = new MediaFormatFactoryImpl(); return formatFactory; } /** * Gets the one and only MediaDevice instance with * MediaDirection not allowing sending and MediaType equal * to AUDIO. * * @return the one and only MediaDevice instance with * MediaDirection not allowing sending and MediaType equal * to AUDIO */ private MediaDevice getNonSendAudioDevice() { if (nonSendAudioDevice == null) nonSendAudioDevice = new AudioMediaDeviceImpl(); return nonSendAudioDevice; } /** * Gets the one and only MediaDevice instance with * MediaDirection not allowing sending and MediaType equal * to VIDEO. * * @return the one and only MediaDevice instance with * MediaDirection not allowing sending and MediaType equal * to VIDEO */ private MediaDevice getNonSendVideoDevice() { if (nonSendVideoDevice == null) nonSendVideoDevice = new MediaDeviceImpl(MediaType.VIDEO); return nonSendVideoDevice; } /** * Starts this MediaService implementation and thus makes it * operational. */ void start() { deviceConfiguration.initialize(); deviceConfiguration.addPropertyChangeListener( deviceConfigurationPropertyChangeListener); encodingConfiguration.initialize(); } /** * Stops this MediaService implementation and thus signals that its * utilization should cease. */ void stop() { deviceConfiguration.removePropertyChangeListener( deviceConfigurationPropertyChangeListener); } /** * Initializes a new ZrtpControl instance which is to control all * ZRTP options. * * @return a new ZrtpControl instance which is to control all ZRTP * options */ public ZrtpControl createZrtpControl() { return new ZrtpControlImpl(); } /** * Initializes a new SDesControl instance which is to control all * SDes options. * * @return a new SDesControl instance which is to control all SDes * options */ public SDesControl createSDesControl() { return new SDesControlImpl(); } /** * Gets the VolumeControl which controls the volume level of audio * output/playback. * * @return the VolumeControl 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 VolumeControl which controls the volume level of audio * input/capture. * * @return the VolumeControl 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 getAvailableScreenDevices() { ScreenDevice screens[] = ScreenDeviceImpl.getAvailableScreenDevice(); List screenList; if ((screens != null) && (screens.length != 0)) screenList = new ArrayList(Arrays.asList(screens)); else screenList = Collections.emptyList(); return screenList; } /** * Get default screen device. * * @return default screen device */ public ScreenDevice getDefaultScreenDevice() { List 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 Recorder instance that can be used to record a * call which captures and plays back media using a specific * MediaDevice. * * @param device the MediaDevice which is used for media capture * and playback by the call to be recorded * @return a new Recorder instance that can be used to record a * call which captures and plays back media using the specified * MediaDevice * @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 getDynamicPayloadTypePreferences() { if(dynamicPayloadTypePreferences == null) { dynamicPayloadTypePreferences = new HashMap(); /* * 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 config = NeomediaActivator.getConfigurationService(); String prefix = DYNAMIC_PAYLOAD_TYPE_PREFERENCES_PNAME_PREFIX; List propertyNames = config.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 = config.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 fmtps = new HashMap(); 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) { JLabel noPreview = new JLabel( NeomediaActivator.getResources().getI18NString( "impl.media.configform.NO_PREVIEW")); noPreview.setHorizontalAlignment(SwingConstants.CENTER); noPreview.setVerticalAlignment(SwingConstants.CENTER); final JComponent videoContainer = new VideoContainer(noPreview); 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 MediaDevice 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 MediaDevice 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, 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 MediaDevice corresponds to partial desktop streaming * device. * * @param mediaDevice MediaDevice * @return true if MediaDevice is a partial desktop streaming * device, false otherwise */ public boolean isPartialStreaming(MediaDevice mediaDevice) { MediaDeviceImpl dev = (MediaDeviceImpl)mediaDevice; CaptureDeviceInfo devInfo = dev.getCaptureDeviceInfo(); return (devInfo != null) && devInfo.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 devInfo = dev.getCaptureDeviceInfo(); if(devInfo == null) return null; MediaLocator locator = devInfo.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); ScreenDevice devs[] = ScreenDeviceImpl.getAvailableScreenDevice(); if (devs.length - 1 >= index) { Rectangle r = ((ScreenDeviceImpl)devs[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 Recorder.Listener to the list of listeners * interested in notifications from a Recorder. * * @param listener the new Recorder.Listener to be added to the * list of listeners interested in notifications from Recorders. */ public void addRecorderListener(Recorder.Listener listener) { synchronized(recorderListeners) { if(!recorderListeners.contains(listener)) recorderListeners.add(listener); } } /** * Removes an existing Recorder.Listener from the list of listeners * interested in notifications from Recorders. * * @param listener the existing Listener to be removed from the * list of listeners interested in notifications from Recorders */ public void removeRecorderListener(Recorder.Listener listener) { synchronized(recorderListeners) { recorderListeners.remove(listener); } } /** * Gives access to currently registered Recorder.Listeners. * @return currently registered Recorder.Listeners. */ public Iterator getRecorderListeners() { return recorderListeners.iterator(); } /** * Notifies this instance that the value of a property of * {@link #deviceConfiguration} has changed. * * @param event a PropertyChangeEvent 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(); if (DeviceConfiguration.AUDIO_CAPTURE_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 RTPTranslator which is to forward RTP and RTCP * traffic between multiple MediaStreams. * * @return a new RTPTranslator which is to forward RTP and RTCP * traffic between multiple MediaStreams * @see MediaService#createRTPTranslator() */ public RTPTranslator createRTPTranslator() { return new RTPTranslatorImpl(); } }