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