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}