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}