/* * 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.service.protocol.media; import java.beans.*; import java.lang.ref.*; import java.util.*; import org.jitsi.service.neomedia.*; import org.jitsi.service.neomedia.device.*; import org.jitsi.util.*; import org.jitsi.util.event.*; import net.java.sip.communicator.service.protocol.*; /** * Extends <tt>CallConference</tt> to represent the media-specific information * associated with the telephony conference-related state of a * <tt>MediaAwareCall</tt>. * * @author Lyubomir Marinov */ public class MediaAwareCallConference extends CallConference { /** * The <tt>PropertyChangeListener</tt> which will listen to the * <tt>MediaService</tt> about <tt>PropertyChangeEvent</tt>s. */ private static WeakPropertyChangeListener mediaServicePropertyChangeListener; /** * The <tt>MediaDevice</tt>s indexed by <tt>MediaType</tt> ordinal which are * to be used by this telephony conference for media capture and/or * playback. If the <tt>MediaDevice</tt> for a specific <tt>MediaType</tt> * is <tt>null</tt>, * {@link MediaService#getDefaultDevice(MediaType, MediaUseCase)} is called. */ private final MediaDevice[] devices; /** * The <tt>MediaDevice</tt>s which implement media mixing on the respective * <tt>MediaDevice</tt> in {@link #devices} for the purposes of this * telephony conference. */ private final MediaDevice[] mixers; /** * The <tt>VolumeControl</tt> implementation which is to control the volume * (level) of the audio played back the telephony conference represented by * this instance. */ private final VolumeControl outputVolumeControl = new BasicVolumeControl( VolumeControl.PLAYBACK_VOLUME_LEVEL_PROPERTY_NAME); /** * The <tt>PropertyChangeListener</tt> which listens to sources of * <tt>PropertyChangeEvent</tt>s on behalf of this instance. */ private final PropertyChangeListener propertyChangeListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent ev) { MediaAwareCallConference.this.propertyChange(ev); } }; /** * The <tt>RTPTranslator</tt> which forwards video RTP and RTCP traffic * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in * this telephony conference when the local peer is acting as a conference * focus. */ private RTPTranslator videoRTPTranslator; /** * The <tt>RTPTranslator</tt> which forwards autio RTP and RTCP traffic * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in * this telephony conference when the local peer is acting as a conference * focus. */ private RTPTranslator audioRTPTranslator; /** * The indicator which determines whether the telephony conference * represented by this instance is mixing or relaying. * By default what can be mixed is mixed (audio) and rest is relayed. */ private boolean translator = false; /** * Initializes a new <tt>MediaAwareCallConference</tt> instance. */ public MediaAwareCallConference() { this(false); } /** * Initializes a new <tt>MediaAwareCallConference</tt> instance which is to * optionally utilize the Jitsi Videobridge server-side telephony * conferencing technology. * * @param jitsiVideobridge <tt>true</tt> if the telephony conference * represented by the new instance is to utilize the Jitsi Videobridge * server-side telephony conferencing technology; otherwise, <tt>false</tt> */ public MediaAwareCallConference(boolean jitsiVideobridge) { this(jitsiVideobridge, false); } /** * Initializes a new <tt>MediaAwareCallConference</tt> instance which is to * optionally utilize the Jitsi Videobridge server-side telephony * conferencing technology. * * @param jitsiVideobridge <tt>true</tt> if the telephony conference * represented by the new instance is to utilize the Jitsi Videobridge * server-side telephony conferencing technology; otherwise, <tt>false</tt> */ public MediaAwareCallConference(boolean jitsiVideobridge, boolean translator) { super(jitsiVideobridge); this.translator = translator; int mediaTypeCount = MediaType.values().length; devices = new MediaDevice[mediaTypeCount]; mixers = new MediaDevice[mediaTypeCount]; /* * Listen to the MediaService in order to reflect changes in the user's * selection with respect to the default media device. */ addMediaServicePropertyChangeListener(propertyChangeListener); } /** * Adds a specific <tt>PropertyChangeListener</tt> to be notified about * <tt>PropertyChangeEvent</tt>s fired by the current <tt>MediaService</tt> * implementation. The implementation adds a <tt>WeakReference</tt> to the * specified <tt>listener</tt> because <tt>MediaAwareCallConference</tt> * is unable to determine when the <tt>PropertyChangeListener</tt> is to be * removed. * * @param listener the <tt>PropertyChangeListener</tt> to add */ private static synchronized void addMediaServicePropertyChangeListener( PropertyChangeListener listener) { if (mediaServicePropertyChangeListener == null) { final MediaService mediaService = ProtocolMediaActivator.getMediaService(); if (mediaService != null) { mediaServicePropertyChangeListener = new WeakPropertyChangeListener() { @Override protected void addThisToNotifier() { mediaService.addPropertyChangeListener(this); } @Override protected void removeThisFromNotifier() { mediaService.removePropertyChangeListener(this); } }; } } if (mediaServicePropertyChangeListener != null) { mediaServicePropertyChangeListener.addPropertyChangeListener( listener); } } /** * {@inheritDoc} * * If this telephony conference switches from being a conference focus to * not being such, disposes of the mixers used by this instance when it was * a conference focus */ @Override protected void conferenceFocusChanged(boolean oldValue, boolean newValue) { /* * If this telephony conference switches from being a conference * focus to not being one, dispose of the mixers used when it was a * conference focus. */ if (oldValue && !newValue) { Arrays.fill(mixers, null); /* Disposing the video translator is not needed when the conference changes as we have video and we will want to continue with the video Removed when chasing a bug where video call becomes conference call and then back again video call and the video from the conference focus side is not transmitted. if (videoRTPTranslator != null) { videoRTPTranslator.dispose(); videoRTPTranslator = null; } */ } super.conferenceFocusChanged(oldValue, newValue); } /** * {@inheritDoc} * * Disposes of <tt>this.videoRTPTranslator</tt> if the removed <tt>Call</tt> * was the last <tt>Call</tt> in this <tt>CallConference</tt>. * * @param call the <tt>Call</tt> which has been removed from the list of * <tt>Call</tt>s participating in this telephony conference. */ @Override protected void callRemoved(Call call) { super.callRemoved(call); if (getCallCount() == 0 && (videoRTPTranslator != null)) { videoRTPTranslator.dispose(); videoRTPTranslator = null; } } /** * Gets a <tt>MediaDevice</tt> which is capable of capture and/or playback * of media of the specified <tt>MediaType</tt> and is the default choice of * the user with respect to such a <tt>MediaDevice</tt>. * * @param mediaType the <tt>MediaType</tt> in which the retrieved * <tt>MediaDevice</tt> is to capture and/or play back media * @param useCase the <tt>MediaUseCase</tt> associated with the intended * utilization of the <tt>MediaDevice</tt> to be retrieved * @return a <tt>MediaDevice</tt> which is capable of capture and/or * playback of media of the specified <tt>mediaType</tt> and is the default * choice of the user with respect to such a <tt>MediaDevice</tt> */ public MediaDevice getDefaultDevice( MediaType mediaType, MediaUseCase useCase) { int mediaTypeIndex = mediaType.ordinal(); MediaDevice device = devices[mediaTypeIndex]; MediaService mediaService = ProtocolMediaActivator.getMediaService(); if (device == null) device = mediaService.getDefaultDevice(mediaType, useCase); /* * Make sure that the device is capable of mixing in order to support * conferencing and call recording. */ if (device != null) { MediaDevice mixer = mixers[mediaTypeIndex]; if (mixer == null) { switch (mediaType) { case AUDIO: /* * TODO AudioMixer leads to very poor audio quality on * Android so do not use it unless it is really really * necessary. */ if ((!OSUtils.IS_ANDROID || isConferenceFocus()) && !this.translator /* * We can use the AudioMixer only if the device is * able to capture (because the AudioMixer will push * when the capture device pushes). */ && device.getDirection().allowsSending()) { mixer = mediaService.createMixer(device); } break; case VIDEO: if (isConferenceFocus()) mixer = mediaService.createMixer(device); break; } mixers[mediaTypeIndex] = mixer; } if (mixer != null) device = mixer; } return device; } /** * Gets the <tt>VolumeControl</tt> which controls the volume (level) of the * audio played back in the telephony conference represented by this * instance. * * @return the <tt>VolumeControl</tt> which controls the volume (level) of * the audio played back in the telephony conference represented by this * instance */ public VolumeControl getOutputVolumeControl() { return outputVolumeControl; } /** * Gets the <tt>RTPTranslator</tt> which forwards RTP and RTCP traffic * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in * this telephony conference when the local peer is acting as a conference * focus. * * @param mediaType the <tt>MediaType</tt> of the <tt>MediaStream</tt> which * RTP and RTCP traffic is to be forwarded between * @return the <tt>RTPTranslator</tt> which forwards RTP and RTCP traffic * between the <tt>CallPeer</tt>s of the <tt>Call</tt>s participating in * this telephony conference when the local peer is acting as a conference * focus */ public RTPTranslator getRTPTranslator(MediaType mediaType) { /* * XXX A mixer is created for audio even when the local peer is not a * conference focus in order to enable additional functionality. * Similarly, the videoRTPTranslator is created even when the local peer * is not a conference focus in order to enable the local peer to turn * into a conference focus at a later time. More specifically, * MediaStreamImpl is unable to accommodate an RTPTranslator after it * has created its RTPManager. Yet again like the audio mixer, we'd * better not try to use it on Android at this time because of * performance issues that might arise. */ if (MediaType.VIDEO.equals(mediaType) && (!OSUtils.IS_ANDROID || isConferenceFocus())) { if (videoRTPTranslator == null) { videoRTPTranslator = ProtocolMediaActivator .getMediaService() .createRTPTranslator(); } return videoRTPTranslator; } if (this.translator) { if(audioRTPTranslator == null) { audioRTPTranslator = ProtocolMediaActivator .getMediaService() .createRTPTranslator(); } return audioRTPTranslator; } return null; } /** * Notifies this <tt>MediaAwareCallConference</tt> about changes in the * values of the properties of sources of <tt>PropertyChangeEvent</tt>s. For * example, this instance listens to changes of the value of * {@link MediaService#DEFAULT_DEVICE} which represents the user's choice * with respect to the default audio device. * * @param ev a <tt>PropertyChangeEvent</tt> which specifies the name of the * property which had its value changed and the old and new values of that * property */ private void propertyChange(PropertyChangeEvent ev) { String propertyName = ev.getPropertyName(); if (MediaService.DEFAULT_DEVICE.equals(propertyName)) { Object source = ev.getSource(); if (source instanceof MediaService) { /* * XXX We only support changing the default audio device at the * time of this writing. */ int mediaTypeIndex = MediaType.AUDIO.ordinal(); MediaDevice mixer = mixers[mediaTypeIndex]; MediaDevice oldValue = (mixer instanceof MediaDeviceWrapper) ? ((MediaDeviceWrapper) mixer).getWrappedDevice() : null; MediaDevice newValue = devices[mediaTypeIndex]; if (newValue == null) { newValue = ProtocolMediaActivator .getMediaService() .getDefaultDevice( MediaType.AUDIO, MediaUseCase.ANY); } /* * XXX If MediaService#getDefaultDevice(MediaType, MediaUseCase) * above returns null and its earlier return value was not null, * we will not notify of an actual change in the value of the * user's choice with respect to the default audio device. */ if (oldValue != newValue) { mixers[mediaTypeIndex] = null; firePropertyChange( MediaAwareCall.DEFAULT_DEVICE, oldValue, newValue); } } } } /** * Sets the <tt>MediaDevice</tt> to be used by this telephony conference for * capture and/or playback of media of a specific <tt>MediaType</tt>. * * @param mediaType the <tt>MediaType</tt> of the media which is to be * captured and/or played back by the specified <tt>device</tt> * @param device the <tt>MediaDevice</tt> to be used by this telephony * conference for capture and/or playback of media of the specified * <tt>mediaType</tt> */ void setDevice(MediaType mediaType, MediaDevice device) { int mediaTypeIndex = mediaType.ordinal(); MediaDevice oldValue = devices[mediaTypeIndex]; /* * XXX While we know the old and the new master/wrapped devices, we * are not sure whether the mixer has been used. Anyway, we have to * report different values in order to have PropertyChangeSupport * really fire an event. */ MediaDevice mixer = mixers[mediaTypeIndex]; if (mixer instanceof MediaDeviceWrapper) oldValue = ((MediaDeviceWrapper) mixer).getWrappedDevice(); MediaDevice newValue = devices[mediaTypeIndex] = device; if (oldValue != newValue) { mixers[mediaTypeIndex] = null; firePropertyChange( MediaAwareCall.DEFAULT_DEVICE, oldValue, newValue); } } /** * Implements a <tt>PropertyChangeListener</tt> which weakly references and * delegates to specific <tt>PropertyChangeListener</tt>s and automatically * adds itself to and removes itself from a specific * <tt>PropertyChangeNotifier</tt> depending on whether there are * <tt>PropertyChangeListener</tt>s to delegate to. Thus enables listening * to a <tt>PropertyChangeNotifier</tt> by invoking * {@link PropertyChangeNotifier#addPropertyChangeListener( * PropertyChangeListener)} without * {@link PropertyChangeNotifier#removePropertyChangeListener( * PropertyChangeListener)}. */ private static class WeakPropertyChangeListener implements PropertyChangeListener { /** * The indicator which determines whether this * <tt>PropertyChangeListener</tt> has been added to {@link #notifier}. */ private boolean added = false; /** * The list of <tt>PropertyChangeListener</tt>s which are to be notified * about <tt>PropertyChangeEvent</tt>s fired by {@link #notifier}. */ private final List<WeakReference<PropertyChangeListener>> listeners = new LinkedList<WeakReference<PropertyChangeListener>>(); /** * The <tt>PropertyChangeNotifier</tt> this instance is to listen to * about <tt>PropertyChangeEvent</tt>s which are to be forwarded to * {@link #listeners}. */ private final PropertyChangeNotifier notifier; /** * Initializes a new <tt>WeakPropertyChangeListener</tt> instance. */ protected WeakPropertyChangeListener() { this(null); } /** * Initializes a new <tt>WeakPropertyChangeListener</tt> instance which * is to listen to a specific <tt>PropertyChangeNotifier</tt>. * * @param notifier the <tt>PropertyChangeNotifier</tt> the new instance * is to listen to */ public WeakPropertyChangeListener(PropertyChangeNotifier notifier) { this.notifier = notifier; } /** * Adds a specific <tt>PropertyChangeListener</tt> to the list of * <tt>PropertyChangeListener</tt>s to be notified about * <tt>PropertyChangeEvent</tt>s fired by the * <tt>PropertyChangeNotifier</tt> associated with this instance. * * @param listener the <tt>PropertyChangeListener</tt> to add */ public synchronized void addPropertyChangeListener( PropertyChangeListener listener) { Iterator<WeakReference<PropertyChangeListener>> i = listeners.iterator(); boolean add = true; while (i.hasNext()) { PropertyChangeListener l = i.next().get(); if (l == null) i.remove(); else if (l.equals(listener)) add = false; } if (add && listeners.add( new WeakReference<PropertyChangeListener>(listener)) && !this.added) { addThisToNotifier(); this.added = true; } } /** * Adds this as a <tt>PropertyChangeListener</tt> to {@link #notifier}. */ protected void addThisToNotifier() { if (notifier != null) notifier.addPropertyChangeListener(this); } /** * {@inheritDoc} * * Notifies this instance about a <tt>PropertyChangeEvent</tt> fired by * {@link #notifier}. */ @Override public void propertyChange(PropertyChangeEvent ev) { PropertyChangeListener[] ls; int n; synchronized (this) { Iterator<WeakReference<PropertyChangeListener>> i = listeners.iterator(); ls = new PropertyChangeListener[listeners.size()]; n = 0; while (i.hasNext()) { PropertyChangeListener l = i.next().get(); if (l == null) i.remove(); else ls[n++] = l; } if ((n == 0) && this.added) { removeThisFromNotifier(); this.added = false; } } if (n != 0) { for (PropertyChangeListener l : ls) { if (l == null) break; else l.propertyChange(ev); } } } /** * Removes a specific <tt>PropertyChangeListener</tt> from the list of * <tt>PropertyChangeListener</tt>s to be notified about * <tt>PropertyChangeEvent</tt>s fired by the * <tt>PropertyChangeNotifier</tt> associated with this instance. * * @param listener the <tt>PropertyChangeListener</tt> to remove */ @SuppressWarnings("unused") public synchronized void removePropertyChangeListener( PropertyChangeListener listener) { Iterator<WeakReference<PropertyChangeListener>> i = listeners.iterator(); while (i.hasNext()) { PropertyChangeListener l = i.next().get(); if ((l == null) || l.equals(listener)) i.remove(); } if (this.added && (listeners.size() == 0)) { removeThisFromNotifier(); this.added = false; } } /** * Removes this as a <tt>PropertyChangeListener</tt> from * {@link #notifier}. */ protected void removeThisFromNotifier() { if (notifier != null) notifier.removePropertyChangeListener(this); } } }