org.restcomm.media.control.mgcp.connection.MgcpRemoteConnection Maven / Gradle / Ivy
/*
* TeleStax, Open Source Cloud Communications
* Copyright 2011-2016, Telestax Inc and individual contributors
* by the @authors tag.
*
* 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 org.apache.log4j.Logger;
import org.restcomm.media.component.audio.AudioComponent;
import org.restcomm.media.component.oob.OOBComponent;
import org.restcomm.media.control.mgcp.exception.MgcpConnectionException;
import org.restcomm.media.control.mgcp.message.LocalConnectionOptionType;
import org.restcomm.media.control.mgcp.message.LocalConnectionOptions;
import org.restcomm.media.control.mgcp.pkg.MgcpEvent;
import org.restcomm.media.control.mgcp.pkg.MgcpEventProvider;
import org.restcomm.media.control.mgcp.pkg.MgcpRequestedEvent;
import org.restcomm.media.control.mgcp.pkg.r.RtpPackage;
import org.restcomm.media.control.mgcp.pkg.r.rto.RtpTimeoutEvent;
import org.restcomm.media.rtp.CnameGenerator;
import org.restcomm.media.rtp.RtpListener;
import org.restcomm.media.rtp.channels.AudioChannel;
import org.restcomm.media.rtp.channels.MediaChannelProvider;
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.ConnectionMode;
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
/**
* Type of connection that connects one endpoint to a remote peer.
*
* @author Henrique Rosa ([email protected])
*
*/
public class MgcpRemoteConnection extends AbstractMgcpConnection implements RtpListener {
private static final Logger log = Logger.getLogger(MgcpRemoteConnection.class);
// Connection Properties
private final String localAddress;
private final String externalAddress;
private final String cname;
private boolean outbound;
private boolean webrtc;
private SessionDescription localSdp;
private SessionDescription remoteSdp;
// Media Channels
private final AudioChannel audioChannel;
public MgcpRemoteConnection(int identifier, int callId, int halfOpenTimeout, int openTimeout, MgcpEventProvider eventProvider, MediaChannelProvider channelProvider, ListeningScheduledExecutorService executor) {
super(identifier, callId, halfOpenTimeout, openTimeout, eventProvider, executor);
// Connection Properties
this.localAddress = channelProvider.getLocalAddress();
this.externalAddress = channelProvider.getExternalAddress();
this.cname = CnameGenerator.generateCname();
this.outbound = false;
this.webrtc = false;
this.localSdp = null;
this.remoteSdp = null;
// Media Channels
this.audioChannel = channelProvider.provideAudioChannel();
this.audioChannel.setCname(this.cname);
}
public MgcpRemoteConnection(int identifier, int callId, int timeout, MgcpEventProvider eventProvider, MediaChannelProvider channelProvider, ListeningScheduledExecutorService executor) {
this(identifier, callId, HALF_OPEN_TIMER, timeout, eventProvider, channelProvider, executor);
}
public MgcpRemoteConnection(int identifier, int callId, MgcpEventProvider eventProvider, MediaChannelProvider channelProvider, ListeningScheduledExecutorService executor) {
this(identifier, callId, 0, eventProvider, channelProvider, executor);
}
@Override
public boolean isLocal() {
return false;
}
@Override
public void setMode(ConnectionMode mode) throws IllegalStateException {
super.setMode(mode);
this.audioChannel.setConnectionMode(mode);
}
@Override
public String halfOpen(LocalConnectionOptions options) throws MgcpConnectionException {
synchronized (this.stateLock) {
switch (this.state) {
case CLOSED:
// Update state
this.state = MgcpConnectionState.HALF_OPEN;
// Read options
String webrtcOption = options.get(LocalConnectionOptionType.WEBRTC);
// Setup audio channel
this.outbound = true;
this.webrtc = webrtcOption != null && webrtcOption.equals("true");
this.audioChannel.open();
try {
this.audioChannel.bind(false, this.webrtc);
} catch (IOException e) {
throw new MgcpConnectionException("Could not bind audio channel " + this.cname, e);
}
// Enable ICE and DTLS for WebRTC calls
if (this.webrtc) {
this.audioChannel.enableICE(this.externalAddress, true);
this.audioChannel.enableDTLS();
}
// Generate SDP offer
this.localSdp = SdpFactory.buildSdp(true, this.localAddress, this.externalAddress, this.audioChannel);
if(log.isDebugEnabled()) {
log.debug("Connection " + getHexIdentifier() + " state is " + this.state.name());
}
if (this.halfOpenTimeout > 0) {
expireIn(this.halfOpenTimeout);
}
return this.localSdp.toString();
default:
throw new MgcpConnectionException("Cannot half-open connection " + this.getHexIdentifier() + " because state is " + this.state.name());
}
}
}
@Override
public String open(String sdp) throws MgcpConnectionException {
synchronized (this.stateLock) {
switch (this.state) {
case CLOSED:
case HALF_OPEN:
// Update state
this.state = MgcpConnectionState.OPEN;
// Parse remote SDP
try {
this.remoteSdp = SessionDescriptionParser.parse(sdp);
} catch (SdpException e) {
throw new MgcpConnectionException(e.getMessage(), e);
}
// Open connection
openConnection();
if(log.isDebugEnabled()) {
log.debug("Connection " + getHexIdentifier() + " state is " + this.state.name());
}
// Submit timer
if (this.timeout > 0) {
expireIn(this.timeout);
}
break;
default:
throw new MgcpConnectionException(
"Cannot open connection " + this.getHexIdentifier() + " because state is " + this.state.name());
}
}
return this.localSdp.toString();
}
private void openConnection() throws MgcpConnectionException {
this.audioChannel.open();
try {
if (this.outbound) {
openOutboundConnection();
} else {
openInboundConnection();
}
} catch (IOException e) {
throw new MgcpConnectionException(e.getMessage(), e);
}
}
/**
* Sets the remote peer based on the remote SDP description.
*
*
* In this case, the connection belongs to an inbound call. So, the remote SDP is the offer and, as result, the proper
* answer is generated.
* The SDP answer can be sent later to the remote peer.
*
*
* @throws IOException If an error occurs while setting up the remote peer
*/
private void openInboundConnection() throws IOException {
// Open audio channel
this.audioChannel.open();
// Connect audio channel to remote peer
MediaDescriptionField remoteAudio = this.remoteSdp.getMediaDescription("audio");
setupAudioChannelInbound(remoteAudio);
// Generate SDP answer
this.localSdp = SdpFactory.buildSdp(false, this.localAddress, this.externalAddress, this.audioChannel);
// Reject video stream (not supported)
MediaDescriptionField remoteVideo = this.remoteSdp.getMediaDescription("video");
if (remoteVideo != null) {
SdpFactory.rejectMediaField(this.localSdp, remoteVideo);
}
// Reject data stream (not supported)
MediaDescriptionField remoteApplication = this.remoteSdp.getMediaDescription("application");
if (remoteApplication != null) {
SdpFactory.rejectMediaField(this.localSdp, remoteApplication);
}
}
/**
* Reads the remote SDP offer and sets up the available resources according to the call type.
*
* @param remoteAudio The description of the remote audio channel.
*
* @throws IOException When binding the audio data channel or when failing to negotiate codecs.
*/
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(false, 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.externalAddress, 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());
}
}
/**
* Sets the remote peer based on the remote SDP description.
*
*
* 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.
*
*
* @throws IOException If an error occurs while setting up the remote peer
*/
private void openOutboundConnection() throws IOException {
// Setup audio channel
MediaDescriptionField remoteAudio = this.remoteSdp.getMediaDescription("audio");
// Set remote DTLS fingerprint
if (this.audioChannel.isDtlsEnabled()) {
FingerprintAttribute fingerprint = remoteAudio.getFingerprint();
this.audioChannel.setRemoteFingerprint(fingerprint.getHashFunction(), fingerprint.getFingerprint());
}
setupAudioChannelOutbound(remoteAudio);
}
/**
* 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 or when failing to negotiate codecs.
*/
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 close() throws MgcpConnectionException {
synchronized (this.stateLock) {
switch (this.state) {
case HALF_OPEN:
case OPEN:
if(this.timerFuture != null && !this.timerFuture.isDone()) {
this.timerFuture.cancel(false);
}
// Deactivate connection
setMode(ConnectionMode.INACTIVE);
// Update connection state
this.state = MgcpConnectionState.CLOSED;
// Close audio channel
if (this.audioChannel.isOpen()) {
this.audioChannel.close();
}
// Reset internal state
reset();
if(log.isDebugEnabled()) {
log.debug("Connection " + getHexIdentifier() + " state is " + this.state.name());
}
break;
default:
throw new MgcpConnectionException("Cannot close connection " + this.getHexIdentifier() + "because state is " + this.state.name());
}
}
}
@Override
protected void listen(MgcpEvent event) {
if(event instanceof RtpTimeoutEvent) {
listen((RtpTimeoutEvent) event);
}
if (log.isDebugEnabled()) {
log.debug("Connection " + getHexIdentifier() + " is listening to event " + event.toString());
}
}
private void listen(RtpTimeoutEvent timeoutEvent) {
// TODO start inter-rtp timer or override existing one.
}
@Override
protected boolean isEventSupported(MgcpRequestedEvent event) {
switch (event.getPackageName()) {
case RtpPackage.PACKAGE_NAME:
switch (event.getEventType()) {
case RtpTimeoutEvent.SYMBOL:
return true;
default:
return false;
}
default:
return false;
}
}
@Override
public AudioComponent getAudioComponent() {
return this.audioChannel.getAudioComponent();
}
@Override
public OOBComponent getOutOfBandComponent() {
return this.audioChannel.getAudioOobComponent();
}
@Override
public void onRtpFailure(Throwable e) {
String message = "RTP channel failure on connection " + this.cname + "!";
if (e != null && e.getMessage() != null) {
message += " Reason: " + e.getMessage();
}
onRtpFailure(message);
}
@Override
public void onRtpFailure(String message) {
final int timeoutValue = MgcpConnectionState.HALF_OPEN.equals(this.state) ? this.halfOpenTimeout : this.timeout;
try {
// RTP is mandatory, if it fails close everything
close();
} catch (Exception e) {
log.warn("Failed to elegantly close connection " + this.cname + " after RTP failure", e);
} finally {
// Warn connection listener about failure
notify(this, new RtpTimeoutEvent(getIdentifier(), timeoutValue));
}
}
@Override
public void onRtcpFailure(Throwable e) {
String message = "Closing RTCP channel on connection " + this.cname + " due to failure!";
if (e != null && e.getMessage() != null) {
message += " Reason: " + e.getMessage();
}
onRtcpFailure(message);
}
@Override
public void onRtcpFailure(String e) {
log.warn(e);
}
private void reset() {
this.outbound = false;
this.webrtc = false;
this.localSdp = null;
this.remoteSdp = null;
}
@Override
protected Logger log() {
return log;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy