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. <p/> <p/> 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<PayloadType>(); 059 060 private final List<PayloadType> remoteAudioPts = new ArrayList<PayloadType>(); 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<IQ>(); 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 havee 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 = null; 212 List<PayloadType> offeredPayloads = new ArrayList<PayloadType>(); 213 214 offeredPayloads = description.getAudioPayloadTypesList(); 215 bestCommonAudioPt = calculateBestCommonAudioPt(offeredPayloads); 216 217 if (bestCommonAudioPt == null) { 218 219 setNegotiatorState(JingleNegotiatorState.FAILED); 220 response = session.createJingleError(jingle, JingleError.NEGOTIATION_ERROR); 221 222 } else { 223 224 setNegotiatorState(JingleNegotiatorState.SUCCEEDED); 225 triggerMediaEstablished(getBestCommonAudioPt()); 226 LOGGER.severe("Media choice:" + getBestCommonAudioPt().getName()); 227 228 response = session.createAck(jingle); 229 } 230 231 return response; 232 } 233 234 /** 235 * Receive a session-initiate packet. 236 * @param jingle 237 * @param description 238 * @return the iq 239 */ 240 private IQ receiveSessionInitiateAction(Jingle jingle, JingleDescription description) { 241 IQ response = null; 242 243 List<PayloadType> offeredPayloads = new ArrayList<PayloadType>(); 244 245 offeredPayloads = description.getAudioPayloadTypesList(); 246 bestCommonAudioPt = calculateBestCommonAudioPt(offeredPayloads); 247 248 synchronized (remoteAudioPts) { 249 remoteAudioPts.addAll(offeredPayloads); 250 } 251 252 // If there are suitable/matching payload types then accept this content. 253 if (bestCommonAudioPt != null) { 254 // Let thre transport negotiators sort-out connectivity and content-accept instead. 255 //response = createAudioPayloadTypesOffer(); 256 setNegotiatorState(JingleNegotiatorState.PENDING); 257 } else { 258 // Don't really know what to send here. XEP-166 is not clear. 259 setNegotiatorState(JingleNegotiatorState.FAILED); 260 } 261 262 return response; 263 } 264 265 /** 266 * A content info has been received. This is done for publishing the 267 * list of payload types... 268 * 269 * @param jin 270 * The input packet 271 * @return a Jingle packet 272 * @throws JingleException 273 */ 274 private IQ receiveSessionInfoAction(Jingle jingle, JingleDescription description) throws JingleException { 275 IQ response = null; 276 PayloadType oldBestCommonAudioPt = bestCommonAudioPt; 277 List<PayloadType> offeredPayloads; 278 boolean ptChange = false; 279 280 offeredPayloads = description.getAudioPayloadTypesList(); 281 if (!offeredPayloads.isEmpty()) { 282 283 synchronized (remoteAudioPts) { 284 remoteAudioPts.clear(); 285 remoteAudioPts.addAll(offeredPayloads); 286 } 287 288 // Calculate the best common codec 289 bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts); 290 if (bestCommonAudioPt != null) { 291 // and send an accept if we have an agreement... 292 ptChange = !bestCommonAudioPt.equals(oldBestCommonAudioPt); 293 if (oldBestCommonAudioPt == null || ptChange) { 294 //response = createAcceptMessage(); 295 } 296 } else { 297 throw new JingleException(JingleError.NO_COMMON_PAYLOAD); 298 } 299 } 300 301 // Parse the Jingle and get the payload accepted 302 return response; 303 } 304 305 /** 306 * A jmf description has been accepted. In this case, we must save the 307 * accepted payload type and notify any listener... 308 * 309 * @param jin 310 * The input packet 311 * @return a Jingle packet 312 * @throws JingleException 313 */ 314 private IQ receiveSessionAcceptAction(Jingle jingle, JingleDescription description) throws JingleException { 315 IQ response = null; 316 PayloadType.Audio agreedCommonAudioPt; 317 List<PayloadType> offeredPayloads = new ArrayList<PayloadType>(); 318 319 if (bestCommonAudioPt == null) { 320 // Update the best common audio PT 321 bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts); 322 //response = createAcceptMessage(); 323 } 324 325 offeredPayloads = description.getAudioPayloadTypesList(); 326 if (!offeredPayloads.isEmpty()) { 327 if (offeredPayloads.size() == 1) { 328 agreedCommonAudioPt = (PayloadType.Audio) offeredPayloads.get(0); 329 if (bestCommonAudioPt != null) { 330 // If the accepted PT matches the best payload 331 // everything is fine 332 if (!agreedCommonAudioPt.equals(bestCommonAudioPt)) { 333 throw new JingleException(JingleError.NEGOTIATION_ERROR); 334 } 335 } 336 337 } else if (offeredPayloads.size() > 1) { 338 throw new JingleException(JingleError.MALFORMED_STANZA); 339 } 340 } 341 342 return response; 343 } 344 345 /** 346 * Return true if the content is negotiated. 347 * 348 * @return true if the content is negotiated. 349 */ 350 public boolean isEstablished() { 351 return getBestCommonAudioPt() != null; 352 } 353 354 /** 355 * Return true if the content is fully negotiated. 356 * 357 * @return true if the content is fully negotiated. 358 */ 359 public boolean isFullyEstablished() { 360 return (isEstablished() && ((getNegotiatorState() == JingleNegotiatorState.SUCCEEDED) || (getNegotiatorState() == JingleNegotiatorState.FAILED))); 361 } 362 363 // Payload types 364 365 private PayloadType calculateBestCommonAudioPt(List<PayloadType> remoteAudioPts) { 366 final ArrayList<PayloadType> commonAudioPtsHere = new ArrayList<PayloadType>(); 367 final ArrayList<PayloadType> commonAudioPtsThere = new ArrayList<PayloadType>(); 368 PayloadType result = null; 369 370 if (!remoteAudioPts.isEmpty()) { 371 commonAudioPtsHere.addAll(localAudioPts); 372 commonAudioPtsHere.retainAll(remoteAudioPts); 373 374 commonAudioPtsThere.addAll(remoteAudioPts); 375 commonAudioPtsThere.retainAll(localAudioPts); 376 377 if (!commonAudioPtsHere.isEmpty() && !commonAudioPtsThere.isEmpty()) { 378 379 if (session.getInitiator().equals(session.getConnection().getUser())) { 380 PayloadType.Audio bestPtHere = null; 381 382 PayloadType payload = mediaManager.getPreferredPayloadType(); 383 384 if (payload != null && payload instanceof PayloadType.Audio) 385 if (commonAudioPtsHere.contains(payload)) 386 bestPtHere = (PayloadType.Audio) payload; 387 388 if (bestPtHere == null) 389 for (PayloadType payloadType : commonAudioPtsHere) 390 if (payloadType instanceof PayloadType.Audio) { 391 bestPtHere = (PayloadType.Audio) payloadType; 392 break; 393 } 394 395 result = bestPtHere; 396 } else { 397 PayloadType.Audio bestPtThere = null; 398 for (PayloadType payloadType : commonAudioPtsThere) 399 if (payloadType instanceof PayloadType.Audio) { 400 bestPtThere = (PayloadType.Audio) payloadType; 401 break; 402 } 403 404 result = bestPtThere; 405 } 406 } 407 } 408 409 return result; 410 } 411 412 /** 413 * Adds a payload type to the list of remote payloads. 414 * 415 * @param pt 416 * the remote payload type 417 */ 418 public void addRemoteAudioPayloadType(PayloadType.Audio pt) { 419 if (pt != null) { 420 synchronized (remoteAudioPts) { 421 remoteAudioPts.add(pt); 422 } 423 } 424 } 425 426// /** 427// * Create an offer for the list of audio payload types. 428// * 429// * @return a new Jingle packet with the list of audio Payload Types 430// */ 431// private Jingle createAudioPayloadTypesOffer() { 432// 433// JingleContent jingleContent = new JingleContent(parentNegotiator.getCreator(), parentNegotiator.getName()); 434// JingleDescription audioDescr = new JingleDescription.Audio(); 435// 436// // Add the list of payloads for audio and create a 437// // JingleDescription 438// // where we announce our payloads... 439// audioDescr.addAudioPayloadTypes(localAudioPts); 440// jingleContent.setDescription(audioDescr); 441// 442// Jingle jingle = new Jingle(JingleActionEnum.CONTENT_ACCEPT); 443// jingle.addContent(jingleContent); 444// 445// return jingle; 446// } 447 448 // Predefined messages and Errors 449 450 /** 451 * Create an IQ "accept" message. 452 */ 453// private Jingle createAcceptMessage() { 454// Jingle jout = null; 455// 456// // If we have a common best codec, send an accept right now... 457// jout = new Jingle(JingleActionEnum.CONTENT_ACCEPT); 458// JingleContent content = new JingleContent(parentNegotiator.getCreator(), parentNegotiator.getName()); 459// content.setDescription(new JingleDescription.Audio(bestCommonAudioPt)); 460// jout.addContent(content); 461// 462// return jout; 463// } 464 465 // Payloads 466 467 /** 468 * Get the best common codec between both parts. 469 * 470 * @return The best common PayloadType codec. 471 */ 472 public PayloadType getBestCommonAudioPt() { 473 return bestCommonAudioPt; 474 } 475 476 // Events 477 478 /** 479 * Trigger a session established event. 480 * 481 * @param bestPt 482 * payload type that has been agreed. 483 * @throws NotConnectedException 484 * @throws InterruptedException 485 */ 486 protected void triggerMediaEstablished(PayloadType bestPt) throws NotConnectedException, InterruptedException { 487 List<JingleListener> listeners = getListenersList(); 488 for (JingleListener li : listeners) { 489 if (li instanceof JingleMediaListener) { 490 JingleMediaListener mli = (JingleMediaListener) li; 491 mli.mediaEstablished(bestPt); 492 } 493 } 494 } 495 496 /** 497 * Trigger a jmf closed event. 498 * 499 * @param currPt 500 * current payload type that is cancelled. 501 */ 502 protected void triggerMediaClosed(PayloadType currPt) { 503 List<JingleListener> listeners = getListenersList(); 504 for (JingleListener li : listeners) { 505 if (li instanceof JingleMediaListener) { 506 JingleMediaListener mli = (JingleMediaListener) li; 507 mli.mediaClosed(currPt); 508 } 509 } 510 } 511 512 /** 513 * Called from above when starting a new session. 514 */ 515 @Override 516 protected void doStart() { 517 518 } 519 520 /** 521 * Terminate the jmf negotiator. 522 */ 523 @Override 524 public void close() { 525 super.close(); 526 triggerMediaClosed(getBestCommonAudioPt()); 527 } 528 529 /** 530 * Create a JingleDescription that matches this negotiator. 531 */ 532 public JingleDescription getJingleDescription() { 533 JingleDescription result = null; 534 PayloadType payloadType = getBestCommonAudioPt(); 535 if (payloadType != null) { 536 result = new JingleDescription.Audio(payloadType); 537 } else { 538 // If we haven't settled on a best payload type yet then just use the first one in our local list. 539 result = new JingleDescription.Audio(); 540 result.addAudioPayloadTypes(localAudioPts); 541 } 542 return result; 543 } 544}