Smack/jingle/extension/source/org/jivesoftware/smackx/jingle/media/MediaNegotiator.java

602 lines
19 KiB
Java
Raw Normal View History

/**
* $RCSfile$
* $Revision: 7329 $
* $Date: 2007-02-28 20:59:28 -0300 (qua, 28 fev 2007) $
*
* Copyright 2003-2005 Jive Software.
*
* All rights reserved. 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.jivesoftware.smackx.jingle.media;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smackx.jingle.JingleNegotiator;
import org.jivesoftware.smackx.jingle.JingleSession;
import org.jivesoftware.smackx.jingle.listeners.JingleListener;
import org.jivesoftware.smackx.jingle.listeners.JingleMediaListener;
import org.jivesoftware.smackx.packet.Jingle;
import org.jivesoftware.smackx.packet.JingleContentDescription;
import org.jivesoftware.smackx.packet.JingleContentDescription.JinglePayloadType;
import org.jivesoftware.smackx.packet.JingleError;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Manager for jmf descriptor negotiation.
* <p/>
* <p/>
* This class is responsible for managing the descriptor negotiation process,
* handling all the xmpp packets interchange and the stage control.
* handling all the xmpp packets interchange and the stage control.
*
* @author Thiago Camargo
*/
public class MediaNegotiator extends JingleNegotiator {
private final JingleSession session; // The session this negotiation
// Local and remote payload types...
private final List<PayloadType> localAudioPts = new ArrayList<PayloadType>();
private final List<PayloadType> remoteAudioPts = new ArrayList<PayloadType>();
private PayloadType.Audio bestCommonAudioPt;
// states
private final Inviting inviting;
private final Accepting accepting;
private final Pending pending;
private final Active active;
/**
* Default constructor. The constructor establishes some basic parameters,
* but it does not start the negotiation. For starting the negotiation, call
* startNegotiation.
*
* @param js The jingle session.
*/
public MediaNegotiator(JingleSession js, List<PayloadType> pts) {
super(js.getConnection());
session = js;
bestCommonAudioPt = null;
if (pts != null) {
if (pts.size() > 0) {
localAudioPts.addAll(pts);
}
}
// Create the states...
inviting = new Inviting(this);
accepting = new Accepting(this);
pending = new Pending(this);
active = new Active(this);
}
/**
* Dispatch an incomming packet. The medthod is responsible for recognizing
* the packet type and, depending on the current state, deliverying the
* packet to the right event handler and wait for a response.
*
* @param iq the packet received
* @return the new Jingle packet to send.
* @throws XMPPException
*/
public IQ dispatchIncomingPacket(IQ iq, String id) throws XMPPException {
IQ jout = null;
if (invalidState()) {
if (iq == null) {
// With a null packet, we are just inviting the other end...
setState(inviting);
jout = getState().eventInvite();
}
else {
if (iq instanceof Jingle) {
// If there is no specific jmf action associated, then we
// are being invited to a new session...
setState(accepting);
jout = getState().eventInitiate((Jingle) iq);
}
else {
throw new IllegalStateException(
"Invitation IQ received is not a Jingle packet in Media negotiator.");
}
}
}
else {
if (iq == null) {
return null;
}
else {
if (iq.getType().equals(IQ.Type.ERROR)) {
// Process errors
getState().eventError(iq);
}
else if (iq.getType().equals(IQ.Type.RESULT)) {
// Process ACKs
if (isExpectedId(iq.getPacketID())) {
jout = getState().eventAck(iq);
removeExpectedId(iq.getPacketID());
}
}
else if (iq instanceof Jingle) {
// Get the action from the Jingle packet
Jingle jin = (Jingle) iq;
Jingle.Action action = jin.getAction();
if (action != null) {
if (action.equals(Jingle.Action.CONTENTACCEPT)) {
jout = getState().eventAccept(jin);
}
else if (action.equals(Jingle.Action.CONTENTDECLINE)) {
jout = getState().eventDecline(jin);
}
else if (action.equals(Jingle.Action.DESCRIPTIONINFO)) {
jout = getState().eventInfo(jin);
}
else if (action.equals(Jingle.Action.CONTENTMODIFY)) {
jout = getState().eventModify(jin);
}
// Any unknown action will be ignored: it is not a msg
// to us...
}
}
}
}
// Save the Id for any ACK
if (id != null) {
addExpectedId(id);
}
else {
if (jout != null) {
addExpectedId(jout.getPacketID());
}
}
return jout;
}
/**
* Return true if the content is negotiated.
*
* @return true if the content is negotiated.
*/
public boolean isEstablished() {
return getBestCommonAudioPt() != null;
}
/**
* Return true if the content is fully negotiated.
*
* @return true if the content is fully negotiated.
*/
public boolean isFullyEstablished() {
return isEstablished() && getState() == active;
}
// Payload types
private PayloadType.Audio calculateBestCommonAudioPt(List remoteAudioPts) {
final ArrayList<PayloadType> commonAudioPtsHere = new ArrayList<PayloadType>();
final ArrayList<PayloadType> commonAudioPtsThere = new ArrayList<PayloadType>();
PayloadType.Audio result = null;
if (!remoteAudioPts.isEmpty()) {
commonAudioPtsHere.addAll(localAudioPts);
commonAudioPtsHere.retainAll(remoteAudioPts);
commonAudioPtsThere.addAll(remoteAudioPts);
commonAudioPtsThere.retainAll(localAudioPts);
if (!commonAudioPtsHere.isEmpty() && !commonAudioPtsThere.isEmpty()) {
if (session.getInitiator().equals(session.getConnection().getUser())) {
PayloadType.Audio bestPtHere = null;
PayloadType payload = this.session.getMediaManager().getPreferredPayloadType();
if (payload != null && payload instanceof PayloadType.Audio)
if (commonAudioPtsHere.contains(payload))
bestPtHere = (PayloadType.Audio) payload;
if (bestPtHere == null)
for (PayloadType payloadType : commonAudioPtsHere)
if (payloadType instanceof PayloadType.Audio) {
bestPtHere = (PayloadType.Audio) payloadType;
break;
}
result = bestPtHere;
}
else {
PayloadType.Audio bestPtThere = null;
for (PayloadType payloadType : commonAudioPtsThere)
if (payloadType instanceof PayloadType.Audio) {
bestPtThere = (PayloadType.Audio) payloadType;
break;
}
result = bestPtThere;
}
}
}
return result;
}
private List obtainPayloads(Jingle jin) {
List result = new ArrayList();
Iterator iDescr = jin.getDescriptions();
// Add the list of payloads: iterate over the descriptions...
while (iDescr.hasNext()) {
JingleContentDescription.Audio descr = (JingleContentDescription.Audio) iDescr
.next();
if (descr != null) {
// ...and, then, over the payloads.
// Note: we use the last "description" in the packet...
result.clear();
result.addAll(descr.getAudioPayloadTypesList());
}
}
return result;
}
/**
* Adds a payload type to the list of remote payloads.
*
* @param pt the remote payload type
*/
public void addRemoteAudioPayloadType(PayloadType.Audio pt) {
if (pt != null) {
synchronized (remoteAudioPts) {
remoteAudioPts.add(pt);
}
}
}
/**
* Create an offer for the list of audio payload types.
*
* @return a new Jingle packet with the list of audio Payload Types
*/
private Jingle getAudioPayloadTypesOffer() {
JingleContentDescription.Audio audioDescr = new JingleContentDescription.Audio();
// Add the list of payloads for audio and create a
// JingleContentDescription
// where we announce our payloads...
audioDescr.addAudioPayloadTypes(localAudioPts);
return new Jingle(audioDescr);
}
// Predefined messages and Errors
/**
* Create an IQ "accept" message.
*/
private Jingle createAcceptMessage() {
Jingle jout = null;
// If we hava a common best codec, send an accept right now...
jout = new Jingle(Jingle.Action.CONTENTACCEPT);
jout.addDescription(new JingleContentDescription.Audio(
new JinglePayloadType.Audio(bestCommonAudioPt)));
return jout;
}
// Payloads
/**
* Get the best common codec between both parts.
*
* @return The best common PayloadType codec.
*/
public PayloadType.Audio getBestCommonAudioPt() {
return bestCommonAudioPt;
}
// Events
/**
* Trigger a session established event.
*
* @param bestPt payload type that has been agreed.
*/
protected void triggerMediaEstablished(PayloadType bestPt) {
ArrayList listeners = getListenersList();
Iterator iter = listeners.iterator();
while (iter.hasNext()) {
JingleListener li = (JingleListener) iter.next();
if (li instanceof JingleMediaListener) {
JingleMediaListener mli = (JingleMediaListener) li;
mli.mediaEstablished(bestPt);
}
}
}
/**
* Trigger a jmf closed event.
*
* @param currPt current payload type that is cancelled.
*/
protected void triggerMediaClosed(PayloadType currPt) {
ArrayList listeners = getListenersList();
Iterator iter = listeners.iterator();
while (iter.hasNext()) {
JingleListener li = (JingleListener) iter.next();
if (li instanceof JingleMediaListener) {
JingleMediaListener mli = (JingleMediaListener) li;
mli.mediaClosed(currPt);
}
}
}
/**
* Terminate the jmf negotiator
*/
public void close() {
super.close();
}
// States
/**
* First stage when we send a session request.
*/
public class Inviting extends JingleNegotiator.State {
public Inviting(MediaNegotiator neg) {
super(neg);
}
/**
* Create an initial Jingle packet, with the list of payload types that
* we support. The list is in order of preference.
*/
public Jingle eventInvite() {
return getAudioPayloadTypesOffer();
}
/**
* We have received the ACK for our invitation.
*
* @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ)
*/
public Jingle eventAck(IQ iq) {
setState(pending);
return null;
}
}
/**
* We are accepting connections.
*/
public class Accepting extends JingleNegotiator.State {
public Accepting(MediaNegotiator neg) {
super(neg);
}
/**
* We have received an invitation! Respond with a list of our payload
* types...
*/
public Jingle eventInitiate(Jingle jin) {
synchronized (remoteAudioPts) {
remoteAudioPts.addAll(obtainPayloads(jin));
}
return getAudioPayloadTypesOffer();
}
/**
* Process the ACK of our list of codecs (our offer).
*
* @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ)
*/
public Jingle eventAck(IQ iq) throws XMPPException {
Jingle response = null;
if (!remoteAudioPts.isEmpty()) {
// Calculate the best common codec
bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts);
// and send an accept if we havee an agreement...
if (bestCommonAudioPt != null) {
response = createAcceptMessage();
}
else {
throw new JingleException(JingleError.NO_COMMON_PAYLOAD);
}
setState(pending);
}
return response;
}
}
/**
* Pending class: we are waiting for the other enpoint, that must say if it
* accepts or not...
*/
public class Pending extends JingleNegotiator.State {
public Pending(MediaNegotiator neg) {
super(neg);
}
/**
* A content info has been received. This is done for publishing the
* list of payload types...
*
* @param jin The input packet
* @return a Jingle packet
* @throws JingleException
*/
public Jingle eventInfo(Jingle jin) throws JingleException {
PayloadType.Audio oldBestCommonAudioPt = bestCommonAudioPt;
List offeredPayloads;
Jingle response = null;
boolean ptChange = false;
offeredPayloads = obtainPayloads(jin);
if (!offeredPayloads.isEmpty()) {
synchronized (remoteAudioPts) {
remoteAudioPts.clear();
remoteAudioPts.addAll(offeredPayloads);
}
// Calculate the best common codec
bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts);
if (bestCommonAudioPt != null) {
// and send an accept if we have an agreement...
ptChange = !bestCommonAudioPt.equals(oldBestCommonAudioPt);
if (oldBestCommonAudioPt == null || ptChange) {
response = createAcceptMessage();
}
}
else {
throw new JingleException(JingleError.NO_COMMON_PAYLOAD);
}
}
// Parse the Jingle and get the payload accepted
return response;
}
/**
* A jmf description has been accepted. In this case, we must save the
* accepted payload type and notify any listener...
*
* @param jin The input packet
* @return a Jingle packet
* @throws JingleException
*/
public Jingle eventAccept(Jingle jin) throws JingleException {
PayloadType.Audio agreedCommonAudioPt;
List offeredPayloads = new ArrayList();
Jingle response = null;
if (bestCommonAudioPt == null) {
// Update the best common audio PT
bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts);
response = createAcceptMessage();
}
offeredPayloads = obtainPayloads(jin);
if (!offeredPayloads.isEmpty()) {
if (offeredPayloads.size() == 1) {
agreedCommonAudioPt = (PayloadType.Audio) offeredPayloads.get(0);
if (bestCommonAudioPt != null) {
// If the accepted PT matches the best payload
// everything is fine
if (!agreedCommonAudioPt.equals(bestCommonAudioPt)) {
throw new JingleException(JingleError.NEGOTIATION_ERROR);
}
}
}
else if (offeredPayloads.size() > 1) {
throw new JingleException(JingleError.MALFORMED_STANZA);
}
}
return response;
}
/**
* The other part has declined the our codec...
*
* @throws JingleException
*/
public Jingle eventDecline(Jingle inJingle) throws JingleException {
triggerMediaClosed(getBestCommonAudioPt());
throw new JingleException();
}
/*
* (non-Javadoc)
*
* @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventError(org.jivesoftware.smack.packet.IQ)
*/
public void eventError(IQ iq) throws XMPPException {
triggerMediaClosed(getBestCommonAudioPt());
super.eventError(iq);
}
/**
* ACK received.
*
* @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventAck(org.jivesoftware.smack.packet.IQ)
*/
public Jingle eventAck(IQ iq) {
if (isEstablished()) {
setState(active);
return null;
}
return null;
}
}
/**
* "Active" state: we have an agreement about the codec...
*/
public class Active extends JingleNegotiator.State {
public Active(MediaNegotiator neg) {
super(neg);
}
/**
* We have an agreement.
*
* @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventEnter()
*/
public void eventEnter() {
triggerMediaEstablished(getBestCommonAudioPt());
System.err.println("BS:"+getBestCommonAudioPt().getName());
super.eventEnter();
}
/**
* We are breaking the contract...
*
* @see org.jivesoftware.smackx.jingle.JingleNegotiator.State#eventExit()
*/
public void eventExit() {
triggerMediaClosed(getBestCommonAudioPt());
super.eventExit();
}
}
}