1 /*
2  * Jicofo, the Jitsi Conference Focus.
3  *
4  * Copyright @ 2015 Atlassian Pty Ltd
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */
18 package org.jitsi.impl.protocol.xmpp.colibri;
19 
20 import org.jitsi.protocol.xmpp.colibri.exception.*;
21 import org.jitsi.xmpp.extensions.colibri.*;
22 import org.jitsi.xmpp.extensions.jingle.*;
23 import net.java.sip.communicator.service.protocol.*;
24 
25 import org.jitsi.eventadmin.*;
26 import org.jitsi.jicofo.*;
27 import org.jitsi.jicofo.event.*;
28 import org.jitsi.jicofo.util.*;
29 import org.jitsi.protocol.xmpp.*;
30 import org.jitsi.protocol.xmpp.colibri.*;
31 import org.jitsi.protocol.xmpp.util.*;
32 import org.jitsi.service.neomedia.*;
33 import org.jitsi.utils.*;
34 import org.jitsi.utils.logging.Logger;
35 import org.jitsi.xmpp.util.*;
36 
37 import org.jivesoftware.smack.packet.*;
38 import org.jxmpp.jid.*;
39 import org.jxmpp.jid.parts.*;
40 
41 import java.util.*;
42 
43 /**
44  * Default implementation of {@link ColibriConference} that uses Smack for
45  * handling XMPP connection. Handles conference state, allocates and expires
46  * channels per single conference ID. Conference ID is stored after first
47  * allocate channels request.
48  *
49  * @author Pawel Domas
50  */
51 public class ColibriConferenceImpl
52     implements ColibriConference
53 {
54     private final static Logger logger
55         = Logger.getLogger(ColibriConferenceImpl.class);
56 
57     /**
58      * The instance of XMPP connection.
59      */
60     private final XmppConnection connection;
61 
62     /**
63      * The {@link EventAdmin} instance used to emit video stream estimation
64      * events.
65      */
66     private final EventAdmin eventAdmin;
67 
68     /**
69      * XMPP address of videobridge component.
70      */
71     private Jid jitsiVideobridge;
72 
73     /**
74      * The {@link ColibriConferenceIQ} that stores the state of whole conference
75      */
76     private ColibriConferenceIQ conferenceState = new ColibriConferenceIQ();
77 
78     /**
79      * Lock used to synchronise access to the fields related with video channels
80      * counting and video stream estimation events.
81      */
82     private final Object stateEstimationSync = new Object();
83 
84     /**
85      * Synchronization root to sync access to {@link #colibriBuilder} and
86      * {@link #conferenceState}.
87      */
88     private final Object syncRoot = new Object();
89 
90     /**
91      * Custom type of semaphore that allows only 1 thread to send initial
92      * Colibri IQ that creates the conference.
93      * It means that if {@link #conferenceState} has no ID then only 1 thread
94      * will be allowed to send allocate request to the bridge. Other threads
95      * will be suspended until we have the response. Error response to create
96      * request will cause <tt>OperationFailedException</tt> on waiting threads.
97      *
98      * By "create request" we mean a channel allocation Colibri IQ that has no
99      * conference id specified.
100      */
101     private final ConferenceCreationSemaphore createConfSemaphore
102         = new ConferenceCreationSemaphore();
103 
104     /**
105      * The exception produced by the allocator thread which is to be passed to
106      * the waiting threads, so that they will throw exceptions consistent with
107      * the allocator thread.
108      *
109      * Note: this is only used to modify the message logged when an exception
110      * is thrown. It is NOT used to decide whether to throw an exception or not.
111      */
112     private ColibriException allocateChannelsException = null;
113 
114     /**
115      * Utility used for building Colibri queries.
116      */
117     private final ColibriBuilder colibriBuilder
118         = new ColibriBuilder(conferenceState);
119 
120     /**
121      * Flag used to figure out if Colibri conference has been
122      * allocated during last
123      * {@link #createColibriChannels(boolean, String, String, boolean, List)}
124      * call.
125      */
126     private boolean justAllocated = false;
127 
128     /**
129      * Flag indicates that this instance has been disposed and should not be
130      * used anymore.
131      */
132     private boolean disposed;
133 
134     /**
135      * Counts how many video channels have been allocated in order to be able
136      * to estimate video stream count changes.
137      */
138     private int videoChannels;
139 
140     /**
141      * The global ID of the conference.
142      */
143     private String gid;
144 
145     /**
146      * Creates new instance of <tt>ColibriConferenceImpl</tt>.
147      * @param connection XMPP connection object that wil be used by the new
148      *        instance to communicate.
149      * @param eventAdmin {@link EventAdmin} instance which will be used to post
150      *        {@link BridgeEvent#VIDEOSTREAMS_CHANGED}.
151      */
ColibriConferenceImpl(XmppConnection connection, EventAdmin eventAdmin)152     public ColibriConferenceImpl(XmppConnection    connection,
153                                  EventAdmin        eventAdmin)
154     {
155         this.connection = Objects.requireNonNull(connection, "connection");
156         this.eventAdmin = Objects.requireNonNull(eventAdmin, "eventAdmin");
157     }
158 
159     /**
160      * Sets the "global" ID of the conference.
161      * @param gid the value to set.
162      */
setGID(String gid)163     public void setGID(String gid)
164     {
165         this.gid = gid;
166         conferenceState.setGID(gid);
167     }
168 
169     /**
170      * Checks if this instance has been disposed already and if so prints
171      * a warning message. It will also cancel execution in case
172      * {@link #jitsiVideobridge} is null or empty.
173      *
174      * @param operationName the name of the operation that will not happen and
175      * should be mentioned in the warning message.
176      *
177      * @return <tt>true</tt> if this instance has been disposed already or
178      * <tt>false</tt> otherwise.
179      */
checkIfDisposed(String operationName)180     private boolean checkIfDisposed(String operationName)
181     {
182         if (disposed)
183         {
184             logger.warn("Not doing " + operationName + " - instance disposed");
185             return true;
186         }
187         if (jitsiVideobridge == null)
188         {
189             logger.error(
190                 "Not doing " + operationName + " - bridge not initialized");
191             return true;
192         }
193         return false;
194     }
195 
196     /**
197      * {@inheritDoc}
198      */
199     @Override
setJitsiVideobridge(Jid videobridgeJid)200     public void setJitsiVideobridge(Jid videobridgeJid)
201     {
202         if (!StringUtils.isNullOrEmpty(conferenceState.getID()))
203         {
204             throw new IllegalStateException(
205                 "Cannot change the bridge on active conference");
206         }
207         this.jitsiVideobridge = videobridgeJid;
208     }
209 
210     /**
211      * {@inheritDoc}
212      */
213     @Override
getJitsiVideobridge()214     public Jid getJitsiVideobridge()
215     {
216         return this.jitsiVideobridge;
217     }
218 
219     /**
220      * {@inheritDoc}
221      */
222     @Override
getConferenceId()223     public String getConferenceId()
224     {
225         return conferenceState.getID();
226     }
227 
228     /**
229      * {@inheritDoc}
230      */
231     @Override
setConfig(JitsiMeetConfig config)232     public void setConfig(JitsiMeetConfig config)
233     {
234         synchronized (syncRoot)
235         {
236             colibriBuilder.setChannelLastN(config.getChannelLastN());
237             colibriBuilder.setAudioPacketDelay(config.getAudioPacketDelay());
238         }
239     }
240 
241     /**
242      * {@inheritDoc}
243      * </p>
244      * Blocks until a reply is received (and might also block waiting for
245      * the conference to be allocated before sending the request).
246      */
247     @Override
createColibriChannels( boolean useBundle, String endpointId, String statsId, boolean peerIsInitiator, List<ContentPacketExtension> contents, Map<String, List<SourcePacketExtension>> sourceMap, Map<String, List<SourceGroupPacketExtension>> sourceGroupsMap, List<String> octoRelayIds)248     public ColibriConferenceIQ createColibriChannels(
249             boolean useBundle,
250             String endpointId,
251             String statsId,
252             boolean peerIsInitiator,
253             List<ContentPacketExtension> contents,
254             Map<String, List<SourcePacketExtension>> sourceMap,
255             Map<String, List<SourceGroupPacketExtension>> sourceGroupsMap,
256             List<String> octoRelayIds)
257         throws ColibriException
258     {
259         ColibriConferenceIQ allocateRequest;
260         // How many new video channels will be allocated
261         final int newVideoChannelsCount
262             = JingleOfferFactory.containsVideoContent(contents) ? 1 : 0;
263 
264         boolean conferenceExisted;
265         try
266         {
267             synchronized (syncRoot)
268             {
269                 // Only if not in 'disposed' state
270                 if (checkIfDisposed("createColibriChannels"))
271                 {
272                     return null;
273                 }
274 
275                 if (newVideoChannelsCount != 0)
276                 {
277                     synchronized (stateEstimationSync)
278                     {
279                         trackVideoChannelsAddedRemoved(newVideoChannelsCount);
280                     }
281                 }
282 
283                 conferenceExisted
284                     = !acquireCreateConferenceSemaphore(endpointId);
285 
286                 colibriBuilder.reset();
287 
288                 colibriBuilder.addAllocateChannelsReq(
289                     useBundle,
290                     endpointId,
291                     statsId,
292                     peerIsInitiator,
293                     contents,
294                     sourceMap,
295                     sourceGroupsMap,
296                     octoRelayIds);
297 
298                 allocateRequest = colibriBuilder.getRequest(jitsiVideobridge);
299             }
300 
301             if (logger.isDebugEnabled())
302             {
303                 logger.debug(Thread.currentThread() + " sending alloc request");
304             }
305 
306             logRequest("Channel allocate request", allocateRequest);
307 
308             // FIXME retry allocation on timeout ?
309             Stanza response = sendAllocRequest(endpointId, allocateRequest);
310 
311             logResponse("Channel allocate response", response);
312 
313             // Verify the response and throw OperationFailedException
314             // if it's not a success
315             maybeThrowOperationFailed(response);
316 
317             /*
318              * Update the complete ColibriConferenceIQ representation maintained by
319              * this instance with the information given by the (current) response.
320              */
321             // FIXME: allocations!!! should be static method
322             synchronized (syncRoot)
323             {
324                 ColibriAnalyser analyser = new ColibriAnalyser(conferenceState);
325 
326                 analyser.processChannelAllocResp((ColibriConferenceIQ) response);
327 
328                 if (!conferenceExisted && getConferenceId() != null)
329                 {
330                     justAllocated = true;
331                 }
332             }
333 
334             /*
335              * Formulate the result to be returned to the caller which is a subset
336              * of the whole conference information kept by this CallJabberImpl and
337              * includes the remote channels explicitly requested by the method
338              * caller and their respective local channels.
339              */
340             return ColibriAnalyser.getResponseContents(
341                         (ColibriConferenceIQ) response, contents);
342 
343         }
344         catch (ColibriException e)
345         {
346             try
347             {
348                 synchronized (syncRoot)
349                 {
350                     // Emit channels expired
351                     if (!checkIfDisposed("post channels expired on Exception"))
352                     {
353                         synchronized (stateEstimationSync)
354                         {
355                             trackVideoChannelsAddedRemoved(
356                                 -newVideoChannelsCount);
357                         }
358                     }
359                 }
360             }
361             catch (Exception innerException)
362             {
363                 // Log the inner Exception
364                 logger.error(innerException.getMessage(), innerException);
365             }
366 
367             throw e;
368         }
369         finally
370         {
371             releaseCreateConferenceSemaphore(endpointId);
372         }
373     }
374 
375     /**
376      * Verifies the JVB's response to allocate channel request and sets
377      * {@link #allocateChannelsException}.
378      *
379      * @param response the packet received from the bridge (with {@code null}
380      * meaning a timeout) as a response to a request to allocate Colibri
381      * channels.
382      *
383      * @throws TimeoutException in case of a timeout.
384      * @throws ConferenceNotFoundException if the request referenced a colibri
385      * conference which does not exist on the bridge.
386      * @throws BadRequestException if the response
387      * @throws WrongResponseTypeException if the response contains no error, but
388      * is not of the expected {@link ColibriConferenceIQ} type.
389      * @throws ColibriConference in case the response contained an XMPP error
390      * not listed above.
391      */
maybeThrowOperationFailed(Stanza response)392     private void maybeThrowOperationFailed(Stanza response)
393         throws ColibriException
394     {
395         synchronized (syncRoot)
396         {
397             ColibriException exception = null;
398             if (response == null)
399             {
400                 exception = new TimeoutException();
401             }
402             else if (response.getError() != null)
403             {
404                 XMPPError error = response.getError();
405                 if (XMPPError.Condition
406                     .bad_request.equals(error.getCondition()))
407                 {
408                     // Currently jitsi-videobridge returns the same error type
409                     // (bad-request) for two separate cases:
410                     // 1. The request was valid, but the conference ID was not
411                     // found (e.g. it has expired)
412                     // 2. The request was invalid (e.g. the endpoint ID format
413                     // was invalid).
414                     //
415                     // We want to handle the two cases differently, so we
416                     // distinguish them by matching the string.
417                     if (error.getDescriptiveText() != null &&
418                             error.getDescriptiveText()
419                                     .matches("Conference not found for ID:.*"))
420                     {
421                         exception
422                             = new ConferenceNotFoundException(
423                                     error.getConditionText());
424                     }
425                     else
426                     {
427                         exception
428                             = new BadRequestException(
429                                     response.toXML().toString());
430                     }
431                 }
432                 else
433                 {
434                     exception
435                         = new ColibriException(
436                                 "XMPP error: " + response.toXML());
437                 }
438             }
439             else if (!(response instanceof ColibriConferenceIQ))
440             {
441                 exception
442                     = new WrongResponseTypeException(
443                             response.getClass().getCanonicalName());
444             }
445 
446             this.allocateChannelsException = exception;
447             if (exception != null)
448             {
449                 throw exception;
450             }
451         }
452     }
453 
454     /**
455      * Obtains create conference semaphore. If the conference does not exist yet
456      * (ID == null) then only first thread will be allowed to obtain it and all
457      * other threads will have to wait for it to process response packet.
458      *
459      * Methods exposed for unit test purpose.
460      *
461      * @param endpointId the ID of the Colibri endpoint (conference participant)
462      *
463      * @return <tt>true</tt> if current thread is conference creator.
464      *
465      * @throws ColibriConference if the current thread is not the conference
466      * creator thread and the conference creator thread produced an exception.
467      * The exception will be a clone of the original.
468      */
acquireCreateConferenceSemaphore(String endpointId)469     protected boolean acquireCreateConferenceSemaphore(String endpointId)
470         throws ColibriException
471     {
472         return createConfSemaphore.acquire();
473     }
474 
475     /**
476      * Releases "create conference semaphore". Must be called to release the
477      * semaphore possibly in "finally" block.
478      *
479      * @param endpointId the ID of the colibri conference endpoint(participant)
480      */
releaseCreateConferenceSemaphore(String endpointId)481     protected void releaseCreateConferenceSemaphore(String endpointId)
482     {
483         createConfSemaphore.release();
484     }
485 
486     /**
487      * Sends Colibri packet and waits for response in
488      * {@link #createColibriChannels(boolean, String, String, boolean, List)}
489      * call.
490      *
491      * Exposed for unit tests purpose.
492      *
493      * @param endpointId The ID of the Colibri endpoint.
494      * @param request Colibri IQ to be send towards the bridge.
495      *
496      * @return <tt>Packet</tt> which is JVB response or <tt>null</tt> if
497      *         the request timed out.
498      *
499      * @throws ColibriException If sending the packet fails (see
500      * {@link XmppConnection#sendPacketAndGetReply(IQ)}).
501      */
sendAllocRequest(String endpointId, ColibriConferenceIQ request)502     protected Stanza sendAllocRequest(String endpointId,
503                                       ColibriConferenceIQ request)
504         throws ColibriException
505     {
506         try
507         {
508             return connection.sendPacketAndGetReply(request);
509         }
510         catch (OperationFailedException ofe)
511         {
512             throw new ColibriException(ofe.getMessage());
513         }
514     }
515 
516     /**
517      * {@inheritDoc}
518      */
hasJustAllocated()519     public boolean hasJustAllocated()
520     {
521         synchronized (syncRoot)
522         {
523             if (justAllocated)
524             {
525                 justAllocated = false;
526                 return true;
527             }
528             return false;
529         }
530     }
531 
logResponse(String message, Stanza response)532     private void logResponse(String message, Stanza response)
533     {
534         if (!logger.isDebugEnabled())
535         {
536             return;
537         }
538 
539         String responseXml = IQUtils.responseToXML(response);
540 
541         responseXml = responseXml.replace(">",">\n");
542 
543         logger.debug(message + "\n" + responseXml);
544     }
545 
logRequest(String message, IQ iq)546     private void logRequest(String message, IQ iq)
547     {
548         if (logger.isDebugEnabled())
549         {
550             logger.debug(message + "\n" + iq.toXML().toString()
551                     .replace(">",">\n"));
552         }
553     }
554 
555     /**
556      * {@inheritDoc}
557      * </t>
558      * Does not block nor wait for a response.
559      */
560     @Override
expireChannels(ColibriConferenceIQ channelInfo)561     public void expireChannels(ColibriConferenceIQ channelInfo)
562     {
563         expireChannels(channelInfo, false);
564     }
565 
566     /**
567      * {@inheritDoc}
568      */
569     @Override
expireChannels(ColibriConferenceIQ channelInfo, boolean synchronous)570     public void expireChannels(ColibriConferenceIQ channelInfo,
571                                boolean             synchronous)
572     {
573         ColibriConferenceIQ request;
574 
575         synchronized (syncRoot)
576         {
577             // Only if not in 'disposed' state
578             if (checkIfDisposed("expireChannels"))
579             {
580                 return;
581             }
582 
583             colibriBuilder.reset();
584 
585             colibriBuilder.addExpireChannelsReq(channelInfo);
586 
587             request = colibriBuilder.getRequest(jitsiVideobridge);
588         }
589 
590         if (request != null)
591         {
592             logRequest("Expire peer channels", request);
593 
594             if (synchronous)
595             {
596                 // Send and wait for the RESULT packet
597                 try
598                 {
599                     connection.sendPacketAndGetReply(request);
600                 }
601                 catch (OperationFailedException e)
602                 {
603                     logger.error("Channel expire error", e);
604                 }
605             }
606             else
607             {
608                 // Send and forget
609                 connection.sendStanza(request);
610             }
611 
612             synchronized (stateEstimationSync)
613             {
614                 int expiredVideoChannels
615                     = ColibriConferenceIQUtil.getChannelCount(
616                             channelInfo, "video");
617 
618                 trackVideoChannelsAddedRemoved(-expiredVideoChannels);
619             }
620         }
621     }
622 
623     /**
624      * {@inheritDoc}
625      * </t>
626      * Does not block or wait for a response.
627      */
628     @Override
updateRtpDescription( Map<String, RtpDescriptionPacketExtension> descriptionMap, ColibriConferenceIQ localChannelsInfo)629     public void updateRtpDescription(
630             Map<String, RtpDescriptionPacketExtension> descriptionMap,
631             ColibriConferenceIQ localChannelsInfo)
632     {
633         ColibriConferenceIQ request;
634 
635         synchronized (syncRoot)
636         {
637             // Only if not in 'disposed' state
638             if (checkIfDisposed("updateRtpDescription"))
639             {
640                 return;
641             }
642 
643             colibriBuilder.reset();
644 
645             for (String contentName : descriptionMap.keySet())
646             {
647                 ColibriConferenceIQ.Channel channel
648                     = localChannelsInfo.getContent(contentName)
649                         .getChannels().get(0);
650                 colibriBuilder.addRtpDescription(
651                         descriptionMap.get(contentName),
652                         contentName,
653                         channel);
654             }
655 
656             request = colibriBuilder.getRequest(jitsiVideobridge);
657         }
658 
659         if (request != null)
660         {
661             logRequest("Sending RTP desc update: ", request);
662 
663             connection.sendStanza(request);
664         }
665     }
666 
667     /**
668      * {@inheritDoc}
669      * </t>
670      * Does not block or wait for a response.
671      */
672     @Override
updateTransportInfo( Map<String, IceUdpTransportPacketExtension> transportMap, ColibriConferenceIQ localChannelsInfo)673     public void updateTransportInfo(
674             Map<String, IceUdpTransportPacketExtension> transportMap,
675             ColibriConferenceIQ localChannelsInfo)
676     {
677         ColibriConferenceIQ request;
678 
679         synchronized (syncRoot)
680         {
681             if (checkIfDisposed("updateTransportInfo"))
682             {
683                 return;
684             }
685 
686             colibriBuilder.reset();
687 
688             colibriBuilder
689                 .addTransportUpdateReq(transportMap, localChannelsInfo);
690 
691             request = colibriBuilder.getRequest(jitsiVideobridge);
692         }
693 
694         if (request != null)
695         {
696             logRequest("Sending transport info update: ", request);
697 
698             connection.sendStanza(request);
699         }
700     }
701 
702     /**
703      * {@inheritDoc}
704      * </t>
705      * Does not block or wait for a response.
706      */
707     @Override
updateSourcesInfo(MediaSourceMap sources, MediaSourceGroupMap sourceGroups, ColibriConferenceIQ localChannelsInfo)708     public void updateSourcesInfo(MediaSourceMap sources,
709                                   MediaSourceGroupMap sourceGroups,
710                                   ColibriConferenceIQ localChannelsInfo)
711     {
712         ColibriConferenceIQ request;
713 
714         synchronized (syncRoot)
715         {
716             if (checkIfDisposed("updateSourcesInfo"))
717             {
718                 return;
719             }
720 
721             if (StringUtils.isNullOrEmpty(conferenceState.getID()))
722             {
723                 logger.error(
724                         "Have not updated source info on the bridge - "
725                             + "no conference in progress");
726                 return;
727             }
728 
729             colibriBuilder.reset();
730 
731             boolean send = false;
732 
733             // sources
734             if (sources != null
735                     && colibriBuilder.addSourceInfo(
736                             sources.toMap(), localChannelsInfo))
737             {
738                 send = true;
739             }
740             // ssrcGroups
741             if (sourceGroups != null
742                     && colibriBuilder.addSourceGroupsInfo(
743                             sourceGroups.toMap(), localChannelsInfo))
744             {
745                 send = true;
746             }
747 
748             request = send ? colibriBuilder.getRequest(jitsiVideobridge) : null;
749         }
750 
751         if (request != null)
752         {
753             logRequest("Sending source update: ", request);
754 
755             connection.sendStanza(request);
756         }
757     }
758 
759     /**
760      * {@inheritDoc}
761      * </t>
762      * Does not block or wait for a response.
763      */
764     @Override
updateBundleTransportInfo( IceUdpTransportPacketExtension transport, String channelBundleId)765     public void updateBundleTransportInfo(
766             IceUdpTransportPacketExtension transport,
767             String channelBundleId)
768     {
769         ColibriConferenceIQ request;
770 
771         synchronized (syncRoot)
772         {
773             if (checkIfDisposed("updateBundleTransportInfo"))
774             {
775                 return;
776             }
777 
778             colibriBuilder.reset();
779 
780             colibriBuilder.addBundleTransportUpdateReq(
781                     transport, channelBundleId);
782 
783             request = colibriBuilder.getRequest(jitsiVideobridge);
784         }
785 
786         if (request != null)
787         {
788             logRequest("Sending bundle transport info update: ", request);
789 
790             connection.sendStanza(request);
791         }
792     }
793 
794     /**
795      * {@inheritDoc}
796      * </t>
797      * Does not block or wait for a response.
798      */
799     @Override
expireConference()800     public void expireConference()
801     {
802         ColibriConferenceIQ request;
803 
804         synchronized (syncRoot)
805         {
806             if (checkIfDisposed("expireConference"))
807             {
808                 return;
809             }
810 
811             colibriBuilder.reset();
812 
813             if (StringUtils.isNullOrEmpty(conferenceState.getID()))
814             {
815                 logger.info("Nothing to expire - no conference allocated yet");
816                 return;
817             }
818 
819             // Expire all channels
820             if (colibriBuilder.addExpireChannelsReq(conferenceState))
821             {
822                 request = colibriBuilder.getRequest(jitsiVideobridge);
823 
824                 if (request != null)
825                 {
826                     logRequest("Expire conference: ", request);
827 
828                     connection.sendStanza(request);
829                 }
830             }
831 
832             // Reset conference state
833             conferenceState = new ColibriConferenceIQ();
834 
835             // Mark instance as 'disposed'
836             dispose();
837         }
838     }
839 
840     /**
841      * {@inheritDoc}
842      */
843     @Override
dispose()844     public void dispose()
845     {
846         this.disposed = true;
847     }
848 
849     /**
850      * {@inheritDoc}
851      */
852     @Override
isDisposed()853     public boolean isDisposed()
854     {
855         return disposed;
856     }
857 
858     /**
859      * {@inheritDoc}
860      */
861     @Override
muteParticipant(ColibriConferenceIQ channelsInfo, boolean mute)862     public boolean muteParticipant(ColibriConferenceIQ channelsInfo,
863                                    boolean mute)
864     {
865         if (checkIfDisposed("muteParticipant"))
866         {
867             return false;
868         }
869 
870         ColibriConferenceIQ request = new ColibriConferenceIQ();
871         request.setID(conferenceState.getID());
872         request.setName(conferenceState.getName());
873 
874         ColibriConferenceIQ.Content audioContent
875             = channelsInfo.getContent("audio");
876 
877         if (audioContent == null || StringUtils.isNullOrEmpty(request.getID()))
878         {
879             logger.error("Failed to mute - no audio content." +
880                              " Conf ID: " + request.getID());
881             return false;
882         }
883 
884         ColibriConferenceIQ.Content requestContent
885             = new ColibriConferenceIQ.Content(audioContent.getName());
886 
887         for (ColibriConferenceIQ.Channel channel : audioContent.getChannels())
888         {
889             ColibriConferenceIQ.Channel requestChannel
890                 = new ColibriConferenceIQ.Channel();
891 
892             requestChannel.setID(channel.getID());
893 
894             requestChannel.setDirection(
895                     mute ? MediaDirection.SENDONLY.toString()
896                         : MediaDirection.SENDRECV.toString());
897 
898             requestContent.addChannel(requestChannel);
899         }
900 
901         if (requestContent.getChannelCount() == 0)
902         {
903             logger.error("Failed to mute - no channels to modify." +
904                              " ConfID:" + request.getID());
905             return false;
906         }
907 
908         request.setType(IQ.Type.set);
909         request.setTo(jitsiVideobridge);
910 
911         request.addContent(requestContent);
912 
913         connection.sendStanza(request);
914 
915         // FIXME wait for response and set local status
916 
917         return true;
918     }
919 
920     /**
921      * Sets world readable name that identifies the conference.
922      * @param name the new name.
923      */
setName(Localpart name)924     public void setName(Localpart name)
925     {
926         conferenceState.setName(name);
927     }
928 
929     /**
930      * Gets world readable name that identifies the conference.
931      * @return the name.
932      */
getName()933     public Localpart getName()
934     {
935         return conferenceState.getName();
936     }
937 
938     /**
939      * {@inheritDoc}
940      */
941     @Override
updateChannelsInfo( ColibriConferenceIQ localChannelsInfo, Map<String, RtpDescriptionPacketExtension> descriptionMap, MediaSourceMap sources, MediaSourceGroupMap sourceGroups, IceUdpTransportPacketExtension bundleTransport, Map<String, IceUdpTransportPacketExtension> transportMap, String endpointId, List<String> relays)942     public void updateChannelsInfo(
943             ColibriConferenceIQ localChannelsInfo,
944             Map<String, RtpDescriptionPacketExtension> descriptionMap,
945             MediaSourceMap sources,
946             MediaSourceGroupMap sourceGroups,
947             IceUdpTransportPacketExtension bundleTransport,
948             Map<String, IceUdpTransportPacketExtension> transportMap,
949             String endpointId,
950             List<String> relays)
951     {
952         ColibriConferenceIQ request;
953         if (localChannelsInfo == null)
954         {
955             logger.error("Can not update channels -- null");
956             return;
957         }
958 
959         synchronized (syncRoot)
960         {
961             if (checkIfDisposed("updateChannelsInfo"))
962             {
963                 return;
964             }
965 
966             colibriBuilder.reset();
967 
968             boolean send = false;
969 
970             // RTP description
971             if (descriptionMap != null)
972             {
973                 for (String contentName : descriptionMap.keySet())
974                 {
975                     ColibriConferenceIQ.Channel channel
976                         = localChannelsInfo.getContent(contentName)
977                             .getChannels().get(0);
978                     send |= colibriBuilder.addRtpDescription(
979                             descriptionMap.get(contentName),
980                             contentName,
981                             channel);
982                 }
983             }
984             // SSRCs
985             if (sources != null
986                     && colibriBuilder.addSourceInfo(
987                             sources.toMap(), localChannelsInfo))
988             {
989                 send = true;
990             }
991             // SSRC groups
992             if (sourceGroups != null
993                     && colibriBuilder.addSourceGroupsInfo(
994                             sourceGroups.toMap(), localChannelsInfo))
995             {
996                 send = true;
997             }
998             // Bundle transport...
999             if (bundleTransport != null
1000                     && colibriBuilder.addBundleTransportUpdateReq(
1001                             bundleTransport, endpointId))
1002             {
1003                 send = true;
1004             }
1005             // ...or non-bundle transport
1006             else if (transportMap != null
1007                     && colibriBuilder.addTransportUpdateReq(
1008                             transportMap, localChannelsInfo))
1009             {
1010                 send = true;
1011             }
1012             if (relays != null
1013                     && colibriBuilder.addOctoRelays(relays, localChannelsInfo))
1014             {
1015                 send = true;
1016             }
1017 
1018             request = send ? colibriBuilder.getRequest(jitsiVideobridge) : null;
1019         }
1020 
1021         if (request != null)
1022         {
1023             logRequest("Sending channel info update: ", request);
1024 
1025             connection.sendStanza(request);
1026         }
1027     }
1028 
1029     /**
1030      * Method called whenever video channels are about to be allocated/expired,
1031      * but before the actual request is sent. It will track the current video
1032      * channel count and emit {@link BridgeEvent#VIDEOSTREAMS_CHANGED}.
1033      *
1034      * @param channelsDiff how many new video channels are to be
1035      *        allocated/expired.
1036      */
trackVideoChannelsAddedRemoved(int channelsDiff)1037     private void trackVideoChannelsAddedRemoved(int channelsDiff)
1038     {
1039         if (channelsDiff == 0)
1040         {
1041             return;
1042         }
1043 
1044         videoChannels += channelsDiff;
1045 
1046         eventAdmin.postEvent(
1047                     BridgeEvent.createVideoChannelsChanged(
1048                             jitsiVideobridge, channelsDiff));
1049     }
1050 
1051     /**
1052      * Custom type of semaphore that allows only 1 thread to send initial
1053      * Colibri IQ that creates the conference.
1054      * It means that if {@link #conferenceState} has no ID then only 1 thread
1055      * will be allowed to send allocate request to the bridge. Other threads
1056      * will be suspended until we have the response(from which we get our
1057      * conference ID). Error response to create request will cause
1058      * <tt>OperationFailedException</tt> on the threads waiting on this
1059      * semaphore.
1060      */
1061     class ConferenceCreationSemaphore
1062     {
1063         /**
1064          * Stores reference to conference creator thread instance.
1065          */
1066         private Thread creatorThread;
1067 
1068         /**
1069          * Acquires conference creation semaphore. If we don't have conference
1070          * ID yet then only first thread to obtain will be allowed to go through
1071          * and all other threads will be suspended until it finishes it's job.
1072          * Once we have a conference allocated all threads are allowed to go
1073          * through immediately.
1074          *
1075          * @return <tt>true</tt> if current thread has just become a conference
1076          *         creator. That is the thread that sends first channel allocate
1077          *         request that results in new conference created.
1078          *
1079          * @throws ColibriException if we are not conference creator
1080          *         thread and conference creator has failed to create the
1081          *         conference while we've been waiting on this semaphore.
1082          */
acquire()1083         public boolean acquire()
1084             throws ColibriException
1085         {
1086             synchronized (syncRoot)
1087             {
1088                 if (conferenceState.getID() == null && creatorThread == null)
1089                 {
1090                     creatorThread = Thread.currentThread();
1091 
1092                     if (logger.isDebugEnabled())
1093                     {
1094                         logger.debug("I'm the conference creator - " +
1095                                          Thread.currentThread().getName());
1096                     }
1097 
1098                     return true;
1099                 }
1100                 else
1101                 {
1102                     if (logger.isDebugEnabled())
1103                     {
1104                         logger.debug(
1105                             "Will have to wait until the conference " +
1106                                 "is created - " + Thread.currentThread()
1107                                 .getName());
1108                     }
1109 
1110                     while (creatorThread != null)
1111                     {
1112                         try
1113                         {
1114                             syncRoot.wait();
1115                         }
1116                         catch (InterruptedException e)
1117                         {
1118                             throw new RuntimeException(e);
1119                         }
1120                     }
1121 
1122                     if (conferenceState.getID() == null)
1123                     {
1124                         throw allocateChannelsException.clone(
1125                             "Creator thread has failed to allocate channels: ");
1126                     }
1127 
1128                     if (logger.isDebugEnabled())
1129                     {
1130                         logger.debug(
1131                             "Conference created ! Continuing with " +
1132                                 "channel allocation -" +
1133                                 Thread.currentThread().getName());
1134                     }
1135                 }
1136             }
1137             return false;
1138         }
1139 
1140         /**
1141          * Releases this semaphore instance. If we're a conference creator then
1142          * all waiting thread will be woken up.
1143          */
release()1144         public void release()
1145         {
1146             synchronized (syncRoot)
1147             {
1148                 if (creatorThread == Thread.currentThread())
1149                 {
1150                     if (logger.isDebugEnabled())
1151                     {
1152                         logger.debug(
1153                                "Conference creator is releasing the lock - "
1154                                     + Thread.currentThread().getName());
1155                     }
1156 
1157                     creatorThread = null;
1158                     syncRoot.notifyAll();
1159                 }
1160             }
1161         }
1162     }
1163 }
1164