/* * TeleStax, Open Source Cloud Communications * Copyright 2011-2014, Telestax Inc and individual contributors * by the @authors tag. * * This program is free software: you can redistribute it and/or modify * under the terms of the GNU Affero General Public License as * published by the Free Software Foundation; either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/> * */ package org.restcomm.media.rtp.channels; import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; import org.apache.log4j.Logger; import org.restcomm.media.ice.IceAuthenticatorImpl; import org.restcomm.media.rtcp.RtcpChannel; import org.restcomm.media.rtp.ChannelsManager; import org.restcomm.media.rtp.RtpChannel; import org.restcomm.media.rtp.RtpClock; import org.restcomm.media.rtp.SsrcGenerator; import org.restcomm.media.rtp.statistics.RtpStatistics; import org.restcomm.media.scheduler.Clock; import org.restcomm.media.sdp.fields.MediaDescriptionField; import org.restcomm.media.sdp.format.AVProfile; import org.restcomm.media.sdp.format.RTPFormat; import org.restcomm.media.sdp.format.RTPFormats; import org.restcomm.media.spi.ConnectionMode; import org.restcomm.media.spi.FormatNotSupportedException; import org.restcomm.media.spi.dsp.Codec; import org.restcomm.media.spi.dsp.Processor; import org.restcomm.media.spi.format.AudioFormat; import org.restcomm.media.spi.format.FormatFactory; import org.restcomm.media.spi.format.Formats; /** * Abstract representation of a media channel with RTP and RTCP components. * * @author Henrique Rosa (henrique.rosa@telestax.com) * */ public abstract class MediaChannel { private static final Logger logger = Logger.getLogger(MediaChannel.class); // Media Formats private final static AudioFormat DTMF_FORMAT = FormatFactory.createAudioFormat("telephone-event", 8000); private final static AudioFormat LINEAR_FORMAT = FormatFactory.createAudioFormat("LINEAR", 8000, 16, 1); // Media Session Properties protected final String mediaType; protected long ssrc; protected String cname; protected boolean rtcpMux; protected boolean open; private boolean ice; private boolean dtls; // RTP Components protected RtpClock clock; protected RtpClock oobClock; protected RtpChannel rtpChannel; protected RtcpChannel rtcpChannel; protected RtpStatistics statistics; protected RTPFormats supportedFormats; protected RTPFormats offeredFormats; protected RTPFormats negotiatedFormats; protected boolean negotiated; // ICE components private final IceAuthenticatorImpl iceAuthenticator; /** * Constructs a new media channel containing both RTP and RTCP components. * * <p> * The channel supports SRTP and ICE, but these features are turned off by * default. * </p> * * @param mediaType * The type of media flowing in the channel * @param wallClock * The wall clock used to synchronize media flows * @param channelsManager * The RTP and RTCP channel provider */ protected MediaChannel(String mediaType, Clock wallClock, ChannelsManager channelsManager) { // Media Session Properties this.mediaType = mediaType; this.ssrc = 0L; this.cname = ""; this.rtcpMux = false; this.open = false; this.ice = false; this.dtls = false; // RTP Components this.clock = new RtpClock(wallClock); this.oobClock = new RtpClock(wallClock); this.statistics = new RtpStatistics(clock, this.ssrc); this.rtpChannel = channelsManager.getRtpChannel(this.statistics, this.clock, this.oobClock); this.rtcpChannel = channelsManager.getRtcpChannel(this.statistics); this.supportedFormats = channelsManager.getCodecs(); this.offeredFormats = new RTPFormats(); this.negotiatedFormats = new RTPFormats(); this.negotiated = false; setFormats(this.supportedFormats); // ICE Components this.iceAuthenticator = new IceAuthenticatorImpl(); } /** * Gets the type of media handled by the channel. * * @return The type of media */ public String getMediaType() { return mediaType; } /** * Gets the synchronization source of the channel. * * @return The unique SSRC identifier of the channel */ public long getSsrc() { return ssrc; } /** * Sets the synchronization source of the channel. * * @param ssrc * The unique SSRC identifier of the channel */ public void setSsrc(long ssrc) { this.ssrc = ssrc; } /** * Gets the CNAME of the channel. * * @return The CNAME associated with the channel */ public String getCname() { return cname; } /** * Sets the CNAME of the channel. * * <p> * This attribute associates a media source with its endpoint, so it must be * shared between all media channels owned by the same connection. * </p> * * @param cname The Canonical End-Point Identifier of the channel */ public void setCname(String cname) { this.cname = cname; this.statistics.setCname(cname); } public String getExternalAddress() { if (this.rtpChannel.isBound()) { return this.rtpChannel.getExternalAddress(); } return ""; } /** * Gets the address the RTP channel is bound to. * * @return The address of the RTP channel. Returns empty String if RTP * channel is not bound. */ public String getRtpAddress() { if(this.rtpChannel.isBound()) { return this.rtpChannel.getLocalHost(); } return ""; } /** * Gets the port where the RTP channel is bound to. * * @return The port of the RTP channel. Returns zero if RTP channel is not * bound. */ public int getRtpPort() { if(this.rtpChannel.isBound()) { return this.rtpChannel.getLocalPort(); } return 0; } /** * Gets the address the RTCP channel is bound to. * * @return The address of the RTCP channel. Returns empty String if RTCP * channel is not bound. */ public String getRtcpAddress() { if(this.rtcpMux) { return getRtpAddress(); } if(this.rtcpChannel.isBound()) { return this.rtcpChannel.getLocalHost(); } return ""; } /** * Gets the port where the RTCP channel is bound to. * * @return The port of the RTCP channel. Returns zero if RTCP channel is not * bound. */ public int getRtcpPort() { if(this.rtcpMux) { return getRtpPort(); } if(this.rtcpChannel.isBound()) { return this.rtcpChannel.getLocalPort(); } return 0; } /** * Enables the channel and activates it's resources. */ public void open() { // generate a new unique identifier for the channel this.ssrc = SsrcGenerator.generateSsrc(); this.statistics.setSsrc(this.ssrc); this.open = true; if(logger.isDebugEnabled()) { logger.debug(this.mediaType + " channel " + this.ssrc + " is open"); } } /** * Disables the channel and deactivates it's resources. * * @throws IllegalStateException * When an attempt is done to deactivate the channel while * inactive. */ public void close() throws IllegalStateException { if (this.open) { // Close channels this.rtpChannel.close(); if (!this.rtcpMux) { this.rtcpChannel.close(); } if(logger.isDebugEnabled()) { logger.debug(this.mediaType + " channel " + this.ssrc + " is closed"); } // Reset state reset(); this.open = false; } else { throw new IllegalStateException("Channel is already inactive"); } } /** * Resets the state of the channel. * * Should be invoked whenever there is intention of reusing the same channel * for different calls. */ private void reset() { // Reset codecs resetFormats(); // Reset channels if (this.rtcpMux) { this.rtcpMux = false; } // Reset ICE if (this.ice) { disableICE(); } // Reset WebRTC if (this.dtls) { disableDTLS(); } // Reset statistics this.statistics.reset(); this.cname = ""; this.ssrc = 0L; } /** * Indicates whether the channel is active or not. * * @return Returns true if the channel is active. Returns false otherwise. */ public boolean isOpen() { return open; } /** * Indicates whether the channel is available (ready to use). * * For regular SIP calls, the channel should be available as soon as it is * activated.<br> * But for WebRTC calls the channel will only become available as soon as * the DTLS handshake completes. * * @return Returns true if the channel is available. Returns false otherwise. */ public boolean isAvailable() { boolean available = this.rtpChannel.isAvailable(); if (!this.rtcpMux) { available = available && this.rtcpChannel.isAvailable(); } return available; } /** * Sets the input Digital Signaling Processor (DSP) of the RTP component. * * @param dsp The input DSP of the RTP component */ public void setInputDsp(Processor dsp) { this.rtpChannel.setInputDsp(dsp); } /** * Gets the input Digital Signaling Processor (DSP) of the RTP component. * * @return The input DSP of the RTP component */ public Processor getInputDsp() { return this.rtpChannel.getInputDsp(); } /** * Sets the output Digital Signaling Processor (DSP) of the RTP component. * * @param dsp The input DSP of the RTP component */ public void setOutputDsp(Processor dsp) { this.rtpChannel.setOutputDsp(dsp); } /** * Gets the output Digital Signaling Processor (DSP) of the RTP component. * * @return The input DSP of the RTP component */ public Processor getOutputDsp() { return this.rtpChannel.getOutputDsp(); } /** * Sets the connection mode of the channel, affecting the receiving and * transmitting capabilities of the underlying RTP component. * * @param mode * The new connection mode of the RTP component */ public void setConnectionMode(ConnectionMode mode) { this.rtpChannel.updateMode(mode); } /** * Sets the supported codecs of the RTP components. * * @param formats * The supported codecs resulting from SDP negotiation */ protected void setFormats(RTPFormats formats) { try { this.rtpChannel.setFormatMap(formats); this.rtpChannel.setOutputFormats(formats.getFormats()); } catch (FormatNotSupportedException e) { // Never happens logger.warn("Could not set output formats", e); } } /** * Gets the list of codecs <b>currently</b> applied to the Media Session. * * @return Returns the list of supported formats if no codec negotiation as * happened over SDP so far.<br> * Returns the list of negotiated codecs otherwise. */ public RTPFormats getFormats() { if(this.negotiated) { return this.negotiatedFormats; } return this.supportedFormats; } /** * Gets the supported codecs of the RTP components. * * @return The codecs currently supported by the RTP component */ public RTPFormats getFormatMap() { return this.rtpChannel.getFormatMap(); } /** * Binds the RTP and RTCP components to a suitable address and port. * * @param isLocal * Whether the binding address is in local range. * @param rtcpMux * Whether RTCP multiplexing is supported.<br> * If so, both RTP and RTCP components will be merged into one * channel only. Otherwise, the RTCP component will be bound to * the odd port immediately after the RTP port. * @throws IOException * When channel cannot be bound to an address. */ public void bind(boolean isLocal, boolean rtcpMux) throws IOException, IllegalStateException { this.rtpChannel.bind(isLocal, rtcpMux); if(!rtcpMux) { this.rtcpChannel.bind(isLocal, this.rtpChannel.getLocalPort() + 1); } this.rtcpMux = rtcpMux; if(logger.isDebugEnabled()) { logger.debug(this.mediaType + " RTP channel " + this.ssrc + " is bound to " + this.rtpChannel.getLocalHost() + ":" + this.rtpChannel.getLocalPort()); if(rtcpMux) { logger.debug(this.mediaType + " is multiplexing RTCP"); } else { logger.debug(this.mediaType + " RTCP channel " + this.ssrc + " is bound to " + this.rtcpChannel.getLocalHost() + ":" + this.rtcpChannel.getLocalPort()); } } } /** * Indicates whether the media channel is multiplexing RTCP or not. * * @return Returns true if using rtcp-mux. Returns false otherwise. */ public boolean isRtcpMux() { return this.rtcpMux; } /** * Connected the RTP component to the remote peer. * * <p> * Once connected, the RTP component can only send/received traffic to/from * the remote peer. * </p> * * @param address * The address of the remote peer */ public void connectRtp(SocketAddress address) { this.rtpChannel.setRemotePeer(address); if(logger.isDebugEnabled()) { logger.debug(this.mediaType + " RTP channel " + this.ssrc + " connected to remote peer " + address.toString()); } } /** * Connected the RTP component to the remote peer. * * <p> * Once connected, the RTP component can only send/received traffic to/from * the remote peer. * </p> * * @param address * The address of the remote peer * @param port * The port of the remote peer */ public void connectRtp(String address, int port) { this.connectRtp(new InetSocketAddress(address, port)); } /** * Binds the RTCP component to a suitable address and port. * * @param isLocal * Whether the binding address must be in local range. * @param port * A specific port to bind to * @throws IOException * When the RTCP component cannot be bound to an address. * @throws IllegalStateException * The binding operation is not allowed if ICE is active */ public void bindRtcp(boolean isLocal, int port) throws IOException, IllegalStateException { if(this.ice) { throw new IllegalStateException("Cannot bind when ICE is enabled"); } this.rtcpChannel.bind(isLocal, port); this.rtcpMux = (port == this.rtpChannel.getLocalPort()); } /** * Connects the RTCP component to the remote peer. * * <p> * Once connected, the RTCP component can only send/received traffic to/from * the remote peer. * </p> * * @param address * The address of the remote peer */ public void connectRtcp(SocketAddress remoteAddress) { this.rtcpChannel.setRemotePeer(remoteAddress); if(logger.isDebugEnabled()) { logger.debug(this.mediaType + " RTCP channel " + this.ssrc + " has connected to remote peer " + remoteAddress.toString()); } } /** * Connects the RTCP component to the remote peer. * * <p> * Once connected, the RTCP component can only send/received traffic to/from * the remote peer. * </p> * * @param address * The address of the remote peer * @param port * A specific port to connect to */ public void connectRtcp(String address, int port) { this.connectRtcp(new InetSocketAddress(address, port)); } /* * CODECS */ /** * Constructs RTP payloads for given channel. * * @param channel * the media channel * @param profile * AVProfile part for media type of given channel * @return collection of RTP formats. */ protected RTPFormats buildRTPMap(RTPFormats profile) { RTPFormats list = new RTPFormats(); Formats fmts = new Formats(); if (this.rtpChannel.getOutputDsp() != null) { Codec[] currCodecs = this.rtpChannel.getOutputDsp().getCodecs(); for (int i = 0; i < currCodecs.length; i++) { if (currCodecs[i].getSupportedInputFormat().matches(LINEAR_FORMAT)) { fmts.add(currCodecs[i].getSupportedOutputFormat()); } } } fmts.add(DTMF_FORMAT); if (fmts != null) { for (int i = 0; i < fmts.size(); i++) { RTPFormat f = profile.find(fmts.get(i)); if (f != null) { list.add(f.clone()); } } } return list; } /** * Resets the list of supported codecs. */ private void resetFormats() { this.offeredFormats.clean(); this.negotiatedFormats.clean(); setFormats(this.supportedFormats); this.negotiated = false; } /** * Gets the list of negotiated codecs. * * @return The list of negotiated codecs. The list may be empty is no codecs * were negotiated over SDP with remote peer. */ public RTPFormats getNegotiatedFormats() { return this.negotiatedFormats; } /** * Gets whether the channel has negotiated codecs with the remote peer over * SDP. * * @return Returns false if the channel has not negotiated codecs yet. * Returns true otherwise. */ public boolean hasNegotiatedFormats() { return this.negotiated; } /** * Negotiates the list of supported codecs with the remote peer over SDP. * * @param media * The corresponding media description of the remote peer which * contains the payload types. */ public void negotiateFormats(MediaDescriptionField media) { // Clean currently offered formats this.offeredFormats.clean(); // Map payload types tp RTP Format for (int payloadType : media.getPayloadTypes()) { RTPFormat format = AVProfile.getFormat(payloadType, AVProfile.AUDIO); if(format != null) { this.offeredFormats.add(format); } } // Negotiate the formats and store intersection this.negotiatedFormats.clean(); this.supportedFormats.intersection(this.offeredFormats, this.negotiatedFormats); // Apply formats setFormats(this.negotiatedFormats); this.negotiated = true; } /** * Indicates whether the channel has successfully negotiated supported * codecs over SDP. * * @return Returns true if codecs have been negotiated. Returns false * otherwise. */ public boolean containsNegotiatedFormats() { return !negotiatedFormats.isEmpty() && negotiatedFormats.hasNonDTMF(); } /* * ICE */ /** * Enables ICE on the channel. * * <p> * An ICE-enabled channel will start an ICE Agent which gathers local * candidates and listens to incoming STUN requests as a mean to select the * proper address to be used during the call. * </p> * * @param externalAddress * The public address of the Media Server. Used for SRFLX * candidates. * @param rtcpMux * Whether RTCP is multiplexed or not. Affects number of * candidates. */ public void enableICE(String externalAddress, boolean rtcpMux) { if (!this.ice) { this.ice = true; this.rtcpMux = rtcpMux; this.iceAuthenticator.generateIceCredentials(); // Enable ICE on RTP channels this.rtpChannel.enableIce(this.iceAuthenticator); if(!rtcpMux) { this.rtcpChannel.enableIce(this.iceAuthenticator); } if (logger.isDebugEnabled()) { logger.debug(this.mediaType + " channel " + this.ssrc + " enabled ICE"); } } } /** * Disables ICE and closes ICE-related resources */ public void disableICE() { if (this.ice) { this.ice = false; this.iceAuthenticator.reset(); // Disable ICE on RTP channels this.rtpChannel.disableIce(); if(!rtcpMux) { this.rtcpChannel.disableIce(); } if (logger.isDebugEnabled()) { logger.debug(this.mediaType + " channel " + this.ssrc + " disabled ICE"); } } } /** * Indicates whether ICE is active or not. * * @return Returns true if ICE is enabled. Returns false otherwise. */ public boolean isIceEnabled() { return this.ice; } /** * Gets the user fragment used in ICE negotiation. * * @return The ICE ufrag. Returns an empty String if ICE is disabled on the * channel. */ public String getIceUfrag() { return this.ice ? this.iceAuthenticator.getUfrag() : ""; } /** * Gets the password used in ICE negotiation. * * @return The ICE password. Returns an empty String if ICE is disabled on * the channel. */ public String getIcePwd() { return this.ice ? this.iceAuthenticator.getPassword() : ""; } /* * DTLS */ /** * Enables DTLS on the channel. RTP and RTCP packets flowing through this * channel will be secured. * * <p> * This method is used in <b>inbound</b> calls where the remote fingerprint is known. * </p> * * @param remoteFingerprint * The DTLS finger print of the remote peer. */ public void enableDTLS(String hashFunction, String remoteFingerprint) { if (!this.dtls) { this.rtpChannel.enableSRTP(hashFunction, remoteFingerprint); if (!this.rtcpMux) { rtcpChannel.enableSRTCP(hashFunction, remoteFingerprint); } this.dtls = true; if (logger.isDebugEnabled()) { logger.debug(this.mediaType + " channel " + this.ssrc + " enabled DTLS"); } } } /** * Enables DTLS on the channel. RTP and RTCP packets flowing through this channel will be secured. * * <p> * This method is used in <b>outbound</b> calls where the remote fingerprint is NOT known.<br> * Once the remote peer replies via SDP, the remote fingerprint must be set. * </p> * * @throws IllegalStateException Cannot be invoked when DTLS is already enabled */ public void enableDTLS() { if (!this.dtls) { this.rtpChannel.enableSRTP(); if (!this.rtcpMux) { rtcpChannel.enableSRTCP(); } this.dtls = true; if (logger.isDebugEnabled()) { logger.debug(this.mediaType + " channel " + this.ssrc + " enabled DTLS"); } } } public void setRemoteFingerprint(String hashFunction, String fingerprint) { if (this.dtls) { this.rtpChannel.setRemoteFingerprint(hashFunction, fingerprint); if (!this.rtcpMux) { this.rtcpChannel.setRemoteFingerprint(hashFunction, fingerprint); } } } /** * Disables DTLS and closes related resources. */ public void disableDTLS() { if (this.dtls) { this.rtpChannel.disableSRTP(); if (!this.rtcpMux) { this.rtcpChannel.disableSRTCP(); } this.dtls = false; if (logger.isDebugEnabled()) { logger.debug(this.mediaType + " channel " + this.ssrc + " disabled DTLS"); } } } /** * Gets whether DTLS is enabled on the channel. * * @return Returns true if DTLS is enabled. Returns false otherwise. */ public boolean isDtlsEnabled() { return this.dtls; } /** * Gets the DTLS finger print. * * @return The DTLS finger print. Returns an empty String if DTLS is not * enabled on the channel. */ public String getDtlsFingerprint() { if(this.dtls) { return this.rtpChannel.getWebRtcLocalFingerprint().toString(); } return ""; } /* * Statistics */ /** * Gets the number of RTP packets received during the current call. * * @return The number of packets received */ public long getPacketsReceived() { if(this.open) { return this.statistics.getRtpPacketsReceived(); } return 0; } /** * Gets the number of bytes received during the current call. * <p> * <b>This number reflects only the payload of all RTP packets</b> received * up to the moment the method is invoked. * </p> * * @return The number of bytes received. */ public long getOctetsReceived() { if(this.open) { return this.statistics.getRtpOctetsReceived(); } return 0; } /** * Gets the number of RTP packets sent during the current call. * * @return The number of packets sent */ public long getPacketsSent() { if(this.open) { return this.statistics.getRtpPacketsSent(); } return 0; } /** * Gets the number of bytes sent during the current call. * <p> * <b>This number reflects only the payload of all RTP packets</b> sent * up to the moment the method is invoked. * </p> * * @return The number of bytes sent. */ public long getOctetsSent() { if(this.open) { return this.statistics.getRtpOctetsSent(); } return 0; } /** * Gets the current jitter of the call. * * <p> * The jitter is an estimate of the statistical variance of the RTP data packet * interarrival time, measured in timestamp units and expressed as an * unsigned integer. * </p> * * @return The current jitter. */ public long getJitter() { if(this.open) { return this.statistics.getMember(this.ssrc).getJitter(); } return 0; } }