/* * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package net.java.sip.communicator.impl.neomedia; import java.awt.*; import java.util.*; import javax.media.*; import javax.media.control.*; import javax.media.format.*; import javax.media.protocol.*; import javax.media.rtp.*; import net.java.sip.communicator.impl.neomedia.codec.*; import net.java.sip.communicator.impl.neomedia.device.*; import net.java.sip.communicator.service.neomedia.*; import net.java.sip.communicator.service.neomedia.device.*; import net.java.sip.communicator.service.neomedia.event.*; import net.java.sip.communicator.util.*; /** * Extends MediaStreamImpl in order to provide an implementation of * VideoMediaStream. * * @author Lubomir Marinov * @author Sébastien Vincent */ public class VideoMediaStreamImpl extends MediaStreamImpl implements VideoMediaStream { /** * The Logger used by the VideoMediaStreamImpl class and * its instances for logging output. */ private static final Logger logger = Logger.getLogger(VideoMediaStreamImpl.class); /** * JMF stores CUSTOM_CODEC_FORMATS statically, so they only need to * be registered once. FMJ does this dynamically (per instance), so it needs * to be done for every time we instantiate an RTP manager. */ private static boolean formatsRegisteredOnce = false; /** * Negociated output size of the video stream. * It may need to scale original capture device stream. */ private Dimension outputSize = null; /** * Selects the VideoFormat from the list of supported formats of a * specific video DataSource which has a size as close as possible * to a specific size and sets it as the format of the specified video * DataSource. * * @param videoDS the video DataSource which is to have its * supported formats examined and its format changed to the * VideoFormat which is as close as possible to the specified * preferredWidth and preferredHeight * @param preferredWidth the width of the VideoFormat to be * selected * @param preferredHeight the height of the VideoFormat to be * selected * @return the size of the VideoFormat from the list of supported * formats of videoDS which is as close as possible to * preferredWidth and preferredHeight and which has been * set as the format of videoDS */ public static Dimension selectVideoSize( DataSource videoDS, final int preferredWidth, final int preferredHeight) { if (videoDS == null) return null; FormatControl formatControl = (FormatControl) videoDS.getControl(FormatControl.class.getName()); if (formatControl == null) return null; Format[] formats = formatControl.getSupportedFormats(); final int count = formats.length; if (count < 1) return null; VideoFormat selectedFormat = null; if (count == 1) selectedFormat = (VideoFormat) formats[0]; else { class FormatInfo { public final VideoFormat format; public final double difference; public FormatInfo(VideoFormat format) { this.format = format; Dimension size = format.getSize(); int width = (size == null) ? 0 : size.width; double xScale; if (width == 0) xScale = Double.POSITIVE_INFINITY; else if (width == preferredWidth) xScale = 1; else xScale = (preferredWidth / (double) width); int height = (size == null) ? 0 : size.height; double yScale; if (height == 0) yScale = Double.POSITIVE_INFINITY; else if (height == preferredHeight) yScale = 1; else yScale = (preferredHeight / (double) height); difference = Math.abs(1 - Math.min(xScale, yScale)); } } FormatInfo[] infos = new FormatInfo[count]; for (int i = 0; i < count; i++) { FormatInfo info = infos[i] = new FormatInfo((VideoFormat) formats[i]); if (info.difference == 0) { selectedFormat = info.format; break; } } if (selectedFormat == null) { Arrays.sort(infos, new Comparator() { public int compare(FormatInfo info0, FormatInfo info1) { return Double.compare(info0.difference, info1.difference); } }); selectedFormat = infos[0].format; } // If videoDS states to support any size, use the preferred one. if ((selectedFormat != null) && (selectedFormat.getSize() == null)) { VideoFormat currentFormat = (VideoFormat) formatControl.getFormat(); int width = preferredWidth; int height = preferredHeight; // Try to preserve the aspect ratio if (currentFormat != null) { Dimension currentSize = currentFormat.getSize(); if ((currentSize != null) && (currentSize.width > 0) && (currentSize.height > 0)) height = (int) (width * (currentSize.width / (double) currentSize.height)); } selectedFormat = (VideoFormat) selectedFormat .intersects( new VideoFormat( null, new Dimension(width, height), Format.NOT_SPECIFIED, null, Format.NOT_SPECIFIED)); } } Format setFormat = formatControl.setFormat(selectedFormat); return (setFormat instanceof VideoFormat) ? ((VideoFormat) setFormat).getSize() : null; } /** * The VideoListener which handles VideoEvents from the * MediaDeviceSession of this instance and fires respective * VideoEvents from this VideoMediaStream to its * VideoListeners. */ private VideoListener deviceSessionVideoListener; /** * The facility which aids this instance in managing a list of * VideoListeners and firing VideoEvents to them. */ private final VideoNotifierSupport videoNotifierSupport = new VideoNotifierSupport(this); /** * Initializes a new VideoMediaStreamImpl instance which will use * the specified MediaDevice for both capture and playback of video * exchanged via the specified StreamConnector. * * @param connector the StreamConnector the new instance is to use * for sending and receiving video * @param device the MediaDevice the new instance is to use for * both capture and playback of video exchanged via the specified * StreamConnector * @param zrtpControl a control which is already created, used to control * the zrtp operations. */ public VideoMediaStreamImpl(StreamConnector connector, MediaDevice device, ZrtpControlImpl zrtpControl) { super(connector, device, zrtpControl); } /** * Adds a specific VideoListener to this VideoMediaStream * in order to receive notifications when visual/video Components * are being added and removed. *

* Adding a listener which has already been added does nothing i.e. it is * not added more than once and thus does not receive one and the same * VideoEvent multiple times. *

* * @param listener the VideoListener to be notified when * visual/video Components are being added or removed in this * VideoMediaStream */ public void addVideoListener(VideoListener listener) { videoNotifierSupport.addVideoListener(listener); } /** * Performs any optional configuration on the BufferControl of the * specified RTPManager which is to be used as the * RTPManager of this MediaStreamImpl. * * @param rtpManager the RTPManager which is to be used by this * MediaStreamImpl * @param bufferControl the BufferControl of rtpManager on * which any optional configuration is to be performed */ @Override protected void configureRTPManagerBufferControl( RTPManager rtpManager, BufferControl bufferControl) { bufferControl.setBufferLength(BufferControl.MAX_VALUE); } /** * Creates the visual Component depicting the video being streamed * from the local peer to the remote peer. * * @return the visual Component depicting the video being streamed * from the local peer to the remote peer if it was immediately created or * null if it was not immediately created and it is to be delivered * to the currently registered VideoListeners in a * VideoEvent with type {@link VideoEvent#VIDEO_ADDED} and origin * {@link VideoEvent#LOCAL} */ public Component createLocalVisualComponent() { MediaDeviceSession deviceSession = getDeviceSession(); return (deviceSession instanceof VideoMediaDeviceSession) ? ((VideoMediaDeviceSession) deviceSession) .createLocalVisualComponent() : null; } /** * Notifies this MediaStream that the MediaDevice (and * respectively the MediaDeviceSession with it) which this instance * uses for capture and playback of media has been changed. Makes sure that * the VideoListeners of this instance get VideoEvents for * the new/current VideoMediaDeviceSession and not for the old one. * * @param oldValue the MediaDeviceSession with the * MediaDevice this instance used work with * @param newValue the MediaDeviceSession with the * MediaDevice this instance is to work with * @see MediaStreamImpl#deviceSessionChanged(MediaDeviceSession, * MediaDeviceSession) */ @Override protected void deviceSessionChanged( MediaDeviceSession oldValue, MediaDeviceSession newValue) { super.deviceSessionChanged(oldValue, newValue); if ((oldValue instanceof VideoMediaDeviceSession) && (deviceSessionVideoListener != null)) ((VideoMediaDeviceSession) oldValue) .removeVideoListener(deviceSessionVideoListener); if (newValue instanceof VideoMediaDeviceSession) { if (deviceSessionVideoListener == null) deviceSessionVideoListener = new VideoListener() { /** * Notifies that a visual Component representing * video has been added to the provider this listener has * been added to. * * @param e a VideoEvent describing the added * visual Component representing video and the * provider it was added into * @see VideoListener#videoAdded(VideoEvent) */ public void videoAdded(VideoEvent e) { if (fireVideoEvent( e.getType(), e.getVisualComponent(), e.getOrigin())) e.consume(); } /** * Notifies that a visual Component representing * video has been removed from the provider this listener * has been added to. * * @param e a VideoEvent describing the removed * visual Component representing video and the * provider it was removed from * @see VideoListener#videoRemoved(VideoEvent) */ public void videoRemoved(VideoEvent e) { videoAdded(e); } public void videoUpdate(VideoEvent e) { fireVideoEvent(e); } }; ((VideoMediaDeviceSession) newValue) .addVideoListener(deviceSessionVideoListener); } } /** * Disposes of the visual Component of the local peer. */ public void disposeLocalVisualComponent() { MediaDeviceSession deviceSession = getDeviceSession(); if(deviceSession instanceof VideoMediaDeviceSession) ((VideoMediaDeviceSession) deviceSession) .disposeLocalVisualComponent(); } /** * Notifies the VideoListeners registered with this * VideoMediaStream about a specific type of change in the * availability of a specific visual Component depicting video. * * @param type the type of change as defined by VideoEvent in the * availability of the specified visual Component depicting video * @param visualComponent the visual Component depicting video * which has been added or removed in this VideoMediaStream * @param origin {@link VideoEvent#LOCAL} if the origin of the video is * local (e.g. it is being locally captured); {@link VideoEvent#REMOTE} if * the origin of the video is remote (e.g. a remote peer is streaming it) * @return true if this event and, more specifically, the visual * Component it describes have been consumed and should be * considered owned, referenced (which is important because * Components belong to a single Container at a time); * otherwise, false */ protected boolean fireVideoEvent( int type, Component visualComponent, int origin) { if (logger.isTraceEnabled()) logger .trace( "Firing VideoEvent with type " + VideoEvent.typeToString(type) + " and origin " + VideoEvent.originToString(origin)); return videoNotifierSupport.fireVideoEvent(type, visualComponent, origin); } /** * Notifies the VideoListeners registered with this instance about * a specific VideoEvent. * * @param event the VideoEvent to be fired to the * VideoListeners registered with this instance */ protected void fireVideoEvent(VideoEvent event) { videoNotifierSupport.fireVideoEvent(event); } /** * Returns a reference to the visual Component where video from the * remote peer is being rendered or null if no video is currently * rendered. * * @return a reference to the visual Component where video from * the remote peer is being rendered or null if no video is * currently rendered */ public Component getVisualComponent() { MediaDeviceSession deviceSession = getDeviceSession(); return (deviceSession instanceof VideoMediaDeviceSession) ? ((VideoMediaDeviceSession) deviceSession).getVisualComponent() : null; } /** * Registers {@link Constants#H264_RTP} with a specific RTPManager. * * @param rtpManager the RTPManager to register * {@link Constants#H264_RTP} with * @see MediaStreamImpl#registerCustomCodecFormats(RTPManager) */ @Override protected void registerCustomCodecFormats(RTPManager rtpManager) { super.registerCustomCodecFormats(rtpManager); // if we have already registered custom formats and we are running JMF // we bail out. if (!FMJConditionals.REGISTER_FORMATS_WITH_EVERY_RTP_MANAGER && formatsRegisteredOnce) return; // We do not have formats to register right now. formatsRegisteredOnce = true; } /** * Removes a specific VideoListener from this * VideoMediaStream in order to have to no longer receive * notifications when visual/video Components are being added and * removed. * * @param listener the VideoListener to no longer be notified when * visual/video Components are being added or removed in this * VideoMediaStream */ public void removeVideoListener(VideoListener listener) { videoNotifierSupport.removeVideoListener(listener); } /** * Sets the MediaDevice that this stream should use to play back * and capture media. *

* Note: Also resets any previous direction set with * {@link #setDirection(MediaDirection)} to the direction of the specified * MediaDevice. *

* * @param device the MediaDevice that this stream should use to * play back and capture media * @see MediaStream#setDevice(MediaDevice) */ public void setDevice(MediaDevice device) { super.setDevice(device); ((VideoMediaDeviceSession)deviceSession).setOutputSize(outputSize); } /** * Set negociated output size. * * @param size output size of video stream */ public void setOutputSize(Dimension size) { outputSize = size; } }