001/**
002 *
003 * Copyright 2003-2005 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smackx.jingle.media;
018
019import java.util.ArrayList;
020import java.util.List;
021import java.util.logging.Logger;
022
023import org.jivesoftware.smack.SmackException.NotConnectedException;
024import org.jivesoftware.smack.XMPPException;
025import org.jivesoftware.smack.packet.IQ;
026import org.jivesoftware.smackx.jingle.ContentNegotiator;
027import org.jivesoftware.smackx.jingle.JingleActionEnum;
028import org.jivesoftware.smackx.jingle.JingleException;
029import org.jivesoftware.smackx.jingle.JingleNegotiator;
030import org.jivesoftware.smackx.jingle.JingleNegotiatorState;
031import org.jivesoftware.smackx.jingle.JingleSession;
032import org.jivesoftware.smackx.jingle.listeners.JingleListener;
033import org.jivesoftware.smackx.jingle.listeners.JingleMediaListener;
034import org.jivesoftware.smackx.jingle.packet.Jingle;
035import org.jivesoftware.smackx.jingle.packet.JingleContent;
036import org.jivesoftware.smackx.jingle.packet.JingleDescription;
037import org.jivesoftware.smackx.jingle.packet.JingleError;
038
039/**
040 * Manager for jmf descriptor negotiation. <p/> <p/> This class is responsible
041 * for managing the descriptor negotiation process, handling all the xmpp
042 * packets interchange and the stage control. handling all the xmpp packets
043 * interchange and the stage control.
044 * 
045 * @author Thiago Camargo
046 */
047public class MediaNegotiator extends JingleNegotiator {
048
049        private static final Logger LOGGER = Logger.getLogger(MediaNegotiator.class.getName());
050
051        //private JingleSession session; // The session this negotiation
052
053    private final JingleMediaManager mediaManager;
054
055    // Local and remote payload types...
056
057    private final List<PayloadType> localAudioPts = new ArrayList<PayloadType>();
058
059    private final List<PayloadType> remoteAudioPts = new ArrayList<PayloadType>();
060
061    private PayloadType bestCommonAudioPt;
062
063    private ContentNegotiator parentNegotiator;
064
065    /**
066     * Default constructor. The constructor establishes some basic parameters,
067     * but it does not start the negotiation. For starting the negotiation, call
068     * startNegotiation.
069     * 
070     * @param session
071     *            The jingle session.
072     */
073    public MediaNegotiator(JingleSession session, JingleMediaManager mediaManager, List<PayloadType> pts,
074            ContentNegotiator parentNegotiator) {
075        super(session);
076
077        this.mediaManager = mediaManager;
078        this.parentNegotiator = parentNegotiator;
079
080        bestCommonAudioPt = null;
081
082        if (pts != null) {
083            if (pts.size() > 0) {
084                localAudioPts.addAll(pts);
085            }
086        }
087    }
088
089    /**
090     * Return   The media manager for this negotiator.
091     */
092    public JingleMediaManager getMediaManager() {
093        return mediaManager;
094    }
095
096    /**
097     * Dispatch an incoming packet. The method is responsible for recognizing
098     * the packet type and, depending on the current state, delivering the
099     * packet to the right event handler and wait for a response.
100     * 
101     * @param iq
102     *            the packet received
103     * @return the new Jingle packet to send.
104     * @throws XMPPException
105     * @throws NotConnectedException 
106     */
107    public List<IQ> dispatchIncomingPacket(IQ iq, String id) throws XMPPException, NotConnectedException {
108        List<IQ> responses = new ArrayList<IQ>();
109        IQ response = null;
110
111        if (iq.getType().equals(IQ.Type.ERROR)) {
112            // Process errors
113            setNegotiatorState(JingleNegotiatorState.FAILED);
114            triggerMediaClosed(getBestCommonAudioPt());
115            // This next line seems wrong, and may subvert the normal closing process.
116            throw new JingleException(iq.getError().getMessage());
117        } else if (iq.getType().equals(IQ.Type.RESULT)) {
118            // Process ACKs
119            if (isExpectedId(iq.getPacketID())) {
120                receiveResult(iq);
121                removeExpectedId(iq.getPacketID());
122            }
123        } else if (iq instanceof Jingle) {
124            Jingle jingle = (Jingle) iq;
125            JingleActionEnum action = jingle.getAction();
126
127            // Only act on the JingleContent sections that belong to this media negotiator.
128            for (JingleContent jingleContent : jingle.getContentsList()) {
129                if (jingleContent.getName().equals(parentNegotiator.getName())) {
130
131                    JingleDescription description = jingleContent.getDescription();
132
133                    if (description != null) {
134
135                        switch (action) {
136                            case CONTENT_ACCEPT:
137                                response = receiveContentAcceptAction(jingle, description);
138                                break;
139
140                            case CONTENT_MODIFY:
141                                break;
142
143                            case CONTENT_REMOVE:
144                                break;
145
146                            case SESSION_INFO:
147                                response = receiveSessionInfoAction(jingle, description);
148                                break;
149
150                            case SESSION_INITIATE:
151                                response = receiveSessionInitiateAction(jingle, description);
152                                break;
153
154                            case SESSION_ACCEPT:
155                                response = receiveSessionAcceptAction(jingle, description);
156                                break;
157
158                            default:
159                                break;
160                        }
161                    }
162                }
163            }
164
165        }
166
167        if (response != null) {
168            addExpectedId(response.getPacketID());
169            responses.add(response);
170        }
171
172        return responses;
173    }
174
175    /**
176     * Process the ACK of our list of codecs (our offer).
177     */
178    private Jingle receiveResult(IQ iq) throws XMPPException {
179        Jingle response = null;
180
181//        if (!remoteAudioPts.isEmpty()) {
182//            // Calculate the best common codec
183//            bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts);
184//
185//            // and send an accept if we havee an agreement...
186//            if (bestCommonAudioPt != null) {
187//                response = createAcceptMessage();
188//            } else {
189//                throw new JingleException(JingleError.NO_COMMON_PAYLOAD);
190//            }
191//        }
192        return response;
193    }
194
195    /**
196      *  The other side has sent us a content-accept.  The payload types in that message may not match with what
197      *  we sent, but XEP-167 says that the other side should retain the order of the payload types we first sent.
198      *  
199      *  This means we can walk through our list, in order, until we find one from their list that matches.  This
200      *  will be the best payload type to use.
201      *  
202      *  @param jingle
203      *  @return the iq
204     * @throws NotConnectedException 
205      */
206    private IQ receiveContentAcceptAction(Jingle jingle, JingleDescription description) throws XMPPException, NotConnectedException {
207        IQ response = null;
208        List<PayloadType> offeredPayloads = new ArrayList<PayloadType>();
209
210        offeredPayloads = description.getAudioPayloadTypesList();
211        bestCommonAudioPt = calculateBestCommonAudioPt(offeredPayloads);
212
213        if (bestCommonAudioPt == null) {
214            
215            setNegotiatorState(JingleNegotiatorState.FAILED);
216            response = session.createJingleError(jingle, JingleError.NEGOTIATION_ERROR);
217            
218        } else {
219
220            setNegotiatorState(JingleNegotiatorState.SUCCEEDED);
221            triggerMediaEstablished(getBestCommonAudioPt());
222            LOGGER.severe("Media choice:" + getBestCommonAudioPt().getName());
223
224            response = session.createAck(jingle);
225        }
226
227        return response;
228    }
229
230    /**
231     *  Receive a session-initiate packet.
232     *  @param jingle
233     *  @param description
234     *  @return the iq
235     */
236    private IQ receiveSessionInitiateAction(Jingle jingle, JingleDescription description) {
237        IQ response = null;
238
239        List<PayloadType> offeredPayloads = new ArrayList<PayloadType>();
240
241        offeredPayloads = description.getAudioPayloadTypesList();
242        bestCommonAudioPt = calculateBestCommonAudioPt(offeredPayloads);
243        
244        synchronized (remoteAudioPts) {
245            remoteAudioPts.addAll(offeredPayloads);
246        }
247
248        // If there are suitable/matching payload types then accept this content.
249        if (bestCommonAudioPt != null) {
250            // Let thre transport negotiators sort-out connectivity and content-accept instead.
251            //response = createAudioPayloadTypesOffer();
252            setNegotiatorState(JingleNegotiatorState.PENDING);
253        } else {
254            // Don't really know what to send here.  XEP-166 is not clear.
255            setNegotiatorState(JingleNegotiatorState.FAILED);
256        }
257
258        return response;
259    }
260
261    /**
262     * A content info has been received. This is done for publishing the
263     * list of payload types...
264     * 
265     * @param jin
266     *            The input packet
267     * @return a Jingle packet
268     * @throws JingleException
269     */
270    private IQ receiveSessionInfoAction(Jingle jingle, JingleDescription description) throws JingleException {
271        IQ response = null;
272        PayloadType oldBestCommonAudioPt = bestCommonAudioPt;
273        List<PayloadType> offeredPayloads;
274        boolean ptChange = false;
275
276        offeredPayloads = description.getAudioPayloadTypesList();
277        if (!offeredPayloads.isEmpty()) {
278
279            synchronized (remoteAudioPts) {
280                remoteAudioPts.clear();
281                remoteAudioPts.addAll(offeredPayloads);
282            }
283
284            // Calculate the best common codec
285            bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts);
286            if (bestCommonAudioPt != null) {
287                // and send an accept if we have an agreement...
288                ptChange = !bestCommonAudioPt.equals(oldBestCommonAudioPt);
289                if (oldBestCommonAudioPt == null || ptChange) {
290                    //response = createAcceptMessage();
291                }
292            } else {
293                throw new JingleException(JingleError.NO_COMMON_PAYLOAD);
294            }
295        }
296
297        // Parse the Jingle and get the payload accepted
298        return response;
299    }
300
301    /**
302     * A jmf description has been accepted. In this case, we must save the
303     * accepted payload type and notify any listener...
304     * 
305     * @param jin
306     *            The input packet
307     * @return a Jingle packet
308     * @throws JingleException
309     */
310    private IQ receiveSessionAcceptAction(Jingle jingle, JingleDescription description) throws JingleException {
311        IQ response = null;
312        PayloadType.Audio agreedCommonAudioPt;
313        List<PayloadType> offeredPayloads = new ArrayList<PayloadType>();
314
315        if (bestCommonAudioPt == null) {
316            // Update the best common audio PT
317            bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts);
318            //response = createAcceptMessage();
319        }
320
321        offeredPayloads = description.getAudioPayloadTypesList();
322        if (!offeredPayloads.isEmpty()) {
323            if (offeredPayloads.size() == 1) {
324                agreedCommonAudioPt = (PayloadType.Audio) offeredPayloads.get(0);
325                if (bestCommonAudioPt != null) {
326                    // If the accepted PT matches the best payload
327                    // everything is fine
328                    if (!agreedCommonAudioPt.equals(bestCommonAudioPt)) {
329                        throw new JingleException(JingleError.NEGOTIATION_ERROR);
330                    }
331                }
332
333            } else if (offeredPayloads.size() > 1) {
334                throw new JingleException(JingleError.MALFORMED_STANZA);
335            }
336        }
337
338        return response;
339    }
340
341    /**
342    * Return true if the content is negotiated.
343    * 
344    * @return true if the content is negotiated.
345    */
346    public boolean isEstablished() {
347        return getBestCommonAudioPt() != null;
348    }
349
350    /**
351     * Return true if the content is fully negotiated.
352     * 
353     * @return true if the content is fully negotiated.
354     */
355    public boolean isFullyEstablished() {
356        return (isEstablished() && ((getNegotiatorState() == JingleNegotiatorState.SUCCEEDED) || (getNegotiatorState() == JingleNegotiatorState.FAILED)));
357    }
358
359    // Payload types
360
361    private PayloadType calculateBestCommonAudioPt(List<PayloadType> remoteAudioPts) {
362        final ArrayList<PayloadType> commonAudioPtsHere = new ArrayList<PayloadType>();
363        final ArrayList<PayloadType> commonAudioPtsThere = new ArrayList<PayloadType>();
364        PayloadType result = null;
365
366        if (!remoteAudioPts.isEmpty()) {
367            commonAudioPtsHere.addAll(localAudioPts);
368            commonAudioPtsHere.retainAll(remoteAudioPts);
369
370            commonAudioPtsThere.addAll(remoteAudioPts);
371            commonAudioPtsThere.retainAll(localAudioPts);
372
373            if (!commonAudioPtsHere.isEmpty() && !commonAudioPtsThere.isEmpty()) {
374
375                if (session.getInitiator().equals(session.getConnection().getUser())) {
376                    PayloadType.Audio bestPtHere = null;
377
378                    PayloadType payload = mediaManager.getPreferredPayloadType();
379
380                    if (payload != null && payload instanceof PayloadType.Audio)
381                        if (commonAudioPtsHere.contains(payload))
382                            bestPtHere = (PayloadType.Audio) payload;
383
384                    if (bestPtHere == null)
385                        for (PayloadType payloadType : commonAudioPtsHere)
386                            if (payloadType instanceof PayloadType.Audio) {
387                                bestPtHere = (PayloadType.Audio) payloadType;
388                                break;
389                            }
390
391                    result = bestPtHere;
392                } else {
393                    PayloadType.Audio bestPtThere = null;
394                    for (PayloadType payloadType : commonAudioPtsThere)
395                        if (payloadType instanceof PayloadType.Audio) {
396                            bestPtThere = (PayloadType.Audio) payloadType;
397                            break;
398                        }
399
400                    result = bestPtThere;
401                }
402            }
403        }
404
405        return result;
406    }
407
408    /**
409     * Adds a payload type to the list of remote payloads.
410     * 
411     * @param pt
412     *            the remote payload type
413     */
414    public void addRemoteAudioPayloadType(PayloadType.Audio pt) {
415        if (pt != null) {
416            synchronized (remoteAudioPts) {
417                remoteAudioPts.add(pt);
418            }
419        }
420    }
421
422//    /**
423//    * Create an offer for the list of audio payload types.
424//    * 
425//    * @return a new Jingle packet with the list of audio Payload Types
426//    */
427//    private Jingle createAudioPayloadTypesOffer() {
428//
429//        JingleContent jingleContent = new JingleContent(parentNegotiator.getCreator(), parentNegotiator.getName());
430//        JingleDescription audioDescr = new JingleDescription.Audio();
431//
432//        // Add the list of payloads for audio and create a
433//        // JingleDescription
434//        // where we announce our payloads...
435//        audioDescr.addAudioPayloadTypes(localAudioPts);
436//        jingleContent.setDescription(audioDescr);
437//
438//        Jingle jingle = new Jingle(JingleActionEnum.CONTENT_ACCEPT);
439//        jingle.addContent(jingleContent);
440//
441//        return jingle;
442//    }
443
444    // Predefined messages and Errors
445
446    /**
447     * Create an IQ "accept" message.
448     */
449//    private Jingle createAcceptMessage() {
450//        Jingle jout = null;
451//
452//        // If we have a common best codec, send an accept right now...
453//        jout = new Jingle(JingleActionEnum.CONTENT_ACCEPT);
454//        JingleContent content = new JingleContent(parentNegotiator.getCreator(), parentNegotiator.getName());
455//        content.setDescription(new JingleDescription.Audio(bestCommonAudioPt));
456//        jout.addContent(content);
457//
458//        return jout;
459//    }
460
461    // Payloads
462
463    /**
464     * Get the best common codec between both parts.
465     * 
466     * @return The best common PayloadType codec.
467     */
468    public PayloadType getBestCommonAudioPt() {
469        return bestCommonAudioPt;
470    }
471
472    // Events
473
474    /**
475     * Trigger a session established event.
476     * 
477     * @param bestPt
478     *            payload type that has been agreed.
479     * @throws NotConnectedException 
480     */
481    protected void triggerMediaEstablished(PayloadType bestPt) throws NotConnectedException {
482        List<JingleListener> listeners = getListenersList();
483        for (JingleListener li : listeners) {
484            if (li instanceof JingleMediaListener) {
485                JingleMediaListener mli = (JingleMediaListener) li;
486                mli.mediaEstablished(bestPt);
487            }
488        }
489    }
490
491    /**
492     * Trigger a jmf closed event.
493     * 
494     * @param currPt
495     *            current payload type that is cancelled.
496     */
497    protected void triggerMediaClosed(PayloadType currPt) {
498        List<JingleListener> listeners = getListenersList();
499        for (JingleListener li : listeners) {
500            if (li instanceof JingleMediaListener) {
501                JingleMediaListener mli = (JingleMediaListener) li;
502                mli.mediaClosed(currPt);
503            }
504        }
505    }
506
507    /**
508     *  Called from above when starting a new session.
509     */
510    protected void doStart() {
511
512    }
513
514    /**
515     * Terminate the jmf negotiator
516     */
517    public void close() {
518        super.close();
519        triggerMediaClosed(getBestCommonAudioPt());
520    }
521
522    /**
523     *  Create a JingleDescription that matches this negotiator.
524     */
525    public JingleDescription getJingleDescription() {
526        JingleDescription result = null;
527        PayloadType payloadType = getBestCommonAudioPt();
528        if (payloadType != null) {
529            result = new JingleDescription.Audio(payloadType);
530        } else {
531            // If we haven't settled on a best payload type yet then just use the first one in our local list.
532            result = new JingleDescription.Audio();
533            result.addAudioPayloadTypes(localAudioPts);
534        }
535        return result;
536    }
537}