/* * JBoss, Home of Professional Open Source * Copyright 2011, Red Hat, Inc. and individual contributors * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.restcomm.media.control.mgcp.connection; import java.io.IOException; import java.net.SocketException; import org.apache.log4j.Logger; import org.restcomm.media.component.audio.AudioComponent; import org.restcomm.media.component.oob.OOBComponent; import org.restcomm.media.rtp.ChannelsManager; import org.restcomm.media.rtp.CnameGenerator; import org.restcomm.media.rtp.RtpListener; import org.restcomm.media.rtp.channels.AudioChannel; import org.restcomm.media.rtp.sdp.SdpFactory; import org.restcomm.media.sdp.SdpException; import org.restcomm.media.sdp.SessionDescription; import org.restcomm.media.sdp.SessionDescriptionParser; import org.restcomm.media.sdp.dtls.attributes.FingerprintAttribute; import org.restcomm.media.sdp.fields.MediaDescriptionField; import org.restcomm.media.sdp.rtcp.attributes.RtcpAttribute; import org.restcomm.media.spi.Connection; import org.restcomm.media.spi.ConnectionFailureListener; import org.restcomm.media.spi.ConnectionMode; import org.restcomm.media.spi.ConnectionState; import org.restcomm.media.spi.ConnectionType; import org.restcomm.media.spi.ModeNotSupportedException; import org.restcomm.media.spi.dsp.DspFactory; import org.restcomm.media.spi.pooling.PooledObject; import org.restcomm.media.spi.utils.Text; /** * * @author Oifa Yulian * @author Amit Bhayani * @author Henrique Rosa (henrique.rosa@telestax.com) * * @see BaseConnection */ public class RtpConnectionImpl extends BaseConnection implements RtpListener, PooledObject { private static final Logger logger = Logger.getLogger(RtpConnectionImpl.class); // Core elements private final ChannelsManager channelsManager; // RTP session elements private String cname; private boolean outbound; private boolean local; // Media Channels private AudioChannel audioChannel; // Session Description private SessionDescription localSdp; private SessionDescription remoteSdp; // Listeners private ConnectionFailureListener connectionFailureListener; /** * Constructs a new RTP connection with one audio channel. * * @param id * The unique ID of the connection * @param channelsManager * The media channel provider * @param dspFactory * The DSP provider */ public RtpConnectionImpl(int id, ChannelsManager channelsManager, DspFactory dspFactory) { // Core elements super(id, channelsManager.getScheduler()); this.channelsManager = channelsManager; // Connection state this.outbound = false; this.local = false; this.cname = CnameGenerator.generateCname(); // Audio Channel this.audioChannel = this.channelsManager.getAudioChannel(); this.audioChannel.setCname(this.cname); try { this.audioChannel.setInputDsp(dspFactory.newProcessor()); this.audioChannel.setOutputDsp(dspFactory.newProcessor()); } catch (InstantiationException | ClassNotFoundException | IllegalAccessException e) { // exception may happen only if invalid classes have been set in configuration throw new RuntimeException("There are invalid classes specified in the configuration.", e); } } @Override public void generateCname() { this.cname = CnameGenerator.generateCname(); if (this.audioChannel != null) { this.audioChannel.setCname(this.cname); } } @Override public String getCname() { return this.cname; } @Override public AudioComponent getAudioComponent() { return this.audioChannel.getAudioComponent(); } @Override public OOBComponent getOOBComponent() { return this.audioChannel.getAudioOobComponent(); } @Override public boolean getIsLocal() { return this.local; } @Override public void setIsLocal(boolean isLocal) { this.local = isLocal; } @Override public void setOtherParty(Connection other) throws IOException { throw new IOException("Applicable only for a local connection"); } @Override public void setOtherParty(byte[] descriptor) throws IOException { try { this.remoteSdp = SessionDescriptionParser.parse(new String(descriptor)); if(ConnectionState.OPEN.equals(getState())) { renegotiateSession(); } else { setOtherParty(); } } catch (SdpException e) { throw new IOException(e); } } @Override public void setOtherParty(Text descriptor) throws IOException { setOtherParty(descriptor.toString().getBytes()); } private void renegotiateSession() throws IOException { MediaDescriptionField remoteAudio = this.remoteSdp.getMediaDescription("audio"); // Negotiate audio codecs this.audioChannel.negotiateFormats(remoteAudio); if (!this.audioChannel.containsNegotiatedFormats()) { throw new IOException("Audio codecs were not supported"); } // Generate SDP answer String bindAddress = this.local ? this.channelsManager.getLocalBindAddress() : this.channelsManager.getBindAddress(); String externalAddress = this.channelsManager.getUdpManager().getExternalAddress(); this.localSdp = SdpFactory.buildSdp(false, bindAddress, externalAddress, this.audioChannel); // Reject any channels other than audio MediaDescriptionField remoteVideo = this.remoteSdp.getMediaDescription("video"); if (remoteVideo != null) { SdpFactory.rejectMediaField(this.localSdp, remoteVideo); } MediaDescriptionField remoteApplication = this.remoteSdp.getMediaDescription("application"); if (remoteApplication != null) { SdpFactory.rejectMediaField(this.localSdp, remoteApplication); } // Connect to new remote address String remoteAddr = remoteAudio.getConnection().getAddress(); this.audioChannel.connectRtp(remoteAddr, remoteAudio.getPort()); this.audioChannel.connectRtcp(remoteAddr, remoteAudio.getRtcpPort()); } /** * Sets the remote peer based on the received remote SDP description. * * <p> * The connection will be setup according to the SDP, specifically whether * the call is WebRTC (with ICE enabled) or not. * </p> * * <p> * <b>SIP call:</b><br> * The RTP audio channel can be immediately bound to the configured bind * address.<br> * By reading connection fields of the SDP offer, we can set the remote peer * of the audio channel. * </p> * * <p> * <b>WebRTC call:</b><br> * An ICE-lite agent is created. This agent assumes a controlled role, and * starts listening for connectivity checks. This agent is responsible for * providing a socket for DTLS hanshake and RTP streaming, once the * connection is established with the remote peer.<br> * So, we can only bind the audio channel and set its remote peer once the * ICE agent has selected the candidate pairs and made such socket * available. * </p> * * @throws IOException * If an error occurs while setting up the remote peer */ private void setOtherParty() throws IOException { if (this.outbound) { setOtherPartyOutboundCall(); } else { setOtherPartyInboundCall(); } if (logger.isDebugEnabled()) { logger.debug("Audio formats: " + this.audioChannel.getFormatMap()); } } /** * Sets the remote peer based on the remote SDP description. * * <p> * In this case, the connection belongs to an outbound call. So, the remote * SDP is the offer and, as result, the proper answer is generated.<br> * The SDP answer can be sent later to the remote peer. * </p> * * @throws IOException * If an error occurs while setting up the remote peer */ private void setOtherPartyInboundCall() throws IOException { // Setup the audio channel based on remote offer MediaDescriptionField remoteAudio = this.remoteSdp .getMediaDescription("audio"); if (remoteAudio != null) { this.audioChannel.open(); setupAudioChannelInbound(remoteAudio); } // Generate SDP answer String bindAddress = this.local ? this.channelsManager .getLocalBindAddress() : this.channelsManager.getBindAddress(); String externalAddress = this.channelsManager.getUdpManager() .getExternalAddress(); if (this.audioChannel.isOpen()) { this.localSdp = SdpFactory.buildSdp(false, bindAddress, externalAddress, this.audioChannel); } else { // In case remote peer did not offer audio channel this.localSdp = SdpFactory.buildSdp(false, bindAddress, externalAddress); } // Reject any channels other than audio MediaDescriptionField remoteVideo = this.remoteSdp .getMediaDescription("video"); if (remoteVideo != null) { SdpFactory.rejectMediaField(this.localSdp, remoteVideo); } MediaDescriptionField remoteApplication = this.remoteSdp .getMediaDescription("application"); if (remoteApplication != null) { SdpFactory.rejectMediaField(this.localSdp, remoteApplication); } // Change the state of this RTP connection from HALF_OPEN to OPEN try { this.join(); } catch (Exception e) { // exception is possible here when already joined logger.warn("Could not set connection state to OPEN", e); } } /** * Sets the remote peer based on the remote SDP description. * * <p> * In this case, the connection belongs to an inbound call. So, the remote SDP is the answer which implies that this * connection already generated the proper offer. * </p> * * @throws IOException If an error occurs while setting up the remote peer */ private void setOtherPartyOutboundCall() throws IOException { // Setup audio channel MediaDescriptionField remoteAudio = this.remoteSdp.getMediaDescription("audio"); if (remoteAudio != null) { // Set remote DTLS fingerprint if(this.audioChannel.isDtlsEnabled()) { FingerprintAttribute fingerprint = remoteAudio.getFingerprint(); this.audioChannel.setRemoteFingerprint(fingerprint.getHashFunction(), fingerprint.getFingerprint()); } setupAudioChannelOutbound(remoteAudio); } // Change the state of this RTP connection from HALF_OPEN to OPEN try { this.join(); } catch (Exception e) { // exception is possible here when already joined logger.warn("Could not set connection state to OPEN", e); } } /** * Reads the remote SDP offer and sets up the available resources according * to the call type. * * <p> * In case of a WebRTC call, an ICE-lite agent is created. The agent will * start listening for connectivity checks from the remote peer.<br> * Also, a WebRTC handler will be enabled on the corresponding audio * channel. * </p> * * @param remoteAudio * The description of the remote audio channel. * * @throws IOException * When binding the audio data channel. Non-WebRTC calls only. * @throws SocketException * When binding the audio data channel. Non-WebRTC calls only. */ private void setupAudioChannelInbound(MediaDescriptionField remoteAudio) throws IOException { // Negotiate audio codecs this.audioChannel.negotiateFormats(remoteAudio); if (!this.audioChannel.containsNegotiatedFormats()) { throw new IOException("Audio codecs were not supported"); } // Bind audio channel to an address provided by UdpManager this.audioChannel.bind(this.local, remoteAudio.isRtcpMux()); boolean enableIce = remoteAudio.containsIce(); if (enableIce) { // Enable ICE. Wait for ICE handshake to finish before connecting RTP/RTCP channels this.audioChannel.enableICE(this.channelsManager.getExternalAddress(), remoteAudio.isRtcpMux()); } else { String remoteAddr = remoteAudio.getConnection().getAddress(); this.audioChannel.connectRtp(remoteAddr, remoteAudio.getPort()); this.audioChannel.connectRtcp(remoteAddr, remoteAudio.getRtcpPort()); } // Enable DTLS according to remote SDP description boolean enableDtls = this.remoteSdp.containsDtls(); if (enableDtls) { FingerprintAttribute fingerprint = this.remoteSdp.getFingerprint(audioChannel.getMediaType()); this.audioChannel.enableDTLS(fingerprint.getHashFunction(), fingerprint.getFingerprint()); } } /** * Reads the remote SDP answer and sets up the proper media channels. * * @param remoteAudio * The description of the remote audio channel. * * @throws IOException * When binding the audio data channel. Non-WebRTC calls only. * @throws SocketException * When binding the audio data channel. Non-WebRTC calls only. */ private void setupAudioChannelOutbound(MediaDescriptionField remoteAudio) throws IOException { // Negotiate audio codecs this.audioChannel.negotiateFormats(remoteAudio); if (!this.audioChannel.containsNegotiatedFormats()) { throw new IOException("Audio codecs were not supported"); } // connect to remote peer - RTP String remoteRtpAddress = remoteAudio.getConnection().getAddress(); int remoteRtpPort = remoteAudio.getPort(); // only connect is calls are plain old SIP // For WebRTC cases, the ICE Agent must connect upon candidate selection boolean connectNow = !(this.outbound && audioChannel.isIceEnabled()); if (connectNow) { this.audioChannel.connectRtp(remoteRtpAddress, remoteRtpPort); // connect to remote peer - RTCP boolean remoteRtcpMux = remoteAudio.isRtcpMux(); if (remoteRtcpMux) { this.audioChannel.connectRtcp(remoteRtpAddress, remoteRtpPort); } else { RtcpAttribute remoteRtcp = remoteAudio.getRtcp(); if (remoteRtcp == null) { // No specific RTCP port, so default is RTP port + 1 this.audioChannel.connectRtcp(remoteRtpAddress, remoteRtpPort + 1); } else { // Specific RTCP address and port contained in SDP String remoteRtcpAddress = remoteRtcp.getAddress(); if (remoteRtcpAddress == null) { // address is optional in rtcp attribute // will match RTP address if not defined remoteRtcpAddress = remoteRtpAddress; } int remoteRtcpPort = remoteRtcp.getPort(); this.audioChannel.connectRtcp(remoteRtcpAddress, remoteRtcpPort); } } } } @Override public void setMode(ConnectionMode mode) throws ModeNotSupportedException { this.audioChannel.setConnectionMode(mode); super.setMode(mode); } @Override public String getDescriptor() { return (this.localSdp == null) ? "" : this.localSdp.toString(); } @Override public void generateOffer(boolean webrtc) throws IOException { // Only open and bind a new channel if not currently configured if (!this.audioChannel.isOpen()) { // call is outbound since the connection is generating the offer this.outbound = true; // setup audio channel this.audioChannel.open(); this.audioChannel.bind(this.local, webrtc); if (webrtc) { this.audioChannel.enableICE(this.channelsManager.getExternalAddress(), true); this.audioChannel.enableDTLS(); } // generate SDP offer based on audio channel String bindAddress = this.local ? this.channelsManager.getLocalBindAddress() : this.channelsManager .getBindAddress(); String externalAddress = this.channelsManager.getUdpManager().getExternalAddress(); this.localSdp = SdpFactory.buildSdp(true, bindAddress, externalAddress, this.audioChannel); this.remoteSdp = null; } } @Override public String getLocalDescriptor() { return (this.localSdp == null) ? "" : this.localSdp.toString(); } @Override public String getRemoteDescriptor() { return (this.remoteSdp == null) ? "" : this.remoteSdp.toString(); } public long getPacketsReceived() { return this.audioChannel.getPacketsReceived(); } @Override public long getBytesReceived() { return this.audioChannel.getOctetsReceived(); } @Override public long getPacketsTransmitted() { return this.audioChannel.getPacketsSent(); } @Override public long getBytesTransmitted() { return this.audioChannel.getOctetsSent(); } @Override public double getJitter() { return this.audioChannel.getJitter(); } @Override public boolean isAvailable() { return this.audioChannel.isAvailable(); } @Override public String toString() { return "RTP Connection [" + getEndpoint().getLocalName() + "]"; } /** * Closes any active resources (like media channels) associated with the * connection. */ private void closeResources() { if (this.audioChannel.isOpen()) { this.audioChannel.close(); } } /** * Resets the state of the connection. */ private void reset() { // Reset SDP this.outbound = false; this.localSdp = null; this.remoteSdp = null; } @Override public void onRtpFailure(String message) { if (this.audioChannel.isOpen()) { logger.warn(message); // RTP is mandatory, if it fails close everything onFailed(); } } @Override public void onRtpFailure(Throwable e) { String message = "RTP failure!"; if (e != null) { message += " Reason: " + e.getMessage(); } onRtpFailure(message); } @Override public void onRtcpFailure(String e) { if (this.audioChannel.isOpen()) { logger.warn(e); // Close the RTCP channel only // Keep the RTP channel open because RTCP is not mandatory onFailed(); } } @Override public void onRtcpFailure(Throwable e) { String message = "RTCP failure!"; if (e != null) { message += " Reason: " + e.getMessage(); } onRtcpFailure(message); } @Override public void setConnectionFailureListener( ConnectionFailureListener connectionFailureListener) { this.connectionFailureListener = connectionFailureListener; } @Override protected void onCreated() throws Exception { // Reset components so they can be re-used in new calls reset(); } @Override protected void onFailed() { closeResources(); if (this.connectionFailureListener != null) { this.connectionFailureListener.onFailure(); } } @Override protected void onOpened() throws Exception { // TODO not implemented } @Override protected void onClosed() { closeResources(); try { setMode(ConnectionMode.INACTIVE); } catch (ModeNotSupportedException e) { logger.warn("Could not set connection mode to INACTIVE.", e); } releaseConnection(ConnectionType.RTP); this.connectionFailureListener = null; } @Override public void checkIn() { closeResources(); reset(); } @Override public void checkOut() { generateCname(); } }