/* * 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.sdp; import org.restcomm.media.ice.IceCandidate; import org.restcomm.media.ice.IceComponent; import org.restcomm.media.rtp.channels.AudioChannel; import org.restcomm.media.rtp.channels.MediaChannel; import org.restcomm.media.sdp.MediaProfile; import org.restcomm.media.sdp.SessionDescription; import org.restcomm.media.sdp.attributes.ConnectionModeAttribute; import org.restcomm.media.sdp.attributes.FormatParameterAttribute; import org.restcomm.media.sdp.attributes.PacketTimeAttribute; import org.restcomm.media.sdp.attributes.RtpMapAttribute; import org.restcomm.media.sdp.attributes.SsrcAttribute; import org.restcomm.media.sdp.dtls.attributes.FingerprintAttribute; import org.restcomm.media.sdp.dtls.attributes.SetupAttribute; import org.restcomm.media.sdp.fields.ConnectionField; import org.restcomm.media.sdp.fields.MediaDescriptionField; import org.restcomm.media.sdp.fields.OriginField; import org.restcomm.media.sdp.fields.SessionNameField; import org.restcomm.media.sdp.fields.TimingField; import org.restcomm.media.sdp.fields.VersionField; import org.restcomm.media.sdp.format.AVProfile; import org.restcomm.media.sdp.format.RTPFormat; import org.restcomm.media.sdp.ice.attributes.CandidateAttribute; import org.restcomm.media.sdp.ice.attributes.IceLiteAttribute; import org.restcomm.media.sdp.ice.attributes.IcePwdAttribute; import org.restcomm.media.sdp.ice.attributes.IceUfragAttribute; import org.restcomm.media.sdp.rtcp.attributes.RtcpAttribute; import org.restcomm.media.sdp.rtcp.attributes.RtcpMuxAttribute; import org.restcomm.media.spi.format.AudioFormat; /** * Factory that produces SDP offers and answers. * * @author Henrique Rosa (henrique.rosa@telestax.com) * */ public class SdpFactory { /** * Builds a Session Description object to be sent to a remote peer. * * @param offer if the SDP is for an answer or answer. * @param localAddress The local address of the media server. * @param externalAddress The public address of the media server. * @param channels The media channels to be included in the session description. * @return The Session Description object. */ public static SessionDescription buildSdp(boolean offer, String localAddress, String externalAddress, MediaChannel... channels) { // Session-level fields SessionDescription sd = new SessionDescription(); sd.setVersion(new VersionField((short) 0)); String originAddress = (externalAddress == null || externalAddress.isEmpty()) ? localAddress : externalAddress; sd.setOrigin(new OriginField("-", String.valueOf(System.currentTimeMillis()), "1", "IN", "IP4", originAddress)); sd.setSessionName(new SessionNameField("Mobicents Media Server")); sd.setConnection(new ConnectionField("IN", "IP4", originAddress)); sd.setTiming(new TimingField(0, 0)); // Media Descriptions boolean ice = false; for (MediaChannel channel : channels) { MediaDescriptionField md = buildMediaDescription(channel, offer); md.setSession(sd); sd.addMediaDescription(md); if(md.containsIce()) { // Fix session-level attribute sd.getConnection().setAddress(md.getConnection().getAddress()); ice = true; } } // Session-level ICE if(ice) { sd.setIceLite(new IceLiteAttribute()); } return sd; } /** * Rejects a media description from an SDP offer. * * @param answer * The SDP answer to include the rejected media * @param media * The offered media description to be rejected */ public static void rejectMediaField(SessionDescription answer, MediaDescriptionField media) { MediaDescriptionField rejected = new MediaDescriptionField(); rejected.setMedia(media.getMedia()); rejected.setPort(0); rejected.setProtocol(media.getProtocol()); rejected.setPayloadTypes(media.getPayloadTypes()); rejected.setSession(answer); answer.addMediaDescription(rejected); } /** * Build an SDP description for a media channel. * * @param channel * The channel to read information from * @return The SDP media description */ public static MediaDescriptionField buildMediaDescription(MediaChannel channel, boolean offer) { MediaDescriptionField md = new MediaDescriptionField(); md.setMedia(channel.getMediaType()); md.setPort(channel.getRtpPort()); MediaProfile profile = channel.isDtlsEnabled() ? MediaProfile.RTP_SAVPF : MediaProfile.RTP_AVP; md.setProtocol(profile.getProfile()); final String externalAddress = channel.getExternalAddress() == null || channel.getExternalAddress().isEmpty() ? null : channel.getExternalAddress(); md.setConnection(new ConnectionField("IN", "IP4", externalAddress != null ? externalAddress : channel.getRtpAddress())); md.setPtime(new PacketTimeAttribute(20)); md.setRtcp(new RtcpAttribute(channel.getRtcpPort(), "IN", "IP4", externalAddress != null ? externalAddress : channel.getRtcpAddress())); if (channel.isRtcpMux()) { md.setRtcpMux(new RtcpMuxAttribute()); } // ICE attributes if (channel.isIceEnabled()) { md.setIceUfrag(new IceUfragAttribute(channel.getIceUfrag())); md.setIcePwd(new IcePwdAttribute(channel.getIcePwd())); // Fix connection address based on default (only) candidate md.getConnection().setAddress(externalAddress != null ? externalAddress : channel.getRtpAddress()); md.setPort(channel.getRtpPort()); // Fix RTCP if rtcp-mux is used if(channel.isRtcpMux()) { md.getRtcp().setAddress(externalAddress != null ? externalAddress : channel.getRtpAddress()); md.getRtcp().setPort(channel.getRtpPort()); } // Add HOST candidate md.addCandidate(processHostCandidate(channel, IceComponent.RTP_ID)); if(!channel.isRtcpMux()) { md.addCandidate(processHostCandidate(channel, IceComponent.RTCP_ID)); } if(channel.getExternalAddress() != null && !channel.getExternalAddress().isEmpty()) { // Add SRFLX candidate md.addCandidate(processSrflxCandidate(channel, IceComponent.RTP_ID)); if(!channel.isRtcpMux()) { md.addCandidate(processSrflxCandidate(channel, IceComponent.RTCP_ID)); } } // List<LocalCandidateWrapper> rtpCandidates = channel.getRtpCandidates(); // if(!rtpCandidates.isEmpty()) { // // Fix connection address based on default candidate // IceCandidate defaultCandidate = channel.getDefaultRtpCandidate().getCandidate(); // md.getConnection().setAddress(defaultCandidate.getHostString()); // md.setPort(defaultCandidate.getPort()); // // // Fix RTCP if rtcp-mux is used // if(channel.isRtcpMux()) { // md.getRtcp().setAddress(defaultCandidate.getHostString()); // md.getRtcp().setPort(defaultCandidate.getPort()); // } // // // Add candidates list for ICE negotiation // for (LocalCandidateWrapper candidate : rtpCandidates) { // md.addCandidate(processCandidate(candidate.getCandidate())); // } // } // if (!channel.isRtcpMux()) { // List<LocalCandidateWrapper> rtcpCandidates = channel.getRtcpCandidates(); // // if(!rtcpCandidates.isEmpty()) { // // Fix RTCP based on default RTCP candidate // IceCandidate defaultCandidate = channel.getDefaultRtcpCandidate().getCandidate(); // md.getRtcp().setAddress(defaultCandidate.getHostString()); // md.getRtcp().setPort(defaultCandidate.getPort()); // // // Add candidates list for ICE negotiation // for (LocalCandidateWrapper candidate : rtcpCandidates) { // md.addCandidate(processCandidate(candidate.getCandidate())); // } // } // } } // Media formats RTPFormat[] negotiatedFormats = channel.getFormats().toArray(); for (int index = 0; index < negotiatedFormats.length; index++) { RTPFormat f = negotiatedFormats[index]; // Fixes #61 - MMS SDP offer should offer only 101 telephone-event if(offer && AVProfile.isDtmf(f) && !AVProfile.isDefaultDtmf(f)) { continue; } RtpMapAttribute rtpMap = new RtpMapAttribute(); rtpMap.setPayloadType(f.getID()); rtpMap.setCodec(f.getFormat().getName().toString()); rtpMap.setClockRate(f.getClockRate()); switch (channel.getMediaType()) { case AudioChannel.MEDIA_TYPE: AudioFormat audioFormat = (AudioFormat) f.getFormat(); if (audioFormat.getChannels() > 1) { rtpMap.setCodecParams(audioFormat.getChannels()); } if (audioFormat.getOptions() != null) { rtpMap.setParameters(new FormatParameterAttribute(f.getID(), audioFormat.getOptions().toString())); } break; default: throw new IllegalArgumentException("Media type " + channel.getMediaType() + " not supported."); } md.addPayloadType(f.getID()); md.addFormat(rtpMap); } // DTLS attributes if (channel.isDtlsEnabled()) { md.setSetup(new SetupAttribute(offer ? SetupAttribute.ACTPASS : SetupAttribute.PASSIVE)); String fingerprint = channel.getDtlsFingerprint(); int whitespace = fingerprint.indexOf(" "); String fingerprintHash = fingerprint.substring(0, whitespace); String fingerprintValue = fingerprint.substring(whitespace + 1); md.setFingerprint(new FingerprintAttribute(fingerprintHash, fingerprintValue)); } md.setConnectionMode(new ConnectionModeAttribute(ConnectionModeAttribute.SENDRECV)); SsrcAttribute ssrcAttribute = new SsrcAttribute(Long.toString(channel.getSsrc())); ssrcAttribute.addAttribute("cname", channel.getCname()); md.setSsrc(ssrcAttribute); return md; } private static CandidateAttribute processCandidate(IceCandidate candidate) { CandidateAttribute candidateSdp = new CandidateAttribute(); candidateSdp.setFoundation(candidate.getFoundation()); candidateSdp.setComponentId(candidate.getComponentId()); candidateSdp.setProtocol(candidate.getProtocol().getDescription()); candidateSdp.setPriority(candidate.getPriority()); candidateSdp.setAddress(candidate.getHostString()); candidateSdp.setPort(candidate.getPort()); String candidateType = candidate.getType().getDescription(); candidateSdp.setCandidateType(candidateType); if (CandidateAttribute.TYP_HOST != candidateType) { candidateSdp.setRelatedAddress(candidate.getBase().getHostString()); candidateSdp.setRelatedPort(candidate.getBase().getPort()); } candidateSdp.setGeneration(0); return candidateSdp; } private static CandidateAttribute processHostCandidate(MediaChannel candidate, short componentId) { CandidateAttribute candidateSdp = new CandidateAttribute(); candidateSdp.setFoundation("11111111"); candidateSdp.setComponentId(componentId); candidateSdp.setProtocol("udp"); candidateSdp.setPriority(1L); switch (componentId) { case IceComponent.RTP_ID: candidateSdp.setAddress(candidate.getRtpAddress()); candidateSdp.setPort(candidate.getRtpPort()); break; case IceComponent.RTCP_ID: candidateSdp.setAddress(candidate.getRtcpAddress()); candidateSdp.setPort(candidate.getRtcpPort()); break; default: break; } candidateSdp.setCandidateType(CandidateAttribute.TYP_HOST); candidateSdp.setGeneration(0); return candidateSdp; } private static CandidateAttribute processSrflxCandidate(MediaChannel candidate, short componentId) { CandidateAttribute candidateSdp = processHostCandidate(candidate, componentId); candidateSdp.setCandidateType(CandidateAttribute.TYP_SRFLX); candidateSdp.setAddress(candidate.getExternalAddress()); switch (componentId) { case IceComponent.RTP_ID: candidateSdp.setRelatedAddress(candidate.getRtpAddress()); candidateSdp.setRelatedPort(candidate.getRtpPort()); break; case IceComponent.RTCP_ID: candidateSdp.setRelatedPort(candidate.getRtcpPort()); candidateSdp.setRelatedPort(candidate.getRtcpPort()); break; default: break; } return candidateSdp; } }