diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/extensions/colibri/ColibriBuilder.java b/src/net/java/sip/communicator/impl/protocol/jabber/extensions/colibri/ColibriBuilder.java index 547ebe8fc..502c36294 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/extensions/colibri/ColibriBuilder.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/extensions/colibri/ColibriBuilder.java @@ -37,9 +37,9 @@ * Add one or multiple requests of the same type by calling * {@link #addAllocateChannelsReq(boolean, String, boolean, java.util.List)}} * or {@link #addExpireChannelsReq(ColibriConferenceIQ)} - * or {@link #addTransportUpdateReq(boolean, java.util.Map, ColibriConferenceIQ)} - * or {@link #addBundleTransportUpdateReq( - * boolean, IceUdpTransportPacketExtension, ColibriConferenceIQ)}. + * or {@link #addRtpDescription(Map, ColibriConferenceIQ)} + * and {@link #addSSSRCGroupsInfo(Map, ColibriConferenceIQ)} + * and {@link #addSSSRCInfo(Map, ColibriConferenceIQ)}. * *
  • * Compile the request by calling {@link #getRequest(String)}. Then send it to @@ -161,18 +161,26 @@ public void reset() * @param contents the list of {@link ContentPacketExtension} describing * channels media. * - * @return this instance fo calls chaining purpose. + * @return true if the request yields any changes in Colibri + * channels state on the bridge or false otherwise. + * In general when false is returned for all + * combined requests it makes no sense to send it. */ - public ColibriBuilder addAllocateChannelsReq( - boolean useBundle, - String endpointName, - boolean peerIsInitiator, + public boolean addAllocateChannelsReq( + boolean useBundle, + String endpointName, + boolean peerIsInitiator, List contents) { + Objects.requireNonNull(endpointName, "endpointName"); + Objects.requireNonNull(contents, "contents"); + assertRequestType(RequestType.ALLOCATE_CHANNELS); request.setType(IQ.Type.GET); + boolean hasAnyChanges = false; + for (ContentPacketExtension cpe : contents) { MediaType mediaType = JingleUtils.getMediaType(cpe); @@ -193,7 +201,7 @@ public ColibriBuilder addAllocateChannelsReq( remoteChannelRequest.setChannelBundleId(endpointName); } - if (mediaType != MediaType.DATA) + if (remoteChannelRequest instanceof ColibriConferenceIQ.Channel) { RtpDescriptionPacketExtension rdpe = cpe.getFirstChildOfType( @@ -221,13 +229,17 @@ public ColibriBuilder addAllocateChannelsReq( copyTransportOnChannel(cpe, remoteChannelRequest); } - if (mediaType != MediaType.DATA) + if (remoteChannelRequest instanceof ColibriConferenceIQ.Channel) { + hasAnyChanges = true; + contentRequest.addChannel( (ColibriConferenceIQ.Channel) remoteChannelRequest); } else { + hasAnyChanges = true; + contentRequest.addSctpConnection( (ColibriConferenceIQ.SctpConnection) remoteChannelRequest); } @@ -246,6 +258,8 @@ public ColibriBuilder addAllocateChannelsReq( IceUdpTransportPacketExtension.class); if (transport != null) { + hasAnyChanges = true; + bundle.setTransport( IceUdpTransportPacketExtension .cloneTransportAndCandidates(transport, true)); @@ -254,106 +268,38 @@ public ColibriBuilder addAllocateChannelsReq( request.addChannelBundle(bundle); } - return this; + return hasAnyChanges; } /** - * Adds next ICE transport update request to - * {@link RequestType#TRANSPORT_UPDATE} query currently being built. - * - * @param initiator the value that will be set in 'initiator' - * attribute({@link ColibriConferenceIQ.Channel#initiator}). - * @param map the map of content name to transport extensions. Maps - * transport to media types. - * @param localChannelsInfo {@link ColibriConferenceIQ} holding info about - * Colibri channels to be updated. - * - * @return this instance fo calls chaining purpose. - */ - public ColibriBuilder addTransportUpdateReq( - boolean initiator, - Map map, - ColibriConferenceIQ localChannelsInfo) - { - if (conferenceState == null - || StringUtils.isNullOrEmpty(conferenceState.getID())) - { - // We are not initialized yet - return null; - } - - assertRequestType(RequestType.TRANSPORT_UPDATE); - - request.setType(IQ.Type.SET); - - for (Map.Entry e - : map.entrySet()) - { - String contentName = e.getKey(); - ColibriConferenceIQ.ChannelCommon channel - = getColibriChannel(localChannelsInfo, contentName); - - if (channel != null) - { - IceUdpTransportPacketExtension transport - = IceUdpTransportPacketExtension - .cloneTransportAndCandidates(e.getValue(), true); - - ColibriConferenceIQ.ChannelCommon channelRequest - = channel instanceof ColibriConferenceIQ.Channel - ? new ColibriConferenceIQ.Channel() - : new ColibriConferenceIQ.SctpConnection(); - - channelRequest.setID(channel.getID()); - channelRequest.setEndpoint(channel.getEndpoint()); - channelRequest.setInitiator(initiator); - channelRequest.setTransport(transport); - - if (channelRequest instanceof ColibriConferenceIQ.Channel) - { - request.getOrCreateContent(contentName) - .addChannel( - (ColibriConferenceIQ.Channel) channelRequest); - } - else - { - request.getOrCreateContent(contentName) - .addSctpConnection( - (ColibriConferenceIQ.SctpConnection) channelRequest); - } - } - } - return this; - } - - /** - * Adds next request to {@link RequestType#BUNDLE_TRANSPORT_UPDATE} query. - * @param initiator the value that will be set in 'initiator' - * attribute({@link ColibriConferenceIQ.Channel#initiator}). + * Adds next request to {@link RequestType#CHANNEL_INFO_UPDATE} query. * @param localChannelsInfo the {@link ColibriConferenceIQ} instance that * describes the channel for which bundle transport will be updated. * It should contain the description of only one "channel bundle". * If it contains more than one then the first one will be used. - * @return this instance for calls chaining purpose. + * @return true if the request yields any changes in Colibri + * channels state on the bridge or false otherwise. + * In general when false is returned for all + * combined requests it makes no sense to send it. * @throws IllegalArgumentException if localChannelsInfo does not * describe any channel bundles. */ - public ColibriBuilder addBundleTransportUpdateReq( - boolean initiator, - IceUdpTransportPacketExtension transport, - ColibriConferenceIQ localChannelsInfo) + public boolean addBundleTransportUpdateReq( + IceUdpTransportPacketExtension transport, + ColibriConferenceIQ localChannelsInfo) throws IllegalArgumentException { - // FIXME:'initiator' not used on bundle transport update ? + Objects.requireNonNull(transport, "transport"); + Objects.requireNonNull(localChannelsInfo, "localChannelsInfo"); if (conferenceState == null || StringUtils.isNullOrEmpty(conferenceState.getID())) { // We are not initialized yet - return null; + return false; } - assertRequestType(RequestType.BUNDLE_TRANSPORT_UPDATE); + assertRequestType(RequestType.CHANNEL_INFO_UPDATE); request.setType(IQ.Type.SET); @@ -371,7 +317,7 @@ public ColibriBuilder addBundleTransportUpdateReq( else { throw new IllegalArgumentException( - "Expected ChannelBundle as not found"); + "Expected ChannelBundle as not found"); } ColibriConferenceIQ.ChannelBundle bundleUpdate @@ -387,7 +333,7 @@ public ColibriBuilder addBundleTransportUpdateReq( request.addChannelBundle(bundleUpdate); - return this; + return true; } /** @@ -395,15 +341,20 @@ public ColibriBuilder addBundleTransportUpdateReq( * {@link RequestType#EXPIRE_CHANNELS} query currently being built. * @param channelInfo the {@link ColibriConferenceIQ} instance that contains * info about the channels to be expired. - * @return this instance for the purpose of calls chaining. + * @return true if the request yields any changes in Colibri + * channels state on the bridge or false otherwise. + * In general when false is returned for all + * combined requests it makes no sense to send it. */ - public ColibriBuilder addExpireChannelsReq(ColibriConferenceIQ channelInfo) + public boolean addExpireChannelsReq(ColibriConferenceIQ channelInfo) { + Objects.requireNonNull(channelInfo, "channelInfo"); + // Formulate the ColibriConferenceIQ request which is to be sent. if (conferenceState == null || StringUtils.isNullOrEmpty(conferenceState.getID())) { - return null; + return false; } assertRequestType(RequestType.EXPIRE_CHANNELS); @@ -411,7 +362,7 @@ public ColibriBuilder addExpireChannelsReq(ColibriConferenceIQ channelInfo) request.setType(IQ.Type.SET); for (ColibriConferenceIQ.Content expiredContent - : channelInfo.getContents()) + : channelInfo.getContents()) { ColibriConferenceIQ.Content stateContent = conferenceState.getContent(expiredContent.getName()); @@ -420,7 +371,7 @@ public ColibriBuilder addExpireChannelsReq(ColibriConferenceIQ channelInfo) { ColibriConferenceIQ.Content requestContent = request.getOrCreateContent( - stateContent.getName()); + stateContent.getName()); for (ColibriConferenceIQ.Channel expiredChannel : expiredContent.getChannels()) @@ -490,7 +441,7 @@ public ColibriBuilder addExpireChannelsReq(ColibriConferenceIQ channelInfo) */ /*if (stateContent.getChannelCount() == 1) { - stateChannel = stateContent.getChannel(0); + stateChannel = stateContent.getRtpChannel(0); ColibriConferenceIQ.Channel channelRequest = new ColibriConferenceIQ.Channel(); @@ -537,7 +488,293 @@ public ColibriBuilder addExpireChannelsReq(ColibriConferenceIQ channelInfo) } } - return this; + return hasAnyChannelsToExpire; + } + + /** + * Adds next payload type information update request to + * {@link RequestType#CHANNEL_INFO_UPDATE} query currently being built. + * + * @param map the map of content name to RTP description packet extension. + * @param localChannelsInfo {@link ColibriConferenceIQ} holding info about + * Colibri channels to be updated. + * + * @return true if the request yields any changes in Colibri + * channels state on the bridge or false otherwise. + * In general when false is returned for all + * combined requests it makes no sense to send it. + */ + public boolean addRtpDescription( + Map map, + ColibriConferenceIQ localChannelsInfo) + { + Objects.requireNonNull(map, "map"); + Objects.requireNonNull(localChannelsInfo, "localChannelsInfo"); + + if (conferenceState == null + || StringUtils.isNullOrEmpty(conferenceState.getID())) + { + // We are not initialized yet + return false; + } + + assertRequestType(RequestType.CHANNEL_INFO_UPDATE); + + request.setType(IQ.Type.SET); + + boolean anyUpdates = false; + + for (Map.Entry e + : map.entrySet()) + { + String contentName = e.getKey(); + ColibriConferenceIQ.ChannelCommon channel + = getColibriChannel(localChannelsInfo, contentName); + + if (channel != null + && channel instanceof ColibriConferenceIQ.Channel) + { + RtpDescriptionPacketExtension rtpPE = e.getValue(); + if (rtpPE == null) + { + continue; + } + + List pts = rtpPE.getPayloadTypes(); + if (pts == null || pts.isEmpty()) + { + continue; + } + + anyUpdates = true; + + ColibriConferenceIQ.Channel channelRequest + = request.getOrCreateContent(contentName) + .getChannel(channel.getID()); + if (channelRequest == null) + { + channelRequest = new ColibriConferenceIQ.Channel(); + channelRequest.setID(channel.getID()); + } + + for (PayloadTypePacketExtension ptPE : pts) + { + channelRequest.addPayloadType(ptPE); + } + } + } + + return anyUpdates; + } + + /** + * Adds next SSRC information update request to + * {@link RequestType#CHANNEL_INFO_UPDATE} query currently being built. + * + * @param ssrcMap the map of content name to the list of + * SourcePacketExtension. + * @param localChannelsInfo {@link ColibriConferenceIQ} holding info about + * Colibri channels to be updated. + * + * @return true if the request yields any changes in Colibri + * channels state on the bridge or false otherwise. + * In general when false is returned for all + * combined requests it makes no sense to send it. + */ + public boolean addSSSRCInfo( + Map> ssrcMap, + ColibriConferenceIQ localChannelsInfo) + { + Objects.requireNonNull(ssrcMap, "ssrcMap"); + Objects.requireNonNull(localChannelsInfo, "localChannelsInfo"); + + if (conferenceState == null + || StringUtils.isNullOrEmpty(conferenceState.getID())) + { + // We are not initialized yet + return false; + } + + assertRequestType(RequestType.CHANNEL_INFO_UPDATE); + + request.setType(IQ.Type.SET); + + boolean anyUpdates = false; + + // Go over SSRCs + for (String contentName : ssrcMap.keySet()) + { + // Get channel from local channel info + ColibriConferenceIQ.ChannelCommon rtpChanel + = getRtpChannel(localChannelsInfo, contentName); + if (rtpChanel == null) + { + // There's no channel for this content name in localChannelsInfo + continue; + } + + anyUpdates = true; + + // Ok we have channel for this content, let's add SSRCs + ColibriConferenceIQ.Channel reqChannel + = (ColibriConferenceIQ.Channel) getRequestChannel( + request.getOrCreateContent(contentName), rtpChanel); + + for (SourcePacketExtension ssrc : ssrcMap.get(contentName)) + { + reqChannel.addSource(ssrc.copy()); + } + + if (reqChannel.getSources() == null + || reqChannel.getSources().isEmpty()) + { + // Put an empty source to remove all sources + SourcePacketExtension emptySource = new SourcePacketExtension(); + emptySource.setSSRC(-1L); + reqChannel.addSource(emptySource); + } + } + + return anyUpdates; + } + + /** + * Adds next SSRC group information update request to + * {@link RequestType#CHANNEL_INFO_UPDATE} query currently being built. + * + * @param ssrcGroupMap the map of content name to the list of + * SourceGroupPacketExtension. + * @param localChannelsInfo {@link ColibriConferenceIQ} holding info about + * Colibri channels to be updated. + * + * @return true if the request yields any changes in Colibri + * channels state on the bridge or false otherwise. + * In general when false is returned for all + * combined requests it makes no sense to send it. + */ + public boolean addSSSRCGroupsInfo( + Map> ssrcGroupMap, + ColibriConferenceIQ localChannelsInfo) + { + Objects.requireNonNull(ssrcGroupMap, "ssrcGroupMap"); + Objects.requireNonNull(localChannelsInfo, "localChannelsInfo"); + + if (conferenceState == null + || StringUtils.isNullOrEmpty(conferenceState.getID())) + { + // We are not initialized yet + return false; + } + + assertRequestType(RequestType.CHANNEL_INFO_UPDATE); + + request.setType(IQ.Type.SET); + + boolean anyUpdates = false; + + // Go over SSRC groups + for (String contentName : ssrcGroupMap.keySet()) + { + // Get channel from local channel info + ColibriConferenceIQ.Channel rtpChannel + = getRtpChannel(localChannelsInfo, contentName); + if (rtpChannel == null) + { + // There's no channel for this content name in localChannelsInfo + continue; + } + + List groups + = ssrcGroupMap.get(contentName); + + // Ok we have channel for this content, let's add SSRCs + ColibriConferenceIQ.Channel reqChannel + = (ColibriConferenceIQ.Channel) getRequestChannel( + request.getOrCreateContent(contentName), rtpChannel); + + if (groups.isEmpty() && "video".equalsIgnoreCase(contentName)) + { + anyUpdates = true; + + // Put empty source group to turn off simulcast layers + reqChannel.addSourceGroup( + SourceGroupPacketExtension.createSimulcastGroup()); + } + + for (SourceGroupPacketExtension group : groups) + { + anyUpdates = true; + + reqChannel.addSourceGroup(group); + } + } + + return anyUpdates; + } + + /** + * Adds next ICE transport update request to + * {@link RequestType#CHANNEL_INFO_UPDATE} query currently being built. + * + * @param map the map of content name to transport extensions. Maps + * transport to media types. + * @param localChannelsInfo {@link ColibriConferenceIQ} holding info about + * Colibri channels to be updated. + * + * @return true if the request yields any changes in Colibri + * channels state on the bridge or false otherwise. + * In general when false is returned for all + * combined requests it makes no sense to send it. + */ + public boolean addTransportUpdateReq( + Map map, + ColibriConferenceIQ localChannelsInfo) + { + Objects.requireNonNull(map, "map"); + Objects.requireNonNull(localChannelsInfo, "localChannelsInfo"); + + if (conferenceState == null + || StringUtils.isNullOrEmpty(conferenceState.getID())) + { + // We are not initialized yet + return false; + } + + boolean hasAnyChanges = false; + + assertRequestType(RequestType.CHANNEL_INFO_UPDATE); + + request.setType(IQ.Type.SET); + + for (Map.Entry e + : map.entrySet()) + { + String contentName = e.getKey(); + ColibriConferenceIQ.ChannelCommon channel + = getColibriChannel(localChannelsInfo, contentName); + + if (channel != null) + { + IceUdpTransportPacketExtension transport + = IceUdpTransportPacketExtension + .cloneTransportAndCandidates(e.getValue(), true); + + ColibriConferenceIQ.ChannelCommon channelRequest + = channel instanceof ColibriConferenceIQ.Channel + ? new ColibriConferenceIQ.Channel() + : new ColibriConferenceIQ.SctpConnection(); + + channelRequest.setID(channel.getID()); + channelRequest.setEndpoint(channel.getEndpoint()); + channelRequest.setTransport(transport); + + request.getOrCreateContent(contentName) + .addChannelCommon(channelRequest); + + hasAnyChanges = true; + } + } + return hasAnyChanges; } /** @@ -583,10 +820,12 @@ public ColibriConferenceIQ getRequest(String videobridge) request.setTo(videobridge); - if (requestType == RequestType.EXPIRE_CHANNELS - && !hasAnyChannelsToExpire) + if (requestType == RequestType.EXPIRE_CHANNELS) { - return null; + if (!hasAnyChannelsToExpire) + return null; + + hasAnyChannelsToExpire = false; } return request; @@ -732,69 +971,64 @@ public void setSimulcastMode(SimulcastMode simulcastMode) } /** - * Adds next payload type information update request to - * {@link RequestType#RTP_DESCRIPTION_UPDATE} query currently being built. + * Creates a new instance of localChannelInfo and initializes only + * the fields required to identify particular Colibri channel on the bridge. + * This instance is meant to be used in Colibri + * {@link RequestType#CHANNEL_INFO_UPDATE} requests. This instance is also + * added to given requestContent which used to construct current + * request. * - * @param map the map of content name to RTP description packet extension. - * @param localChannelsInfo {@link ColibriConferenceIQ} holding info about - * Colibri channels to be updated. + * @param requestContent Content of Colibri update request to which + * new instance wil be automatically added after has been created. + * @param localChannelInfo the original channel for which "update request" + * equivalent is to be created with this call. * - * @return this instance for calls chaining purpose. + * @return new instance of localChannelInfo and initialized with + * only those fields required to identify particular Colibri channel on + * the bridge. */ - public ColibriBuilder addRtpDescription( - Map map, - ColibriConferenceIQ localChannelsInfo) { - - if (conferenceState == null - || StringUtils.isNullOrEmpty(conferenceState.getID())) - { - // We are not initialized yet - return null; - } - - assertRequestType(RequestType.RTP_DESCRIPTION_UPDATE); - - request.setType(IQ.Type.SET); - - for (Map.Entry e - : map.entrySet()) + private ColibriConferenceIQ.ChannelCommon getRequestChannel( + ColibriConferenceIQ.Content requestContent, + ColibriConferenceIQ.ChannelCommon localChannelInfo) + { + ColibriConferenceIQ.ChannelCommon reqChannel + = requestContent.getChannel(localChannelInfo.getID()); + if (reqChannel == null) { - String contentName = e.getKey(); - ColibriConferenceIQ.ChannelCommon channel - = getColibriChannel(localChannelsInfo, contentName); - - if (channel != null - && channel instanceof ColibriConferenceIQ.Channel) + if (localChannelInfo instanceof ColibriConferenceIQ.Channel) { - RtpDescriptionPacketExtension rtpPE = e.getValue(); - if (rtpPE == null) - { - continue; - } - - List pts = rtpPE.getPayloadTypes(); - if (pts == null || pts.isEmpty()) - { - continue; - } - - ColibriConferenceIQ.Channel channelRequest - = new ColibriConferenceIQ.Channel(); + reqChannel = new ColibriConferenceIQ.Channel(); + } + else if ( + localChannelInfo instanceof ColibriConferenceIQ.SctpConnection) + { + reqChannel = new ColibriConferenceIQ.SctpConnection(); + } + else + { + throw new RuntimeException( + "Unsupported ChannelCommon class: " + + localChannelInfo.getClass()); + } - channelRequest.setID(channel.getID()); + reqChannel.setID(localChannelInfo.getID()); - for (PayloadTypePacketExtension ptPE : rtpPE.getPayloadTypes()) - { - channelRequest.addPayloadType(ptPE); - } + requestContent.addChannelCommon(reqChannel); + } + return reqChannel; + } - request.getOrCreateContent(contentName) - .addChannel(channelRequest); - } + private ColibriConferenceIQ.Channel getRtpChannel( + ColibriConferenceIQ localChannelsInfo, + String contentName) + { + ColibriConferenceIQ.Content content + = localChannelsInfo.getContent(contentName); - } + if (content == null) + return null; - return this; + return content.getChannelCount() > 0 ? content.getChannel(0) : null; } /** @@ -808,19 +1042,10 @@ public enum RequestType ALLOCATE_CHANNELS, /** - * Updates transport information for channels that use RTP bundle. - */ - BUNDLE_TRANSPORT_UPDATE, - - /** - * Updates channel transport information(ICE transport candidates). - */ - TRANSPORT_UPDATE, - - /** - * Updates the RTP description of a channel (payload types). + * An update request which is meant to modify some values of existing + * Colibri channels on the bridge. */ - RTP_DESCRIPTION_UPDATE, + CHANNEL_INFO_UPDATE, /** * Expires specified Colibri channels. diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/extensions/colibri/ColibriConferenceIQ.java b/src/net/java/sip/communicator/impl/protocol/jabber/extensions/colibri/ColibriConferenceIQ.java index 48e344b33..3fefc9f03 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/extensions/colibri/ColibriConferenceIQ.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/extensions/colibri/ColibriConferenceIQ.java @@ -1924,6 +1924,25 @@ public boolean addChannel(Channel channel) return channels.contains(channel) ? false : channels.add(channel); } + /** + * Adds ChannelCommon to this Content. + * @param channelCommon {@link ChannelCommon} instance to be added to + * this content. + * @return true if given channelCommon has been + * actually added to this Content instance. + */ + public boolean addChannelCommon(ChannelCommon channelCommon) + { + if (channelCommon instanceof Channel) + { + return addChannel((Channel) channelCommon); + } + else + { + return addSctpConnection((SctpConnection) channelCommon); + } + } + /** * Adds a specific SctpConnection to the list of * SctpConnections included into this Content. diff --git a/src/net/java/sip/communicator/impl/protocol/jabber/extensions/jingle/IceUdpTransportPacketExtension.java b/src/net/java/sip/communicator/impl/protocol/jabber/extensions/jingle/IceUdpTransportPacketExtension.java index 1112c8c27..173701f48 100644 --- a/src/net/java/sip/communicator/impl/protocol/jabber/extensions/jingle/IceUdpTransportPacketExtension.java +++ b/src/net/java/sip/communicator/impl/protocol/jabber/extensions/jingle/IceUdpTransportPacketExtension.java @@ -190,6 +190,27 @@ public boolean removeCandidate(CandidatePacketExtension candidate) } } + /** + * Removes given PacketExtension from the list of child packet + * extensions. CandidatePacketExtension are not taken into account + * in this method and {@link #removeCandidate(CandidatePacketExtension)} + * should be used instead. + * + * @param childExtension PacketExtension instance to be removed + * from child packet extensions list. + * + * @return true if given childExtension has been in the + * list and was removed or false otherwise. + */ + public boolean removeChildExtension(PacketExtension childExtension) + { + List childExtensions + = super.getChildExtensions(); + + return childExtensions != null + && childExtensions.remove(childExtension); + } + /** * Returns the list of {@link CandidatePacketExtension}s currently * registered with this transport.