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