/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.impl.protocol.jabber.extensions.colibri; import net.java.sip.communicator.impl.protocol.jabber.extensions.*; import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; import net.java.sip.communicator.service.protocol.jabber.*; import org.jitsi.service.neomedia.*; import org.jitsi.util.*; import org.jivesoftware.smack.packet.*; import org.jivesoftware.smack.provider.*; import org.xmlpull.v1.*; /** * Implements an <tt>org.jivesoftware.smack.provider.IQProvider</tt> for the * Jitsi Videobridge extension <tt>ColibriConferenceIQ</tt>. * * @author Lyubomir Marinov * @author Boris Grozev */ public class ColibriIQProvider implements IQProvider { /** * Smack interoperation layer */ private AbstractSmackInteroperabilityLayer smackInteroperabilityLayer = AbstractSmackInteroperabilityLayer.getInstance(); /** Initializes a new <tt>ColibriIQProvider</tt> instance. */ public ColibriIQProvider() { smackInteroperabilityLayer.addExtensionProvider( PayloadTypePacketExtension.ELEMENT_NAME, ColibriConferenceIQ.NAMESPACE, new DefaultPacketExtensionProvider<PayloadTypePacketExtension>( PayloadTypePacketExtension.class)); smackInteroperabilityLayer.addExtensionProvider( RtcpFbPacketExtension.ELEMENT_NAME, RtcpFbPacketExtension.NAMESPACE, new DefaultPacketExtensionProvider<RtcpFbPacketExtension>( RtcpFbPacketExtension.class)); smackInteroperabilityLayer.addExtensionProvider( RTPHdrExtPacketExtension.ELEMENT_NAME, ColibriConferenceIQ.NAMESPACE, new DefaultPacketExtensionProvider<RTPHdrExtPacketExtension>( RTPHdrExtPacketExtension.class)); smackInteroperabilityLayer.addExtensionProvider( SourcePacketExtension.ELEMENT_NAME, SourcePacketExtension.NAMESPACE, new DefaultPacketExtensionProvider<SourcePacketExtension>( SourcePacketExtension.class)); smackInteroperabilityLayer.addExtensionProvider( SourceGroupPacketExtension.ELEMENT_NAME, SourceGroupPacketExtension.NAMESPACE, new DefaultPacketExtensionProvider<SourceGroupPacketExtension>( SourceGroupPacketExtension.class)); PacketExtensionProvider parameterProvider = new DefaultPacketExtensionProvider<ParameterPacketExtension>( ParameterPacketExtension.class); smackInteroperabilityLayer.addExtensionProvider( ParameterPacketExtension.ELEMENT_NAME, ColibriConferenceIQ.NAMESPACE, parameterProvider); smackInteroperabilityLayer.addExtensionProvider( ParameterPacketExtension.ELEMENT_NAME, SourcePacketExtension.NAMESPACE, parameterProvider); // Shutdown IQ smackInteroperabilityLayer.addIQProvider( ShutdownIQ.GRACEFUL_ELEMENT_NAME, ShutdownIQ.NAMESPACE, this); smackInteroperabilityLayer.addIQProvider( ShutdownIQ.FORCE_ELEMENT_NAME, ShutdownIQ.NAMESPACE, this); // Shutdown extension PacketExtensionProvider shutdownProvider = new DefaultPacketExtensionProvider <ColibriConferenceIQ.GracefulShutdown>( ColibriConferenceIQ.GracefulShutdown.class); smackInteroperabilityLayer.addExtensionProvider( ColibriConferenceIQ.GracefulShutdown.ELEMENT_NAME, ColibriConferenceIQ.GracefulShutdown.NAMESPACE, shutdownProvider); // ColibriStatsIQ smackInteroperabilityLayer.addIQProvider( ColibriStatsIQ.ELEMENT_NAME, ColibriStatsIQ.NAMESPACE, this); // ColibriStatsExtension PacketExtensionProvider statsProvider = new DefaultPacketExtensionProvider<ColibriStatsExtension>( ColibriStatsExtension.class); smackInteroperabilityLayer.addExtensionProvider( ColibriStatsExtension.ELEMENT_NAME, ColibriStatsExtension.NAMESPACE, statsProvider); // ColibriStatsExtension.Stat PacketExtensionProvider statProvider = new DefaultPacketExtensionProvider <ColibriStatsExtension.Stat>( ColibriStatsExtension.Stat.class); smackInteroperabilityLayer.addExtensionProvider( ColibriStatsExtension.Stat.ELEMENT_NAME, ColibriStatsExtension.NAMESPACE, statProvider); } private void addChildExtension( ColibriConferenceIQ.Channel channel, PacketExtension childExtension) { if (childExtension instanceof PayloadTypePacketExtension) { PayloadTypePacketExtension payloadType = (PayloadTypePacketExtension) childExtension; if ("opus".equals(payloadType.getName()) && (payloadType.getChannels() != 2)) { /* * We only have a Format for opus with 2 channels, because it * MUST be advertised with 2 channels. Fixing the number of * channels here allows us to be compatible with agents who * advertise it with 1 channel. */ payloadType.setChannels(2); } channel.addPayloadType(payloadType); } else if (childExtension instanceof IceUdpTransportPacketExtension) { IceUdpTransportPacketExtension transport = (IceUdpTransportPacketExtension) childExtension; channel.setTransport(transport); } else if (childExtension instanceof SourceGroupPacketExtension) { SourceGroupPacketExtension sourceGroup = (SourceGroupPacketExtension)childExtension; channel.addSourceGroup(sourceGroup); } else if (childExtension instanceof RTPHdrExtPacketExtension) { RTPHdrExtPacketExtension rtpHdrExtPacketExtension = (RTPHdrExtPacketExtension) childExtension; channel.addRtpHeaderExtension(rtpHdrExtPacketExtension); } } private void addChildExtension( ColibriConferenceIQ.ChannelBundle bundle, PacketExtension childExtension) { if (childExtension instanceof IceUdpTransportPacketExtension) { IceUdpTransportPacketExtension transport = (IceUdpTransportPacketExtension) childExtension; bundle.setTransport(transport); } } private void addChildExtension( ColibriConferenceIQ.SctpConnection sctpConnection, PacketExtension childExtension) { if (childExtension instanceof IceUdpTransportPacketExtension) { IceUdpTransportPacketExtension transport = (IceUdpTransportPacketExtension) childExtension; sctpConnection.setTransport(transport); } } private PacketExtension parseExtension( XmlPullParser parser, String name, String namespace) throws Exception { PacketExtensionProvider extensionProvider = smackInteroperabilityLayer.getExtensionProvider( name, namespace); PacketExtension extension; if (extensionProvider == null) { /* * No PacketExtensionProvider for the specified name and namespace * has been registered. Throw away the element. */ throwAway(parser, name); extension = null; } else { extension = extensionProvider.parseExtension(parser); } return extension; } /** * Parses an IQ sub-document and creates an * <tt>org.jivesoftware.smack.packet.IQ</tt> instance. * * @param parser an <tt>XmlPullParser</tt> which specifies the IQ * sub-document to be parsed into a new <tt>IQ</tt> instance * @return a new <tt>IQ</tt> instance parsed from the specified IQ * sub-document * @see IQProvider#parseIQ(XmlPullParser) */ @SuppressWarnings("deprecation") // Compatibility with legacy Jitsi and // Jitsi Videobridge public IQ parseIQ(XmlPullParser parser) throws Exception { String namespace = parser.getNamespace(); IQ iq; if (ColibriConferenceIQ.ELEMENT_NAME.equals(parser.getName()) && ColibriConferenceIQ.NAMESPACE.equals(namespace)) { ColibriConferenceIQ conference = new ColibriConferenceIQ(); String conferenceID = parser .getAttributeValue("", ColibriConferenceIQ.ID_ATTR_NAME); if ((conferenceID != null) && (conferenceID.length() != 0)) conference.setID(conferenceID); String conferenceName = parser .getAttributeValue("", ColibriConferenceIQ.NAME_ATTR_NAME); if ((conferenceName != null) && (conferenceName.length() != 0)) conference.setName(conferenceName); boolean done = false; ColibriConferenceIQ.Channel channel = null; ColibriConferenceIQ.RTCPTerminationStrategy rtcpTerminationStrategy = null; ColibriConferenceIQ.SctpConnection sctpConnection = null; ColibriConferenceIQ.ChannelBundle bundle = null; ColibriConferenceIQ.Content content = null; ColibriConferenceIQ.Recording recording = null; ColibriConferenceIQ.Endpoint conferenceEndpoint = null; StringBuilder ssrc = null; SourcePacketExtension ssrcPe = null; while (!done) { switch (parser.next()) { case XmlPullParser.END_TAG: { String name = parser.getName(); if (ColibriConferenceIQ.ELEMENT_NAME.equals(name)) { done = true; } else if (ColibriConferenceIQ.Channel.ELEMENT_NAME.equals( name)) { content.addChannel(channel); channel = null; } else if (ColibriConferenceIQ.SctpConnection.ELEMENT_NAME .equals(name)) { if (sctpConnection != null) content.addSctpConnection(sctpConnection); sctpConnection = null; } else if (ColibriConferenceIQ.ChannelBundle.ELEMENT_NAME .equals(name)) { if (bundle != null) { conference.addChannelBundle(bundle); bundle = null; } } else if (ColibriConferenceIQ.Endpoint.ELEMENT_NAME .equals(name)) { conference.addEndpoint(conferenceEndpoint); conferenceEndpoint = null; } else if (ColibriConferenceIQ.Channel.SSRC_ELEMENT_NAME .equals(name)) { String s = ssrc.toString().trim(); if (s.length() != 0) { int i; /* * Legacy versions of Jitsi and Jitsi Videobridge * may send a synchronization source (SSRC) * identifier as a negative integer. */ if (s.startsWith("-")) i = Integer.parseInt(s); else i = (int) Long.parseLong(s); channel.addSSRC(i); } ssrc = null; } else if (SourcePacketExtension.ELEMENT_NAME.equals(name)) { if (channel != null && ssrcPe != null) { channel.addSource(ssrcPe); } ssrcPe = null; } else if (ColibriConferenceIQ.Content.ELEMENT_NAME.equals( name)) { conference.addContent(content); content = null; } else if (ColibriConferenceIQ.RTCPTerminationStrategy .ELEMENT_NAME.equals(name)) { conference.setRTCPTerminationStrategy( rtcpTerminationStrategy); rtcpTerminationStrategy = null; } else if (ColibriConferenceIQ.Recording.ELEMENT_NAME.equals( name)) { conference.setRecording(recording); recording = null; } else if (ColibriConferenceIQ.GracefulShutdown.ELEMENT_NAME .equals(name)) { conference.setGracefulShutdown(true); } break; } case XmlPullParser.START_TAG: { String name = parser.getName(); if (ColibriConferenceIQ.Channel.ELEMENT_NAME.equals(name)) { channel = new ColibriConferenceIQ.Channel(); // direction String direction = parser.getAttributeValue( "", ColibriConferenceIQ.Channel .DIRECTION_ATTR_NAME); if ((direction != null) && (direction.length() != 0)) { channel.setDirection( MediaDirection.parseString(direction)); } // endpoint String endpoint = parser.getAttributeValue( "", ColibriConferenceIQ.Channel .ENDPOINT_ATTR_NAME); if ((endpoint != null) && (endpoint.length() != 0)) channel.setEndpoint(endpoint); String channelBundleId = parser.getAttributeValue( "", ColibriConferenceIQ.ChannelCommon .CHANNEL_BUNDLE_ID_ATTR_NAME); if (!StringUtils.isNullOrEmpty(channelBundleId)) channel.setChannelBundleId(channelBundleId); // expire String expire = parser.getAttributeValue( "", ColibriConferenceIQ.Channel .EXPIRE_ATTR_NAME); if ((expire != null) && (expire.length() != 0)) channel.setExpire(Integer.parseInt(expire)); String packetDelay = parser.getAttributeValue( "", ColibriConferenceIQ.Channel .PACKET_DELAY_ATTR_NAME); if (!StringUtils.isNullOrEmpty(packetDelay)) channel.setPacketDelay( Integer.parseInt(packetDelay)); // host String host = parser.getAttributeValue( "", ColibriConferenceIQ.Channel.HOST_ATTR_NAME); if ((host != null) && (host.length() != 0)) channel.setHost(host); // id String channelID = parser.getAttributeValue( "", ColibriConferenceIQ.Channel.ID_ATTR_NAME); if ((channelID != null) && (channelID.length() != 0)) channel.setID(channelID); // initiator String initiator = parser.getAttributeValue( "", ColibriConferenceIQ.Channel .INITIATOR_ATTR_NAME); if ((initiator != null) && (initiator.length() != 0)) channel.setInitiator(Boolean.valueOf(initiator)); // lastN String lastN = parser.getAttributeValue( "", ColibriConferenceIQ.Channel .LAST_N_ATTR_NAME); if ((lastN != null) && (lastN.length() != 0)) channel.setLastN(Integer.parseInt(lastN)); // simulcastMode String simulcastMode = parser.getAttributeValue( "", ColibriConferenceIQ.Channel .SIMULCAST_MODE_ATTR_NAME); if (!StringUtils.isNullOrEmpty(simulcastMode)) channel.setSimulcastMode( SimulcastMode.fromString(simulcastMode)); // receiving simulcast layer String receivingSimulcastLayer = parser.getAttributeValue( "", ColibriConferenceIQ.Channel .RECEIVING_SIMULCAST_LAYER); if ((receivingSimulcastLayer != null) && (receivingSimulcastLayer.length() != 0)) channel.setReceivingSimulcastLayer( Integer.parseInt(receivingSimulcastLayer)); // rtcpPort String rtcpPort = parser.getAttributeValue( "", ColibriConferenceIQ.Channel .RTCP_PORT_ATTR_NAME); if ((rtcpPort != null) && (rtcpPort.length() != 0)) channel.setRTCPPort(Integer.parseInt(rtcpPort)); // rtpLevelRelayType String rtpLevelRelayType = parser.getAttributeValue( "", ColibriConferenceIQ.Channel .RTP_LEVEL_RELAY_TYPE_ATTR_NAME); if ((rtpLevelRelayType != null) && (rtpLevelRelayType.length() != 0)) { channel.setRTPLevelRelayType(rtpLevelRelayType); } // rtpPort String rtpPort = parser.getAttributeValue( "", ColibriConferenceIQ.Channel .RTP_PORT_ATTR_NAME); if ((rtpPort != null) && (rtpPort.length() != 0)) channel.setRTPPort(Integer.parseInt(rtpPort)); } else if (ColibriConferenceIQ.ChannelBundle .ELEMENT_NAME.equals(name)) { String bundleId = parser.getAttributeValue( "", ColibriConferenceIQ .ChannelBundle.ID_ATTR_NAME); if(!StringUtils.isNullOrEmpty(bundleId)) { bundle = new ColibriConferenceIQ .ChannelBundle(bundleId); } } else if (ColibriConferenceIQ.RTCPTerminationStrategy .ELEMENT_NAME.equals(name)) { rtcpTerminationStrategy = new ColibriConferenceIQ.RTCPTerminationStrategy(); // name String strategyName = parser.getAttributeValue( "", ColibriConferenceIQ.RTCPTerminationStrategy .NAME_ATTR_NAME); if ((strategyName != null) && (strategyName.length() != 0)) rtcpTerminationStrategy.setName(strategyName); } else if (ColibriConferenceIQ.Channel.SSRC_ELEMENT_NAME .equals(name)) { ssrc = new StringBuilder(); } else if (SourcePacketExtension.ELEMENT_NAME.equals(name)) { ssrcPe = new SourcePacketExtension(); String ssrcStr = parser.getAttributeValue( "", SourcePacketExtension.SSRC_ATTR_NAME); if (!StringUtils.isNullOrEmpty(ssrcStr)) { ssrcPe.setSSRC(Long.parseLong(ssrcStr)); } } else if (ColibriConferenceIQ.Content.ELEMENT_NAME.equals( name)) { content = new ColibriConferenceIQ.Content(); String contentName = parser.getAttributeValue( "", ColibriConferenceIQ.Content.NAME_ATTR_NAME); if ((contentName != null) && (contentName.length() != 0)) content.setName(contentName); } else if (ColibriConferenceIQ.Recording.ELEMENT_NAME.equals( name)) { String stateStr = parser.getAttributeValue( "", ColibriConferenceIQ.Recording.STATE_ATTR_NAME); String token = parser.getAttributeValue( "", ColibriConferenceIQ.Recording.TOKEN_ATTR_NAME); recording = new ColibriConferenceIQ.Recording( stateStr, token); } else if (ColibriConferenceIQ.SctpConnection.ELEMENT_NAME .equals(name)) { // Endpoint String endpoint = parser.getAttributeValue( "", ColibriConferenceIQ. SctpConnection.ENDPOINT_ATTR_NAME); // id String connID = parser.getAttributeValue( "", ColibriConferenceIQ. ChannelCommon.ID_ATTR_NAME); if(StringUtils.isNullOrEmpty(connID) && StringUtils.isNullOrEmpty(endpoint)) { sctpConnection = null; continue; } sctpConnection = new ColibriConferenceIQ.SctpConnection(); if (!StringUtils.isNullOrEmpty(connID)) sctpConnection.setID(connID); if (!StringUtils.isNullOrEmpty(endpoint)) sctpConnection.setEndpoint(endpoint); // port String port = parser.getAttributeValue( "", ColibriConferenceIQ.SctpConnection.PORT_ATTR_NAME); if (!StringUtils.isNullOrEmpty(port)) sctpConnection.setPort(Integer.parseInt(port)); String channelBundleId = parser.getAttributeValue( "", ColibriConferenceIQ.ChannelCommon .CHANNEL_BUNDLE_ID_ATTR_NAME); if (!StringUtils.isNullOrEmpty(channelBundleId)) sctpConnection.setChannelBundleId(channelBundleId); // initiator String initiator = parser.getAttributeValue( "", ColibriConferenceIQ.SctpConnection .INITIATOR_ATTR_NAME); if (!StringUtils.isNullOrEmpty(initiator)) sctpConnection.setInitiator( Boolean.valueOf(initiator)); // expire String expire = parser.getAttributeValue( "", ColibriConferenceIQ.SctpConnection .EXPIRE_ATTR_NAME); if (!StringUtils.isNullOrEmpty(expire)) sctpConnection.setExpire(Integer.parseInt(expire)); } else if (ColibriConferenceIQ.Endpoint.ELEMENT_NAME .equals(name)) { String id = parser.getAttributeValue( "", ColibriConferenceIQ.Endpoint.ID_ATTR_NAME); String endpointName = parser.getAttributeValue( "", ColibriConferenceIQ.Endpoint .DISPLAYNAME_ATTR_NAME); conferenceEndpoint = new ColibriConferenceIQ.Endpoint(id, endpointName); } else if ( channel != null || sctpConnection != null || bundle != null ) { String peName = null; String peNamespace = null; if (IceUdpTransportPacketExtension.ELEMENT_NAME .equals(name) && IceUdpTransportPacketExtension.NAMESPACE .equals(parser.getNamespace())) { peName = name; peNamespace = IceUdpTransportPacketExtension.NAMESPACE; } else if (PayloadTypePacketExtension.ELEMENT_NAME.equals( name)) { /* * The channel element of the Jitsi Videobridge * protocol reuses the payload-type element defined * in XEP-0167: Jingle RTP Sessions. */ peName = name; peNamespace = namespace; } else if (RtcpFbPacketExtension.ELEMENT_NAME.equals( name) && RtcpFbPacketExtension.NAMESPACE .equals(parser.getNamespace())) { /* * The channel element of the Jitsi Videobridge * protocol reuses the payload-type element defined * in XEP-0167: Jingle RTP Sessions. */ peName = name; peNamespace = namespace; } else if (RTPHdrExtPacketExtension.ELEMENT_NAME.equals( name)) { /* * The channel element of the Jitsi Videobridge * protocol reuses the rtp-hdrext element defined * in XEP-0167: Jingle RTP Sessions. */ peName = name; peNamespace = namespace; } else if (RawUdpTransportPacketExtension.ELEMENT_NAME .equals(name) && RawUdpTransportPacketExtension.NAMESPACE .equals(parser.getNamespace())) { peName = name; peNamespace = RawUdpTransportPacketExtension.NAMESPACE; } else if (SourcePacketExtension.ELEMENT_NAME.equals(name) && SourcePacketExtension.NAMESPACE.equals( parser.getNamespace())) { peName = name; peNamespace = SourcePacketExtension.NAMESPACE; } else if (SourceGroupPacketExtension.ELEMENT_NAME .equals(name) && SourceGroupPacketExtension.NAMESPACE .equals(parser.getNamespace())) { peName = name; peNamespace = SourceGroupPacketExtension.NAMESPACE; } if (peName == null) { throwAway(parser, name); } else { PacketExtension extension = parseExtension(parser, peName, peNamespace); if (extension != null) { if(channel != null) addChildExtension(channel, extension); else if (sctpConnection != null) addChildExtension(sctpConnection, extension); else addChildExtension(bundle, extension); } } } break; } case XmlPullParser.TEXT: { if (ssrc != null) ssrc.append(parser.getText()); break; } } } iq = conference; } else if (ShutdownIQ.NAMESPACE.equals(namespace) && ShutdownIQ.isValidElementName(parser.getName())) { String rootElement = parser.getName(); iq = ShutdownIQ.createShutdownIQ(rootElement); boolean done = false; while (!done) { switch (parser.next()) { case XmlPullParser.END_TAG: { String name = parser.getName(); if (rootElement.equals(name)) { done = true; } break; } } } } else if (ColibriStatsIQ.ELEMENT_NAME.equals(parser.getName()) && ColibriStatsIQ.NAMESPACE.equals(namespace)) { String rootElement = parser.getName(); ColibriStatsIQ statsIQ = new ColibriStatsIQ(); iq = statsIQ; ColibriStatsExtension.Stat stat = null; boolean done = false; while (!done) { switch (parser.next()) { case XmlPullParser.START_TAG: { String name = parser.getName(); if (ColibriStatsExtension.Stat .ELEMENT_NAME.equals(name)) { stat = new ColibriStatsExtension.Stat(); String statName = parser.getAttributeValue( "", ColibriStatsExtension.Stat.NAME_ATTR_NAME); stat.setName(statName); String statValue = parser.getAttributeValue( "", ColibriStatsExtension.Stat.VALUE_ATTR_NAME); stat.setValue(statValue); } break; } case XmlPullParser.END_TAG: { String name = parser.getName(); if (rootElement.equals(name)) { done = true; } else if (ColibriStatsExtension.Stat.ELEMENT_NAME .equals(name)) { if (stat != null) { statsIQ.addStat(stat); stat = null; } } break; } } } } else iq = null; return iq; } /** * Parses using a specific <tt>XmlPullParser</tt> and ignores XML content * presuming that the specified <tt>parser</tt> is currently at the start * tag of an element with a specific name and throwing away until the end * tag with the specified name is encountered. * * @param parser the <tt>XmlPullParser</tt> which parses the XML content * @param name the name of the element at the start tag of which the * specified <tt>parser</tt> is presumed to currently be and until the end * tag of which XML content is to be thrown away * @throws Exception if an errors occurs while parsing the XML content */ private void throwAway(XmlPullParser parser, String name) throws Exception { while ((XmlPullParser.END_TAG != parser.next()) || !name.equals(parser.getName())); } }