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