/* * Jicofo, the Jitsi Conference Focus. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package org.jitsi.impl.protocol.xmpp; import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*; import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.util.Logger; import org.jitsi.jicofo.*; import org.jitsi.protocol.*; import org.jitsi.protocol.xmpp.*; import org.jitsi.service.neomedia.*; import org.jitsi.util.*; import org.jivesoftware.smack.packet.*; import org.jivesoftware.smack.provider.*; import java.util.*; import org.jivesoftware.util.JiveGlobals; /** * Default implementation of {@link OperationSetColibriConference} that uses * Smack for handling XMPP connection. Handles conference state, allocates and * expires channels. * * @author Pawel Domas */ public class OperationSetColibriConferenceImpl implements OperationSetColibriConference { private final static net.java.sip.communicator.util.Logger logger = Logger.getLogger(OperationSetColibriConferenceImpl.class); /** * The instance of XMPP connection. */ private XmppConnection connection; /** * XMPP address of videobridge component. */ private String jitsiVideobridge; /** * The {@link ColibriConferenceIQ} that stores the state of whole conference */ private ColibriConferenceIQ conferenceState = new ColibriConferenceIQ(); /** * Utility used for building Colibri queries. */ private ColibriBuilder colibriBuilder = new ColibriBuilder(conferenceState); /** * Initializes this operation set. * * @param connection Smack XMPP connection impl that will be used to send * and receive XMPP packets. */ public void initialize(XmppConnection connection) { this.connection = connection; // FIXME: Register Colibri ProviderManager.getInstance().addIQProvider( ColibriConferenceIQ.ELEMENT_NAME, ColibriConferenceIQ.NAMESPACE, new ColibriIQProvider()); // FIXME: register Jingle ProviderManager.getInstance().addIQProvider( JingleIQ.ELEMENT_NAME, JingleIQ.NAMESPACE, new JingleIQProvider()); } /** * {@inheritDoc} */ @Override public void setJitsiVideobridge(String videobridgeJid) { if (!StringUtils.isNullOrEmpty(conferenceState.getID())) { throw new IllegalStateException( "Can not change the bridge on active conference"); } this.jitsiVideobridge = videobridgeJid; } /** * {@inheritDoc} */ @Override public String getJitsiVideobridge() { return this.jitsiVideobridge; } /** * {@inheritDoc} */ @Override public void setJitsiMeetConfig(JitsiMeetConfig config) { colibriBuilder.setChannelLastN(config.getChannelLastN()); colibriBuilder.setAdaptiveLastN(config.isAdaptiveLastNEnabled()); colibriBuilder.setAdaptiveSimulcast(config.isAdaptiveSimulcastEnabled()); } /** * {@inheritDoc} */ @Override public String getConferenceId() { return conferenceState.getID(); } /** * {@inheritDoc} */ @Override public synchronized ColibriConferenceIQ createColibriChannels( boolean useBundle, String endpointName, boolean peerIsInitiator, List<ContentPacketExtension> contents) throws OperationFailedException { colibriBuilder.reset(); colibriBuilder.addAllocateChannelsReq( useBundle, endpointName, peerIsInitiator, contents); ColibriConferenceIQ allocateRequest = colibriBuilder.getRequest(jitsiVideobridge); ColibriConferenceIQ.Content colibriContent = allocateRequest.getContent("audio"); boolean useAudioMixer = false; String useAudioString = JiveGlobals.getProperty("org.jitsi.videobridge.ofmeet.audio.mixer"); // BAO if (useAudioString != null) useAudioMixer = useAudioString.equals("true"); if (useAudioMixer && colibriContent != null) { logger.info("audio mixer set " + colibriContent); for (ColibriConferenceIQ.Channel channel : colibriContent.getChannels()) { channel.setRTPLevelRelayType(RTPLevelRelayType.MIXER); // BAO } } //FIXME: retry allocation on timeout Packet response = connection.sendPacketAndGetReply(allocateRequest); if (response == null) { throw new OperationFailedException( "Failed to allocate colibri channels: response is null." + " Maybe the response timed out.", OperationFailedException.NETWORK_FAILURE); } else if (response.getError() != null) { throw new OperationFailedException( "Failed to allocate colibri channels: " + response.getError(), OperationFailedException.GENERAL_ERROR); } else if (!(response instanceof ColibriConferenceIQ)) { throw new OperationFailedException( "Failed to allocate colibri channels: response is not a" + " colibri conference", OperationFailedException.GENERAL_ERROR); } /* * Update the complete ColibriConferenceIQ representation maintained by * this instance with the information given by the (current) response. */ ColibriAnalyser analyser = new ColibriAnalyser(conferenceState); analyser.processChannelAllocResp((ColibriConferenceIQ) response); /* * Formulate the result to be returned to the caller which is a subset * of the whole conference information kept by this CallJabberImpl and * includes the remote channels explicitly requested by the method * caller and their respective local channels. */ return ColibriAnalyser.getResponseContents( (ColibriConferenceIQ) response, contents); } /** * {@inheritDoc} */ @Override public void expireChannels(ColibriConferenceIQ channelInfo) { colibriBuilder.reset(); colibriBuilder.addExpireChannelsReq(channelInfo); ColibriConferenceIQ iq = colibriBuilder.getRequest(jitsiVideobridge); if (iq != null) { connection.sendPacket(iq); } } /** * {@inheritDoc} */ @Override public void updateTransportInfo( boolean initiator, Map<String, IceUdpTransportPacketExtension> map, ColibriConferenceIQ localChannelsInfo) { colibriBuilder.reset(); colibriBuilder.addTransportUpdateReq( initiator, map, localChannelsInfo); ColibriConferenceIQ conferenceRequest = colibriBuilder.getRequest(jitsiVideobridge); if (conferenceRequest != null) { connection.sendPacket(conferenceRequest); } } /** * {@inheritDoc} */ @Override public void updateSsrcGroupsInfo(MediaSSRCGroupMap ssrcGroups, ColibriConferenceIQ localChannelsInfo) { // FIXME: move to ColibriBuilder ColibriConferenceIQ updateIq = new ColibriConferenceIQ(); updateIq.setID(conferenceState.getID()); updateIq.setType(IQ.Type.SET); updateIq.setTo(jitsiVideobridge); boolean updateNeeded = false; for (ColibriConferenceIQ.Content content : localChannelsInfo.getContents()) { String contentName = content.getName(); if ("video".compareToIgnoreCase(contentName) != 0) { // Simulcast currently used for video only continue; } ColibriConferenceIQ.Content reqContent = new ColibriConferenceIQ.Content(content.getName()); boolean hasChannels = false; for (ColibriConferenceIQ.Channel channel : content.getChannels()) { ColibriConferenceIQ.Channel reqChannel = new ColibriConferenceIQ.Channel(); reqChannel.setID(channel.getID()); List<SSRCGroup> groups = ssrcGroups.getSSRCGroupsForMedia(content.getName()); for (SSRCGroup group : groups) { try { reqChannel.addSourceGroup(group.getExtensionCopy()); hasChannels = true; updateNeeded = true; } catch (Exception e) { logger.error("Error copying extension", e); } } if (groups.isEmpty()) { // Put empty source group to turn off simulcast layers reqChannel.addSourceGroup( SourceGroupPacketExtension.createSimulcastGroup()); hasChannels = true; updateNeeded = true; } reqContent.addChannel(reqChannel); } if (hasChannels) { updateIq.addContent(reqContent); } } if (updateNeeded) { connection.sendPacketAndGetReply(updateIq); } } /** * {@inheritDoc} */ @Override public void updateBundleTransportInfo( boolean initiator, IceUdpTransportPacketExtension transport, ColibriConferenceIQ localChannelsInfo) { colibriBuilder.reset(); colibriBuilder.addBundleTransportUpdateReq( initiator, transport, localChannelsInfo); ColibriConferenceIQ conferenceRequest = colibriBuilder.getRequest(jitsiVideobridge); if (conferenceRequest != null) { connection.sendPacket(conferenceRequest); } } /** * {@inheritDoc} */ @Override public void expireConference() { colibriBuilder.reset(); if (StringUtils.isNullOrEmpty(conferenceState.getID())) { logger.info("Nothing to expire - no conference allocated yet"); return; } // Expire all channels colibriBuilder.addExpireChannelsReq(conferenceState); ColibriConferenceIQ colibriRequest = colibriBuilder.getRequest(jitsiVideobridge); if (colibriRequest != null) { connection.sendPacket(colibriRequest); } // Reset conference state conferenceState = new ColibriConferenceIQ(); } @Override public boolean muteParticipant(ColibriConferenceIQ channelsInfo, boolean mute) { ColibriConferenceIQ request = new ColibriConferenceIQ(); request.setID(conferenceState.getID()); ColibriConferenceIQ.Content audioContent = channelsInfo.getContent("audio"); if (audioContent == null) { logger.error("Failed to mute - no audio content." + " Conf ID: " + request.getID()); return false; } ColibriConferenceIQ.Content contentRequest = new ColibriConferenceIQ.Content(audioContent.getName()); for (ColibriConferenceIQ.Channel channel : audioContent.getChannels()) { ColibriConferenceIQ.Channel channelRequest = new ColibriConferenceIQ.Channel(); channelRequest.setID(channel.getID()); if (mute) { channelRequest.setDirection(MediaDirection.SENDONLY); } else { channelRequest.setDirection(MediaDirection.SENDRECV); } contentRequest.addChannel(channelRequest); } if (contentRequest.getChannelCount() == 0) { logger.error("Failed to mute - no channels to modify." + " ConfID:" + request.getID()); return false; } request.setType(IQ.Type.SET); request.setTo(jitsiVideobridge); request.addContent(contentRequest); connection.sendPacket(request); // FIXME wait for response and set local status return true; } }