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