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. 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<>();
059
060    private final List<PayloadType> remoteAudioPts = new ArrayList<>();
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<>();
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 have 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;
212
213        List<PayloadType> 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 = description.getAudioPayloadTypesList();
243        bestCommonAudioPt = calculateBestCommonAudioPt(offeredPayloads);
244
245        synchronized (remoteAudioPts) {
246            remoteAudioPts.addAll(offeredPayloads);
247        }
248
249        // If there are suitable/matching payload types then accept this content.
250        if (bestCommonAudioPt != null) {
251            // Let the transport negotiators sort-out connectivity and content-accept instead.
252            // response = createAudioPayloadTypesOffer();
253            setNegotiatorState(JingleNegotiatorState.PENDING);
254        } else {
255            // Don't really know what to send here.  XEP-166 is not clear.
256            setNegotiatorState(JingleNegotiatorState.FAILED);
257        }
258
259        return response;
260    }
261
262    /**
263     * A content info has been received. This is done for publishing the
264     * list of payload types...
265     * 
266     * @param jingle
267     *            The input packet
268     * @return a Jingle packet
269     * @throws JingleException
270     */
271    private IQ receiveSessionInfoAction(Jingle jingle, JingleDescription description) throws JingleException {
272        IQ response = null;
273        PayloadType oldBestCommonAudioPt = bestCommonAudioPt;
274        List<PayloadType> offeredPayloads;
275        boolean ptChange = false;
276
277        offeredPayloads = description.getAudioPayloadTypesList();
278        if (!offeredPayloads.isEmpty()) {
279
280            synchronized (remoteAudioPts) {
281                remoteAudioPts.clear();
282                remoteAudioPts.addAll(offeredPayloads);
283            }
284
285            // Calculate the best common codec
286            bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts);
287            if (bestCommonAudioPt != null) {
288                // and send an accept if we have an agreement...
289                ptChange = !bestCommonAudioPt.equals(oldBestCommonAudioPt);
290                if (oldBestCommonAudioPt == null || ptChange) {
291                    // response = createAcceptMessage();
292                }
293            } else {
294                throw new JingleException(JingleError.NO_COMMON_PAYLOAD);
295            }
296        }
297
298        // Parse the Jingle and get the payload accepted
299        return response;
300    }
301
302    /**
303     * A jmf description has been accepted. In this case, we must save the
304     * accepted payload type and notify any listener...
305     * 
306     * @param jin
307     *            The input packet
308     * @return a Jingle packet
309     * @throws JingleException
310     */
311    private IQ receiveSessionAcceptAction(Jingle jingle, JingleDescription description) throws JingleException {
312        IQ response = null;
313        PayloadType.Audio agreedCommonAudioPt;
314
315        if (bestCommonAudioPt == null) {
316            // Update the best common audio PT
317            bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts);
318            // response = createAcceptMessage();
319        }
320
321        List<PayloadType> 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<>();
363        final ArrayList<PayloadType> commonAudioPtsThere = new ArrayList<>();
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     * @throws InterruptedException 
481     */
482    protected void triggerMediaEstablished(PayloadType bestPt) throws NotConnectedException, InterruptedException {
483        List<JingleListener> listeners = getListenersList();
484        for (JingleListener li : listeners) {
485            if (li instanceof JingleMediaListener) {
486                JingleMediaListener mli = (JingleMediaListener) li;
487                mli.mediaEstablished(bestPt);
488            }
489        }
490    }
491
492    /**
493     * Trigger a jmf closed event.
494     * 
495     * @param currPt
496     *            current payload type that is cancelled.
497     */
498    protected void triggerMediaClosed(PayloadType currPt) {
499        List<JingleListener> listeners = getListenersList();
500        for (JingleListener li : listeners) {
501            if (li instanceof JingleMediaListener) {
502                JingleMediaListener mli = (JingleMediaListener) li;
503                mli.mediaClosed(currPt);
504            }
505        }
506    }
507
508    /**
509     *  Called from above when starting a new session.
510     */
511    @Override
512    protected void doStart() {
513
514    }
515
516    /**
517     * Terminate the jmf negotiator.
518     */
519    @Override
520    public void close() {
521        super.close();
522        triggerMediaClosed(getBestCommonAudioPt());
523    }
524
525    /**
526     *  Create a JingleDescription that matches this negotiator.
527     */
528    public JingleDescription getJingleDescription() {
529        JingleDescription result = null;
530        PayloadType payloadType = getBestCommonAudioPt();
531        if (payloadType != null) {
532            result = new JingleDescription.Audio(payloadType);
533        } else {
534            // If we haven't settled on a best payload type yet then just use the first one in our local list.
535            result = new JingleDescription.Audio();
536            result.addAudioPayloadTypes(localAudioPts);
537        }
538        return result;
539    }
540}