Java tutorial
/* * 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 org.jitsi.videobridge; import java.beans.*; import java.io.*; import java.lang.ref.*; import java.lang.reflect.*; import java.text.*; import java.util.*; import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.ColibriConferenceIQ.Recording.*; import net.java.sip.communicator.util.*; import org.jitsi.service.configuration.*; import org.jitsi.service.libjitsi.*; import org.jitsi.service.neomedia.*; import org.jitsi.service.neomedia.recording.*; import org.jitsi.util.Logger; import org.jitsi.util.event.*; import org.jitsi.eventadmin.*; import org.json.simple.*; import org.osgi.framework.*; /** * Represents a conference in the terms of Jitsi Videobridge. * * @author Lyubomir Marinov * @author Boris Grozev * @author Hristo Terezov * @author George Politis */ public class Conference extends PropertyChangeNotifier implements PropertyChangeListener { /** * The name of the <tt>Conference</tt> property <tt>endpoints</tt> which * lists the <tt>Endpoint</tt>s participating in/contributing to the * <tt>Conference</tt>. */ public static final String ENDPOINTS_PROPERTY_NAME = Conference.class.getName() + ".endpoints"; /** * The <tt>Logger</tt> used by the <tt>Conference</tt> class and its * instances to print debug information. */ private static final Logger logger = Logger.getLogger(Conference.class); /** * The <tt>Content</tt>s of this <tt>Conference</tt>. */ private final List<Content> contents = new LinkedList<>(); /** * An instance used to save information about the endpoints of this * <tt>Conference</tt>, when media recording is enabled. */ private EndpointRecorder endpointRecorder = null; /** * The <tt>Endpoint</tt>s participating in this <tt>Conference</tt>. */ private final List<WeakReference<Endpoint>> endpoints = new LinkedList<>(); /** * The indicator which determines whether {@link #expire()} has been called * on this <tt>Conference</tt>. */ private boolean expired = false; /** * The JID of the conference focus who has initialized this instance and * from whom requests to manage this instance must come or they will be * ignored. If <tt>null</tt> value is assigned we don't care who modifies * the conference. */ private final String focus; /** * The (unique) identifier/ID of this instance. */ private final String id; /** * The world readable name of this instance if any. */ private String name; /** * The time in milliseconds of the last activity related to this * <tt>Conference</tt>. In the time interval between the last activity and * now, this <tt>Conference</tt> is considered inactive. */ private long lastActivityTime; /** * If {@link #focus} is <tt>null</tt> the value of the last known focus is * stored in this member. */ private String lastKnownFocus; /** * The <tt>PropertyChangeListener</tt> which listens to * <tt>PropertyChangeEvent</tt>s on behalf of this instance while * referencing it by a <tt>WeakReference</tt>. */ private final PropertyChangeListener propertyChangeListener = new WeakReferencePropertyChangeListener(this); /** * The <tt>RecorderEventHandler</tt> which is used to handle recording * events for this <tt>Conference</tt>. */ private RecorderEventHandlerImpl recorderEventHandler = null; /** * Whether media recording is currently enabled for this <tt>Conference</tt>. */ private boolean recording = false; /** * The directory into which files associated with media recordings * for this <tt>Conference</tt> will be stored. */ private String recordingDirectory = null; /** * The path to the directory into which files associated with media * recordings for this <tt>Conference</tt> will be stored. */ private String recordingPath = null; /** * The speech activity (representation) of the <tt>Endpoint</tt>s of this * <tt>Conference</tt>. */ private final ConferenceSpeechActivity speechActivity; /** * Maps an ID of a channel-bundle to the <tt>TransportManager</tt> instance * responsible for its transport. */ private final Map<String, IceUdpTransportManager> transportManagers = new HashMap<>(); /** * The <tt>Videobridge</tt> which has initialized this <tt>Conference</tt>. */ private final Videobridge videobridge; /** * The <tt>WebRtcpDataStreamListener</tt> which listens to the * <tt>SctpConnection</tt>s of the <tt>Endpoint</tt>s participating in this * multipoint conference in order to detect when they are ready (to fire * initial events such as the current dominant speaker in this multipoint * conference). */ private final WebRtcDataStreamListener webRtcDataStreamListener = new WebRtcDataStreamAdapter() { /** * {@inheritDoc} */ @Override public void onSctpConnectionReady(SctpConnection source) { Conference.this.sctpConnectionReady(source); } }; /** * Initializes a new <tt>Conference</tt> instance which is to represent a * conference in the terms of Jitsi Videobridge which has a specific * (unique) ID and is managed by a conference focus with a specific JID. * * @param videobridge the <tt>Videobridge</tt> on which the new * <tt>Conference</tt> instance is to be initialized * @param id the (unique) ID of the new instance to be initialized * @param focus the JID of the conference focus who has requested the * initialization of the new instance and from whom further/future requests * to manage the new instance must come or they will be ignored. * Pass <tt>null</tt> to override this safety check. */ public Conference(Videobridge videobridge, String id, String focus) { if (videobridge == null) throw new NullPointerException("videobridge"); if (id == null) throw new NullPointerException("id"); this.videobridge = videobridge; this.id = id; this.focus = focus; this.lastKnownFocus = focus; speechActivity = new ConferenceSpeechActivity(this); speechActivity.addPropertyChangeListener(propertyChangeListener); EventAdmin eventAdmin = videobridge.getEventAdmin(); if (eventAdmin != null) eventAdmin.sendEvent(EventFactory.conferenceCreated(this)); } /** * Used to send a message to a subset of endpoints in the call, primary use * case being a message that has originated from an endpoint (as opposed to * a message originating from the bridge and being sent to all endpoints in * the call, for that see broadcastMessageOnDataChannels below * * @param msg * @param endpoints */ public void sendMessageOnDataChannels(String msg, List<Endpoint> endpoints) { for (Endpoint endpoint : endpoints) { try { endpoint.sendMessageOnDataChannel(msg); } catch (IOException e) { logger.error("Failed to send message on data channel.", e); } } } /** * Broadcasts string message to all participants over default data channel. * * @param msg the message to be advertised across conference peers. */ private void broadcastMessageOnDataChannels(String msg) { sendMessageOnDataChannels(msg, getEndpoints()); } /** * Checks whether <tt>path</tt> is a valid directory for recording (creates * it if necessary). * @param path the path to the directory to check. * @return <tt>true</tt> if the directory <tt>path</tt> can be used for * media recording, <tt>false</tt> otherwise. */ private boolean checkRecordingDirectory(String path) { if (path == null || "".equals(path)) return false; File dir = new File(path); if (!dir.exists()) { dir.mkdir(); if (!dir.exists()) return false; } if (!dir.isDirectory() || !dir.canWrite()) return false; return true; } /** * Closes given {@link #transportManagers} of this <tt>Conference</tt> * and removes corresponding channel bundle. */ void closeTransportManager(TransportManager transportManager) { synchronized (transportManagers) { for (Iterator<IceUdpTransportManager> i = transportManagers.values().iterator(); i.hasNext();) { if (i.next() == transportManager) { i.remove(); // Presumably, we have a single association for // transportManager. break; } } // Close manager try { transportManager.close(); } catch (Throwable t) { logger.warn("Failed to close an IceUdpTransportManager of" + " conference " + getID() + "!", t); // The whole point of explicitly closing the // transportManagers of this Conference is to prevent memory // leaks. Hence, it does not make sense to possibly leave // TransportManagers open because a TransportManager has // failed to close. if (t instanceof InterruptedException) Thread.currentThread().interrupt(); else if (t instanceof ThreadDeath) throw (ThreadDeath) t; } } } /** * Closes the {@link #transportManagers} of this <tt>Conference</tt>. */ private void closeTransportManagers() { synchronized (transportManagers) { for (Iterator<IceUdpTransportManager> i = transportManagers.values().iterator(); i.hasNext();) { IceUdpTransportManager transportManager = i.next(); i.remove(); closeTransportManager(transportManager); } } } /** * Initializes a new <tt>String</tt> to be sent over an * <tt>SctpConnection</tt> in order to notify an <tt>Endpoint</tt> that the * dominant speaker in this multipoint conference has changed to a specific * <tt>Endpoint</tt>. * * @param dominantSpeaker the dominant speaker in this multipoint conference * @return a new <tt>String</tt> to be sent over an <tt>SctpConnection</tt> * in order to notify an <tt>Endpoint</tt> that the dominant speaker in this * multipoint conference has changed to <tt>dominantSpeaker</tt> */ private String createDominantSpeakerEndpointChangeEvent(Endpoint dominantSpeaker) { return "{\"colibriClass\":\"DominantSpeakerEndpointChangeEvent\"," + "\"dominantSpeakerEndpoint\":\"" + JSONValue.escape(dominantSpeaker.getID()) + "\"}"; } /** * Adds the channel-bundles of this <tt>Conference</tt> as * <tt>ColibriConferenceIQ.ChannelBundle</tt> instances in <tt>iq</tt>. * @param iq the <tt>ColibriConferenceIQ</tt> in which to describe. */ void describeChannelBundles(ColibriConferenceIQ iq) { synchronized (transportManagers) { for (Map.Entry<String, IceUdpTransportManager> entry : transportManagers.entrySet()) { ColibriConferenceIQ.ChannelBundle responseBundleIQ = new ColibriConferenceIQ.ChannelBundle( entry.getKey()); entry.getValue().describe(responseBundleIQ); iq.addChannelBundle(responseBundleIQ); } } } /** * Sets the values of the properties of a specific * <tt>ColibriConferenceIQ</tt> to the values of the respective * properties of this instance. Thus, the specified <tt>iq</tt> may be * thought of as a description of this instance. * <p> * <b>Note</b>: The copying of the values is deep i.e. the * <tt>Contents</tt>s of this instance are described in the specified * <tt>iq</tt>. * </p> * * @param iq the <tt>ColibriConferenceIQ</tt> to set the values of the * properties of this instance on */ public void describeDeep(ColibriConferenceIQ iq) { describeShallow(iq); if (isRecording()) { ColibriConferenceIQ.Recording recordingIQ = new ColibriConferenceIQ.Recording(State.ON.toString()); recordingIQ.setDirectory(getRecordingDirectory()); iq.setRecording(recordingIQ); } for (Content content : getContents()) { ColibriConferenceIQ.Content contentIQ = iq.getOrCreateContent(content.getName()); for (Channel channel : content.getChannels()) { if (channel instanceof SctpConnection) { ColibriConferenceIQ.SctpConnection sctpConnectionIQ = new ColibriConferenceIQ.SctpConnection(); channel.describe(sctpConnectionIQ); contentIQ.addSctpConnection(sctpConnectionIQ); } else { ColibriConferenceIQ.Channel channelIQ = new ColibriConferenceIQ.Channel(); channel.describe(channelIQ); contentIQ.addChannel(channelIQ); } } } } /** * Sets the values of the properties of a specific * <tt>ColibriConferenceIQ</tt> to the values of the respective * properties of this instance. Thus, the specified <tt>iq</tt> may be * thought of as a description of this instance. * <p> * <b>Note</b>: The copying of the values is shallow i.e. the * <tt>Content</tt>s of this instance are not described in the specified * <tt>iq</tt>. * </p> * * @param iq the <tt>ColibriConferenceIQ</tt> to set the values of the * properties of this instance on */ public void describeShallow(ColibriConferenceIQ iq) { iq.setID(getID()); iq.setName(getName()); } /** * Notifies this instance that {@link #speechActivity} has identified a * speaker switch event in this multipoint conference and there is now a new * dominant speaker. */ private void dominantSpeakerChanged() { Endpoint dominantSpeaker = speechActivity.getDominantEndpoint(); if (logger.isTraceEnabled()) { logger.trace("The dominant speaker in conference " + getID() + " is now the endpoint " + ((dominantSpeaker == null) ? "(null)" : dominantSpeaker.getID()) + "."); } if (dominantSpeaker != null) { broadcastMessageOnDataChannels(createDominantSpeakerEndpointChangeEvent(dominantSpeaker)); if (isRecording() && (recorderEventHandler != null)) recorderEventHandler.dominantSpeakerChanged(dominantSpeaker); } } /** * Notifies this instance that there was a change in the value of a property * of an <tt>Endpoint</tt> participating in this multipoint conference. * * @param endpoint the <tt>Endpoint</tt> which is the source of the * event/notification and is participating in this multipoint conference * @param ev a <tt>PropertyChangeEvent</tt> which specifies the source of * the event/notification, the name of the property and the old and new * values of that property */ private void endpointPropertyChange(Endpoint endpoint, PropertyChangeEvent ev) { String propertyName = ev.getPropertyName(); boolean maybeRemoveEndpoint; if (Endpoint.SCTP_CONNECTION_PROPERTY_NAME.equals(propertyName)) { // The SctpConnection of/associated with an Endpoint has changed. We // may want to fire initial events over that SctpConnection (as soon // as it is ready). SctpConnection oldValue = (SctpConnection) ev.getOldValue(); SctpConnection newValue = (SctpConnection) ev.getNewValue(); endpointSctpConnectionChanged(endpoint, oldValue, newValue); // The SctpConnection may have expired. maybeRemoveEndpoint = (newValue == null); } else if (Endpoint.CHANNELS_PROPERTY_NAME.equals(propertyName)) { // An RtpChannel may have expired. maybeRemoveEndpoint = true; } else { maybeRemoveEndpoint = false; } if (maybeRemoveEndpoint) { // It looks like there is a chance that the Endpoint may have // expired. Endpoints are held by this Conference via WeakReferences // but WeakReferences are unpredictable. We have functionality // though which could benefit from discovering that an Endpoint has // expired as quickly as possible (e.g. ConferenceSpeechActivity). // Consequently, try to expedite the removal of expired Endpoints. if (endpoint.getSctpConnection() == null && endpoint.getChannelCount(null) == 0) { removeEndpoint(endpoint); } } } /** * Notifies this instance that the <tt>SctpConnection</tt> of/associated * with a specific <tt>Endpoint</tt> participating in this * <tt>Conference</tt> has changed. * * @param endpoint the <tt>Endpoint</tt> participating in this * <tt>Conference</tt> which has had its (associated) * <tt>SctpConnection</tt> changed */ private void endpointSctpConnectionChanged(Endpoint endpoint, SctpConnection oldValue, SctpConnection newValue) { // We want to fire initial events (e.g. dominant speaker) over the // SctpConnection as soon as it is ready. if (oldValue != null) { oldValue.removeChannelListener(webRtcDataStreamListener); } if (newValue != null) { newValue.addChannelListener(webRtcDataStreamListener); // The SctpConnection may itself be ready already. If this is the // case, then it has now become ready for this Conference. if (newValue.isReady()) sctpConnectionReady(newValue); } } /** * Expires this <tt>Conference</tt>, its <tt>Content</tt>s and their * respective <tt>Channel</tt>s. Releases the resources acquired by this * instance throughout its life time and prepares it to be garbage * collected. */ public void expire() { synchronized (this) { if (expired) return; else expired = true; } EventAdmin eventAdmin = videobridge.getEventAdmin(); if (eventAdmin != null) eventAdmin.sendEvent(EventFactory.conferenceExpired(this)); setRecording(false); if (recorderEventHandler != null) { recorderEventHandler.close(); recorderEventHandler = null; } Videobridge videobridge = getVideobridge(); try { videobridge.expireConference(this); } finally { // Expire the Contents of this Conference. for (Content content : getContents()) { try { content.expire(); } catch (Throwable t) { logger.warn("Failed to expire content " + content.getName() + " of conference " + getID() + "!", t); if (t instanceof InterruptedException) Thread.currentThread().interrupt(); else if (t instanceof ThreadDeath) throw (ThreadDeath) t; } } // Close the transportManagers of this Conference. Normally, there // will be no TransportManager left to close at this point because // all Channels have expired and the last Channel to be removed from // a TransportManager closes the TransportManager. However, a // Channel may have expired before it has learned of its // TransportManager and then the TransportManager will not close. closeTransportManagers(); if (logger.isInfoEnabled()) { logger.info("Expired conference " + getID() + ". " + videobridge.getConferenceCountString()); } } } /** * Expires a specific <tt>Content</tt> of this <tt>Conference</tt> (i.e. if * the specified <tt>content</tt> is not in the list of <tt>Content</tt>s of * this <tt>Conference</tt>, does nothing). * * @param content the <tt>Content</tt> to be expired by this * <tt>Conference</tt> */ public void expireContent(Content content) { boolean expireContent; synchronized (contents) { if (contents.contains(content)) { contents.remove(content); expireContent = true; } else expireContent = false; } if (expireContent) content.expire(); } /** * Finds a <tt>Channel</tt> of this <tt>Conference</tt> which receives a * specific SSRC and is with a specific <tt>MediaType</tt>. * * @param receiveSSRC the SSRC of a received RTP stream whose receiving * <tt>Channel</tt> in this <tt>Conference</tt> is to be found * @param mediaType the <tt>MediaType</tt> of the <tt>Channel</tt> to be * found * @return the <tt>Channel</tt> in this <tt>Conference</tt> which receives * the specified <tt>ssrc</tt> and is with the specified <tt>mediaType</tt>; * otherwise, <tt>null</tt> */ public Channel findChannelByReceiveSSRC(long receiveSSRC, MediaType mediaType) { for (Content content : getContents()) { if (mediaType.equals(content.getMediaType())) { Channel channel = content.findChannelByReceiveSSRC(receiveSSRC); if (channel != null) return channel; } } return null; } /** * Finds an <tt>Endpoint</tt> of this <tt>Conference</tt> which sends an RTP * stream with a specific SSRC and with a specific <tt>MediaType</tt>. * * @param receiveSSRC the SSRC of an RTP stream received by this * <tt>Conference</tt> whose sending <tt>Endpoint</tt> is to be found * @param mediaType the <tt>MediaType</tt> of the RTP stream identified by * the specified <tt>ssrc</tt> * @return <tt>Endpoint</tt> of this <tt>Conference</tt> which sends an RTP * stream with the specified <tt>ssrc</tt> and with the specified * <tt>mediaType</tt>; otherwise, <tt>null</tt> */ Endpoint findEndpointByReceiveSSRC(long receiveSSRC, MediaType mediaType) { Channel channel = findChannelByReceiveSSRC(receiveSSRC, mediaType); return (channel == null) ? null : channel.getEndpoint(); } /** * Returns the OSGi <tt>BundleContext</tt> in which this Conference is * executing. * * @return the OSGi <tt>BundleContext</tt> in which the Conference is * executing. */ public BundleContext getBundleContext() { return getVideobridge().getBundleContext(); } /** * Gets the <tt>Content</tt>s of this <tt>Conference</tt>. * * @return the <tt>Content</tt>s of this <tt>Conference</tt> */ public Content[] getContents() { synchronized (contents) { return contents.toArray(new Content[contents.size()]); } } /** * Gets an <tt>Endpoint</tt> participating in this <tt>Conference</tt> which * has a specific identifier/ID. * * @param id the identifier/ID of the <tt>Endpoint</tt> which is to be * returned * @return an <tt>Endpoint</tt> participating in this <tt>Conference</tt> * which has the specified <tt>id</tt> or <tt>null</tt> */ public Endpoint getEndpoint(String id) { return getEndpoint(id, /* create */ false); } /** * Gets an <tt>Endpoint</tt> participating in this <tt>Conference</tt> which * has a specific identifier/ID. If an <tt>Endpoint</tt> participating in * this <tt>Conference</tt> with the specified <tt>id</tt> does not exist at * the time the method is invoked, the method optionally initializes a new * <tt>Endpoint</tt> instance with the specified <tt>id</tt> and adds it to * the list of <tt>Endpoint</tt>s participating in this <tt>Conference</tt>. * * @param id the identifier/ID of the <tt>Endpoint</tt> which is to be * returned * @return an <tt>Endpoint</tt> participating in this <tt>Conference</tt> * which has the specified <tt>id</tt> or <tt>null</tt> if there is no such * <tt>Endpoint</tt> and <tt>create</tt> equals <tt>false</tt> */ private Endpoint getEndpoint(String id, boolean create) { Endpoint endpoint = null; boolean changed = false; synchronized (endpoints) { for (Iterator<WeakReference<Endpoint>> i = endpoints.iterator(); i.hasNext();) { Endpoint e = i.next().get(); if (e == null) { i.remove(); changed = true; } else if (e.getID().equals(id)) { endpoint = e; } } if (create && endpoint == null) { endpoint = new Endpoint(id, this); // The propertyChangeListener will weakly reference this // Conference and will unregister itself from the endpoint // sooner or later. endpoint.addPropertyChangeListener(propertyChangeListener); endpoints.add(new WeakReference<>(endpoint)); changed = true; EventAdmin eventAdmin = videobridge.getEventAdmin(); if (eventAdmin != null) eventAdmin.sendEvent(EventFactory.endpointCreated(endpoint)); } } if (changed) firePropertyChange(ENDPOINTS_PROPERTY_NAME, null, null); return endpoint; } /** * Returns the number of <tt>Endpoint</tt>s in this <tt>Conference</tt>. * * @return the number of <tt>Endpoint</tt>s in this <tt>Conference</tt>. */ public int getEndpointCount() { return getEndpoints().size(); } /** * Returns the <tt>EndpointRecorder</tt> instance used to save the * endpoints information for this <tt>Conference</tt>. Creates an instance * if none exists. * @return the <tt>EndpointRecorder</tt> instance used to save the * endpoints information for this <tt>Conference</tt>. */ private EndpointRecorder getEndpointRecorder() { if (endpointRecorder == null) { try { endpointRecorder = new EndpointRecorder(getRecordingPath() + "/endpoints.json"); } catch (IOException ioe) { logger.warn("Could not create EndpointRecorder. " + ioe); } } return endpointRecorder; } /** * Gets the <tt>Endpoint</tt>s participating in/contributing to this * <tt>Conference</tt>. * * @return the <tt>Endpoint</tt>s participating in/contributing to this * <tt>Conference</tt> */ public List<Endpoint> getEndpoints() { List<Endpoint> endpoints; boolean changed = false; synchronized (this.endpoints) { endpoints = new ArrayList<>(this.endpoints.size()); for (Iterator<WeakReference<Endpoint>> i = this.endpoints.iterator(); i.hasNext();) { Endpoint endpoint = i.next().get(); if (endpoint == null) { i.remove(); changed = true; } else { endpoints.add(endpoint); } } } if (changed) firePropertyChange(ENDPOINTS_PROPERTY_NAME, null, null); return endpoints; } /** * Gets the JID of the conference focus who has initialized this instance * and from whom requests to manage this instance must come or they will be * ignored. * * @return the JID of the conference focus who has initialized this instance * and from whom requests to manage this instance must come or they will be * ignored */ public final String getFocus() { return focus; } /** * Gets the (unique) identifier/ID of this instance. * * @return the (unique) identifier/ID of this instance */ public final String getID() { return id; } /** * Gets the time in milliseconds of the last activity related to this * <tt>Conference</tt>. * * @return the time in milliseconds of the last activity related to this * <tt>Conference</tt> */ public long getLastActivityTime() { synchronized (this) { return lastActivityTime; } } /** * Returns the JID of the last known focus. * @return the JID of the last known focus. */ public String getLastKnowFocus() { return lastKnownFocus; } /** * Returns a <tt>MediaService</tt> implementation (if any). * * @return a <tt>MediaService</tt> implementation (if any) */ MediaService getMediaService() { MediaService mediaService = ServiceUtils.getService(getBundleContext(), MediaService.class); // TODO For an unknown reason, ServiceUtils2.getService fails to // retrieve the MediaService implementation. In the form of a temporary // workaround, get it through LibJitsi. if (mediaService == null) mediaService = LibJitsi.getMediaService(); return mediaService; } /** * Gets a <tt>Content</tt> of this <tt>Conference</tt> which has a specific * name. If a <tt>Content</tt> of this <tt>Conference</tt> with the * specified <tt>name</tt> does not exist at the time the method is invoked, * the method initializes a new <tt>Content</tt> instance with the specified * <tt>name</tt> and adds it to the list of <tt>Content</tt>s of this * <tt>Conference</tt>. * * @param name the name of the <tt>Content</tt> which is to be returned * @return a <tt>Content</tt> of this <tt>Conference</tt> which has the * specified <tt>name</tt> */ public Content getOrCreateContent(String name) { Content content; synchronized (contents) { for (Content aContent : contents) { if (aContent.getName().equals(name)) { aContent.touch(); // It seems the content is still active. return aContent; } } content = new Content(this, name); if (isRecording()) { content.setRecording(true, getRecordingPath()); } contents.add(content); } if (logger.isInfoEnabled()) { /* * The method Videobridge.getChannelCount() should better be * executed outside synchronized blocks in order to reduce the risks * of causing deadlocks. */ Videobridge videobridge = getVideobridge(); logger.info("Created content " + name + " of conference " + getID() + ". " + videobridge.getConferenceCountString()); } return content; } /** * Gets an <tt>Endpoint</tt> participating in this <tt>Conference</tt> which * has a specific identifier/ID. If an <tt>Endpoint</tt> participating in * this <tt>Conference</tt> with the specified <tt>id</tt> does not exist at * the time the method is invoked, the method initializes a new * <tt>Endpoint</tt> instance with the specified <tt>id</tt> and adds it to * the list of <tt>Endpoint</tt>s participating in this <tt>Conference</tt>. * * @param id the identifier/ID of the <tt>Endpoint</tt> which is to be * returned * @return an <tt>Endpoint</tt> participating in this <tt>Conference</tt> * which has the specified <tt>id</tt> */ public Endpoint getOrCreateEndpoint(String id) { return getEndpoint(id, /* create */ true); } RecorderEventHandler getRecorderEventHandler() { if (recorderEventHandler == null) { Throwable t; try { recorderEventHandler = new RecorderEventHandlerImpl(this, getMediaService().createRecorderEventHandlerJson(getRecordingPath() + "/metadata.json")); t = null; } catch (IOException ioe) { t = ioe; } catch (IllegalArgumentException iae) { t = iae; } if (t != null) logger.warn("Could not create RecorderEventHandler. " + t); } return recorderEventHandler; } /** * Returns the directory where the recording should be stored * * @return the directory of the new recording */ String getRecordingDirectory() { if (this.recordingDirectory == null) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd.HH-mm-ss."); this.recordingDirectory = dateFormat.format(new Date()) + getID() + ((name != null) ? "_" + name : ""); } return this.recordingDirectory; } /** * Returns the path to the directory where the media recording related files * should be saved, or <tt>null</tt> if recording is not enabled in the * configuration, or a recording path has not been configured. * * @return the path to the directory where the media recording related files * should be saved, or <tt>null</tt> if recording is not enabled in the * configuration, or a recording path has not been configured. */ String getRecordingPath() { if (recordingPath == null) { ConfigurationService cfg = getVideobridge().getConfigurationService(); if (cfg != null) { boolean recordingIsEnabled = cfg.getBoolean(Videobridge.ENABLE_MEDIA_RECORDING_PNAME, false); if (recordingIsEnabled) { String path = cfg.getString(Videobridge.MEDIA_RECORDING_PATH_PNAME, null); if (path != null) { this.recordingPath = path + "/" + this.getRecordingDirectory(); } } } } return recordingPath; } /** * Gets the speech activity (representation) of the <tt>Endpoint</tt>s of * this <tt>Conference</tt>. * * @return the speech activity (representation) of the <tt>Endpoint</tt>s of * this <tt>Conference</tt> */ public ConferenceSpeechActivity getSpeechActivity() { return speechActivity; } /** * Returns, the <tt>TransportManager</tt> instance for the channel-bundle * with ID <tt>channelBundleId</tt>, or <tt>null</tt> if one doesn't exist. * * @param channelBundleId the ID of the channel-bundle for which to return * the <tt>TransportManager</tt>. * @return the <tt>TransportManager</tt> instance for the channel-bundle * with ID <tt>channelBundleId</tt>, or <tt>null</tt> if one doesn't exist. */ TransportManager getTransportManager(String channelBundleId) { return getTransportManager(channelBundleId, false); } /** * Returns, the <tt>TransportManager</tt> instance for the channel-bundle * with ID <tt>channelBundleId</tt>. If no instance exists and * <tt>create</tt> is <tt>true</tt>, one will be created. * * @param channelBundleId the ID of the channel-bundle for which to return * the <tt>TransportManager</tt>. * @param create whether to create a new instance, if one doesn't exist. * @return the <tt>TransportManager</tt> instance for the channel-bundle * with ID <tt>channelBundleId</tt>. */ IceUdpTransportManager getTransportManager(String channelBundleId, boolean create) { IceUdpTransportManager transportManager; synchronized (transportManagers) { transportManager = transportManagers.get(channelBundleId); if (transportManager == null && create && !isExpired()) { try { //FIXME: the initiator is hard-coded // We assume rtcp-mux when bundle is used, so we make only // one component. transportManager = new IceUdpTransportManager(this, true, 1); } catch (IOException ioe) { throw new UndeclaredThrowableException(ioe); } transportManagers.put(channelBundleId, transportManager); } } return transportManager; } /** * Gets the <tt>Videobridge</tt> which has initialized this * <tt>Conference</tt>. * * @return the <tt>Videobridge</tt> which has initialized this * <tt>Conference</tt> */ public final Videobridge getVideobridge() { return videobridge; } /** * Gets the indicator which determines whether this <tt>Conference</tt> has * expired. * * @return <tt>true</tt> if this <tt>Conference</tt> has expired; otherwise, * <tt>false</tt> */ public boolean isExpired() { // Conference starts with expired equal to false and the only assignment // to expired is to set it to true so there is no need to synchronize // the reading of expired. return expired; } /** * Checks whether media recording is currently enabled for this * <tt>Conference</tt>. * @return <tt>true</tt> if media recording is currently enabled for this * <tt>Conference</tt>, false otherwise. */ public boolean isRecording() { boolean recording = this.recording; //if one of the contents is not recording, stop all recording if (recording) { synchronized (contents) { for (Content content : contents) { MediaType mediaType = content.getMediaType(); if (!MediaType.VIDEO.equals(mediaType) && !MediaType.AUDIO.equals(mediaType)) continue; if (!content.isRecording()) recording = false; } } } if (this.recording != recording) setRecording(recording); return this.recording; } /** * Notifies this instance that there was a change in the value of a property * of an object in which this instance is interested. * * @param ev a <tt>PropertyChangeEvent</tt> which specifies the object of * interest, the name of the property and the old and new values of that * property */ @Override public void propertyChange(PropertyChangeEvent ev) { Object source = ev.getSource(); if (isExpired()) { // An expired Conference is to be treated like a null Conference // i.e. it does not handle any PropertyChangeEvents. If possible, // make sure that no further PropertyChangeEvents will be delivered // to this Conference. if (source instanceof PropertyChangeNotifier) { ((PropertyChangeNotifier) source).removePropertyChangeListener(propertyChangeListener); } } else if (source == speechActivity) { speechActivityPropertyChange(ev); } else if (source instanceof Endpoint) { // We care about PropertyChangeEvents from Endpoint but only if the // Endpoint in question is still participating in this Conference. Endpoint endpoint = getEndpoint(((Endpoint) source).getID()); if (endpoint != null) endpointPropertyChange(endpoint, ev); } } /** * Removes a specific <tt>Endpoint</tt> instance from this list of * <tt>Endpoint</tt>s participating in this multipoint conference. * * @param endpoint the <tt>Endpoint</tt> to remove * @return <tt>true</tt> if the list of <tt>Endpoint</tt>s participating in * this multipoint conference changed as a result of the execution of the * method; otherwise, <tt>false</tt> */ private boolean removeEndpoint(Endpoint endpoint) { boolean removed = false; synchronized (endpoints) { for (Iterator<WeakReference<Endpoint>> i = endpoints.iterator(); i.hasNext();) { Endpoint e = i.next().get(); if (e == null || e == endpoint) { i.remove(); removed = true; } } if (endpoint != null) { endpoint.expire(); } } if (removed) firePropertyChange(ENDPOINTS_PROPERTY_NAME, null, null); return removed; } /** * Notifies this instance that a specific <tt>SctpConnection</tt> has become * ready i.e. connected to a/the remote peer and operational. * * @param sctpConnection the <tt>SctpConnection</tt> which has become ready * and is the cause of the method invocation */ private void sctpConnectionReady(SctpConnection sctpConnection) { /* * We want to fire initial events over the SctpConnection as soon as it * is ready, we do not want to fire them multiple times i.e. every time * the SctpConnection becomes ready. */ sctpConnection.removeChannelListener(webRtcDataStreamListener); if (!isExpired() && !sctpConnection.isExpired() && sctpConnection.isReady()) { Endpoint endpoint = sctpConnection.getEndpoint(); if (endpoint != null) endpoint = getEndpoint(endpoint.getID()); if (endpoint != null) { /* * It appears that this Conference, the SctpConnection and the * Endpoint are in states which allow them to fire the initial * events. */ Endpoint dominantSpeaker = speechActivity.getDominantEndpoint(); if (dominantSpeaker != null) { try { endpoint.sendMessageOnDataChannel( createDominantSpeakerEndpointChangeEvent(dominantSpeaker)); } catch (IOException e) { logger.error("Failed to send message on data channel.", e); } } /* * Determining the instant at which an SctpConnection associated * with an Endpoint becomes ready (i.e. connected to the remote * peer and operational) is a multi-step ordeal. The Conference * class implements the procedure so do not make other classes * implement it as well. */ endpoint.sctpConnectionReady(sctpConnection); } } } /** * Sets the JID of the last known focus. * * @param jid the JID of the last known focus. */ public void setLastKnownFocus(String jid) { lastKnownFocus = jid; } /** * Attempts to enable or disable media recording for this * <tt>Conference</tt>. * * @param recording whether to enable or disable recording. * @return the state of the media recording for this <tt>Conference</tt> * after the attempt to enable (or disable). */ public boolean setRecording(boolean recording) { if (recording != this.recording) { if (recording) { //try enable recording if (logger.isDebugEnabled()) { logger.debug("Starting recording for conference with id=" + getID()); } String path = getRecordingPath(); boolean failedToStart = !checkRecordingDirectory(path); if (!failedToStart) { RecorderEventHandler handler = getRecorderEventHandler(); if (handler == null) failedToStart = true; } if (!failedToStart) { EndpointRecorder endpointRecorder = getEndpointRecorder(); if (endpointRecorder == null) { failedToStart = true; } else { for (Endpoint endpoint : getEndpoints()) endpointRecorder.updateEndpoint(endpoint); } } /* * The Recorders of the Contents need to share a single * Synchronizer, we take it from the first Recorder. */ boolean first = true; Synchronizer synchronizer = null; for (Content content : contents) { MediaType mediaType = content.getMediaType(); if (!MediaType.VIDEO.equals(mediaType) && !MediaType.AUDIO.equals(mediaType)) { continue; } if (!failedToStart) failedToStart = !content.setRecording(true, path); if (failedToStart) break; if (first) { first = false; synchronizer = content.getRecorder().getSynchronizer(); } else { Recorder recorder = content.getRecorder(); if (recorder != null) recorder.setSynchronizer(synchronizer); } content.feedKnownSsrcsToSynchronizer(); } if (failedToStart) { recording = false; logger.warn("Failed to start media recording for conference " + getID()); } } // either we were asked to disable recording, or we failed to // enable it if (!recording) { if (logger.isDebugEnabled()) { logger.debug("Stopping recording for conference with id=" + getID()); } for (Content content : contents) { MediaType mediaType = content.getMediaType(); if (MediaType.AUDIO.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) { content.setRecording(false, null); } } if (recorderEventHandler != null) recorderEventHandler.close(); recorderEventHandler = null; recordingPath = null; recordingDirectory = null; if (endpointRecorder != null) endpointRecorder.close(); endpointRecorder = null; } this.recording = recording; } return this.recording; } /** * Notifies this <tt>Conference</tt> that the ordered list of * <tt>Endpoint</tt>s of {@link #speechActivity} i.e. the dominant speaker * history has changed. * <p> * This instance notifies the video <tt>Channel</tt>s about the change so * that they may update their last-n lists and report to this instance which * <tt>Endpoint</tt>s are to be asked for video keyframes. * </p> */ private void speechActivityEndpointsChanged() { List<Endpoint> endpoints = null; for (Content content : getContents()) { if (MediaType.VIDEO.equals(content.getMediaType())) { Set<Endpoint> endpointsToAskForKeyframes = null; endpoints = speechActivity.getEndpoints(); for (Channel channel : content.getChannels()) { if (!(channel instanceof RtpChannel)) continue; RtpChannel rtpChannel = (RtpChannel) channel; List<Endpoint> channelEndpointsToAskForKeyframes = rtpChannel .speechActivityEndpointsChanged(endpoints); if ((channelEndpointsToAskForKeyframes != null) && !channelEndpointsToAskForKeyframes.isEmpty()) { if (endpointsToAskForKeyframes == null) { endpointsToAskForKeyframes = new HashSet<>(); } endpointsToAskForKeyframes.addAll(channelEndpointsToAskForKeyframes); } } if ((endpointsToAskForKeyframes != null) && !endpointsToAskForKeyframes.isEmpty()) { content.askForKeyframes(endpointsToAskForKeyframes); } } } } /** * Notifies this instance that there was a change in the value of a property * of {@link #speechActivity}. * * @param ev a <tt>PropertyChangeEvent</tt> which specifies the source of * the event/notification, the name of the property and the old and new * values of that property */ private void speechActivityPropertyChange(PropertyChangeEvent ev) { String propertyName = ev.getPropertyName(); if (ConferenceSpeechActivity.DOMINANT_ENDPOINT_PROPERTY_NAME.equals(propertyName)) { // The dominant speaker in this Conference has changed. We will // likely want to notify the Endpoints participating in this // Conference. dominantSpeakerChanged(); } else if (ConferenceSpeechActivity.ENDPOINTS_PROPERTY_NAME.equals(propertyName)) { speechActivityEndpointsChanged(); } } /** * Sets the time in milliseconds of the last activity related to this * <tt>Conference</tt> to the current system time. */ public void touch() { long now = System.currentTimeMillis(); synchronized (this) { if (getLastActivityTime() < now) lastActivityTime = now; } } /** * Updates an <tt>Endpoint</tt> of this <tt>Conference</tt> with the * information contained in <tt>colibriEndpoint</tt>. The ID of * <tt>colibriEndpoint</tt> is used to select the <tt>Endpoint</tt> to * update. * * @param colibriEndpoint a <tt>ColibriConferenceIQ.Endpoint</tt> instance * that contains information to be set on an <tt>Endpoint</tt> instance of * this <tt>Conference</tt>. */ void updateEndpoint(ColibriConferenceIQ.Endpoint colibriEndpoint) { String id = colibriEndpoint.getId(); if (id != null) { Endpoint endpoint = getEndpoint(id); if (endpoint != null) { String oldDisplayName = endpoint.getDisplayName(); String newDisplayName = colibriEndpoint.getDisplayName(); if ((oldDisplayName == null && newDisplayName != null) || (oldDisplayName != null && !oldDisplayName.equals(newDisplayName))) { endpoint.setDisplayName(newDisplayName); if (isRecording() && endpointRecorder != null) endpointRecorder.updateEndpoint(endpoint); EventAdmin eventAdmin = getVideobridge().getEventAdmin(); if (eventAdmin != null) { eventAdmin.sendEvent(EventFactory.endpointDisplayNameChanged(endpoint)); } } } } } /** * Sets the conference name. * * @param name the new name. */ public void setName(String name) { this.name = name; } /** * Gets the conference name. * * @return the conference name */ public String getName() { return name; } }