diff --git a/doc/rtcp-termination.md b/doc/rtcp-termination.md index c1f2fd8baf..3d4ebd25e2 100644 --- a/doc/rtcp-termination.md +++ b/doc/rtcp-termination.md @@ -65,7 +65,8 @@ We implemented that in the following way : - libjitsi and/or other parts of the video bridge can override the default RTCP report generation behavior by parametrizing the RTP session manager (in FMJ) with an implementation of the - `RTCPReportBuilder` interface. + `RTCPReportBuilder` interface. Keep in mind that in the JVB each content + has its own RTP translator. - Created an interface called `RTCPPacketTransformer` whose purpose is to inspect and/or modify and/or eliminate incoming RTCP packets. It diff --git a/lib/fmj.jar b/lib/fmj.jar index 3de9f38279..8c510757f9 100644 Binary files a/lib/fmj.jar and b/lib/fmj.jar differ diff --git a/lib/libjitsi-1.0-SNAPSHOT.jar b/lib/libjitsi-1.0-SNAPSHOT.jar index f6bfe149a0..dfd8b4c477 100644 Binary files a/lib/libjitsi-1.0-SNAPSHOT.jar and b/lib/libjitsi-1.0-SNAPSHOT.jar differ diff --git a/src/main/java/org/jitsi/videobridge/Content.java b/src/main/java/org/jitsi/videobridge/Content.java index a4b938a2a9..636f0f41ee 100644 --- a/src/main/java/org/jitsi/videobridge/Content.java +++ b/src/main/java/org/jitsi/videobridge/Content.java @@ -15,7 +15,6 @@ */ package org.jitsi.videobridge; -import java.beans.*; import java.io.*; import java.lang.ref.*; import java.util.*; @@ -23,13 +22,11 @@ import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*; import org.jitsi.impl.neomedia.rtp.translator.*; -import org.jitsi.service.configuration.*; import org.jitsi.service.neomedia.*; import org.jitsi.service.neomedia.device.*; import org.jitsi.service.neomedia.recording.*; import org.jitsi.util.*; import org.jitsi.videobridge.eventadmin.*; -import org.jitsi.videobridge.rtcp.*; import org.osgi.framework.*; /** @@ -63,8 +60,6 @@ public class Content */ private boolean expired = false; - private String fallbackSrategyFQN; - /** * The local synchronization source identifier (SSRC) associated with this * Content, which is to to be pre-announced by the @@ -117,8 +112,6 @@ public class Content private RTCPFeedbackMessageSender rtcpFeedbackMessageSender; - private String rtcpTerminationStrategyFQN; - /** * The Object which synchronizes the access to the RTP-level relays * (i.e. {@link #mixer} and {@link #rtpTranslator}) provided by this @@ -151,23 +144,6 @@ public Content(Conference conference, String name) this.conference = conference; this.name = name; - // If endpoints have changed, maybe change the RTCP termination - // strategy. - // TODO(gp) this should not always be enabled, it should be configurable - this.conference.addPropertyChangeListener( - new PropertyChangeListener() - { - @Override - public void propertyChange(PropertyChangeEvent ev) - { - if (Conference.ENDPOINTS_PROPERTY_NAME.equals( - ev.getPropertyName())) - { - updateRTCPTerminationStrategy(); - } - } - }); - mediaType = MediaType.parseString(this.name); EventAdmin eventAdmin @@ -487,7 +463,7 @@ void feedKnownSsrcsToSynchronizer() * ssrc in its list of received SSRCs, or null in case no * such Channel exists. */ - Channel findChannel(long ssrc) + public Channel findChannel(long ssrc) { for (Channel channel : getChannels()) { @@ -790,13 +766,24 @@ public RTPTranslator getRTPTranslator() RTPTranslatorImpl rtpTranslatorImpl = (RTPTranslatorImpl) rtpTranslator; + /** + * XXX(gp) some thoughts on the use of initialLocalSSRC: + * + * 1. By using the initialLocalSSRC as the SSRC of the + * translator aren't we breaking the mixing + * functionality? because FMJ is going to use its "own" + * SSRC to for mixed stream, which remains unannounced. + * + * 2. By using an initialLocalSSRC we're losing the FMJ + * collision detection mechanism. + * + * The places that are involved in this have been tagged + * with TAG(cat4-local-ssrc-hurricane). + */ initialLocalSSRC = Videobridge.RANDOM.nextInt(); rtpTranslatorImpl.setLocalSSRC(initialLocalSSRC); - if (MediaType.VIDEO.equals(getMediaType())) - setRTCPTerminationStrategyFromConfiguration(); - rtcpFeedbackMessageSender = rtpTranslatorImpl.getRtcpFeedbackMessageSender(); } @@ -891,63 +878,6 @@ public boolean setRecording(boolean recording, String path) return this.recording; } - public void setRTCPTerminationFallbackStrategyFQN( - String rtcpTerminationFallbackStrategyFQN) - { - this.fallbackSrategyFQN = rtcpTerminationFallbackStrategyFQN; - } - - public void setRTCPTerminationStrategyFQN(String strategyFQN) - { - // NOTE(gp) we want to *always* update the RTCP termination strategy, - // even if rtcpTerminationStrategyFQN.equals(strategyFQN). - // - // The reason for this is that "this.rtpTranslator" (the translator of - // this content) can be "null" when the updateRTCPTerminationStrategy() - // method is called, and, as a result, "this.rtpTranslator" might have - // not been configured with the correct RTCP termination strategy. - // - // This is especially true when adaptive last N and adaptive simulcast - // are used. Those two features need the basic bridge RTCP termination - // strategy but, when they set it, "this.translator" is "null". - // - // Calling the updateRTCPTerminationStrategy() method even if - // rtcpTerminationStrategyFQN.equals(strategyFQN) is fine because the - // method checks for class equality. - - this.rtcpTerminationStrategyFQN = strategyFQN; - this.updateRTCPTerminationStrategy(); - } - - /** - * Sets the RTCP termination strategy of the rtpTranslator to the - * one specified in the configuration. - * - */ - public void setRTCPTerminationStrategyFromConfiguration() - { - if (!MediaType.VIDEO.equals(mediaType)) - { - return; - } - - ConfigurationService cfg = getConference().getVideobridge() - .getConfigurationService(); - - if (cfg != null) - { - String fallbackFQN = cfg.getString( - Videobridge.RTCP_TERMINATION_FALLBACK_STRATEGY_PNAME, ""); - - setRTCPTerminationFallbackStrategyFQN(fallbackFQN); - - String strategyFQN = cfg.getString( - Videobridge.RTCP_TERMINATION_STRATEGY_PNAME, ""); - - setRTCPTerminationStrategyFQN(strategyFQN); - } - } - /** * Tries to start a specific Recorder. * @param recorder the Recorder to start. @@ -993,68 +923,6 @@ public void touch() } } - /** - * Sets the RTCP termination strategy of the rtpTranslator to the - * one specified in the rtcpTerminationStrategyFQN parameter. - * - */ - private void updateRTCPTerminationStrategy() - { - Conference conf = this.conference; - - if (conf == null) - return; - - RTPTranslator rtpTranslator = this.rtpTranslator; - - if (rtpTranslator == null) - return; - - String strategyFQN; - - // If the conference has less than 3 participants, it switches the RTCP - // termination strategy to the RTCP termination strategy defined by - // {@link fallbackSrategyFQN}. It restores the configured RTCP - // termination strategy otherwise. - if (conf.getEndpointCount() < 3) - { - strategyFQN = this.fallbackSrategyFQN; - if (StringUtils.isNullOrEmpty(strategyFQN)) - strategyFQN = this.rtcpTerminationStrategyFQN; - } - else - { - strategyFQN = this.rtcpTerminationStrategyFQN; - } - - if (StringUtils.isNullOrEmpty(strategyFQN)) - return; - - try - { - Class clazz = Class.forName(strategyFQN); - RTCPTerminationStrategy oldStrategy - = rtpTranslator.getRTCPTerminationStrategy(); - - if (clazz.isInstance(oldStrategy)) - return; - - RTCPTerminationStrategy strategy - = (RTCPTerminationStrategy) clazz.newInstance(); - - if (strategy instanceof AbstractBridgeRTCPTerminationStrategy) - ((AbstractBridgeRTCPTerminationStrategy) strategy).setConference(conf); - - rtpTranslator.setRTCPTerminationStrategy(strategy); - } - catch (Exception e) - { - logger.error( - "Failed to configure the RTCP termination strategy", - e); - } - } - private static class RTPTranslatorWriteFilter implements RTPTranslator.WriteFilter { diff --git a/src/main/java/org/jitsi/videobridge/VideoChannel.java b/src/main/java/org/jitsi/videobridge/VideoChannel.java index 049c9f9a14..2d3e3fc678 100644 --- a/src/main/java/org/jitsi/videobridge/VideoChannel.java +++ b/src/main/java/org/jitsi/videobridge/VideoChannel.java @@ -30,10 +30,13 @@ import org.jitsi.impl.neomedia.*; import org.jitsi.impl.neomedia.rtcp.*; +import org.jitsi.impl.neomedia.rtcp.termination.strategies.*; import org.jitsi.impl.neomedia.rtp.remotebitrateestimator.*; import org.jitsi.impl.neomedia.transform.*; +import org.jitsi.service.configuration.*; +import org.jitsi.service.neomedia.*; import org.jitsi.service.neomedia.codec.*; -import org.jitsi.util.Logger; +import org.jitsi.util.*; import org.jitsi.videobridge.ratecontrol.*; import org.jitsi.videobridge.rtcp.*; import org.jitsi.videobridge.simulcast.*; @@ -57,6 +60,13 @@ public class VideoChannel */ private static final int INCOMING_BITRATE_INTERVAL_MS = 5000; + /** + * The name of the property which specifies the FQN name of the RTCP + * strategy to use by default. + */ + public static final String RTCP_TERMINATION_STRATEGY_PNAME + = "org.jitsi.videobridge.rtcp.strategy"; + /** * The Logger used by the VideoChannel class and its * instances to print debug information. @@ -183,6 +193,66 @@ public VideoChannel(Content content, setTransformEngine(new RtpChannelTransformEngine(this)); } + /** + * {@inheritDoc} + * + * Creates media stream. + */ + @Override + public void initialize() + throws IOException + { + super.initialize(); + + ConfigurationService configurationService + = getContent().getConference().getVideobridge() + .getConfigurationService(); + + if (configurationService == null) + { + return; + } + + // Initialize the RTCP termination strategy from the configuration. + String strategyFQN = configurationService.getString( + RTCP_TERMINATION_STRATEGY_PNAME, ""); + + RTCPTerminationStrategy strategy; + if (StringUtils.isNullOrEmpty(strategyFQN)) + { + return; + } + + try + { + Class clazz = Class.forName(strategyFQN); + strategy = (RTCPTerminationStrategy) clazz.newInstance(); + } + catch (Exception e) + { + logger.error( + "Failed to configure the video channel RTCP termination strategy", + e); + + return; + } + + // Initialize the RTCP termination strategy. + if (strategy instanceof MediaStreamRTCPTerminationStrategy) + { + ((MediaStreamRTCPTerminationStrategy) strategy) + .initialize(getStream()); + } + else if (strategy + instanceof VideoChannelRTCPTerminationStrategy) + { + ((VideoChannelRTCPTerminationStrategy) strategy) + .initialize(this); + } + + getStream().setRTCPTerminationStrategy(strategy); + } + /** * {@inheritDoc} */ @@ -878,14 +948,6 @@ private void sendLastNEndpointsChangeEventOnDataChannel( public void setAdaptiveLastN(boolean adaptiveLastN) { this.adaptiveLastN = adaptiveLastN; - - if (adaptiveLastN) - { - // Ensure that we are using BasicBridgeRTCPTerminationStrategy, - // which is currently needed to notify us of incoming REMBs. - getContent().setRTCPTerminationStrategyFQN( - BasicBridgeRTCPTerminationStrategy.class.getName()); - } } /** @@ -895,14 +957,6 @@ public void setAdaptiveLastN(boolean adaptiveLastN) public void setAdaptiveSimulcast(boolean adaptiveSimulcast) { this.adaptiveSimulcast = adaptiveSimulcast; - - if (adaptiveSimulcast) - { - // Ensure that we are using BasicBridgeRTCPTerminationStrategy, - // which is currently needed to notify us of incoming REMBs. - getContent().setRTCPTerminationStrategyFQN( - BasicBridgeRTCPTerminationStrategy.class.getName()); - } } /** @@ -1265,10 +1319,18 @@ public void handleNACK(NACKPacket nackPacket) logger.debug("Retransmitting packet from cache. SSRC " + ssrc + " seq " + seq); } - getStream().injectPacket( - createPacketForRetransmission(pkt), - true, - true); + try + { + getStream().injectPacket( + createPacketForRetransmission(pkt), + true, + true); + } + catch (TransmissionFailedException e) + { + logger.warn("Failed to inject packet in MediaStream: " + + e); + } iter.remove(); } } @@ -1344,7 +1406,15 @@ public void handleNACK(NACKPacket nackPacket) + " on channel " + c.getID()); } - c.getStream().injectPacket(pkt, false, true); + try + { + c.getStream().injectPacket(pkt, false, true); + } + catch (TransmissionFailedException e) + { + logger.warn("Failed to inject packet in MediaStream: " + + e); + } } } } diff --git a/src/main/java/org/jitsi/videobridge/Videobridge.java b/src/main/java/org/jitsi/videobridge/Videobridge.java index e4f00232ab..2e89aa97c5 100644 --- a/src/main/java/org/jitsi/videobridge/Videobridge.java +++ b/src/main/java/org/jitsi/videobridge/Videobridge.java @@ -130,20 +130,6 @@ public class Videobridge public static final String REST_API_PNAME = "org.jitsi.videobridge." + REST_API; - /** - * The name of the property which specifies the FQN name of the RTCP - * strategy to use when there are less than 3 participants. - */ - static final String RTCP_TERMINATION_FALLBACK_STRATEGY_PNAME - = "org.jitsi.videobridge.rtcp.fallbackStrategy"; - - /** - * The name of the property which specifies the FQN name of the RTCP - * strategy to use by default. - */ - static final String RTCP_TERMINATION_STRATEGY_PNAME - = "org.jitsi.videobridge.rtcp.strategy"; - /** * The property that specifies allowed entities for turning on graceful * shutdown mode. For XMPP API this is "from" JID. In case of REST @@ -698,26 +684,7 @@ else if (authorizedSourcePattern != null && } } - // Get the RTCP termination strategy. - ColibriConferenceIQ.RTCPTerminationStrategy strategyIQ - = conferenceIQ.getRTCPTerminationStrategy(); - String strategyFQN; - - if (strategyIQ == null) - { - strategyFQN = null; - } - else - { - strategyFQN = strategyIQ.getName(); - if (strategyFQN != null) - { - strategyFQN = strategyFQN.trim(); - if (strategyFQN.length() == 0) - strategyFQN = null; - } - } - + // TODO(gp) Remove ColibriConferenceIQ.RTCPTerminationStrategy for (ColibriConferenceIQ.Content contentIQ : conferenceIQ.getContents()) { @@ -735,10 +702,6 @@ else if (authorizedSourcePattern != null && } else { - // Set the RTCP termination strategy. - if (strategyFQN != null) - content.setRTCPTerminationStrategyFQN(strategyFQN); - ColibriConferenceIQ.Content responseContentIQ = new ColibriConferenceIQ.Content(content.getName()); diff --git a/src/main/java/org/jitsi/videobridge/rtcp/AbstractBridgeRTCPTerminationStrategy.java b/src/main/java/org/jitsi/videobridge/rtcp/AbstractBridgeRTCPTerminationStrategy.java deleted file mode 100644 index 0b7f28477b..0000000000 --- a/src/main/java/org/jitsi/videobridge/rtcp/AbstractBridgeRTCPTerminationStrategy.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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 org.jitsi.videobridge.rtcp; - -import org.jitsi.impl.neomedia.rtcp.termination.strategies.*; -import org.jitsi.videobridge.*; - -/** - * @author George Politis - */ -public abstract class AbstractBridgeRTCPTerminationStrategy - extends AbstractRTCPTerminationStrategy -{ - private Conference conference; - - /** - * Sets the Conference associated to this - * BridgeRTCPTerminationStrategy - * - * @param conference The Conference associated to this - * BridgeRTCPTerminationStrategy - */ - public void setConference(Conference conference) - { - this.conference = conference; - } - - /** - * Gets the Conference associated to this - * BridgeRTCPTerminationStrategy - * - * @return The Conference associated to this - * BridgeRTCPTerminationStrategy - */ - public Conference getConference() - { - return conference; - } -} diff --git a/src/main/java/org/jitsi/videobridge/rtcp/BasicBridgeRTCPTerminationStrategy.java b/src/main/java/org/jitsi/videobridge/rtcp/BasicBridgeRTCPTerminationStrategy.java deleted file mode 100644 index 441eb64c7f..0000000000 --- a/src/main/java/org/jitsi/videobridge/rtcp/BasicBridgeRTCPTerminationStrategy.java +++ /dev/null @@ -1,303 +0,0 @@ -/* - * 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 org.jitsi.videobridge.rtcp; - -import net.sf.fmj.media.rtp.*; - -import org.jitsi.impl.neomedia.rtcp.*; -import org.jitsi.impl.neomedia.rtcp.termination.strategies.*; -import org.jitsi.impl.neomedia.rtp.translator.*; -import org.jitsi.service.neomedia.*; -import org.jitsi.service.neomedia.rtp.*; -import org.jitsi.util.*; -import org.jitsi.videobridge.*; - -import java.util.*; - -/** - * This class extends the BasicRTCPTerminationStrategy to make it work - * with features found exclusively in the video bridge like, for example, lastN - * and simulcast. - * - * @author George Politis - */ -public class BasicBridgeRTCPTerminationStrategy - extends AbstractBridgeRTCPTerminationStrategy -{ - private static final Logger logger - = Logger.getLogger(BasicBridgeRTCPTerminationStrategy.class); - - /** - * Constructor. - */ - public BasicBridgeRTCPTerminationStrategy() - { - setTransformerChain(new Transformer[]{ - new REMBNotifier(this), - new ReceiverFeedbackFilter(), - new SenderFeedbackExploder(this) - }); - } - - /** - * Sends RRs using data from FMJ and and REMBs using data from our remote - * bitrate estimator. - * - * @return null - */ - public RTCPPacket[] makeReports() - { - RTPTranslator rtpTranslator = this.getRTPTranslator(); - - if (!(rtpTranslator instanceof RTPTranslatorImpl)) - return null; - - Conference conference = this.getConference(); - if (conference == null) - return null; - - long time = System.currentTimeMillis(); - RTPTranslatorImpl rtpTranslatorImpl = (RTPTranslatorImpl) rtpTranslator; - // Use the SSRC of the bridge (that is announced through signaling) so - // that the endpoints won't drop the packet. - int localSSRC = (int) rtpTranslatorImpl.getLocalSSRC(null); - RTCPTransmitter rtcpTransmitter - = this.getRTCPReportBuilder().getRTCPTransmitter(); - - for (Endpoint endpoint : conference.getEndpoints()) - { - for (RtpChannel channel : endpoint.getChannels(MediaType.VIDEO)) - { - // Make the RTCP reports. - RTCPPacket[] packets - = makeReportsForChannel( - (VideoChannel) channel, - time, - localSSRC); - - // Transmit the RTCP reports. - if ((packets != null) && (packets.length != 0)) - { - RTCPCompoundPacket compoundPacket - = new RTCPCompoundPacket(packets); - Payload payload = new RTCPPacketPayload(compoundPacket); - - rtpTranslatorImpl.writeControlPayload( - payload, - channel.getStream()); - - /* - * NOTE(gp, lyubomir): RTCPTransmitter cannot transmit - * specific reports to specific destinations so we've - * implemented the transmission ourselves. We're updating - * the (global) transmission statistics maintained by - * RTCPTranmitter by calling its onRTCPCompoundPacketSent - * method. - */ - rtcpTransmitter.onRTCPCompoundPacketSent(compoundPacket); - } - } - } - - return null; - } - - - /** - * - * @param ssrc - * @return - */ - private RTCPSDES createRTCPSDES(int ssrc) - { - RTCPTransmitter rtcpTransmitter - = this.getRTCPReportBuilder().getRTCPTransmitter(); - - SSRCInfo ssrcInfo = rtcpTransmitter.cache.cache.get(ssrc); - RTCPSDES rtcpSDES = null; - - if (ssrcInfo != null) - { - String cname = ssrcInfo.getCNAME(); - - if (cname != null) - { - rtcpSDES = new RTCPSDES(); - - rtcpSDES.ssrc = ssrc; - rtcpSDES.items - = new RTCPSDESItem[] - { - new RTCPSDESItem(RTCPSDESItem.CNAME, cname) - }; - } - } - return rtcpSDES; - } - - /** - * - * @param videoChannel - * @param time - * @return - */ - private RTCPReportBlock[] createRTCPReportBlocksForChannel( - VideoChannel videoChannel, - long time) - { - RTCPTransmitter rtcpTransmitter - = this.getRTCPReportBuilder().getRTCPTransmitter(); - - int[] ssrcs = videoChannel.getReceiveSSRCs(); - List receiverReports - = new ArrayList(ssrcs.length); - - for (int ssrc : ssrcs) - { - // TODO(gp) we need a mutex here for accessing the RTCP transmitter - // cache. - SSRCInfo info = rtcpTransmitter.cache.cache.get(ssrc); - - if (info != null) - { - RTCPReportBlock receiverReport - = info.makeReceiverReport(time); - - receiverReports.add(receiverReport); - } - else - { - // Don't send RTCP feedback information for this sub-stream. - // TODO(gp) Any endpoints receiving this stream must switch to - // a lower quality stream. - if (logger.isInfoEnabled()) - { - logger.info( - "FMJ has no information for SSRC " - + (ssrc & 0xffffffffl) + " (" + ssrc + ")"); - } - } - } - - return - receiverReports.toArray( - new RTCPReportBlock[receiverReports.size()]); - } - - /** - * - * @param videoChannel - * @param localSSRC - * @return - */ - private RTCPREMBPacket createRTCPREMBPacketForChannel( - VideoChannel videoChannel, - int localSSRC) - { - if (videoChannel == null) - throw new IllegalArgumentException("videoChannel"); - - // Destination - RemoteBitrateEstimator remoteBitrateEstimator - = ((VideoMediaStream) videoChannel.getStream()) - .getRemoteBitrateEstimator(); - Collection ssrcs = remoteBitrateEstimator.getSsrcs(); - - // TODO(gp) intersect with SSRCs from signaled simulcast layers - // NOTE(gp) The Google Congestion Control algorithm (sender side) - // doesn't seem to care about the SSRCs in the dest field. - long[] dest = new long[ssrcs.size()]; - int i = 0; - - for (Integer ssrc : ssrcs) - dest[i++] = ssrc & 0xFFFFFFFFL; - - // Exp & mantissa - long bitrate = remoteBitrateEstimator.getLatestEstimate(); - - if (bitrate == -1) - return null; - - if (logger.isDebugEnabled()) - logger.debug("Estimated bitrate: " + bitrate); - - // Create and return the packet. - return - new RTCPREMBPacket( - localSSRC & 0xFFFFFFFFL, - /* mediaSSRC */ 0L, - bitrate, - dest); - } - - private RTCPPacket[] makeReportsForChannel( - VideoChannel videoChannel, - long time, - int localSSRC) - { - List packets = new ArrayList(3); - - // RTCP RR - RTCPReportBlock[] receiverReports - = createRTCPReportBlocksForChannel(videoChannel, time); - RTCPPacket rr = new RTCPRRPacket(localSSRC, receiverReports); - - packets.add(rr); - - // RTCP REMB - RTCPREMBPacket remb - = createRTCPREMBPacketForChannel(videoChannel, localSSRC); - - if (remb != null) - { - packets.add(remb); - if (logger.isDebugEnabled()) - logger.debug(remb); - } - - // RTCP SDES - List sdesChunks - = new ArrayList(1 + receiverReports.length); - RTCPSDES sdesChunk = createRTCPSDES(localSSRC); - - if (sdesChunk != null) - sdesChunks.add(sdesChunk); - - long[] dest = new long[receiverReports.length]; - - for (int i = 0; i < dest.length; i++) - dest[i] = receiverReports[i].getSSRC(); - - for (long ssrc : dest) - { - sdesChunk = createRTCPSDES((int) ssrc); - if (sdesChunk != null) - sdesChunks.add(sdesChunk); - } - - // TODO(gp) why does this happen : sdesChunks.size() == 0 - if (sdesChunks.size() != 0) - { - RTCPSDESPacket sdes - = new RTCPSDESPacket( - sdesChunks.toArray(new RTCPSDES[sdesChunks.size()])); - - packets.add(sdes); - } - - return packets.toArray(new RTCPPacket[packets.size()]); - } -} diff --git a/src/main/java/org/jitsi/videobridge/rtcp/FallbackingRTCPTerminationStrategy.java b/src/main/java/org/jitsi/videobridge/rtcp/FallbackingRTCPTerminationStrategy.java new file mode 100644 index 0000000000..968f7c2fdf --- /dev/null +++ b/src/main/java/org/jitsi/videobridge/rtcp/FallbackingRTCPTerminationStrategy.java @@ -0,0 +1,242 @@ +/* + * 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 org.jitsi.videobridge.rtcp; + +import org.jitsi.impl.neomedia.*; +import org.jitsi.impl.neomedia.rtcp.termination.strategies.*; +import org.jitsi.impl.neomedia.transform.*; +import org.jitsi.service.configuration.*; +import org.jitsi.service.neomedia.*; +import org.jitsi.util.*; +import org.jitsi.videobridge.*; + +/** + * This RTCP termination strategy delegates to an "active" RTCP + * termination strategy. The "active" RTCP termination strategy is determined + * by the number of Endpoints in a Conference. The default + * fallback RTCP termination strategy is the + * SilentBridgeRTCPTerminationStrategy. The default main RTCP + * termination strategy is the BasicRTCPTerminationStrategy. + * + * @author George Politis + */ +public class FallbackingRTCPTerminationStrategy + extends VideoChannelRTCPTerminationStrategy +{ + /** + * The Logger used by the + * FallbackingRTCPTerminationStrategy class and its instances to + * print debug information. + */ + private static final Logger logger + = Logger.getLogger(FallbackingRTCPTerminationStrategy.class); + + /** + * The name of the property which specifies the FQN name of the RTCP + * strategy to use when there are less than 3 participants. + */ + public static final String FALLBACK_STRATEGY_PNAME + = FallbackingRTCPTerminationStrategy.class.getName() + + ".fallbackStrategy"; + + /** + * The name of the property which specifies the FQN name of the RTCP + * strategy to use when there are less than 3 participants. + */ + public static final String MAIN_STRATEGY_PNAME + = FallbackingRTCPTerminationStrategy.class.getName() + ".mainStrategy"; + + /** + * Determines the number of participants needed for the RTCP termination + * strategy to switch to the main one. + */ + private static final int PARTICIPANTS_THRESHOLD = 3; + + /** + * The fallback RTCPTerminationStrategy that is to be used when < 3 + * participants are in the conference. + */ + private RTCPTerminationStrategy fallbackRTCPTerminationStrategy; + + /** + * The main RTCPTerminationStrategy that is to be used when >= 3 + * participants are in the conference. + */ + private RTCPTerminationStrategy mainRTCPTerminationStrategy; + + //#region Private methods + + /** + * Sets the {@link this.mainRTCPTerminationStrategy} and + * {@link this.fallbackRTCPTerminationStrategy} from the configuration. + */ + public void initialize(VideoChannel vc) + { + super.initialize(vc); + + ConfigurationService configurationService + = vc.getContent().getConference().getVideobridge() + .getConfigurationService(); + + if (configurationService == null) + { + return; + } + + // Initialize {@link this.fallbackRTCPTerminationStrategy} from the + // configuration. + String fallbackFQN = configurationService.getString( + FALLBACK_STRATEGY_PNAME, ""); + + if (!StringUtils.isNullOrEmpty(fallbackFQN)) + { + try + { + Class clazz = Class.forName(fallbackFQN); + RTCPTerminationStrategy fallbackRTCPTerminationSrategy + = (RTCPTerminationStrategy) clazz.newInstance(); + + this.fallbackRTCPTerminationStrategy + = fallbackRTCPTerminationSrategy; + } + catch (Exception e) + { + logger.error( + "Failed to configure the fallback RTCP termination " + + "strategy", e); + } + } + else + { + this.fallbackRTCPTerminationStrategy + = new SilentBridgeRTCPTerminationStrategy(); + } + + // Initialize the RTCP termination strategy. + if (fallbackRTCPTerminationStrategy != null) + { + if (fallbackRTCPTerminationStrategy + instanceof MediaStreamRTCPTerminationStrategy) + { + ((MediaStreamRTCPTerminationStrategy) + fallbackRTCPTerminationStrategy) + .initialize(vc.getStream()); + } + else if (fallbackRTCPTerminationStrategy + instanceof VideoChannelRTCPTerminationStrategy) + { + ((VideoChannelRTCPTerminationStrategy) + fallbackRTCPTerminationStrategy) + .initialize(vc); + } + + } + + // Initialize {@link this.mainRTCPTerminationStrategy} from the + // configuration. + String mainRTCPTerminationStrategyFQN = configurationService.getString( + MAIN_STRATEGY_PNAME, ""); + + if (!StringUtils.isNullOrEmpty(mainRTCPTerminationStrategyFQN)) + { + try + { + Class clazz = Class.forName(mainRTCPTerminationStrategyFQN); + RTCPTerminationStrategy mainRTCPTerminationStrategy + = (RTCPTerminationStrategy) clazz.newInstance(); + + this.mainRTCPTerminationStrategy = mainRTCPTerminationStrategy; + } + catch (Exception e) + { + logger.error( + "Failed to configure the main RTCP termination strategy", + e); + } + } + else + { + this.mainRTCPTerminationStrategy + = new BasicRTCPTerminationStrategy(); + } + + // Initialize the RTCP termination strategy. + if (this.mainRTCPTerminationStrategy != null) + { + if (mainRTCPTerminationStrategy + instanceof MediaStreamRTCPTerminationStrategy) + { + ((MediaStreamRTCPTerminationStrategy) + mainRTCPTerminationStrategy) + .initialize(vc.getStream()); + } + else if (mainRTCPTerminationStrategy + instanceof VideoChannelRTCPTerminationStrategy) + { + ((VideoChannelRTCPTerminationStrategy) + mainRTCPTerminationStrategy) + .initialize(vc); + } + + } + } + + /** + * Returns a boolean indicating whether to use the fallback or the + * main RTCPTerminationStrategy. + * + * @return + */ + private boolean fallback() + { + VideoChannel vc = getVideoChannel(); + + return (vc != null && fallbackRTCPTerminationStrategy != null + && vc.getContent() + .getConference().getEndpointCount() < PARTICIPANTS_THRESHOLD); + } + + //#endregion + + //#region TransformEngineWrapper implementation + + public RTCPTerminationStrategy getActiveRTCPTerminationStrategy() + { + return fallback() ? fallbackRTCPTerminationStrategy + : mainRTCPTerminationStrategy; + } + + //#endregion + + //#region Settable implementation + + public PacketTransformer getRTPTransformer() + { + return getActiveRTCPTerminationStrategy().getRTPTransformer(); + } + + public PacketTransformer getRTCPTransformer() + { + return getActiveRTCPTerminationStrategy().getRTCPTransformer(); + } + + public RawPacket report() + { + return getActiveRTCPTerminationStrategy().report(); + } + + //#endregion +} diff --git a/src/main/java/org/jitsi/videobridge/rtcp/HighestQualityBridgeRTCPTerminationStrategy.java b/src/main/java/org/jitsi/videobridge/rtcp/HighestQualityBridgeRTCPTerminationStrategy.java deleted file mode 100644 index e3d99313f1..0000000000 --- a/src/main/java/org/jitsi/videobridge/rtcp/HighestQualityBridgeRTCPTerminationStrategy.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 org.jitsi.videobridge.rtcp; - -import net.sf.fmj.media.rtp.*; -import org.jitsi.impl.neomedia.rtcp.termination.strategies.*; -import org.jitsi.impl.neomedia.rtp.translator.*; -import org.jitsi.service.neomedia.*; -import org.jitsi.util.*; - -/** - * @author George Politis - */ -@Deprecated -public class HighestQualityBridgeRTCPTerminationStrategy - extends AbstractBridgeRTCPTerminationStrategy -{ - private static final Logger logger = - Logger.getLogger(HighestQualityBridgeRTCPTerminationStrategy.class); - - /** - * The cache processor that will be making the RTCP reports coming from - * the bridge. - */ - private final FeedbackCacheProcessor feedbackCacheProcessor; - - /** - * A cache of media receiver feedback. It contains both receiver report - * blocks and REMB packets. - */ - private final FeedbackCache feedbackCache; - - public HighestQualityBridgeRTCPTerminationStrategy() - { - logger.warn("This RTCP termination strategy is deprecated and should" + - "not be used!"); - - this.feedbackCache = new FeedbackCache(); - this.feedbackCacheProcessor - = new FeedbackCacheProcessor(feedbackCache); - - // TODO(gp) make percentile configurable. - this.feedbackCacheProcessor.setPercentile(70); - - setTransformerChain(new Transformer[]{ - new REMBNotifier(this), - new FeedbackCacheUpdater(feedbackCache), - new ReceiverFeedbackFilter(), - new SenderFeedbackExploder(this) - }); - } - - @Override - public RTCPPacket[] makeReports() - { - // Uses the cache processor to make the RTCP reports. - - RTPTranslator t = this.getRTPTranslator(); - if (t == null || !(t instanceof RTPTranslatorImpl)) - return new RTCPPacket[0]; - - long localSSRC = ((RTPTranslatorImpl)t).getLocalSSRC(null); - - RTCPPacket[] packets = feedbackCacheProcessor.makeReports( - (int) localSSRC); - - return packets; - } -} diff --git a/src/main/java/org/jitsi/videobridge/rtcp/MaxThroughputBridgeRTCPTerminationStrategy.java b/src/main/java/org/jitsi/videobridge/rtcp/MaxThroughputBridgeRTCPTerminationStrategy.java deleted file mode 100644 index 4f28c90477..0000000000 --- a/src/main/java/org/jitsi/videobridge/rtcp/MaxThroughputBridgeRTCPTerminationStrategy.java +++ /dev/null @@ -1,349 +0,0 @@ -/* - * 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 org.jitsi.videobridge.rtcp; - -import java.util.*; - -import net.sf.fmj.media.rtp.*; - -import org.jitsi.impl.neomedia.rtcp.*; -import org.jitsi.impl.neomedia.rtp.translator.*; -import org.jitsi.service.neomedia.*; -import org.jitsi.service.neomedia.rtp.*; -import org.jitsi.util.*; -import org.jitsi.videobridge.*; -import org.jitsi.videobridge.simulcast.*; - -/** - * - * @author George Politis - * @author Lyubomir Marinov - */ -@Deprecated -public class MaxThroughputBridgeRTCPTerminationStrategy - extends AbstractBridgeRTCPTerminationStrategy -{ - private static final Logger logger - = Logger.getLogger(MaxThroughputBridgeRTCPTerminationStrategy.class); - - private RTCPSDES createRTCPSDES(RTCPTransmitter rtcpTransmitter, int ssrc) - { - SSRCInfo ssrcInfo = rtcpTransmitter.cache.cache.get(ssrc); - RTCPSDES rtcpSDES = null; - - if (ssrcInfo != null) - { - String cname = ssrcInfo.getCNAME(); - - if (cname != null) - { - rtcpSDES = new RTCPSDES(); - - rtcpSDES.ssrc = ssrc; - rtcpSDES.items - = new RTCPSDESItem[] - { - new RTCPSDESItem(RTCPSDESItem.CNAME, cname) - }; - } - } - return rtcpSDES; - } - - private RTCPReportBlock[] makeReceiverReports( - VideoChannel videoChannel, - RTCPTransmitter rtcpTransmitter, - long time) - { - SortedSet layers - = videoChannel.getSimulcastManager().getSimulcastLayers(); - List receiverReports - = new ArrayList(layers.size()); - - for (SimulcastLayer layer : layers) - { - int ssrc = (int) layer.getPrimarySSRC(); - SSRCInfo info = rtcpTransmitter.cache.cache.get(ssrc); - - if (info != null) - { - RTCPReportBlock receiverReport - = info.makeReceiverReport(time); - - receiverReports.add(receiverReport); - } - else - { - // Don't send RTCP feedback information for this sub-stream. - // TODO(gp) Any endpoints receiving this stream must switch to - // a lower quality stream. - if (logger.isInfoEnabled()) - { - logger.info( - "FMJ has no information for SSRC " - + (ssrc & 0xffffffffl) + " (" + ssrc + ")"); - } - } - } - - return - receiverReports.toArray( - new RTCPReportBlock[receiverReports.size()]); - } - - private RTCPREMBPacket makeREMBPacket( - VideoChannel videoChannel, - int localSSRC) - { - if (videoChannel == null) - throw new IllegalArgumentException("videoChannel"); - - // Media SSRC (always 0) - final long mediaSSRC = 0l; - - // Destination - RemoteBitrateEstimator remoteBitrateEstimator - = ((VideoMediaStream) videoChannel.getStream()) - .getRemoteBitrateEstimator(); - Collection ssrcs = remoteBitrateEstimator.getSsrcs(); - - // TODO(gp) intersect with SSRCs from signaled simulcast layers - long[] dest = new long[ssrcs.size()]; - int i = 0; - - for (Integer ssrc : ssrcs) - dest[i++] = ssrc & 0xffffffffl; - - // Exp & mantissa - long bitrate = remoteBitrateEstimator.getLatestEstimate(); - if (bitrate == -1) - { - return null; - } - - if (logger.isDebugEnabled()) - logger.debug("Estimated bitrate: " + bitrate); - - // Create and return the packet. - return - new RTCPREMBPacket( - localSSRC & 0xFFFFFFFFL, - mediaSSRC, - bitrate, - dest); - } - - @Override - public RTCPPacket[] makeReports() - { - RTPTranslator rtpTranslator = this.getRTPTranslator(); - - if (!(rtpTranslator instanceof RTPTranslatorImpl)) - return null; - - long time = System.currentTimeMillis(); - - RTPTranslatorImpl rtpTranslatorImpl = (RTPTranslatorImpl) rtpTranslator; - - // Use the SSRC of the bridge (that is announced through signaling) so - // that the endpoints won't drop the packet. - int localSSRC = (int) rtpTranslatorImpl.getLocalSSRC(null); - - for (Endpoint endpoint : getConference().getEndpoints()) - { - for (RtpChannel channel : endpoint.getChannels(MediaType.VIDEO)) - { - // Make the RTCP reports. - RTCPPacket[] packets - = makeReports( - (VideoChannel) channel, - getRTCPTransmitter(), - time, - localSSRC); - - // Transmit the RTCP reports. - if ((packets != null) && (packets.length != 0)) - { - RTCPCompoundPacket compoundPacket - = new RTCPCompoundPacket(packets); - Payload payload = new RTCPPacketPayload(compoundPacket); - - rtpTranslatorImpl.writeControlPayload( - payload, - channel.getStream()); - - /* - * NOTE(gp, lyubomir): RTCPTransmitter cannot transmit - * specific reports to specific destinations so we've - * implemented the transmission ourselves. We're updating - * the (global) transmission statistics maintained by - * RTCPTranmitter by calling its onRTCPCompoundPacketSent - * method. - */ - getRTCPTransmitter().onRTCPCompoundPacketSent(compoundPacket); - } - } - } - - return null; - } - - private RTCPPacket[] makeReports( - VideoChannel videoChannel, - RTCPTransmitter rtcpTransmitter, - long time, - int localSSRC) - { - // RTCP RR - RTCPReportBlock[] receiverReports - = makeReceiverReports(videoChannel, rtcpTransmitter, time); - RTCPPacket rr = new RTCPRRPacket(localSSRC, receiverReports); - - // RTCP REMB - RTCPREMBPacket remb = makeREMBPacket(videoChannel, localSSRC); - - if (remb != null) - { - if (logger.isDebugEnabled()) - logger.debug(remb); - } - - // RTCP SDES - List sdesChunks - = new ArrayList(1 + receiverReports.length); - RTCPSDES sdesChunk = createRTCPSDES(rtcpTransmitter, localSSRC); - - if (sdesChunk != null) - sdesChunks.add(sdesChunk); - - long[] dest = new long[receiverReports.length]; - - for (int i = 0; i < dest.length; i++) - dest[i] = receiverReports[i].getSSRC(); - - for (long ssrc : dest) - { - sdesChunk = createRTCPSDES(rtcpTransmitter, (int) ssrc); - if (sdesChunk != null) - sdesChunks.add(sdesChunk); - } - - RTCPSDESPacket sdes - = new RTCPSDESPacket( - sdesChunks.toArray(new RTCPSDES[sdesChunks.size()])); - - return (remb != null) - ? new RTCPPacket[] { rr, remb, sdes } - : new RTCPPacket[] { rr, sdes }; - } - - public MaxThroughputBridgeRTCPTerminationStrategy() - { - logger.warn("This RTCP termination strategy is deprecated and should" + - "not be used!"); - setTransformerChain(new Transformer[]{ - transformer - }); - } - - Transformer transformer = new Transformer() - { - @Override - public RTCPCompoundPacket reverseTransform(RTCPCompoundPacket inPacket) - { - if (inPacket == null) - return inPacket; - - RTCPPacket[] inPackets = inPacket.packets; - - if ((inPackets == null) || (inPackets.length == 0)) - return inPacket; - - List outPackets - = new ArrayList(inPackets.length); - - for (RTCPPacket p : inPackets) - { - switch (p.type) - { - case RTCPPacket.RR: - // Mute RRs from the peers. We send our own. - break; - - case RTCPPacket.SR: - // Remove feedback information from the SR and forward. - RTCPSRPacket sr = (RTCPSRPacket) p; - - sr.reports = new RTCPReportBlock[0]; - outPackets.add(sr); - break; - - case RTCPFBPacket.PSFB: - RTCPFBPacket psfb = (RTCPFBPacket) p; - - switch (psfb.fmt) - { - case RTCPREMBPacket.FMT: - // Mute REMBs. - break; - default: - // Pass through everything else, like PLIs and NACKs - outPackets.add(psfb); - break; - } - break; - - default: - // Pass through everything else, like PLIs and NACKs - outPackets.add(p); - break; - } - } - - RTCPCompoundPacket outPacket; - - if (outPackets.isEmpty()) - { - outPacket = null; - } - else - { - outPacket - = new RTCPCompoundPacket( - outPackets.toArray(new RTCPPacket[outPackets.size()])); - } - return outPacket; - } - - /** - * {@inheritDoc} - */ - @Override - public void close() - { - // nothing to be done here - } - - /** - * {@inheritDoc} - */ - @Override - public RTCPCompoundPacket transform(RTCPCompoundPacket inPacket) - { - return inPacket; - } - }; -} diff --git a/src/main/java/org/jitsi/videobridge/rtcp/NACKNotifier.java b/src/main/java/org/jitsi/videobridge/rtcp/NACKNotifier.java index 0d1c1fd7b6..25c528f9d9 100644 --- a/src/main/java/org/jitsi/videobridge/rtcp/NACKNotifier.java +++ b/src/main/java/org/jitsi/videobridge/rtcp/NACKNotifier.java @@ -16,8 +16,11 @@ package org.jitsi.videobridge.rtcp; import net.sf.fmj.media.rtp.*; +import net.sf.fmj.media.rtp.util.*; +import org.jitsi.impl.neomedia.*; import org.jitsi.impl.neomedia.rtcp.*; -import org.jitsi.service.neomedia.*; +import org.jitsi.impl.neomedia.transform.*; +import org.jitsi.util.function.*; import java.util.*; @@ -28,7 +31,8 @@ * @author Boris Grozev */ public class NACKNotifier - implements Transformer + extends SinglePacketTransformer + implements TransformEngine { /** * The handler to use for intercepted NACK packets. @@ -40,6 +44,18 @@ public class NACKNotifier */ private boolean enabled = true; + /** + * The Function that generates RTCPCompoundPackets from + * RawPackets. + */ + private RTCPPacketParserEx parser = new RTCPPacketParserEx(); + + /** + * The Function that generates RawPackets from + * RTCPCompoundPackets. + */ + private RTCPGenerator generator = new RTCPGenerator(); + /** * Initializes a new NACKNotifier which is to use a specific * NACKHandler instalce. @@ -50,21 +66,45 @@ public NACKNotifier(NACKHandler handler) this.handler = handler; } + @Override + public RawPacket transform(RawPacket pkt) + { + return pkt; + } + /** * Looks for RTCP NACK packets contained in the compound packet * inPacket, passes them on the the handler and removes them from * the resulting compount packet. - * @param inPacket the input RTCP compound packet. + * @param pkt the input RTCP compound packet. * @return a packet which consists of the packets from inPacket, * with NACK packets removed. */ @Override - public RTCPCompoundPacket reverseTransform(RTCPCompoundPacket inPacket) + public RawPacket reverseTransform(RawPacket pkt) { - if (!enabled || inPacket == null || inPacket.packets == null - || inPacket.packets.length == 0) + if (!enabled) { - return inPacket; + return pkt; + } + + RTCPCompoundPacket inPacket; + try + { + inPacket = (RTCPCompoundPacket) parser.parse( + pkt.getBuffer(), + pkt.getOffset(), + pkt.getLength()); + } + catch (BadFormatException e) + { + return null; + } + + if (inPacket == null || inPacket.packets == null + || inPacket.packets.length == 0) + { + return pkt; } List outPackets = new LinkedList(); @@ -91,33 +131,30 @@ public RTCPCompoundPacket reverseTransform(RTCPCompoundPacket inPacket) if (removed) { if (outPackets.size() > 0) - return new RTCPCompoundPacket(outPackets.toArray( + { + RTCPCompoundPacket outPacket + = new RTCPCompoundPacket(outPackets.toArray( new RTCPPacket[outPackets.size()])); + + return generator.apply(outPacket); + } else return null; } else { - return inPacket; + return pkt; } } - /** - * {@inheritDoc} - */ - @Override - public void close() + public PacketTransformer getRTPTransformer() { - // nothing to be done here + return null; } - /** - * {@inheritDoc} - */ - @Override - public RTCPCompoundPacket transform(RTCPCompoundPacket inPacket) + public PacketTransformer getRTCPTransformer() { - return inPacket; + return this; } } diff --git a/src/main/java/org/jitsi/videobridge/rtcp/REMBNotifier.java b/src/main/java/org/jitsi/videobridge/rtcp/REMBNotifier.java index 62d7f0315e..10e1394804 100644 --- a/src/main/java/org/jitsi/videobridge/rtcp/REMBNotifier.java +++ b/src/main/java/org/jitsi/videobridge/rtcp/REMBNotifier.java @@ -16,67 +16,106 @@ package org.jitsi.videobridge.rtcp; import net.sf.fmj.media.rtp.*; +import net.sf.fmj.media.rtp.util.*; +import org.jitsi.impl.neomedia.*; import org.jitsi.impl.neomedia.rtcp.*; -import org.jitsi.service.neomedia.*; +import org.jitsi.impl.neomedia.transform.*; import org.jitsi.videobridge.*; +import java.lang.ref.*; + /** + * Intercepts REMBs and passes them down to the VideoChannel logic. + * * @author George Politis */ -class REMBNotifier implements Transformer +public class REMBNotifier + implements TransformEngine { - private AbstractBridgeRTCPTerminationStrategy strategy; + /** + * + */ + private final WeakReference weakVideoChannel; + + /** + * + */ + private final RTCPPacketParserEx parserEx = new RTCPPacketParserEx(); + + /** + * Ctor. + * + * @param videoChannel + */ + public REMBNotifier(VideoChannel videoChannel) + { + this.weakVideoChannel = new WeakReference(videoChannel); + } - public REMBNotifier(AbstractBridgeRTCPTerminationStrategy strategy) + public PacketTransformer getRTPTransformer() { - this.strategy = strategy; + return null; } - @Override - public RTCPCompoundPacket reverseTransform(RTCPCompoundPacket inPacket) + public PacketTransformer getRTCPTransformer() { - // Intercept REMBs and forward them to the VideoChannel logic - for (RTCPPacket p : inPacket.packets) + return new SinglePacketTransformer() { - if (p != null && p.type == RTCPFBPacket.PSFB) + @Override + public RawPacket transform(RawPacket pkt) + { + return pkt; + } + + @Override + public RawPacket reverseTransform(RawPacket pkt) { - RTCPFBPacket psfb = (RTCPFBPacket) p; - if (psfb.fmt == RTCPREMBPacket.FMT) + if (pkt == null) + { + return pkt; + } + + RTCPCompoundPacket inPacket; + try + { + inPacket = (RTCPCompoundPacket) parserEx.parse( + pkt.getBuffer(), pkt.getOffset(), pkt.getLength()); + } + catch (BadFormatException ex) { - RTCPREMBPacket remb = (RTCPREMBPacket) psfb; - Conference conference = strategy.getConference(); - if (conference != null) + return pkt; + } + + if (inPacket == null) + { + return pkt; + } + + // Intercept REMBs and forward them to the VideoChannel logic + for (RTCPPacket p : inPacket.packets) + { + if (p != null && p.type == RTCPFBPacket.PSFB) { - Channel channel - = conference.findChannelByReceiveSSRC(remb.senderSSRC, - MediaType.VIDEO); - if (channel != null && channel instanceof VideoChannel) + RTCPFBPacket psfb = (RTCPFBPacket) p; + if (psfb.fmt == RTCPREMBPacket.FMT) { - ((VideoChannel) channel).receivedREMB(remb.getBitrate()); + RTCPREMBPacket remb = (RTCPREMBPacket) psfb; + + WeakReference wc = weakVideoChannel; + VideoChannel videoChannel = wc == null + ? null + : wc.get(); + + if (videoChannel != null) + { + videoChannel.receivedREMB(remb.getBitrate()); + } } } } - } - } - - return inPacket; - } - - /** - * {@inheritDoc} - */ - @Override - public void close() - { - // nothing to be done here - } - /** - * {@inheritDoc} - */ - @Override - public RTCPCompoundPacket transform(RTCPCompoundPacket inPacket) - { - return inPacket; + return pkt; + } + }; } } diff --git a/src/main/java/org/jitsi/videobridge/rtcp/RTCPPacketPayload.java b/src/main/java/org/jitsi/videobridge/rtcp/RTCPPacketPayload.java deleted file mode 100644 index b83266a9af..0000000000 --- a/src/main/java/org/jitsi/videobridge/rtcp/RTCPPacketPayload.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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 org.jitsi.videobridge.rtcp; - -import net.sf.fmj.media.rtp.*; -import org.jitsi.impl.neomedia.rtp.translator.*; - -import javax.media.rtp.*; - -/** -* @author George Politis -*/ -class RTCPPacketPayload - implements Payload -{ - private final RTCPCompoundPacket packet; - - public RTCPPacketPayload(RTCPCompoundPacket p) - { - this.packet = p; - } - - @Override - public void writeTo(OutputDataStream stream) - { - if (packet != null) - { - int len = packet.calcLength(); - packet.assemble(len, false); - byte[] buf = packet.data; - - stream.write(buf, 0, len); - } - } -} diff --git a/src/main/java/org/jitsi/videobridge/rtcp/SenderFeedbackExploder.java b/src/main/java/org/jitsi/videobridge/rtcp/SenderFeedbackExploder.java deleted file mode 100644 index 1be0cd5bdd..0000000000 --- a/src/main/java/org/jitsi/videobridge/rtcp/SenderFeedbackExploder.java +++ /dev/null @@ -1,319 +0,0 @@ -/* - * 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 org.jitsi.videobridge.rtcp; - -import net.sf.fmj.media.rtp.*; -import org.jitsi.impl.neomedia.rtp.translator.*; -import org.jitsi.service.neomedia.*; -import org.jitsi.service.neomedia.recording.*; -import org.jitsi.videobridge.*; - -import java.util.*; - -/** - * @author George Politis - */ -class SenderFeedbackExploder implements Transformer -{ - private final AbstractBridgeRTCPTerminationStrategy strategy; - - /** - * Ctor. - * - * @param strategy - */ - public SenderFeedbackExploder(AbstractBridgeRTCPTerminationStrategy strategy) - { - this.strategy = strategy; - } - - @Override - public RTCPCompoundPacket reverseTransform(RTCPCompoundPacket inPacket) - { - // Call the super method that: - // - // 1. Removes receiver report blocks from RRs and SRs and kills REMBs. - // 2. Updates the receiver feedback cache. - - // RTCPCompoundPacket outPacket = super.reverseTransform(inPacket); - RTCPCompoundPacket outPacket = inPacket; - - if (outPacket.packets != null - && outPacket.packets.length != 0 - && outPacket.packets[0].type == RTCPPacket.SR) - { - // 3. This is a sender report, pass it on to the bridge sender - // reporting for "explosion". - if (explodeSenderReport(outPacket)) - { - return null; - } else - { - // "Explosion" failed, send as is. - return outPacket; - } - } else - { - // Not an SR, don't touch. - return outPacket; - } - } - - /** - * {@inheritDoc} - */ - @Override - public void close() - { - // nothing to be done here - } - - /** - * {@inheritDoc} - */ - @Override - public RTCPCompoundPacket transform(RTCPCompoundPacket inPacket) - { - return inPacket; - } - - private final Map> - lastSenderInformationMap - = new HashMap>(); - - private void explodeSenderReport(boolean destIsReceiving, - RTCPCompoundPacket outPacket, - RTCPSRPacket senderReport, - RTPTranslatorImpl rtpTranslatorImpl, - Integer senderSSRC, - Map receiverSenderInformationMap, - MediaStream stream) - { - // "Clone" the SR. - RTCPSRPacket sr = new RTCPSRPacket( - senderSSRC, new RTCPReportBlock[0]); - sr.ntptimestampmsw = senderReport.ntptimestampmsw; - sr.ntptimestamplsw = senderReport.ntptimestamplsw; - sr.rtptimestamp = senderReport.rtptimestamp; - sr.octetcount = senderReport.octetcount; - sr.packetcount = senderReport.packetcount; - - Integer receiverSSRC = (int) stream.getLocalSourceID(); - - if (destIsReceiving) - { - // The sender is being received by this receiver: - // Cache the sender information. - SenderInformation si = new SenderInformation(); - si.octetCount = senderReport.octetcount; - si.packetCount = senderReport.packetcount; - - synchronized (receiverSenderInformationMap) - { - receiverSenderInformationMap.put(receiverSSRC, si); - } - } - else - { - // The sender is NOT being received by this receiver: - // We keep the packet count/octet count stable. - SenderInformation si; - synchronized (receiverSenderInformationMap) - { - if (receiverSenderInformationMap - .containsKey(receiverSSRC)) - { - si = receiverSenderInformationMap - .get(receiverSSRC); - } - else - { - si = null; - } - } - - if (si != null) - { - sr.packetcount = si.packetCount; - sr.octetcount = si.octetCount; - } - else - { - sr.packetcount = 0L; - sr.octetcount = 0L; - } - } - - // Send the SR to the receiver. - RTCPPacket[] packets - = new RTCPPacket[outPacket.packets.length]; - - packets[0] = sr; - - System.arraycopy( - outPacket.packets, 1, - packets, 1, outPacket.packets.length - 1); - - RTCPCompoundPacket compoundPacket - = new RTCPCompoundPacket(packets); - - Payload payload = new RTCPPacketPayload(compoundPacket); - rtpTranslatorImpl.writeControlPayload(payload, stream); - } - - /** - * Explode the SRs to make them compliant with features from the translator. - * - * @param outPacket - * @return - */ - public boolean explodeSenderReport(RTCPCompoundPacket outPacket) - { - if (outPacket.packets == null - || outPacket.packets.length == 0 - || outPacket.packets[0].type != RTCPPacket.SR) - { - return false; - } - - RTCPSRPacket senderReport = (RTCPSRPacket) outPacket.packets[0]; - - Conference conf = strategy.getConference(); - if (senderReport == null || conf == null) - return false; - - RTPTranslator rtpTranslator = strategy.getRTPTranslator(); - if (rtpTranslator == null - || !(rtpTranslator instanceof RTPTranslatorImpl)) - return false; - - RTPTranslatorImpl rtpTranslatorImpl = (RTPTranslatorImpl)rtpTranslator; - - long ssrc = senderReport.ssrc & 0xFFFFFFFFL; - if (ssrc < 1) - return false; - - Integer senderSSRC = senderReport.ssrc; - Map receiverSenderInformationMap - = getReceiverSenderInformationMap(senderSSRC); - - Channel srcChannel = conf - .findChannelByReceiveSSRC(ssrc, MediaType.VIDEO); - - if (srcChannel == null || !(srcChannel instanceof RtpChannel)) - return false; - - RtpChannel srcRtpChannel = (RtpChannel)srcChannel; - - // Send to every channel that receives this sender an SR. - for (Content content : conf.getContents()) - { - if (MediaType.VIDEO.equals(content.getMediaType())) - { - for (Channel destChannel : content.getChannels()) - { - if (!(destChannel instanceof RtpChannel) - || srcRtpChannel == destChannel) - continue; - - RtpChannel destRtpChannel = (RtpChannel) destChannel; - MediaStream stream = destRtpChannel.getStream(); - if (stream == null) - continue; - - boolean destIsReceiving - = srcRtpChannel.isInLastN(destChannel); - - if (destIsReceiving && srcRtpChannel instanceof VideoChannel) - { - VideoChannel srcVideoChannel - = (VideoChannel) srcRtpChannel; - - if (!(destChannel instanceof VideoChannel)) - { - destIsReceiving = false; - } - else - { - VideoChannel destVideoChannel - = (VideoChannel) destChannel; - - destIsReceiving - = destVideoChannel.getSimulcastManager().accept( - ssrc, - srcVideoChannel); - } - } - - explodeSenderReport(destIsReceiving, outPacket, - senderReport, - rtpTranslatorImpl, - senderSSRC, - receiverSenderInformationMap, - stream); - } - - if (content.isRecording()) - { - Recorder recorder = content.getRecorder(); - MediaStream s; - - if (recorder != null && (s = recorder.getMediaStream()) != null) - { - explodeSenderReport(true, outPacket, - senderReport, - rtpTranslatorImpl, - senderSSRC, - receiverSenderInformationMap, - s); - } - } - } - } - - return true; - } - - private Map getReceiverSenderInformationMap( - Integer senderSSRC) - { - Map receiverSenderInformationMap; - synchronized (lastSenderInformationMap) - { - if (lastSenderInformationMap.containsKey(senderSSRC)) - { - receiverSenderInformationMap - = lastSenderInformationMap.get(senderSSRC); - } - else - { - receiverSenderInformationMap - = new HashMap(); - - lastSenderInformationMap.put(senderSSRC, - receiverSenderInformationMap); - } - } - - return receiverSenderInformationMap; - } - - private static class SenderInformation - { - long octetCount; - long packetCount; - } -} diff --git a/src/main/java/org/jitsi/videobridge/rtcp/VideoChannelRTCPTerminationStrategy.java b/src/main/java/org/jitsi/videobridge/rtcp/VideoChannelRTCPTerminationStrategy.java new file mode 100644 index 0000000000..96f72bea14 --- /dev/null +++ b/src/main/java/org/jitsi/videobridge/rtcp/VideoChannelRTCPTerminationStrategy.java @@ -0,0 +1,63 @@ +/* + * 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 org.jitsi.videobridge.rtcp; + +import org.jitsi.service.neomedia.*; +import org.jitsi.videobridge.*; + +import java.lang.ref.*; + +/** + * @author George Politis + */ +public abstract class VideoChannelRTCPTerminationStrategy + implements RTCPTerminationStrategy +{ + /** + * A WeakReference to the VideoChannel that owns this + * this VideoChannelRTCPTerminationStrategy. + */ + private WeakReference weakVideoChannel; + + /** + * Gets the VideoChannel that owns this + * VideoChannelRTCPTerminationStrategy. + * + * @return the VideoChannel that owns this + * VideoChannelRTCPTerminationStrategy. + */ + public VideoChannel getVideoChannel() + { + WeakReference wvc = this.weakVideoChannel; + return wvc != null ? wvc.get() : null; + } + + /** + * Initializes this RTCP termination strategy with a VideoChannel. + * + * @param vc the VideoChannel that owns this + * VideoChannelRTCPTerminationStrategy. + */ + public void initialize(VideoChannel vc) + { + if (vc == null) + { + return; + } + + this.weakVideoChannel = new WeakReference(vc); + } +} diff --git a/src/main/java/org/jitsi/videobridge/transform/RTCPTransformEngine.java b/src/main/java/org/jitsi/videobridge/transform/RTCPTransformEngine.java deleted file mode 100644 index 87692051ff..0000000000 --- a/src/main/java/org/jitsi/videobridge/transform/RTCPTransformEngine.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * 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 org.jitsi.videobridge.transform; - -import net.sf.fmj.media.rtp.*; -import net.sf.fmj.media.rtp.util.*; -import org.jitsi.impl.neomedia.*; -import org.jitsi.impl.neomedia.rtcp.*; -import org.jitsi.impl.neomedia.transform.*; -import org.jitsi.service.neomedia.*; -import org.jitsi.util.*; - -/** - * A TransformEngine implementation which parses RTCP packets and - * transforms them using a transformer for RTCPCompoundPackets. - * This is similar to (and based on) libjitsi's - * RTCPTerminationTransformEngine but is not connected with a - * MediaStream. - * @author Boris Grozev - */ -public class RTCPTransformEngine - extends SinglePacketTransformer - implements TransformEngine, Transformer -{ - /** - * The Logger used by the RTCPTransformEngine class - * and its instances to print debug information. - */ - private static final Logger logger - = Logger.getLogger(RTCPTransformEngine.class); - - /** - * The chain of transformers that operate on RTCPCompoundPackets - * (already parsed). - */ - private Transformer[] chain; - - /** - * The instance used to parse the input RawPackets/bytes into - * RTCPCompoundPackets. - */ - private final RTCPPacketParserEx parser = new RTCPPacketParserEx(); - - /** - * Initializes this transformer with the given chain of transformers. - * @param chain - */ - public RTCPTransformEngine(Transformer[] chain) - { - this.chain = chain; - } - - /** - * Implements - * {@link org.jitsi.service.neomedia.Transformer#transform(Object)}. - * - * Does not touch outgoing packets. - */ - @Override - public RTCPCompoundPacket transform(RTCPCompoundPacket rtcpCompoundPacket) - { - // Not implemented. - return rtcpCompoundPacket; - } - - /** - * Implements - * {@link org.jitsi.service.neomedia.Transformer#reverseTransform(Object)}. - * - * Transforms incoming RTCP packets through the configured transformer - * chain. - */ - @Override - public RTCPCompoundPacket reverseTransform( - RTCPCompoundPacket inPacket) - { - if (chain != null) - { - for (Transformer transformer : chain) - { - if (transformer != null) - inPacket = transformer.reverseTransform(inPacket); - } - } - - return inPacket; - } - - /** - * Implements - * {@link org.jitsi.service.neomedia.Transformer#close()}. - */ - @Override - public void close() - { - } - - /** - * Implements - * {@link org.jitsi.impl.neomedia.transform.TransformEngine#getRTPTransformer()}. - */ - @Override - public PacketTransformer getRTPTransformer() - { - return null; - } - - /** - * Implements - * {@link org.jitsi.impl.neomedia.transform.TransformEngine#getRTCPTransformer()}. - */ - @Override - public PacketTransformer getRTCPTransformer() - { - return this; - } - - /** - * Implements - * {@link org.jitsi.impl.neomedia.transform.SinglePacketTransformer#transform(org.jitsi.impl.neomedia.RawPacket)} - * - * Does not touch outgoing packets. - */ - @Override - public RawPacket transform(RawPacket packet) - { - // Not implemented. - return packet; - } - - /** - * Implements - * {@link org.jitsi.impl.neomedia.transform.SinglePacketTransformer#reverseTransform(org.jitsi.impl.neomedia.RawPacket)} - * - * Parses the given packet as an RTCP packet and transforms the result - * using the configured transformer chain. Returns the RawPacket - * constructed from the transformed RTCPCompoundPacket. - */ - @Override - public RawPacket reverseTransform(RawPacket pkt) - { - // Parse the RTCP packet. - RTCPCompoundPacket inRTCPPacket; - try - { - inRTCPPacket = (RTCPCompoundPacket) parser.parse( - pkt.getBuffer(), - pkt.getOffset(), - pkt.getLength()); - } - catch (BadFormatException e) - { - // TODO(gp) decide what to do with malformed packets! - logger.error("Could not parse RTCP packet.", e); - return pkt; - } - - // Transform the RTCP packet with the transform chain. - RTCPCompoundPacket outRTCPPacket = reverseTransform(inRTCPPacket); - - if (outRTCPPacket == null - || outRTCPPacket.packets == null - || outRTCPPacket.packets.length == 0) - return null; - - // Assemble the RTCP packet. - int len = outRTCPPacket.calcLength(); - outRTCPPacket.assemble(len, false); - - pkt.setBuffer(outRTCPPacket.data); - pkt.setLength(outRTCPPacket.data.length); - pkt.setOffset(0); - - return pkt; - } -} diff --git a/src/main/java/org/jitsi/videobridge/transform/RetransmissionRequester.java b/src/main/java/org/jitsi/videobridge/transform/RetransmissionRequester.java index 524f395576..0042a45929 100644 --- a/src/main/java/org/jitsi/videobridge/transform/RetransmissionRequester.java +++ b/src/main/java/org/jitsi/videobridge/transform/RetransmissionRequester.java @@ -230,7 +230,15 @@ private void runInRequesterThread() } if (pkt != null) + try + { mediaStream.injectPacket(pkt, false, true); + } + catch (TransmissionFailedException ex) + { + logger.warn("Failed to inject packet in MediaStream: " + + ex); + } } } diff --git a/src/main/java/org/jitsi/videobridge/transform/RtpChannelTransformEngine.java b/src/main/java/org/jitsi/videobridge/transform/RtpChannelTransformEngine.java index d096aa8087..ccc49ac8c6 100644 --- a/src/main/java/org/jitsi/videobridge/transform/RtpChannelTransformEngine.java +++ b/src/main/java/org/jitsi/videobridge/transform/RtpChannelTransformEngine.java @@ -17,7 +17,6 @@ import org.jitsi.impl.neomedia.transform.*; import org.jitsi.service.configuration.*; -import org.jitsi.service.neomedia.*; import org.jitsi.util.*; import org.jitsi.videobridge.*; import org.jitsi.videobridge.rewriting.*; @@ -77,15 +76,16 @@ public class RtpChannelTransformEngine private CachingTransformer cache; /** - * The transformer which parses incoming RTCP packets. + * The transformer which intercepts NACK packets and passes them on to the + * channel logic. */ - private RTCPTransformEngine rtcpTransformEngine; + private NACKNotifier nackNotifier; /** - * The transformer which intercepts NACK packets and passes them on to the + * The transformer which intercepts REMB packets and passes them on to the * channel logic. */ - private NACKNotifier nackNotifier; + private REMBNotifier rembNotifier; /** * The transformer which replaces the timestamp in an abs-send-time RTP @@ -162,23 +162,15 @@ private TransformEngine[] createChain() cache = new CachingTransformer(); transformerList.add(cache); - // Note: we use a separate RTCPTransformer here, instead of using - // the RTCPTerminationStrategy, because interpreting RTCP NACK - // packets should happen in the context of a specific channel, and - // the RTCPTermination strategy is a single instance for a - // conference. The current intention/idea is to eventually move - // the RTCP parsing code from the RTCPTerminationStrategy here, so - // that we only parse RTCP once, and so that the REMB/RR code - // doesn't have to find the source Channel by SSRC. nackNotifier = new NACKNotifier((NACKHandler) channel); - rtcpTransformEngine - = new RTCPTransformEngine(new Transformer[] {nackNotifier}); - transformerList.add(rtcpTransformEngine); + transformerList.add(nackNotifier); } if (channel instanceof VideoChannel) { VideoChannel videoChannel = (VideoChannel) channel; + rembNotifier = new REMBNotifier(videoChannel); + transformerList.add(rembNotifier); ssrcRewritingEngine = new SsrcRewritingEngine(videoChannel); transformerList.add(ssrcRewritingEngine); }